Merge branch 'livekit' into renovate/major-eslint-monorepo

This commit is contained in:
Hugh Nimmo-Smith
2025-01-06 12:24:00 +00:00
225 changed files with 8195 additions and 4249 deletions

View File

@@ -42,7 +42,7 @@ jobs:
- name: Extract metadata (tags, labels) for Docker - name: Extract metadata (tags, labels) for Docker
id: meta id: meta
uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1 uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5.6.1
with: with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: ${{ inputs.docker_tags}} tags: ${{ inputs.docker_tags}}
@@ -51,7 +51,7 @@ jobs:
uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1 uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0 uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.0
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64

View File

@@ -28,7 +28,7 @@ jobs:
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4 uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
with: with:
cache: "yarn" cache: "yarn"
node-version: "lts/*" node-version-file: ".node-version"
- name: Install dependencies - name: Install dependencies
run: "yarn install" run: "yarn install"
- name: Build - name: Build

View File

@@ -12,7 +12,7 @@ jobs:
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4 uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
with: with:
cache: "yarn" cache: "yarn"
node-version: "lts/*" node-version-file: ".node-version"
- name: Install dependencies - name: Install dependencies
run: "yarn install" run: "yarn install"
- name: Prettier - name: Prettier

View File

@@ -14,7 +14,7 @@ jobs:
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4 uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
with: with:
cache: "yarn" cache: "yarn"
node-version: "lts/*" node-version-file: ".node-version"
- name: Install dependencies - name: Install dependencies
run: "yarn install" run: "yarn install"
- name: Vitest - name: Vitest

View File

@@ -18,7 +18,7 @@ jobs:
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
with: with:
cache: "yarn" cache: "yarn"
node-version: "lts/*" node-version-file: ".node-version"
- name: Install Deps - name: Install Deps
run: "yarn install --frozen-lockfile" run: "yarn install --frozen-lockfile"

1
.node-version Normal file
View File

@@ -0,0 +1 @@
22

View File

@@ -92,6 +92,9 @@ work:
experimental_features: experimental_features:
# MSC3266: Room summary API. Used for knocking over federation # MSC3266: Room summary API. Used for knocking over federation
msc3266_enabled: true 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 # The maximum allowed duration by which sent events can be delayed, as
# per MSC4140. # 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 prompted with the option to knock ("Request to join call"), a cannot join error or the
join view. 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 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 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 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. 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 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 ```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: the English translation:
```jsonc ```jsonc

View File

@@ -25,6 +25,9 @@ trusted_key_servers:
experimental_features: experimental_features:
# MSC3266: Room summary API. Used for knocking over federation # MSC3266: Room summary API. Used for knocking over federation
msc3266_enabled: true 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 # The maximum allowed duration by which sent events can be delayed, as
# per MSC4140. Must be a positive value if set. Defaults to no # per MSC4140. Must be a positive value if set. Defaults to no

View File

@@ -17,5 +17,9 @@
}, },
"rageshake": { "rageshake": {
"submit_url": "https://element.io/bugreports/submit" "submit_url": "https://element.io/bugreports/submit"
},
"sentry": {
"environment": "netlify-pr-preview",
"DSN": "https://b1e328d49be3402ba96101338989fb35@sentry.tools.element.io/41"
} }
} }

View File

@@ -5,7 +5,7 @@
<Location "/"> <Location "/">
# disable cache entriely by default (apart from Etag which is accurate enough) # 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 CacheDisable on
ExpiresActive off ExpiresActive off

View File

@@ -32,31 +32,32 @@ There are two formats for Element Call urls.
## Parameters ## Parameters
| Name | Values | Required for widget | Required for SPA | Description | | 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 doesnt provide any. | | `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 doesnt provide any. |
| `analyticsID` | Posthog analytics ID | No | No | Available only with user's consent for sharing telemetry in Element Web. | | `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. | | `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. | | `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. | | `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. | | `deviceId` | Matrix device ID | Yes | Not applicable | The Matrix device ID for the widget host. |
| `displayName` | | No | No | Display name used for auto-registration. | | `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. | | `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 | No | Factor by which to scale the interface's font size. | | `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...`. | | `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. | | `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. | | `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. | | `homeserver` | | Not applicable | No | Homeserver for registering a new (guest) user, configures non-default guest user server when creating a spa link. |
| `lang` | [BCP 47](https://www.rfc-editor.org/info/bcp47) code | No | No | The language the app should use. | | `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`. |
| `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) | | `lang` | [BCP 47](https://www.rfc-editor.org/info/bcp47) code | No | No | The language the app should use. |
| `password` | | No | No | E2EE password when using a shared secret. (For individual sender keys in embedded mode this is not required.) | | `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) |
| `perParticipantE2EE` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Enables per participant encryption with Keys exchanged over encrypted matrix room messages. | | `password` | | No | No | E2EE password when using a shared secret. (For individual sender keys in embedded mode this is not required.) |
| `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. | | `perParticipantE2EE` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Enables per participant encryption with Keys exchanged over encrypted matrix room messages. |
| `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. | | `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. |
| `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()`. | | `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. |
| `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. | | `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()`. |
| `skipLobby` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Skips the lobby to join a call directly, can be combined with preload in widget. | | `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. |
| `theme` | One of: `light`, `dark`, `light-high-contrast`, `dark-high-contrast` | No, defaults to `dark` | No, defaults to `dark` | UI theme to use. | | `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.) |
| `userId` | [Matrix User Identifier](https://spec.matrix.org/v1.12/appendices/#user-identifiers) | Yes | Not applicable | The Matrix user ID. | | `theme` | One of: `light`, `dark`, `light-high-contrast`, `dark-high-contrast` | No, defaults to `dark` | No, defaults to `dark` | UI theme to use. |
| `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 users default homeserver. | | `userId` | [Matrix User Identifier](https://spec.matrix.org/v1.12/appendices/#user-identifiers) | Yes | Not applicable | The Matrix user ID. |
| `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`. | | `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 users 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`. |

View File

@@ -37,7 +37,14 @@ module.exports = {
"@typescript-eslint/promise-function-async": "error", "@typescript-eslint/promise-function-async": "error",
"@typescript-eslint/require-await": "error", "@typescript-eslint/require-await": "error",
"@typescript-eslint/await-thenable": "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/no-exposed-subjects": "error",
"rxjs/finnish": "error",
}, },
settings: { settings: {
react: { react: {

View File

@@ -21,7 +21,7 @@ export default {
}, },
], ],
}, },
locales: ["en-GB"], locales: ["en"],
output: "locales/$LOCALE/$NAMESPACE.json", output: "locales/$LOCALE/$NAMESPACE.json",
input: ["src/**/*.{ts,tsx}"], input: ["src/**/*.{ts,tsx}"],
sort: true, sort: true,

View File

@@ -7,13 +7,13 @@
"features": ["plural_postfix_us", "filter_untranslated"], "features": ["plural_postfix_us", "filter_untranslated"],
"files": [ "files": [
{ {
"pattern": "locales/en-GB/*.json", "pattern": "locales/en/*.json",
"lang": "inherited" "lang": "inherited"
}, },
{ {
"group": "existing", "group": "existing",
"pattern": "locales/*/*.json", "pattern": "locales/*/*.json",
"excludes": ["locales/en-GB/*.json"], "excludes": ["locales/en/*.json"],
"lang": "${autodetectLang}" "lang": "${autodetectLang}"
} }
] ]
@@ -25,9 +25,6 @@
"output": "locales/${langLsrDash}/${file}" "output": "locales/${langLsrDash}/${file}"
} }
], ],
"includeSourceLang": "${includeSourceLang|false}", "includeSourceLang": "${includeSourceLang|false}"
"langAliases": {
"en": "en_GB"
}
} }
} }

View File

@@ -62,7 +62,6 @@
"developer_tab_title": "Разработчик", "developer_tab_title": "Разработчик",
"feedback_tab_h4": "Изпрати обратна връзка", "feedback_tab_h4": "Изпрати обратна връзка",
"feedback_tab_send_logs_label": "Включи debug логове", "feedback_tab_send_logs_label": "Включи debug логове",
"more_tab_title": "Още",
"speaker_device_selection_label": "Говорител" "speaker_device_selection_label": "Говорител"
}, },
"unauthenticated_view_body": "Все още не сте регистрирани? <2>Създайте акаунт</2>", "unauthenticated_view_body": "Все още не сте регистрирани? <2>Създайте акаунт</2>",

View File

@@ -60,12 +60,9 @@
"return_home_button": "Vrátit se na domácí obrazovku", "return_home_button": "Vrátit se na domácí obrazovku",
"screenshare_button_label": "Sdílet obrazovku", "screenshare_button_label": "Sdílet obrazovku",
"settings": { "settings": {
"developer_settings_label": "Vývojářské nastavení",
"developer_settings_label_description": "Zobrazit vývojářské nastavení.",
"developer_tab_title": "Vývojář", "developer_tab_title": "Vývojář",
"feedback_tab_h4": "Dát feedback", "feedback_tab_h4": "Dát feedback",
"feedback_tab_send_logs_label": "Zahrnout ladící záznamy", "feedback_tab_send_logs_label": "Zahrnout ladící záznamy",
"more_tab_title": "Více",
"speaker_device_selection_label": "Reproduktor" "speaker_device_selection_label": "Reproduktor"
}, },
"unauthenticated_view_body": "Nejste registrovaní? <2>Vytvořit účet</2>", "unauthenticated_view_body": "Nejste registrovaní? <2>Vytvořit účet</2>",

View File

@@ -68,8 +68,13 @@
"username": "Benutzername", "username": "Benutzername",
"video": "Video" "video": "Video"
}, },
"crypto_version": "Krypto-Version:{{version}}", "developer_mode": {
"device_id": "Geräte-ID: {{id}}", "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.", "disconnected_banner": "Die Verbindung zum Server wurde getrennt.",
"full_screen_view_description": "<0>Übermittelte Problemberichte helfen uns, Fehler zu beheben.</0>", "full_screen_view_description": "<0>Übermittelte Problemberichte helfen uns, Fehler zu beheben.</0>",
"full_screen_view_h1": "<0>Hoppla, etwas ist schiefgelaufen.</0>", "full_screen_view_h1": "<0>Hoppla, etwas ist schiefgelaufen.</0>",
@@ -111,7 +116,6 @@
"login_auth_links_prompt": "Noch nicht registriert?", "login_auth_links_prompt": "Noch nicht registriert?",
"login_subheading": "Weiter zu Element", "login_subheading": "Weiter zu Element",
"login_title": "Anmelden", "login_title": "Anmelden",
"matrix_id": "Matrix-ID: {{id}}",
"microphone_off": "Mikrofon aus", "microphone_off": "Mikrofon aus",
"microphone_on": "Mikrofon an", "microphone_on": "Mikrofon an",
"mute_microphone_button_label": "Mikrofon stumm schalten", "mute_microphone_button_label": "Mikrofon stumm schalten",
@@ -146,29 +150,20 @@
"effect_volume_description": "Lautstärke anpassen, mit der Reaktionen und Handmeldungen abgespielt werden", "effect_volume_description": "Lautstärke anpassen, mit der Reaktionen und Handmeldungen abgespielt werden",
"effect_volume_label": "Lautstärke der Soundeffekte" "effect_volume_label": "Lautstärke der Soundeffekte"
}, },
"developer_settings_label": "Entwicklereinstellungen",
"developer_settings_label_description": "Zeige die Entwicklereinstellungen im Einstellungsfenster.",
"developer_tab_title": "Entwickler", "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_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_description_label": "Deine Rückmeldung",
"feedback_tab_h4": "Rückmeldung geben", "feedback_tab_h4": "Rückmeldung geben",
"feedback_tab_send_logs_label": "Debug-Protokolle anhängen", "feedback_tab_send_logs_label": "Debug-Protokolle anhängen",
"feedback_tab_thank_you": "Danke, wir haben deine Rückmeldung erhalten!", "feedback_tab_thank_you": "Danke, wir haben deine Rückmeldung erhalten!",
"feedback_tab_title": "Rückmeldung", "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.", "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": { "preferences_tab": {
"reactions_play_sound_description": "Einen Soundeffekt abspielen, wenn jemand eine Reaktion sendet", "reactions_play_sound_description": "Einen Soundeffekt abspielen, wenn jemand eine Reaktion sendet",
"reactions_play_sound_label": "Reaktionstöne abspielen", "reactions_play_sound_label": "Reaktionstöne abspielen",
"reactions_show_description": "Zeige eine Animation, wenn jemand eine Reaktion sendet.", "reactions_show_description": "Zeige eine Animation, wenn jemand eine Reaktion sendet.",
"reactions_show_label": "Reaktionen anzeigen", "reactions_show_label": "Reaktionen anzeigen"
"reactions_title": "Reaktionen"
}, },
"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" "speaker_device_selection_label": "Lautsprecher"
}, },
"star_rating_input_label_one": "{{count}} Stern", "star_rating_input_label_one": "{{count}} Stern",
@@ -191,6 +186,7 @@
"expand": "Erweitern", "expand": "Erweitern",
"mute_for_me": "Für mich stumm schalten", "mute_for_me": "Für mich stumm schalten",
"muted_for_me": "Für mich stumm geschaltet", "muted_for_me": "Für mich stumm geschaltet",
"volume": "Lautstärke" "volume": "Lautstärke",
"waiting_for_media": "Warten auf Medien..."
} }
} }

View File

@@ -67,8 +67,6 @@
"return_home_button": "Επιστροφή στην αρχική οθόνη", "return_home_button": "Επιστροφή στην αρχική οθόνη",
"screenshare_button_label": "Κοινή χρήση οθόνης", "screenshare_button_label": "Κοινή χρήση οθόνης",
"settings": { "settings": {
"developer_settings_label": "Ρυθμίσεις προγραμματιστή",
"developer_settings_label_description": "Εμφάνιση ρυθμίσεων προγραμματιστή στο παράθυρο ρυθμίσεων.",
"developer_tab_title": "Προγραμματιστής", "developer_tab_title": "Προγραμματιστής",
"feedback_tab_body": "Εάν αντιμετωπίζετε προβλήματα ή απλά θέλετε να μας δώσετε κάποια σχόλια, παρακαλούμε στείλτε μας μια σύντομη περιγραφή παρακάτω.", "feedback_tab_body": "Εάν αντιμετωπίζετε προβλήματα ή απλά θέλετε να μας δώσετε κάποια σχόλια, παρακαλούμε στείλτε μας μια σύντομη περιγραφή παρακάτω.",
"feedback_tab_description_label": "Τα σχόλιά σας", "feedback_tab_description_label": "Τα σχόλιά σας",
@@ -76,7 +74,6 @@
"feedback_tab_send_logs_label": "Να συμπεριληφθούν αρχεία καταγραφής", "feedback_tab_send_logs_label": "Να συμπεριληφθούν αρχεία καταγραφής",
"feedback_tab_thank_you": "Ευχαριστούμε, λάβαμε τα σχόλιά σας!", "feedback_tab_thank_you": "Ευχαριστούμε, λάβαμε τα σχόλιά σας!",
"feedback_tab_title": "Ανατροφοδότηση", "feedback_tab_title": "Ανατροφοδότηση",
"more_tab_title": "Περισσότερα",
"opt_in_description": "<0></0><1></1>Μπορείτε να ανακαλέσετε τη συγκατάθεσή σας αποεπιλέγοντας αυτό το πλαίσιο. Εάν βρίσκεστε σε κλήση, η ρύθμιση αυτή θα τεθεί σε ισχύ στο τέλος της.", "opt_in_description": "<0></0><1></1>Μπορείτε να ανακαλέσετε τη συγκατάθεσή σας αποεπιλέγοντας αυτό το πλαίσιο. Εάν βρίσκεστε σε κλήση, η ρύθμιση αυτή θα τεθεί σε ισχύ στο τέλος της.",
"speaker_device_selection_label": "Ηχείο" "speaker_device_selection_label": "Ηχείο"
}, },

View File

@@ -48,13 +48,11 @@
"audio": "Audio", "audio": "Audio",
"avatar": "Avatar", "avatar": "Avatar",
"back": "Back", "back": "Back",
"camera": "Camera",
"display_name": "Display name", "display_name": "Display name",
"encrypted": "Encrypted", "encrypted": "Encrypted",
"error": "Error", "error": "Error",
"home": "Home", "home": "Home",
"loading": "Loading…", "loading": "Loading…",
"microphone": "Microphone",
"next": "Next", "next": "Next",
"options": "Options", "options": "Options",
"password": "Password", "password": "Password",
@@ -68,8 +66,16 @@
"username": "Username", "username": "Username",
"video": "Video" "video": "Video"
}, },
"crypto_version": "Crypto version: {{version}}", "developer_mode": {
"device_id": "Device ID: {{id}}", "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}}",
"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.", "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_description": "<0>Submitting debug logs will help us track down the problem.</0>",
"full_screen_view_h1": "<0>Oops, something's gone wrong.</0>", "full_screen_view_h1": "<0>Oops, something's gone wrong.</0>",
@@ -111,7 +117,6 @@
"login_auth_links_prompt": "Not registered yet?", "login_auth_links_prompt": "Not registered yet?",
"login_subheading": "To continue to Element", "login_subheading": "To continue to Element",
"login_title": "Login", "login_title": "Login",
"matrix_id": "Matrix ID: {{id}}",
"microphone_off": "Microphone off", "microphone_off": "Microphone off",
"microphone_on": "Microphone on", "microphone_on": "Microphone on",
"mute_microphone_button_label": "Mute microphone", "mute_microphone_button_label": "Mute microphone",
@@ -143,33 +148,38 @@
"screenshare_button_label": "Share screen", "screenshare_button_label": "Share screen",
"settings": { "settings": {
"audio_tab": { "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" "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", "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_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_description_label": "Your feedback",
"feedback_tab_h4": "Submit feedback", "feedback_tab_h4": "Submit feedback",
"feedback_tab_send_logs_label": "Include debug logs", "feedback_tab_send_logs_label": "Include debug logs",
"feedback_tab_thank_you": "Thanks, we received your feedback!", "feedback_tab_thank_you": "Thanks, we received your feedback!",
"feedback_tab_title": "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.", "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": { "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_description": "Play a sound effect when anyone sends a reaction into a call.",
"reactions_play_sound_label": "Play reaction sounds", "reactions_play_sound_label": "Play reaction sounds",
"reactions_show_description": "Show an animation when anyone sends a reaction.", "reactions_show_description": "Show an animation when anyone sends a reaction.",
"reactions_show_label": "Show reactions", "reactions_show_label": "Show reactions",
"reactions_title": "Reactions" "show_hand_raised_timer_description": "Show a timer when a participant raises their hand",
}, "show_hand_raised_timer_label": "Show hand raise duration"
"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"
}, },
"star_rating_input_label_one": "{{count}} star", "star_rating_input_label_one": "{{count}} star",
"star_rating_input_label_other": "{{count}} stars", "star_rating_input_label_other": "{{count}} stars",
@@ -186,11 +196,13 @@
"version": "{{productName}} version: {{version}}", "version": "{{productName}} version: {{version}}",
"video_tile": { "video_tile": {
"always_show": "Always show", "always_show": "Always show",
"camera_starting": "Video loading...",
"change_fit_contain": "Fit to frame", "change_fit_contain": "Fit to frame",
"collapse": "Collapse", "collapse": "Collapse",
"expand": "Expand", "expand": "Expand",
"mute_for_me": "Mute for me", "mute_for_me": "Mute for me",
"muted_for_me": "Muted for me", "muted_for_me": "Muted for me",
"volume": "Volume" "volume": "Volume",
"waiting_for_media": "Waiting for media..."
} }
} }

View File

@@ -67,8 +67,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>", "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", "screenshare_button_label": "Compartir pantalla",
"settings": { "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", "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_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", "feedback_tab_description_label": "Tus comentarios",
@@ -76,7 +74,6 @@
"feedback_tab_send_logs_label": "Incluir registros de depuración", "feedback_tab_send_logs_label": "Incluir registros de depuración",
"feedback_tab_thank_you": "¡Gracias, hemos recibido tus comentarios!", "feedback_tab_thank_you": "¡Gracias, hemos recibido tus comentarios!",
"feedback_tab_title": "Danos tu opinión", "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.", "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" "speaker_device_selection_label": "Altavoz"
}, },

View File

@@ -97,8 +97,6 @@
"room_auth_view_eula_caption": "Klõpsides „Liitu kõnega kohe“, nõustud sa meie <2>Lõppkasutaja litsentsilepinguga (EULA)</2>", "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", "screenshare_button_label": "Jaga ekraani",
"settings": { "settings": {
"developer_settings_label": "Arendaja seadistused",
"developer_settings_label_description": "Näita seadistuste aknas arendajale vajalikke seadeid.",
"developer_tab_title": "Arendaja", "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_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", "feedback_tab_description_label": "Sinu tagasiside",
@@ -106,7 +104,6 @@
"feedback_tab_send_logs_label": "Lisa veatuvastuslogid", "feedback_tab_send_logs_label": "Lisa veatuvastuslogid",
"feedback_tab_thank_you": "Tänud, me oleme sinu tagasiside kätte saanud!", "feedback_tab_thank_you": "Tänud, me oleme sinu tagasiside kätte saanud!",
"feedback_tab_title": "Tagasiside", "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.", "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" "speaker_device_selection_label": "Kõlar"
}, },

View File

@@ -64,7 +64,6 @@
"developer_tab_title": "توسعه دهنده", "developer_tab_title": "توسعه دهنده",
"feedback_tab_h4": "بازخورد ارائه دهید", "feedback_tab_h4": "بازخورد ارائه دهید",
"feedback_tab_send_logs_label": "شامل لاگ‌های عیب‌یابی", "feedback_tab_send_logs_label": "شامل لاگ‌های عیب‌یابی",
"more_tab_title": "بیشتر",
"speaker_device_selection_label": "بلندگو" "speaker_device_selection_label": "بلندگو"
}, },
"unauthenticated_view_body": "هنوز ثبت‌نام نکرده‌اید؟ <2>ساخت حساب کاربری</2>", "unauthenticated_view_body": "هنوز ثبت‌نام نکرده‌اید؟ <2>ساخت حساب کاربری</2>",

View File

@@ -95,8 +95,6 @@
"room_auth_view_eula_caption": "En cliquant sur « Rejoindre lappel maintenant », vous acceptez notre <2>Contrat de Licence Utilisateur Final (CLUF)</2>", "room_auth_view_eula_caption": "En cliquant sur « Rejoindre lappel maintenant », vous acceptez notre <2>Contrat de Licence Utilisateur Final (CLUF)</2>",
"screenshare_button_label": "Partage décran", "screenshare_button_label": "Partage décran",
"settings": { "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", "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_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", "feedback_tab_description_label": "Votre commentaire",
@@ -104,7 +102,6 @@
"feedback_tab_send_logs_label": "Inclure les journaux de débogage", "feedback_tab_send_logs_label": "Inclure les journaux de débogage",
"feedback_tab_thank_you": "Merci, nous avons reçu vos commentaires !", "feedback_tab_thank_you": "Merci, nous avons reçu vos commentaires !",
"feedback_tab_title": "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 lappel.", "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 lappel.",
"speaker_device_selection_label": "Intervenant" "speaker_device_selection_label": "Intervenant"
}, },

View File

@@ -96,8 +96,6 @@
"room_auth_view_eula_caption": "Dengan mengeklik \"Bergabung ke panggilan sekarang\", Anda menyetujui <2>Perjanjian Lisensi Pengguna Akhir (EULA)</2> kami", "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", "screenshare_button_label": "Bagikan layar",
"settings": { "settings": {
"developer_settings_label": "Pengaturan Pengembang",
"developer_settings_label_description": "Ekspos pengaturan pengembang dalam jendela pengaturan.",
"developer_tab_title": "Pengembang", "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_body": "Jika Anda mengalami masalah atau hanya ingin memberikan masukan, silakan kirimkan kami deskripsi pendek di bawah.",
"feedback_tab_description_label": "Masukan Anda", "feedback_tab_description_label": "Masukan Anda",
@@ -105,7 +103,6 @@
"feedback_tab_send_logs_label": "Termasuk catatan pengawakutuan", "feedback_tab_send_logs_label": "Termasuk catatan pengawakutuan",
"feedback_tab_thank_you": "Terima kasih, kami telah menerima masukan Anda!", "feedback_tab_thank_you": "Terima kasih, kami telah menerima masukan Anda!",
"feedback_tab_title": "Masukan", "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.", "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" "speaker_device_selection_label": "Pembicara"
}, },

View File

@@ -94,15 +94,12 @@
"room_auth_view_eula_caption": "Cliccando \"Entra in chiamata ora\", accetti il nostro <2>accordo di licenza con l'utente finale (EULA)</2>", "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", "screenshare_button_label": "Condividi schermo",
"settings": { "settings": {
"developer_settings_label": "Impostazioni per sviluppatori",
"developer_settings_label_description": "Mostra le impostazioni per sviluppatori nella finestra delle impostazioni.",
"developer_tab_title": "Sviluppatore", "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_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_description_label": "Il tuo commento",
"feedback_tab_h4": "Invia commento", "feedback_tab_h4": "Invia commento",
"feedback_tab_send_logs_label": "Includi registri di debug", "feedback_tab_send_logs_label": "Includi registri di debug",
"feedback_tab_thank_you": "Grazie, abbiamo ricevuto il tuo commento!", "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.", "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" "speaker_device_selection_label": "Altoparlante"
}, },

View File

@@ -75,8 +75,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>", "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", "screenshare_button_label": "Kopīgot ekrānu",
"settings": { "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", "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_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", "feedback_tab_description_label": "Tava atsauksme",
@@ -84,7 +82,6 @@
"feedback_tab_send_logs_label": "Iekļaut atkļūdošanas žurnāla ierakstus", "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_thank_you": "Paldies, mēs saņēmām atsauksmi!",
"feedback_tab_title": "Atsauksmes", "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.", "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" "speaker_device_selection_label": "Runātājs"
}, },

View File

@@ -99,8 +99,6 @@
"room_auth_view_eula_caption": "Klikając \"Dołącz teraz do rozmowy\", zgadzasz się na naszą <2>Umowę licencyjną (EULA)</2>", "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", "screenshare_button_label": "Udostępnij ekran",
"settings": { "settings": {
"developer_settings_label": "Opcje programisty",
"developer_settings_label_description": "Wyświetl opcje programisty w oknie ustawień.",
"developer_tab_title": "Programista", "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_body": "Jeśli posiadasz problemy lub chciałbyś zgłosić swoją opinię, wyślij nam krótki opis.",
"feedback_tab_description_label": "Twoje opinie", "feedback_tab_description_label": "Twoje opinie",
@@ -108,7 +106,6 @@
"feedback_tab_send_logs_label": "Dołącz dzienniki debugowania", "feedback_tab_send_logs_label": "Dołącz dzienniki debugowania",
"feedback_tab_thank_you": "Dziękujemy, otrzymaliśmy Twoją opinię!", "feedback_tab_thank_you": "Dziękujemy, otrzymaliśmy Twoją opinię!",
"feedback_tab_title": "Opinia użytkownika", "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.", "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" "speaker_device_selection_label": "Głośnik"
}, },

187
locales/ro/app.json Normal file
View File

@@ -0,0 +1,187 @@
{
"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": {
"body": "Ai fost deconectat de la apel",
"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ă.",
"full_screen_view_description": "<0>Trimiterea jurnalelor de depanare ne va ajuta să urmărim problema. </0>",
"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"
}
}

View File

@@ -69,8 +69,6 @@
"return_home_button": "Вернуться в Начало", "return_home_button": "Вернуться в Начало",
"screenshare_button_label": "Поделиться экраном", "screenshare_button_label": "Поделиться экраном",
"settings": { "settings": {
"developer_settings_label": "Настройки Разработчика",
"developer_settings_label_description": "Раскрыть настройки разработчика в окне настроек.",
"developer_tab_title": "Разработчику", "developer_tab_title": "Разработчику",
"feedback_tab_body": "Если у вас возникли проблемы или вы просто хотите оставить отзыв, отправьте нам краткое описание ниже.", "feedback_tab_body": "Если у вас возникли проблемы или вы просто хотите оставить отзыв, отправьте нам краткое описание ниже.",
"feedback_tab_description_label": "Ваш отзыв", "feedback_tab_description_label": "Ваш отзыв",
@@ -78,7 +76,6 @@
"feedback_tab_send_logs_label": "Приложить журнал отладки", "feedback_tab_send_logs_label": "Приложить журнал отладки",
"feedback_tab_thank_you": "Спасибо. Мы получили ваш отзыв!", "feedback_tab_thank_you": "Спасибо. Мы получили ваш отзыв!",
"feedback_tab_title": "Отзыв", "feedback_tab_title": "Отзыв",
"more_tab_title": "Больше",
"opt_in_description": "<0></0><1></1>Вы можете отозвать согласие, сняв этот флажок. Если вы в данный момент находитесь в разговоре, эта настройка вступит в силу по окончании разговора.", "opt_in_description": "<0></0><1></1>Вы можете отозвать согласие, сняв этот флажок. Если вы в данный момент находитесь в разговоре, эта настройка вступит в силу по окончании разговора.",
"speaker_device_selection_label": "Динамик" "speaker_device_selection_label": "Динамик"
}, },

View File

@@ -97,8 +97,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>", "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", "screenshare_button_label": "Zdieľať obrazovku",
"settings": { "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", "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_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", "feedback_tab_description_label": "Vaša spätná väzba",
@@ -106,7 +104,6 @@
"feedback_tab_send_logs_label": "Zahrnúť záznamy o ladení", "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_thank_you": "Ďakujeme, dostali sme vašu spätnú väzbu!",
"feedback_tab_title": "Spätná väzba", "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.", "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" "speaker_device_selection_label": "Reproduktor"
}, },

View File

@@ -52,8 +52,7 @@
"settings": { "settings": {
"developer_tab_title": "Geliştirici", "developer_tab_title": "Geliştirici",
"feedback_tab_h4": "Geri bildirim ver", "feedback_tab_h4": "Geri bildirim ver",
"feedback_tab_send_logs_label": "Hata ayıklama kütüğünü dahil et", "feedback_tab_send_logs_label": "Hata ayıklama kütüğünü dahil et"
"more_tab_title": "Daha"
}, },
"unauthenticated_view_body": "Kaydolmadınız mı? <2>Hesap açın</2>", "unauthenticated_view_body": "Kaydolmadınız mı? <2>Hesap açın</2>",
"unauthenticated_view_login_button": "Hesabınıza girin" "unauthenticated_view_login_button": "Hesabınıza girin"

View File

@@ -99,8 +99,6 @@
"room_auth_view_eula_caption": "Натискаючи \"Приєднатися до виклику зараз\", ви погоджуєтеся з нашою <2>Ліцензійною угодою з кінцевим користувачем (EULA)</2>", "room_auth_view_eula_caption": "Натискаючи \"Приєднатися до виклику зараз\", ви погоджуєтеся з нашою <2>Ліцензійною угодою з кінцевим користувачем (EULA)</2>",
"screenshare_button_label": "Поділитися екраном", "screenshare_button_label": "Поділитися екраном",
"settings": { "settings": {
"developer_settings_label": "Налаштування розробника",
"developer_settings_label_description": "Відкрийте налаштування розробника у вікні налаштувань.",
"developer_tab_title": "Розробнику", "developer_tab_title": "Розробнику",
"feedback_tab_body": "Якщо у вас виникли проблеми або ви просто хочете залишити відгук, надішліть нам короткий опис нижче.", "feedback_tab_body": "Якщо у вас виникли проблеми або ви просто хочете залишити відгук, надішліть нам короткий опис нижче.",
"feedback_tab_description_label": "Ваш відгук", "feedback_tab_description_label": "Ваш відгук",
@@ -108,7 +106,6 @@
"feedback_tab_send_logs_label": "Долучити журнали налагодження", "feedback_tab_send_logs_label": "Долучити журнали налагодження",
"feedback_tab_thank_you": "Дякуємо, ми отримали ваш відгук!", "feedback_tab_thank_you": "Дякуємо, ми отримали ваш відгук!",
"feedback_tab_title": "Відгук", "feedback_tab_title": "Відгук",
"more_tab_title": "Докладніше",
"opt_in_description": "<0></0><1></1>Ви можете відкликати згоду, прибравши цей прапорець. Якщо ви зараз розмовляєте, це налаштування застосується після завершення виклику.", "opt_in_description": "<0></0><1></1>Ви можете відкликати згоду, прибравши цей прапорець. Якщо ви зараз розмовляєте, це налаштування застосується після завершення виклику.",
"speaker_device_selection_label": "Динамік" "speaker_device_selection_label": "Динамік"
}, },

View File

@@ -55,7 +55,6 @@
"register_confirm_password_label": "Xác nhận mật khẩu", "register_confirm_password_label": "Xác nhận mật khẩu",
"screenshare_button_label": "Chia sẻ màn hình", "screenshare_button_label": "Chia sẻ màn hình",
"settings": { "settings": {
"developer_settings_label": "Cài đặt phát triển",
"developer_tab_title": "Nhà 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_description_label": "Phản hồi của bạn",
"feedback_tab_h4": "Gửi phản hồi", "feedback_tab_h4": "Gửi phản hồi",

View File

@@ -92,8 +92,6 @@
"room_auth_view_eula_caption": "点击 \"加入通话\",即表示您同意我们的<2>最终用户许可协议 (EULA)</2>", "room_auth_view_eula_caption": "点击 \"加入通话\",即表示您同意我们的<2>最终用户许可协议 (EULA)</2>",
"screenshare_button_label": "屏幕共享", "screenshare_button_label": "屏幕共享",
"settings": { "settings": {
"developer_settings_label": "开发者设置",
"developer_settings_label_description": "在设置中显示开发者设置。",
"developer_tab_title": "开发者", "developer_tab_title": "开发者",
"feedback_tab_body": "如果遇到问题或想提供一些反馈意见,请在下面向我们发送简短描述。", "feedback_tab_body": "如果遇到问题或想提供一些反馈意见,请在下面向我们发送简短描述。",
"feedback_tab_description_label": "您的反馈", "feedback_tab_description_label": "您的反馈",
@@ -101,7 +99,6 @@
"feedback_tab_send_logs_label": "包含调试日志", "feedback_tab_send_logs_label": "包含调试日志",
"feedback_tab_thank_you": "谢谢,我们收到了反馈!", "feedback_tab_thank_you": "谢谢,我们收到了反馈!",
"feedback_tab_title": "反馈", "feedback_tab_title": "反馈",
"more_tab_title": "更多",
"opt_in_description": "<0></0><1></1>您可以取消选中复选框来撤回同意。如果正在通话中,此设置将在通话结束时生效。", "opt_in_description": "<0></0><1></1>您可以取消选中复选框来撤回同意。如果正在通话中,此设置将在通话结束时生效。",
"speaker_device_selection_label": "发言人" "speaker_device_selection_label": "发言人"
}, },

View File

@@ -99,8 +99,6 @@
"room_auth_view_eula_caption": "點擊「立刻加入通話」即表示您同意我們的<2>終端使用者授權協議 (EULA)</2>", "room_auth_view_eula_caption": "點擊「立刻加入通話」即表示您同意我們的<2>終端使用者授權協議 (EULA)</2>",
"screenshare_button_label": "分享畫面", "screenshare_button_label": "分享畫面",
"settings": { "settings": {
"developer_settings_label": "開發者設定",
"developer_settings_label_description": "在設定視窗中顯示開發者設定。",
"developer_tab_title": "開發者", "developer_tab_title": "開發者",
"feedback_tab_body": "若您遇到問題或只是想提供一些回饋,請在下方傳送簡短說明給我們。", "feedback_tab_body": "若您遇到問題或只是想提供一些回饋,請在下方傳送簡短說明給我們。",
"feedback_tab_description_label": "您的回饋", "feedback_tab_description_label": "您的回饋",
@@ -108,7 +106,6 @@
"feedback_tab_send_logs_label": "包含除錯紀錄", "feedback_tab_send_logs_label": "包含除錯紀錄",
"feedback_tab_thank_you": "感謝,我們已經收到您的回饋了!", "feedback_tab_thank_you": "感謝,我們已經收到您的回饋了!",
"feedback_tab_title": "回饋", "feedback_tab_title": "回饋",
"more_tab_title": "更多",
"opt_in_description": "<0></0><1></1>您可以透過取消核取此方塊來撤回同意。若您目前正在通話中,此設定將在通話結束時生效。", "opt_in_description": "<0></0><1></1>您可以透過取消核取此方塊來撤回同意。若您目前正在通話中,此設定將在通話結束時生效。",
"speaker_device_selection_label": "發言者" "speaker_device_selection_label": "發言者"
}, },

View File

@@ -27,13 +27,13 @@
"@codecov/vite-plugin": "^1.3.0", "@codecov/vite-plugin": "^1.3.0",
"@fontsource/inconsolata": "^5.1.0", "@fontsource/inconsolata": "^5.1.0",
"@fontsource/inter": "^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", "@formatjs/intl-segmenter": "^11.7.3",
"@livekit/components-core": "^0.11.0", "@livekit/components-core": "^0.11.0",
"@livekit/components-react": "^2.0.0", "@livekit/components-react": "^2.0.0",
"@opentelemetry/api": "^1.4.0", "@opentelemetry/api": "^1.4.0",
"@opentelemetry/core": "^1.25.1", "@opentelemetry/core": "^1.25.1",
"@opentelemetry/exporter-trace-otlp-http": "^0.55.0", "@opentelemetry/exporter-trace-otlp-http": "^0.57.0",
"@opentelemetry/resources": "^1.25.1", "@opentelemetry/resources": "^1.25.1",
"@opentelemetry/sdk-trace-base": "^1.25.1", "@opentelemetry/sdk-trace-base": "^1.25.1",
"@opentelemetry/sdk-trace-web": "^1.9.1", "@opentelemetry/sdk-trace-web": "^1.9.1",
@@ -45,6 +45,7 @@
"@sentry/react": "^8.0.0", "@sentry/react": "^8.0.0",
"@sentry/vite-plugin": "^2.0.0", "@sentry/vite-plugin": "^2.0.0",
"@testing-library/dom": "^10.1.0", "@testing-library/dom": "^10.1.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.0.0", "@testing-library/react": "^16.0.0",
"@testing-library/react-hooks": "^8.0.1", "@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^14.5.1", "@testing-library/user-event": "^14.5.1",
@@ -52,7 +53,7 @@
"@types/grecaptcha": "^3.0.9", "@types/grecaptcha": "^3.0.9",
"@types/jsdom": "^21.1.7", "@types/jsdom": "^21.1.7",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^20.0.0", "@types/node": "^22.0.0",
"@types/pako": "^2.0.3", "@types/pako": "^2.0.3",
"@types/qrcode": "^1.5.5", "@types/qrcode": "^1.5.5",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
@@ -62,7 +63,7 @@
"@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0", "@typescript-eslint/parser": "^8.0.0",
"@use-gesture/react": "^10.2.11", "@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", "@vector-im/compound-web": "^7.2.0",
"@vitejs/plugin-basic-ssl": "^1.0.1", "@vitejs/plugin-basic-ssl": "^1.0.1",
"@vitejs/plugin-react": "^4.0.1", "@vitejs/plugin-react": "^4.0.1",
@@ -90,7 +91,7 @@
"livekit-client": "^2.5.7", "livekit-client": "^2.5.7",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"loglevel": "^1.9.1", "loglevel": "^1.9.1",
"matrix-js-sdk": "matrix-org/matrix-js-sdk#2210255d6ffc909c574fb8ef16f92140b2fb7797", "matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
"matrix-widget-api": "^1.10.0", "matrix-widget-api": "^1.10.0",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"observable-hooks": "^4.2.3", "observable-hooks": "^4.2.3",
@@ -112,7 +113,7 @@
"typescript-eslint-language-service": "^5.0.5", "typescript-eslint-language-service": "^5.0.5",
"unique-names-generator": "^4.6.0", "unique-names-generator": "^4.6.0",
"vaul": "^1.0.0", "vaul": "^1.0.0",
"vite": "^5.0.0", "vite": "^6.0.0",
"vite-plugin-compression2": "^1.3.1", "vite-plugin-compression2": "^1.3.1",
"vite-plugin-html-template": "^1.1.0", "vite-plugin-html-template": "^1.1.0",
"vite-plugin-svgr": "^4.0.0", "vite-plugin-svgr": "^4.0.0",

View File

@@ -7,7 +7,7 @@ Please see LICENSE in the repository root for full details.
import "matrix-js-sdk/src/@types/global"; import "matrix-js-sdk/src/@types/global";
import type { DurationFormat as PolyfillDurationFormat } from "@formatjs/intl-durationformat"; import type { DurationFormat as PolyfillDurationFormat } from "@formatjs/intl-durationformat";
import { Controls } from "../controls"; import { type Controls } from "../controls";
declare global { declare global {
interface Document { interface Document {

View File

@@ -7,7 +7,7 @@ Please see LICENSE in the repository root for full details.
import "i18next"; import "i18next";
// import all namespaces (for the default language, only) // 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" { declare module "i18next" {
interface CustomTypeOptions { interface CustomTypeOptions {

View File

@@ -6,8 +6,8 @@ Please see LICENSE in the repository root for full details.
*/ */
import { import {
ElementCallReactionEventType, type ElementCallReactionEventType,
ECallReactionEventContent, type ECallReactionEventContent,
} from "../reactions"; } from "../reactions";
// Extend Matrix JS SDK types via Typescript declaration merging to support unspecced event fields and types // Extend Matrix JS SDK types via Typescript declaration merging to support unspecced event fields and types

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details. Please see LICENSE in the repository root for full details.
*/ */
import { FC, Suspense, useEffect, useState } from "react"; import { type FC, Suspense, useEffect, useState } from "react";
import { import {
BrowserRouter as Router, BrowserRouter as Router,
Switch, Switch,
@@ -13,7 +13,7 @@ import {
useLocation, useLocation,
} from "react-router-dom"; } from "react-router-dom";
import * as Sentry from "@sentry/react"; import * as Sentry from "@sentry/react";
import { History } from "history"; import { type History } from "history";
import { TooltipProvider } from "@vector-im/compound-web"; import { TooltipProvider } from "@vector-im/compound-web";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";

156
src/Avatar.test.tsx Normal file
View 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}` },
});
});

View File

@@ -5,11 +5,17 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details. 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 { Avatar as CompoundAvatar } from "@vector-im/compound-web";
import { type MatrixClient } from "matrix-js-sdk/src/client";
import { getAvatarUrl } from "./utils/matrix"; import { useClientState } from "./ClientContext";
import { useClient } from "./ClientContext";
export enum Size { export enum Size {
XS = "xs", XS = "xs",
@@ -27,12 +33,35 @@ export const sizes = new Map([
[Size.XL, 90], [Size.XL, 90],
]); ]);
interface Props { export interface Props {
id: string; id: string;
name: string; name: string;
className?: string; className?: string;
src?: string; src?: string;
size?: Size | number; 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> = ({ export const Avatar: FC<Props> = ({
@@ -41,8 +70,10 @@ export const Avatar: FC<Props> = ({
name, name,
src, src,
size = Size.MD, size = Size.MD,
style,
...props
}) => { }) => {
const { client } = useClient(); const clientState = useClientState();
const sizePx = useMemo( const sizePx = useMemo(
() => () =>
@@ -52,10 +83,50 @@ export const Avatar: FC<Props> = ({
[size], [size],
); );
const resolvedSrc = useMemo(() => { const [avatarUrl, setAvatarUrl] = useState<string | undefined>(undefined);
if (!client || !src || !sizePx) return undefined;
return src.startsWith("mxc://") ? getAvatarUrl(client, src, sizePx) : src; useEffect(() => {
}, [client, src, sizePx]); 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 ( return (
<CompoundAvatar <CompoundAvatar
@@ -63,7 +134,9 @@ export const Avatar: FC<Props> = ({
id={id} id={id}
name={name} name={name}
size={`${sizePx}px`} size={`${sizePx}px`}
src={resolvedSrc} src={avatarUrl}
style={style}
{...props}
/> />
); );
}; };

View File

@@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details.
*/ */
import { import {
FC, type FC,
useCallback, useCallback,
useEffect, useEffect,
useState, useState,
@@ -18,7 +18,7 @@ import {
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { useTranslation } from "react-i18next"; 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 { ClientEvent, type MatrixClient } from "matrix-js-sdk/src/client";
import type { WidgetApi } from "matrix-widget-api"; import type { WidgetApi } from "matrix-widget-api";
@@ -48,6 +48,7 @@ export type ValidClientState = {
disconnected: boolean; disconnected: boolean;
supportedFeatures: { supportedFeatures: {
reactions: boolean; reactions: boolean;
thumbnails: boolean;
}; };
setClient: (params?: SetClientParams) => void; setClient: (params?: SetClientParams) => void;
}; };
@@ -71,6 +72,8 @@ export type SetClientParams = {
const ClientContext = createContext<ClientState | undefined>(undefined); const ClientContext = createContext<ClientState | undefined>(undefined);
export const ClientContextProvider = ClientContext.Provider;
export const useClientState = (): ClientState | undefined => export const useClientState = (): ClientState | undefined =>
useContext(ClientContext); useContext(ClientContext);
@@ -253,6 +256,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
const [isDisconnected, setIsDisconnected] = useState(false); const [isDisconnected, setIsDisconnected] = useState(false);
const [supportsReactions, setSupportsReactions] = useState(false); const [supportsReactions, setSupportsReactions] = useState(false);
const [supportsThumbnails, setSupportsThumbnails] = useState(false);
const state: ClientState | undefined = useMemo(() => { const state: ClientState | undefined = useMemo(() => {
if (alreadyOpenedErr) { if (alreadyOpenedErr) {
@@ -278,6 +282,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
disconnected: isDisconnected, disconnected: isDisconnected,
supportedFeatures: { supportedFeatures: {
reactions: supportsReactions, reactions: supportsReactions,
thumbnails: supportsThumbnails,
}, },
}; };
}, [ }, [
@@ -288,6 +293,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
setClient, setClient,
isDisconnected, isDisconnected,
supportsReactions, supportsReactions,
supportsThumbnails,
]); ]);
const onSync = useCallback( const onSync = useCallback(
@@ -313,6 +319,8 @@ export const ClientProvider: FC<Props> = ({ children }) => {
} }
if (initClientState.widgetApi) { if (initClientState.widgetApi) {
// There is currently no widget API for authenticated media thumbnails.
setSupportsThumbnails(false);
const reactSend = initClientState.widgetApi.hasCapability( const reactSend = initClientState.widgetApi.hasCapability(
"org.matrix.msc2762.send.event:m.reaction", "org.matrix.msc2762.send.event:m.reaction",
); );
@@ -334,6 +342,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
} }
} else { } else {
setSupportsReactions(true); setSupportsReactions(true);
setSupportsThumbnails(true);
} }
return (): void => { return (): void => {

View File

@@ -6,11 +6,11 @@ Please see LICENSE in the repository root for full details.
*/ */
import classNames from "classnames"; 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 { useTranslation } from "react-i18next";
import styles from "./DisconnectedBanner.module.css"; import styles from "./DisconnectedBanner.module.css";
import { ValidClientState, useClientState } from "./ClientContext"; import { type ValidClientState, useClientState } from "./ClientContext";
interface Props extends HTMLAttributes<HTMLElement> { interface Props extends HTMLAttributes<HTMLElement> {
children?: ReactNode; children?: ReactNode;

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details. Please see LICENSE in the repository root for full details.
*/ */
import { FC, ReactNode, useCallback, useEffect } from "react"; import { type FC, type ReactNode, useCallback, useEffect } from "react";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import classNames from "classnames"; import classNames from "classnames";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";

View File

@@ -6,7 +6,12 @@ Please see LICENSE in the repository root for full details.
*/ */
import classNames from "classnames"; 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 { Link } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Heading, Text } from "@vector-im/compound-web"; import { Heading, Text } from "@vector-im/compound-web";

View File

@@ -5,10 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details. 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 { render } from "@testing-library/react";
import { ReactNode, useState } from "react"; import { type ReactNode, useState } from "react";
import { afterEach } from "node:test";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { Modal } from "./Modal"; import { Modal } from "./Modal";

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details. 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 { useTranslation } from "react-i18next";
import { import {
Root as DialogRoot, Root as DialogRoot,

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details. 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 { toDataURL } from "qrcode";
import classNames from "classnames"; import classNames from "classnames";
import { t } from "i18next"; import { t } from "i18next";

View 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
View 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">
&nbsp;{(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">
&nbsp;{video.framesPerSecond.toFixed(0)}fps
</Text>
)}
{"jitter" in video && typeof video.jitter === "number" && (
<Text as="span" size="xs" title="jitter">
&nbsp;{(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">
&nbsp;{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">
&nbsp;{video.qualityLimitationReason}
</Text>
)}
</Button>
</div>
)}
</div>
);
};

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details. 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 { Root, Track, Range, Thumb } from "@radix-ui/react-slider";
import classNames from "classnames"; import classNames from "classnames";
import { Tooltip } from "@vector-im/compound-web"; import { Tooltip } from "@vector-im/compound-web";
@@ -16,6 +16,9 @@ interface Props {
className?: string; className?: string;
label: string; label: string;
value: number; value: number;
/**
* Event handler called when the value changes during an interaction.
*/
onValueChange: (value: number) => void; onValueChange: (value: number) => void;
/** /**
* Event handler called when the value changes at the end of an interaction. * Event handler called when the value changes at the end of an interaction.

View File

@@ -6,9 +6,9 @@ Please see LICENSE in the repository root for full details.
*/ */
import { import {
ComponentType, type ComponentType,
FC, type FC,
SVGAttributes, type SVGAttributes,
useCallback, useCallback,
useEffect, useEffect,
} from "react"; } from "react";

View File

@@ -22,7 +22,7 @@ export abstract class TranslatedError extends Error {
messageKey: ParseKeys<DefaultNamespace, TOptions>, messageKey: ParseKeys<DefaultNamespace, TOptions>,
translationFn: TFunction<DefaultNamespace>, translationFn: TFunction<DefaultNamespace>,
) { ) {
super(translationFn(messageKey, { lng: "en-GB" } as TOptions)); super(translationFn(messageKey, { lng: "en" } as TOptions));
this.translatedMessage = translationFn(messageKey); this.translatedMessage = translationFn(messageKey);
} }
} }

View File

@@ -7,7 +7,11 @@ Please see LICENSE in the repository root for full details.
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { getRoomIdentifierFromUrl, getUrlParams } from "../src/UrlParams"; import {
getRoomIdentifierFromUrl,
getUrlParams,
UserIntent,
} from "../src/UrlParams";
const ROOM_NAME = "roomNameHere"; const ROOM_NAME = "roomNameHere";
const ROOM_ID = "!d45f138fsd"; const ROOM_ID = "!d45f138fsd";
@@ -97,7 +101,146 @@ describe("UrlParams", () => {
}); });
it("respected in widget mode", () => { it("respected in widget mode", () => {
expect(getUrlParams("?preload=true&widgetId=12345").preload).toBe(true); 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);
}); });
}); });
}); });

View File

@@ -10,7 +10,7 @@ import { useLocation } from "react-router-dom";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { Config } from "./config/Config"; import { Config } from "./config/Config";
import { EncryptionSystem } from "./e2ee/sharedKeyManagement"; import { type EncryptionSystem } from "./e2ee/sharedKeyManagement";
import { E2eeType } from "./e2ee/e2eeType"; import { E2eeType } from "./e2ee/e2eeType";
interface RoomIdentifier { interface RoomIdentifier {
@@ -19,6 +19,12 @@ interface RoomIdentifier {
viaServers: string[]; 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 // 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 // a specific behavior (such as 'confineToRoom'), rather than one that describes
// the situations that call for this behavior ('isEmbedded'). This makes it // the situations that call for this behavior ('isEmbedded'). This makes it
@@ -142,6 +148,13 @@ export interface UrlParams {
* creating a spa link. * creating a spa link.
*/ */
homeserver: string | null; 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 // This is here as a stopgap, but what would be far nicer is a function that
@@ -211,12 +224,17 @@ export const getUrlParams = (
const fontScale = parseFloat(parser.getParam("fontScale") ?? ""); 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 widgetId = parser.getParam("widgetId");
const isWidget = !!widgetId; const parentUrl = parser.getParam("parentUrl");
const isWidget = !!widgetId && !!parentUrl;
return { return {
widgetId, widgetId,
parentUrl: parser.getParam("parentUrl"), parentUrl,
// NB. we don't validate roomId here as we do in getRoomIdentifierFromUrl: // 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 // what would we do if it were invalid? If the widget API says that's what
@@ -232,21 +250,25 @@ export const getUrlParams = (
showControls: parser.getFlagParam("showControls", true), showControls: parser.getFlagParam("showControls", true),
hideScreensharing: parser.getFlagParam("hideScreensharing"), hideScreensharing: parser.getFlagParam("hideScreensharing"),
e2eEnabled: parser.getFlagParam("enableE2EE", true), e2eEnabled: parser.getFlagParam("enableE2EE", true),
userId: parser.getParam("userId"), userId: isWidget ? parser.getParam("userId") : null,
displayName: parser.getParam("displayName"), displayName: parser.getParam("displayName"),
deviceId: parser.getParam("deviceId"), deviceId: isWidget ? parser.getParam("deviceId") : null,
baseUrl: parser.getParam("baseUrl"), baseUrl: isWidget ? parser.getParam("baseUrl") : null,
lang: parser.getParam("lang"), lang: parser.getParam("lang"),
fonts: parser.getAllParams("font"), fonts: parser.getAllParams("font"),
fontScale: Number.isNaN(fontScale) ? null : fontScale, fontScale: Number.isNaN(fontScale) ? null : fontScale,
analyticsID: parser.getParam("analyticsID"), analyticsID: parser.getParam("analyticsID"),
allowIceFallback: parser.getFlagParam("allowIceFallback"), allowIceFallback: parser.getFlagParam("allowIceFallback"),
perParticipantE2EE: parser.getFlagParam("perParticipantE2EE"), perParticipantE2EE: parser.getFlagParam("perParticipantE2EE"),
skipLobby: parser.getFlagParam("skipLobby"), skipLobby: parser.getFlagParam(
returnToLobby: parser.getFlagParam("returnToLobby"), "skipLobby",
isWidget && intent === UserIntent.StartNewCall,
),
returnToLobby: isWidget ? parser.getFlagParam("returnToLobby") : true,
theme: parser.getParam("theme"), theme: parser.getParam("theme"),
viaServers: parser.getParam("viaServers"), viaServers: !isWidget ? parser.getParam("viaServers") : null,
homeserver: parser.getParam("homeserver"), homeserver: !isWidget ? parser.getParam("homeserver") : null,
intent,
}; };
}; };

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details. 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 { useLocation } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Menu, MenuItem } from "@vector-im/compound-web"; import { Menu, MenuItem } from "@vector-im/compound-web";

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details. Please see LICENSE in the repository root for full details.
*/ */
import { FC, useCallback, useState } from "react"; import { type FC, useCallback, useState } from "react";
import { useHistory, useLocation } from "react-router-dom"; import { useHistory, useLocation } from "react-router-dom";
import { useClientLegacy } from "./ClientContext"; import { useClientLegacy } from "./ClientContext";

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details. 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 { Trans } from "react-i18next";
import { ExternalLink } from "../button/Link"; import { ExternalLink } from "../button/Link";

View File

@@ -5,9 +5,13 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details. 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 { 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 { Buffer } from "buffer";
import { widget } from "../widget"; import { widget } from "../widget";
@@ -411,7 +415,7 @@ export class PosthogAnalytics {
// * When the user changes their preferences on this device // * When the user changes their preferences on this device
// Note that for new accounts, pseudonymousAnalyticsOptIn won't be set, so updateAnonymityFromSettings // 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) // 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.setAnonymity(optIn ? Anonymity.Pseudonymous : Anonymity.Disabled);
this.maybeIdentifyUser().catch(() => this.maybeIdentifyUser().catch(() =>
logger.log("Could not identify user"), logger.log("Could not identify user"),

View File

@@ -5,12 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details. 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 { 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 { import {
IPosthogEvent, type IPosthogEvent,
PosthogAnalytics, PosthogAnalytics,
RegistrationType, RegistrationType,
} from "./PosthogAnalytics"; } from "./PosthogAnalytics";

View File

@@ -6,9 +6,9 @@ Please see LICENSE in the repository root for full details.
*/ */
import { import {
SpanProcessor, type SpanProcessor,
ReadableSpan, type ReadableSpan,
Span, type Span,
} from "@opentelemetry/sdk-trace-base"; } from "@opentelemetry/sdk-trace-base";
import { hrTimeToMilliseconds } from "@opentelemetry/core"; import { hrTimeToMilliseconds } from "@opentelemetry/core";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";

View File

@@ -5,12 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details. 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 { hrTimeToMicroseconds } from "@opentelemetry/core";
import { import {
SpanProcessor, type SpanProcessor,
ReadableSpan, type ReadableSpan,
Span, type Span,
} from "@opentelemetry/sdk-trace-base"; } from "@opentelemetry/sdk-trace-base";
const dumpAttributes = ( const dumpAttributes = (

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details. Please see LICENSE in the repository root for full details.
*/ */
import { FC, FormEvent, useCallback, useRef, useState } from "react"; import { type FC, type FormEvent, useCallback, useRef, useState } from "react";
import { useHistory, useLocation } from "react-router-dom"; import { useHistory, useLocation } from "react-router-dom";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { Button } from "@vector-im/compound-web"; import { Button } from "@vector-im/compound-web";

View File

@@ -6,9 +6,9 @@ Please see LICENSE in the repository root for full details.
*/ */
import { import {
ChangeEvent, type ChangeEvent,
FC, type FC,
FormEvent, type FormEvent,
useCallback, useCallback,
useEffect, useEffect,
useRef, useRef,

View File

@@ -9,12 +9,12 @@ import { useCallback } from "react";
import { InteractiveAuth } from "matrix-js-sdk/src/interactive-auth"; import { InteractiveAuth } from "matrix-js-sdk/src/interactive-auth";
import { import {
createClient, createClient,
LoginResponse, type LoginResponse,
MatrixClient, type MatrixClient,
} from "matrix-js-sdk/src/matrix"; } from "matrix-js-sdk/src/matrix";
import { initClient } from "../utils/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. * 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 * @param oldClient If there is an already authenticated client it should be passed to this hook

View File

@@ -9,13 +9,13 @@ import { useState, useEffect, useCallback, useRef } from "react";
import { InteractiveAuth } from "matrix-js-sdk/src/interactive-auth"; import { InteractiveAuth } from "matrix-js-sdk/src/interactive-auth";
import { import {
createClient, createClient,
MatrixClient, type MatrixClient,
RegisterResponse, type RegisterResponse,
} from "matrix-js-sdk/src/matrix"; } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { initClient } from "../utils/matrix"; import { initClient } from "../utils/matrix";
import { Session } from "../ClientContext"; import { type Session } from "../ClientContext";
import { Config } from "../config/Config"; import { Config } from "../config/Config";
import { widget } from "../widget"; import { widget } from "../widget";

View File

@@ -4,7 +4,7 @@ Copyright 2022-2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details. 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 classNames from "classnames";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Button as CpdButton, Tooltip } from "@vector-im/compound-web"; import { Button as CpdButton, Tooltip } from "@vector-im/compound-web";

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details. 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 { Button } from "@vector-im/compound-web";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { UserAddIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { UserAddIcon } from "@vector-im/compound-design-tokens/assets/web/icons";

View File

@@ -6,15 +6,15 @@ Please see LICENSE in the repository root for full details.
*/ */
import { import {
ComponentPropsWithoutRef, type ComponentPropsWithoutRef,
forwardRef, forwardRef,
MouseEvent, type MouseEvent,
useCallback, useCallback,
useMemo, useMemo,
} from "react"; } from "react";
import { Link as CpdLink } from "@vector-im/compound-web"; import { Link as CpdLink } from "@vector-im/compound-web";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { createPath, LocationDescriptor, Path } from "history"; import { createPath, type LocationDescriptor, type Path } from "history";
import classNames from "classnames"; import classNames from "classnames";
import { useLatest } from "../useLatest"; import { useLatest } from "../useLatest";

View File

@@ -5,9 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details. 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 { Button } from "@vector-im/compound-web";
import { LocationDescriptor } from "history"; import { type LocationDescriptor } from "history";
import { useLink } from "./Link"; import { useLink } from "./Link";

View File

@@ -5,47 +5,47 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details. 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 { expect, test } from "vitest";
import { TooltipProvider } from "@vector-im/compound-web"; import { TooltipProvider } from "@vector-im/compound-web";
import { userEvent } from "@testing-library/user-event"; 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 { ReactionToggleButton } from "./ReactionToggleButton";
import { ElementCallReactionEventType } from "../reactions"; 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 localIdent = `${localRtcMember.sender}:${localRtcMember.deviceId}`;
const memberEventAlice = "$membership-alice:example.org";
const membership: Record<string, string> = {
[memberEventAlice]: memberUserIdAlice,
};
function TestComponent({ function TestComponent({
rtcSession, rtcSession,
vm,
}: { }: {
rtcSession: MockRTCSession; rtcSession: MockRTCSession;
vm: CallViewModel;
}): ReactNode { }): ReactNode {
return ( return (
<TooltipProvider> <TooltipProvider>
<TestReactionsWrapper rtcSession={rtcSession}> <ReactionsSenderProvider
<ReactionToggleButton userId={memberUserIdAlice} /> vm={vm}
</TestReactionsWrapper> rtcSession={rtcSession as unknown as MatrixRTCSession}
>
<ReactionToggleButton vm={vm} identifier={localIdent} />
</ReactionsSenderProvider>
</TooltipProvider> </TooltipProvider>
); );
} }
test("Can open menu", async () => { test("Can open menu", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const room = new MockRoom(memberUserIdAlice); const { vm, rtcSession } = getBasicCallViewModelEnvironment([alice]);
const rtcSession = new MockRTCSession(room, membership);
const { getByLabelText, container } = render( const { getByLabelText, container } = render(
<TestComponent rtcSession={rtcSession} />, <TestComponent vm={vm} rtcSession={rtcSession} />,
); );
await user.click(getByLabelText("common.reactions")); await user.click(getByLabelText("common.reactions"));
expect(container).toMatchSnapshot(); expect(container).toMatchSnapshot();
@@ -53,102 +53,120 @@ test("Can open menu", async () => {
test("Can raise hand", async () => { test("Can raise hand", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const room = new MockRoom(memberUserIdAlice); const { vm, rtcSession, handRaisedSubject$ } =
const rtcSession = new MockRTCSession(room, membership); getBasicCallViewModelEnvironment([local, alice]);
const { getByLabelText, container } = render( const { getByLabelText, container } = render(
<TestComponent rtcSession={rtcSession} />, <TestComponent vm={vm} rtcSession={rtcSession} />,
); );
await user.click(getByLabelText("common.reactions")); await user.click(getByLabelText("common.reactions"));
await user.click(getByLabelText("action.raise_hand")); await user.click(getByLabelText("action.raise_hand"));
expect(room.testSentEvents).toEqual([ expect(rtcSession.room.client.sendEvent).toHaveBeenCalledWith(
[ rtcSession.room.roomId,
undefined, "m.reaction",
"m.reaction", {
{ "m.relates_to": {
"m.relates_to": { event_id: localRtcMember.eventId,
event_id: memberEventAlice, key: "🖐️",
key: "🖐️", rel_type: "m.annotation",
rel_type: "m.annotation",
},
}, },
], },
]); );
act(() => {
// Mock receiving a reaction.
handRaisedSubject$.next({
[localIdent]: {
time: new Date(),
reactionEventId: "",
membershipEventId: localRtcMember.eventId!,
},
});
});
expect(container).toMatchSnapshot(); expect(container).toMatchSnapshot();
}); });
test("Can lower hand", async () => { test("Can lower hand", async () => {
const reactionEventId = "$my-reaction-event:example.org";
const user = userEvent.setup(); const user = userEvent.setup();
const room = new MockRoom(memberUserIdAlice); const { vm, rtcSession, handRaisedSubject$ } =
const rtcSession = new MockRTCSession(room, membership); getBasicCallViewModelEnvironment([local, alice]);
const { getByLabelText, container } = render( 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("common.reactions"));
await user.click(getByLabelText("action.lower_hand")); 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(); expect(container).toMatchSnapshot();
}); });
test("Can react with emoji", async () => { test("Can react with emoji", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const room = new MockRoom(memberUserIdAlice); const { vm, rtcSession } = getBasicCallViewModelEnvironment([local, alice]);
const rtcSession = new MockRTCSession(room, membership);
const { getByLabelText, getByText } = render( const { getByLabelText, getByText } = render(
<TestComponent rtcSession={rtcSession} />, <TestComponent vm={vm} rtcSession={rtcSession} />,
); );
await user.click(getByLabelText("common.reactions")); await user.click(getByLabelText("common.reactions"));
await user.click(getByText("🐶")); await user.click(getByText("🐶"));
expect(room.testSentEvents).toEqual([ expect(rtcSession.room.client.sendEvent).toHaveBeenCalledWith(
[ rtcSession.room.roomId,
undefined, ElementCallReactionEventType,
ElementCallReactionEventType, {
{ "m.relates_to": {
"m.relates_to": { event_id: localRtcMember.eventId,
event_id: memberEventAlice, rel_type: "m.reference",
rel_type: "m.reference",
},
name: "dog",
emoji: "🐶",
}, },
], name: "dog",
]); emoji: "🐶",
},
);
}); });
test("Can fully expand emoji picker", async () => { test("Can fully expand emoji picker", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const room = new MockRoom(memberUserIdAlice); const { vm, rtcSession } = getBasicCallViewModelEnvironment([local, alice]);
const rtcSession = new MockRTCSession(room, membership); const { getByLabelText, container, getByText } = render(
const { getByText, container, getByLabelText } = render( <TestComponent vm={vm} rtcSession={rtcSession} />,
<TestComponent rtcSession={rtcSession} />,
); );
await user.click(getByLabelText("common.reactions")); await user.click(getByLabelText("common.reactions"));
await user.click(getByLabelText("action.show_more")); await user.click(getByLabelText("action.show_more"));
expect(container).toMatchSnapshot(); expect(container).toMatchSnapshot();
await user.click(getByText("🦗")); await user.click(getByText("🦗"));
expect(rtcSession.room.client.sendEvent).toHaveBeenCalledWith(
expect(room.testSentEvents).toEqual([ rtcSession.room.roomId,
[ ElementCallReactionEventType,
undefined, {
ElementCallReactionEventType, "m.relates_to": {
{ event_id: localRtcMember.eventId,
"m.relates_to": { rel_type: "m.reference",
event_id: memberEventAlice,
rel_type: "m.reference",
},
name: "crickets",
emoji: "🦗",
}, },
], name: "crickets",
]); emoji: "🦗",
},
);
}); });
test("Can close reaction dialog", async () => { test("Can close reaction dialog", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const room = new MockRoom(memberUserIdAlice); const { vm, rtcSession } = getBasicCallViewModelEnvironment([local, alice]);
const rtcSession = new MockRTCSession(room, membership);
const { getByLabelText, container } = render( const { getByLabelText, container } = render(
<TestComponent rtcSession={rtcSession} />, <TestComponent vm={vm} rtcSession={rtcSession} />,
); );
await user.click(getByLabelText("common.reactions")); await user.click(getByLabelText("common.reactions"));
await user.click(getByLabelText("action.show_more")); await user.click(getByLabelText("action.show_more"));

View File

@@ -13,9 +13,9 @@ import {
ReactionSolidIcon, ReactionSolidIcon,
} from "@vector-im/compound-design-tokens/assets/web/icons"; } from "@vector-im/compound-design-tokens/assets/web/icons";
import { import {
ComponentPropsWithoutRef, type ComponentPropsWithoutRef,
FC, type FC,
ReactNode, type ReactNode,
useCallback, useCallback,
useEffect, useEffect,
useMemo, useMemo,
@@ -24,11 +24,18 @@ import {
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import classNames from "classnames"; 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 styles from "./ReactionToggleButton.module.css";
import { ReactionOption, ReactionSet, ReactionsRowSize } from "../reactions"; import {
type ReactionOption,
ReactionSet,
ReactionsRowSize,
} from "../reactions";
import { Modal } from "../Modal"; import { Modal } from "../Modal";
import { type CallViewModel } from "../state/CallViewModel";
interface InnerButtonProps extends ComponentPropsWithoutRef<"button"> { interface InnerButtonProps extends ComponentPropsWithoutRef<"button"> {
raised: boolean; raised: boolean;
@@ -158,22 +165,27 @@ export function ReactionPopupMenu({
} }
interface ReactionToggleButtonProps extends ComponentPropsWithoutRef<"button"> { interface ReactionToggleButtonProps extends ComponentPropsWithoutRef<"button"> {
userId: string; identifier: string;
vm: CallViewModel;
} }
export function ReactionToggleButton({ export function ReactionToggleButton({
userId, identifier,
vm,
...props ...props
}: ReactionToggleButtonProps): ReactNode { }: ReactionToggleButtonProps): ReactNode {
const { t } = useTranslation(); const { t } = useTranslation();
const { raisedHands, toggleRaisedHand, sendReaction, reactions } = const { toggleRaisedHand, sendReaction } = useReactionsSender();
useReactions();
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const [showReactionsMenu, setShowReactionsMenu] = useState(false); const [showReactionsMenu, setShowReactionsMenu] = useState(false);
const [errorText, setErrorText] = useState<string>(); const [errorText, setErrorText] = useState<string>();
const isHandRaised = !!raisedHands[userId]; const isHandRaised = useObservableState(
const canReact = !reactions[userId]; vm.handsRaised$.pipe(map((v) => !!v[identifier])),
);
const canReact = useObservableState(
vm.reactions$.pipe(map((v) => !v[identifier])),
);
useEffect(() => { useEffect(() => {
// Clear whenever the reactions menu state changes. // Clear whenever the reactions menu state changes.
@@ -219,7 +231,7 @@ export function ReactionToggleButton({
<InnerButton <InnerButton
disabled={busy} disabled={busy}
onClick={() => setShowReactionsMenu((show) => !show)} onClick={() => setShowReactionsMenu((show) => !show)}
raised={isHandRaised} raised={!!isHandRaised}
open={showReactionsMenu} open={showReactionsMenu}
{...props} {...props}
/> />
@@ -233,8 +245,8 @@ export function ReactionToggleButton({
> >
<ReactionPopupMenu <ReactionPopupMenu
errorText={errorText} errorText={errorText}
isHandRaised={isHandRaised} isHandRaised={!!isHandRaised}
canReact={!busy && canReact} canReact={!busy && !!canReact}
sendReaction={(reaction) => void sendRelation(reaction)} sendReaction={(reaction) => void sendRelation(reaction)}
toggleRaisedHand={wrappedToggleRaisedHand} toggleRaisedHand={wrappedToggleRaisedHand}
/> />

View File

@@ -9,7 +9,7 @@ exports[`Can close reaction dialog 1`] = `
aria-disabled="false" aria-disabled="false"
aria-expanded="true" aria-expanded="true"
aria-haspopup="true" aria-haspopup="true"
aria-labelledby=":r9l:" aria-labelledby=":rav:"
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59" class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
data-kind="primary" data-kind="primary"
data-size="lg" data-size="lg"
@@ -43,7 +43,7 @@ exports[`Can fully expand emoji picker 1`] = `
aria-disabled="false" aria-disabled="false"
aria-expanded="true" aria-expanded="true"
aria-haspopup="true" aria-haspopup="true"
aria-labelledby=":r6c:" aria-labelledby=":r7m:"
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59" class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
data-kind="primary" data-kind="primary"
data-size="lg" data-size="lg"
@@ -75,8 +75,8 @@ exports[`Can lower hand 1`] = `
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
aria-labelledby=":r36:" aria-labelledby=":r36:"
class="_button_i91xf_17 raisedButton _has-icon_i91xf_66 _icon-only_i91xf_59" class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
data-kind="primary" data-kind="secondary"
data-size="lg" data-size="lg"
role="button" role="button"
tabindex="0" tabindex="0"
@@ -90,7 +90,9 @@ exports[`Can lower hand 1`] = `
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <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> </svg>
</button> </button>
@@ -138,8 +140,8 @@ exports[`Can raise hand 1`] = `
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
aria-labelledby=":r1j:" aria-labelledby=":r1j:"
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59" class="_button_i91xf_17 raisedButton _has-icon_i91xf_66 _icon-only_i91xf_59"
data-kind="secondary" data-kind="primary"
data-size="lg" data-size="lg"
role="button" role="button"
tabindex="0" tabindex="0"
@@ -153,9 +155,7 @@ exports[`Can raise hand 1`] = `
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <path
clip-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"
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> </svg>
</button> </button>

View File

@@ -10,8 +10,8 @@ import { merge } from "lodash-es";
import { getUrlParams } from "../UrlParams"; import { getUrlParams } from "../UrlParams";
import { import {
DEFAULT_CONFIG, DEFAULT_CONFIG,
ConfigOptions, type ConfigOptions,
ResolvedConfigOptions, type ResolvedConfigOptions,
} from "./ConfigOptions"; } from "./ConfigOptions";
export class Config { export class Config {

View File

@@ -51,7 +51,7 @@ export interface ConfigOptions {
// a livekit service url in the client well-known. // a livekit service url in the client well-known.
// The well known needs to be formatted like so: // The well known needs to be formatted like so:
// {"type":"livekit", "livekit_service_url":"https://livekit.example.com"} // {"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; livekit_service_url: string;
}; };

View File

@@ -13,18 +13,18 @@ export interface Controls {
disablePip: () => void; disablePip: () => void;
} }
export const setPipEnabled = new Subject<boolean>(); export const setPipEnabled$ = new Subject<boolean>();
window.controls = { window.controls = {
canEnterPip(): boolean { canEnterPip(): boolean {
return setPipEnabled.observed; return setPipEnabled$.observed;
}, },
enablePip(): void { enablePip(): void {
if (!setPipEnabled.observed) throw new Error("No call is running"); if (!setPipEnabled$.observed) throw new Error("No call is running");
setPipEnabled.next(true); setPipEnabled$.next(true);
}, },
disablePip(): void { disablePip(): void {
if (!setPipEnabled.observed) throw new Error("No call is running"); if (!setPipEnabled$.observed) throw new Error("No call is running");
setPipEnabled.next(false); setPipEnabled$.next(false);
}, },
}; };

View File

@@ -7,7 +7,7 @@ Please see LICENSE in the repository root for full details.
import { describe, expect, test, vi } from "vitest"; import { describe, expect, test, vi } from "vitest";
import { import {
MatrixRTCSession, type MatrixRTCSession,
MatrixRTCSessionEvent, MatrixRTCSessionEvent,
} from "matrix-js-sdk/src/matrixrtc"; } from "matrix-js-sdk/src/matrixrtc";
import { KeyProviderEvent } from "livekit-client"; import { KeyProviderEvent } from "livekit-client";

View File

@@ -8,7 +8,7 @@ Please see LICENSE in the repository root for full details.
import { BaseKeyProvider, createKeyMaterialFromBuffer } from "livekit-client"; import { BaseKeyProvider, createKeyMaterialFromBuffer } from "livekit-client";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { import {
MatrixRTCSession, type MatrixRTCSession,
MatrixRTCSessionEvent, MatrixRTCSessionEvent,
} from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";

View File

@@ -8,7 +8,7 @@ Please see LICENSE in the repository root for full details.
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import { setLocalStorageItem, useLocalStorage } from "../useLocalStorage"; import { setLocalStorageItem, useLocalStorage } from "../useLocalStorage";
import { UrlParams, getUrlParams, useUrlParams } from "../UrlParams"; import { type UrlParams, getUrlParams, useUrlParams } from "../UrlParams";
import { E2eeType } from "./e2eeType"; import { E2eeType } from "./e2eeType";
import { useClient } from "../ClientContext"; import { useClient } from "../ClientContext";

View File

@@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details.
*/ */
import classNames from "classnames"; import classNames from "classnames";
import { FormEventHandler, forwardRef, ReactNode } from "react"; import { type FormEventHandler, forwardRef, type ReactNode } from "react";
import styles from "./Form.module.css"; import styles from "./Form.module.css";

View File

@@ -5,11 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details. Please see LICENSE in the repository root for full details.
*/ */
import { BehaviorSubject, Observable } from "rxjs"; import { type BehaviorSubject, type Observable } from "rxjs";
import { ComponentType } from "react"; import { type ComponentType } from "react";
import { LayoutProps } from "./Grid"; import { type LayoutProps } from "./Grid";
import { TileViewModel } from "../state/TileViewModel"; import { type TileViewModel } from "../state/TileViewModel";
export interface Bounds { export interface Bounds {
width: number; width: number;
@@ -31,15 +31,15 @@ export interface CallLayoutInputs {
/** /**
* The minimum bounds of the layout area. * The minimum bounds of the layout area.
*/ */
minBounds: Observable<Bounds>; minBounds$: Observable<Bounds>;
/** /**
* The alignment of the floating spotlight tile, if present. * 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. * The alignment of the small picture-in-picture tile, if present.
*/ */
pipAlignment: BehaviorSubject<Alignment>; pipAlignment$: BehaviorSubject<Alignment>;
} }
export interface CallLayoutOutputs<Model> { export interface CallLayoutOutputs<Model> {

View File

@@ -6,25 +6,24 @@ Please see LICENSE in the repository root for full details.
*/ */
import { import {
SpringRef, type SpringRef,
TransitionFn, type TransitionFn,
animated, type animated,
useTransition, useTransition,
} from "@react-spring/web"; } from "@react-spring/web";
import { EventTypes, Handler, useScroll } from "@use-gesture/react"; import { type EventTypes, type Handler, useScroll } from "@use-gesture/react";
import { import {
CSSProperties, type CSSProperties,
ComponentProps, type ComponentProps,
ComponentType, type ComponentType,
Dispatch, type Dispatch,
FC, type FC,
LegacyRef, type LegacyRef,
ReactNode, type ReactNode,
SetStateAction, type SetStateAction,
createContext, createContext,
forwardRef, forwardRef,
memo, memo,
useCallback,
useContext, useContext,
useEffect, useEffect,
useMemo, useMemo,
@@ -54,7 +53,6 @@ interface Tile<Model> {
id: string; id: string;
model: Model; model: Model;
onDrag: DragCallback | undefined; onDrag: DragCallback | undefined;
setVisible: (visible: boolean) => void;
} }
type PlacedTile<Model> = Tile<Model> & Rect; type PlacedTile<Model> = Tile<Model> & Rect;
@@ -88,7 +86,6 @@ interface SlotProps<Model> extends Omit<ComponentProps<"div">, "onDrag"> {
id: string; id: string;
model: Model; model: Model;
onDrag?: DragCallback; onDrag?: DragCallback;
onVisibilityChange?: (visible: boolean) => void;
style?: CSSProperties; style?: CSSProperties;
className?: string; className?: string;
} }
@@ -115,28 +112,51 @@ function offset(element: HTMLElement, relativeTo: Element): Offset {
} }
} }
export type VisibleTilesCallback = (visibleTiles: number) => void;
interface LayoutContext { interface LayoutContext {
setGeneration: Dispatch<SetStateAction<number | null>>; setGeneration: Dispatch<SetStateAction<number | null>>;
setVisibleTilesCallback: Dispatch<
SetStateAction<VisibleTilesCallback | null>
>;
} }
const LayoutContext = createContext<LayoutContext | null>(null); const LayoutContext = createContext<LayoutContext | null>(null);
function useLayoutContext(): LayoutContext {
const context = useContext(LayoutContext);
if (context === null)
throw new Error("useUpdateLayout called outside a Grid layout context");
return context;
}
/** /**
* Enables Grid to react to layout changes. You must call this in your Layout * Enables Grid to react to layout changes. You must call this in your Layout
* component or else Grid will not be reactive. * component or else Grid will not be reactive.
*/ */
export function useUpdateLayout(): void { export function useUpdateLayout(): void {
const context = useContext(LayoutContext); const { setGeneration } = useLayoutContext();
if (context === null)
throw new Error("useUpdateLayout called outside a Grid layout context");
// On every render, tell Grid that the layout may have changed // On every render, tell Grid that the layout may have changed
useEffect(() => useEffect(() => setGeneration((prev) => (prev === null ? 0 : prev + 1)));
context.setGeneration((prev) => (prev === null ? 0 : prev + 1)), }
/**
* Asks Grid to call a callback whenever the number of visible tiles may have
* changed.
*/
export function useVisibleTiles(callback: VisibleTilesCallback): void {
const { setVisibleTilesCallback } = useLayoutContext();
useEffect(
() => setVisibleTilesCallback(() => callback),
[callback, setVisibleTilesCallback],
);
useEffect(
() => (): void => setVisibleTilesCallback(null),
[setVisibleTilesCallback],
); );
} }
const windowHeightObservable = fromEvent(window, "resize").pipe( const windowHeightObservable$ = fromEvent(window, "resize").pipe(
startWith(null), startWith(null),
map(() => window.innerHeight), map(() => window.innerHeight),
); );
@@ -242,42 +262,23 @@ export function Grid<
const [gridRoot, gridRef2] = useState<HTMLElement | null>(null); const [gridRoot, gridRef2] = useState<HTMLElement | null>(null);
const gridRef = useMergedRefs<HTMLElement>(gridRef1, gridRef2); const gridRef = useMergedRefs<HTMLElement>(gridRef1, gridRef2);
const windowHeight = useObservableEagerState(windowHeightObservable); const windowHeight = useObservableEagerState(windowHeightObservable$);
const [layoutRoot, setLayoutRoot] = useState<HTMLElement | null>(null); const [layoutRoot, setLayoutRoot] = useState<HTMLElement | null>(null);
const [generation, setGeneration] = useState<number | null>(null); const [generation, setGeneration] = useState<number | null>(null);
const [visibleTilesCallback, setVisibleTilesCallback] =
useState<VisibleTilesCallback | null>(null);
const tiles = useInitial(() => new Map<string, Tile<TileModel>>()); const tiles = useInitial(() => new Map<string, Tile<TileModel>>());
const prefersReducedMotion = usePrefersReducedMotion(); const prefersReducedMotion = usePrefersReducedMotion();
const Slot: FC<SlotProps<TileModel>> = useMemo( const Slot: FC<SlotProps<TileModel>> = useMemo(
() => () =>
function Slot({ function Slot({ id, model, onDrag, style, className, ...props }) {
id,
model,
onDrag,
onVisibilityChange,
style,
className,
...props
}) {
const ref = useRef<HTMLDivElement | null>(null); const ref = useRef<HTMLDivElement | null>(null);
const prevVisible = useRef<boolean | null>(null);
const setVisible = useCallback(
(visible: boolean) => {
if (
onVisibilityChange !== undefined &&
visible !== prevVisible.current
) {
onVisibilityChange(visible);
prevVisible.current = visible;
}
},
[onVisibilityChange],
);
useEffect(() => { useEffect(() => {
tiles.set(id, { id, model, onDrag, setVisible }); tiles.set(id, { id, model, onDrag });
return (): void => void tiles.delete(id); return (): void => void tiles.delete(id);
}, [id, model, onDrag, setVisible]); }, [id, model, onDrag]);
return ( return (
<div <div
@@ -307,7 +308,10 @@ export function Grid<
[], [],
); );
const context: LayoutContext = useMemo(() => ({ setGeneration }), []); const context: LayoutContext = useMemo(
() => ({ setGeneration, setVisibleTilesCallback }),
[setVisibleTilesCallback],
);
// Combine the tile definitions and slots together to create placed tiles // Combine the tile definitions and slots together to create placed tiles
const placedTiles = useMemo(() => { const placedTiles = useMemo(() => {
@@ -342,9 +346,11 @@ export function Grid<
); );
useEffect(() => { useEffect(() => {
for (const tile of placedTiles) visibleTilesCallback?.(
tile.setVisible(tile.y + tile.height <= visibleHeight); placedTiles.filter((tile) => tile.y + tile.height <= visibleHeight)
}, [placedTiles, visibleHeight]); .length,
);
}, [placedTiles, visibleTilesCallback, visibleHeight]);
// Drag state is stored in a ref rather than component state, because we use // Drag state is stored in a ref rather than component state, because we use
// react-spring's imperative API during gestures to improve responsiveness // react-spring's imperative API during gestures to improve responsiveness

View File

@@ -5,15 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details. Please see LICENSE in the repository root for full details.
*/ */
import { CSSProperties, forwardRef, useCallback, useMemo } from "react"; import { type CSSProperties, forwardRef, useCallback, useMemo } from "react";
import { distinctUntilChanged } from "rxjs"; import { distinctUntilChanged } from "rxjs";
import { useObservableEagerState } from "observable-hooks"; import { useObservableEagerState } from "observable-hooks";
import { GridLayout as GridLayoutModel } from "../state/CallViewModel"; import { type GridLayout as GridLayoutModel } from "../state/CallViewModel";
import styles from "./GridLayout.module.css"; import styles from "./GridLayout.module.css";
import { useInitial } from "../useInitial"; import { useInitial } from "../useInitial";
import { CallLayout, arrangeTiles } from "./CallLayout"; import { type CallLayout, arrangeTiles } from "./CallLayout";
import { DragCallback, useUpdateLayout } from "./Grid"; import { type DragCallback, useUpdateLayout, useVisibleTiles } from "./Grid";
interface GridCSSProperties extends CSSProperties { interface GridCSSProperties extends CSSProperties {
"--gap": string; "--gap": string;
@@ -26,8 +26,8 @@ interface GridCSSProperties extends CSSProperties {
* together in a scrolling grid. * together in a scrolling grid.
*/ */
export const makeGridLayout: CallLayout<GridLayoutModel> = ({ export const makeGridLayout: CallLayout<GridLayoutModel> = ({
minBounds, minBounds$,
spotlightAlignment, spotlightAlignment$,
}) => ({ }) => ({
scrollingOnTop: false, scrollingOnTop: false,
@@ -37,7 +37,7 @@ export const makeGridLayout: CallLayout<GridLayoutModel> = ({
useUpdateLayout(); useUpdateLayout();
const alignment = useObservableEagerState( const alignment = useObservableEagerState(
useInitial(() => useInitial(() =>
spotlightAlignment.pipe( spotlightAlignment$.pipe(
distinctUntilChanged( distinctUntilChanged(
(a1, a2) => a1.block === a2.block && a1.inline === a2.inline, (a1, a2) => a1.block === a2.block && a1.inline === a2.inline,
), ),
@@ -47,7 +47,7 @@ export const makeGridLayout: CallLayout<GridLayoutModel> = ({
const onDragSpotlight: DragCallback = useCallback( const onDragSpotlight: DragCallback = useCallback(
({ xRatio, yRatio }) => ({ xRatio, yRatio }) =>
spotlightAlignment.next({ spotlightAlignment$.next({
block: yRatio < 0.5 ? "start" : "end", block: yRatio < 0.5 ? "start" : "end",
inline: xRatio < 0.5 ? "start" : "end", inline: xRatio < 0.5 ? "start" : "end",
}), }),
@@ -73,7 +73,8 @@ export const makeGridLayout: CallLayout<GridLayoutModel> = ({
// The scrolling part of the layout is where all the grid tiles live // The scrolling part of the layout is where all the grid tiles live
scrolling: forwardRef(function GridLayout({ model, Slot }, ref) { scrolling: forwardRef(function GridLayout({ model, Slot }, ref) {
useUpdateLayout(); useUpdateLayout();
const { width, height: minHeight } = useObservableEagerState(minBounds); useVisibleTiles(model.setVisibleTiles);
const { width, height: minHeight } = useObservableEagerState(minBounds$);
const { gap, tileWidth, tileHeight } = useMemo( const { gap, tileWidth, tileHeight } = useMemo(
() => arrangeTiles(width, minHeight, model.grid.length), () => arrangeTiles(width, minHeight, model.grid.length),
[width, minHeight, model.grid.length], [width, minHeight, model.grid.length],
@@ -93,13 +94,7 @@ export const makeGridLayout: CallLayout<GridLayoutModel> = ({
} }
> >
{model.grid.map((m) => ( {model.grid.map((m) => (
<Slot <Slot key={m.id} className={styles.slot} id={m.id} model={m} />
key={m.id}
className={styles.slot}
id={m.id}
model={m}
onVisibilityChange={m.setVisible}
/>
))} ))}
</div> </div>
); );

View File

@@ -9,18 +9,18 @@ import { forwardRef, useCallback, useMemo } from "react";
import { useObservableEagerState } from "observable-hooks"; import { useObservableEagerState } from "observable-hooks";
import classNames from "classnames"; import classNames from "classnames";
import { OneOnOneLayout as OneOnOneLayoutModel } from "../state/CallViewModel"; import { type OneOnOneLayout as OneOnOneLayoutModel } from "../state/CallViewModel";
import { CallLayout, arrangeTiles } from "./CallLayout"; import { type CallLayout, arrangeTiles } from "./CallLayout";
import styles from "./OneOnOneLayout.module.css"; import styles from "./OneOnOneLayout.module.css";
import { DragCallback, useUpdateLayout } from "./Grid"; import { type DragCallback, useUpdateLayout } from "./Grid";
/** /**
* An implementation of the "one-on-one" layout, in which the remote participant * An implementation of the "one-on-one" layout, in which the remote participant
* is shown at maximum size, overlaid by a small view of the local participant. * is shown at maximum size, overlaid by a small view of the local participant.
*/ */
export const makeOneOnOneLayout: CallLayout<OneOnOneLayoutModel> = ({ export const makeOneOnOneLayout: CallLayout<OneOnOneLayoutModel> = ({
minBounds, minBounds$,
pipAlignment, pipAlignment$,
}) => ({ }) => ({
scrollingOnTop: false, scrollingOnTop: false,
@@ -31,8 +31,8 @@ export const makeOneOnOneLayout: CallLayout<OneOnOneLayoutModel> = ({
scrolling: forwardRef(function OneOnOneLayoutScrolling({ model, Slot }, ref) { scrolling: forwardRef(function OneOnOneLayoutScrolling({ model, Slot }, ref) {
useUpdateLayout(); useUpdateLayout();
const { width, height } = useObservableEagerState(minBounds); const { width, height } = useObservableEagerState(minBounds$);
const pipAlignmentValue = useObservableEagerState(pipAlignment); const pipAlignmentValue = useObservableEagerState(pipAlignment$);
const { tileWidth, tileHeight } = useMemo( const { tileWidth, tileHeight } = useMemo(
() => arrangeTiles(width, height, 1), () => arrangeTiles(width, height, 1),
[width, height], [width, height],
@@ -40,7 +40,7 @@ export const makeOneOnOneLayout: CallLayout<OneOnOneLayoutModel> = ({
const onDragLocalTile: DragCallback = useCallback( const onDragLocalTile: DragCallback = useCallback(
({ xRatio, yRatio }) => ({ xRatio, yRatio }) =>
pipAlignment.next({ pipAlignment$.next({
block: yRatio < 0.5 ? "start" : "end", block: yRatio < 0.5 ? "start" : "end",
inline: xRatio < 0.5 ? "start" : "end", inline: xRatio < 0.5 ? "start" : "end",
}), }),
@@ -52,7 +52,6 @@ export const makeOneOnOneLayout: CallLayout<OneOnOneLayoutModel> = ({
<Slot <Slot
id={model.remote.id} id={model.remote.id}
model={model.remote} model={model.remote}
onVisibilityChange={model.remote.setVisible}
className={styles.container} className={styles.container}
style={{ width: tileWidth, height: tileHeight }} style={{ width: tileWidth, height: tileHeight }}
> >
@@ -61,7 +60,6 @@ export const makeOneOnOneLayout: CallLayout<OneOnOneLayoutModel> = ({
id={model.local.id} id={model.local.id}
model={model.local} model={model.local}
onDrag={onDragLocalTile} onDrag={onDragLocalTile}
onVisibilityChange={model.local.setVisible}
data-block-alignment={pipAlignmentValue.block} data-block-alignment={pipAlignmentValue.block}
data-inline-alignment={pipAlignmentValue.inline} data-inline-alignment={pipAlignmentValue.inline}
/> />

View File

@@ -8,9 +8,9 @@ Please see LICENSE in the repository root for full details.
import { forwardRef, useCallback } from "react"; import { forwardRef, useCallback } from "react";
import { useObservableEagerState } from "observable-hooks"; import { useObservableEagerState } from "observable-hooks";
import { SpotlightExpandedLayout as SpotlightExpandedLayoutModel } from "../state/CallViewModel"; import { type SpotlightExpandedLayout as SpotlightExpandedLayoutModel } from "../state/CallViewModel";
import { CallLayout } from "./CallLayout"; import { type CallLayout } from "./CallLayout";
import { DragCallback, useUpdateLayout } from "./Grid"; import { type DragCallback, useUpdateLayout } from "./Grid";
import styles from "./SpotlightExpandedLayout.module.css"; import styles from "./SpotlightExpandedLayout.module.css";
/** /**
@@ -19,7 +19,7 @@ import styles from "./SpotlightExpandedLayout.module.css";
*/ */
export const makeSpotlightExpandedLayout: CallLayout< export const makeSpotlightExpandedLayout: CallLayout<
SpotlightExpandedLayoutModel SpotlightExpandedLayoutModel
> = ({ pipAlignment }) => ({ > = ({ pipAlignment$ }) => ({
scrollingOnTop: true, scrollingOnTop: true,
fixed: forwardRef(function SpotlightExpandedLayoutFixed( fixed: forwardRef(function SpotlightExpandedLayoutFixed(
@@ -44,11 +44,11 @@ export const makeSpotlightExpandedLayout: CallLayout<
ref, ref,
) { ) {
useUpdateLayout(); useUpdateLayout();
const pipAlignmentValue = useObservableEagerState(pipAlignment); const pipAlignmentValue = useObservableEagerState(pipAlignment$);
const onDragPip: DragCallback = useCallback( const onDragPip: DragCallback = useCallback(
({ xRatio, yRatio }) => ({ xRatio, yRatio }) =>
pipAlignment.next({ pipAlignment$.next({
block: yRatio < 0.5 ? "start" : "end", block: yRatio < 0.5 ? "start" : "end",
inline: xRatio < 0.5 ? "start" : "end", inline: xRatio < 0.5 ? "start" : "end",
}), }),
@@ -63,7 +63,6 @@ export const makeSpotlightExpandedLayout: CallLayout<
id={model.pip.id} id={model.pip.id}
model={model.pip} model={model.pip}
onDrag={onDragPip} onDrag={onDragPip}
onVisibilityChange={model.pip.setVisible}
data-block-alignment={pipAlignmentValue.block} data-block-alignment={pipAlignmentValue.block}
data-inline-alignment={pipAlignmentValue.inline} data-inline-alignment={pipAlignmentValue.inline}
/> />

View File

@@ -9,10 +9,10 @@ import { forwardRef } from "react";
import { useObservableEagerState } from "observable-hooks"; import { useObservableEagerState } from "observable-hooks";
import classNames from "classnames"; import classNames from "classnames";
import { CallLayout } from "./CallLayout"; import { type CallLayout } from "./CallLayout";
import { SpotlightLandscapeLayout as SpotlightLandscapeLayoutModel } from "../state/CallViewModel"; import { type SpotlightLandscapeLayout as SpotlightLandscapeLayoutModel } from "../state/CallViewModel";
import styles from "./SpotlightLandscapeLayout.module.css"; import styles from "./SpotlightLandscapeLayout.module.css";
import { useUpdateLayout } from "./Grid"; import { useUpdateLayout, useVisibleTiles } from "./Grid";
/** /**
* An implementation of the "spotlight landscape" layout, in which the spotlight * An implementation of the "spotlight landscape" layout, in which the spotlight
@@ -21,7 +21,7 @@ import { useUpdateLayout } from "./Grid";
*/ */
export const makeSpotlightLandscapeLayout: CallLayout< export const makeSpotlightLandscapeLayout: CallLayout<
SpotlightLandscapeLayoutModel SpotlightLandscapeLayoutModel
> = ({ minBounds }) => ({ > = ({ minBounds$ }) => ({
scrollingOnTop: false, scrollingOnTop: false,
fixed: forwardRef(function SpotlightLandscapeLayoutFixed( fixed: forwardRef(function SpotlightLandscapeLayoutFixed(
@@ -29,7 +29,7 @@ export const makeSpotlightLandscapeLayout: CallLayout<
ref, ref,
) { ) {
useUpdateLayout(); useUpdateLayout();
useObservableEagerState(minBounds); useObservableEagerState(minBounds$);
return ( return (
<div ref={ref} className={styles.layer}> <div ref={ref} className={styles.layer}>
@@ -50,9 +50,10 @@ export const makeSpotlightLandscapeLayout: CallLayout<
ref, ref,
) { ) {
useUpdateLayout(); useUpdateLayout();
useObservableEagerState(minBounds); useVisibleTiles(model.setVisibleTiles);
useObservableEagerState(minBounds$);
const withIndicators = const withIndicators =
useObservableEagerState(model.spotlight.media).length > 1; useObservableEagerState(model.spotlight.media$).length > 1;
return ( return (
<div ref={ref} className={styles.layer}> <div ref={ref} className={styles.layer}>
@@ -63,13 +64,7 @@ export const makeSpotlightLandscapeLayout: CallLayout<
/> />
<div className={styles.grid}> <div className={styles.grid}>
{model.grid.map((m) => ( {model.grid.map((m) => (
<Slot <Slot key={m.id} className={styles.slot} id={m.id} model={m} />
key={m.id}
className={styles.slot}
id={m.id}
model={m}
onVisibilityChange={m.setVisible}
/>
))} ))}
</div> </div>
</div> </div>

View File

@@ -5,14 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details. Please see LICENSE in the repository root for full details.
*/ */
import { CSSProperties, forwardRef } from "react"; import { type CSSProperties, forwardRef } from "react";
import { useObservableEagerState } from "observable-hooks"; import { useObservableEagerState } from "observable-hooks";
import classNames from "classnames"; import classNames from "classnames";
import { CallLayout, arrangeTiles } from "./CallLayout"; import { type CallLayout, arrangeTiles } from "./CallLayout";
import { SpotlightPortraitLayout as SpotlightPortraitLayoutModel } from "../state/CallViewModel"; import { type SpotlightPortraitLayout as SpotlightPortraitLayoutModel } from "../state/CallViewModel";
import styles from "./SpotlightPortraitLayout.module.css"; import styles from "./SpotlightPortraitLayout.module.css";
import { useUpdateLayout } from "./Grid"; import { useUpdateLayout, useVisibleTiles } from "./Grid";
interface GridCSSProperties extends CSSProperties { interface GridCSSProperties extends CSSProperties {
"--grid-gap": string; "--grid-gap": string;
@@ -27,7 +27,7 @@ interface GridCSSProperties extends CSSProperties {
*/ */
export const makeSpotlightPortraitLayout: CallLayout< export const makeSpotlightPortraitLayout: CallLayout<
SpotlightPortraitLayoutModel SpotlightPortraitLayoutModel
> = ({ minBounds }) => ({ > = ({ minBounds$ }) => ({
scrollingOnTop: false, scrollingOnTop: false,
fixed: forwardRef(function SpotlightPortraitLayoutFixed( fixed: forwardRef(function SpotlightPortraitLayoutFixed(
@@ -54,7 +54,8 @@ export const makeSpotlightPortraitLayout: CallLayout<
ref, ref,
) { ) {
useUpdateLayout(); useUpdateLayout();
const { width } = useObservableEagerState(minBounds); useVisibleTiles(model.setVisibleTiles);
const { width } = useObservableEagerState(minBounds$);
const { gap, tileWidth, tileHeight } = arrangeTiles( const { gap, tileWidth, tileHeight } = arrangeTiles(
width, width,
// TODO: We pretend that the minimum height is the width, because the // TODO: We pretend that the minimum height is the width, because the
@@ -63,7 +64,7 @@ export const makeSpotlightPortraitLayout: CallLayout<
model.grid.length, model.grid.length,
); );
const withIndicators = const withIndicators =
useObservableEagerState(model.spotlight.media).length > 1; useObservableEagerState(model.spotlight.media$).length > 1;
return ( return (
<div <div
@@ -84,13 +85,7 @@ export const makeSpotlightPortraitLayout: CallLayout<
/> />
<div className={styles.grid}> <div className={styles.grid}>
{model.grid.map((m) => ( {model.grid.map((m) => (
<Slot <Slot key={m.id} className={styles.slot} id={m.id} model={m} />
key={m.id}
className={styles.slot}
id={m.id}
model={m}
onVisibilityChange={m.setVisible}
/>
))} ))}
</div> </div>
</div> </div>

View File

@@ -5,12 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details. Please see LICENSE in the repository root for full details.
*/ */
import { ComponentType, memo, RefObject, useRef } from "react"; import { type ComponentType, memo, type RefObject, useRef } from "react";
import { EventTypes, Handler, useDrag } from "@use-gesture/react"; import { type EventTypes, type Handler, useDrag } from "@use-gesture/react";
import { SpringValue } from "@react-spring/web"; import { type SpringValue } from "@react-spring/web";
import classNames from "classnames"; import classNames from "classnames";
import { TileProps } from "./Grid"; import { type TileProps } from "./Grid";
import styles from "./TileWrapper.module.css"; import styles from "./TileWrapper.module.css";
interface Props<M, R extends HTMLElement> { interface Props<M, R extends HTMLElement> {

View File

@@ -5,13 +5,13 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details. Please see LICENSE in the repository root for full details.
*/ */
import { render, RenderResult } from "@testing-library/react"; import { render, type RenderResult } from "@testing-library/react";
import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { type MatrixClient } from "matrix-js-sdk/src/matrix";
import { MemoryRouter } from "react-router-dom"; import { MemoryRouter } from "react-router-dom";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { CallList } from "../../src/home/CallList"; import { CallList } from "../../src/home/CallList";
import { GroupCallRoom } from "../../src/home/useGroupCallRooms"; import { type GroupCallRoom } from "../../src/home/useGroupCallRooms";
describe("CallList", () => { describe("CallList", () => {
const renderComponent = (rooms: GroupCallRoom[]): RenderResult => { const renderComponent = (rooms: GroupCallRoom[]): RenderResult => {

View File

@@ -6,10 +6,10 @@ Please see LICENSE in the repository root for full details.
*/ */
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { type MatrixClient } from "matrix-js-sdk/src/client";
import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { type RoomMember } from "matrix-js-sdk/src/models/room-member";
import { Room } from "matrix-js-sdk/src/models/room"; import { type Room } from "matrix-js-sdk/src/models/room";
import { FC, useCallback, MouseEvent, useState } from "react"; import { type FC, useCallback, type MouseEvent, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { IconButton, Text } from "@vector-im/compound-web"; import { IconButton, Text } from "@vector-im/compound-web";
import { CloseIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { CloseIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
@@ -18,7 +18,7 @@ import classNames from "classnames";
import { Avatar, Size } from "../Avatar"; import { Avatar, Size } from "../Avatar";
import styles from "./CallList.module.css"; import styles from "./CallList.module.css";
import { getRelativeRoomUrl } from "../utils/matrix"; import { getRelativeRoomUrl } from "../utils/matrix";
import { GroupCallRoom } from "./useGroupCallRooms"; import { type GroupCallRoom } from "./useGroupCallRooms";
import { useRoomEncryptionSystem } from "../e2ee/sharedKeyManagement"; import { useRoomEncryptionSystem } from "../e2ee/sharedKeyManagement";
interface CallListProps { interface CallListProps {

View File

@@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details.
*/ */
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { FC } from "react"; import { type FC } from "react";
import { useClientState } from "../ClientContext"; import { useClientState } from "../ClientContext";
import { ErrorView, LoadingView } from "../FullScreenView"; import { ErrorView, LoadingView } from "../FullScreenView";

View File

@@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details.
*/ */
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { FC, MouseEvent } from "react"; import { type FC, type MouseEvent } from "react";
import { Button } from "@vector-im/compound-web"; import { Button } from "@vector-im/compound-web";
import { Modal } from "../Modal"; import { Modal } from "../Modal";

View File

@@ -5,9 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details. Please see LICENSE in the repository root for full details.
*/ */
import { useState, useCallback, FormEvent, FormEventHandler, FC } from "react"; import {
useState,
useCallback,
type FormEvent,
type FormEventHandler,
type FC,
} from "react";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { type MatrixClient } from "matrix-js-sdk/src/client";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Heading, Text } from "@vector-im/compound-web"; import { Heading, Text } from "@vector-im/compound-web";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details. Please see LICENSE in the repository root for full details.
*/ */
import { FC, useCallback, useState, FormEventHandler } from "react"; import { type FC, useCallback, useState, type FormEventHandler } from "react";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { randomString } from "matrix-js-sdk/src/randomstring"; import { randomString } from "matrix-js-sdk/src/randomstring";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";

View File

@@ -5,12 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details. Please see LICENSE in the repository root for full details.
*/ */
import { MatrixClient } from "matrix-js-sdk/src/client"; import { type MatrixClient } from "matrix-js-sdk/src/client";
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room"; import { type Room, RoomEvent } from "matrix-js-sdk/src/models/room";
import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { type RoomMember } from "matrix-js-sdk/src/models/room-member";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { EventTimeline, EventType, JoinRule } from "matrix-js-sdk/src/matrix"; import { EventTimeline, EventType, JoinRule } from "matrix-js-sdk/src/matrix";
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import { MatrixRTCSessionManagerEvents } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSessionManager"; import { MatrixRTCSessionManagerEvents } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSessionManager";
import { KnownMembership } from "matrix-js-sdk/src/types"; import { KnownMembership } from "matrix-js-sdk/src/types";

View File

@@ -47,6 +47,11 @@ layer(compound);
--background-gradient: url("graphics/backgroundGradient.svg"); --background-gradient: url("graphics/backgroundGradient.svg");
} }
:root,
[class*="cpd-theme-"] {
--video-tile-background: var(--cpd-color-bg-subtle-secondary);
}
.cpd-theme-dark { .cpd-theme-dark {
--cpd-color-border-accent: var(--cpd-color-green-1100); --cpd-color-border-accent: var(--cpd-color-green-1100);
--stopgap-color-on-solid-accent: var(--cpd-color-text-primary); --stopgap-color-on-solid-accent: var(--cpd-color-text-primary);

View File

@@ -24,7 +24,7 @@ import { platform } from "./Platform";
// This generates a map of locale names to their URL (based on import.meta.url), which looks like this: // This generates a map of locale names to their URL (based on import.meta.url), which looks like this:
// { // {
// "../locales/en-GB/app.json": "/whatever/assets/root/locales/en-aabbcc.json", // "../locales/en/app.json": "/whatever/assets/root/locales/en-aabbcc.json",
// ... // ...
// } // }
const locales = import.meta.glob<string>("../locales/*/*.json", { const locales = import.meta.glob<string>("../locales/*/*.json", {
@@ -41,7 +41,7 @@ const getLocaleUrl = (
const supportedLngs = [ const supportedLngs = [
...new Set( ...new Set(
Object.keys(locales).map((url) => { Object.keys(locales).map((url) => {
// The URLs are of the form ../locales/en-GB/app.json // The URLs are of the form ../locales/en/app.json
// This extracts the language code from the URL // This extracts the language code from the URL
const lang = url.match(/\/([^/]+)\/[^/]+\.json$/)?.[1]; const lang = url.match(/\/([^/]+)\/[^/]+\.json$/)?.[1];
if (!lang) { if (!lang) {
@@ -133,7 +133,7 @@ export class Initializer {
.use(languageDetector) .use(languageDetector)
.use(initReactI18next) .use(initReactI18next)
.init({ .init({
fallbackLng: "en-GB", fallbackLng: "en",
defaultNS: "app", defaultNS: "app",
keySeparator: ".", keySeparator: ".",
nsSeparator: false, nsSeparator: false,

View File

@@ -6,13 +6,13 @@ Please see LICENSE in the repository root for full details.
*/ */
import { import {
AllHTMLAttributes, type AllHTMLAttributes,
useEffect, useEffect,
useCallback, useCallback,
useState, useState,
ChangeEvent, type ChangeEvent,
useRef, useRef,
FC, type FC,
} from "react"; } from "react";
import classNames from "classnames"; import classNames from "classnames";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";

View File

@@ -6,11 +6,11 @@ Please see LICENSE in the repository root for full details.
*/ */
import { import {
ChangeEvent, type ChangeEvent,
FC, type FC,
ForwardedRef, type ForwardedRef,
forwardRef, forwardRef,
ReactNode, type ReactNode,
useId, useId,
} from "react"; } from "react";
import classNames from "classnames"; import classNames from "classnames";
@@ -73,6 +73,7 @@ interface InputFieldProps {
defaultValue?: string; defaultValue?: string;
placeholder?: string; placeholder?: string;
defaultChecked?: boolean; defaultChecked?: boolean;
min?: number;
onChange?: (event: ChangeEvent<HTMLInputElement>) => void; onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
} }
@@ -91,6 +92,7 @@ export const InputField = forwardRef<
suffix, suffix,
description, description,
disabled, disabled,
min,
...rest ...rest
}, },
ref, ref,
@@ -127,6 +129,7 @@ export const InputField = forwardRef<
checked={checked} checked={checked}
disabled={disabled} disabled={disabled}
aria-describedby={descriptionId} aria-describedby={descriptionId}
min={min}
{...rest} {...rest}
/> />
)} )}

Some files were not shown because too many files have changed in this diff Show More