mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-20 20:49:20 +00:00
Compare commits
179 Commits
hs/remove-
...
tmp/test-n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b7abb6dd3 | ||
|
|
294e67a5ba | ||
|
|
6e3fd10268 | ||
|
|
0128f2f258 | ||
|
|
e23d8828ad | ||
|
|
613675553f | ||
|
|
6dcfdf8d39 | ||
|
|
4fc7fbe8fe | ||
|
|
f49355a51f | ||
|
|
c3ad7844fb | ||
|
|
1fa4a6f2e4 | ||
|
|
e25e6caa89 | ||
|
|
bff2b73c85 | ||
|
|
1dde54daa9 | ||
|
|
1d1289b7bf | ||
|
|
7ba7ddcdc5 | ||
|
|
31e59c1da1 | ||
|
|
7d444fc261 | ||
|
|
26ce899548 | ||
|
|
06d4206524 | ||
|
|
a16391c342 | ||
|
|
3506ca7feb | ||
|
|
5f78f544b7 | ||
|
|
d862466bb0 | ||
|
|
e636542b1e | ||
|
|
cda802a2e9 | ||
|
|
c218dc2f36 | ||
|
|
74d4556878 | ||
|
|
e68593ec47 | ||
|
|
e79a32ac63 | ||
|
|
3189bdba2a | ||
|
|
a1eb5a9770 | ||
|
|
723fa290ff | ||
|
|
d2e5514928 | ||
|
|
0f2e67dd60 | ||
|
|
d9e0c67315 | ||
|
|
497ee3c7f7 | ||
|
|
2f5f0978ad | ||
|
|
c8b30dd8a7 | ||
|
|
3d2663d740 | ||
|
|
2206ad287e | ||
|
|
402d028696 | ||
|
|
0f40cb77ad | ||
|
|
df92f4814b | ||
|
|
8bfdd1f455 | ||
|
|
d985bd3b3f | ||
|
|
54c669126f | ||
|
|
dc784e95cd | ||
|
|
acd4a1f179 | ||
|
|
c66b8c0fd3 | ||
|
|
cdde53cc44 | ||
|
|
d5bf65057a | ||
|
|
7e507d6310 | ||
|
|
00759e2431 | ||
|
|
9bfc08ab53 | ||
|
|
7a03adb054 | ||
|
|
50d30809ef | ||
|
|
123942901d | ||
|
|
9dcd7aaf50 | ||
|
|
6420e211c6 | ||
|
|
8c2844e0b5 | ||
|
|
939aaa39eb | ||
|
|
698a38a265 | ||
|
|
b5f4a07868 | ||
|
|
46a82b2c39 | ||
|
|
d6942bc71d | ||
|
|
2c33d65824 | ||
|
|
f521e260b6 | ||
|
|
89e33743ae | ||
|
|
dc904fdbd2 | ||
|
|
6776c2af35 | ||
|
|
e477c16a0b | ||
|
|
abf2ecd521 | ||
|
|
7d00f85abc | ||
|
|
6d5dc0dfb7 | ||
|
|
ba5da7e9af | ||
|
|
19d0f84f02 | ||
|
|
6b8dddfaaa | ||
|
|
5d8804d7e8 | ||
|
|
53fff37d5d | ||
|
|
2b6acb9cce | ||
|
|
79c40f198c | ||
|
|
e4bd9d7cf9 | ||
|
|
65cf8bf3ee | ||
|
|
ed269e0711 | ||
|
|
b822b9f80d | ||
|
|
f1fa814fa2 | ||
|
|
92afd5d63a | ||
|
|
25d0338f35 | ||
|
|
f9e3fe3176 | ||
|
|
de276b1fc3 | ||
|
|
9c57720852 | ||
|
|
a723f10d2c | ||
|
|
31be6cc804 | ||
|
|
0c66ec5d71 | ||
|
|
92813bd3ca | ||
|
|
e84ecc68b3 | ||
|
|
7807c44fdc | ||
|
|
1cbb6dff81 | ||
|
|
b993c5eb12 | ||
|
|
53565ddb76 | ||
|
|
00056a7cd9 | ||
|
|
4d6740cf2c | ||
|
|
77facd01e4 | ||
|
|
6c81f69590 | ||
|
|
b834d8f679 | ||
|
|
54149a496c | ||
|
|
0087e37f30 | ||
|
|
8558f0349f | ||
|
|
45564fd629 | ||
|
|
da63db26d6 | ||
|
|
f8f054a4f9 | ||
|
|
22678a7c71 | ||
|
|
8b19beae68 | ||
|
|
69ed91d6d4 | ||
|
|
d698705ffa | ||
|
|
a8a95c3f00 | ||
|
|
9d4cd211ed | ||
|
|
4fab1a25b4 | ||
|
|
2fe199de71 | ||
|
|
65cc583c8a | ||
|
|
43c81a2758 | ||
|
|
21b62dbd89 | ||
|
|
d09efa3cfe | ||
|
|
818abaac0f | ||
|
|
76ba02584d | ||
|
|
27620b9148 | ||
|
|
68867810f7 | ||
|
|
3495f5b472 | ||
|
|
260a1df21b | ||
|
|
44e5d2ea91 | ||
|
|
5e449bd03f | ||
|
|
cf0bb4676a | ||
|
|
97fd1f3696 | ||
|
|
f4883ac098 | ||
|
|
cc260811e2 | ||
|
|
a37c3bfb48 | ||
|
|
ea6f2952af | ||
|
|
cc58e4d429 | ||
|
|
e142493380 | ||
|
|
34cc50b84d | ||
|
|
84b2dc8680 | ||
|
|
e12f4fada0 | ||
|
|
8cd681c6cc | ||
|
|
82360c674c | ||
|
|
32ccfe8813 | ||
|
|
65e962bf1b | ||
|
|
28da8c48da | ||
|
|
f7c7f41afd | ||
|
|
cf174261c9 | ||
|
|
e36029c9c0 | ||
|
|
0469d8ef56 | ||
|
|
fc8da6ef58 | ||
|
|
4e1b4fae19 | ||
|
|
5c18868aa4 | ||
|
|
9ce16b68e9 | ||
|
|
ad9114a6e4 | ||
|
|
44e1c136df | ||
|
|
1bbbd519c7 | ||
|
|
51e4a3b14b | ||
|
|
8da38d173a | ||
|
|
13887a8943 | ||
|
|
03721f078b | ||
|
|
d146bedf05 | ||
|
|
63f595f958 | ||
|
|
279912d951 | ||
|
|
94e5d032a1 | ||
|
|
f249b7d463 | ||
|
|
e5117b962c | ||
|
|
720c400e5f | ||
|
|
0718774cef | ||
|
|
a2b452c1d8 | ||
|
|
465a784345 | ||
|
|
8de96878c0 | ||
|
|
b7b9771577 | ||
|
|
3885eefa4c | ||
|
|
9176e06195 | ||
|
|
38085baab8 | ||
|
|
770730ba6c |
@@ -37,7 +37,14 @@ module.exports = {
|
||||
"@typescript-eslint/promise-function-async": "error",
|
||||
"@typescript-eslint/require-await": "error",
|
||||
"@typescript-eslint/await-thenable": "error",
|
||||
// To help ensure that we get proper vite/rollup lazy loading (e.g. for matrix-js-sdk):
|
||||
"@typescript-eslint/consistent-type-imports": [
|
||||
"error",
|
||||
{ fixStyle: "inline-type-imports" },
|
||||
],
|
||||
// To encourage good usage of RxJS:
|
||||
"rxjs/no-exposed-subjects": "error",
|
||||
"rxjs/finnish": "error",
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Docker - Deploy
|
||||
name: Build and publish docker image
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
@@ -42,16 +42,16 @@ jobs:
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1
|
||||
uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5.6.1
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: ${{ inputs.docker_tags}}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1
|
||||
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0
|
||||
uses: docker/build-push-action@b32b51a8eda65d6793cd0494a773d4f6bcef32dc # v6.11.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Element Call - Build
|
||||
name: Build Element Call
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
@@ -28,9 +28,9 @@ jobs:
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
node-version-file: ".node-version"
|
||||
- name: Install dependencies
|
||||
run: "yarn install"
|
||||
run: "yarn install --frozen-lockfile"
|
||||
- name: Build
|
||||
run: "yarn run build"
|
||||
env:
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
VITE_APP_VERSION: ${{ inputs.vite_app_version }}
|
||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4
|
||||
with:
|
||||
name: build-output
|
||||
path: dist
|
||||
43
.github/workflows/build.yaml
vendored
43
.github/workflows/build.yaml
vendored
@@ -6,16 +6,18 @@ on:
|
||||
- opened
|
||||
- labeled
|
||||
paths-ignore:
|
||||
- ".github/**"
|
||||
- ".github/ISSUE_TEMPLATE/**"
|
||||
- ".github/CODEOWNERS"
|
||||
- "docs/**"
|
||||
push:
|
||||
branches: [livekit, full-mesh]
|
||||
paths-ignore:
|
||||
- ".github/**"
|
||||
- ".github/ISSUE_TEMPLATE/**"
|
||||
- ".github/CODEOWNERS"
|
||||
- "docs/**"
|
||||
jobs:
|
||||
build_element_call:
|
||||
uses: ./.github/workflows/element-call.yaml
|
||||
uses: ./.github/workflows/build-element-call.yaml
|
||||
with:
|
||||
vite_app_version: ${{ github.event.release.tag_name || github.sha }}
|
||||
secrets:
|
||||
@@ -24,3 +26,38 @@ jobs:
|
||||
SENTRY_URL: ${{ secrets.SENTRY_URL }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
deploy_develop:
|
||||
# Deploy livekit branch to call.element.dev after build completes
|
||||
if: github.ref == 'refs/heads/livekit'
|
||||
needs: build_element_call
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Deploy to call.element.dev
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ secrets.DEVELOP_DEPLOYMENT_TOKEN }}
|
||||
script: |
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
owner: 'element-hq',
|
||||
repo: 'element-call-webapp-deployments',
|
||||
workflow_id: 'deploy.yml',
|
||||
ref: 'main',
|
||||
inputs: {
|
||||
target: 'call.element.dev',
|
||||
version: '${{ github.sha }}'
|
||||
}
|
||||
})
|
||||
docker_for_develop:
|
||||
# Build docker and publish docker for livekit branch after build completes
|
||||
if: github.ref == 'refs/heads/livekit'
|
||||
needs: build_element_call
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
uses: ./.github/workflows/build-and-publish-docker.yaml
|
||||
with:
|
||||
artifact_run_id: ${{ github.run_id }}
|
||||
docker_tags: |
|
||||
type=sha,format=short,event=branch
|
||||
type=raw,value=latest-ci
|
||||
type=raw,value=latest-ci_{{date 'X' }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Netlify - Deploy
|
||||
name: Deploy to Netlify
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
24
.github/workflows/e2e.yml
vendored
24
.github/workflows/e2e.yml
vendored
@@ -1,24 +0,0 @@
|
||||
name: Run E2E tests
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["deploy"]
|
||||
types:
|
||||
- completed
|
||||
branches-ignore:
|
||||
- "livekit"
|
||||
jobs:
|
||||
e2e:
|
||||
name: E2E tests runs on Element Call
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out test private repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
repository: element-hq/static-call-participant
|
||||
ref: refs/heads/main
|
||||
path: static-call-participant
|
||||
token: ${{ secrets.GH_E2E_TEST_TOKEN }}
|
||||
- name: Build E2E Image
|
||||
run: "cd static-call-participant && docker build --no-cache --tag matrixdotorg/chrome-node-static-call-participant:latest ."
|
||||
- name: Run E2E tests in container
|
||||
run: "docker run --rm -v '${{ github.workspace }}/static-call-participant/callemshost-users.txt:/opt/app/callemshost-users.txt' matrixdotorg/chrome-node-static-call-participant:latest ./e2e.sh"
|
||||
4
.github/workflows/lint.yaml
vendored
4
.github/workflows/lint.yaml
vendored
@@ -12,9 +12,9 @@ jobs:
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
node-version-file: ".node-version"
|
||||
- name: Install dependencies
|
||||
run: "yarn install"
|
||||
run: "yarn install --frozen-lockfile"
|
||||
- name: Prettier
|
||||
run: "yarn run prettier:check"
|
||||
- name: i18n
|
||||
|
||||
6
.github/workflows/pr-deploy.yaml
vendored
6
.github/workflows/pr-deploy.yaml
vendored
@@ -1,4 +1,4 @@
|
||||
name: PR Preview Deployments
|
||||
name: Deploy previews for PRs
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build"]
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
needs: prdetails
|
||||
permissions:
|
||||
deployments: write
|
||||
uses: ./.github/workflows/netlify.yaml
|
||||
uses: ./.github/workflows/deploy-to-netlify.yaml
|
||||
with:
|
||||
artifact_run_id: ${{ github.event.workflow_run.id || github.run_id }}
|
||||
pr_number: ${{ needs.prdetails.outputs.pr_number }}
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
uses: ./.github/workflows/docker.yaml
|
||||
uses: ./.github/workflows/build-and-publish-docker.yaml
|
||||
with:
|
||||
artifact_run_id: ${{ github.event.workflow_run.id || github.run_id }}
|
||||
docker_tags: |
|
||||
|
||||
15
.github/workflows/publish.yaml
vendored
15
.github/workflows/publish.yaml
vendored
@@ -1,13 +1,8 @@
|
||||
name: Build & publish images to the package registry for tags
|
||||
name: Build & publish images to the package registry for releases
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_run:
|
||||
workflows: ["Build"]
|
||||
branches: [livekit]
|
||||
types:
|
||||
- completed
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
@@ -16,7 +11,7 @@ env:
|
||||
jobs:
|
||||
build_element_call:
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
uses: ./.github/workflows/element-call.yaml
|
||||
uses: ./.github/workflows/build-element-call.yaml
|
||||
with:
|
||||
vite_app_version: ${{ github.event.release.tag_name || github.sha }}
|
||||
secrets:
|
||||
@@ -51,7 +46,7 @@ jobs:
|
||||
run: |
|
||||
tar --numeric-owner --transform "s/dist/element-call-${TARBALL_VERSION}/" -cvzf element-call-${TARBALL_VERSION}.tar.gz dist
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
@@ -62,11 +57,9 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
uses: ./.github/workflows/docker.yaml
|
||||
uses: ./.github/workflows/build-and-publish-docker.yaml
|
||||
with:
|
||||
artifact_run_id: ${{ github.event.workflow_run.id || github.run_id }}
|
||||
docker_tags: |
|
||||
type=sha,format=short,event=branch
|
||||
type=semver,pattern=v{{version}}
|
||||
type=raw,value=latest-ci,enable={{is_default_branch}}
|
||||
type=raw,value=latest-ci_${{needs.publish_tarball.outputs.unix_time}},enable={{is_default_branch}}
|
||||
|
||||
6
.github/workflows/test.yaml
vendored
6
.github/workflows/test.yaml
vendored
@@ -14,13 +14,13 @@ jobs:
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
node-version-file: ".node-version"
|
||||
- name: Install dependencies
|
||||
run: "yarn install"
|
||||
run: "yarn install --frozen-lockfile"
|
||||
- name: Vitest
|
||||
run: "yarn run test:coverage"
|
||||
- name: Upload to codecov
|
||||
uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4
|
||||
uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
with:
|
||||
|
||||
4
.github/workflows/translations-download.yaml
vendored
4
.github/workflows/translations-download.yaml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
node-version-file: ".node-version"
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install --frozen-lockfile"
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7.0.5
|
||||
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7.0.6
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
branch: actions/localazy-download
|
||||
|
||||
1
.node-version
Normal file
1
.node-version
Normal file
@@ -0,0 +1 @@
|
||||
22
|
||||
11
Dockerfile
11
Dockerfile
@@ -1,4 +1,13 @@
|
||||
FROM alpine AS builder
|
||||
|
||||
COPY ./dist /dist
|
||||
|
||||
# Compress assets to work with nginx-gzip-static-module
|
||||
WORKDIR /dist/assets
|
||||
RUN gzip -k ../index.html *.js *.map *.css *.wasm *-app-*.json
|
||||
|
||||
FROM nginxinc/nginx-unprivileged:alpine
|
||||
|
||||
COPY ./dist /app
|
||||
COPY --from=builder ./dist /app
|
||||
|
||||
COPY config/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
6
LICENSE-COMMERCIAL
Normal file
6
LICENSE-COMMERCIAL
Normal file
@@ -0,0 +1,6 @@
|
||||
Licensees holding a valid commercial license with Element may use this
|
||||
software in accordance with the terms contained in a written agreement
|
||||
between you and Element.
|
||||
|
||||
To purchase a commercial license please contact our sales team at
|
||||
licensing@element.io
|
||||
22
README.md
22
README.md
@@ -92,6 +92,9 @@ work:
|
||||
experimental_features:
|
||||
# MSC3266: Room summary API. Used for knocking over federation
|
||||
msc3266_enabled: true
|
||||
# MSC4222 needed for syncv2 state_after. This allow clients to
|
||||
# correctly track the state of the room.
|
||||
msc4222_enabled: true
|
||||
|
||||
# The maximum allowed duration by which sent events can be delayed, as
|
||||
# per MSC4140.
|
||||
@@ -109,6 +112,10 @@ summary contains the room join rules. We need that to decide if the user gets
|
||||
prompted with the option to knock ("Request to join call"), a cannot join error or the
|
||||
join view.
|
||||
|
||||
MSC4222 allow clients to opt-in to a change of the sync v2 API that allows them
|
||||
to correctly track the state of the room. This is required by Element Call to
|
||||
track room state reliably.
|
||||
|
||||
Element Call requires a Livekit SFU alongside a [Livekit JWT
|
||||
service](https://github.com/element-hq/lk-jwt-service) to work. The url to the
|
||||
Livekit JWT service can either be configured in the config of Element Call
|
||||
@@ -213,7 +220,7 @@ To add a new translation key you can do these steps:
|
||||
|
||||
1. Add the new key entry to the code where the new key is used: `t("some_new_key")`
|
||||
1. Run `yarn i18n` to extract the new key and update the translation files. This
|
||||
will add a skeleton entry to the `locales/en-GB/app.json` file:
|
||||
will add a skeleton entry to the `locales/en/app.json` file:
|
||||
```jsonc
|
||||
{
|
||||
...
|
||||
@@ -221,7 +228,7 @@ To add a new translation key you can do these steps:
|
||||
...
|
||||
}
|
||||
```
|
||||
1. Update the skeleton entry in the `locales/en-GB/app.json` file with
|
||||
1. Update the skeleton entry in the `locales/en/app.json` file with
|
||||
the English translation:
|
||||
|
||||
```jsonc
|
||||
@@ -237,3 +244,14 @@ To add a new translation key you can do these steps:
|
||||
Usage and other technical details about the project can be found here:
|
||||
|
||||
[**Docs**](./docs/README.md)
|
||||
|
||||
## Copyright & License
|
||||
|
||||
Copyright 2021-2025 New Vector Ltd
|
||||
|
||||
This software is dual-licensed by New Vector Ltd (Element). It can be used either:
|
||||
|
||||
(1) for free under the terms of the GNU Affero General Public License (as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version); OR
|
||||
|
||||
(2) under the terms of a paid-for Element Commercial License agreement between you and Element (the terms of which may vary depending on what you and Element have agreed to).
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the Licenses is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the Licenses for the specific language governing permissions and limitations under the Licenses.
|
||||
|
||||
@@ -25,6 +25,9 @@ trusted_key_servers:
|
||||
experimental_features:
|
||||
# MSC3266: Room summary API. Used for knocking over federation
|
||||
msc3266_enabled: true
|
||||
# MSC4222 needed for syncv2 state_after. This allow clients to
|
||||
# correctly track the state of the room.
|
||||
msc4222_enabled: true
|
||||
|
||||
# The maximum allowed duration by which sent events can be delayed, as
|
||||
# per MSC4140. Must be a positive value if set. Defaults to no
|
||||
|
||||
@@ -17,5 +17,9 @@
|
||||
},
|
||||
"rageshake": {
|
||||
"submit_url": "https://element.io/bugreports/submit"
|
||||
},
|
||||
"sentry": {
|
||||
"environment": "netlify-pr-preview",
|
||||
"DSN": "https://b1e328d49be3402ba96101338989fb35@sentry.tools.element.io/41"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
<Location "/">
|
||||
# disable cache entriely by default (apart from Etag which is accurate enough)
|
||||
Header add Cache-Control "private no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"
|
||||
Header add Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"
|
||||
CacheDisable on
|
||||
ExpiresActive off
|
||||
|
||||
|
||||
@@ -25,230 +25,39 @@ There are two formats for Element Call urls.
|
||||
```
|
||||
|
||||
With this format the livekit alias that will be used is the `<room_name>`.
|
||||
All ppl connecting to this url will end up in the same unencrypted room.
|
||||
All people connecting to this URL will end up in the same unencrypted room.
|
||||
This does not scale, is super unsecure
|
||||
(ppl could end up in the same room by accident) and it also is not really
|
||||
(people could end up in the same room by accident) and it also is not really
|
||||
possible to support encryption.
|
||||
The url parameters are spit into two categories: **general** and **widget related**.
|
||||
|
||||
## Widget related params
|
||||
## Parameters
|
||||
|
||||
**widgetId**
|
||||
The id used by the widget. The presence of this parameter implies that element
|
||||
call will not connect to a homeserver directly and instead tries to establish
|
||||
postMessage communication via the `parentUrl`.
|
||||
|
||||
```ts
|
||||
widgetId: string | null;
|
||||
```
|
||||
|
||||
**parentUrl**
|
||||
The url used to send widget action postMessages. This should be the domain of
|
||||
the client or the webview the widget is hosted in. (in case the widget is not
|
||||
in an Iframe but in a dedicated webview we send the postMessages same webview
|
||||
the widget lives in. Filtering is done in the widget so it ignores the messages
|
||||
it receives from itself)
|
||||
|
||||
```ts
|
||||
parentUrl: string | null;
|
||||
```
|
||||
|
||||
**userId**
|
||||
The user's ID (only used in matryoshka mode).
|
||||
|
||||
```ts
|
||||
userId: string | null;
|
||||
```
|
||||
|
||||
**deviceId**
|
||||
The device's ID (only used in matryoshka mode).
|
||||
|
||||
```ts
|
||||
deviceId: string | null;
|
||||
```
|
||||
|
||||
**baseUrl**
|
||||
The base URL of the homeserver to use for media lookups in matryoshka mode.
|
||||
|
||||
```ts
|
||||
baseUrl: string | null;
|
||||
```
|
||||
|
||||
### General url parameters
|
||||
|
||||
**roomId**
|
||||
Anything about what room we're pointed to should be from useRoomIdentifier which
|
||||
parses the path and resolves alias with respect to the default server name, however
|
||||
roomId is an exception as we need the room ID in embedded (matroyska) mode, and not
|
||||
the room alias (or even the via params because we are not trying to join it). This
|
||||
is also not validated, where it is in useRoomIdentifier().
|
||||
|
||||
```ts
|
||||
roomId: string | null;
|
||||
```
|
||||
|
||||
**confineToRoom**
|
||||
Whether the app should keep the user confined to the current call/room.
|
||||
|
||||
```ts
|
||||
confineToRoom: boolean; (default: false)
|
||||
```
|
||||
|
||||
**appPrompt**
|
||||
Whether upon entering a room, the user should be prompted to launch the
|
||||
native mobile app. (Affects only Android and iOS.)
|
||||
|
||||
The app prompt must also be enabled in the config for this to take effect.
|
||||
|
||||
```ts
|
||||
appPrompt: boolean; (default: true)
|
||||
```
|
||||
|
||||
**preload**
|
||||
Whether the app should pause before joining the call until it sees an
|
||||
io.element.join widget action, allowing it to be preloaded.
|
||||
|
||||
```ts
|
||||
preload: boolean; (default: false)
|
||||
```
|
||||
|
||||
**hideHeader**
|
||||
Whether to hide the room header when in a call.
|
||||
|
||||
```ts
|
||||
hideHeader: boolean; (default: false)
|
||||
```
|
||||
|
||||
**showControls**
|
||||
Whether to show the buttons to mute, screen-share, invite, hangup are shown
|
||||
when in a call.
|
||||
|
||||
```ts
|
||||
showControls: boolean; (default: true)
|
||||
```
|
||||
|
||||
**hideScreensharing**
|
||||
Whether to hide the screen-sharing button.
|
||||
|
||||
```ts
|
||||
hideScreensharing: boolean; (default: false)
|
||||
```
|
||||
|
||||
**enableE2EE** (Deprecated)
|
||||
Whether to use end-to-end encryption. This is a legacy flag for the full mesh branch.
|
||||
It is not used on the livekit branch and has no impact there!
|
||||
|
||||
```ts
|
||||
enableE2EE: boolean; (default: true)
|
||||
```
|
||||
|
||||
**perParticipantE2EE**
|
||||
Whether to use per participant encryption.
|
||||
Keys will be exchanged over encrypted matrix room messages.
|
||||
|
||||
```ts
|
||||
perParticipantE2EE: boolean; (default: false)
|
||||
```
|
||||
|
||||
**password**
|
||||
E2EE password when using a shared secret.
|
||||
(For individual sender keys in embedded mode this is not required.)
|
||||
|
||||
```ts
|
||||
password: string | null;
|
||||
```
|
||||
|
||||
**displayName**
|
||||
The display name to use for auto-registration.
|
||||
|
||||
```ts
|
||||
displayName: string | null;
|
||||
```
|
||||
|
||||
**lang**
|
||||
The BCP 47 code of the language the app should use.
|
||||
|
||||
```ts
|
||||
lang: string | null;
|
||||
```
|
||||
|
||||
**fonts**
|
||||
The font/fonts which the interface should use.
|
||||
There can be multiple font url parameters: `?font=font-one&font=font-two...`
|
||||
|
||||
```ts
|
||||
font: string;
|
||||
font: string;
|
||||
...
|
||||
```
|
||||
|
||||
**fontScale**
|
||||
The factor by which to scale the interface's font size.
|
||||
|
||||
```ts
|
||||
fontScale: number | null;
|
||||
```
|
||||
|
||||
**analyticsID**
|
||||
The Posthog analytics ID. It is only available if the user has given consent for
|
||||
sharing telemetry in element web.
|
||||
|
||||
```ts
|
||||
analyticsID: string | null;
|
||||
```
|
||||
|
||||
**allowIceFallback**
|
||||
Whether the app is allowed to use fallback STUN servers for ICE in case the
|
||||
user's homeserver doesn't provide any.
|
||||
|
||||
```ts
|
||||
allowIceFallback: boolean; (default: false)
|
||||
```
|
||||
|
||||
**skipLobby**
|
||||
Setting this flag skips the lobby and brings you in the call directly.
|
||||
In the widget this can be combined with preload to pass the device settings
|
||||
with the join widget action.
|
||||
|
||||
```ts
|
||||
skipLobby: boolean; (default: false)
|
||||
```
|
||||
|
||||
**returnToLobby**
|
||||
Setting this flag makes element call show the lobby in widget mode after leaving
|
||||
a call.
|
||||
This is useful for video rooms.
|
||||
If set to false, the widget will show a blank page after leaving the call.
|
||||
|
||||
```ts
|
||||
returnToLobby: boolean; (default: false)
|
||||
```
|
||||
|
||||
**theme**
|
||||
The theme to use for element call.
|
||||
can be "light", "dark", "light-high-contrast" or "dark-high-contrast".
|
||||
If not set element call will use the dark theme.
|
||||
|
||||
```ts
|
||||
theme: string | null;
|
||||
```
|
||||
|
||||
**viaServers**
|
||||
This defines the homeserver that is going to be used when joining a room.
|
||||
It has to be set to a non default value for links to rooms
|
||||
that are not on the default homeserver,
|
||||
that is in use for the current user.
|
||||
|
||||
```ts
|
||||
viaServers: string; (default: undefined)
|
||||
```
|
||||
|
||||
**homeserver**
|
||||
This defines the homeserver that is going to be used when registering
|
||||
a new (guest) user.
|
||||
This can be user to configure a non default guest user server when
|
||||
creating a spa link.
|
||||
|
||||
```ts
|
||||
homeserver: string; (default: undefined)
|
||||
```
|
||||
| Name | Values | Required for widget | Required for SPA | Description |
|
||||
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `allowIceFallback` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Allows use of fallback STUN servers for ICE if the user's homeserver doesn’t provide any. |
|
||||
| `analyticsID` | Posthog analytics ID | No | No | Available only with user's consent for sharing telemetry in Element Web. |
|
||||
| `appPrompt` | `true` or `false` | No, defaults to `true` | No, defaults to `true` | Prompts the user to launch the native mobile app upon entering a room, applicable only on Android and iOS, and must be enabled in config. |
|
||||
| `baseUrl` | | Yes | Not applicable | The base URL of the homeserver to use for media lookups. |
|
||||
| `confineToRoom` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Keeps the user confined to the current call/room. |
|
||||
| `deviceId` | Matrix device ID | Yes | Not applicable | The Matrix device ID for the widget host. |
|
||||
| `displayName` | | No | No | Display name used for auto-registration. |
|
||||
| `enableE2EE` (deprecated) | `true` or `false` | No, defaults to `true` | No, defaults to `true` | Legacy flag to enable end-to-end encryption, not used in the `livekit` branch. |
|
||||
| `fontScale` | A decimal number such as `0.9` | No, defaults to `1.0` | No, defaults to `1.0` | Factor by which to scale the interface's font size. |
|
||||
| `fonts` | | No | No | Defines the font(s) used by the interface. Multiple font parameters can be specified: `?font=font-one&font=font-two...`. |
|
||||
| `hideHeader` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Hides the room header when in a call. |
|
||||
| `hideScreensharing` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Hides the screen-sharing button. |
|
||||
| `homeserver` | | Not applicable | No | Homeserver for registering a new (guest) user, configures non-default guest user server when creating a spa link. |
|
||||
| `intent` | `start_call` or `join_existing` | No, defaults to `start_call` | No, defaults to `start_call` | The intent of the user with respect to the call. e.g. if they clicked a Start Call button, this would be `start_call`. If it was a Join Call button, it would be `join_existing`. |
|
||||
| `lang` | [BCP 47](https://www.rfc-editor.org/info/bcp47) code | No | No | The language the app should use. |
|
||||
| `parentUrl` | | Yes | Not applicable | The url used to send widget action postMessages. This should be the domain of the client or the webview the widget is hosted in. (in case the widget is not in an Iframe but in a dedicated webview we send the postMessages same WebView the widget lives in. Filtering is done in the widget so it ignores the messages it receives from itself) |
|
||||
| `password` | | No | No | E2EE password when using a shared secret. (For individual sender keys in embedded mode this is not required.) |
|
||||
| `perParticipantE2EE` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Enables per participant encryption with Keys exchanged over encrypted matrix room messages. |
|
||||
| `preload` | `true` or `false` | No, defaults to `false` | Not applicable | Pauses app before joining a call until an `io.element.join` widget action is seen, allowing preloading. |
|
||||
| `returnToLobby` | `true` or `false` | No, defaults to `false` | Not applicable | Displays the lobby in widget mode after leaving a call; shows a blank page if set to `false`. Useful for video rooms. |
|
||||
| `roomId` | [Matrix Room ID](https://spec.matrix.org/v1.12/appendices/#room-ids) | Yes | No | Anything about what room we're pointed to should be from useRoomIdentifier which parses the path and resolves alias with respect to the default server name, however roomId is an exception as we need the room ID in embedded widget mode, and not the room alias (or even the via params because we are not trying to join it). This is also not validated, where it is in `useRoomIdentifier()`. |
|
||||
| `showControls` | `true` or `false` | No, defaults to `true` | No, defaults to `true` | Displays controls like mute, screen-share, invite, and hangup buttons during a call. |
|
||||
| `skipLobby` (deprecated: use `intent` instead) | `true` or `false` | No. If `intent` is explicitly `start_call` then defaults to `true`. Otherwise defaults to `false` | No, defaults to `false` | Skips the lobby to join a call directly, can be combined with preload in widget. When `true` the audio and video inputs will be muted by default. (This means there currently is no way to start without muted video if one wants to skip the lobby. Also not in widget mode.) |
|
||||
| `theme` | One of: `light`, `dark`, `light-high-contrast`, `dark-high-contrast` | No, defaults to `dark` | No, defaults to `dark` | UI theme to use. |
|
||||
| `userId` | [Matrix User Identifier](https://spec.matrix.org/v1.12/appendices/#user-identifiers) | Yes | Not applicable | The Matrix user ID. |
|
||||
| `viaServers` | Comma separated list of [Matrix Server Names](https://spec.matrix.org/v1.12/appendices/#server-name) | Not applicable | No | Homeserver for joining a room, non-empty value required for rooms not on the user’s default homeserver. |
|
||||
| `widgetId` | [MSC2774](https://github.com/matrix-org/matrix-spec-proposals/pull/2774) format widget ID | Yes | Not applicable | The id used by the widget. The presence of this parameter implies that element call will not connect to a homeserver directly and instead tries to establish postMessage communication via the `parentUrl`. |
|
||||
|
||||
@@ -21,7 +21,7 @@ export default {
|
||||
},
|
||||
],
|
||||
},
|
||||
locales: ["en-GB"],
|
||||
locales: ["en"],
|
||||
output: "locales/$LOCALE/$NAMESPACE.json",
|
||||
input: ["src/**/*.{ts,tsx}"],
|
||||
sort: true,
|
||||
|
||||
@@ -8,10 +8,11 @@
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0"
|
||||
/>
|
||||
<title><%- title %></title>
|
||||
<title>Element Call</title>
|
||||
<script>
|
||||
window.global = window;
|
||||
</script>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</head>
|
||||
|
||||
<!-- The default class is: .no-theme {display: none}. It will be overwritten once the app is loaded. -->
|
||||
@@ -7,13 +7,13 @@
|
||||
"features": ["plural_postfix_us", "filter_untranslated"],
|
||||
"files": [
|
||||
{
|
||||
"pattern": "locales/en-GB/*.json",
|
||||
"pattern": "locales/en/*.json",
|
||||
"lang": "inherited"
|
||||
},
|
||||
{
|
||||
"group": "existing",
|
||||
"pattern": "locales/*/*.json",
|
||||
"excludes": ["locales/en-GB/*.json"],
|
||||
"excludes": ["locales/en/*.json"],
|
||||
"lang": "${autodetectLang}"
|
||||
}
|
||||
]
|
||||
@@ -25,9 +25,6 @@
|
||||
"output": "locales/${langLsrDash}/${file}"
|
||||
}
|
||||
],
|
||||
"includeSourceLang": "${includeSourceLang|false}",
|
||||
"langAliases": {
|
||||
"en": "en_GB"
|
||||
}
|
||||
"includeSourceLang": "${includeSourceLang|false}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,6 @@
|
||||
"developer_tab_title": "Разработчик",
|
||||
"feedback_tab_h4": "Изпрати обратна връзка",
|
||||
"feedback_tab_send_logs_label": "Включи debug логове",
|
||||
"more_tab_title": "Още",
|
||||
"speaker_device_selection_label": "Говорител"
|
||||
},
|
||||
"unauthenticated_view_body": "Все още не сте регистрирани? <2>Създайте акаунт</2>",
|
||||
|
||||
@@ -27,7 +27,9 @@
|
||||
"settings": "Nastavení",
|
||||
"username": "Uživatelské jméno"
|
||||
},
|
||||
"full_screen_view_description": "<0>Odeslání ladících záznamů nám pomůže diagnostikovat problém.</0>",
|
||||
"error": {
|
||||
"generic_description": "Odeslání ladících záznamů nám pomůže diagnostikovat problém."
|
||||
},
|
||||
"full_screen_view_h1": "<0>Oops, něco se pokazilo.</0>",
|
||||
"header_label": "Domov Element Call",
|
||||
"join_existing_call_modal": {
|
||||
@@ -60,12 +62,9 @@
|
||||
"return_home_button": "Vrátit se na domácí obrazovku",
|
||||
"screenshare_button_label": "Sdílet obrazovku",
|
||||
"settings": {
|
||||
"developer_settings_label": "Vývojářské nastavení",
|
||||
"developer_settings_label_description": "Zobrazit vývojářské nastavení.",
|
||||
"developer_tab_title": "Vývojář",
|
||||
"feedback_tab_h4": "Dát feedback",
|
||||
"feedback_tab_send_logs_label": "Zahrnout ladící záznamy",
|
||||
"more_tab_title": "Více",
|
||||
"speaker_device_selection_label": "Reproduktor"
|
||||
},
|
||||
"unauthenticated_view_body": "Nejste registrovaní? <2>Vytvořit účet</2>",
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
"browser_media_e2ee_unsupported": "Dein Webbrowser unterstützt keine Medien-Ende-zu-Ende-Verschlüsselung. Unterstützte Browser sind Chrome, Safari, Firefox >=117",
|
||||
"browser_media_e2ee_unsupported_heading": "Inkompatibler Browser",
|
||||
"call_ended_view": {
|
||||
"body": "Deine Verbindung wurde getrennt",
|
||||
"create_account_button": "Konto erstellen",
|
||||
"create_account_prompt": "<0>Warum vergibst du nicht abschließend ein Passwort, um dein Konto zu erhalten?</0><1>Du kannst deinen Namen behalten und ein Profilbild für zukünftige Anrufe festlegen.</1>",
|
||||
"feedback_done": "<0>Danke für deine Rückmeldung!</0>",
|
||||
@@ -68,10 +67,18 @@
|
||||
"username": "Benutzername",
|
||||
"video": "Video"
|
||||
},
|
||||
"crypto_version": "Krypto-Version:{{version}}",
|
||||
"device_id": "Geräte-ID: {{id}}",
|
||||
"developer_mode": {
|
||||
"crypto_version": "Krypto-Version: {{version}}",
|
||||
"device_id": "Geräte-ID: {{id}}",
|
||||
"duplicate_tiles_label": "Anzahl zusätzlicher Kachelkopien pro Teilnehmer",
|
||||
"hostname": "Hostname: {{hostname}}",
|
||||
"matrix_id": "Matrix-ID: {{id}}"
|
||||
},
|
||||
"disconnected_banner": "Die Verbindung zum Server wurde getrennt.",
|
||||
"full_screen_view_description": "<0>Übermittelte Problemberichte helfen uns, Fehler zu beheben.</0>",
|
||||
"error": {
|
||||
"connection_lost_description": "Deine Verbindung wurde getrennt",
|
||||
"generic_description": "Übermittelte Problemberichte helfen uns, Fehler zu beheben."
|
||||
},
|
||||
"full_screen_view_h1": "<0>Hoppla, etwas ist schiefgelaufen.</0>",
|
||||
"group_call_loader": {
|
||||
"banned_body": "Du wurdest aus dem Raum verbannt.",
|
||||
@@ -111,7 +118,6 @@
|
||||
"login_auth_links_prompt": "Noch nicht registriert?",
|
||||
"login_subheading": "Weiter zu Element",
|
||||
"login_title": "Anmelden",
|
||||
"matrix_id": "Matrix-ID: {{id}}",
|
||||
"microphone_off": "Mikrofon aus",
|
||||
"microphone_on": "Mikrofon an",
|
||||
"mute_microphone_button_label": "Mikrofon stumm schalten",
|
||||
@@ -146,29 +152,20 @@
|
||||
"effect_volume_description": "Lautstärke anpassen, mit der Reaktionen und Handmeldungen abgespielt werden",
|
||||
"effect_volume_label": "Lautstärke der Soundeffekte"
|
||||
},
|
||||
"developer_settings_label": "Entwicklereinstellungen",
|
||||
"developer_settings_label_description": "Zeige die Entwicklereinstellungen im Einstellungsfenster.",
|
||||
"developer_tab_title": "Entwickler",
|
||||
"duplicate_tiles_label": "Anzahl zusätzlicher Kachelkopien pro Teilnehmer",
|
||||
"feedback_tab_body": "Falls du auf Probleme stößt oder einfach nur eine Rückmeldung geben möchtest, sende uns bitte eine kurze Beschreibung.",
|
||||
"feedback_tab_description_label": "Deine Rückmeldung",
|
||||
"feedback_tab_h4": "Rückmeldung geben",
|
||||
"feedback_tab_send_logs_label": "Debug-Protokolle anhängen",
|
||||
"feedback_tab_thank_you": "Danke, wir haben deine Rückmeldung erhalten!",
|
||||
"feedback_tab_title": "Rückmeldung",
|
||||
"more_tab_title": "Mehr",
|
||||
"opt_in_description": "<0></0><1></1>Du kannst deine Zustimmung durch Abwählen dieses Kästchens zurückziehen. Falls du dich aktuell in einem Anruf befindest, wird diese Einstellung nach dem Ende des Anrufs wirksam.",
|
||||
"preferences_tab": {
|
||||
"reactions_play_sound_description": "Einen Soundeffekt abspielen, wenn jemand eine Reaktion sendet",
|
||||
"reactions_play_sound_label": "Reaktionstöne abspielen",
|
||||
"reactions_show_description": "Zeige eine Animation, wenn jemand eine Reaktion sendet.",
|
||||
"reactions_show_label": "Reaktionen anzeigen",
|
||||
"reactions_title": "Reaktionen"
|
||||
"reactions_show_label": "Reaktionen anzeigen"
|
||||
},
|
||||
"preferences_tab_body": "Hier können zusätzliche Optionen für individuelle Anforderungen eingestellt werden",
|
||||
"preferences_tab_h4": "Einstellungen",
|
||||
"preferences_tab_show_hand_raised_timer_description": "Einen Timer zur Handmeldung anzeigen",
|
||||
"preferences_tab_show_hand_raised_timer_label": "Dauer der Handmeldung anzeigen",
|
||||
"speaker_device_selection_label": "Lautsprecher"
|
||||
},
|
||||
"star_rating_input_label_one": "{{count}} Stern",
|
||||
@@ -191,6 +188,7 @@
|
||||
"expand": "Erweitern",
|
||||
"mute_for_me": "Für mich stumm schalten",
|
||||
"muted_for_me": "Für mich stumm geschaltet",
|
||||
"volume": "Lautstärke"
|
||||
"volume": "Lautstärke",
|
||||
"waiting_for_media": "Warten auf Medien..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,9 @@
|
||||
"username": "Όνομα χρήστη",
|
||||
"video": "Βίντεο"
|
||||
},
|
||||
"full_screen_view_description": "<0>Η υποβολή αρχείων καταγραφής σφαλμάτων θα μας βοηθήσει να εντοπίσουμε το πρόβλημα.</0>",
|
||||
"error": {
|
||||
"generic_description": "Η υποβολή αρχείων καταγραφής σφαλμάτων θα μας βοηθήσει να εντοπίσουμε το πρόβλημα."
|
||||
},
|
||||
"full_screen_view_h1": "<0>Ωχ, κάτι πήγε στραβά.</0>",
|
||||
"header_label": "Element Κεντρική Οθόνη Κλήσεων",
|
||||
"join_existing_call_modal": {
|
||||
@@ -67,8 +69,6 @@
|
||||
"return_home_button": "Επιστροφή στην αρχική οθόνη",
|
||||
"screenshare_button_label": "Κοινή χρήση οθόνης",
|
||||
"settings": {
|
||||
"developer_settings_label": "Ρυθμίσεις προγραμματιστή",
|
||||
"developer_settings_label_description": "Εμφάνιση ρυθμίσεων προγραμματιστή στο παράθυρο ρυθμίσεων.",
|
||||
"developer_tab_title": "Προγραμματιστής",
|
||||
"feedback_tab_body": "Εάν αντιμετωπίζετε προβλήματα ή απλά θέλετε να μας δώσετε κάποια σχόλια, παρακαλούμε στείλτε μας μια σύντομη περιγραφή παρακάτω.",
|
||||
"feedback_tab_description_label": "Τα σχόλιά σας",
|
||||
@@ -76,7 +76,6 @@
|
||||
"feedback_tab_send_logs_label": "Να συμπεριληφθούν αρχεία καταγραφής",
|
||||
"feedback_tab_thank_you": "Ευχαριστούμε, λάβαμε τα σχόλιά σας!",
|
||||
"feedback_tab_title": "Ανατροφοδότηση",
|
||||
"more_tab_title": "Περισσότερα",
|
||||
"opt_in_description": "<0></0><1></1>Μπορείτε να ανακαλέσετε τη συγκατάθεσή σας αποεπιλέγοντας αυτό το πλαίσιο. Εάν βρίσκεστε σε κλήση, η ρύθμιση αυτή θα τεθεί σε ισχύ στο τέλος της.",
|
||||
"speaker_device_selection_label": "Ηχείο"
|
||||
},
|
||||
|
||||
@@ -28,11 +28,7 @@
|
||||
"text": "Ready to join?",
|
||||
"title": "Select app"
|
||||
},
|
||||
"application_opened_another_tab": "This application has been opened in another tab.",
|
||||
"browser_media_e2ee_unsupported": "Your web browser does not support media end-to-end encryption. Supported Browsers are Chrome, Safari, Firefox >=117",
|
||||
"browser_media_e2ee_unsupported_heading": "Incompatible Browser",
|
||||
"call_ended_view": {
|
||||
"body": "You were disconnected from the call",
|
||||
"create_account_button": "Create account",
|
||||
"create_account_prompt": "<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>",
|
||||
"feedback_done": "<0>Thanks for your feedback!</0>",
|
||||
@@ -48,13 +44,10 @@
|
||||
"audio": "Audio",
|
||||
"avatar": "Avatar",
|
||||
"back": "Back",
|
||||
"camera": "Camera",
|
||||
"display_name": "Display name",
|
||||
"encrypted": "Encrypted",
|
||||
"error": "Error",
|
||||
"home": "Home",
|
||||
"loading": "Loading…",
|
||||
"microphone": "Microphone",
|
||||
"next": "Next",
|
||||
"options": "Options",
|
||||
"password": "Password",
|
||||
@@ -63,26 +56,43 @@
|
||||
"reaction": "Reaction",
|
||||
"reactions": "Reactions",
|
||||
"settings": "Settings",
|
||||
"something_went_wrong": "Something went wrong",
|
||||
"unencrypted": "Not encrypted",
|
||||
"username": "Username",
|
||||
"video": "Video"
|
||||
},
|
||||
"crypto_version": "Crypto version: {{version}}",
|
||||
"device_id": "Device ID: {{id}}",
|
||||
"developer_mode": {
|
||||
"crypto_version": "Crypto version: {{version}}",
|
||||
"debug_tile_layout_label": "Debug tile layout",
|
||||
"device_id": "Device ID: {{id}}",
|
||||
"duplicate_tiles_label": "Number of additional tile copies per participant",
|
||||
"hostname": "Hostname: {{hostname}}",
|
||||
"livekit_server_info": "LiveKit Server Info",
|
||||
"livekit_sfu": "LiveKit SFU: {{url}}",
|
||||
"matrix_id": "Matrix ID: {{id}}",
|
||||
"show_connection_stats": "Show connection statistics",
|
||||
"show_non_member_tiles": "Show tiles for non-member media"
|
||||
},
|
||||
"disconnected_banner": "Connectivity to the server has been lost.",
|
||||
"full_screen_view_description": "<0>Submitting debug logs will help us track down the problem.</0>",
|
||||
"full_screen_view_h1": "<0>Oops, something's gone wrong.</0>",
|
||||
"error": {
|
||||
"call_not_found": "Call not found",
|
||||
"call_not_found_description": "<0>That link doesn't appear to belong to any existing call. Check that you have the right link, or <1>create a new one</1>.</0>",
|
||||
"connection_lost": "Connection lost",
|
||||
"connection_lost_description": "You were disconnected from the call.",
|
||||
"e2ee_unsupported": "Incompatible browser",
|
||||
"e2ee_unsupported_description": "Your web browser does not support encrypted calls. Supported browsers include Chrome, Safari, and Firefox 117+.",
|
||||
"generic": "Something went wrong",
|
||||
"generic_description": "Submitting debug logs will help us track down the problem.",
|
||||
"open_elsewhere": "Opened in another tab",
|
||||
"open_elsewhere_description": "{{brand}} has been opened in another tab. If that doesn't sound right, try reloading the page."
|
||||
},
|
||||
"group_call_loader": {
|
||||
"banned_body": "You have been banned from the room.",
|
||||
"banned_heading": "Banned",
|
||||
"call_ended_body": "You have been removed from the call.",
|
||||
"call_ended_heading": "Call ended",
|
||||
"failed_heading": "Failed to join",
|
||||
"failed_text": "Call not found or is not accessible.",
|
||||
"knock_reject_body": "Your request to join was declined.",
|
||||
"knock_reject_heading": "Access denied",
|
||||
"reason": "Reason"
|
||||
"reason": "Reason: {{reason}}"
|
||||
},
|
||||
"hangup_button_label": "End call",
|
||||
"header_label": "Element Call Home",
|
||||
@@ -111,7 +121,6 @@
|
||||
"login_auth_links_prompt": "Not registered yet?",
|
||||
"login_subheading": "To continue to Element",
|
||||
"login_title": "Login",
|
||||
"matrix_id": "Matrix ID: {{id}}",
|
||||
"microphone_off": "Microphone off",
|
||||
"microphone_on": "Microphone on",
|
||||
"mute_microphone_button_label": "Mute microphone",
|
||||
@@ -143,35 +152,40 @@
|
||||
"screenshare_button_label": "Share screen",
|
||||
"settings": {
|
||||
"audio_tab": {
|
||||
"effect_volume_description": "Adjust the volume at which reactions and hand raised effects play",
|
||||
"effect_volume_description": "Adjust the volume at which reactions and hand raised effects play.",
|
||||
"effect_volume_label": "Sound effect volume"
|
||||
},
|
||||
"developer_settings_label": "Developer Settings",
|
||||
"developer_settings_label_description": "Expose developer settings in the settings window.",
|
||||
"developer_tab_title": "Developer",
|
||||
"duplicate_tiles_label": "Number of additional tile copies per participant",
|
||||
"devices": {
|
||||
"camera": "Camera",
|
||||
"camera_numbered": "Camera {{n}}",
|
||||
"default": "Default",
|
||||
"default_named": "Default <2>({{name}})</2>",
|
||||
"microphone": "Microphone",
|
||||
"microphone_numbered": "Microphone {{n}}",
|
||||
"speaker": "Speaker",
|
||||
"speaker_numbered": "Speaker {{n}}"
|
||||
},
|
||||
"feedback_tab_body": "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.",
|
||||
"feedback_tab_description_label": "Your feedback",
|
||||
"feedback_tab_h4": "Submit feedback",
|
||||
"feedback_tab_send_logs_label": "Include debug logs",
|
||||
"feedback_tab_thank_you": "Thanks, we received your feedback!",
|
||||
"feedback_tab_title": "Feedback",
|
||||
"more_tab_title": "More",
|
||||
"opt_in_description": "<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.",
|
||||
"preferences_tab": {
|
||||
"developer_mode_label": "Developer mode",
|
||||
"developer_mode_label_description": "Enable developer mode and show developer settings tab.",
|
||||
"introduction": "Here you can configure extra options for an improved experience.",
|
||||
"reactions_play_sound_description": "Play a sound effect when anyone sends a reaction into a call.",
|
||||
"reactions_play_sound_label": "Play reaction sounds",
|
||||
"reactions_show_description": "Show an animation when anyone sends a reaction.",
|
||||
"reactions_show_label": "Show reactions",
|
||||
"reactions_title": "Reactions"
|
||||
},
|
||||
"preferences_tab_body": "Here you can configure extra options for an improved experience",
|
||||
"preferences_tab_h4": "Preferences",
|
||||
"preferences_tab_show_hand_raised_timer_description": "Show a timer when a participant raises their hand",
|
||||
"preferences_tab_show_hand_raised_timer_label": "Show hand raise duration",
|
||||
"speaker_device_selection_label": "Speaker"
|
||||
"show_hand_raised_timer_description": "Show a timer when a participant raises their hand",
|
||||
"show_hand_raised_timer_label": "Show hand raise duration"
|
||||
}
|
||||
},
|
||||
"star_rating_input_label_one": "{{count}} stars",
|
||||
"star_rating_input_label_one": "{{count}} star",
|
||||
"star_rating_input_label_other": "{{count}} stars",
|
||||
"start_new_call": "Start new call",
|
||||
"start_video_button_label": "Start video",
|
||||
@@ -186,11 +200,13 @@
|
||||
"version": "{{productName}} version: {{version}}",
|
||||
"video_tile": {
|
||||
"always_show": "Always show",
|
||||
"camera_starting": "Video loading...",
|
||||
"change_fit_contain": "Fit to frame",
|
||||
"collapse": "Collapse",
|
||||
"expand": "Expand",
|
||||
"mute_for_me": "Mute for me",
|
||||
"muted_for_me": "Muted for me",
|
||||
"volume": "Volume"
|
||||
"volume": "Volume",
|
||||
"waiting_for_media": "Waiting for media..."
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,9 @@
|
||||
"settings": "Ajustes",
|
||||
"username": "Nombre de usuario"
|
||||
},
|
||||
"full_screen_view_description": "<0>Subir los registros de depuración nos ayudará a encontrar el problema.</0>",
|
||||
"error": {
|
||||
"generic_description": "Subir los registros de depuración nos ayudará a encontrar el problema."
|
||||
},
|
||||
"full_screen_view_h1": "<0>Ups, algo ha salido mal.</0>",
|
||||
"header_label": "Inicio de Element Call",
|
||||
"join_existing_call_modal": {
|
||||
@@ -67,8 +69,6 @@
|
||||
"room_auth_view_eula_caption": "Al hacer clic en \"Unirse a la llamada ahora\", aceptas nuestro <2>Contrato de Licencia de Usuario Final (CLUF)</2>",
|
||||
"screenshare_button_label": "Compartir pantalla",
|
||||
"settings": {
|
||||
"developer_settings_label": "Ajustes de desarrollador",
|
||||
"developer_settings_label_description": "Muestra los ajustes de desarrollador en la ventana de ajustes.",
|
||||
"developer_tab_title": "Desarrollador",
|
||||
"feedback_tab_body": "Si tienes algún problema o simplemente quieres darnos tu opinión, por favor envíanos una breve descripción.",
|
||||
"feedback_tab_description_label": "Tus comentarios",
|
||||
@@ -76,7 +76,6 @@
|
||||
"feedback_tab_send_logs_label": "Incluir registros de depuración",
|
||||
"feedback_tab_thank_you": "¡Gracias, hemos recibido tus comentarios!",
|
||||
"feedback_tab_title": "Danos tu opinión",
|
||||
"more_tab_title": "Más",
|
||||
"opt_in_description": "<0></0><1></1>Puedes retirar tu consentimiento desmarcando esta casilla. Si estás en una llamada, este ajuste se aplicará al final de esta.",
|
||||
"speaker_device_selection_label": "Altavoz"
|
||||
},
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
},
|
||||
"browser_media_e2ee_unsupported": "Sinu veebibrauser ei toeta meedia läbivat krüptimist. Toetatud brauserid on Chrome, Chromium, Safari ja Firefox >=117",
|
||||
"call_ended_view": {
|
||||
"body": "Sinu ühendus kõnega katkes",
|
||||
"create_account_button": "Loo konto",
|
||||
"create_account_prompt": "<0>Kas soovid salasõna seadistada ja sellega oma kasutajakonto alles jätta?</0><1>Nii saad säilitada oma nime ja määrata profiilipildi, mida saad kasutada tulevastes kõnedes</1>",
|
||||
"feedback_done": "<0>Täname Sind tagasiside eest!</0>",
|
||||
@@ -50,7 +49,10 @@
|
||||
"username": "Kasutajanimi"
|
||||
},
|
||||
"disconnected_banner": "Võrguühendus serveriga on katkenud.",
|
||||
"full_screen_view_description": "<0>Kui saadad meile vealogid, siis on lihtsam vea põhjust otsida.</0>",
|
||||
"error": {
|
||||
"connection_lost_description": "Sinu ühendus kõnega katkes",
|
||||
"generic_description": "Kui saadad meile vealogid, siis on lihtsam vea põhjust otsida."
|
||||
},
|
||||
"full_screen_view_h1": "<0>Ohoo, midagi on nüüd katki.</0>",
|
||||
"hangup_button_label": "Lõpeta kõne",
|
||||
"header_participants_label": "Osalejad",
|
||||
@@ -97,8 +99,6 @@
|
||||
"room_auth_view_eula_caption": "Klõpsides „Liitu kõnega kohe“, nõustud sa meie <2>Lõppkasutaja litsentsilepinguga (EULA)</2>",
|
||||
"screenshare_button_label": "Jaga ekraani",
|
||||
"settings": {
|
||||
"developer_settings_label": "Arendaja seadistused",
|
||||
"developer_settings_label_description": "Näita seadistuste aknas arendajale vajalikke seadeid.",
|
||||
"developer_tab_title": "Arendaja",
|
||||
"feedback_tab_body": "Kui selle rakenduse kasutamisel tekib sul probleeme või lihtsalt soovid oma arvamust avaldada, siis palun täida alljärgnev lühike kirjeldus.",
|
||||
"feedback_tab_description_label": "Sinu tagasiside",
|
||||
@@ -106,7 +106,6 @@
|
||||
"feedback_tab_send_logs_label": "Lisa veatuvastuslogid",
|
||||
"feedback_tab_thank_you": "Tänud, me oleme sinu tagasiside kätte saanud!",
|
||||
"feedback_tab_title": "Tagasiside",
|
||||
"more_tab_title": "Rohkem",
|
||||
"opt_in_description": "<0></0><1></1>Sa võid selle valiku eelmaldamisega alati oma nõusoleku tagasi võtta. Kui sul parasjagu on kõne pooleli, siis seadistuste muudatus jõustub pärast kõne lõppu.",
|
||||
"speaker_device_selection_label": "Kõlar"
|
||||
},
|
||||
|
||||
@@ -64,7 +64,6 @@
|
||||
"developer_tab_title": "توسعه دهنده",
|
||||
"feedback_tab_h4": "بازخورد ارائه دهید",
|
||||
"feedback_tab_send_logs_label": "شامل لاگهای عیبیابی",
|
||||
"more_tab_title": "بیشتر",
|
||||
"speaker_device_selection_label": "بلندگو"
|
||||
},
|
||||
"unauthenticated_view_body": "هنوز ثبتنام نکردهاید؟ <2>ساخت حساب کاربری</2>",
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
},
|
||||
"browser_media_e2ee_unsupported": "Votre navigateur web ne prend pas en charge le chiffrement de bout-en-bout des médias. Les navigateurs pris en charge sont Chrome, Safari, Firefox >= 117",
|
||||
"call_ended_view": {
|
||||
"body": "Vous avez été déconnecté de l’appel",
|
||||
"create_account_button": "Créer un compte",
|
||||
"create_account_prompt": "<0>Pourquoi ne pas créer un mot de passe pour conserver votre compte ?</0><1>Vous pourrez garder votre nom et définir un avatar pour vos futurs appels</1>",
|
||||
"feedback_done": "<0>Merci pour votre commentaire !</0>",
|
||||
@@ -48,7 +47,10 @@
|
||||
"video": "Vidéo"
|
||||
},
|
||||
"disconnected_banner": "La connexion avec le serveur a été perdue.",
|
||||
"full_screen_view_description": "<0>Soumettre les journaux de débogage nous aidera à déterminer le problème.</0>",
|
||||
"error": {
|
||||
"connection_lost_description": "Vous avez été déconnecté de l’appel",
|
||||
"generic_description": "Soumettre les journaux de débogage nous aidera à déterminer le problème."
|
||||
},
|
||||
"full_screen_view_h1": "<0>Oups, quelque chose s’est mal passé.</0>",
|
||||
"hangup_button_label": "Terminer l’appel",
|
||||
"header_label": "Accueil Element Call",
|
||||
@@ -95,8 +97,6 @@
|
||||
"room_auth_view_eula_caption": "En cliquant sur « Rejoindre l’appel maintenant », vous acceptez notre <2>Contrat de Licence Utilisateur Final (CLUF)</2>",
|
||||
"screenshare_button_label": "Partage d’écran",
|
||||
"settings": {
|
||||
"developer_settings_label": "Paramètres développeurs",
|
||||
"developer_settings_label_description": "Affiche les paramètres développeurs dans la fenêtre des paramètres.",
|
||||
"developer_tab_title": "Développeur",
|
||||
"feedback_tab_body": "Si vous rencontrez des problèmes, ou vous voulez simplement faire un commentaire, faites-en une courte description ci-dessous.",
|
||||
"feedback_tab_description_label": "Votre commentaire",
|
||||
@@ -104,7 +104,6 @@
|
||||
"feedback_tab_send_logs_label": "Inclure les journaux de débogage",
|
||||
"feedback_tab_thank_you": "Merci, nous avons reçu vos commentaires !",
|
||||
"feedback_tab_title": "Commentaires",
|
||||
"more_tab_title": "Plus",
|
||||
"opt_in_description": "<0></0><1></1>Vous pouvez retirer votre consentement en décochant cette case. Si vous êtes actuellement en communication, ce paramètre prendra effet à la fin de l’appel.",
|
||||
"speaker_device_selection_label": "Intervenant"
|
||||
},
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
},
|
||||
"browser_media_e2ee_unsupported": "Peramban web Anda tidak mendukung enkripsi media ujung ke ujung. Peramban yang didukung adalah Chrome, Safari, dan Firefox >=117",
|
||||
"call_ended_view": {
|
||||
"body": "Anda terputus dari panggilan",
|
||||
"create_account_button": "Buat akun",
|
||||
"create_account_prompt": "<0>Kenapa tidak selesaikan dengan mengatur sebuah kata sandi untuk menjaga akun Anda?</0><1>Anda akan dapat tetap menggunakan nama Anda dan atur sebuah avatar untuk digunakan dalam panggilan di masa mendatang</1>",
|
||||
"feedback_done": "<0>Terima kasih atas masukan Anda!</0>",
|
||||
@@ -48,7 +47,10 @@
|
||||
"username": "Nama pengguna"
|
||||
},
|
||||
"disconnected_banner": "Koneksi ke server telah hilang.",
|
||||
"full_screen_view_description": "<0>Mengirim catatan pengawakutuan akan membantu kami melacak masalahnya.</0>",
|
||||
"error": {
|
||||
"connection_lost_description": "Anda terputus dari panggilan",
|
||||
"generic_description": "Mengirim catatan pengawakutuan akan membantu kami melacak masalahnya."
|
||||
},
|
||||
"full_screen_view_h1": "<0>Aduh, ada yang salah.</0>",
|
||||
"hangup_button_label": "Akhiri panggilan",
|
||||
"header_label": "Beranda Element Call",
|
||||
@@ -96,8 +98,6 @@
|
||||
"room_auth_view_eula_caption": "Dengan mengeklik \"Bergabung ke panggilan sekarang\", Anda menyetujui <2>Perjanjian Lisensi Pengguna Akhir (EULA)</2> kami",
|
||||
"screenshare_button_label": "Bagikan layar",
|
||||
"settings": {
|
||||
"developer_settings_label": "Pengaturan Pengembang",
|
||||
"developer_settings_label_description": "Ekspos pengaturan pengembang dalam jendela pengaturan.",
|
||||
"developer_tab_title": "Pengembang",
|
||||
"feedback_tab_body": "Jika Anda mengalami masalah atau hanya ingin memberikan masukan, silakan kirimkan kami deskripsi pendek di bawah.",
|
||||
"feedback_tab_description_label": "Masukan Anda",
|
||||
@@ -105,7 +105,6 @@
|
||||
"feedback_tab_send_logs_label": "Termasuk catatan pengawakutuan",
|
||||
"feedback_tab_thank_you": "Terima kasih, kami telah menerima masukan Anda!",
|
||||
"feedback_tab_title": "Masukan",
|
||||
"more_tab_title": "Lainnya",
|
||||
"opt_in_description": "<0></0><1></1>Anda dapat mengurungkan kembali izin dengan mencentang kotak ini. Jika Anda saat ini dalam panggilan, pengaturan ini akan diterapkan di akhir panggilan.",
|
||||
"speaker_device_selection_label": "Pembicara"
|
||||
},
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
},
|
||||
"browser_media_e2ee_unsupported": "Il tuo browser non supporta la crittografia end-to-end dei media. I browser supportati sono Chrome, Safari, Firefox >=117",
|
||||
"call_ended_view": {
|
||||
"body": "Sei stato disconnesso dalla chiamata",
|
||||
"create_account_button": "Crea profilo",
|
||||
"create_account_prompt": "<0>Ti va di terminare impostando una password per mantenere il profilo?</0><1>Potrai mantenere il tuo nome e impostare un avatar da usare in chiamate future</1>",
|
||||
"feedback_done": "<0>Grazie per la tua opinione!</0>",
|
||||
@@ -46,7 +45,10 @@
|
||||
"username": "Nome utente"
|
||||
},
|
||||
"disconnected_banner": "La connessione al server è stata persa.",
|
||||
"full_screen_view_description": "<0>L'invio di registri di debug ci aiuterà ad individuare il problema.</0>",
|
||||
"error": {
|
||||
"connection_lost_description": "Sei stato disconnesso dalla chiamata",
|
||||
"generic_description": "L'invio di registri di debug ci aiuterà ad individuare il problema."
|
||||
},
|
||||
"full_screen_view_h1": "<0>Ops, qualcosa è andato storto.</0>",
|
||||
"hangup_button_label": "Termina chiamata",
|
||||
"header_label": "Inizio di Element Call",
|
||||
@@ -94,15 +96,12 @@
|
||||
"room_auth_view_eula_caption": "Cliccando \"Entra in chiamata ora\", accetti il nostro <2>accordo di licenza con l'utente finale (EULA)</2>",
|
||||
"screenshare_button_label": "Condividi schermo",
|
||||
"settings": {
|
||||
"developer_settings_label": "Impostazioni per sviluppatori",
|
||||
"developer_settings_label_description": "Mostra le impostazioni per sviluppatori nella finestra delle impostazioni.",
|
||||
"developer_tab_title": "Sviluppatore",
|
||||
"feedback_tab_body": "Se stai riscontrando problemi o semplicemente vuoi dare un'opinione, inviaci una breve descrizione qua sotto.",
|
||||
"feedback_tab_description_label": "Il tuo commento",
|
||||
"feedback_tab_h4": "Invia commento",
|
||||
"feedback_tab_send_logs_label": "Includi registri di debug",
|
||||
"feedback_tab_thank_you": "Grazie, abbiamo ricevuto il tuo commento!",
|
||||
"more_tab_title": "Altro",
|
||||
"opt_in_description": "<0></0><1></1>Puoi revocare il consenso deselezionando questa casella. Se attualmente sei in una chiamata, avrà effetto al termine di essa.",
|
||||
"speaker_device_selection_label": "Altoparlante"
|
||||
},
|
||||
|
||||
@@ -28,7 +28,9 @@
|
||||
"username": "ユーザー名",
|
||||
"video": "ビデオ"
|
||||
},
|
||||
"full_screen_view_h1": "<0>何かがうまく行きませんでした。</0>",
|
||||
"error": {
|
||||
"generic_description": "何かがうまく行きませんでした。"
|
||||
},
|
||||
"header_label": "Element Call ホーム",
|
||||
"join_existing_call_modal": {
|
||||
"join_button": "はい、通話に参加",
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
},
|
||||
"analytics_notice": "Piedalīšanās šajā beta apliecina piekrišanu anonīmu datu ievākšanai, ko mēs izmantojam, lai uzlabotu izstrādājumu. Vairāk informācijas par datiem, ko mēs ievācam, var atrast mūsu <2>privātuma nosacījumos</2> un <5>sīkdatņu nosacījumos</5>.",
|
||||
"call_ended_view": {
|
||||
"body": "Tu tiki atvienots no zvana",
|
||||
"create_account_button": "Izveidot kontu",
|
||||
"create_account_prompt": "<0>Kādēļ nepabeigt ar paroles iestatīšanu, lai paturētu savu kontu?</0><1>Būs iespējams paturēt savu vārdu un iestatīt attēlu izmantošanai turpmākajos zvanos</1>",
|
||||
"feedback_done": "<0>Paldies par atsauksmi!</0>",
|
||||
@@ -38,7 +37,10 @@
|
||||
"username": "Lietotājvārds"
|
||||
},
|
||||
"disconnected_banner": "Ir zaudēts savienojums ar serveri.",
|
||||
"full_screen_view_description": "<0>Atkļūdošanas žurnāla ierakstu iesūtīšana palīdzēs mums atklāt nepilnību.</0>",
|
||||
"error": {
|
||||
"connection_lost_description": "Tu tiki atvienots no zvana",
|
||||
"generic_description": "Atkļūdošanas žurnāla ierakstu iesūtīšana palīdzēs mums atklāt nepilnību."
|
||||
},
|
||||
"full_screen_view_h1": "<0>Ak vai, kaut kas nogāja greizi!</0>",
|
||||
"header_label": "Element Call sākums",
|
||||
"join_existing_call_modal": {
|
||||
@@ -75,8 +77,6 @@
|
||||
"room_auth_view_eula_caption": "Klikšķināšana uz \"Pievienoties zvanam tagad\" apliecina piekrišanu mūsu <2>galalietotāja licencēšanas nolīgumam (GLLN)</2>",
|
||||
"screenshare_button_label": "Kopīgot ekrānu",
|
||||
"settings": {
|
||||
"developer_settings_label": "Izstrādātāja iestatījumi",
|
||||
"developer_settings_label_description": "Izstādīt izstrādātāja iestatījumus iestatījumu logā.",
|
||||
"developer_tab_title": "Izstrādātājs",
|
||||
"feedback_tab_body": "Ja tiek piedzīvoti sarežģījumi vai vienkārši ir vēlme sniegt kādu atsauksmi, lūgums zemāk nosūtīt mums īsu aprakstu.",
|
||||
"feedback_tab_description_label": "Tava atsauksme",
|
||||
@@ -84,7 +84,6 @@
|
||||
"feedback_tab_send_logs_label": "Iekļaut atkļūdošanas žurnāla ierakstus",
|
||||
"feedback_tab_thank_you": "Paldies, mēs saņēmām atsauksmi!",
|
||||
"feedback_tab_title": "Atsauksmes",
|
||||
"more_tab_title": "Vairāk",
|
||||
"opt_in_description": "<0></0><1></1>Savu piekrišanu var atsaukt ar atzīmes noņemšanu no šīs rūtiņas. Ja pašreiz atrodies zvanā, šis iestatījums stāsies spēkā zvana beigās.",
|
||||
"speaker_device_selection_label": "Runātājs"
|
||||
},
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
},
|
||||
"browser_media_e2ee_unsupported": "Twoja przeglądarka nie wspiera szyfrowania end-to-end. Wspierane przeglądarki to Chrome, Safari, Firefox >=117",
|
||||
"call_ended_view": {
|
||||
"body": "Rozłączono Cię z połączenia",
|
||||
"create_account_button": "Utwórz konto",
|
||||
"create_account_prompt": "<0>Może zechcesz ustawić hasło, aby zachować swoje konto?</0><1>Będziesz w stanie utrzymać swoją nazwę i ustawić awatar do wyświetlania podczas połączeń w przyszłości</1>",
|
||||
"feedback_done": "<0>Dziękujemy za Twoją opinię!</0>",
|
||||
@@ -51,7 +50,10 @@
|
||||
"video": "Wideo"
|
||||
},
|
||||
"disconnected_banner": "Utracono połączenie z serwerem.",
|
||||
"full_screen_view_description": "<0>Wysłanie dzienników debuggowania pomoże nam ustalić przyczynę problemu.</0>",
|
||||
"error": {
|
||||
"connection_lost_description": "Rozłączono Cię z połączenia",
|
||||
"generic_description": "Wysłanie dzienników debuggowania pomoże nam ustalić przyczynę problemu."
|
||||
},
|
||||
"full_screen_view_h1": "<0>Ojej, coś poszło nie tak.</0>",
|
||||
"hangup_button_label": "Zakończ połączenie",
|
||||
"header_label": "Strona główna Element Call",
|
||||
@@ -99,8 +101,6 @@
|
||||
"room_auth_view_eula_caption": "Klikając \"Dołącz teraz do rozmowy\", zgadzasz się na naszą <2>Umowę licencyjną (EULA)</2>",
|
||||
"screenshare_button_label": "Udostępnij ekran",
|
||||
"settings": {
|
||||
"developer_settings_label": "Opcje programisty",
|
||||
"developer_settings_label_description": "Wyświetl opcje programisty w oknie ustawień.",
|
||||
"developer_tab_title": "Programista",
|
||||
"feedback_tab_body": "Jeśli posiadasz problemy lub chciałbyś zgłosić swoją opinię, wyślij nam krótki opis.",
|
||||
"feedback_tab_description_label": "Twoje opinie",
|
||||
@@ -108,7 +108,6 @@
|
||||
"feedback_tab_send_logs_label": "Dołącz dzienniki debugowania",
|
||||
"feedback_tab_thank_you": "Dziękujemy, otrzymaliśmy Twoją opinię!",
|
||||
"feedback_tab_title": "Opinia użytkownika",
|
||||
"more_tab_title": "Więcej",
|
||||
"opt_in_description": "<0></0><1></1>Możesz wycofać swoją zgodę poprzez odznaczenie tego pola. Jeśli już jesteś w trakcie rozmowy, opcja zostanie zastosowana po jej zakończeniu.",
|
||||
"speaker_device_selection_label": "Głośnik"
|
||||
},
|
||||
|
||||
189
locales/ro/app.json
Normal file
189
locales/ro/app.json
Normal file
@@ -0,0 +1,189 @@
|
||||
{
|
||||
"a11y": {
|
||||
"user_menu": "Meniul utilizatorului"
|
||||
},
|
||||
"action": {
|
||||
"close": "Închide",
|
||||
"copy_link": "Copiază linkul",
|
||||
"edit": "Editare",
|
||||
"go": "Du-te",
|
||||
"invite": "Invită",
|
||||
"lower_hand": "Mâna inferioară",
|
||||
"no": "No",
|
||||
"pick_reaction": "Alegeți reacția",
|
||||
"raise_hand": "Ridicați mâna",
|
||||
"register": "Inregistrare",
|
||||
"remove": "elimina",
|
||||
"show_less": "Arată mai puțin",
|
||||
"show_more": "Arată mai mult",
|
||||
"sign_in": "Autentificare",
|
||||
"sign_out": "Sign out",
|
||||
"submit": "Trimiteți",
|
||||
"upload_file": "Încărcați fișierul"
|
||||
},
|
||||
"analytics_notice": "Prin participarea la această versiune beta, sunteți de acord cu colectarea de date anonime, pe care le folosim pentru a îmbunătăți produsul. Puteți găsi mai multe informații despre datele pe care le urmărim în Politica noastră de <2> confidențialitate </2> și Politica noastră <6> privind cookie-urile</6>.",
|
||||
"app_selection_modal": {
|
||||
"continue_in_browser": "Continuați în browser",
|
||||
"open_in_app": "Deschideți în aplicație",
|
||||
"text": "Sunteți gata să vă alăturați?",
|
||||
"title": "Selectați aplicația"
|
||||
},
|
||||
"application_opened_another_tab": "Această aplicație a fost deschisă într-o altă filă.",
|
||||
"browser_media_e2ee_unsupported": "Browserul dvs. web nu acceptă criptarea media end-to-end. Browserele acceptate sunt Chrome, Safari, Firefox > = 117",
|
||||
"browser_media_e2ee_unsupported_heading": "Browser incompatibil",
|
||||
"call_ended_view": {
|
||||
"create_account_button": "Creează cont",
|
||||
"create_account_prompt": "<0>De ce să nu terminați prin configurarea unei parole pentru a vă păstra contul? </0><1>Veți putea să vă păstrați numele și să setați un avatar pentru a fi utilizat la apelurile viitoare </1>",
|
||||
"feedback_done": "<0>Vă mulțumim pentru feedback! </0>",
|
||||
"feedback_prompt": "<0>Ne-ar plăcea să auzim feedback-ul dvs., astfel încât să vă putem îmbunătăți experiența. </0>",
|
||||
"headline": "{{displayName}}, apelul tău s-a încheiat.",
|
||||
"not_now_button": "Nu acum, reveniți la ecranul de pornire",
|
||||
"reconnect_button": "Reconecta",
|
||||
"survey_prompt": "Cum a mers?"
|
||||
},
|
||||
"call_name": "Numele apelului",
|
||||
"common": {
|
||||
"analytics": "Analiză",
|
||||
"audio": "Audio",
|
||||
"avatar": "avatar",
|
||||
"back": "Înapoi",
|
||||
"camera": "Aparat foto",
|
||||
"display_name": "Nume afișat",
|
||||
"encrypted": "Criptat",
|
||||
"error": "Eroare",
|
||||
"home": "Acasa",
|
||||
"loading": "Se încarcă...",
|
||||
"microphone": "Microfon",
|
||||
"next": "Urmator\n",
|
||||
"options": "Opțiuni",
|
||||
"password": "Parolă",
|
||||
"preferences": "preferinte",
|
||||
"profile": "Profil",
|
||||
"reaction": "Reacție",
|
||||
"reactions": "Reacții",
|
||||
"settings": "Settings",
|
||||
"something_went_wrong": "Ceva nu a mers bine",
|
||||
"unencrypted": "Nu este criptat",
|
||||
"username": "Nume utilizator",
|
||||
"video": "Videoclip"
|
||||
},
|
||||
"developer_mode": {
|
||||
"crypto_version": "Versiunea Crypto: {{version}}",
|
||||
"device_id": "ID-ul dispozitivului: {{id}}",
|
||||
"duplicate_tiles_label": "Numărul de exemplare suplimentare de cartonașe per participant",
|
||||
"hostname": "Numele gazdei: {{hostname}}",
|
||||
"matrix_id": "ID-ul matricei: {{id}}"
|
||||
},
|
||||
"disconnected_banner": "Conectivitatea la server a fost pierdută.",
|
||||
"error": {
|
||||
"connection_lost_description": "Ai fost deconectat de la apel",
|
||||
"generic_description": "Trimiterea jurnalelor de depanare ne va ajuta să urmărim problema."
|
||||
},
|
||||
"full_screen_view_h1": "<0>Hopa, ceva nu a mers bine. </0>",
|
||||
"group_call_loader": {
|
||||
"banned_body": "Ai fost interzis să ieși din cameră.",
|
||||
"banned_heading": "Interzis",
|
||||
"call_ended_body": "Ați fost eliminat din apel.",
|
||||
"call_ended_heading": "Apel încheiat",
|
||||
"failed_heading": "Nu s-a putut alătura",
|
||||
"failed_text": "Apelul nu a fost găsit sau nu este accesibil.",
|
||||
"knock_reject_body": "Cererea dvs. de a vă alătura a fost respinsă.",
|
||||
"knock_reject_heading": "Acces refuzat",
|
||||
"reason": "Motivul"
|
||||
},
|
||||
"hangup_button_label": "Încheiați apelul",
|
||||
"header_label": "Element Call Home",
|
||||
"header_participants_label": "Participanți",
|
||||
"invite_modal": {
|
||||
"link_copied_toast": "Link copiat în clipboard",
|
||||
"title": "Invitați la acest apel"
|
||||
},
|
||||
"join_existing_call_modal": {
|
||||
"join_button": "Da, alăturați-vă apelului",
|
||||
"text": "Acest apel există deja, doriți să vă alăturați?",
|
||||
"title": "Alăturați-vă apelului existent?"
|
||||
},
|
||||
"layout_grid_label": "GRILĂ",
|
||||
"layout_spotlight_label": "Spotlight",
|
||||
"lobby": {
|
||||
"ask_to_join": "Solicitare de participare la apel",
|
||||
"join_as_guest": "Alăturați-vă ca invitat",
|
||||
"join_button": "Alăturați-vă apelului",
|
||||
"leave_button": "Înapoi la cele mai recente",
|
||||
"waiting_for_invite": "Solicitare trimisă! În așteptarea permisiunii de a participa..."
|
||||
},
|
||||
"log_in": "Autentificare",
|
||||
"logging_in": "Autentificare...",
|
||||
"login_auth_links": "<0>Creați un cont </0> sau <2> accesați ca invitat </2>",
|
||||
"login_auth_links_prompt": "Nu sunteți încă înregistrat?",
|
||||
"login_subheading": "Pentru a continua la Element",
|
||||
"login_title": "Logare",
|
||||
"microphone_off": "Microfon oprit",
|
||||
"microphone_on": "Microfon pornit",
|
||||
"mute_microphone_button_label": "Dezactivați microfonul",
|
||||
"qr_code": "COD QR",
|
||||
"rageshake_button_error_caption": "Încearcă din nou trimiterea jurnalelor",
|
||||
"rageshake_request_modal": {
|
||||
"body": "Un alt utilizator al acestui apel are o problemă. Pentru a diagnostica mai bine aceste probleme, am dori să colectăm un jurnal de depanare.",
|
||||
"title": "Solicitare jurnal de depanare"
|
||||
},
|
||||
"rageshake_send_logs": "Trimiteți jurnale de depanare",
|
||||
"rageshake_sending": "Trimiterea...",
|
||||
"rageshake_sending_logs": "Trimiterea jurnalelor de depanare...",
|
||||
"rageshake_sent": "Multumesc!",
|
||||
"recaptcha_caption": "Acest site este protejat de reCAPTCHA și se aplică Politica de <2> confidențialitate Google </2> și <6> Termenii și condițiile. </6> <9></9>Făcând clic pe „Înregistrare”, sunteți de acord cu Acordul nostru de licențiere pentru utilizatorul <12> final (EULA) </12>",
|
||||
"recaptcha_dismissed": "Recaptcha a fost respins",
|
||||
"recaptcha_not_loaded": "Recaptcha nu a fost încărcat",
|
||||
"register": {
|
||||
"passwords_must_match": "Parolele trebuie să se potrivească",
|
||||
"registering": "Înregistrare..."
|
||||
},
|
||||
"register_auth_links": "<0>Ai deja un cont? </0><1><0>Conectați-vă </0> sau <2> accesați ca invitat </2> </1>",
|
||||
"register_confirm_password_label": "Confirmă Parola",
|
||||
"register_heading": "Creează-ți contul",
|
||||
"return_home_button": "Reveniți la ecranul de pornire",
|
||||
"room_auth_view_continue_button": "Continuă",
|
||||
"room_auth_view_eula_caption": "Făcând clic pe „Continuați”, sunteți de acord cu Acordul nostru de licențiere pentru utilizatorul <2> final (EULA) </2>",
|
||||
"screenshare_button_label": "Partajare ecran",
|
||||
"settings": {
|
||||
"audio_tab": {
|
||||
"effect_volume_description": "Reglați volumul la care reacționează reacțiile și efectele ridicate de mână",
|
||||
"effect_volume_label": "Volumul efectului sonor"
|
||||
},
|
||||
"developer_tab_title": "dezvoltator",
|
||||
"feedback_tab_body": "Dacă întâmpinați probleme sau pur și simplu doriți să oferiți feedback, vă rugăm să ne trimiteți o scurtă descriere mai jos.",
|
||||
"feedback_tab_description_label": "Feedback-ul tău",
|
||||
"feedback_tab_h4": "Trimiteți Feedback",
|
||||
"feedback_tab_send_logs_label": "Includeți jurnale de depanare",
|
||||
"feedback_tab_thank_you": "Vă mulțumim, am primit feedback-ul dvs.!",
|
||||
"feedback_tab_title": "Feedback",
|
||||
"opt_in_description": "<0></0><1></1>Puteți retrage consimțământul debifând această casetă. Dacă sunteți în prezent la un apel, această setare va intra în vigoare la sfârșitul apelului.",
|
||||
"preferences_tab": {
|
||||
"reactions_play_sound_description": "Redați un efect sonor atunci când cineva trimite o reacție la un apel.",
|
||||
"reactions_play_sound_label": "Redați sunete de reacție",
|
||||
"reactions_show_description": "Afișați o animație atunci când cineva trimite o reacție.",
|
||||
"reactions_show_label": "Afișați reacțiile"
|
||||
},
|
||||
"speaker_device_selection_label": "vorbitor"
|
||||
},
|
||||
"start_new_call": "Începe un nou apel",
|
||||
"start_video_button_label": "Începeți videoclipul",
|
||||
"stop_screenshare_button_label": "Partajarea ecranului",
|
||||
"stop_video_button_label": "Opriți videoclipul",
|
||||
"submitting": "Trimiterea...",
|
||||
"switch_camera": "Comutați camera",
|
||||
"unauthenticated_view_body": "Nu sunteți încă înregistrat? <2>Creați un cont </2>",
|
||||
"unauthenticated_view_eula_caption": "Făcând clic pe „Go”, sunteți de acord cu Acordul nostru de licențiere pentru utilizatorul <2> final (EULA) </2>",
|
||||
"unauthenticated_view_login_button": "Conectați-vă la contul dvs.",
|
||||
"unmute_microphone_button_label": "Anulează microfonul",
|
||||
"version": "{{productName}}Versiune: {{version}}",
|
||||
"video_tile": {
|
||||
"always_show": "Arată întotdeauna",
|
||||
"change_fit_contain": "Se potrivește cadrului",
|
||||
"collapse": "colaps",
|
||||
"expand": "Extindeți",
|
||||
"mute_for_me": "Mute pentru mine",
|
||||
"muted_for_me": "Dezactivat pentru mine",
|
||||
"volume": "VOLUM"
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,9 @@
|
||||
"username": "Имя пользователя",
|
||||
"video": "Видео"
|
||||
},
|
||||
"full_screen_view_description": "<0>Отправка журналов поможет нам найти и устранить проблему.</0>",
|
||||
"error": {
|
||||
"generic_description": "Отправка журналов поможет нам найти и устранить проблему."
|
||||
},
|
||||
"full_screen_view_h1": "<0>Упс, что-то пошло не так.</0>",
|
||||
"header_label": "Главная Element Call",
|
||||
"join_existing_call_modal": {
|
||||
@@ -69,8 +71,6 @@
|
||||
"return_home_button": "Вернуться в Начало",
|
||||
"screenshare_button_label": "Поделиться экраном",
|
||||
"settings": {
|
||||
"developer_settings_label": "Настройки Разработчика",
|
||||
"developer_settings_label_description": "Раскрыть настройки разработчика в окне настроек.",
|
||||
"developer_tab_title": "Разработчику",
|
||||
"feedback_tab_body": "Если у вас возникли проблемы или вы просто хотите оставить отзыв, отправьте нам краткое описание ниже.",
|
||||
"feedback_tab_description_label": "Ваш отзыв",
|
||||
@@ -78,7 +78,6 @@
|
||||
"feedback_tab_send_logs_label": "Приложить журнал отладки",
|
||||
"feedback_tab_thank_you": "Спасибо. Мы получили ваш отзыв!",
|
||||
"feedback_tab_title": "Отзыв",
|
||||
"more_tab_title": "Больше",
|
||||
"opt_in_description": "<0></0><1></1>Вы можете отозвать согласие, сняв этот флажок. Если вы в данный момент находитесь в разговоре, эта настройка вступит в силу по окончании разговора.",
|
||||
"speaker_device_selection_label": "Динамик"
|
||||
},
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
},
|
||||
"browser_media_e2ee_unsupported": "Váš webový prehliadač nepodporuje end-to-end šifrovanie médií. Podporované prehliadače sú Chrome, Safari, Firefox >=117",
|
||||
"call_ended_view": {
|
||||
"body": "Boli ste odpojení z hovoru",
|
||||
"create_account_button": "Vytvoriť účet",
|
||||
"create_account_prompt": "<0>Prečo neskončiť nastavením hesla, aby ste si zachovali svoj účet? </0><1>Budete si môcť ponechať svoje meno a nastaviť obrázok, ktorý sa bude používať pri budúcich hovoroch</1>",
|
||||
"feedback_done": "<0> Ďakujeme za vašu spätnú väzbu!</0>",
|
||||
@@ -49,7 +48,10 @@
|
||||
"username": "Meno používateľa"
|
||||
},
|
||||
"disconnected_banner": "Spojenie so serverom sa stratilo.",
|
||||
"full_screen_view_description": "<0>Odoslanie záznamov ladenia nám pomôže nájsť problém.</0>",
|
||||
"error": {
|
||||
"connection_lost_description": "Boli ste odpojení z hovoru",
|
||||
"generic_description": "Odoslanie záznamov ladenia nám pomôže nájsť problém."
|
||||
},
|
||||
"full_screen_view_h1": "<0>Hups, niečo sa pokazilo.</0>",
|
||||
"hangup_button_label": "Ukončiť hovor",
|
||||
"header_label": "Domov Element Call",
|
||||
@@ -97,8 +99,6 @@
|
||||
"room_auth_view_eula_caption": "Kliknutím na \"Pripojiť sa k hovoru teraz\" súhlasíte s našou <2>Licenčnou zmluvou s koncovým používateľom (EULA)</2>",
|
||||
"screenshare_button_label": "Zdieľať obrazovku",
|
||||
"settings": {
|
||||
"developer_settings_label": "Nastavenia pre vývojárov",
|
||||
"developer_settings_label_description": "Zobraziť nastavenia pre vývojárov v okne nastavení.",
|
||||
"developer_tab_title": "Vývojár",
|
||||
"feedback_tab_body": "Ak máte problémy alebo jednoducho chcete poskytnúť spätnú väzbu, pošlite nám krátky popis nižšie.",
|
||||
"feedback_tab_description_label": "Vaša spätná väzba",
|
||||
@@ -106,7 +106,6 @@
|
||||
"feedback_tab_send_logs_label": "Zahrnúť záznamy o ladení",
|
||||
"feedback_tab_thank_you": "Ďakujeme, dostali sme vašu spätnú väzbu!",
|
||||
"feedback_tab_title": "Spätná väzba",
|
||||
"more_tab_title": "Viac",
|
||||
"opt_in_description": "<0></0><1></1>Súhlas môžete odvolať zrušením označenia tohto políčka. Ak práve prebieha hovor, toto nastavenie nadobudne platnosť po skončení hovoru.",
|
||||
"speaker_device_selection_label": "Reproduktor"
|
||||
},
|
||||
|
||||
@@ -52,8 +52,7 @@
|
||||
"settings": {
|
||||
"developer_tab_title": "Geliştirici",
|
||||
"feedback_tab_h4": "Geri bildirim ver",
|
||||
"feedback_tab_send_logs_label": "Hata ayıklama kütüğünü dahil et",
|
||||
"more_tab_title": "Daha"
|
||||
"feedback_tab_send_logs_label": "Hata ayıklama kütüğünü dahil et"
|
||||
},
|
||||
"unauthenticated_view_body": "Kaydolmadınız mı? <2>Hesap açın</2>",
|
||||
"unauthenticated_view_login_button": "Hesabınıza girin"
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
},
|
||||
"browser_media_e2ee_unsupported": "Ваш браузер не підтримує наскрізне шифрування мультимедійних даних. Підтримувані браузери: Chrome, Safari, Firefox >=117",
|
||||
"call_ended_view": {
|
||||
"body": "Вас від'єднано від виклику",
|
||||
"create_account_button": "Створити обліковий запис",
|
||||
"create_account_prompt": "<0>Чому б не завершити, налаштувавши пароль для збереження свого облікового запису?</0><1>Ви зможете зберегти своє ім'я та встановити аватарку для подальшого користування під час майбутніх викликів</1>",
|
||||
"feedback_done": "<0>Дякуємо за ваш відгук!</0>",
|
||||
@@ -51,7 +50,10 @@
|
||||
"video": "Відео"
|
||||
},
|
||||
"disconnected_banner": "Втрачено зв'язок з сервером.",
|
||||
"full_screen_view_description": "<0>Надсилання журналів налагодження допоможе нам виявити проблему.</0>",
|
||||
"error": {
|
||||
"connection_lost_description": "Вас від'єднано від виклику",
|
||||
"generic_description": "Надсилання журналів налагодження допоможе нам виявити проблему."
|
||||
},
|
||||
"full_screen_view_h1": "<0>Йой, щось пішло не за планом.</0>",
|
||||
"hangup_button_label": "Завершити виклик",
|
||||
"header_label": "Домівка Element Call",
|
||||
@@ -99,8 +101,6 @@
|
||||
"room_auth_view_eula_caption": "Натискаючи \"Приєднатися до виклику зараз\", ви погоджуєтеся з нашою <2>Ліцензійною угодою з кінцевим користувачем (EULA)</2>",
|
||||
"screenshare_button_label": "Поділитися екраном",
|
||||
"settings": {
|
||||
"developer_settings_label": "Налаштування розробника",
|
||||
"developer_settings_label_description": "Відкрийте налаштування розробника у вікні налаштувань.",
|
||||
"developer_tab_title": "Розробнику",
|
||||
"feedback_tab_body": "Якщо у вас виникли проблеми або ви просто хочете залишити відгук, надішліть нам короткий опис нижче.",
|
||||
"feedback_tab_description_label": "Ваш відгук",
|
||||
@@ -108,7 +108,6 @@
|
||||
"feedback_tab_send_logs_label": "Долучити журнали налагодження",
|
||||
"feedback_tab_thank_you": "Дякуємо, ми отримали ваш відгук!",
|
||||
"feedback_tab_title": "Відгук",
|
||||
"more_tab_title": "Докладніше",
|
||||
"opt_in_description": "<0></0><1></1>Ви можете відкликати згоду, прибравши цей прапорець. Якщо ви зараз розмовляєте, це налаштування застосується після завершення виклику.",
|
||||
"speaker_device_selection_label": "Динамік"
|
||||
},
|
||||
|
||||
@@ -27,7 +27,9 @@
|
||||
"username": "Tên người dùng",
|
||||
"video": "Truyền hình"
|
||||
},
|
||||
"full_screen_view_description": "<0>Gửi nhật ký gỡ lỗi sẽ giúp chúng tôi theo dõi vấn đề.</0>",
|
||||
"error": {
|
||||
"generic_description": "Gửi nhật ký gỡ lỗi sẽ giúp chúng tôi theo dõi vấn đề."
|
||||
},
|
||||
"full_screen_view_h1": "<0>Ối, có cái gì đó sai.</0>",
|
||||
"join_existing_call_modal": {
|
||||
"join_button": "Vâng, tham gia cuộc gọi",
|
||||
@@ -55,7 +57,6 @@
|
||||
"register_confirm_password_label": "Xác nhận mật khẩu",
|
||||
"screenshare_button_label": "Chia sẻ màn hình",
|
||||
"settings": {
|
||||
"developer_settings_label": "Cài đặt phát triển",
|
||||
"developer_tab_title": "Nhà phát triển",
|
||||
"feedback_tab_description_label": "Phản hồi của bạn",
|
||||
"feedback_tab_h4": "Gửi phản hồi",
|
||||
|
||||
@@ -92,8 +92,6 @@
|
||||
"room_auth_view_eula_caption": "点击 \"加入通话\",即表示您同意我们的<2>最终用户许可协议 (EULA)</2>",
|
||||
"screenshare_button_label": "屏幕共享",
|
||||
"settings": {
|
||||
"developer_settings_label": "开发者设置",
|
||||
"developer_settings_label_description": "在设置中显示开发者设置。",
|
||||
"developer_tab_title": "开发者",
|
||||
"feedback_tab_body": "如果遇到问题或想提供一些反馈意见,请在下面向我们发送简短描述。",
|
||||
"feedback_tab_description_label": "您的反馈",
|
||||
@@ -101,7 +99,6 @@
|
||||
"feedback_tab_send_logs_label": "包含调试日志",
|
||||
"feedback_tab_thank_you": "谢谢,我们收到了反馈!",
|
||||
"feedback_tab_title": "反馈",
|
||||
"more_tab_title": "更多",
|
||||
"opt_in_description": "<0></0><1></1>您可以取消选中复选框来撤回同意。如果正在通话中,此设置将在通话结束时生效。",
|
||||
"speaker_device_selection_label": "发言人"
|
||||
},
|
||||
|
||||
@@ -99,8 +99,6 @@
|
||||
"room_auth_view_eula_caption": "點擊「立刻加入通話」即表示您同意我們的<2>終端使用者授權協議 (EULA)</2>",
|
||||
"screenshare_button_label": "分享畫面",
|
||||
"settings": {
|
||||
"developer_settings_label": "開發者設定",
|
||||
"developer_settings_label_description": "在設定視窗中顯示開發者設定。",
|
||||
"developer_tab_title": "開發者",
|
||||
"feedback_tab_body": "若您遇到問題或只是想提供一些回饋,請在下方傳送簡短說明給我們。",
|
||||
"feedback_tab_description_label": "您的回饋",
|
||||
@@ -108,7 +106,6 @@
|
||||
"feedback_tab_send_logs_label": "包含除錯紀錄",
|
||||
"feedback_tab_thank_you": "感謝,我們已經收到您的回饋了!",
|
||||
"feedback_tab_title": "回饋",
|
||||
"more_tab_title": "更多",
|
||||
"opt_in_description": "<0></0><1></1>您可以透過取消核取此方塊來撤回同意。若您目前正在通話中,此設定將在通話結束時生效。",
|
||||
"speaker_device_selection_label": "發言者"
|
||||
},
|
||||
|
||||
45
package.json
45
package.json
@@ -27,13 +27,13 @@
|
||||
"@codecov/vite-plugin": "^1.3.0",
|
||||
"@fontsource/inconsolata": "^5.1.0",
|
||||
"@fontsource/inter": "^5.1.0",
|
||||
"@formatjs/intl-durationformat": "^0.6.1",
|
||||
"@formatjs/intl-durationformat": "^0.7.0",
|
||||
"@formatjs/intl-segmenter": "^11.7.3",
|
||||
"@livekit/components-core": "^0.11.0",
|
||||
"@livekit/components-core": "^0.12.0",
|
||||
"@livekit/components-react": "^2.0.0",
|
||||
"@opentelemetry/api": "^1.4.0",
|
||||
"@opentelemetry/core": "^1.25.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.54.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.57.0",
|
||||
"@opentelemetry/resources": "^1.25.1",
|
||||
"@opentelemetry/sdk-trace-base": "^1.25.1",
|
||||
"@opentelemetry/sdk-trace-web": "^1.9.1",
|
||||
@@ -43,55 +43,55 @@
|
||||
"@radix-ui/react-visually-hidden": "^1.0.3",
|
||||
"@react-spring/web": "^9.4.4",
|
||||
"@sentry/react": "^8.0.0",
|
||||
"@sentry/vite-plugin": "^2.0.0",
|
||||
"@sentry/vite-plugin": "^3.0.0",
|
||||
"@stylistic/eslint-plugin": "^3.0.0",
|
||||
"@testing-library/dom": "^10.1.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@testing-library/user-event": "^14.5.1",
|
||||
"@types/content-type": "^1.1.5",
|
||||
"@types/grecaptcha": "^3.0.9",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/pako": "^2.0.3",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react": "^18.3.0",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/sdp-transform": "^2.4.5",
|
||||
"@types/uuid": "10",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"@use-gesture/react": "^10.2.11",
|
||||
"@vector-im/compound-design-tokens": "^1.9.1",
|
||||
"@vector-im/compound-design-tokens": "^3.0.0",
|
||||
"@vector-im/compound-web": "^7.2.0",
|
||||
"@vitejs/plugin-basic-ssl": "^1.0.1",
|
||||
"@vitejs/plugin-react": "^4.0.1",
|
||||
"@vitest/coverage-v8": "^2.0.5",
|
||||
"@vitest/coverage-v8": "^3.0.0",
|
||||
"babel-plugin-transform-vite-meta-env": "^1.0.3",
|
||||
"classnames": "^2.3.1",
|
||||
"eslint": "^8.14.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
"eslint-plugin-deprecate": "^0.8.2",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-matrix-org": "^1.2.1",
|
||||
"eslint-plugin-matrix-org": "^2.0.0",
|
||||
"eslint-plugin-react": "^7.29.4",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-rxjs": "^5.0.3",
|
||||
"eslint-plugin-unicorn": "^56.0.0",
|
||||
"global-jsdom": "^25.0.0",
|
||||
"history": "^4.0.0",
|
||||
"i18next": "^23.0.0",
|
||||
"global-jsdom": "^26.0.0",
|
||||
"i18next": "^24.0.0",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"i18next-parser": "^9.0.0",
|
||||
"jsdom": "^25.0.0",
|
||||
"i18next-parser": "^9.1.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"knip": "^5.27.2",
|
||||
"livekit-client": "^2.5.7",
|
||||
"lodash-es": "^4.17.21",
|
||||
"loglevel": "^1.9.1",
|
||||
"matrix-js-sdk": "matrix-org/matrix-js-sdk#80582a734684b532585b52cde63ef34dc7d774fa",
|
||||
"matrix-widget-api": "^1.10.0",
|
||||
"matrix-js-sdk": "^36.1.0",
|
||||
"matrix-widget-api": "1.11.0",
|
||||
"normalize.css": "^8.0.1",
|
||||
"observable-hooks": "^4.2.3",
|
||||
"pako": "^2.0.4",
|
||||
@@ -103,7 +103,7 @@
|
||||
"react": "18",
|
||||
"react-dom": "18",
|
||||
"react-i18next": "^15.0.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-router-dom": "^7.0.0",
|
||||
"react-use-clipboard": "^1.0.7",
|
||||
"react-use-measure": "^2.1.1",
|
||||
"rxjs": "^7.8.1",
|
||||
@@ -112,14 +112,13 @@
|
||||
"typescript-eslint-language-service": "^5.0.5",
|
||||
"unique-names-generator": "^4.6.0",
|
||||
"vaul": "^1.0.0",
|
||||
"vite": "^5.0.0",
|
||||
"vite-plugin-compression2": "^1.3.1",
|
||||
"vite": "^6.0.0",
|
||||
"vite-plugin-html-template": "^1.1.0",
|
||||
"vite-plugin-svgr": "^4.0.0",
|
||||
"vitest": "^2.0.0",
|
||||
"vitest": "^3.0.0",
|
||||
"vitest-axe": "^1.0.0-pre.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"strip-ansi": "6.0.1"
|
||||
"matrix-widget-api": "1.11.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"applinks": {
|
||||
"details": [
|
||||
{
|
||||
"appIDs": [
|
||||
"7J4U792NQT.io.element.elementx",
|
||||
"7J4U792NQT.io.element.elementx.nightly",
|
||||
"7J4U792NQT.io.element.elementx.pr"
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"?": {
|
||||
"no_universal_links": "?*"
|
||||
},
|
||||
"exclude": true,
|
||||
"comment": "Opt out of universal links"
|
||||
},
|
||||
{
|
||||
"/": "/*",
|
||||
"comment": "Matches any URL"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
[
|
||||
{
|
||||
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||
"target": {
|
||||
"namespace": "android_app",
|
||||
"package_name": "io.element.android.x.debug",
|
||||
"sha256_cert_fingerprints": [
|
||||
"B0:B0:51:DC:56:5C:81:2F:E1:7F:6F:3E:94:5B:4D:79:04:71:23:AB:0D:A6:12:86:76:9E:B2:94:91:97:13:0E"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||
"target": {
|
||||
"namespace": "android_app",
|
||||
"package_name": "io.element.android.x.nightly",
|
||||
"sha256_cert_fingerprints": [
|
||||
"CA:D3:85:16:84:3A:05:CC:EB:00:AB:7B:D3:80:0F:01:BA:8F:E0:4B:38:86:F3:97:D8:F7:9A:1B:C4:54:E4:0F"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||
"target": {
|
||||
"namespace": "android_app",
|
||||
"package_name": "io.element.android.x",
|
||||
"sha256_cert_fingerprints": [
|
||||
"C6:DB:9B:9C:8C:BD:D6:5D:16:E8:EC:8C:8B:91:C8:31:B9:EF:C9:5C:BF:98:AE:41:F6:A9:D8:35:15:1A:7E:16"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,18 +1,22 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended"],
|
||||
"extends": [
|
||||
"config:recommended",
|
||||
"schedule:monthly",
|
||||
":enableVulnerabilityAlertsWithLabel(security)"
|
||||
],
|
||||
"addLabels": ["dependencies"],
|
||||
"minimumReleaseAge": "5 days",
|
||||
"packageRules": [
|
||||
{
|
||||
"groupName": "all non-major dependencies",
|
||||
"groupSlug": "all-minor-patch",
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"extends": ["schedule:weekly"]
|
||||
"matchUpdateTypes": ["minor", "patch"]
|
||||
},
|
||||
{
|
||||
"groupName": "GitHub Actions",
|
||||
"matchDepTypes": ["action"],
|
||||
"pinDigests": true,
|
||||
"extends": ["schedule:monthly"]
|
||||
"pinDigests": true
|
||||
},
|
||||
{
|
||||
"description": "Disable Renovate for packages we want to monitor ourselves",
|
||||
@@ -22,28 +26,35 @@
|
||||
},
|
||||
{
|
||||
"groupName": "matrix-widget-api",
|
||||
"matchDepNames": ["matrix-widget-api"]
|
||||
"matchDepNames": ["matrix-widget-api"],
|
||||
"extends": ["schedule:weekly"]
|
||||
},
|
||||
{
|
||||
"groupName": "Compound",
|
||||
"schedule": "before 5am on Tuesday and Friday",
|
||||
"matchPackageNames": ["@vector-im/compound-{/,}**"]
|
||||
"matchPackageNames": ["@vector-im/compound-{/,}**"],
|
||||
"extends": ["schedule:weekly"]
|
||||
},
|
||||
{
|
||||
"groupName": "LiveKit client",
|
||||
"matchDepNames": ["livekit-client"]
|
||||
"matchDepNames": ["livekit-client"],
|
||||
"extends": ["schedule:weekly"]
|
||||
},
|
||||
{
|
||||
"groupName": "LiveKit components",
|
||||
"matchPackageNames": ["@livekit/components-{/,}**"]
|
||||
"matchPackageNames": ["@livekit/components-{/,}**"],
|
||||
"extends": ["schedule:weekly"]
|
||||
},
|
||||
{
|
||||
"groupName": "Vaul",
|
||||
"matchDepNames": ["vaul"],
|
||||
"extends": ["schedule:monthly"],
|
||||
"prHeader": "Please review modals on mobile for visual regressions."
|
||||
}
|
||||
],
|
||||
"semanticCommits": "disabled",
|
||||
"ignoreDeps": ["posthog-js"]
|
||||
"ignoreDeps": ["posthog-js"],
|
||||
"vulnerabilityAlerts": {
|
||||
"schedule": ["at any time"],
|
||||
"prHourlyLimit": 0,
|
||||
"minimumReleaseAge": null
|
||||
}
|
||||
}
|
||||
|
||||
2
src/@types/global.d.ts
vendored
2
src/@types/global.d.ts
vendored
@@ -7,7 +7,7 @@ Please see LICENSE in the repository root for full details.
|
||||
|
||||
import "matrix-js-sdk/src/@types/global";
|
||||
import type { DurationFormat as PolyfillDurationFormat } from "@formatjs/intl-durationformat";
|
||||
import { Controls } from "../controls";
|
||||
import { type Controls } from "../controls";
|
||||
|
||||
declare global {
|
||||
interface Document {
|
||||
|
||||
2
src/@types/i18next.d.ts
vendored
2
src/@types/i18next.d.ts
vendored
@@ -7,7 +7,7 @@ Please see LICENSE in the repository root for full details.
|
||||
|
||||
import "i18next";
|
||||
// import all namespaces (for the default language, only)
|
||||
import app from "../../locales/en-GB/app.json";
|
||||
import type app from "../../locales/en/app.json";
|
||||
|
||||
declare module "i18next" {
|
||||
interface CustomTypeOptions {
|
||||
|
||||
12
src/@types/matrix-js-sdk.d.ts
vendored
12
src/@types/matrix-js-sdk.d.ts
vendored
@@ -6,8 +6,8 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import {
|
||||
ElementCallReactionEventType,
|
||||
ECallReactionEventContent,
|
||||
type ElementCallReactionEventType,
|
||||
type ECallReactionEventContent,
|
||||
} from "../reactions";
|
||||
|
||||
// Extend Matrix JS SDK types via Typescript declaration merging to support unspecced event fields and types
|
||||
@@ -15,4 +15,12 @@ declare module "matrix-js-sdk/src/types" {
|
||||
export interface TimelineEvents {
|
||||
[ElementCallReactionEventType]: ECallReactionEventContent;
|
||||
}
|
||||
|
||||
export interface AccountDataEvents {
|
||||
// Analytics account data event
|
||||
"im.vector.analytics": {
|
||||
id: string;
|
||||
pseudonymousAnalyticsOptIn?: boolean;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
53
src/App.tsx
53
src/App.tsx
@@ -5,15 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { FC, Suspense, useEffect, useState } from "react";
|
||||
import {
|
||||
BrowserRouter as Router,
|
||||
Switch,
|
||||
Route,
|
||||
useLocation,
|
||||
} from "react-router-dom";
|
||||
import { type FC, type JSX, Suspense, useEffect, useState } from "react";
|
||||
import { BrowserRouter, Route, useLocation, Routes } from "react-router-dom";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { History } from "history";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
@@ -22,14 +16,14 @@ import { LoginPage } from "./auth/LoginPage";
|
||||
import { RegisterPage } from "./auth/RegisterPage";
|
||||
import { RoomPage } from "./room/RoomPage";
|
||||
import { ClientProvider } from "./ClientContext";
|
||||
import { CrashView, LoadingView } from "./FullScreenView";
|
||||
import { ErrorPage, LoadingPage } from "./FullScreenView";
|
||||
import { DisconnectedBanner } from "./DisconnectedBanner";
|
||||
import { Initializer } from "./initializer";
|
||||
import { MediaDevicesProvider } from "./livekit/MediaDevicesContext";
|
||||
import { widget } from "./widget";
|
||||
import { useTheme } from "./useTheme";
|
||||
|
||||
const SentryRoute = Sentry.withSentryRouting(Route);
|
||||
const SentryRoute = Sentry.withSentryReactRouterV7Routing(Route);
|
||||
|
||||
interface SimpleProviderProps {
|
||||
children: JSX.Element;
|
||||
@@ -55,11 +49,7 @@ const ThemeProvider: FC<SimpleProviderProps> = ({ children }) => {
|
||||
return children;
|
||||
};
|
||||
|
||||
interface AppProps {
|
||||
history: History;
|
||||
}
|
||||
|
||||
export const App: FC<AppProps> = ({ history }) => {
|
||||
export const App: FC = () => {
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
useEffect(() => {
|
||||
Initializer.init()
|
||||
@@ -71,12 +61,10 @@ export const App: FC<AppProps> = ({ history }) => {
|
||||
.catch(logger.error);
|
||||
});
|
||||
|
||||
const errorPage = <CrashView />;
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
<Router history={history}>
|
||||
<BrowserRouter>
|
||||
<BackgroundProvider>
|
||||
<ThemeProvider>
|
||||
<TooltipProvider>
|
||||
@@ -84,32 +72,27 @@ export const App: FC<AppProps> = ({ history }) => {
|
||||
<Suspense fallback={null}>
|
||||
<ClientProvider>
|
||||
<MediaDevicesProvider>
|
||||
<Sentry.ErrorBoundary fallback={errorPage}>
|
||||
<Sentry.ErrorBoundary fallback={ErrorPage}>
|
||||
<DisconnectedBanner />
|
||||
<Switch>
|
||||
<SentryRoute exact path="/">
|
||||
<HomePage />
|
||||
</SentryRoute>
|
||||
<SentryRoute exact path="/login">
|
||||
<LoginPage />
|
||||
</SentryRoute>
|
||||
<SentryRoute exact path="/register">
|
||||
<RegisterPage />
|
||||
</SentryRoute>
|
||||
<SentryRoute path="*">
|
||||
<RoomPage />
|
||||
</SentryRoute>
|
||||
</Switch>
|
||||
<Routes>
|
||||
<SentryRoute path="/" element={<HomePage />} />
|
||||
<SentryRoute path="/login" element={<LoginPage />} />
|
||||
<SentryRoute
|
||||
path="/register"
|
||||
element={<RegisterPage />}
|
||||
/>
|
||||
<SentryRoute path="*" element={<RoomPage />} />
|
||||
</Routes>
|
||||
</Sentry.ErrorBoundary>
|
||||
</MediaDevicesProvider>
|
||||
</ClientProvider>
|
||||
</Suspense>
|
||||
) : (
|
||||
<LoadingView />
|
||||
<LoadingPage />
|
||||
)}
|
||||
</TooltipProvider>
|
||||
</ThemeProvider>
|
||||
</BackgroundProvider>
|
||||
</Router>
|
||||
</BrowserRouter>
|
||||
);
|
||||
};
|
||||
|
||||
156
src/Avatar.test.tsx
Normal file
156
src/Avatar.test.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { type FC, type PropsWithChildren } from "react";
|
||||
|
||||
import { ClientContextProvider } from "./ClientContext";
|
||||
import { Avatar } from "./Avatar";
|
||||
import { mockMatrixRoomMember, mockRtcMembership } from "./utils/test";
|
||||
|
||||
const TestComponent: FC<
|
||||
PropsWithChildren<{ client: MatrixClient; supportsThumbnails?: boolean }>
|
||||
> = ({ client, children, supportsThumbnails }) => {
|
||||
return (
|
||||
<ClientContextProvider
|
||||
value={{
|
||||
state: "valid",
|
||||
disconnected: false,
|
||||
supportedFeatures: {
|
||||
reactions: true,
|
||||
thumbnails: supportsThumbnails ?? true,
|
||||
},
|
||||
setClient: vi.fn(),
|
||||
authenticated: {
|
||||
client,
|
||||
isPasswordlessUser: true,
|
||||
changePassword: vi.fn(),
|
||||
logout: vi.fn(),
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ClientContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
test("should just render a placeholder when the user has no avatar", () => {
|
||||
const client = vi.mocked<MatrixClient>({
|
||||
getAccessToken: () => "my-access-token",
|
||||
mxcUrlToHttp: () => vi.fn(),
|
||||
} as unknown as MatrixClient);
|
||||
|
||||
vi.spyOn(client, "mxcUrlToHttp");
|
||||
const member = mockMatrixRoomMember(
|
||||
mockRtcMembership("@alice:example.org", "AAAA"),
|
||||
{
|
||||
getMxcAvatarUrl: () => undefined,
|
||||
},
|
||||
);
|
||||
const displayName = "Alice";
|
||||
render(
|
||||
<TestComponent client={client}>
|
||||
<Avatar
|
||||
id={member.userId}
|
||||
name={displayName}
|
||||
size={96}
|
||||
src={member.getMxcAvatarUrl()}
|
||||
/>
|
||||
</TestComponent>,
|
||||
);
|
||||
const element = screen.getByRole("img", { name: "@alice:example.org" });
|
||||
expect(element.tagName).toEqual("SPAN");
|
||||
expect(client.mxcUrlToHttp).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
test("should just render a placeholder when thumbnails are not supported", () => {
|
||||
const client = vi.mocked<MatrixClient>({
|
||||
getAccessToken: () => "my-access-token",
|
||||
mxcUrlToHttp: () => vi.fn(),
|
||||
} as unknown as MatrixClient);
|
||||
|
||||
vi.spyOn(client, "mxcUrlToHttp");
|
||||
const member = mockMatrixRoomMember(
|
||||
mockRtcMembership("@alice:example.org", "AAAA"),
|
||||
{
|
||||
getMxcAvatarUrl: () => "mxc://example.org/alice-avatar",
|
||||
},
|
||||
);
|
||||
const displayName = "Alice";
|
||||
render(
|
||||
<TestComponent client={client} supportsThumbnails={false}>
|
||||
<Avatar
|
||||
id={member.userId}
|
||||
name={displayName}
|
||||
size={96}
|
||||
src={member.getMxcAvatarUrl()}
|
||||
/>
|
||||
</TestComponent>,
|
||||
);
|
||||
const element = screen.getByRole("img", { name: "@alice:example.org" });
|
||||
expect(element.tagName).toEqual("SPAN");
|
||||
expect(client.mxcUrlToHttp).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
test("should attempt to fetch authenticated media", async () => {
|
||||
const expectedAuthUrl = "http://example.org/media/alice-avatar";
|
||||
const expectedObjectURL = "my-object-url";
|
||||
const accessToken = "my-access-token";
|
||||
const theBlob = new Blob([]);
|
||||
|
||||
// vitest doesn't have a implementation of create/revokeObjectURL, so we need
|
||||
// to delete the property. It's a bit odd, but it works.
|
||||
Reflect.deleteProperty(global.window.URL, "createObjectURL");
|
||||
globalThis.URL.createObjectURL = vi.fn().mockReturnValue(expectedObjectURL);
|
||||
Reflect.deleteProperty(global.window.URL, "revokeObjectURL");
|
||||
globalThis.URL.revokeObjectURL = vi.fn();
|
||||
|
||||
const fetchFn = vi.fn().mockResolvedValue({
|
||||
blob: async () => Promise.resolve(theBlob),
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchFn);
|
||||
|
||||
const client = vi.mocked<MatrixClient>({
|
||||
getAccessToken: () => accessToken,
|
||||
mxcUrlToHttp: () => vi.fn(),
|
||||
} as unknown as MatrixClient);
|
||||
|
||||
vi.spyOn(client, "mxcUrlToHttp").mockReturnValue(expectedAuthUrl);
|
||||
const member = mockMatrixRoomMember(
|
||||
mockRtcMembership("@alice:example.org", "AAAA"),
|
||||
{
|
||||
getMxcAvatarUrl: () => "mxc://example.org/alice-avatar",
|
||||
},
|
||||
);
|
||||
const displayName = "Alice";
|
||||
render(
|
||||
<TestComponent client={client}>
|
||||
<Avatar
|
||||
id={member.userId}
|
||||
name={displayName}
|
||||
size={96}
|
||||
src={member.getMxcAvatarUrl()}
|
||||
/>
|
||||
</TestComponent>,
|
||||
);
|
||||
|
||||
// Fetch is asynchronous, so wait for this to resolve.
|
||||
await vi.waitUntil(() =>
|
||||
document.querySelector(`img[src='${expectedObjectURL}']`),
|
||||
);
|
||||
|
||||
expect(client.mxcUrlToHttp).toBeCalledTimes(1);
|
||||
expect(globalThis.fetch).toBeCalledWith(expectedAuthUrl, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
});
|
||||
@@ -5,11 +5,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { useMemo, FC } from "react";
|
||||
import {
|
||||
useMemo,
|
||||
type FC,
|
||||
type CSSProperties,
|
||||
useState,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import { Avatar as CompoundAvatar } from "@vector-im/compound-web";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/client";
|
||||
|
||||
import { getAvatarUrl } from "./utils/matrix";
|
||||
import { useClient } from "./ClientContext";
|
||||
import { useClientState } from "./ClientContext";
|
||||
|
||||
export enum Size {
|
||||
XS = "xs",
|
||||
@@ -27,12 +33,35 @@ export const sizes = new Map([
|
||||
[Size.XL, 90],
|
||||
]);
|
||||
|
||||
interface Props {
|
||||
export interface Props {
|
||||
id: string;
|
||||
name: string;
|
||||
className?: string;
|
||||
src?: string;
|
||||
size?: Size | number;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export function getAvatarUrl(
|
||||
client: MatrixClient,
|
||||
mxcUrl: string | null,
|
||||
avatarSize = 96,
|
||||
): string | null {
|
||||
const width = Math.floor(avatarSize * window.devicePixelRatio);
|
||||
const height = Math.floor(avatarSize * window.devicePixelRatio);
|
||||
// scale is more suitable for larger sizes
|
||||
const resizeMethod = avatarSize <= 96 ? "crop" : "scale";
|
||||
return mxcUrl
|
||||
? client.mxcUrlToHttp(
|
||||
mxcUrl,
|
||||
width,
|
||||
height,
|
||||
resizeMethod,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
: null;
|
||||
}
|
||||
|
||||
export const Avatar: FC<Props> = ({
|
||||
@@ -41,8 +70,10 @@ export const Avatar: FC<Props> = ({
|
||||
name,
|
||||
src,
|
||||
size = Size.MD,
|
||||
style,
|
||||
...props
|
||||
}) => {
|
||||
const { client } = useClient();
|
||||
const clientState = useClientState();
|
||||
|
||||
const sizePx = useMemo(
|
||||
() =>
|
||||
@@ -52,10 +83,50 @@ export const Avatar: FC<Props> = ({
|
||||
[size],
|
||||
);
|
||||
|
||||
const resolvedSrc = useMemo(() => {
|
||||
if (!client || !src || !sizePx) return undefined;
|
||||
return src.startsWith("mxc://") ? getAvatarUrl(client, src, sizePx) : src;
|
||||
}, [client, src, sizePx]);
|
||||
const [avatarUrl, setAvatarUrl] = useState<string | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (clientState?.state !== "valid") {
|
||||
return;
|
||||
}
|
||||
const { authenticated, supportedFeatures } = clientState;
|
||||
const client = authenticated?.client;
|
||||
|
||||
if (!client || !src || !sizePx || !supportedFeatures.thumbnails) {
|
||||
return;
|
||||
}
|
||||
|
||||
const token = client.getAccessToken();
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
const resolveSrc = getAvatarUrl(client, src, sizePx);
|
||||
if (!resolveSrc) {
|
||||
setAvatarUrl(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
let objectUrl: string | undefined;
|
||||
fetch(resolveSrc, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
.then(async (req) => req.blob())
|
||||
.then((blob) => {
|
||||
objectUrl = URL.createObjectURL(blob);
|
||||
setAvatarUrl(objectUrl);
|
||||
})
|
||||
.catch((ex) => {
|
||||
setAvatarUrl(undefined);
|
||||
});
|
||||
|
||||
return (): void => {
|
||||
if (objectUrl) {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
}
|
||||
};
|
||||
}, [clientState, src, sizePx]);
|
||||
|
||||
return (
|
||||
<CompoundAvatar
|
||||
@@ -63,7 +134,9 @@ export const Avatar: FC<Props> = ({
|
||||
id={id}
|
||||
name={name}
|
||||
size={`${sizePx}px`}
|
||||
src={resolvedSrc}
|
||||
src={avatarUrl}
|
||||
style={style}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import {
|
||||
FC,
|
||||
type FC,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
@@ -14,22 +14,22 @@ import {
|
||||
useContext,
|
||||
useRef,
|
||||
useMemo,
|
||||
type JSX,
|
||||
} from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
|
||||
import { type ISyncStateData, type SyncState } from "matrix-js-sdk/src/sync";
|
||||
import { ClientEvent, type MatrixClient } from "matrix-js-sdk/src/client";
|
||||
|
||||
import type { WidgetApi } from "matrix-widget-api";
|
||||
import { ErrorView } from "./FullScreenView";
|
||||
import { ErrorPage } from "./FullScreenView";
|
||||
import { widget } from "./widget";
|
||||
import {
|
||||
PosthogAnalytics,
|
||||
RegistrationType,
|
||||
} from "./analytics/PosthogAnalytics";
|
||||
import { translatedError } from "./TranslatedError";
|
||||
import { useEventTarget } from "./useEvents";
|
||||
import { OpenElsewhereError } from "./RichError";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -48,6 +48,7 @@ export type ValidClientState = {
|
||||
disconnected: boolean;
|
||||
supportedFeatures: {
|
||||
reactions: boolean;
|
||||
thumbnails: boolean;
|
||||
};
|
||||
setClient: (params?: SetClientParams) => void;
|
||||
};
|
||||
@@ -71,6 +72,8 @@ export type SetClientParams = {
|
||||
|
||||
const ClientContext = createContext<ClientState | undefined>(undefined);
|
||||
|
||||
export const ClientContextProvider = ClientContext.Provider;
|
||||
|
||||
export const useClientState = (): ClientState | undefined =>
|
||||
useContext(ClientContext);
|
||||
|
||||
@@ -141,7 +144,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const ClientProvider: FC<Props> = ({ children }) => {
|
||||
const history = useHistory();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// null = signed out, undefined = loading
|
||||
const [initClientState, setInitClientState] = useState<
|
||||
@@ -225,11 +228,9 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||
await client.clearStores();
|
||||
clearSession();
|
||||
setInitClientState(null);
|
||||
history.push("/");
|
||||
await navigate("/");
|
||||
PosthogAnalytics.instance.setRegistrationType(RegistrationType.Guest);
|
||||
}, [history, initClientState?.client]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
}, [navigate, initClientState?.client]);
|
||||
|
||||
// To protect against multiple sessions writing to the same storage
|
||||
// simultaneously, we send a broadcast message that shuts down all other
|
||||
@@ -247,12 +248,13 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||
"message",
|
||||
useCallback(() => {
|
||||
initClientState?.client.stopClient();
|
||||
setAlreadyOpenedErr(translatedError("application_opened_another_tab", t));
|
||||
}, [initClientState?.client, setAlreadyOpenedErr, t]),
|
||||
setAlreadyOpenedErr(new OpenElsewhereError());
|
||||
}, [initClientState?.client, setAlreadyOpenedErr]),
|
||||
);
|
||||
|
||||
const [isDisconnected, setIsDisconnected] = useState(false);
|
||||
const [supportsReactions, setSupportsReactions] = useState(false);
|
||||
const [supportsThumbnails, setSupportsThumbnails] = useState(false);
|
||||
|
||||
const state: ClientState | undefined = useMemo(() => {
|
||||
if (alreadyOpenedErr) {
|
||||
@@ -278,6 +280,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||
disconnected: isDisconnected,
|
||||
supportedFeatures: {
|
||||
reactions: supportsReactions,
|
||||
thumbnails: supportsThumbnails,
|
||||
},
|
||||
};
|
||||
}, [
|
||||
@@ -288,6 +291,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||
setClient,
|
||||
isDisconnected,
|
||||
supportsReactions,
|
||||
supportsThumbnails,
|
||||
]);
|
||||
|
||||
const onSync = useCallback(
|
||||
@@ -313,6 +317,8 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||
}
|
||||
|
||||
if (initClientState.widgetApi) {
|
||||
// There is currently no widget API for authenticated media thumbnails.
|
||||
setSupportsThumbnails(false);
|
||||
const reactSend = initClientState.widgetApi.hasCapability(
|
||||
"org.matrix.msc2762.send.event:m.reaction",
|
||||
);
|
||||
@@ -334,6 +340,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||
}
|
||||
} else {
|
||||
setSupportsReactions(true);
|
||||
setSupportsThumbnails(true);
|
||||
}
|
||||
|
||||
return (): void => {
|
||||
@@ -344,7 +351,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||
}, [initClientState, onSync]);
|
||||
|
||||
if (alreadyOpenedErr) {
|
||||
return <ErrorView error={alreadyOpenedErr} />;
|
||||
return <ErrorPage error={alreadyOpenedErr} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,11 +6,11 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import classNames from "classnames";
|
||||
import { FC, HTMLAttributes, ReactNode } from "react";
|
||||
import { type FC, type HTMLAttributes, type ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import styles from "./DisconnectedBanner.module.css";
|
||||
import { ValidClientState, useClientState } from "./ClientContext";
|
||||
import { type ValidClientState, useClientState } from "./ClientContext";
|
||||
|
||||
interface Props extends HTMLAttributes<HTMLElement> {
|
||||
children?: ReactNode;
|
||||
|
||||
21
src/ErrorView.module.css
Normal file
21
src/ErrorView.module.css
Normal file
@@ -0,0 +1,21 @@
|
||||
.error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--cpd-space-2x);
|
||||
max-inline-size: 480px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-block-end: var(--cpd-space-4x);
|
||||
}
|
||||
|
||||
.error > h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.error > p {
|
||||
font: var(--cpd-font-body-lg-regular);
|
||||
color: var(--cpd-color-text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
82
src/ErrorView.tsx
Normal file
82
src/ErrorView.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { BigIcon, Button, Heading } from "@vector-im/compound-web";
|
||||
import {
|
||||
useCallback,
|
||||
type ComponentType,
|
||||
type FC,
|
||||
type ReactNode,
|
||||
type SVGAttributes,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { RageshakeButton } from "./settings/RageshakeButton";
|
||||
import styles from "./ErrorView.module.css";
|
||||
import { useUrlParams } from "./UrlParams";
|
||||
import { LinkButton } from "./button";
|
||||
|
||||
interface Props {
|
||||
Icon: ComponentType<SVGAttributes<SVGElement>>;
|
||||
title: string;
|
||||
/**
|
||||
* Show an option to submit a rageshake.
|
||||
* @default false
|
||||
*/
|
||||
rageshake?: boolean;
|
||||
/**
|
||||
* Whether the error is considered fatal, i.e. non-recoverable. Causes the app
|
||||
* to fully reload when clicking 'return to home'.
|
||||
* @default false
|
||||
*/
|
||||
fatal?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const ErrorView: FC<Props> = ({
|
||||
Icon,
|
||||
title,
|
||||
rageshake,
|
||||
fatal,
|
||||
children,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { confineToRoom } = useUrlParams();
|
||||
|
||||
const onReload = useCallback(() => {
|
||||
window.location.href = "/";
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.error}>
|
||||
<BigIcon className={styles.icon}>
|
||||
<Icon />
|
||||
</BigIcon>
|
||||
<Heading as="h1" weight="semibold" size="md">
|
||||
{title}
|
||||
</Heading>
|
||||
{children}
|
||||
{rageshake && (
|
||||
<RageshakeButton description={`***Error View***: ${title}`} />
|
||||
)}
|
||||
{!confineToRoom &&
|
||||
(fatal || location.pathname === "/" ? (
|
||||
<Button
|
||||
kind="tertiary"
|
||||
className={styles.homeLink}
|
||||
onClick={onReload}
|
||||
>
|
||||
{t("return_home_button")}
|
||||
</Button>
|
||||
) : (
|
||||
<LinkButton kind="tertiary" className={styles.homeLink} to="/">
|
||||
{t("return_home_button")}
|
||||
</LinkButton>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -5,21 +5,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { FC, ReactNode, useCallback, useEffect } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { type FC, type ReactElement, type ReactNode, useEffect } from "react";
|
||||
import classNames from "classnames";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { Button } from "@vector-im/compound-web";
|
||||
import { ErrorIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import { Header, HeaderLogo, LeftNav, RightNav } from "./Header";
|
||||
import { LinkButton } from "./button";
|
||||
import styles from "./FullScreenView.module.css";
|
||||
import { TranslatedError } from "./TranslatedError";
|
||||
import { Config } from "./config/Config";
|
||||
import { RageshakeButton } from "./settings/RageshakeButton";
|
||||
import { useUrlParams } from "./UrlParams";
|
||||
import { RichError } from "./RichError";
|
||||
import { ErrorView } from "./ErrorView";
|
||||
|
||||
interface FullScreenViewProps {
|
||||
className?: string;
|
||||
@@ -44,74 +41,33 @@ export const FullScreenView: FC<FullScreenViewProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
interface ErrorViewProps {
|
||||
error: Error;
|
||||
interface ErrorPageProps {
|
||||
error: Error | unknown;
|
||||
}
|
||||
|
||||
export const ErrorView: FC<ErrorViewProps> = ({ error }) => {
|
||||
const location = useLocation();
|
||||
const { confineToRoom } = useUrlParams();
|
||||
// Due to this component being used as the crash fallback for Sentry, which has
|
||||
// weird type requirements, we can't just give this a type of FC<ErrorPageProps>
|
||||
export const ErrorPage = ({ error }: ErrorPageProps): ReactElement => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
logger.error(error);
|
||||
Sentry.captureException(error);
|
||||
}, [error]);
|
||||
|
||||
const onReload = useCallback(() => {
|
||||
window.location.href = "/";
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<FullScreenView>
|
||||
<h1>{t("common.error")}</h1>
|
||||
<p>
|
||||
{error instanceof TranslatedError
|
||||
? error.translatedMessage
|
||||
: error.message}
|
||||
</p>
|
||||
<RageshakeButton description={`***Error View***: ${error.message}`} />
|
||||
{!confineToRoom &&
|
||||
(location.pathname === "/" ? (
|
||||
<Button className={styles.homeLink} onClick={onReload}>
|
||||
{t("return_home_button")}
|
||||
</Button>
|
||||
) : (
|
||||
<LinkButton className={styles.homeLink} to="/">
|
||||
{t("return_home_button")}
|
||||
</LinkButton>
|
||||
))}
|
||||
</FullScreenView>
|
||||
);
|
||||
};
|
||||
|
||||
export const CrashView: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onReload = useCallback(() => {
|
||||
window.location.href = "/";
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<FullScreenView>
|
||||
<Trans i18nKey="full_screen_view_h1">
|
||||
<h1>Oops, something's gone wrong.</h1>
|
||||
</Trans>
|
||||
{Config.get().rageshake?.submit_url && (
|
||||
<Trans i18nKey="full_screen_view_description">
|
||||
<p>Submitting debug logs will help us track down the problem.</p>
|
||||
</Trans>
|
||||
{error instanceof RichError ? (
|
||||
error.richMessage
|
||||
) : (
|
||||
<ErrorView Icon={ErrorIcon} title={t("error.generic")} rageshake fatal>
|
||||
<p>{t("error.generic_description")}</p>
|
||||
</ErrorView>
|
||||
)}
|
||||
|
||||
<RageshakeButton description="***Soft Crash***" />
|
||||
<Button className={styles.wideButton} onClick={onReload}>
|
||||
{t("return_home_button")}
|
||||
</Button>
|
||||
</FullScreenView>
|
||||
);
|
||||
};
|
||||
|
||||
export const LoadingView: FC = () => {
|
||||
export const LoadingPage: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,7 +6,12 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import classNames from "classnames";
|
||||
import { FC, HTMLAttributes, ReactNode, forwardRef } from "react";
|
||||
import {
|
||||
type FC,
|
||||
type HTMLAttributes,
|
||||
type ReactNode,
|
||||
forwardRef,
|
||||
} from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Heading, Text } from "@vector-im/compound-web";
|
||||
|
||||
@@ -5,10 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { expect, test } from "vitest";
|
||||
import { expect, test, afterEach } from "vitest";
|
||||
import { render } from "@testing-library/react";
|
||||
import { ReactNode, useState } from "react";
|
||||
import { afterEach } from "node:test";
|
||||
import { type ReactNode, useState } from "react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { Modal } from "./Modal";
|
||||
|
||||
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { FC, ReactNode, useCallback } from "react";
|
||||
import { type FC, type ReactNode, useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Root as DialogRoot,
|
||||
|
||||
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { type FC, useEffect, useState } from "react";
|
||||
import { toDataURL } from "qrcode";
|
||||
import classNames from "classnames";
|
||||
import { t } from "i18next";
|
||||
|
||||
20
src/RTCConnectionStats.module.css
Normal file
20
src/RTCConnectionStats.module.css
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
.modal pre {
|
||||
font-size: var(--font-size-micro);
|
||||
}
|
||||
|
||||
.statsPill {
|
||||
border-radius: var(--media-view-border-radius);
|
||||
grid-area: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
112
src/RTCConnectionStats.tsx
Normal file
112
src/RTCConnectionStats.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
Copyright 2023, 2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { useState, type FC } from "react";
|
||||
import { Button, Text } from "@vector-im/compound-web";
|
||||
import {
|
||||
MicOnSolidIcon,
|
||||
VideoCallSolidIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { Modal } from "./Modal";
|
||||
import styles from "./RTCConnectionStats.module.css";
|
||||
import mediaViewStyles from "../src/tile/MediaView.module.css";
|
||||
interface Props {
|
||||
audio?: RTCInboundRtpStreamStats | RTCOutboundRtpStreamStats;
|
||||
video?: RTCInboundRtpStreamStats | RTCOutboundRtpStreamStats;
|
||||
}
|
||||
|
||||
// This is only used in developer mode for debugging purposes, so we don't need full localization
|
||||
export const RTCConnectionStats: FC<Props> = ({ audio, video, ...rest }) => {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [modalContents, setModalContents] = useState<
|
||||
"video" | "audio" | "none"
|
||||
>("none");
|
||||
|
||||
const showFullModal = (contents: "video" | "audio"): void => {
|
||||
setShowModal(true);
|
||||
setModalContents(contents);
|
||||
};
|
||||
|
||||
const onDismissModal = (): void => {
|
||||
setShowModal(false);
|
||||
setModalContents("none");
|
||||
};
|
||||
return (
|
||||
<div className={classNames(mediaViewStyles.nameTag, styles.statsPill)}>
|
||||
<Modal
|
||||
title="RTC Connection Stats"
|
||||
open={showModal}
|
||||
onDismiss={onDismissModal}
|
||||
>
|
||||
<div className={styles.modal}>
|
||||
<pre>
|
||||
{modalContents !== "none" &&
|
||||
JSON.stringify(
|
||||
modalContents === "video" ? video : audio,
|
||||
null,
|
||||
2,
|
||||
)}
|
||||
</pre>
|
||||
</div>
|
||||
</Modal>
|
||||
{audio && (
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => showFullModal("audio")}
|
||||
size="sm"
|
||||
kind="tertiary"
|
||||
Icon={MicOnSolidIcon}
|
||||
>
|
||||
{"jitter" in audio && typeof audio.jitter === "number" && (
|
||||
<Text as="span" size="xs" title="jitter">
|
||||
{(audio.jitter * 1000).toFixed(0)}ms
|
||||
</Text>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{video && (
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => showFullModal("video")}
|
||||
size="sm"
|
||||
kind="tertiary"
|
||||
Icon={VideoCallSolidIcon}
|
||||
>
|
||||
{!!video?.framesPerSecond && (
|
||||
<Text as="span" size="xs" title="frame rate">
|
||||
{video.framesPerSecond.toFixed(0)}fps
|
||||
</Text>
|
||||
)}
|
||||
{"jitter" in video && typeof video.jitter === "number" && (
|
||||
<Text as="span" size="xs" title="jitter">
|
||||
{(video.jitter * 1000).toFixed(0)}ms
|
||||
</Text>
|
||||
)}
|
||||
{"frameHeight" in video &&
|
||||
typeof video.frameHeight === "number" &&
|
||||
"frameWidth" in video &&
|
||||
typeof video.frameWidth === "number" && (
|
||||
<Text as="span" size="xs" title="frame size">
|
||||
{video.frameWidth}x{video.frameHeight}
|
||||
</Text>
|
||||
)}
|
||||
{"qualityLimitationReason" in video &&
|
||||
typeof video.qualityLimitationReason === "string" &&
|
||||
video.qualityLimitationReason !== "none" && (
|
||||
<Text as="span" size="xs" title="quality limitation reason">
|
||||
{video.qualityLimitationReason}
|
||||
</Text>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
48
src/RichError.tsx
Normal file
48
src/RichError.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type FC, type ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { PopOutIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import { ErrorView } from "./ErrorView";
|
||||
|
||||
/**
|
||||
* An error consisting of a terse message to be logged to the console and a
|
||||
* richer message to be shown to the user, as a full-screen page.
|
||||
*/
|
||||
export class RichError extends Error {
|
||||
public constructor(
|
||||
message: string,
|
||||
/**
|
||||
* The pretty, more helpful message to be shown on the error screen.
|
||||
*/
|
||||
public readonly richMessage: ReactNode,
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
const OpenElsewhere: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ErrorView Icon={PopOutIcon} title={t("error.open_elsewhere")}>
|
||||
<p>
|
||||
{t("error.open_elsewhere_description", {
|
||||
brand: import.meta.env.VITE_PRODUCT_NAME || "Element Call",
|
||||
})}
|
||||
</p>
|
||||
</ErrorView>
|
||||
);
|
||||
};
|
||||
|
||||
export class OpenElsewhereError extends RichError {
|
||||
public constructor() {
|
||||
super("App opened in another tab", <OpenElsewhere />);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { FC, useCallback } from "react";
|
||||
import { type FC, useCallback } from "react";
|
||||
import { Root, Track, Range, Thumb } from "@radix-ui/react-slider";
|
||||
import classNames from "classnames";
|
||||
import { Tooltip } from "@vector-im/compound-web";
|
||||
@@ -16,6 +16,9 @@ interface Props {
|
||||
className?: string;
|
||||
label: string;
|
||||
value: number;
|
||||
/**
|
||||
* Event handler called when the value changes during an interaction.
|
||||
*/
|
||||
onValueChange: (value: number) => void;
|
||||
/**
|
||||
* Event handler called when the value changes at the end of an interaction.
|
||||
|
||||
@@ -6,9 +6,9 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import {
|
||||
ComponentType,
|
||||
FC,
|
||||
SVGAttributes,
|
||||
type ComponentType,
|
||||
type FC,
|
||||
type SVGAttributes,
|
||||
useCallback,
|
||||
useEffect,
|
||||
} from "react";
|
||||
|
||||
@@ -9,6 +9,9 @@ import type { DefaultNamespace, ParseKeys, TFunction, TOptions } from "i18next";
|
||||
|
||||
/**
|
||||
* An error with messages in both English and the user's preferred language.
|
||||
* Use this for errors that need to be displayed inline within another
|
||||
* component. For errors that could be given their own screen, prefer
|
||||
* {@link RichError}.
|
||||
*/
|
||||
// Abstract to force consumers to use the function below rather than calling the
|
||||
// constructor directly
|
||||
@@ -22,7 +25,7 @@ export abstract class TranslatedError extends Error {
|
||||
messageKey: ParseKeys<DefaultNamespace, TOptions>,
|
||||
translationFn: TFunction<DefaultNamespace>,
|
||||
) {
|
||||
super(translationFn(messageKey, { lng: "en-GB" } as TOptions));
|
||||
super(translationFn(messageKey, { lng: "en" } as TOptions));
|
||||
this.translatedMessage = translationFn(messageKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,11 @@ Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { getRoomIdentifierFromUrl } from "../src/UrlParams";
|
||||
import {
|
||||
getRoomIdentifierFromUrl,
|
||||
getUrlParams,
|
||||
UserIntent,
|
||||
} from "../src/UrlParams";
|
||||
|
||||
const ROOM_NAME = "roomNameHere";
|
||||
const ROOM_ID = "!d45f138fsd";
|
||||
@@ -86,4 +90,157 @@ describe("UrlParams", () => {
|
||||
.roomAlias,
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
describe("preload", () => {
|
||||
it("defaults to false", () => {
|
||||
expect(getUrlParams().preload).toBe(false);
|
||||
});
|
||||
|
||||
it("ignored in SPA mode", () => {
|
||||
expect(getUrlParams("?preload=true").preload).toBe(false);
|
||||
});
|
||||
|
||||
it("respected in widget mode", () => {
|
||||
expect(
|
||||
getUrlParams(
|
||||
"?preload=true&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||
).preload,
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("returnToLobby", () => {
|
||||
it("is true in SPA mode", () => {
|
||||
expect(getUrlParams("?returnToLobby=false").returnToLobby).toBe(true);
|
||||
});
|
||||
|
||||
it("defaults to false in widget mode", () => {
|
||||
expect(
|
||||
getUrlParams("?widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo")
|
||||
.returnToLobby,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("respected in widget mode", () => {
|
||||
expect(
|
||||
getUrlParams(
|
||||
"?returnToLobby=true&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||
).returnToLobby,
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("userId", () => {
|
||||
it("is ignored in SPA mode", () => {
|
||||
expect(getUrlParams("?userId=asd").userId).toBe(null);
|
||||
});
|
||||
|
||||
it("is parsed in widget mode", () => {
|
||||
expect(
|
||||
getUrlParams(
|
||||
"?userId=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||
).userId,
|
||||
).toBe("asd");
|
||||
});
|
||||
});
|
||||
|
||||
describe("deviceId", () => {
|
||||
it("is ignored in SPA mode", () => {
|
||||
expect(getUrlParams("?deviceId=asd").deviceId).toBe(null);
|
||||
});
|
||||
|
||||
it("is parsed in widget mode", () => {
|
||||
expect(
|
||||
getUrlParams(
|
||||
"?deviceId=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||
).deviceId,
|
||||
).toBe("asd");
|
||||
});
|
||||
});
|
||||
|
||||
describe("baseUrl", () => {
|
||||
it("is ignored in SPA mode", () => {
|
||||
expect(getUrlParams("?baseUrl=asd").baseUrl).toBe(null);
|
||||
});
|
||||
|
||||
it("is parsed in widget mode", () => {
|
||||
expect(
|
||||
getUrlParams(
|
||||
"?baseUrl=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||
).baseUrl,
|
||||
).toBe("asd");
|
||||
});
|
||||
});
|
||||
|
||||
describe("viaServers", () => {
|
||||
it("is ignored in widget mode", () => {
|
||||
expect(
|
||||
getUrlParams(
|
||||
"?viaServers=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||
).viaServers,
|
||||
).toBe(null);
|
||||
});
|
||||
|
||||
it("is parsed in SPA mode", () => {
|
||||
expect(getUrlParams("?viaServers=asd").viaServers).toBe("asd");
|
||||
});
|
||||
});
|
||||
|
||||
describe("homeserver", () => {
|
||||
it("is ignored in widget mode", () => {
|
||||
expect(
|
||||
getUrlParams(
|
||||
"?homeserver=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||
).homeserver,
|
||||
).toBe(null);
|
||||
});
|
||||
|
||||
it("is parsed in SPA mode", () => {
|
||||
expect(getUrlParams("?homeserver=asd").homeserver).toBe("asd");
|
||||
});
|
||||
});
|
||||
|
||||
describe("intent", () => {
|
||||
it("defaults to unknown", () => {
|
||||
expect(getUrlParams().intent).toBe(UserIntent.Unknown);
|
||||
});
|
||||
|
||||
it("ignores intent if it is not a valid value", () => {
|
||||
expect(getUrlParams("?intent=foo").intent).toBe(UserIntent.Unknown);
|
||||
});
|
||||
|
||||
it("accepts start_call", () => {
|
||||
expect(getUrlParams("?intent=start_call").intent).toBe(
|
||||
UserIntent.StartNewCall,
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts join_existing", () => {
|
||||
expect(getUrlParams("?intent=join_existing").intent).toBe(
|
||||
UserIntent.JoinExistingCall,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("skipLobby", () => {
|
||||
it("defaults to false", () => {
|
||||
expect(getUrlParams().skipLobby).toBe(false);
|
||||
});
|
||||
|
||||
it("defaults to false if intent is start_call in SPA mode", () => {
|
||||
expect(getUrlParams("?intent=start_call").skipLobby).toBe(false);
|
||||
});
|
||||
|
||||
it("defaults to true if intent is start_call in widget mode", () => {
|
||||
expect(
|
||||
getUrlParams(
|
||||
"?intent=start_call&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||
).skipLobby,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("default to false if intent is join_existing", () => {
|
||||
expect(getUrlParams("?intent=join_existing").skipLobby).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useLocation } from "react-router-dom";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { Config } from "./config/Config";
|
||||
import { EncryptionSystem } from "./e2ee/sharedKeyManagement";
|
||||
import { type EncryptionSystem } from "./e2ee/sharedKeyManagement";
|
||||
import { E2eeType } from "./e2ee/e2eeType";
|
||||
|
||||
interface RoomIdentifier {
|
||||
@@ -19,6 +19,12 @@ interface RoomIdentifier {
|
||||
viaServers: string[];
|
||||
}
|
||||
|
||||
export enum UserIntent {
|
||||
StartNewCall = "start_call",
|
||||
JoinExistingCall = "join_existing",
|
||||
Unknown = "unknown",
|
||||
}
|
||||
|
||||
// If you need to add a new flag to this interface, prefer a name that describes
|
||||
// a specific behavior (such as 'confineToRoom'), rather than one that describes
|
||||
// the situations that call for this behavior ('isEmbedded'). This makes it
|
||||
@@ -142,6 +148,13 @@ export interface UrlParams {
|
||||
* creating a spa link.
|
||||
*/
|
||||
homeserver: string | null;
|
||||
|
||||
/**
|
||||
* The user's intent with respect to the call.
|
||||
* e.g. if they clicked a Start Call button, this would be `start_call`.
|
||||
* If it was a Join Call button, it would be `join_existing`.
|
||||
*/
|
||||
intent: string | null;
|
||||
}
|
||||
|
||||
// This is here as a stopgap, but what would be far nicer is a function that
|
||||
@@ -211,9 +224,17 @@ export const getUrlParams = (
|
||||
|
||||
const fontScale = parseFloat(parser.getParam("fontScale") ?? "");
|
||||
|
||||
let intent = parser.getParam("intent");
|
||||
if (!intent || !Object.values(UserIntent).includes(intent as UserIntent)) {
|
||||
intent = UserIntent.Unknown;
|
||||
}
|
||||
const widgetId = parser.getParam("widgetId");
|
||||
const parentUrl = parser.getParam("parentUrl");
|
||||
const isWidget = !!widgetId && !!parentUrl;
|
||||
|
||||
return {
|
||||
widgetId: parser.getParam("widgetId"),
|
||||
parentUrl: parser.getParam("parentUrl"),
|
||||
widgetId,
|
||||
parentUrl,
|
||||
|
||||
// NB. we don't validate roomId here as we do in getRoomIdentifierFromUrl:
|
||||
// what would we do if it were invalid? If the widget API says that's what
|
||||
@@ -224,26 +245,30 @@ export const getUrlParams = (
|
||||
confineToRoom:
|
||||
parser.getFlagParam("confineToRoom") || parser.getFlagParam("embed"),
|
||||
appPrompt: parser.getFlagParam("appPrompt", true),
|
||||
preload: parser.getFlagParam("preload"),
|
||||
preload: isWidget ? parser.getFlagParam("preload") : false,
|
||||
hideHeader: parser.getFlagParam("hideHeader"),
|
||||
showControls: parser.getFlagParam("showControls", true),
|
||||
hideScreensharing: parser.getFlagParam("hideScreensharing"),
|
||||
e2eEnabled: parser.getFlagParam("enableE2EE", true),
|
||||
userId: parser.getParam("userId"),
|
||||
userId: isWidget ? parser.getParam("userId") : null,
|
||||
displayName: parser.getParam("displayName"),
|
||||
deviceId: parser.getParam("deviceId"),
|
||||
baseUrl: parser.getParam("baseUrl"),
|
||||
deviceId: isWidget ? parser.getParam("deviceId") : null,
|
||||
baseUrl: isWidget ? parser.getParam("baseUrl") : null,
|
||||
lang: parser.getParam("lang"),
|
||||
fonts: parser.getAllParams("font"),
|
||||
fontScale: Number.isNaN(fontScale) ? null : fontScale,
|
||||
analyticsID: parser.getParam("analyticsID"),
|
||||
allowIceFallback: parser.getFlagParam("allowIceFallback"),
|
||||
perParticipantE2EE: parser.getFlagParam("perParticipantE2EE"),
|
||||
skipLobby: parser.getFlagParam("skipLobby"),
|
||||
returnToLobby: parser.getFlagParam("returnToLobby"),
|
||||
skipLobby: parser.getFlagParam(
|
||||
"skipLobby",
|
||||
isWidget && intent === UserIntent.StartNewCall,
|
||||
),
|
||||
returnToLobby: isWidget ? parser.getFlagParam("returnToLobby") : true,
|
||||
theme: parser.getParam("theme"),
|
||||
viaServers: parser.getParam("viaServers"),
|
||||
homeserver: parser.getParam("homeserver"),
|
||||
viaServers: !isWidget ? parser.getParam("viaServers") : null,
|
||||
homeserver: !isWidget ? parser.getParam("homeserver") : null,
|
||||
intent,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { FC, useMemo, useState } from "react";
|
||||
import { type FC, useMemo, useState } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Menu, MenuItem } from "@vector-im/compound-web";
|
||||
@@ -82,7 +82,7 @@ export const UserMenu: FC<Props> = ({
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<LinkButton to={{ pathname: "/login", state: { from: location } }}>
|
||||
<LinkButton to={{ pathname: "/login" }} state={{ from: location }}>
|
||||
{t("log_in")}
|
||||
</LinkButton>
|
||||
);
|
||||
|
||||
@@ -5,8 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { FC, useCallback, useState } from "react";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import { type FC, useCallback, useState } from "react";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { useClientLegacy } from "./ClientContext";
|
||||
import { useProfile } from "./profile/useProfile";
|
||||
@@ -19,7 +20,7 @@ interface Props {
|
||||
|
||||
export const UserMenuContainer: FC<Props> = ({ preventNavigation = false }) => {
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const navigate = useNavigate();
|
||||
const { client, logout, authenticated, passwordlessUser } = useClientLegacy();
|
||||
const { displayName, avatarUrl } = useProfile(client);
|
||||
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
|
||||
@@ -45,11 +46,13 @@ export const UserMenuContainer: FC<Props> = ({ preventNavigation = false }) => {
|
||||
logout?.();
|
||||
break;
|
||||
case "login":
|
||||
history.push("/login", { state: { from: location } });
|
||||
navigate("/login", { state: { from: location } })?.catch((error) =>
|
||||
logger.error("Failed to navigate to login", error),
|
||||
);
|
||||
break;
|
||||
}
|
||||
},
|
||||
[history, location, logout, setSettingsModalOpen],
|
||||
[navigate, location, logout, setSettingsModalOpen],
|
||||
);
|
||||
|
||||
const userName = client?.getUserIdLocalpart() ?? "";
|
||||
|
||||
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { FC } from "react";
|
||||
import { type FC } from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
import { ExternalLink } from "../button/Link";
|
||||
|
||||
@@ -5,9 +5,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import posthog, { CaptureOptions, PostHog, Properties } from "posthog-js";
|
||||
import posthog, {
|
||||
type CaptureOptions,
|
||||
type PostHog,
|
||||
type Properties,
|
||||
} from "posthog-js";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { Buffer } from "buffer";
|
||||
|
||||
import { widget } from "../widget";
|
||||
@@ -87,7 +91,7 @@ export class PosthogAnalytics {
|
||||
* 1. Declare a type for the event, extending IPosthogEvent.
|
||||
*/
|
||||
|
||||
private static ANALYTICS_EVENT_TYPE = "im.vector.analytics";
|
||||
private static ANALYTICS_EVENT_TYPE = "im.vector.analytics" as const;
|
||||
|
||||
// set true during the constructor if posthog config is present, otherwise false
|
||||
private static internalInstance: PosthogAnalytics | null = null;
|
||||
@@ -268,14 +272,14 @@ export class PosthogAnalytics {
|
||||
|
||||
private async getAnalyticsId(): Promise<string | null> {
|
||||
const client: MatrixClient = window.matrixclient;
|
||||
let accountAnalyticsId;
|
||||
let accountAnalyticsId: string | null;
|
||||
if (widget) {
|
||||
accountAnalyticsId = getUrlParams().analyticsID;
|
||||
} else {
|
||||
const accountData = await client.getAccountDataFromServer(
|
||||
PosthogAnalytics.ANALYTICS_EVENT_TYPE,
|
||||
);
|
||||
accountAnalyticsId = accountData?.id;
|
||||
accountAnalyticsId = accountData?.id ?? null;
|
||||
}
|
||||
if (accountAnalyticsId) {
|
||||
// we dont just use the element web analytics ID because that would allow to associate
|
||||
@@ -411,7 +415,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)
|
||||
optInAnalytics.value.subscribe((optIn) => {
|
||||
optInAnalytics.value$.subscribe((optIn) => {
|
||||
this.setAnonymity(optIn ? Anonymity.Pseudonymous : Anonymity.Disabled);
|
||||
this.maybeIdentifyUser().catch(() =>
|
||||
logger.log("Could not identify user"),
|
||||
|
||||
@@ -5,12 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { DisconnectReason } from "livekit-client";
|
||||
import { type DisconnectReason } from "livekit-client";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";
|
||||
import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";
|
||||
|
||||
import {
|
||||
IPosthogEvent,
|
||||
type IPosthogEvent,
|
||||
PosthogAnalytics,
|
||||
RegistrationType,
|
||||
} from "./PosthogAnalytics";
|
||||
|
||||
@@ -6,9 +6,9 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import {
|
||||
SpanProcessor,
|
||||
ReadableSpan,
|
||||
Span,
|
||||
type SpanProcessor,
|
||||
type ReadableSpan,
|
||||
type Span,
|
||||
} from "@opentelemetry/sdk-trace-base";
|
||||
import { hrTimeToMilliseconds } from "@opentelemetry/core";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
@@ -5,12 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { AttributeValue, Attributes } from "@opentelemetry/api";
|
||||
import { type AttributeValue, type Attributes } from "@opentelemetry/api";
|
||||
import { hrTimeToMicroseconds } from "@opentelemetry/core";
|
||||
import {
|
||||
SpanProcessor,
|
||||
ReadableSpan,
|
||||
Span,
|
||||
type SpanProcessor,
|
||||
type ReadableSpan,
|
||||
type Span,
|
||||
} from "@opentelemetry/sdk-trace-base";
|
||||
|
||||
const dumpAttributes = (
|
||||
|
||||
@@ -5,8 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { FC, FormEvent, useCallback, useRef, useState } from "react";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import { type FC, type FormEvent, useCallback, useRef, useState } from "react";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { Button } from "@vector-im/compound-web";
|
||||
|
||||
@@ -29,7 +29,7 @@ export const LoginPage: FC = () => {
|
||||
const homeserver = Config.defaultHomeserverUrl(); // TODO: Make this configurable
|
||||
const usernameRef = useRef<HTMLInputElement>(null);
|
||||
const passwordRef = useRef<HTMLInputElement>(null);
|
||||
const history = useHistory();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<Error>();
|
||||
@@ -48,7 +48,7 @@ export const LoginPage: FC = () => {
|
||||
}
|
||||
|
||||
login(homeserver, usernameRef.current.value, passwordRef.current.value)
|
||||
.then(([client, session]) => {
|
||||
.then(async ([client, session]) => {
|
||||
if (!setClient) {
|
||||
return;
|
||||
}
|
||||
@@ -61,9 +61,9 @@ export const LoginPage: FC = () => {
|
||||
if (locationState && locationState.from) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
history.push(locationState.from);
|
||||
await navigate(locationState.from);
|
||||
} else {
|
||||
history.push("/");
|
||||
await navigate("/");
|
||||
}
|
||||
PosthogAnalytics.instance.eventLogin.track();
|
||||
})
|
||||
@@ -72,7 +72,7 @@ export const LoginPage: FC = () => {
|
||||
setLoading(false);
|
||||
});
|
||||
},
|
||||
[login, location, history, homeserver, setClient],
|
||||
[login, location, navigate, homeserver, setClient],
|
||||
);
|
||||
// we need to limit the length of the homserver name to not cover the whole loginview input with the string.
|
||||
let shortendHomeserverName = Config.defaultServerName()?.slice(0, 25);
|
||||
|
||||
@@ -6,15 +6,15 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import {
|
||||
ChangeEvent,
|
||||
FC,
|
||||
FormEvent,
|
||||
type ChangeEvent,
|
||||
type FC,
|
||||
type FormEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { captureException } from "@sentry/react";
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
@@ -26,7 +26,7 @@ import { useClientLegacy } from "../ClientContext";
|
||||
import { useInteractiveRegistration } from "./useInteractiveRegistration";
|
||||
import styles from "./LoginPage.module.css";
|
||||
import Logo from "../icons/LogoLarge.svg?react";
|
||||
import { LoadingView } from "../FullScreenView";
|
||||
import { LoadingPage } from "../FullScreenView";
|
||||
import { useRecaptcha } from "./useRecaptcha";
|
||||
import { usePageTitle } from "../usePageTitle";
|
||||
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
||||
@@ -41,7 +41,7 @@ export const RegisterPage: FC = () => {
|
||||
useClientLegacy();
|
||||
|
||||
const confirmPasswordRef = useRef<HTMLInputElement>(null);
|
||||
const history = useHistory();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [registering, setRegistering] = useState(false);
|
||||
const [error, setError] = useState<Error>();
|
||||
@@ -100,15 +100,15 @@ export const RegisterPage: FC = () => {
|
||||
};
|
||||
|
||||
submit()
|
||||
.then(() => {
|
||||
.then(async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
if (location.state?.from) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
history.push(location.state?.from);
|
||||
await navigate(location.state?.from);
|
||||
} else {
|
||||
history.push("/");
|
||||
await navigate("/");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -120,7 +120,7 @@ export const RegisterPage: FC = () => {
|
||||
[
|
||||
register,
|
||||
location,
|
||||
history,
|
||||
navigate,
|
||||
passwordlessUser,
|
||||
reset,
|
||||
execute,
|
||||
@@ -141,12 +141,14 @@ export const RegisterPage: FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && authenticated && !passwordlessUser && !registering) {
|
||||
history.push("/");
|
||||
navigate("/")?.catch((error) => {
|
||||
logger.error("Failed to navigate to /", error);
|
||||
});
|
||||
}
|
||||
}, [loading, history, authenticated, passwordlessUser, registering]);
|
||||
}, [loading, navigate, authenticated, passwordlessUser, registering]);
|
||||
|
||||
if (loading) {
|
||||
return <LoadingView />;
|
||||
return <LoadingPage />;
|
||||
} else {
|
||||
PosthogAnalytics.instance.eventSignup.cacheSignupStart(new Date());
|
||||
}
|
||||
|
||||
@@ -9,12 +9,12 @@ import { useCallback } from "react";
|
||||
import { InteractiveAuth } from "matrix-js-sdk/src/interactive-auth";
|
||||
import {
|
||||
createClient,
|
||||
LoginResponse,
|
||||
MatrixClient,
|
||||
type LoginResponse,
|
||||
type MatrixClient,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { initClient } from "../utils/matrix";
|
||||
import { Session } from "../ClientContext";
|
||||
import { type Session } from "../ClientContext";
|
||||
/**
|
||||
* This provides the login method to login using user credentials.
|
||||
* @param oldClient If there is an already authenticated client it should be passed to this hook
|
||||
|
||||
@@ -9,13 +9,13 @@ import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { InteractiveAuth } from "matrix-js-sdk/src/interactive-auth";
|
||||
import {
|
||||
createClient,
|
||||
MatrixClient,
|
||||
RegisterResponse,
|
||||
type MatrixClient,
|
||||
type RegisterResponse,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { initClient } from "../utils/matrix";
|
||||
import { Session } from "../ClientContext";
|
||||
import { type Session } from "../ClientContext";
|
||||
import { Config } from "../config/Config";
|
||||
import { widget } from "../widget";
|
||||
|
||||
@@ -39,7 +39,7 @@ export const useInteractiveRegistration = (
|
||||
undefined,
|
||||
);
|
||||
|
||||
const authClient = useRef<MatrixClient>();
|
||||
const authClient = useRef<MatrixClient | undefined>(undefined);
|
||||
if (!authClient.current) {
|
||||
authClient.current = createClient({
|
||||
baseUrl: Config.defaultHomeserverUrl()!,
|
||||
|
||||
@@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { useEffect, useCallback, useRef, useState } from "react";
|
||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
@@ -31,8 +31,8 @@ export function useRecaptcha(sitekey?: string): {
|
||||
recaptchaId: string;
|
||||
} {
|
||||
const { t } = useTranslation();
|
||||
const [recaptchaId] = useState(() => randomString(16));
|
||||
const promiseRef = useRef<RecaptchaPromiseRef>();
|
||||
const [recaptchaId] = useState(() => secureRandomString(16));
|
||||
const promiseRef = useRef<RecaptchaPromiseRef | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (!sitekey) return;
|
||||
|
||||
@@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
|
||||
|
||||
import { useClient } from "../ClientContext";
|
||||
import { useInteractiveRegistration } from "../auth/useInteractiveRegistration";
|
||||
@@ -42,7 +42,7 @@ export function useRegisterPasswordlessUser(): UseRegisterPasswordlessUserType {
|
||||
const userName = generateRandomName();
|
||||
const [client, session] = await register(
|
||||
userName,
|
||||
randomString(16),
|
||||
secureRandomString(16),
|
||||
displayName,
|
||||
recaptchaResponse,
|
||||
true,
|
||||
|
||||
@@ -4,7 +4,7 @@ Copyright 2022-2024 New Vector Ltd.
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
import { ComponentPropsWithoutRef, FC } from "react";
|
||||
import { type ComponentPropsWithoutRef, type FC } from "react";
|
||||
import classNames from "classnames";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button as CpdButton, Tooltip } from "@vector-im/compound-web";
|
||||
|
||||
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { ComponentPropsWithoutRef, FC } from "react";
|
||||
import { type ComponentPropsWithoutRef, type FC } from "react";
|
||||
import { Button } from "@vector-im/compound-web";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UserAddIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
@@ -6,45 +6,30 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import {
|
||||
ComponentPropsWithoutRef,
|
||||
type ComponentPropsWithoutRef,
|
||||
forwardRef,
|
||||
MouseEvent,
|
||||
useCallback,
|
||||
useMemo,
|
||||
type MouseEvent,
|
||||
} from "react";
|
||||
import { Link as CpdLink } from "@vector-im/compound-web";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { createPath, LocationDescriptor, Path } from "history";
|
||||
import { type LinkProps, useHref, useLinkClickHandler } from "react-router-dom";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { useLatest } from "../useLatest";
|
||||
import styles from "./Link.module.css";
|
||||
|
||||
export function useLink(
|
||||
to: LocationDescriptor,
|
||||
to: LinkProps["to"],
|
||||
state?: unknown,
|
||||
): [Path, (e: MouseEvent) => void] {
|
||||
const latestState = useLatest(state);
|
||||
const history = useHistory();
|
||||
const path = useMemo(
|
||||
() => (typeof to === "string" ? to : createPath(to)),
|
||||
[to],
|
||||
);
|
||||
const onClick = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
history.push(to, latestState.current);
|
||||
},
|
||||
[history, to, latestState],
|
||||
);
|
||||
): [string, (e: MouseEvent<HTMLAnchorElement>) => void] {
|
||||
const href = useHref(to);
|
||||
const onClick = useLinkClickHandler(to, { state });
|
||||
|
||||
return [path, onClick];
|
||||
return [href, onClick];
|
||||
}
|
||||
|
||||
type Props = Omit<
|
||||
ComponentPropsWithoutRef<typeof CpdLink>,
|
||||
"href" | "onClick"
|
||||
> & { to: LocationDescriptor; state?: unknown };
|
||||
> & { to: LinkProps["to"]; state?: unknown };
|
||||
|
||||
/**
|
||||
* A version of Compound's link component that integrates with our router setup.
|
||||
|
||||
@@ -5,24 +5,24 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { ComponentPropsWithoutRef, forwardRef } from "react";
|
||||
import { type ComponentPropsWithoutRef, forwardRef } from "react";
|
||||
import { Button } from "@vector-im/compound-web";
|
||||
import { LocationDescriptor } from "history";
|
||||
|
||||
import type { LinkProps } from "react-router-dom";
|
||||
import { useLink } from "./Link";
|
||||
|
||||
type Props = Omit<
|
||||
ComponentPropsWithoutRef<typeof Button<"a">>,
|
||||
"as" | "href"
|
||||
> & { to: LocationDescriptor };
|
||||
> & { to: LinkProps["to"]; state?: unknown };
|
||||
|
||||
/**
|
||||
* A version of Compound's button component that acts as a link and integrates
|
||||
* with our router setup.
|
||||
*/
|
||||
export const LinkButton = forwardRef<HTMLAnchorElement, Props>(
|
||||
function LinkButton({ to, ...props }, ref) {
|
||||
const [path, onClick] = useLink(to);
|
||||
function LinkButton({ to, state, ...props }, ref) {
|
||||
const [path, onClick] = useLink(to, state);
|
||||
return <Button as="a" ref={ref} {...props} href={path} onClick={onClick} />;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -5,47 +5,47 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { render } from "@testing-library/react";
|
||||
import { act, render } from "@testing-library/react";
|
||||
import { expect, test } from "vitest";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
import { userEvent } from "@testing-library/user-event";
|
||||
import { ReactNode } from "react";
|
||||
import { type ReactNode } from "react";
|
||||
import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||
|
||||
import {
|
||||
MockRoom,
|
||||
MockRTCSession,
|
||||
TestReactionsWrapper,
|
||||
} from "../utils/testReactions";
|
||||
import { ReactionToggleButton } from "./ReactionToggleButton";
|
||||
import { ElementCallReactionEventType } from "../reactions";
|
||||
import { type CallViewModel } from "../state/CallViewModel";
|
||||
import { getBasicCallViewModelEnvironment } from "../utils/test-viewmodel";
|
||||
import { alice, local, localRtcMember } from "../utils/test-fixtures";
|
||||
import { type MockRTCSession } from "../utils/test";
|
||||
import { ReactionsSenderProvider } from "../reactions/useReactionsSender";
|
||||
|
||||
const memberUserIdAlice = "@alice:example.org";
|
||||
const memberEventAlice = "$membership-alice:example.org";
|
||||
|
||||
const membership: Record<string, string> = {
|
||||
[memberEventAlice]: memberUserIdAlice,
|
||||
};
|
||||
const localIdent = `${localRtcMember.sender}:${localRtcMember.deviceId}`;
|
||||
|
||||
function TestComponent({
|
||||
rtcSession,
|
||||
vm,
|
||||
}: {
|
||||
rtcSession: MockRTCSession;
|
||||
vm: CallViewModel;
|
||||
}): ReactNode {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TestReactionsWrapper rtcSession={rtcSession}>
|
||||
<ReactionToggleButton userId={memberUserIdAlice} />
|
||||
</TestReactionsWrapper>
|
||||
<ReactionsSenderProvider
|
||||
vm={vm}
|
||||
rtcSession={rtcSession as unknown as MatrixRTCSession}
|
||||
>
|
||||
<ReactionToggleButton vm={vm} identifier={localIdent} />
|
||||
</ReactionsSenderProvider>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
test("Can open menu", async () => {
|
||||
const user = userEvent.setup();
|
||||
const room = new MockRoom(memberUserIdAlice);
|
||||
const rtcSession = new MockRTCSession(room, membership);
|
||||
const { vm, rtcSession } = getBasicCallViewModelEnvironment([alice]);
|
||||
const { getByLabelText, container } = render(
|
||||
<TestComponent rtcSession={rtcSession} />,
|
||||
<TestComponent vm={vm} rtcSession={rtcSession} />,
|
||||
);
|
||||
await user.click(getByLabelText("common.reactions"));
|
||||
expect(container).toMatchSnapshot();
|
||||
@@ -53,102 +53,120 @@ test("Can open menu", async () => {
|
||||
|
||||
test("Can raise hand", async () => {
|
||||
const user = userEvent.setup();
|
||||
const room = new MockRoom(memberUserIdAlice);
|
||||
const rtcSession = new MockRTCSession(room, membership);
|
||||
const { vm, rtcSession, handRaisedSubject$ } =
|
||||
getBasicCallViewModelEnvironment([local, alice]);
|
||||
const { getByLabelText, container } = render(
|
||||
<TestComponent rtcSession={rtcSession} />,
|
||||
<TestComponent vm={vm} rtcSession={rtcSession} />,
|
||||
);
|
||||
await user.click(getByLabelText("common.reactions"));
|
||||
await user.click(getByLabelText("action.raise_hand"));
|
||||
expect(room.testSentEvents).toEqual([
|
||||
[
|
||||
undefined,
|
||||
"m.reaction",
|
||||
{
|
||||
"m.relates_to": {
|
||||
event_id: memberEventAlice,
|
||||
key: "🖐️",
|
||||
rel_type: "m.annotation",
|
||||
},
|
||||
expect(rtcSession.room.client.sendEvent).toHaveBeenCalledWith(
|
||||
rtcSession.room.roomId,
|
||||
"m.reaction",
|
||||
{
|
||||
"m.relates_to": {
|
||||
event_id: localRtcMember.eventId,
|
||||
key: "🖐️",
|
||||
rel_type: "m.annotation",
|
||||
},
|
||||
],
|
||||
]);
|
||||
},
|
||||
);
|
||||
act(() => {
|
||||
// Mock receiving a reaction.
|
||||
handRaisedSubject$.next({
|
||||
[localIdent]: {
|
||||
time: new Date(),
|
||||
reactionEventId: "",
|
||||
membershipEventId: localRtcMember.eventId!,
|
||||
},
|
||||
});
|
||||
});
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("Can lower hand", async () => {
|
||||
const reactionEventId = "$my-reaction-event:example.org";
|
||||
const user = userEvent.setup();
|
||||
const room = new MockRoom(memberUserIdAlice);
|
||||
const rtcSession = new MockRTCSession(room, membership);
|
||||
const { vm, rtcSession, handRaisedSubject$ } =
|
||||
getBasicCallViewModelEnvironment([local, alice]);
|
||||
const { getByLabelText, container } = render(
|
||||
<TestComponent rtcSession={rtcSession} />,
|
||||
<TestComponent vm={vm} rtcSession={rtcSession} />,
|
||||
);
|
||||
const reactionEvent = room.testSendHandRaise(memberEventAlice, membership);
|
||||
await user.click(getByLabelText("common.reactions"));
|
||||
await user.click(getByLabelText("action.raise_hand"));
|
||||
act(() => {
|
||||
handRaisedSubject$.next({
|
||||
[localIdent]: {
|
||||
time: new Date(),
|
||||
reactionEventId,
|
||||
membershipEventId: localRtcMember.eventId!,
|
||||
},
|
||||
});
|
||||
});
|
||||
await user.click(getByLabelText("common.reactions"));
|
||||
await user.click(getByLabelText("action.lower_hand"));
|
||||
expect(room.testRedactedEvents).toEqual([[undefined, reactionEvent]]);
|
||||
expect(rtcSession.room.client.redactEvent).toHaveBeenCalledWith(
|
||||
rtcSession.room.roomId,
|
||||
reactionEventId,
|
||||
);
|
||||
act(() => {
|
||||
// Mock receiving a redacted reaction.
|
||||
handRaisedSubject$.next({});
|
||||
});
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("Can react with emoji", async () => {
|
||||
const user = userEvent.setup();
|
||||
const room = new MockRoom(memberUserIdAlice);
|
||||
const rtcSession = new MockRTCSession(room, membership);
|
||||
const { vm, rtcSession } = getBasicCallViewModelEnvironment([local, alice]);
|
||||
const { getByLabelText, getByText } = render(
|
||||
<TestComponent rtcSession={rtcSession} />,
|
||||
<TestComponent vm={vm} rtcSession={rtcSession} />,
|
||||
);
|
||||
await user.click(getByLabelText("common.reactions"));
|
||||
await user.click(getByText("🐶"));
|
||||
expect(room.testSentEvents).toEqual([
|
||||
[
|
||||
undefined,
|
||||
ElementCallReactionEventType,
|
||||
{
|
||||
"m.relates_to": {
|
||||
event_id: memberEventAlice,
|
||||
rel_type: "m.reference",
|
||||
},
|
||||
name: "dog",
|
||||
emoji: "🐶",
|
||||
expect(rtcSession.room.client.sendEvent).toHaveBeenCalledWith(
|
||||
rtcSession.room.roomId,
|
||||
ElementCallReactionEventType,
|
||||
{
|
||||
"m.relates_to": {
|
||||
event_id: localRtcMember.eventId,
|
||||
rel_type: "m.reference",
|
||||
},
|
||||
],
|
||||
]);
|
||||
name: "dog",
|
||||
emoji: "🐶",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("Can fully expand emoji picker", async () => {
|
||||
const user = userEvent.setup();
|
||||
const room = new MockRoom(memberUserIdAlice);
|
||||
const rtcSession = new MockRTCSession(room, membership);
|
||||
const { getByText, container, getByLabelText } = render(
|
||||
<TestComponent rtcSession={rtcSession} />,
|
||||
const { vm, rtcSession } = getBasicCallViewModelEnvironment([local, alice]);
|
||||
const { getByLabelText, container, getByText } = render(
|
||||
<TestComponent vm={vm} rtcSession={rtcSession} />,
|
||||
);
|
||||
await user.click(getByLabelText("common.reactions"));
|
||||
await user.click(getByLabelText("action.show_more"));
|
||||
expect(container).toMatchSnapshot();
|
||||
await user.click(getByText("🦗"));
|
||||
|
||||
expect(room.testSentEvents).toEqual([
|
||||
[
|
||||
undefined,
|
||||
ElementCallReactionEventType,
|
||||
{
|
||||
"m.relates_to": {
|
||||
event_id: memberEventAlice,
|
||||
rel_type: "m.reference",
|
||||
},
|
||||
name: "crickets",
|
||||
emoji: "🦗",
|
||||
expect(rtcSession.room.client.sendEvent).toHaveBeenCalledWith(
|
||||
rtcSession.room.roomId,
|
||||
ElementCallReactionEventType,
|
||||
{
|
||||
"m.relates_to": {
|
||||
event_id: localRtcMember.eventId,
|
||||
rel_type: "m.reference",
|
||||
},
|
||||
],
|
||||
]);
|
||||
name: "crickets",
|
||||
emoji: "🦗",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("Can close reaction dialog", async () => {
|
||||
const user = userEvent.setup();
|
||||
const room = new MockRoom(memberUserIdAlice);
|
||||
const rtcSession = new MockRTCSession(room, membership);
|
||||
const { vm, rtcSession } = getBasicCallViewModelEnvironment([local, alice]);
|
||||
const { getByLabelText, container } = render(
|
||||
<TestComponent rtcSession={rtcSession} />,
|
||||
<TestComponent vm={vm} rtcSession={rtcSession} />,
|
||||
);
|
||||
await user.click(getByLabelText("common.reactions"));
|
||||
await user.click(getByLabelText("action.show_more"));
|
||||
|
||||
@@ -13,9 +13,9 @@ import {
|
||||
ReactionSolidIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import {
|
||||
ComponentPropsWithoutRef,
|
||||
FC,
|
||||
ReactNode,
|
||||
type ComponentPropsWithoutRef,
|
||||
type FC,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
@@ -24,11 +24,18 @@ import {
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import classNames from "classnames";
|
||||
import { useObservableState } from "observable-hooks";
|
||||
import { map } from "rxjs";
|
||||
|
||||
import { useReactions } from "../useReactions";
|
||||
import { useReactionsSender } from "../reactions/useReactionsSender";
|
||||
import styles from "./ReactionToggleButton.module.css";
|
||||
import { ReactionOption, ReactionSet, ReactionsRowSize } from "../reactions";
|
||||
import {
|
||||
type ReactionOption,
|
||||
ReactionSet,
|
||||
ReactionsRowSize,
|
||||
} from "../reactions";
|
||||
import { Modal } from "../Modal";
|
||||
import { type CallViewModel } from "../state/CallViewModel";
|
||||
|
||||
interface InnerButtonProps extends ComponentPropsWithoutRef<"button"> {
|
||||
raised: boolean;
|
||||
@@ -80,7 +87,7 @@ export function ReactionPopupMenu({
|
||||
<Alert
|
||||
className={styles.alert}
|
||||
type="critical"
|
||||
title={t("common.something_went_wrong")}
|
||||
title={t("error.generic")}
|
||||
>
|
||||
{errorText}
|
||||
</Alert>
|
||||
@@ -158,22 +165,27 @@ export function ReactionPopupMenu({
|
||||
}
|
||||
|
||||
interface ReactionToggleButtonProps extends ComponentPropsWithoutRef<"button"> {
|
||||
userId: string;
|
||||
identifier: string;
|
||||
vm: CallViewModel;
|
||||
}
|
||||
|
||||
export function ReactionToggleButton({
|
||||
userId,
|
||||
identifier,
|
||||
vm,
|
||||
...props
|
||||
}: ReactionToggleButtonProps): ReactNode {
|
||||
const { t } = useTranslation();
|
||||
const { raisedHands, toggleRaisedHand, sendReaction, reactions } =
|
||||
useReactions();
|
||||
const { toggleRaisedHand, sendReaction } = useReactionsSender();
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [showReactionsMenu, setShowReactionsMenu] = useState(false);
|
||||
const [errorText, setErrorText] = useState<string>();
|
||||
|
||||
const isHandRaised = !!raisedHands[userId];
|
||||
const canReact = !reactions[userId];
|
||||
const isHandRaised = useObservableState(
|
||||
vm.handsRaised$.pipe(map((v) => !!v[identifier])),
|
||||
);
|
||||
const canReact = useObservableState(
|
||||
vm.reactions$.pipe(map((v) => !v[identifier])),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Clear whenever the reactions menu state changes.
|
||||
@@ -219,7 +231,7 @@ export function ReactionToggleButton({
|
||||
<InnerButton
|
||||
disabled={busy}
|
||||
onClick={() => setShowReactionsMenu((show) => !show)}
|
||||
raised={isHandRaised}
|
||||
raised={!!isHandRaised}
|
||||
open={showReactionsMenu}
|
||||
{...props}
|
||||
/>
|
||||
@@ -233,8 +245,8 @@ export function ReactionToggleButton({
|
||||
>
|
||||
<ReactionPopupMenu
|
||||
errorText={errorText}
|
||||
isHandRaised={isHandRaised}
|
||||
canReact={!busy && canReact}
|
||||
isHandRaised={!!isHandRaised}
|
||||
canReact={!busy && !!canReact}
|
||||
sendReaction={(reaction) => void sendRelation(reaction)}
|
||||
toggleRaisedHand={wrappedToggleRaisedHand}
|
||||
/>
|
||||
|
||||
@@ -9,7 +9,7 @@ exports[`Can close reaction dialog 1`] = `
|
||||
aria-disabled="false"
|
||||
aria-expanded="true"
|
||||
aria-haspopup="true"
|
||||
aria-labelledby=":r9l:"
|
||||
aria-labelledby=":rb5:"
|
||||
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
@@ -43,7 +43,7 @@ exports[`Can fully expand emoji picker 1`] = `
|
||||
aria-disabled="false"
|
||||
aria-expanded="true"
|
||||
aria-haspopup="true"
|
||||
aria-labelledby=":r6c:"
|
||||
aria-labelledby=":r7m:"
|
||||
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
@@ -75,8 +75,8 @@ exports[`Can lower hand 1`] = `
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-labelledby=":r36:"
|
||||
class="_button_i91xf_17 raisedButton _has-icon_i91xf_66 _icon-only_i91xf_59"
|
||||
data-kind="primary"
|
||||
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
|
||||
data-kind="secondary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@@ -90,7 +90,9 @@ exports[`Can lower hand 1`] = `
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11 3a1 1 0 1 1 2 0v8.5a.5.5 0 0 0 1 0V4a1 1 0 1 1 2 0v10.2l3.284-2.597a1.081 1.081 0 0 1 1.47 1.577c-.613.673-1.214 1.367-1.818 2.064-1.267 1.463-2.541 2.934-3.944 4.235A6 6 0 0 1 5 15V7a1 1 0 0 1 2 0v5.5a.5.5 0 0 0 1 0V4a1 1 0 0 1 2 0v7.5a.5.5 0 0 0 1 0V3Z"
|
||||
clip-rule="evenodd"
|
||||
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Zm3.536-6.464a1 1 0 0 0-1.415-1.415A2.988 2.988 0 0 1 12 15a2.988 2.988 0 0 1-2.121-.879 1 1 0 1 0-1.414 1.415A4.987 4.987 0 0 0 12 17c1.38 0 2.632-.56 3.536-1.464ZM10 10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm5.5 1.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
@@ -138,8 +140,8 @@ exports[`Can raise hand 1`] = `
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-labelledby=":r1j:"
|
||||
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
|
||||
data-kind="secondary"
|
||||
class="_button_i91xf_17 raisedButton _has-icon_i91xf_66 _icon-only_i91xf_59"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@@ -153,9 +155,7 @@ exports[`Can raise hand 1`] = `
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Zm3.536-6.464a1 1 0 0 0-1.415-1.415A2.988 2.988 0 0 1 12 15a2.988 2.988 0 0 1-2.121-.879 1 1 0 1 0-1.414 1.415A4.987 4.987 0 0 0 12 17c1.38 0 2.632-.56 3.536-1.464ZM10 10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm5.5 1.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"
|
||||
fill-rule="evenodd"
|
||||
d="M11 3a1 1 0 1 1 2 0v8.5a.5.5 0 0 0 1 0V4a1 1 0 1 1 2 0v10.2l3.284-2.597a1.081 1.081 0 0 1 1.47 1.577c-.613.673-1.214 1.367-1.818 2.064-1.267 1.463-2.541 2.934-3.944 4.235A6 6 0 0 1 5 15V7a1 1 0 0 1 2 0v5.5a.5.5 0 0 0 1 0V4a1 1 0 0 1 2 0v7.5a.5.5 0 0 0 1 0V3Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
@@ -10,8 +10,8 @@ import { merge } from "lodash-es";
|
||||
import { getUrlParams } from "../UrlParams";
|
||||
import {
|
||||
DEFAULT_CONFIG,
|
||||
ConfigOptions,
|
||||
ResolvedConfigOptions,
|
||||
type ConfigOptions,
|
||||
type ResolvedConfigOptions,
|
||||
} from "./ConfigOptions";
|
||||
|
||||
export class Config {
|
||||
|
||||
@@ -51,7 +51,7 @@ export interface ConfigOptions {
|
||||
// a livekit service url in the client well-known.
|
||||
// The well known needs to be formatted like so:
|
||||
// {"type":"livekit", "livekit_service_url":"https://livekit.example.com"}
|
||||
// and stored under the key: "livekit_focus"
|
||||
// and stored under the key: "org.matrix.msc4143.rtc_foci"
|
||||
livekit_service_url: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -13,18 +13,18 @@ export interface Controls {
|
||||
disablePip: () => void;
|
||||
}
|
||||
|
||||
export const setPipEnabled = new Subject<boolean>();
|
||||
export const setPipEnabled$ = new Subject<boolean>();
|
||||
|
||||
window.controls = {
|
||||
canEnterPip(): boolean {
|
||||
return setPipEnabled.observed;
|
||||
return setPipEnabled$.observed;
|
||||
},
|
||||
enablePip(): void {
|
||||
if (!setPipEnabled.observed) throw new Error("No call is running");
|
||||
setPipEnabled.next(true);
|
||||
if (!setPipEnabled$.observed) throw new Error("No call is running");
|
||||
setPipEnabled$.next(true);
|
||||
},
|
||||
disablePip(): void {
|
||||
if (!setPipEnabled.observed) throw new Error("No call is running");
|
||||
setPipEnabled.next(false);
|
||||
if (!setPipEnabled$.observed) throw new Error("No call is running");
|
||||
setPipEnabled$.next(false);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import {
|
||||
MatrixRTCSession,
|
||||
type MatrixRTCSession,
|
||||
MatrixRTCSessionEvent,
|
||||
} from "matrix-js-sdk/src/matrixrtc";
|
||||
import { KeyProviderEvent } from "livekit-client";
|
||||
|
||||
@@ -8,7 +8,7 @@ Please see LICENSE in the repository root for full details.
|
||||
import { BaseKeyProvider, createKeyMaterialFromBuffer } from "livekit-client";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import {
|
||||
MatrixRTCSession,
|
||||
type MatrixRTCSession,
|
||||
MatrixRTCSessionEvent,
|
||||
} from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ Please see LICENSE in the repository root for full details.
|
||||
import { useEffect, useMemo } from "react";
|
||||
|
||||
import { setLocalStorageItem, useLocalStorage } from "../useLocalStorage";
|
||||
import { UrlParams, getUrlParams, useUrlParams } from "../UrlParams";
|
||||
import { type UrlParams, getUrlParams, useUrlParams } from "../UrlParams";
|
||||
import { E2eeType } from "./e2eeType";
|
||||
import { useClient } from "../ClientContext";
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import classNames from "classnames";
|
||||
import { FormEventHandler, forwardRef, ReactNode } from "react";
|
||||
import { type FormEventHandler, forwardRef, type ReactNode } from "react";
|
||||
|
||||
import styles from "./Form.module.css";
|
||||
|
||||
|
||||
@@ -5,11 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { BehaviorSubject, Observable } from "rxjs";
|
||||
import { ComponentType } from "react";
|
||||
import { type BehaviorSubject, type Observable } from "rxjs";
|
||||
import { type ComponentType } from "react";
|
||||
|
||||
import { LayoutProps } from "./Grid";
|
||||
import { TileViewModel } from "../state/TileViewModel";
|
||||
import { type LayoutProps } from "./Grid";
|
||||
import { type TileViewModel } from "../state/TileViewModel";
|
||||
|
||||
export interface Bounds {
|
||||
width: number;
|
||||
@@ -31,15 +31,15 @@ export interface CallLayoutInputs {
|
||||
/**
|
||||
* The minimum bounds of the layout area.
|
||||
*/
|
||||
minBounds: Observable<Bounds>;
|
||||
minBounds$: Observable<Bounds>;
|
||||
/**
|
||||
* The alignment of the floating spotlight tile, if present.
|
||||
*/
|
||||
spotlightAlignment: BehaviorSubject<Alignment>;
|
||||
spotlightAlignment$: BehaviorSubject<Alignment>;
|
||||
/**
|
||||
* The alignment of the small picture-in-picture tile, if present.
|
||||
*/
|
||||
pipAlignment: BehaviorSubject<Alignment>;
|
||||
pipAlignment$: BehaviorSubject<Alignment>;
|
||||
}
|
||||
|
||||
export interface CallLayoutOutputs<Model> {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user