mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-20 20:49:20 +00:00
Compare commits
125 Commits
dbkr/dev_v
...
dbkr/fixpt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38bb2895db | ||
|
|
00f14c9814 | ||
|
|
0a7723d218 | ||
|
|
3e72604391 | ||
|
|
973d396249 | ||
|
|
3331a23373 | ||
|
|
1bdb0db6d2 | ||
|
|
e124b77f7f | ||
|
|
b96b77472d | ||
|
|
0bf96472eb | ||
|
|
8e090af370 | ||
|
|
ba23c1e269 | ||
|
|
f6a296bc0c | ||
|
|
1e892b452e | ||
|
|
a3edf08776 | ||
|
|
27b5ede72e | ||
|
|
29af360df9 | ||
|
|
dfe8f4ab2f | ||
|
|
b1ae9523aa | ||
|
|
3e64553815 | ||
|
|
b1b30f3d97 | ||
|
|
900abc7fa2 | ||
|
|
186fe91529 | ||
|
|
2fc4681f0b | ||
|
|
784f2ee99c | ||
|
|
4c0feaa30d | ||
|
|
58a335e8a2 | ||
|
|
771e20bdc4 | ||
|
|
7549e24479 | ||
|
|
550d7acaa8 | ||
|
|
1789571921 | ||
|
|
405904ded1 | ||
|
|
0e0f6f7fc5 | ||
|
|
7f5e02852c | ||
|
|
65564f8662 | ||
|
|
d55af2b9a5 | ||
|
|
b04e53a9bd | ||
|
|
2d5ea82ecf | ||
|
|
8c09a4361a | ||
|
|
24b652f696 | ||
|
|
6d24473fb0 | ||
|
|
5d24b55d30 | ||
|
|
a73597b30b | ||
|
|
8cf936eb5d | ||
|
|
e490b39718 | ||
|
|
3d9d19b721 | ||
|
|
41b28b80fc | ||
|
|
bc258c05d7 | ||
|
|
1305779e8e | ||
|
|
a35f1cb23f | ||
|
|
3494f0fb4d | ||
|
|
18b87bcd55 | ||
|
|
eef286e71e | ||
|
|
1a559b2e0d | ||
|
|
35a69a9c51 | ||
|
|
e389798baf | ||
|
|
e9fab62e21 | ||
|
|
8a02c5fbb9 | ||
|
|
96857476f9 | ||
|
|
8b596fe8ab | ||
|
|
c7c6e2c007 | ||
|
|
1a135f9466 | ||
|
|
39f76b65d4 | ||
|
|
dedd0b07c6 | ||
|
|
63ccd56573 | ||
|
|
3958158cf8 | ||
|
|
2e866770f6 | ||
|
|
6f4c549961 | ||
|
|
6f4834e168 | ||
|
|
ea3c346d39 | ||
|
|
6004e2d303 | ||
|
|
8a94220d74 | ||
|
|
6c999e652b | ||
|
|
3a0c895b7a | ||
|
|
3062e015c9 | ||
|
|
922af1ba3e | ||
|
|
11e78cf06b | ||
|
|
fcf0ec9b90 | ||
|
|
641fdbcc10 | ||
|
|
3aace3e4f6 | ||
|
|
841a31a642 | ||
|
|
1b2287646d | ||
|
|
8ea086a971 | ||
|
|
91b3cc2abb | ||
|
|
d71264a1f2 | ||
|
|
73b505987d | ||
|
|
aa628cc63c | ||
|
|
1dc05bed7d | ||
|
|
1ef64f3a4d | ||
|
|
0315230e3a | ||
|
|
e635740760 | ||
|
|
9d142c1ea5 | ||
|
|
a73516380a | ||
|
|
d0fe055af6 | ||
|
|
3ea908c59f | ||
|
|
74c70627dc | ||
|
|
b4b33563be | ||
|
|
11b5f537de | ||
|
|
5cd8c0538e | ||
|
|
54157611eb | ||
|
|
22690c4a0b | ||
|
|
edfae0138c | ||
|
|
e246c053c1 | ||
|
|
c86ef40162 | ||
|
|
b2f5f90f37 | ||
|
|
506b363f38 | ||
|
|
4d394decce | ||
|
|
96f264db87 | ||
|
|
e51492b3c7 | ||
|
|
5c3d8efebf | ||
|
|
8facf7808b | ||
|
|
923043a58c | ||
|
|
6a3c02f7f7 | ||
|
|
819734e2dd | ||
|
|
466d7ee394 | ||
|
|
935d2188f0 | ||
|
|
c4e5e1afb1 | ||
|
|
80f97cba32 | ||
|
|
7ace81a7cc | ||
|
|
89ae0e1e62 | ||
|
|
fd8f50957b | ||
|
|
9cd20ed1da | ||
|
|
8fdc1e3684 | ||
|
|
59653fea07 | ||
|
|
1a5898eb30 |
23
.env.example
23
.env.example
@@ -10,19 +10,10 @@ LIVEKIT_SECRET="secret"
|
||||
# Used for determining the homeserver to use for short urls etc.
|
||||
# VITE_FALLBACK_STUN_ALLOWED=false
|
||||
|
||||
# VITE_CUSTOM_THEME=true
|
||||
# VITE_THEME_ACCENT=#0dbd8b
|
||||
# VITE_THEME_ACCENT_20=#0dbd8b33
|
||||
# VITE_THEME_ALERT=#ff5b55
|
||||
# VITE_THEME_ALERT_20=#ff5b5533
|
||||
# VITE_THEME_LINKS=#0086e6
|
||||
# VITE_THEME_PRIMARY_CONTENT=#ffffff
|
||||
# VITE_THEME_SECONDARY_CONTENT=#a9b2bc
|
||||
# VITE_THEME_TERTIARY_CONTENT=#8e99a4
|
||||
# VITE_THEME_TERTIARY_CONTENT_20=#8e99a433
|
||||
# VITE_THEME_QUATERNARY_CONTENT=#6f7882
|
||||
# VITE_THEME_QUINARY_CONTENT=#394049
|
||||
# VITE_THEME_SYSTEM=#21262c
|
||||
# VITE_THEME_BACKGROUND=#15191e
|
||||
# VITE_THEME_BACKGROUND_85=#15191ed9
|
||||
# VITE_THEME_SUBTLE_PRIMARY=#26282D
|
||||
# CSS to be injected into the page for the purpose of custom theming.
|
||||
# Generally, writing a custom theme involves overriding Compound design tokens,
|
||||
# which are documented here:
|
||||
# https://compound.element.io/?path=/docs/foundations-design-tokens--docs
|
||||
# https://compound.element.io/?path=/docs/tokens-color-palettes--docs
|
||||
# https://compound.element.io/?path=/docs/tokens-semantic-colors--docs
|
||||
# VITE_CUSTOM_CSS=".cpd-theme-dark.cpd-theme-dark { --cpd-color-theme-bg: #101317; }"
|
||||
|
||||
1
.github/workflows/build.yaml
vendored
1
.github/workflows/build.yaml
vendored
@@ -23,6 +23,7 @@ jobs:
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
SENTRY_URL: ${{ secrets.SENTRY_URL }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
VITE_APP_VERSION: ${{ github.sha }}
|
||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
|
||||
4
.github/workflows/netlify-fullmesh.yaml
vendored
4
.github/workflows/netlify-fullmesh.yaml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
ref: ${{ github.event.workflow_run.head_sha }}
|
||||
|
||||
- name: "Download artifact"
|
||||
uses: actions/github-script@v3.1.0
|
||||
uses: actions/github-script@v3.2.0
|
||||
with:
|
||||
script: |
|
||||
const artifacts = await github.actions.listWorkflowRunArtifacts({
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
|
||||
- name: Deploy to Netlify
|
||||
id: netlify
|
||||
uses: nwtgck/actions-netlify@v1.2.3
|
||||
uses: nwtgck/actions-netlify@v1.2.4
|
||||
with:
|
||||
publish-dir: dist
|
||||
deploy-message: "Deploy from GitHub Actions"
|
||||
|
||||
4
.github/workflows/netlify-livekit.yaml
vendored
4
.github/workflows/netlify-livekit.yaml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
ref: ${{ github.event.workflow_run.head_sha }}
|
||||
|
||||
- name: "Download artifact"
|
||||
uses: actions/github-script@v3.1.0
|
||||
uses: actions/github-script@v3.2.0
|
||||
with:
|
||||
script: |
|
||||
const artifacts = await github.actions.listWorkflowRunArtifacts({
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
|
||||
- name: Deploy to Netlify
|
||||
id: netlify
|
||||
uses: nwtgck/actions-netlify@v1.2.3
|
||||
uses: nwtgck/actions-netlify@v1.2.4
|
||||
with:
|
||||
publish-dir: dist
|
||||
deploy-message: "Deploy from GitHub Actions"
|
||||
|
||||
1
.github/workflows/netlify-pr.yaml
vendored
1
.github/workflows/netlify-pr.yaml
vendored
@@ -6,6 +6,7 @@ on:
|
||||
- completed
|
||||
branches-ignore:
|
||||
- "main"
|
||||
- "livekit"
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request'
|
||||
|
||||
12
.github/workflows/publish.yaml
vendored
12
.github/workflows/publish.yaml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Log in to container registry
|
||||
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
|
||||
uses: docker/login-action@cf8514a65188af1d4f94f8c28a7a4153af1088ce
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
tar --numeric-owner --transform "s/dist/element-call-${TARBALL_VERSION}/" -cvzf element-call-${TARBALL_VERSION}.tar.gz dist
|
||||
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce
|
||||
uses: actions/upload-artifact@65d862660abb392b8c4a3d1195a2108db131dd05
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
@@ -62,20 +62,20 @@ jobs:
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@818d4b7b91585d195f67373fd9cb0332e31a7175
|
||||
uses: docker/metadata-action@0f8c876bafbf5dbce05c36682ec68e9a0274a48a
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=sha,format=short,event=branch
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern=v{{version}}
|
||||
type=raw,value=latest-ci,enable={{is_default_branch}}
|
||||
type=raw,value=latest-ci_${{steps.current-time.outputs.unix_time}},enable={{is_default_branch}}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@dc7b9719a96d48369863986a06765841d7ea23f6
|
||||
uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
|
||||
uses: docker/build-push-action@9311bf5263ae5b36f3ec67aff768790c6e2344ad
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"default_server_config": {
|
||||
"m.homeserver": {
|
||||
"base_url": "https://call.ems.host",
|
||||
"server_name": "call.ems.host"
|
||||
"base_url": "https://call-unstable.ems.host",
|
||||
"server_name": "call-unstable.ems.host"
|
||||
}
|
||||
},
|
||||
"livekit": {
|
||||
|
||||
19
package.json
19
package.json
@@ -20,14 +20,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@juggle/resize-observer": "^3.3.1",
|
||||
"@livekit/components-react": "1.1.0",
|
||||
"@livekit/components-react": "^1.1.0",
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz",
|
||||
"@opentelemetry/api": "^1.4.0",
|
||||
"@opentelemetry/context-zone": "^1.9.1",
|
||||
"@opentelemetry/exporter-jaeger": "^1.9.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.35.1",
|
||||
"@opentelemetry/instrumentation-document-load": "^0.31.1",
|
||||
"@opentelemetry/instrumentation-user-interaction": "^0.32.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.41.0",
|
||||
"@opentelemetry/instrumentation-document-load": "^0.33.0",
|
||||
"@opentelemetry/instrumentation-user-interaction": "^0.33.0",
|
||||
"@opentelemetry/sdk-trace-web": "^1.9.1",
|
||||
"@react-aria/button": "^3.3.4",
|
||||
"@react-aria/dialog": "^3.1.4",
|
||||
@@ -47,6 +47,8 @@
|
||||
"@sentry/react": "^6.13.3",
|
||||
"@sentry/tracing": "^6.13.3",
|
||||
"@use-gesture/react": "^10.2.11",
|
||||
"@vector-im/compound-design-tokens": "^0.0.5",
|
||||
"@vector-im/compound-web": "^0.2.15",
|
||||
"@vitejs/plugin-basic-ssl": "^1.0.1",
|
||||
"@vitejs/plugin-react": "^4.0.1",
|
||||
"classnames": "^2.3.1",
|
||||
@@ -55,11 +57,11 @@
|
||||
"i18next": "^21.10.0",
|
||||
"i18next-browser-languagedetector": "^6.1.8",
|
||||
"i18next-http-backend": "^1.4.4",
|
||||
"livekit-client": "1.12.3",
|
||||
"livekit-client": "^1.12.3",
|
||||
"lodash": "^4.17.21",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#b698217445318f453e0b1086364a33113eaa85d9",
|
||||
"matrix-widget-api": "^1.3.1",
|
||||
"mermaid": "^8.13.8",
|
||||
"mermaid": "^9.0.0",
|
||||
"normalize.css": "^8.0.1",
|
||||
"pako": "^2.0.4",
|
||||
"postcss-preset-env": "^7",
|
||||
@@ -79,12 +81,15 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.16.5",
|
||||
"@react-spring/rafz": "^9.7.3",
|
||||
"@sentry/vite-plugin": "^0.3.0",
|
||||
"@storybook/react": "^6.5.0-alpha.5",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@types/content-type": "^1.1.5",
|
||||
"@types/d3": "^7.4.0",
|
||||
"@types/dom-screen-wake-lock": "^1.0.1",
|
||||
"@types/dompurify": "^3.0.2",
|
||||
"@types/grecaptcha": "^3.0.4",
|
||||
"@types/node": "^18.13.0",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
@@ -117,7 +122,7 @@
|
||||
"vite-plugin-svgr": "^3.2.0"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "jsdom",
|
||||
"testEnvironment": "./test/environment.ts",
|
||||
"testMatch": [
|
||||
"<rootDir>/test/**/*-test.[jt]s?(x)"
|
||||
],
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body class="cpd-theme-dark">
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"Element Call Home": "Element Call Home",
|
||||
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call is temporarily not end-to-end encrypted while we test scalability.",
|
||||
"Enable end-to-end encryption (password protected calls)": "Enable end-to-end encryption (password protected calls)",
|
||||
"End call": "End call",
|
||||
"End-to-end encryption isn't supported on your browser.": "End-to-end encryption isn't supported on your browser.",
|
||||
"Exit full screen": "Exit full screen",
|
||||
"Expose developer settings in the settings window.": "Expose developer settings in the settings window.",
|
||||
@@ -61,21 +62,20 @@
|
||||
"Join call": "Join call",
|
||||
"Join call now": "Join call now",
|
||||
"Join existing call?": "Join existing call?",
|
||||
"Leave": "Leave",
|
||||
"Loading…": "Loading…",
|
||||
"Local volume": "Local volume",
|
||||
"Logging in…": "Logging in…",
|
||||
"Login": "Login",
|
||||
"Login to your account": "Login to your account",
|
||||
"Microphone": "Microphone",
|
||||
"Microphone off": "Microphone off",
|
||||
"Microphone on": "Microphone on",
|
||||
"More": "More",
|
||||
"Mute microphone": "Mute microphone",
|
||||
"No": "No",
|
||||
"Not now, return to home screen": "Not now, return to home screen",
|
||||
"Not registered yet? <2>Create an account</2>": "Not registered yet? <2>Create an account</2>",
|
||||
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>",
|
||||
"Password": "Password",
|
||||
"Password (if none, E2EE is disabled)": "Password (if none, E2EE is disabled)",
|
||||
"Passwords must match": "Passwords must match",
|
||||
"Profile": "Profile",
|
||||
"Recaptcha dismissed": "Recaptcha dismissed",
|
||||
@@ -92,13 +92,13 @@
|
||||
"Sending…": "Sending…",
|
||||
"Settings": "Settings",
|
||||
"Share screen": "Share screen",
|
||||
"Sharing screen": "Sharing screen",
|
||||
"Show call inspector": "Show call inspector",
|
||||
"Show connection stats": "Show connection stats",
|
||||
"Sign in": "Sign in",
|
||||
"Sign out": "Sign out",
|
||||
"Speaker": "Speaker",
|
||||
"Spotlight": "Spotlight",
|
||||
"Stop sharing screen": "Stop sharing screen",
|
||||
"Submit": "Submit",
|
||||
"Submit feedback": "Submit feedback",
|
||||
"Submitting…": "Submitting…",
|
||||
@@ -106,16 +106,16 @@
|
||||
"Thanks, we received your feedback!": "Thanks, we received your feedback!",
|
||||
"Thanks!": "Thanks!",
|
||||
"This call already exists, would you like to join?": "This call already exists, would you like to join?",
|
||||
"This call is not end-to-end encrypted.": "This call is not end-to-end encrypted.",
|
||||
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)</12>": "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)</12>",
|
||||
"Turn off camera": "Turn off camera",
|
||||
"Turn on camera": "Turn on camera",
|
||||
"Unmute microphone": "Unmute microphone",
|
||||
"User menu": "User menu",
|
||||
"Username": "Username",
|
||||
"Version: {{version}}": "Version: {{version}}",
|
||||
"Video": "Video",
|
||||
"Video call": "Video call",
|
||||
"Video call name": "Video call name",
|
||||
"Video off": "Video off",
|
||||
"Video on": "Video on",
|
||||
"Waiting for other participants…": "Waiting for other participants…",
|
||||
"Walkie-talkie call": "Walkie-talkie call",
|
||||
"Walkie-talkie call name": "Walkie-talkie call name",
|
||||
|
||||
11
renovate.json
Normal file
11
renovate.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:base"],
|
||||
"packageRules": [
|
||||
{
|
||||
"description": "Disable renoavte for packages we want to monitor ourselves",
|
||||
"matchPackagePatterns": ["matrix-js-sdk"],
|
||||
"enabled": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
.avatar {
|
||||
position: relative;
|
||||
color: var(--primary-content);
|
||||
color: var(--stopgap-color-on-solid-accent);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -33,7 +33,7 @@ limitations under the License.
|
||||
}
|
||||
|
||||
.avatar svg * {
|
||||
fill: var(--primary-content);
|
||||
fill: var(--cpd-color-text-primary);
|
||||
}
|
||||
|
||||
.avatar span {
|
||||
|
||||
@@ -18,5 +18,5 @@ limitations under the License.
|
||||
flex: 1;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
background-color: var(--subtle-primary);
|
||||
background-color: var(--cpd-color-bg-subtle-primary);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ limitations under the License.
|
||||
.banner {
|
||||
position: absolute;
|
||||
padding: 29px;
|
||||
background-color: var(--quaternary-content);
|
||||
background-color: var(--cpd-color-bg-subtle-primary);
|
||||
vertical-align: middle;
|
||||
font-size: var(--font-size-body);
|
||||
text-align: center;
|
||||
|
||||
@@ -19,8 +19,12 @@ import { Trans } from "react-i18next";
|
||||
import { Banner } from "./Banner";
|
||||
import styles from "./E2EEBanner.module.css";
|
||||
import { ReactComponent as LockOffIcon } from "./icons/LockOff.svg";
|
||||
import { useEnableE2EE } from "./settings/useSetting";
|
||||
|
||||
export const E2EEBanner = () => {
|
||||
const [e2eeEnabled] = useEnableE2EE();
|
||||
if (e2eeEnabled) return null;
|
||||
|
||||
return (
|
||||
<Banner>
|
||||
<div className={styles.e2eeBanner}>
|
||||
|
||||
@@ -24,5 +24,5 @@ limitations under the License.
|
||||
margin: 8px;
|
||||
|
||||
border-radius: 100%;
|
||||
background-color: var(--subtle-primary);
|
||||
background-color: var(--cpd-color-bg-subtle-primary);
|
||||
}
|
||||
|
||||
@@ -26,10 +26,7 @@ import { TooltipTrigger } from "./Tooltip";
|
||||
export const E2EELock = () => {
|
||||
const { t } = useTranslation();
|
||||
const tooltip = useCallback(
|
||||
() =>
|
||||
t(
|
||||
"Element Call is temporarily not end-to-end encrypted while we test scalability."
|
||||
),
|
||||
() => t("This call is not end-to-end encrypted."),
|
||||
[t]
|
||||
);
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ limitations under the License.
|
||||
.facepile .avatar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
border: 1px solid var(--system);
|
||||
border: 1px solid var(--cpd-color-bg-canvas-default);
|
||||
}
|
||||
|
||||
.facepile.md .avatar {
|
||||
|
||||
@@ -82,27 +82,6 @@ limitations under the License.
|
||||
stroke: white;
|
||||
}
|
||||
|
||||
.backButton {
|
||||
background: transparent;
|
||||
border: none;
|
||||
display: flex;
|
||||
color: var(--primary-content);
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.backButton h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.backButton > * {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.backButton > :last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.userName {
|
||||
font-weight: 600;
|
||||
margin-right: 8px;
|
||||
@@ -111,15 +90,6 @@ limitations under the License.
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.signOutButton {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: rgb(255, 75, 85);
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.versionMismatchWarning {
|
||||
padding-left: 15px;
|
||||
}
|
||||
@@ -134,7 +104,7 @@ limitations under the License.
|
||||
mask-image: url("./icons/AlertTriangleFilled.svg");
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
background-color: var(--alert);
|
||||
background-color: var(--cpd-color-icon-critical-primary);
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ limitations under the License.
|
||||
overflow-y: auto;
|
||||
list-style: none;
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--quinary-content);
|
||||
background-color: var(--background);
|
||||
border: 1px solid var(--cpd-color-border-interactive-secondary);
|
||||
background-color: var(--cpd-color-bg-canvas-default);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ limitations under the License.
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: transparent;
|
||||
color: var(--primary-content);
|
||||
color: var(--cpd-color-text-primary);
|
||||
padding: 8px 16px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
@@ -44,6 +44,6 @@ limitations under the License.
|
||||
}
|
||||
|
||||
.option.disabled {
|
||||
color: var(--quaternary-content);
|
||||
background-color: var(--bgColor3);
|
||||
color: var(--cpd-color-text-disabled);
|
||||
background-color: var(--stopgap-bgColor3);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ limitations under the License.
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
color: var(--primary-content);
|
||||
color: var(--cpd-color-text-primary);
|
||||
font-size: var(--font-size-body);
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
@@ -44,7 +44,11 @@ limitations under the License.
|
||||
|
||||
.menuItem.focused,
|
||||
.menuItem:hover {
|
||||
background-color: var(--quinary-content);
|
||||
background-color: var(--cpd-color-bg-action-secondary-hovered);
|
||||
}
|
||||
|
||||
.menuItem:active {
|
||||
background-color: var(--cpd-color-bg-action-secondary-pressed);
|
||||
}
|
||||
|
||||
.menuItem.focused:first-child,
|
||||
@@ -65,5 +69,5 @@ limitations under the License.
|
||||
}
|
||||
|
||||
.checkIcon * {
|
||||
stroke: var(--primary-content);
|
||||
stroke: var(--cpd-color-text-primary);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import { Node } from "@react-types/shared";
|
||||
import styles from "./Menu.module.css";
|
||||
|
||||
interface MenuProps<T> extends AriaMenuOptions<T> {
|
||||
className?: String;
|
||||
className?: string;
|
||||
onClose: () => void;
|
||||
onAction: (value: Key) => void;
|
||||
label?: string;
|
||||
|
||||
@@ -28,7 +28,7 @@ limitations under the License.
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: #21262c;
|
||||
background: var(--cpd-color-bg-subtle-secondary);
|
||||
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 8px;
|
||||
max-width: 90vw;
|
||||
|
||||
@@ -15,12 +15,12 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
.tooltip {
|
||||
background-color: var(--system);
|
||||
background-color: var(--cpd-color-bg-subtle-secondary);
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
color: var(--primary-content);
|
||||
color: var(--cpd-color-text-primary);
|
||||
border-radius: 8px;
|
||||
max-width: 135px;
|
||||
width: max-content;
|
||||
|
||||
@@ -19,6 +19,8 @@ import { useLocation } from "react-router-dom";
|
||||
|
||||
import { Config } from "./config/Config";
|
||||
|
||||
export const PASSWORD_STRING = "password=";
|
||||
|
||||
interface UrlParams {
|
||||
roomAlias: string | null;
|
||||
roomId: string | null;
|
||||
@@ -86,6 +88,10 @@ interface UrlParams {
|
||||
* user's homeserver doesn't provide any.
|
||||
*/
|
||||
allowIceFallback: boolean;
|
||||
/**
|
||||
* E2EE password
|
||||
*/
|
||||
password: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -102,9 +108,12 @@ export const getUrlParams = (
|
||||
pathname = window.location.pathname,
|
||||
hash = window.location.hash
|
||||
): UrlParams => {
|
||||
// This is legacy code - we're moving away from using aliases
|
||||
let roomAlias: string | null = null;
|
||||
if (!ignoreRoomAlias) {
|
||||
if (hash === "") {
|
||||
// Here we handle the beginning of the alias and make sure it starts with a
|
||||
// "#"
|
||||
if (hash === "" || hash.startsWith("#?")) {
|
||||
roomAlias = pathname.substring(1); // Strip the "/"
|
||||
|
||||
// Delete "/room/", if present
|
||||
@@ -119,17 +128,17 @@ export const getUrlParams = (
|
||||
roomAlias = hash;
|
||||
}
|
||||
|
||||
// Add server part, if not present
|
||||
if (!roomAlias.includes(":")) {
|
||||
roomAlias = `${roomAlias}:${Config.defaultServerName()}`;
|
||||
}
|
||||
|
||||
// Delete "?" and what comes afterwards
|
||||
roomAlias = roomAlias.split("?")[0];
|
||||
|
||||
// Make roomAlias undefined, if empty
|
||||
if (roomAlias.length <= 1) {
|
||||
// Make roomAlias is null, if it only is a "#"
|
||||
roomAlias = null;
|
||||
} else {
|
||||
// Add server part, if not present
|
||||
if (!roomAlias.includes(":")) {
|
||||
roomAlias = `${roomAlias}:${Config.defaultServerName()}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,6 +173,7 @@ export const getUrlParams = (
|
||||
return {
|
||||
roomAlias,
|
||||
roomId,
|
||||
password: getParam("password"),
|
||||
viaServers: getAllParams("via"),
|
||||
isEmbedded: hasParam("embed"),
|
||||
preload: hasParam("preload"),
|
||||
|
||||
@@ -22,7 +22,7 @@ limitations under the License.
|
||||
}
|
||||
|
||||
.userButton svg * {
|
||||
fill: var(--primary-content);
|
||||
fill: var(--cpd-color-icon-primary);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
|
||||
@@ -20,7 +20,7 @@ import { MatrixClient } from "matrix-js-sdk";
|
||||
import { Buffer } from "buffer";
|
||||
|
||||
import { widget } from "../widget";
|
||||
import { getSetting, setSetting, settingsBus } from "../settings/useSetting";
|
||||
import { getSetting, setSetting, getSettingKey } from "../settings/useSetting";
|
||||
import {
|
||||
CallEndedTracker,
|
||||
CallStartedTracker,
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
} from "./PosthogEvents";
|
||||
import { Config } from "../config/Config";
|
||||
import { getUrlParams } from "../UrlParams";
|
||||
import { localStorageBus } from "../useLocalStorage";
|
||||
|
||||
/* Posthog analytics tracking.
|
||||
*
|
||||
@@ -413,7 +414,7 @@ export class PosthogAnalytics {
|
||||
// * When the user changes their preferences on this device
|
||||
// Note that for new accounts, pseudonymousAnalyticsOptIn won't be set, so updateAnonymityFromSettings
|
||||
// won't be called (i.e. this.anonymity will be left as the default, until the setting changes)
|
||||
settingsBus.on("opt-in-analytics", (optInAnalytics) => {
|
||||
localStorageBus.on(getSettingKey("opt-in-analytics"), (optInAnalytics) => {
|
||||
this.updateAnonymityAndIdentifyUser(optInAnalytics);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ limitations under the License.
|
||||
}
|
||||
|
||||
.authLinks a {
|
||||
color: var(--accent);
|
||||
color: var(--cpd-color-text-action-accent);
|
||||
text-decoration: none;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
@@ -46,8 +46,8 @@ limitations under the License.
|
||||
}
|
||||
|
||||
.button {
|
||||
color: var(--primary-content);
|
||||
background-color: var(--accent);
|
||||
color: var(--stopgap-color-on-solid-accent);
|
||||
background-color: var(--cpd-color-text-action-accent);
|
||||
}
|
||||
|
||||
.button:focus,
|
||||
@@ -62,7 +62,8 @@ limitations under the License.
|
||||
}
|
||||
|
||||
.toolbarButton:disabled {
|
||||
opacity: 0.55;
|
||||
background-color: var(--cpd-color-bg-action-primary-disabled);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.toolbarButton,
|
||||
@@ -70,72 +71,76 @@ limitations under the License.
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50px;
|
||||
background-color: var(--system);
|
||||
}
|
||||
|
||||
.toolbarButton:hover,
|
||||
.toolbarButtonSecondary:hover {
|
||||
background-color: var(--quinary-content);
|
||||
background-color: var(--cpd-color-bg-canvas-default);
|
||||
color: var(--cpd-color-icon-primary);
|
||||
border: 1px solid var(--cpd-color-gray-400);
|
||||
box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);
|
||||
}
|
||||
|
||||
.toolbarButton.on,
|
||||
.toolbarButton.off {
|
||||
background-color: var(--primary-content);
|
||||
background-color: var(--cpd-color-bg-action-primary-rest);
|
||||
color: var(--cpd-color-icon-on-solid-primary);
|
||||
}
|
||||
|
||||
.toolbarButtonSecondary.on {
|
||||
background-color: var(--accent);
|
||||
background-color: var(--cpd-color-text-success-primary);
|
||||
}
|
||||
|
||||
.toolbarButton:active,
|
||||
.toolbarButtonSecondary:active {
|
||||
background-color: var(--cpd-color-bg-subtle-primary);
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.toolbarButton.on:active,
|
||||
.toolbarButton.off:active {
|
||||
background-color: var(--cpd-color-bg-action-primary-pressed);
|
||||
}
|
||||
|
||||
.iconButton:not(.stroke) svg * {
|
||||
fill: var(--primary-content);
|
||||
fill: var(--cpd-color-bg-action-primary-rest);
|
||||
}
|
||||
|
||||
.iconButton:not(.stroke):hover svg * {
|
||||
fill: var(--accent);
|
||||
.iconButton:not(.stroke):tertiary svg * {
|
||||
fill: var(--cpd-color-icon-accent-tertiary);
|
||||
}
|
||||
|
||||
.iconButton.on:not(.stroke) svg * {
|
||||
fill: var(--accent);
|
||||
fill: var(--cpd-color-icon-accent-tertiary);
|
||||
}
|
||||
|
||||
.iconButton.on.stroke svg * {
|
||||
stroke: var(--accent);
|
||||
stroke: var(--cpd-color-icon-accent-tertiary);
|
||||
}
|
||||
|
||||
.hangupButton,
|
||||
.hangupButton:hover {
|
||||
background-color: var(--alert);
|
||||
.hangupButton {
|
||||
background-color: var(--cpd-color-bg-critical-primary);
|
||||
border-color: var(--cpd-color-border-critical-subtle);
|
||||
color: var(--stopgap-color-on-solid-accent);
|
||||
}
|
||||
|
||||
.toolbarButton.on svg * {
|
||||
fill: var(--accent);
|
||||
}
|
||||
|
||||
.toolbarButton.off svg * {
|
||||
fill: #21262c;
|
||||
}
|
||||
|
||||
.toolbarButtonSecondary.on svg * {
|
||||
fill: var(--primary-content);
|
||||
.hangupButton:active {
|
||||
background-color: var(--cpd-color-bg-critical-pressed);
|
||||
}
|
||||
|
||||
.secondary,
|
||||
.copyButton {
|
||||
color: var(--accent);
|
||||
border: 2px solid var(--accent);
|
||||
color: var(--cpd-color-text-action-accent);
|
||||
border: 2px solid var(--cpd-color-text-action-accent);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.secondaryHangup {
|
||||
color: var(--alert);
|
||||
border: 2px solid var(--alert);
|
||||
color: var(--cpd-color-text-critical-primary);
|
||||
border: 2px solid var(--cpd-color-border-critical-primary);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.copyButton.secondaryCopy {
|
||||
color: var(--primary-content);
|
||||
border-color: var(--primary-content);
|
||||
color: var(--cpd-color-text-primary);
|
||||
border-color: var(--cpd-color-border-interactive-primary);
|
||||
}
|
||||
|
||||
.copyButton {
|
||||
@@ -158,12 +163,12 @@ limitations under the License.
|
||||
}
|
||||
|
||||
.copyButton:not(.on) svg * {
|
||||
fill: var(--accent);
|
||||
fill: var(--cpd-color-icon-accent-tertiary);
|
||||
}
|
||||
|
||||
.copyButton.on {
|
||||
border-color: transparent;
|
||||
background-color: var(--accent);
|
||||
background-color: var(--cpd-color-text-action-accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@@ -172,32 +177,28 @@ limitations under the License.
|
||||
}
|
||||
|
||||
.copyButton.secondaryCopy:not(.on) svg * {
|
||||
fill: var(--primary-content);
|
||||
fill: var(--cpd-color-bg-action-primary-rest);
|
||||
}
|
||||
|
||||
.iconCopyButton svg * {
|
||||
fill: var(--tertiary-content);
|
||||
}
|
||||
|
||||
.iconCopyButton:hover svg * {
|
||||
fill: var(--accent);
|
||||
fill: var(--cpd-color-icon-secondary);
|
||||
}
|
||||
|
||||
.iconCopyButton.on svg *,
|
||||
.iconCopyButton.on:hover svg * {
|
||||
fill: transparent;
|
||||
stroke: var(--accent);
|
||||
stroke: var(--cpd-color-text-action-accent);
|
||||
}
|
||||
|
||||
.dropdownButton {
|
||||
color: var(--primary-content);
|
||||
color: var(--cpd-color-text-primary);
|
||||
padding: 2px 8px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.dropdownButton:hover,
|
||||
.dropdownButton:active,
|
||||
.dropdownButton.on {
|
||||
background-color: var(--quinary-content);
|
||||
background-color: var(--cpd-color-bg-action-secondary-pressed);
|
||||
}
|
||||
|
||||
.dropdownButton svg {
|
||||
@@ -205,7 +206,7 @@ limitations under the License.
|
||||
}
|
||||
|
||||
.dropdownButton svg * {
|
||||
fill: var(--primary-content);
|
||||
fill: var(--cpd-color-icon-primary);
|
||||
}
|
||||
|
||||
.lg {
|
||||
@@ -215,6 +216,36 @@ limitations under the License.
|
||||
.linkButton {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: var(--accent);
|
||||
color: var(--cpd-color-text-action-accent);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.toolbarButton:hover,
|
||||
.toolbarButtonSecondary:hover {
|
||||
background-color: var(--cpd-color-bg-subtle-primary);
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.toolbarButton.on:hover,
|
||||
.toolbarButton.off:hover {
|
||||
background-color: var(--cpd-color-bg-action-primary-hovered);
|
||||
}
|
||||
|
||||
.iconButton:not(.stroke):hover svg * {
|
||||
fill: var(--cpd-color-icon-accent-tertiary);
|
||||
}
|
||||
|
||||
.hangupButton:hover {
|
||||
background-color: var(--cpd-color-bg-critical-hovered);
|
||||
}
|
||||
|
||||
.iconCopyButton:hover svg * {
|
||||
fill: var(--cpd-color-icon-accent-tertiary);
|
||||
}
|
||||
|
||||
.dropdownButton:hover {
|
||||
background-color: var(--cpd-color-bg-action-secondary-hovered);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,17 +19,18 @@ import classNames from "classnames";
|
||||
import { useButton } from "@react-aria/button";
|
||||
import { mergeProps, useObjectRef } from "@react-aria/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Tooltip } from "@vector-im/compound-web";
|
||||
import { ReactComponent as MicOnSolidIcon } from "@vector-im/compound-design-tokens/icons/mic-on-solid.svg";
|
||||
import { ReactComponent as MicOffSolidIcon } from "@vector-im/compound-design-tokens/icons/mic-off-solid.svg";
|
||||
import { ReactComponent as VideoCallIcon } from "@vector-im/compound-design-tokens/icons/video-call.svg";
|
||||
import { ReactComponent as VideoCallOffIcon } from "@vector-im/compound-design-tokens/icons/video-call-off.svg";
|
||||
import { ReactComponent as EndCallIcon } from "@vector-im/compound-design-tokens/icons/end-call.svg";
|
||||
import { ReactComponent as ShareScreenSolidIcon } from "@vector-im/compound-design-tokens/icons/share-screen-solid.svg";
|
||||
import { ReactComponent as SettingsSolidIcon } from "@vector-im/compound-design-tokens/icons/settings-solid.svg";
|
||||
import { ReactComponent as UserAddSolidIcon } from "@vector-im/compound-design-tokens/icons/user-add-solid.svg";
|
||||
import { ReactComponent as ChevronDownIcon } from "@vector-im/compound-design-tokens/icons/chevron-down.svg";
|
||||
|
||||
import styles from "./Button.module.css";
|
||||
import { ReactComponent as MicIcon } from "../icons/Mic.svg";
|
||||
import { ReactComponent as MuteMicIcon } from "../icons/MuteMic.svg";
|
||||
import { ReactComponent as VideoIcon } from "../icons/Video.svg";
|
||||
import { ReactComponent as DisableVideoIcon } from "../icons/DisableVideo.svg";
|
||||
import { ReactComponent as HangupIcon } from "../icons/Hangup.svg";
|
||||
import { ReactComponent as ScreenshareIcon } from "../icons/Screenshare.svg";
|
||||
import { ReactComponent as SettingsIcon } from "../icons/Settings.svg";
|
||||
import { ReactComponent as AddUserIcon } from "../icons/AddUser.svg";
|
||||
import { ReactComponent as ArrowDownIcon } from "../icons/ArrowDown.svg";
|
||||
import { ReactComponent as Fullscreen } from "../icons/Fullscreen.svg";
|
||||
import { ReactComponent as FullscreenExit } from "../icons/FullscreenExit.svg";
|
||||
import { TooltipTrigger } from "../Tooltip";
|
||||
@@ -129,7 +130,7 @@ export const Button = forwardRef<HTMLButtonElement, Props>(
|
||||
>
|
||||
<>
|
||||
{children}
|
||||
{variant === "dropdown" && <ArrowDownIcon />}
|
||||
{variant === "dropdown" && <ChevronDownIcon />}
|
||||
</>
|
||||
</button>
|
||||
);
|
||||
@@ -147,13 +148,11 @@ export function MicButton({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<TooltipTrigger
|
||||
tooltip={() => (muted ? t("Unmute microphone") : t("Mute microphone"))}
|
||||
>
|
||||
<Button variant="toolbar" {...rest} off={muted}>
|
||||
{muted ? <MuteMicIcon /> : <MicIcon />}
|
||||
<Tooltip label={muted ? t("Microphone off") : t("Microphone on")}>
|
||||
<Button variant="toolbar" {...rest} on={!muted}>
|
||||
{muted ? <MicOffSolidIcon /> : <MicOnSolidIcon />}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -168,13 +167,11 @@ export function VideoButton({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<TooltipTrigger
|
||||
tooltip={() => (muted ? t("Turn on camera") : t("Turn off camera"))}
|
||||
>
|
||||
<Button variant="toolbar" {...rest} off={muted}>
|
||||
{muted ? <DisableVideoIcon /> : <VideoIcon />}
|
||||
<Tooltip label={muted ? t("Video off") : t("Video on")}>
|
||||
<Button variant="toolbar" {...rest} on={!muted}>
|
||||
{muted ? <VideoCallOffIcon /> : <VideoCallIcon />}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -191,13 +188,11 @@ export function ScreenshareButton({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<TooltipTrigger
|
||||
tooltip={() => (enabled ? t("Stop sharing screen") : t("Share screen"))}
|
||||
>
|
||||
<Button variant="toolbarSecondary" {...rest} on={enabled}>
|
||||
<ScreenshareIcon />
|
||||
<Tooltip label={enabled ? t("Sharing screen") : t("Share screen")}>
|
||||
<Button variant="toolbar" {...rest} on={enabled}>
|
||||
<ShareScreenSolidIcon />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -210,18 +205,17 @@ export function HangupButton({
|
||||
[index: string]: unknown;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const tooltip = useCallback(() => t("Leave"), [t]);
|
||||
|
||||
return (
|
||||
<TooltipTrigger tooltip={tooltip}>
|
||||
<Tooltip label={t("End call")}>
|
||||
<Button
|
||||
variant="toolbar"
|
||||
className={classNames(styles.hangupButton, className)}
|
||||
{...rest}
|
||||
>
|
||||
<HangupIcon />
|
||||
<EndCallIcon />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -234,14 +228,13 @@ export function SettingsButton({
|
||||
[index: string]: unknown;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const tooltip = useCallback(() => t("Settings"), [t]);
|
||||
|
||||
return (
|
||||
<TooltipTrigger tooltip={tooltip}>
|
||||
<Tooltip label={t("Settings")}>
|
||||
<Button variant="toolbar" {...rest}>
|
||||
<SettingsIcon width={20} height={20} />
|
||||
<SettingsSolidIcon />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -256,14 +249,13 @@ export function InviteButton({
|
||||
[index: string]: unknown;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const tooltip = useCallback(() => t("Invite"), [t]);
|
||||
|
||||
return (
|
||||
<TooltipTrigger tooltip={tooltip}>
|
||||
<Tooltip label={t("Invite")}>
|
||||
<Button variant={variant} {...rest}>
|
||||
<AddUserIcon />
|
||||
<UserAddSolidIcon />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
85
src/e2ee/sharedKeyManagement.ts
Normal file
85
src/e2ee/sharedKeyManagement.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
Copyright 2023 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo } from "react";
|
||||
|
||||
import { useEnableE2EE } from "../settings/useSetting";
|
||||
import { useLocalStorage } from "../useLocalStorage";
|
||||
import { useClient } from "../ClientContext";
|
||||
import { PASSWORD_STRING, useUrlParams } from "../UrlParams";
|
||||
|
||||
export const getRoomSharedKeyLocalStorageKey = (roomId: string): string =>
|
||||
`room-shared-key-${roomId}`;
|
||||
|
||||
export const useInternalRoomSharedKey = (
|
||||
roomId: string
|
||||
): [string | null, (value: string) => void] => {
|
||||
const key = useMemo(() => getRoomSharedKeyLocalStorageKey(roomId), [roomId]);
|
||||
const [e2eeEnabled] = useEnableE2EE();
|
||||
const [roomSharedKey, setRoomSharedKey] = useLocalStorage(key);
|
||||
|
||||
return [e2eeEnabled ? roomSharedKey : null, setRoomSharedKey];
|
||||
};
|
||||
|
||||
export const useRoomSharedKey = (roomId: string): string | null => {
|
||||
return useInternalRoomSharedKey(roomId)[0];
|
||||
};
|
||||
|
||||
export const useManageRoomSharedKey = (roomId: string): string | null => {
|
||||
const { password } = useUrlParams();
|
||||
const [e2eeSharedKey, setE2EESharedKey] = useInternalRoomSharedKey(roomId);
|
||||
|
||||
useEffect(() => {
|
||||
if (!password) return;
|
||||
if (password === "") return;
|
||||
if (password === e2eeSharedKey) return;
|
||||
|
||||
setE2EESharedKey(password);
|
||||
}, [password, e2eeSharedKey, setE2EESharedKey]);
|
||||
|
||||
useEffect(() => {
|
||||
const hash = location.hash;
|
||||
|
||||
if (!hash.includes("?")) return;
|
||||
if (!hash.includes(PASSWORD_STRING)) return;
|
||||
if (password !== e2eeSharedKey) return;
|
||||
|
||||
const [hashStart, passwordStart] = hash.split(PASSWORD_STRING);
|
||||
const hashEnd = passwordStart.split("&")[1];
|
||||
|
||||
location.replace((hashStart ?? "") + (hashEnd ?? ""));
|
||||
}, [password, e2eeSharedKey]);
|
||||
|
||||
return e2eeSharedKey;
|
||||
};
|
||||
|
||||
export const useIsRoomE2EE = (roomId: string): boolean | null => {
|
||||
const { isEmbedded } = useUrlParams();
|
||||
const client = useClient();
|
||||
const room = useMemo(
|
||||
() => client.client?.getRoom(roomId) ?? null,
|
||||
[roomId, client.client]
|
||||
);
|
||||
const isE2EE = useMemo(() => {
|
||||
if (isEmbedded) {
|
||||
return false;
|
||||
} else {
|
||||
return room ? !room?.getCanonicalAlias() : null;
|
||||
}
|
||||
}, [isEmbedded, room]);
|
||||
|
||||
return isE2EE;
|
||||
};
|
||||
@@ -26,7 +26,7 @@ limitations under the License.
|
||||
.callTile {
|
||||
height: 95px;
|
||||
padding: 12px;
|
||||
background-color: var(--system);
|
||||
background-color: var(--cpd-color-bg-subtle-secondary);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
@@ -52,7 +52,7 @@ limitations under the License.
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
padding: 0 16px;
|
||||
color: var(--primary-content);
|
||||
color: var(--cpd-color-text-primary);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,8 +23,9 @@ import { Facepile } from "../Facepile";
|
||||
import { Avatar, Size } from "../Avatar";
|
||||
import styles from "./CallList.module.css";
|
||||
import { getRoomUrl } from "../matrix-utils";
|
||||
import { Body, Caption } from "../typography/Typography";
|
||||
import { Body } from "../typography/Typography";
|
||||
import { GroupCallRoom } from "./useGroupCallRooms";
|
||||
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
|
||||
|
||||
interface CallListProps {
|
||||
rooms: GroupCallRoom[];
|
||||
@@ -35,13 +36,13 @@ export function CallList({ rooms, client, disableFacepile }: CallListProps) {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.callList}>
|
||||
{rooms.map(({ roomAlias, roomName, avatarUrl, participants }) => (
|
||||
{rooms.map(({ room, roomAlias, roomName, avatarUrl, participants }) => (
|
||||
<CallTile
|
||||
key={roomAlias}
|
||||
client={client}
|
||||
name={roomName}
|
||||
avatarUrl={avatarUrl}
|
||||
roomAlias={roomAlias}
|
||||
roomId={room.roomId}
|
||||
participants={participants}
|
||||
disableFacepile={disableFacepile}
|
||||
/>
|
||||
@@ -59,7 +60,7 @@ export function CallList({ rooms, client, disableFacepile }: CallListProps) {
|
||||
interface CallTileProps {
|
||||
name: string;
|
||||
avatarUrl: string;
|
||||
roomAlias: string;
|
||||
roomId: string;
|
||||
participants: RoomMember[];
|
||||
client: MatrixClient;
|
||||
disableFacepile?: boolean;
|
||||
@@ -67,17 +68,16 @@ interface CallTileProps {
|
||||
function CallTile({
|
||||
name,
|
||||
avatarUrl,
|
||||
roomAlias,
|
||||
roomId,
|
||||
participants,
|
||||
client,
|
||||
disableFacepile,
|
||||
}: CallTileProps) {
|
||||
const roomSharedKey = useRoomSharedKey(roomId);
|
||||
|
||||
return (
|
||||
<div className={styles.callTile}>
|
||||
<Link
|
||||
to={`/${roomAlias.substring(1).split(":")[0]}`}
|
||||
className={styles.callTileLink}
|
||||
>
|
||||
<Link to={`/room/#?roomId=${roomId}`} className={styles.callTileLink}>
|
||||
<Avatar
|
||||
size={Size.LG}
|
||||
bgKey={name}
|
||||
@@ -89,7 +89,6 @@ function CallTile({
|
||||
<Body overflowEllipsis fontWeight="semiBold">
|
||||
{name}
|
||||
</Body>
|
||||
<Caption overflowEllipsis>{getRoomUrl(roomAlias)}</Caption>
|
||||
{participants && !disableFacepile && (
|
||||
<Facepile
|
||||
className={styles.facePile}
|
||||
@@ -103,7 +102,7 @@ function CallTile({
|
||||
<CopyButton
|
||||
className={styles.copyButton}
|
||||
variant="icon"
|
||||
value={getRoomUrl(roomAlias)}
|
||||
value={getRoomUrl(roomId, roomSharedKey ?? undefined)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -39,5 +39,5 @@ limitations under the License.
|
||||
}
|
||||
|
||||
.notice {
|
||||
color: var(--secondary-content);
|
||||
color: var(--cpd-color-text-secondary);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ limitations under the License.
|
||||
import { useState, useCallback, FormEvent, FormEventHandler } from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import {
|
||||
@@ -37,9 +38,11 @@ import { JoinExistingCallModal } from "./JoinExistingCallModal";
|
||||
import { Caption, Title } from "../typography/Typography";
|
||||
import { Form } from "../form/Form";
|
||||
import { CallType, CallTypeDropdown } from "./CallTypeDropdown";
|
||||
import { useOptInAnalytics } from "../settings/useSetting";
|
||||
import { useEnableE2EE, useOptInAnalytics } from "../settings/useSetting";
|
||||
import { AnalyticsNotice } from "../analytics/AnalyticsNotice";
|
||||
import { E2EEBanner } from "../E2EEBanner";
|
||||
import { setLocalStorageItem } from "../useLocalStorage";
|
||||
import { getRoomSharedKeyLocalStorageKey } from "../e2ee/sharedKeyManagement";
|
||||
|
||||
interface Props {
|
||||
client: MatrixClient;
|
||||
@@ -54,6 +57,7 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
|
||||
const history = useHistory();
|
||||
const { t } = useTranslation();
|
||||
const { modalState, modalProps } = useModalTriggerState();
|
||||
const [e2eeEnabled] = useEnableE2EE();
|
||||
|
||||
const onSubmit: FormEventHandler<HTMLFormElement> = useCallback(
|
||||
(e: FormEvent) => {
|
||||
@@ -70,16 +74,23 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
|
||||
setError(undefined);
|
||||
setLoading(true);
|
||||
|
||||
const [roomAlias] = await createRoom(client, roomName, ptt);
|
||||
const roomId = (
|
||||
await createRoom(client, roomName, ptt, e2eeEnabled ?? false)
|
||||
)[1];
|
||||
|
||||
if (roomAlias) {
|
||||
history.push(`/${roomAlias.substring(1).split(":")[0]}`);
|
||||
if (e2eeEnabled) {
|
||||
setLocalStorageItem(
|
||||
getRoomSharedKeyLocalStorageKey(roomId),
|
||||
randomString(32)
|
||||
);
|
||||
}
|
||||
|
||||
history.push(`/room/#?roomId=${roomId}`);
|
||||
}
|
||||
|
||||
submit().catch((error) => {
|
||||
if (error.errcode === "M_ROOM_IN_USE") {
|
||||
setExistingRoomId(roomAliasLocalpartFromRoomName(roomName));
|
||||
setExistingAlias(roomAliasLocalpartFromRoomName(roomName));
|
||||
setLoading(false);
|
||||
setError(undefined);
|
||||
modalState.open();
|
||||
@@ -90,15 +101,15 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
|
||||
}
|
||||
});
|
||||
},
|
||||
[client, history, modalState, callType]
|
||||
[client, history, modalState, callType, e2eeEnabled]
|
||||
);
|
||||
|
||||
const recentRooms = useGroupCallRooms(client);
|
||||
|
||||
const [existingRoomId, setExistingRoomId] = useState<string>();
|
||||
const [existingAlias, setExistingAlias] = useState<string>();
|
||||
const onJoinExistingRoom = useCallback(() => {
|
||||
history.push(`/${existingRoomId}`);
|
||||
}, [history, existingRoomId]);
|
||||
history.push(`/${existingAlias}`);
|
||||
}, [history, existingAlias]);
|
||||
|
||||
const callNameLabel =
|
||||
callType === CallType.Video
|
||||
|
||||
@@ -47,5 +47,5 @@ limitations under the License.
|
||||
}
|
||||
|
||||
.notice {
|
||||
color: var(--secondary-content);
|
||||
color: var(--cpd-color-text-secondary);
|
||||
}
|
||||
|
||||
@@ -40,9 +40,11 @@ import styles from "./UnauthenticatedView.module.css";
|
||||
import commonStyles from "./common.module.css";
|
||||
import { generateRandomName } from "../auth/generateRandomName";
|
||||
import { AnalyticsNotice } from "../analytics/AnalyticsNotice";
|
||||
import { useOptInAnalytics } from "../settings/useSetting";
|
||||
import { useEnableE2EE, useOptInAnalytics } from "../settings/useSetting";
|
||||
import { Config } from "../config/Config";
|
||||
import { E2EEBanner } from "../E2EEBanner";
|
||||
import { getRoomSharedKeyLocalStorageKey } from "../e2ee/sharedKeyManagement";
|
||||
import { setLocalStorageItem } from "../useLocalStorage";
|
||||
|
||||
export const UnauthenticatedView: FC = () => {
|
||||
const { setClient } = useClient();
|
||||
@@ -58,6 +60,8 @@ export const UnauthenticatedView: FC = () => {
|
||||
const history = useHistory();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [e2eeEnabled] = useEnableE2EE();
|
||||
|
||||
const onSubmit: FormEventHandler<HTMLFormElement> = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
@@ -79,9 +83,18 @@ export const UnauthenticatedView: FC = () => {
|
||||
true
|
||||
);
|
||||
|
||||
let roomAlias: string;
|
||||
let roomId: string;
|
||||
try {
|
||||
[roomAlias] = await createRoom(client, roomName, ptt);
|
||||
roomId = (
|
||||
await createRoom(client, roomName, ptt, e2eeEnabled ?? false)
|
||||
)[1];
|
||||
|
||||
if (e2eeEnabled) {
|
||||
setLocalStorageItem(
|
||||
getRoomSharedKeyLocalStorageKey(roomId),
|
||||
randomString(32)
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!setClient) {
|
||||
throw error;
|
||||
@@ -110,7 +123,7 @@ export const UnauthenticatedView: FC = () => {
|
||||
}
|
||||
|
||||
setClient({ client, session });
|
||||
history.push(`/${roomAlias.substring(1).split(":")[0]}`);
|
||||
history.push(`/room/#?roomId=${roomId}`);
|
||||
}
|
||||
|
||||
submit().catch((error) => {
|
||||
@@ -120,7 +133,16 @@ export const UnauthenticatedView: FC = () => {
|
||||
reset();
|
||||
});
|
||||
},
|
||||
[register, reset, execute, history, callType, modalState, setClient]
|
||||
[
|
||||
register,
|
||||
reset,
|
||||
execute,
|
||||
history,
|
||||
callType,
|
||||
modalState,
|
||||
setClient,
|
||||
e2eeEnabled,
|
||||
]
|
||||
);
|
||||
|
||||
const callNameLabel =
|
||||
|
||||
@@ -89,8 +89,7 @@ export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] {
|
||||
|
||||
const groupCalls = client.groupCallEventHandler.groupCalls.values();
|
||||
const rooms = Array.from(groupCalls).map((groupCall) => groupCall.room);
|
||||
const filteredRooms = rooms.filter((r) => r.getCanonicalAlias()); // We don't display rooms without an alias
|
||||
const sortedRooms = sortRooms(client, filteredRooms);
|
||||
const sortedRooms = sortRooms(client, rooms);
|
||||
const items = sortedRooms.map((room) => {
|
||||
const groupCall = client.getGroupCallForRoom(room.roomId)!;
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ limitations under the License.
|
||||
(to avoid having to maintain a fork of Inter). */
|
||||
|
||||
@import "normalize.css/normalize.css";
|
||||
@import "@vector-im/compound-design-tokens/assets/web/css/compound-design-tokens.css";
|
||||
@import "@vector-im/compound-web/dist/style.css";
|
||||
|
||||
:root {
|
||||
--font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
@@ -37,22 +39,17 @@ limitations under the License.
|
||||
--font-size-title: calc(24px * var(--font-scale));
|
||||
--font-size-headline: calc(32px * var(--font-scale));
|
||||
|
||||
--accent: #0dbd8b;
|
||||
--accent-20: #0dbd8b33;
|
||||
--alert: #ff5b55;
|
||||
--alert-20: #ff5b5533;
|
||||
--links: #0086e6;
|
||||
--primary-content: #ffffff;
|
||||
--secondary-content: #a9b2bc;
|
||||
--tertiary-content: #8e99a4;
|
||||
--tertiary-content-20: #8e99a433;
|
||||
--quaternary-content: #6f7882;
|
||||
--quinary-content: #394049;
|
||||
--system: #21262c;
|
||||
--background: #15191e;
|
||||
--background-85: rgba(23, 25, 28, 0.85);
|
||||
--bgColor3: #444; /* This isn't found anywhere in the designs or Compound */
|
||||
--subtle-primary: #26282d;
|
||||
--cpd-color-border-accent: var(--cpd-color-green-1100);
|
||||
/* These colors are needed during the transitionary period between the old and
|
||||
new Compound design systems, but should be removed ASAP */
|
||||
--stopgap-color-on-solid-accent: var(--cpd-color-bg-canvas-default);
|
||||
--stopgap-background-85: rgba(255, 255, 255, 0.85);
|
||||
--stopgap-bgColor3: #444;
|
||||
}
|
||||
|
||||
.cpd-theme-dark {
|
||||
--stopgap-color-on-solid-accent: var(--cpd-color-text-primary);
|
||||
--stopgap-background-85: rgba(16, 19, 23, 0.85);
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@@ -136,8 +133,8 @@ limitations under the License.
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background);
|
||||
color: var(--primary-content);
|
||||
background-color: var(--cpd-color-bg-canvas-default);
|
||||
color: var(--cpd-color-text-primary);
|
||||
color-scheme: dark;
|
||||
margin: 0;
|
||||
font-family: var(--font-family);
|
||||
@@ -204,7 +201,7 @@ p {
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent);
|
||||
color: var(--cpd-color-text-action-accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -216,8 +213,8 @@ a:active {
|
||||
hr {
|
||||
width: calc(100% - 24px);
|
||||
border: none;
|
||||
border-top: 1px solid var(--quinary-content);
|
||||
color: var(--quaternary-content);
|
||||
border-top: 1px solid var(--cpd-color-border-interactive-secondary);
|
||||
color: var(--cpd-color-border-interactive-secondary);
|
||||
overflow: visible;
|
||||
text-align: center;
|
||||
height: 5px;
|
||||
|
||||
@@ -87,62 +87,10 @@ export class Initializer {
|
||||
});
|
||||
|
||||
// Custom Themeing
|
||||
if (import.meta.env.VITE_CUSTOM_THEME) {
|
||||
const style = document.documentElement.style;
|
||||
style.setProperty(
|
||||
"--accent",
|
||||
import.meta.env.VITE_THEME_ACCENT as string
|
||||
);
|
||||
style.setProperty(
|
||||
"--accent-20",
|
||||
import.meta.env.VITE_THEME_ACCENT_20 as string
|
||||
);
|
||||
style.setProperty("--alert", import.meta.env.VITE_THEME_ALERT as string);
|
||||
style.setProperty(
|
||||
"--alert-20",
|
||||
import.meta.env.VITE_THEME_ALERT_20 as string
|
||||
);
|
||||
style.setProperty("--links", import.meta.env.VITE_THEME_LINKS as string);
|
||||
style.setProperty(
|
||||
"--primary-content",
|
||||
import.meta.env.VITE_THEME_PRIMARY_CONTENT as string
|
||||
);
|
||||
style.setProperty(
|
||||
"--secondary-content",
|
||||
import.meta.env.VITE_THEME_SECONDARY_CONTENT as string
|
||||
);
|
||||
style.setProperty(
|
||||
"--tertiary-content",
|
||||
import.meta.env.VITE_THEME_TERTIARY_CONTENT as string
|
||||
);
|
||||
style.setProperty(
|
||||
"--tertiary-content-20",
|
||||
import.meta.env.VITE_THEME_TERTIARY_CONTENT_20 as string
|
||||
);
|
||||
style.setProperty(
|
||||
"--quaternary-content",
|
||||
import.meta.env.VITE_THEME_QUATERNARY_CONTENT as string
|
||||
);
|
||||
style.setProperty(
|
||||
"--quinary-content",
|
||||
import.meta.env.VITE_THEME_QUINARY_CONTENT as string
|
||||
);
|
||||
style.setProperty(
|
||||
"--system",
|
||||
import.meta.env.VITE_THEME_SYSTEM as string
|
||||
);
|
||||
style.setProperty(
|
||||
"--background",
|
||||
import.meta.env.VITE_THEME_BACKGROUND as string
|
||||
);
|
||||
style.setProperty(
|
||||
"--background-85",
|
||||
import.meta.env.VITE_THEME_BACKGROUND_85 as string
|
||||
);
|
||||
style.setProperty(
|
||||
"--subtle-primary",
|
||||
import.meta.env.VITE_THEME_SUBTLE_PRIMARY as string
|
||||
);
|
||||
if (import.meta.env.VITE_CUSTOM_CSS) {
|
||||
const style = document.createElement("style");
|
||||
style.textContent = import.meta.env.VITE_CUSTOM_CSS;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
// Custom fonts
|
||||
|
||||
@@ -42,7 +42,7 @@ limitations under the License.
|
||||
position: absolute;
|
||||
bottom: 11px;
|
||||
right: -4px;
|
||||
background-color: var(--quinary-content);
|
||||
background-color: var(--cpd-color-subtle-primary);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 10px;
|
||||
@@ -53,7 +53,7 @@ limitations under the License.
|
||||
}
|
||||
|
||||
.removeButton {
|
||||
color: var(--accent);
|
||||
color: var(--cpd-color-text-action-accent);
|
||||
font-size: var(--font-size-caption);
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ limitations under the License.
|
||||
.inputField {
|
||||
border-radius: 4px;
|
||||
transition: border-color 0.25s;
|
||||
border: 1px solid var(--quinary-content);
|
||||
border: 1px solid var(--cpd-color-border-interactive-primary);
|
||||
}
|
||||
|
||||
.inputField input,
|
||||
@@ -52,8 +52,8 @@ limitations under the License.
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 12px 9px 10px 9px;
|
||||
color: var(--primary-content);
|
||||
background-color: var(--background);
|
||||
color: var(--cpd-color-text-primary);
|
||||
background-color: var(--cpd-color-bg-canvas-default);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -61,7 +61,7 @@ limitations under the License.
|
||||
.inputField.disabled input,
|
||||
.inputField.disabled textarea,
|
||||
.inputField.disabled span {
|
||||
color: var(--quaternary-content);
|
||||
color: var(--cpd-color-text-disabled);
|
||||
}
|
||||
|
||||
.inputField span {
|
||||
@@ -81,13 +81,13 @@ limitations under the License.
|
||||
.inputField input:placeholder-shown:focus::placeholder,
|
||||
.inputField textarea:placeholder-shown:focus::placeholder {
|
||||
transition: color 0.25s ease-in 0.1s;
|
||||
color: var(--quaternary-content);
|
||||
color: var(--cpd-color-text-placeholder);
|
||||
}
|
||||
|
||||
.inputField label {
|
||||
transition: font-size 0.25s ease-out 0.1s, color 0.25s ease-out 0.1s,
|
||||
top 0.25s ease-out 0.1s, background-color 0.25s ease-out 0.1s;
|
||||
color: var(--tertiary-content);
|
||||
color: var(--cpd-color-text-secondary);
|
||||
background-color: transparent;
|
||||
font-size: var(--font-size-body);
|
||||
position: absolute;
|
||||
@@ -103,7 +103,7 @@ limitations under the License.
|
||||
}
|
||||
|
||||
.inputField:focus-within {
|
||||
border-color: var(--links);
|
||||
border-color: var(--cpd-color-text-link-external);
|
||||
}
|
||||
|
||||
.inputField input:focus,
|
||||
@@ -117,7 +117,7 @@ limitations under the License.
|
||||
.inputField textarea:focus + label,
|
||||
.inputField textarea:not(:placeholder-shown) + label,
|
||||
.inputField.prefix textarea + label {
|
||||
background-color: var(--system);
|
||||
background-color: var(--cpd-color-bg-canvas-default);
|
||||
transition: font-size 0.25s ease-out 0s, color 0.25s ease-out 0s,
|
||||
top 0.25s ease-out 0s, background-color 0.25s ease-out 0s;
|
||||
font-size: var(--font-size-micro);
|
||||
@@ -128,7 +128,7 @@ limitations under the License.
|
||||
|
||||
.inputField input:focus + label,
|
||||
.inputField textarea:focus + label {
|
||||
color: var(--links);
|
||||
color: var(--cpd-color-border-focused);
|
||||
}
|
||||
|
||||
.checkboxField {
|
||||
@@ -169,11 +169,11 @@ limitations under the License.
|
||||
|
||||
.checkboxField.disabled,
|
||||
.checkboxField.disabled .description {
|
||||
color: var(--quinary-content);
|
||||
color: var(--cpd-color-text-disabled);
|
||||
}
|
||||
|
||||
.checkboxField.disabled .checkbox {
|
||||
border-color: var(--quinary-content);
|
||||
border-color: var(--cpd-color-border-disabled);
|
||||
}
|
||||
|
||||
.checkbox svg {
|
||||
@@ -181,12 +181,12 @@ limitations under the License.
|
||||
}
|
||||
|
||||
.checkbox svg * {
|
||||
stroke: var(--primary-content);
|
||||
stroke: var(--stopgap-color-on-solid-accent);
|
||||
}
|
||||
|
||||
.checkboxField input[type="checkbox"]:checked + label > .checkbox {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
background: var(--cpd-color-text-action-accent);
|
||||
border-color: var(--cpd-color-text-action-accent);
|
||||
}
|
||||
|
||||
.checkboxField input[type="checkbox"]:checked + label > .checkbox svg {
|
||||
@@ -194,18 +194,18 @@ limitations under the License.
|
||||
}
|
||||
|
||||
.checkboxField:focus-within .checkbox {
|
||||
border: 1.5px solid var(--links) !important;
|
||||
border: 1.5px solid var(--cpd-color-text-link-external) !important;
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-caption);
|
||||
color: var(--alert);
|
||||
color: var(--cpd-color-text-critical-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--secondary-content);
|
||||
color: var(--cpd-color-text-secondary);
|
||||
margin-left: 26px;
|
||||
width: 100%; /* Ensure that it breaks onto the next row */
|
||||
}
|
||||
|
||||
@@ -31,11 +31,11 @@ limitations under the License.
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 12px;
|
||||
background-color: var(--background);
|
||||
background-color: var(--cpd-color-bg-canvas-default);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--quinary-content);
|
||||
border: 1px solid var(--cpd-color-border-interactive-primary);
|
||||
font-size: var(--font-size-body);
|
||||
color: var(--primary-content);
|
||||
color: var(--cpd-color-text-primary);
|
||||
height: 40px;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
|
||||
@@ -11,7 +11,9 @@ import {
|
||||
const defaultLiveKitPublishOptions: TrackPublishDefaults = {
|
||||
audioPreset: AudioPresets.music,
|
||||
dtx: true,
|
||||
red: true,
|
||||
// disable red because the livekit server strips out red packets for clients
|
||||
// that don't support it (firefox) but of course that doesn't work with e2ee.
|
||||
red: false,
|
||||
forceStereo: false,
|
||||
simulcast: true,
|
||||
videoSimulcastLayers: [VideoPresets.h180, VideoPresets.h360] as VideoPreset[],
|
||||
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
import type { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||
import IndexedDBWorker from "./IndexedDBWorker?worker";
|
||||
import { getUrlParams } from "./UrlParams";
|
||||
import { getUrlParams, PASSWORD_STRING } from "./UrlParams";
|
||||
import { loadOlm } from "./olm";
|
||||
import { Config } from "./config/Config";
|
||||
|
||||
@@ -273,14 +273,15 @@ export function isLocalRoomId(roomId: string, client: MatrixClient): boolean {
|
||||
export async function createRoom(
|
||||
client: MatrixClient,
|
||||
name: string,
|
||||
ptt: boolean
|
||||
ptt: boolean,
|
||||
e2ee: boolean
|
||||
): Promise<[string, string]> {
|
||||
logger.log(`Creating room for group call`);
|
||||
const createPromise = client.createRoom({
|
||||
visibility: Visibility.Private,
|
||||
preset: Preset.PublicChat,
|
||||
name,
|
||||
room_alias_name: roomAliasLocalpartFromRoomName(name),
|
||||
room_alias_name: e2ee ? undefined : roomAliasLocalpartFromRoomName(name),
|
||||
power_level_content_override: {
|
||||
invite: 100,
|
||||
kick: 100,
|
||||
@@ -342,15 +343,16 @@ export async function createRoom(
|
||||
return [fullAliasFromRoomName(name, client), result.room_id];
|
||||
}
|
||||
|
||||
// Returns a URL to that will load Element Call with the given room
|
||||
export function getRoomUrl(roomIdOrAlias: string): string {
|
||||
if (roomIdOrAlias.startsWith("#")) {
|
||||
return `${window.location.protocol}//${window.location.host}/${
|
||||
roomIdOrAlias.substring(1).split(":")[0]
|
||||
}`;
|
||||
} else {
|
||||
return `${window.location.protocol}//${window.location.host}/room?roomId=${roomIdOrAlias}`;
|
||||
}
|
||||
/**
|
||||
* Returns a URL to that will load Element Call with the given room
|
||||
* @param roomId of the room
|
||||
* @param password
|
||||
* @returns
|
||||
*/
|
||||
export function getRoomUrl(roomId: string, password?: string): string {
|
||||
return `${window.location.protocol}//${
|
||||
window.location.host
|
||||
}/room/#?roomId=${roomId}${password ? "&" + PASSWORD_STRING + password : ""}`;
|
||||
}
|
||||
|
||||
export function getAvatarUrl(
|
||||
|
||||
@@ -18,7 +18,7 @@ limitations under the License.
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 194px;
|
||||
background: var(--system);
|
||||
background: var(--cpd-color-bg-subtle-secondary);
|
||||
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
.inspector {
|
||||
background-color: var(--system);
|
||||
background-color: var(--cpd-color-bg-subtle-secondary);
|
||||
}
|
||||
|
||||
.scrollContainer {
|
||||
@@ -36,6 +36,6 @@ limitations under the License.
|
||||
|
||||
.sequenceDiagramViewer :global(.messageText) {
|
||||
font-size: var(--font-size-caption);
|
||||
fill: var(--primary-content) !important;
|
||||
stroke: var(--primary-content) !important;
|
||||
fill: var(--cpd-color-text-primary) !important;
|
||||
stroke: var(--cpd-color-text-primary) !important;
|
||||
}
|
||||
|
||||
@@ -32,13 +32,17 @@ import { CallEndedView } from "./CallEndedView";
|
||||
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
|
||||
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
||||
import { useProfile } from "../profile/useProfile";
|
||||
import { E2EEConfig } from "../livekit/useLiveKit";
|
||||
import { findDeviceByName } from "../media-utils";
|
||||
import { OpenIDLoader } from "../livekit/OpenIDLoader";
|
||||
import { ActiveCall } from "./InCallView";
|
||||
import { Config } from "../config/Config";
|
||||
import { MuteStates, useMuteStates } from "./MuteStates";
|
||||
import { useMediaDevices, MediaDevices } from "../livekit/MediaDevicesContext";
|
||||
import {
|
||||
useManageRoomSharedKey,
|
||||
useIsRoomE2EE,
|
||||
} from "../e2ee/sharedKeyManagement";
|
||||
import { useEnableE2EE } from "../settings/useSetting";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -73,6 +77,9 @@ export function GroupCallView({
|
||||
otelGroupCallMembership,
|
||||
} = useGroupCall(groupCall, client);
|
||||
|
||||
const e2eeSharedKey = useManageRoomSharedKey(groupCall.room.roomId);
|
||||
const isRoomE2EE = useIsRoomE2EE(groupCall.room.roomId);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -241,8 +248,11 @@ export function GroupCallView({
|
||||
}
|
||||
}, [groupCall, state, leave]);
|
||||
|
||||
const [e2eeConfig, setE2EEConfig] = useState<E2EEConfig | undefined>(
|
||||
undefined
|
||||
const [e2eeEnabled] = useEnableE2EE();
|
||||
|
||||
const e2eeConfig = useMemo(
|
||||
() => (e2eeSharedKey ? { sharedKey: e2eeSharedKey } : undefined),
|
||||
[e2eeSharedKey]
|
||||
);
|
||||
|
||||
const onReconnect = useCallback(() => {
|
||||
@@ -251,6 +261,22 @@ export function GroupCallView({
|
||||
groupCall.enter();
|
||||
}, [groupCall]);
|
||||
|
||||
if (e2eeEnabled && isRoomE2EE && !e2eeSharedKey) {
|
||||
return (
|
||||
<ErrorView
|
||||
error={
|
||||
new Error(
|
||||
"No E2EE key provided: please make sure the URL you're using to join this call has been retrieved using the in-app button."
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!e2eeEnabled && isRoomE2EE) {
|
||||
return <ErrorView error={new Error("You need to enable E2EE to join.")} />;
|
||||
}
|
||||
|
||||
const livekitServiceURL =
|
||||
groupCall.livekitServiceURL ?? Config.get().livekit?.livekit_service_url;
|
||||
if (!livekitServiceURL) {
|
||||
@@ -319,10 +345,7 @@ export function GroupCallView({
|
||||
<LobbyView
|
||||
matrixInfo={matrixInfo}
|
||||
muteStates={muteStates}
|
||||
onEnter={(e2eeConfig?: E2EEConfig) => {
|
||||
setE2EEConfig(e2eeConfig);
|
||||
enter();
|
||||
}}
|
||||
onEnter={() => enter()}
|
||||
isEmbedded={isEmbedded}
|
||||
hideHeader={hideHeader}
|
||||
/>
|
||||
|
||||
@@ -52,30 +52,23 @@ limitations under the License.
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: var(--footerPadding) 0;
|
||||
/* TODO: Un-hardcode these colors */
|
||||
background: linear-gradient(
|
||||
360deg,
|
||||
#15191e 0%,
|
||||
rgba(21, 25, 30, 0.9) 37%,
|
||||
rgba(21, 25, 30, 0.8) 49.68%,
|
||||
rgba(21, 25, 30, 0.7) 56.68%,
|
||||
rgba(21, 25, 30, 0.427397) 72.92%,
|
||||
rgba(21, 25, 30, 0.257534) 81.06%,
|
||||
rgba(21, 25, 30, 0.136986) 87.29%,
|
||||
rgba(21, 25, 30, 0.0658079) 92.4%,
|
||||
rgba(21, 25, 30, 0) 100%
|
||||
rgba(16, 19, 23, 0.9) 37%,
|
||||
rgba(16, 19, 23, 0.8) 49.68%,
|
||||
rgba(16, 19, 23, 0.7) 56.68%,
|
||||
rgba(16, 19, 23, 0.427397) 72.92%,
|
||||
rgba(16, 19, 23, 0.257534) 81.06%,
|
||||
rgba(16, 19, 23, 0.136986) 87.29%,
|
||||
rgba(16, 19, 23, 0.0658079) 92.4%,
|
||||
rgba(16, 19, 23, 0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.footer > * {
|
||||
margin-right: 30px;
|
||||
}
|
||||
|
||||
.footer > :last-child {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.maximised .footer {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
@@ -84,12 +77,16 @@ limitations under the License.
|
||||
|
||||
@media (min-height: 300px) {
|
||||
.inRoom {
|
||||
--footerPadding: 24px;
|
||||
--footerPadding: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
.inRoom {
|
||||
--footerPadding: 32px;
|
||||
--footerPadding: 60px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ import { useEventEmitterThree } from "../useEvents";
|
||||
import { useWakeLock } from "../useWakeLock";
|
||||
import { useMergedRefs } from "../useMergedRefs";
|
||||
import { MuteStates } from "./MuteStates";
|
||||
import { useIsRoomE2EE } from "../e2ee/sharedKeyManagement";
|
||||
|
||||
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
||||
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
||||
@@ -145,6 +146,8 @@ export function InCallView({
|
||||
usePreventScroll();
|
||||
useWakeLock();
|
||||
|
||||
const isRoomE2EE = useIsRoomE2EE(groupCall.room.roomId);
|
||||
|
||||
const containerRef1 = useRef<HTMLDivElement | null>(null);
|
||||
const [containerRef2, bounds] = useMeasure({ polyfill: ResizeObserver });
|
||||
const boundsValid = bounds.height > 0;
|
||||
@@ -187,7 +190,7 @@ export function InCallView({
|
||||
containerRef1,
|
||||
toggleMicrophone,
|
||||
toggleCamera,
|
||||
async (muted) => await localParticipant.setMicrophoneEnabled(!muted)
|
||||
(muted) => muteStates.audio.setEnabled?.(!muted)
|
||||
);
|
||||
|
||||
const onDisconnected = useCallback(
|
||||
@@ -378,19 +381,19 @@ export function InCallView({
|
||||
const buttons: JSX.Element[] = [];
|
||||
|
||||
buttons.push(
|
||||
<MicButton
|
||||
key="1"
|
||||
muted={!muteStates.audio.enabled}
|
||||
onPress={toggleMicrophone}
|
||||
disabled={muteStates.audio.setEnabled === null}
|
||||
data-testid="incall_mute"
|
||||
/>,
|
||||
<VideoButton
|
||||
key="2"
|
||||
muted={!muteStates.video.enabled}
|
||||
onPress={toggleCamera}
|
||||
disabled={muteStates.video.setEnabled === null}
|
||||
data-testid="incall_videomute"
|
||||
/>,
|
||||
<MicButton
|
||||
key="1"
|
||||
muted={!muteStates.audio.enabled}
|
||||
onPress={toggleMicrophone}
|
||||
disabled={muteStates.audio.setEnabled === null}
|
||||
data-testid="incall_mute"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -424,7 +427,7 @@ export function InCallView({
|
||||
users={unencryptedEventsFromUsers}
|
||||
room={groupCall.room}
|
||||
/>
|
||||
<E2EELock />
|
||||
{!isRoomE2EE && <E2EELock />}
|
||||
</LeftNav>
|
||||
<RightNav>
|
||||
<GridLayoutMenu layout={layout} setLayout={setLayout} />
|
||||
@@ -461,12 +464,7 @@ export function InCallView({
|
||||
/>
|
||||
)}
|
||||
{inviteModalState.isOpen && (
|
||||
<InviteModal
|
||||
roomIdOrAlias={
|
||||
groupCall.room.getCanonicalAlias() ?? groupCall.room.roomId
|
||||
}
|
||||
{...inviteModalProps}
|
||||
/>
|
||||
<InviteModal roomId={groupCall.room.roomId} {...inviteModalProps} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 New Vector Ltd
|
||||
Copyright 2022 - 2023 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -21,13 +21,15 @@ import { Modal, ModalContent, ModalProps } from "../Modal";
|
||||
import { CopyButton } from "../button";
|
||||
import { getRoomUrl } from "../matrix-utils";
|
||||
import styles from "./InviteModal.module.css";
|
||||
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
|
||||
|
||||
interface Props extends Omit<ModalProps, "title" | "children"> {
|
||||
roomIdOrAlias: string;
|
||||
roomId: string;
|
||||
}
|
||||
|
||||
export const InviteModal: FC<Props> = ({ roomIdOrAlias, ...rest }) => {
|
||||
export const InviteModal: FC<Props> = ({ roomId, ...rest }) => {
|
||||
const { t } = useTranslation();
|
||||
const roomSharedKey = useRoomSharedKey(roomId);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -40,7 +42,7 @@ export const InviteModal: FC<Props> = ({ roomIdOrAlias, ...rest }) => {
|
||||
<p>{t("Copy and share this call link")}</p>
|
||||
<CopyButton
|
||||
className={styles.copyButton}
|
||||
value={getRoomUrl(roomIdOrAlias)}
|
||||
value={getRoomUrl(roomId, roomSharedKey ?? undefined)}
|
||||
data-testid="modal_inviteLink"
|
||||
/>
|
||||
</ModalContent>
|
||||
|
||||
@@ -14,14 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
useRef,
|
||||
useEffect,
|
||||
useState,
|
||||
useCallback,
|
||||
ChangeEvent,
|
||||
FC,
|
||||
} from "react";
|
||||
import { useRef, useEffect, FC } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
import styles from "./LobbyView.module.css";
|
||||
@@ -32,15 +25,13 @@ import { UserMenuContainer } from "../UserMenuContainer";
|
||||
import { Body, Link } from "../typography/Typography";
|
||||
import { useLocationNavigation } from "../useLocationNavigation";
|
||||
import { MatrixInfo, VideoPreview } from "./VideoPreview";
|
||||
import { E2EEConfig } from "../livekit/useLiveKit";
|
||||
import { InputField } from "../input/Input";
|
||||
import { useEnableE2EE } from "../settings/useSetting";
|
||||
import { MuteStates } from "./MuteStates";
|
||||
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
|
||||
|
||||
interface Props {
|
||||
matrixInfo: MatrixInfo;
|
||||
muteStates: MuteStates;
|
||||
onEnter: (e2eeConfig?: E2EEConfig) => void;
|
||||
onEnter: () => void;
|
||||
isEmbedded: boolean;
|
||||
hideHeader: boolean;
|
||||
}
|
||||
@@ -53,10 +44,9 @@ export const LobbyView: FC<Props> = ({
|
||||
hideHeader,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const roomSharedKey = useRoomSharedKey(matrixInfo.roomId);
|
||||
useLocationNavigation();
|
||||
|
||||
const [enableE2EE] = useEnableE2EE();
|
||||
|
||||
const joinCallButtonRef = useRef<HTMLButtonElement>(null);
|
||||
useEffect(() => {
|
||||
if (joinCallButtonRef.current) {
|
||||
@@ -64,18 +54,6 @@ export const LobbyView: FC<Props> = ({
|
||||
}
|
||||
}, [joinCallButtonRef]);
|
||||
|
||||
const [e2eeSharedKey, setE2EESharedKey] = useState<string | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const onE2EESharedKeyChanged = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value;
|
||||
setE2EESharedKey(value === "" ? undefined : value);
|
||||
},
|
||||
[setE2EESharedKey]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.room}>
|
||||
{!hideHeader && (
|
||||
@@ -91,25 +69,12 @@ export const LobbyView: FC<Props> = ({
|
||||
<div className={styles.joinRoom}>
|
||||
<div className={styles.joinRoomContent}>
|
||||
<VideoPreview matrixInfo={matrixInfo} muteStates={muteStates} />
|
||||
{enableE2EE && (
|
||||
<InputField
|
||||
className={styles.passwordField}
|
||||
label={t("Password (if none, E2EE is disabled)")}
|
||||
type="text"
|
||||
onChange={onE2EESharedKeyChanged}
|
||||
value={e2eeSharedKey}
|
||||
/>
|
||||
)}
|
||||
<Trans>
|
||||
<Button
|
||||
ref={joinCallButtonRef}
|
||||
className={styles.copyButton}
|
||||
size="lg"
|
||||
onPress={() =>
|
||||
onEnter(
|
||||
e2eeSharedKey ? { sharedKey: e2eeSharedKey } : undefined
|
||||
)
|
||||
}
|
||||
onPress={() => onEnter()}
|
||||
data-testid="lobby_joinCall"
|
||||
>
|
||||
Join call now
|
||||
@@ -117,7 +82,7 @@ export const LobbyView: FC<Props> = ({
|
||||
<Body>Or</Body>
|
||||
<CopyButton
|
||||
variant="secondaryCopy"
|
||||
value={getRoomUrl(matrixInfo.roomAlias ?? matrixInfo.roomId)}
|
||||
value={getRoomUrl(matrixInfo.roomId, roomSharedKey ?? undefined)}
|
||||
className={styles.copyButton}
|
||||
copiedMessage={t("Call link copied")}
|
||||
data-testid="lobby_inviteLink"
|
||||
|
||||
@@ -20,7 +20,7 @@ limitations under the License.
|
||||
height: 50vh;
|
||||
border-radius: 24px;
|
||||
overflow: hidden;
|
||||
background-color: var(--bgColor3);
|
||||
background-color: var(--stopgap-bgColor3);
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ limitations under the License.
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: var(--bgColor3);
|
||||
background-color: var(--stopgap-bgColor3);
|
||||
}
|
||||
|
||||
.cameraPermissions {
|
||||
@@ -62,7 +62,7 @@ limitations under the License.
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: rgba(23, 25, 28, 0.9);
|
||||
background-color: var(--stopgap-background-85);
|
||||
}
|
||||
|
||||
.previewButtons > * {
|
||||
|
||||
@@ -68,6 +68,9 @@ export const VideoPreview: FC<Props> = ({ matrixInfo, muteStates }) => {
|
||||
|
||||
const devices = useMediaDevices();
|
||||
|
||||
// Capture the audio options as they were when we first mounted, because
|
||||
// we're not doing anything with the audio anyway so we don't need to
|
||||
// re-open the devices when they change (see below).
|
||||
const initialAudioOptions = useRef<CreateLocalTracksOptions["audio"]>();
|
||||
initialAudioOptions.current ??= muteStates.audio.enabled && {
|
||||
deviceId: devices.audioInput.selectedId,
|
||||
@@ -79,7 +82,9 @@ export const VideoPreview: FC<Props> = ({ matrixInfo, muteStates }) => {
|
||||
// request over with at the same time. But changing the audio settings
|
||||
// shouldn't cause this hook to recreate the track, which is why we
|
||||
// reference the initial values here.
|
||||
audio: initialAudioOptions.current,
|
||||
// We also pass in a clone because livekit mutates the object passed in,
|
||||
// which would cause the devices to be re-opened on the next render.
|
||||
audio: Object.assign({}, initialAudioOptions.current),
|
||||
video: muteStates.video.enabled && {
|
||||
deviceId: devices.videoInput.selectedId,
|
||||
},
|
||||
@@ -131,16 +136,16 @@ export const VideoPreview: FC<Props> = ({ matrixInfo, muteStates }) => {
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.previewButtons}>
|
||||
<MicButton
|
||||
muted={!muteStates.audio.enabled}
|
||||
onPress={onAudioPress}
|
||||
disabled={muteStates.audio.setEnabled === null}
|
||||
/>
|
||||
<VideoButton
|
||||
muted={!muteStates.video.enabled}
|
||||
onPress={onVideoPress}
|
||||
disabled={muteStates.video.setEnabled === null}
|
||||
/>
|
||||
<MicButton
|
||||
muted={!muteStates.audio.enabled}
|
||||
onPress={onAudioPress}
|
||||
disabled={muteStates.audio.setEnabled === null}
|
||||
/>
|
||||
<SettingsButton onPress={openSettings} />
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -25,12 +25,16 @@ import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { SyncState } from "matrix-js-sdk/src/sync";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||
|
||||
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||
import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||
import { setLocalStorageItem } from "../useLocalStorage";
|
||||
import { isLocalRoomId, createRoom, roomNameFromRoomId } from "../matrix-utils";
|
||||
import { translatedError } from "../TranslatedError";
|
||||
import { widget } from "../widget";
|
||||
import { useEnableE2EE } from "../settings/useSetting";
|
||||
import { getRoomSharedKeyLocalStorageKey } from "../e2ee/sharedKeyManagement";
|
||||
|
||||
const STATS_COLLECT_INTERVAL_TIME_MS = 10000;
|
||||
|
||||
@@ -67,6 +71,8 @@ export const useLoadGroupCall = (
|
||||
const { t } = useTranslation();
|
||||
const [state, setState] = useState<GroupCallStatus>({ kind: "loading" });
|
||||
|
||||
const [e2eeEnabled] = useEnableE2EE();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchOrCreateRoom = async (): Promise<Room> => {
|
||||
try {
|
||||
@@ -104,8 +110,17 @@ export const useLoadGroupCall = (
|
||||
const [, roomId] = await createRoom(
|
||||
client,
|
||||
roomNameFromRoomId(roomIdOrAlias),
|
||||
createPtt
|
||||
createPtt,
|
||||
e2eeEnabled ?? false
|
||||
);
|
||||
|
||||
if (e2eeEnabled) {
|
||||
setLocalStorageItem(
|
||||
getRoomSharedKeyLocalStorageKey(roomId),
|
||||
randomString(32)
|
||||
);
|
||||
}
|
||||
|
||||
// likewise, wait for the room
|
||||
await client.waitUntilRoomReadyForGroupCalls(roomId);
|
||||
return client.getRoom(roomId)!;
|
||||
@@ -194,7 +209,7 @@ export const useLoadGroupCall = (
|
||||
.then(fetchOrCreateGroupCall)
|
||||
.then((groupCall) => setState({ kind: "loaded", groupCall }))
|
||||
.catch((error) => setState({ kind: "failed", error }));
|
||||
}, [client, roomIdOrAlias, viaServers, createPtt, t]);
|
||||
}, [client, roomIdOrAlias, viaServers, createPtt, t, e2eeEnabled]);
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ limitations under the License.
|
||||
}
|
||||
|
||||
.settingsModal p {
|
||||
color: var(--secondary-content);
|
||||
color: var(--cpd-color-text-secondary);
|
||||
}
|
||||
|
||||
.tabContainer {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 New Vector Ltd
|
||||
Copyright 2022 - 2023 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -14,59 +14,49 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
import { useMemo, useState, useEffect, useCallback } from "react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { isE2EESupported } from "livekit-client";
|
||||
|
||||
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
||||
import {
|
||||
getLocalStorageItem,
|
||||
setLocalStorageItem,
|
||||
useLocalStorage,
|
||||
} from "../useLocalStorage";
|
||||
|
||||
type Setting<T> = [T, (value: T) => void];
|
||||
type DisableableSetting<T> = [T, ((value: T) => void) | null];
|
||||
|
||||
// Bus to notify other useSetting consumers when a setting is changed
|
||||
export const settingsBus = new EventEmitter();
|
||||
|
||||
const getSettingKey = (name: string): string => {
|
||||
export const getSettingKey = (name: string): string => {
|
||||
return `matrix-setting-${name}`;
|
||||
};
|
||||
// Like useState, but reads from and persists the value to localStorage
|
||||
const useSetting = <T>(name: string, defaultValue: T): Setting<T> => {
|
||||
export const useSetting = <T>(name: string, defaultValue: T): Setting<T> => {
|
||||
const key = useMemo(() => getSettingKey(name), [name]);
|
||||
|
||||
const [value, setValue] = useState<T>(() => {
|
||||
const item = localStorage.getItem(key);
|
||||
return item == null ? defaultValue : JSON.parse(item);
|
||||
});
|
||||
const [item, setItem] = useLocalStorage(key);
|
||||
|
||||
useEffect(() => {
|
||||
settingsBus.on(name, setValue);
|
||||
return () => {
|
||||
settingsBus.off(name, setValue);
|
||||
};
|
||||
}, [name, setValue]);
|
||||
const value = useMemo(
|
||||
() => (item == null ? defaultValue : JSON.parse(item)),
|
||||
[item, defaultValue]
|
||||
);
|
||||
const setValue = useCallback(
|
||||
(value: T) => {
|
||||
setItem(JSON.stringify(value));
|
||||
},
|
||||
[setItem]
|
||||
);
|
||||
|
||||
return [
|
||||
value,
|
||||
useCallback(
|
||||
(newValue: T) => {
|
||||
setValue(newValue);
|
||||
localStorage.setItem(key, JSON.stringify(newValue));
|
||||
settingsBus.emit(name, newValue);
|
||||
},
|
||||
[name, key, setValue]
|
||||
),
|
||||
];
|
||||
return [value, setValue];
|
||||
};
|
||||
|
||||
export const getSetting = <T>(name: string, defaultValue: T): T => {
|
||||
const item = localStorage.getItem(getSettingKey(name));
|
||||
const item = getLocalStorageItem(getSettingKey(name));
|
||||
return item === null ? defaultValue : JSON.parse(item);
|
||||
};
|
||||
|
||||
export const setSetting = <T>(name: string, newValue: T) => {
|
||||
localStorage.setItem(getSettingKey(name), JSON.stringify(newValue));
|
||||
settingsBus.emit(name, newValue);
|
||||
};
|
||||
export const setSetting = <T>(name: string, newValue: T) =>
|
||||
setLocalStorageItem(getSettingKey(name), JSON.stringify(newValue));
|
||||
|
||||
const canEnableSpatialAudio = () => {
|
||||
const { userAgent } = navigator;
|
||||
@@ -102,7 +92,7 @@ export const useOptInAnalytics = (): DisableableSetting<boolean | null> => {
|
||||
export const useEnableE2EE = (): DisableableSetting<boolean | null> => {
|
||||
const settingVal = useSetting<boolean | null>(
|
||||
"enable-end-to-end-encryption",
|
||||
false
|
||||
true
|
||||
);
|
||||
if (isE2EESupported()) return settingVal;
|
||||
|
||||
|
||||
@@ -44,12 +44,12 @@ limitations under the License.
|
||||
}
|
||||
|
||||
.tab > * {
|
||||
color: var(--secondary-content);
|
||||
color: var(--cpd-color-text-secondary);
|
||||
margin: 0 8px 0 0;
|
||||
}
|
||||
|
||||
.tab svg * {
|
||||
fill: var(--secondary-content);
|
||||
fill: var(--cpd-color-text-secondary);
|
||||
}
|
||||
|
||||
.tab > :last-child {
|
||||
@@ -57,15 +57,15 @@ limitations under the License.
|
||||
}
|
||||
|
||||
.tab.selected {
|
||||
background-color: var(--accent);
|
||||
background-color: var(--cpd-color-text-action-accent);
|
||||
}
|
||||
|
||||
.tab.selected * {
|
||||
color: var(--primary-content);
|
||||
color: var(--stopgap-color-on-solid-accent);
|
||||
}
|
||||
|
||||
.tab.selected svg * {
|
||||
fill: var(--primary-content);
|
||||
fill: var(--stopgap-color-on-solid-accent);
|
||||
}
|
||||
|
||||
.tab.disabled {
|
||||
|
||||
@@ -37,7 +37,7 @@ limitations under the License.
|
||||
}
|
||||
|
||||
.link {
|
||||
color: var(--links);
|
||||
color: var(--cpd-color-text-link-external);
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
@@ -46,7 +46,7 @@ limitations under the License.
|
||||
}
|
||||
|
||||
.primary {
|
||||
color: var(--accent);
|
||||
color: var(--cpd-color-text-action-accent);
|
||||
}
|
||||
|
||||
.overflowEllipsis {
|
||||
|
||||
59
src/useLocalStorage.ts
Normal file
59
src/useLocalStorage.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
Copyright 2023 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import EventEmitter from "events";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
type LocalStorageItem = ReturnType<typeof localStorage.getItem>;
|
||||
|
||||
// Bus to notify other useLocalStorage consumers when an item is changed
|
||||
export const localStorageBus = new EventEmitter();
|
||||
|
||||
// Like useState, but reads from and persists the value to localStorage
|
||||
export const useLocalStorage = (
|
||||
key: string
|
||||
): [LocalStorageItem, (value: string) => void] => {
|
||||
const [value, setValue] = useState<LocalStorageItem>(() =>
|
||||
localStorage.getItem(key)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
localStorageBus.on(key, setValue);
|
||||
return () => {
|
||||
localStorageBus.off(key, setValue);
|
||||
};
|
||||
}, [key, setValue]);
|
||||
|
||||
return [
|
||||
value,
|
||||
useCallback(
|
||||
(newValue: string) => {
|
||||
setValue(newValue);
|
||||
localStorage.setItem(key, newValue);
|
||||
localStorageBus.emit(key, newValue);
|
||||
},
|
||||
[key, setValue]
|
||||
),
|
||||
];
|
||||
};
|
||||
|
||||
export const getLocalStorageItem = (key: string): LocalStorageItem =>
|
||||
localStorage.getItem(key);
|
||||
|
||||
export const setLocalStorageItem = (key: string, value: string): void => {
|
||||
localStorage.setItem(key, value);
|
||||
localStorageBus.emit(key, value);
|
||||
};
|
||||
@@ -53,7 +53,7 @@ limitations under the License.
|
||||
bottom: -1px;
|
||||
content: "";
|
||||
border-radius: var(--tileRadius);
|
||||
box-shadow: inset 0 0 0 4px var(--accent) !important;
|
||||
box-shadow: inset 0 0 0 4px var(--cpd-color-border-accent) !important;
|
||||
opacity: 0;
|
||||
transition: opacity ease 0.15s;
|
||||
}
|
||||
@@ -77,8 +77,8 @@ limitations under the License.
|
||||
position: absolute;
|
||||
height: 24px;
|
||||
padding: 0 8px;
|
||||
color: var(--primary-content);
|
||||
background-color: var(--background-85);
|
||||
color: var(--cpd-color-text-primary);
|
||||
background-color: var(--stopgap-background-85);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -95,6 +95,10 @@ limitations under the License.
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.infoBubble > svg * {
|
||||
fill: var(--cpd-color-icon-primary);
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -103,8 +107,8 @@ limitations under the License.
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
|
||||
color: var(--primary-content);
|
||||
background-color: var(--background-85);
|
||||
color: var(--cpd-color-text-primary);
|
||||
background-color: var(--stopgap-background-85);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -154,7 +158,7 @@ limitations under the License.
|
||||
.videoMutedOverlay {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #21262c;
|
||||
background-color: var(--cpd-color-bg-subtle-secondary);
|
||||
}
|
||||
|
||||
.presenterLabel {
|
||||
@@ -162,7 +166,7 @@ limitations under the License.
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background-color: #17191c;
|
||||
background-color: var(--stopgap-background-85);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -40,9 +40,9 @@ limitations under the License.
|
||||
appearance: none;
|
||||
|
||||
background-color: transparent;
|
||||
--slider-color: var(--quinary-content);
|
||||
--slider-color: var(--cpd-color-bg-subtle-primary);
|
||||
--slider-height: 4px;
|
||||
--thumb-color: var(--accent);
|
||||
--thumb-color: var(--cpd-color-text-action-accent);
|
||||
--thumb-radius: 100%;
|
||||
--thumb-size: 16px;
|
||||
--thumb-margin-top: -6px;
|
||||
|
||||
18
test/environment.ts
Normal file
18
test/environment.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { TextEncoder } from "util";
|
||||
import JSDOMEnvironment_, {
|
||||
TestEnvironment as TestEnvironment_,
|
||||
} from "jest-environment-jsdom";
|
||||
import { JestEnvironmentConfig, EnvironmentContext } from "@jest/environment";
|
||||
|
||||
// This is a patched version of jsdom that adds TextEncoder, as a workaround for
|
||||
// https://github.com/jsdom/jsdom/issues/2524
|
||||
// Once that issue is resolved, this custom environment file can be deleted
|
||||
export default class JSDOMEnvironment extends JSDOMEnvironment_ {
|
||||
constructor(config: JestEnvironmentConfig, context: EnvironmentContext) {
|
||||
super(config, context);
|
||||
this.global.TextEncoder ??= TextEncoder;
|
||||
}
|
||||
}
|
||||
|
||||
export const TestEnvironment =
|
||||
TestEnvironment_ === JSDOMEnvironment_ ? JSDOMEnvironment : TestEnvironment_;
|
||||
@@ -34,7 +34,13 @@ describe("CallList", () => {
|
||||
|
||||
it("should show room", async () => {
|
||||
const rooms = [
|
||||
{ roomName: "Room #1", roomAlias: "#room-name:server.org" },
|
||||
{
|
||||
roomName: "Room #1",
|
||||
roomAlias: "#room-name:server.org",
|
||||
room: {
|
||||
roomId: "!roomId",
|
||||
},
|
||||
},
|
||||
] as GroupCallRoom[];
|
||||
|
||||
const result = renderComponent(rooms);
|
||||
|
||||
Reference in New Issue
Block a user