diff --git a/.eslintrc.js b/.eslintrc.js index f168a87a066..2b0dd2c186b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -42,6 +42,10 @@ module.exports = { name: "setImmediate", message: "Use setTimeout instead.", }, + { + name: "Buffer", + message: "Buffer is not available in the web.", + }, ], "import/no-duplicates": ["error"], @@ -255,6 +259,9 @@ module.exports = { additionalTestBlockFunctions: ["beforeAll", "beforeEach", "oldBackendOnly"], }, ], + + // These are fine in tests + "no-restricted-globals": "off", }, }, { diff --git a/CHANGELOG.md b/CHANGELOG.md index ebe9f061edd..d5e000f494a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,38 @@ +Changes in [1.11.89](https://github.com/element-hq/element-web/releases/tag/v1.11.89) (2024-12-18) +================================================================================================== +This is a patch release to fix a bug which could prevent loading stored crypto state from storage, and also to fix URL previews when switching back to a room. + +## 🐛 Bug Fixes + +* Upgrade matrix-sdk-crypto-wasm to 1.11.0 (https://github.com/matrix-org/matrix-js-sdk/pull/4593) +* Fix url preview display ([#28766](https://github.com/element-hq/element-web/pull/28766)). + + +Changes in [1.11.88](https://github.com/element-hq/element-web/releases/tag/v1.11.88) (2024-12-17) +================================================================================================== +## ✨ Features + +* Allow trusted Element Call widget to send and receive media encryption key to-device messages ([#28316](https://github.com/element-hq/element-web/pull/28316)). Contributed by @hughns. +* increase ringing timeout from 10 seconds to 90 seconds ([#28630](https://github.com/element-hq/element-web/pull/28630)). Contributed by @fkwp. +* Add `Close` tooltip to dialog ([#28617](https://github.com/element-hq/element-web/pull/28617)). Contributed by @florianduros. +* New UX for Share dialog ([#28598](https://github.com/element-hq/element-web/pull/28598)). Contributed by @florianduros. +* Improve performance of RoomContext in RoomHeader ([#28574](https://github.com/element-hq/element-web/pull/28574)). Contributed by @t3chguy. +* Remove `Features.RustCrypto` flag ([#28582](https://github.com/element-hq/element-web/pull/28582)). Contributed by @florianduros. +* Add Modernizr warning when running in non-secure context ([#28581](https://github.com/element-hq/element-web/pull/28581)). Contributed by @t3chguy. + +## 🐛 Bug Fixes + +* Fix jumpy timeline when the pinned message banner is displayed ([#28654](https://github.com/element-hq/element-web/pull/28654)). Contributed by @florianduros. +* Fix font \& spaces in settings subsection ([#28631](https://github.com/element-hq/element-web/pull/28631)). Contributed by @florianduros. +* Remove manual device verification which is not supported by the new cryptography stack ([#28588](https://github.com/element-hq/element-web/pull/28588)). Contributed by @florianduros. +* Fix code block highlighting not working reliably with many code blocks ([#28613](https://github.com/element-hq/element-web/pull/28613)). Contributed by @t3chguy. +* Remove remaining reply fallbacks code ([#28610](https://github.com/element-hq/element-web/pull/28610)). Contributed by @t3chguy. +* Provide a way to activate GIFs via the keyboard for a11y ([#28611](https://github.com/element-hq/element-web/pull/28611)). Contributed by @t3chguy. +* Fix format bar position ([#28591](https://github.com/element-hq/element-web/pull/28591)). Contributed by @florianduros. +* Fix room taking long time to load ([#28579](https://github.com/element-hq/element-web/pull/28579)). Contributed by @florianduros. +* Show the correct shield status in tooltip for more conditions ([#28476](https://github.com/element-hq/element-web/pull/28476)). Contributed by @uhoreg. + + Changes in [1.11.87](https://github.com/element-hq/element-web/releases/tag/v1.11.87) (2024-12-03) ================================================================================================== ## ✨ Features diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 49741b073c8..fa887929fb6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,26 +20,26 @@ Definitely don't use the GitHub default of "Update file.ts". As for your PR description, it should include these things: -- References to any bugs fixed by the change (in GitHub's `Fixes` notation) -- Describe the why and what is changing in the PR description so it's easy for - onlookers and reviewers to onboard and context switch. This information is - also helpful when we come back to look at this in 6 months and ask "why did - we do it like that?" we have a chance of finding out. - - Why didn't it work before? Why does it work now? What use cases does it - unlock? - - If you find yourself adding information on how the code works or why you - chose to do it the way you did, make sure this information is instead - written as comments in the code itself. - - Sometimes a PR can change considerably as it is developed. In this case, - the description should be updated to reflect the most recent state of - the PR. (It can be helpful to retain the old content under a suitable - heading, for additional context.) -- Include both **before** and **after** screenshots to easily compare and discuss - what's changing. -- Include a step-by-step testing strategy so that a reviewer can check out the - code locally and easily get to the point of testing your change. -- Add comments to the diff for the reviewer that might help them to understand - why the change is necessary or how they might better understand and review it. +- References to any bugs fixed by the change (in GitHub's `Fixes` notation) +- Describe the why and what is changing in the PR description so it's easy for + onlookers and reviewers to onboard and context switch. This information is + also helpful when we come back to look at this in 6 months and ask "why did + we do it like that?" we have a chance of finding out. + - Why didn't it work before? Why does it work now? What use cases does it + unlock? + - If you find yourself adding information on how the code works or why you + chose to do it the way you did, make sure this information is instead + written as comments in the code itself. + - Sometimes a PR can change considerably as it is developed. In this case, + the description should be updated to reflect the most recent state of + the PR. (It can be helpful to retain the old content under a suitable + heading, for additional context.) +- Include both **before** and **after** screenshots to easily compare and discuss + what's changing. +- Include a step-by-step testing strategy so that a reviewer can check out the + code locally and easily get to the point of testing your change. +- Add comments to the diff for the reviewer that might help them to understand + why the change is necessary or how they might better understand and review it. ### Changelogs @@ -79,8 +79,8 @@ element-web notes: Fix a bug where the 'Herd' button only worked on Tuesdays This example is for Element Web. You can specify: -- element-web -- element-desktop +- element-web +- element-desktop If your PR introduces a breaking change, use the `Notes` section in the same way, additionally adding the `X-Breaking-Change` label (see below). There's no need @@ -96,10 +96,10 @@ Notes: Remove legacy `Camelopard` class. `Giraffe` should be used instead. Other metadata can be added using labels. -- `X-Breaking-Change`: A breaking change - adding this label will mean the change causes a _major_ version bump. -- `T-Enhancement`: A new feature - adding this label will mean the change causes a _minor_ version bump. -- `T-Defect`: A bug fix (in either code or docs). -- `T-Task`: No user-facing changes, eg. code comments, CI fixes, refactors or tests. Won't have a changelog entry unless you specify one. +- `X-Breaking-Change`: A breaking change - adding this label will mean the change causes a _major_ version bump. +- `T-Enhancement`: A new feature - adding this label will mean the change causes a _minor_ version bump. +- `T-Defect`: A bug fix (in either code or docs). +- `T-Task`: No user-facing changes, eg. code comments, CI fixes, refactors or tests. Won't have a changelog entry unless you specify one. If you don't have permission to add labels, your PR reviewer(s) can work with you to add them: ask in the PR description or comments. diff --git a/__mocks__/FontManager.js b/__mocks__/FontManager.js deleted file mode 100644 index 41eab4bf94e..00000000000 --- a/__mocks__/FontManager.js +++ /dev/null @@ -1,6 +0,0 @@ -// Stub out FontManager for tests as it doesn't validate anything we don't already know given -// our fixed test environment and it requires the installation of node-canvas. - -module.exports = { - fixupColorFonts: () => Promise.resolve(), -}; diff --git a/code_style.md b/code_style.md index e5f7485cec9..9aa6836442f 100644 --- a/code_style.md +++ b/code_style.md @@ -3,9 +3,9 @@ This code style applies to projects which the element-web team directly maintains or is reasonably adjacent to. As of writing, these are: -- element-desktop -- element-web -- matrix-js-sdk +- element-desktop +- element-web +- matrix-js-sdk Other projects might extend this code style for increased strictness. For example, matrix-events-sdk has stricter code organization to reduce the maintenance burden. These projects will declare their code diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 23229bf00a6..57b017bc1c6 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -1,55 +1,55 @@ # Summary -- [Introduction](../README.md) +- [Introduction](../README.md) # Usage -- [Betas](betas.md) -- [Labs](labs.md) +- [Betas](betas.md) +- [Labs](labs.md) # Setup -- [Install](install.md) -- [Config](config.md) -- [Custom home page](custom-home.md) -- [Kubernetes](kubernetes.md) -- [Jitsi](jitsi.md) -- [Encryption](e2ee.md) +- [Install](install.md) +- [Config](config.md) +- [Custom home page](custom-home.md) +- [Kubernetes](kubernetes.md) +- [Jitsi](jitsi.md) +- [Encryption](e2ee.md) # Build -- [Customisations](customisations.md) -- [Modules](modules.md) -- [Native Node modules](native-node-modules.md) +- [Customisations](customisations.md) +- [Modules](modules.md) +- [Native Node modules](native-node-modules.md) # Contribution -- [Choosing an issue](choosing-an-issue.md) -- [Translation](translating.md) -- [Netlify builds](pr-previews.md) -- [Code review](review.md) +- [Choosing an issue](choosing-an-issue.md) +- [Translation](translating.md) +- [Netlify builds](pr-previews.md) +- [Code review](review.md) # Development -- [App load order](app-load.md) -- [Translation](translating-dev.md) -- [Theming](theming.md) -- [Playwright end to end tests](playwright.md) -- [Memory profiling](memory-profiles-and-leaks.md) -- [Jitsi](jitsi-dev.md) -- [Feature flags](feature-flags.md) -- [OIDC and delegated authentication](oidc.md) -- [Release Process](release.md) +- [App load order](app-load.md) +- [Translation](translating-dev.md) +- [Theming](theming.md) +- [Playwright end to end tests](playwright.md) +- [Memory profiling](memory-profiles-and-leaks.md) +- [Jitsi](jitsi-dev.md) +- [Feature flags](feature-flags.md) +- [OIDC and delegated authentication](oidc.md) +- [Release Process](release.md) # Deep dive -- [Skinning](skinning.md) -- [Cider editor](ciderEditor.md) -- [Iconography](icons.md) -- [Jitsi](jitsi.md) -- [Local echo](local-echo-dev.md) -- [Media](media-handling.md) -- [Room List Store](room-list-store.md) -- [Scrolling](scrolling.md) -- [Usercontent](usercontent.md) -- [Widget layouts](widget-layouts.md) +- [Skinning](skinning.md) +- [Cider editor](ciderEditor.md) +- [Iconography](icons.md) +- [Jitsi](jitsi.md) +- [Local echo](local-echo-dev.md) +- [Media](media-handling.md) +- [Room List Store](room-list-store.md) +- [Scrolling](scrolling.md) +- [Usercontent](usercontent.md) +- [Widget layouts](widget-layouts.md) diff --git a/docs/app-load.md b/docs/app-load.md index 849e95cb8d4..7f72b3fea4d 100644 --- a/docs/app-load.md +++ b/docs/app-load.md @@ -61,18 +61,18 @@ flowchart TD Key: -- Parallelogram: async/await task -- Box: sync task -- Diamond: conditional branch -- Circle: user interaction -- Blue arrow: async task is allowed to settle but allowed to fail -- Red arrow: async task success is asserted +- Parallelogram: async/await task +- Box: sync task +- Diamond: conditional branch +- Circle: user interaction +- Blue arrow: async task is allowed to settle but allowed to fail +- Red arrow: async task success is asserted Notes: -- A task begins when all its dependencies (arrows going into it) are fulfilled. -- The success of setting up rageshake is never asserted, element-web has a fallback path for running without IDB (and thus rageshake). -- Everything is awaited to be settled before the Modernizr check, to allow it to make use of things like i18n if they are successful. +- A task begins when all its dependencies (arrows going into it) are fulfilled. +- The success of setting up rageshake is never asserted, element-web has a fallback path for running without IDB (and thus rageshake). +- Everything is awaited to be settled before the Modernizr check, to allow it to make use of things like i18n if they are successful. Underlying dependencies: diff --git a/docs/choosing-an-issue.md b/docs/choosing-an-issue.md index 9d008782a18..ca179793674 100644 --- a/docs/choosing-an-issue.md +++ b/docs/choosing-an-issue.md @@ -32,19 +32,19 @@ someone to add something. When you're looking through the list, here are some things that might make an issue a **GOOD** choice: -- It is a problem or feature you care about. -- It concerns a type of code you know a little about. -- You think you can understand what's needed. -- It already has approval from Element Web's designers (look for comments from - members of the - [Product](https://github.com/orgs/element-hq/teams/product/members) or - [Design](https://github.com/orgs/element-hq/teams/design/members) teams). +- It is a problem or feature you care about. +- It concerns a type of code you know a little about. +- You think you can understand what's needed. +- It already has approval from Element Web's designers (look for comments from + members of the + [Product](https://github.com/orgs/element-hq/teams/product/members) or + [Design](https://github.com/orgs/element-hq/teams/design/members) teams). Here are some things that might make it a **BAD** choice: -- You don't understand it (maybe add a comment asking a clarifying question). -- It sounds difficult, or is part of a larger change you don't know about. -- **It is tagged with `X-Needs-Design` or `X-Needs-Product`.** +- You don't understand it (maybe add a comment asking a clarifying question). +- It sounds difficult, or is part of a larger change you don't know about. +- **It is tagged with `X-Needs-Design` or `X-Needs-Product`.** **Element Web's Design and Product teams tend to be very busy**, so if you make changes that require approval from one of those teams, you will probably have diff --git a/docs/config.md b/docs/config.md index cc40179740f..8ca4ba4eb8b 100644 --- a/docs/config.md +++ b/docs/config.md @@ -455,7 +455,7 @@ If you would like to use Scalar, the integration manager maintained by Element, For widgets in general (from an integration manager or not) there is also: -- `default_widget_container_height` +- `default_widget_container_height` This controls the height that the top widget panel initially appears as and is the height in pixels, default 280. @@ -551,38 +551,38 @@ preferences. Currently, the following UI feature flags are supported: -- `UIFeature.urlPreviews` - Whether URL previews are enabled across the entire application. -- `UIFeature.feedback` - Whether prompts to supply feedback are shown. -- `UIFeature.voip` - Whether or not VoIP is shown readily to the user. When disabled, - Jitsi widgets will still work though they cannot easily be added. -- `UIFeature.widgets` - Whether or not widgets will be shown. -- `UIFeature.advancedSettings` - Whether or not sections titled "advanced" in room and - user settings are shown to the user. -- `UIFeature.shareQrCode` - Whether or not the QR code on the share room/event dialog - is shown. -- `UIFeature.shareSocial` - Whether or not the social icons on the share room/event dialog - are shown. -- `UIFeature.identityServer` - Whether or not functionality requiring an identity server - is shown. When disabled, the user will not be able to interact with the identity - server (sharing email addresses, 3PID invites, etc). -- `UIFeature.thirdPartyId` - Whether or not UI relating to third party identifiers (3PIDs) - is shown. Typically this is considered "contact information" on the homeserver, and is - not directly related to the identity server. -- `UIFeature.registration` - Whether or not the registration page is accessible. Typically - useful if accounts are managed externally. -- `UIFeature.passwordReset` - Whether or not the password reset page is accessible. Typically - useful if accounts are managed externally. -- `UIFeature.deactivate` - Whether or not the deactivate account button is accessible. Typically - useful if accounts are managed externally. -- `UIFeature.advancedEncryption` - Whether or not advanced encryption options are shown to the - user. -- `UIFeature.roomHistorySettings` - Whether or not the room history settings are shown to the user. - This should only be used if the room history visibility options are managed by the server. -- `UIFeature.TimelineEnableRelativeDates` - Display relative date separators (eg: 'Today', 'Yesterday') in the - timeline for recent messages. When false day dates will be used. -- `UIFeature.BulkUnverifiedSessionsReminder` - Display popup reminders to verify or remove unverified sessions. Defaults - to true. -- `UIFeature.locationSharing` - Whether or not location sharing menus will be shown. +- `UIFeature.urlPreviews` - Whether URL previews are enabled across the entire application. +- `UIFeature.feedback` - Whether prompts to supply feedback are shown. +- `UIFeature.voip` - Whether or not VoIP is shown readily to the user. When disabled, + Jitsi widgets will still work though they cannot easily be added. +- `UIFeature.widgets` - Whether or not widgets will be shown. +- `UIFeature.advancedSettings` - Whether or not sections titled "advanced" in room and + user settings are shown to the user. +- `UIFeature.shareQrCode` - Whether or not the QR code on the share room/event dialog + is shown. +- `UIFeature.shareSocial` - Whether or not the social icons on the share room/event dialog + are shown. +- `UIFeature.identityServer` - Whether or not functionality requiring an identity server + is shown. When disabled, the user will not be able to interact with the identity + server (sharing email addresses, 3PID invites, etc). +- `UIFeature.thirdPartyId` - Whether or not UI relating to third party identifiers (3PIDs) + is shown. Typically this is considered "contact information" on the homeserver, and is + not directly related to the identity server. +- `UIFeature.registration` - Whether or not the registration page is accessible. Typically + useful if accounts are managed externally. +- `UIFeature.passwordReset` - Whether or not the password reset page is accessible. Typically + useful if accounts are managed externally. +- `UIFeature.deactivate` - Whether or not the deactivate account button is accessible. Typically + useful if accounts are managed externally. +- `UIFeature.advancedEncryption` - Whether or not advanced encryption options are shown to the + user. +- `UIFeature.roomHistorySettings` - Whether or not the room history settings are shown to the user. + This should only be used if the room history visibility options are managed by the server. +- `UIFeature.TimelineEnableRelativeDates` - Display relative date separators (eg: 'Today', 'Yesterday') in the + timeline for recent messages. When false day dates will be used. +- `UIFeature.BulkUnverifiedSessionsReminder` - Display popup reminders to verify or remove unverified sessions. Defaults + to true. +- `UIFeature.locationSharing` - Whether or not location sharing menus will be shown. ## Undocumented / developer options @@ -592,4 +592,3 @@ The following are undocumented or intended for developer use only. 2. `sync_timeline_limit` 3. `dangerously_allow_unsafe_and_insecure_passwords` 4. `latex_maths_delims`: An optional setting to override the default delimiters used for maths parsing. See https://github.com/matrix-org/matrix-react-sdk/pull/5939 for details. Only used when `feature_latex_maths` is enabled. -5. `voice_broadcast.chunk_length`: Target chunk length in seconds for the Voice Broadcast feature currently under development. diff --git a/docs/customisations.md b/docs/customisations.md index a6f72ab1abc..42cb8c7c5c8 100644 --- a/docs/customisations.md +++ b/docs/customisations.md @@ -50,9 +50,9 @@ that properties/state machines won't change. UI for some actions can be hidden via the ComponentVisibility customisation: -- inviting users to rooms and spaces, -- creating rooms, -- creating spaces, +- inviting users to rooms and spaces, +- creating rooms, +- creating spaces, To customise visibility create a customisation module from [ComponentVisibility](https://github.com/element-hq/element-web/blob/master/src/customisations/ComponentVisibility.ts) following the instructions above. diff --git a/docs/e2ee.md b/docs/e2ee.md index 1229f55f38b..835c38a1d59 100644 --- a/docs/e2ee.md +++ b/docs/e2ee.md @@ -31,9 +31,9 @@ Set the following on your homeserver's When `force_disable` is true: -- all rooms will be created with encryption disabled, and it will not be possible to enable - encryption from room settings. -- any `io.element.e2ee.default` value will be disregarded. +- all rooms will be created with encryption disabled, and it will not be possible to enable + encryption from room settings. +- any `io.element.e2ee.default` value will be disregarded. Note: If the server is configured to forcibly enable encryption for some or all rooms, this behaviour will be overridden. diff --git a/docs/feature-flags.md b/docs/feature-flags.md index 46e5f1243e2..54d54e3b1bf 100644 --- a/docs/feature-flags.md +++ b/docs/feature-flags.md @@ -5,10 +5,10 @@ flexibility and control over when and where those features are enabled. For example, flags make the following things possible: -- Extended testing of a feature via labs on develop -- Enabling features when ready instead of the first moment the code is released -- Testing a feature with a specific set of users (by enabling only on a specific - Element instance) +- Extended testing of a feature via labs on develop +- Enabling features when ready instead of the first moment the code is released +- Testing a feature with a specific set of users (by enabling only on a specific + Element instance) The size of the feature controlled by a feature flag may vary widely: it could be a large project like reactions or a smaller change to an existing algorithm. diff --git a/docs/features/composer.md b/docs/features/composer.md index 408c78a8d9c..1af4c9c8940 100644 --- a/docs/features/composer.md +++ b/docs/features/composer.md @@ -2,37 +2,37 @@ ## Auto Complete -- Hitting tab tries to auto-complete the word before the caret as a room member - - If no matching name is found, a visual bell is shown -- @ + a letter opens auto complete for members starting with the given letter - - When inserting a user pill at the start in the composer, a colon and space is appended to the pill - - When inserting a user pill anywhere else in composer, only a space is appended to the pill -- # + a letter opens auto complete for rooms starting with the given letter -- : open auto complete for emoji -- Pressing arrow-up/arrow-down while the autocomplete is open navigates between auto complete options -- Pressing tab while the autocomplete is open goes to the next autocomplete option, - wrapping around at the end after reverting to the typed text first. +- Hitting tab tries to auto-complete the word before the caret as a room member + - If no matching name is found, a visual bell is shown +- @ + a letter opens auto complete for members starting with the given letter + - When inserting a user pill at the start in the composer, a colon and space is appended to the pill + - When inserting a user pill anywhere else in composer, only a space is appended to the pill +- # + a letter opens auto complete for rooms starting with the given letter +- : open auto complete for emoji +- Pressing arrow-up/arrow-down while the autocomplete is open navigates between auto complete options +- Pressing tab while the autocomplete is open goes to the next autocomplete option, + wrapping around at the end after reverting to the typed text first. ## Formatting -- When selecting text, a formatting bar appears above the selection. -- The formatting bar allows to format the selected test as: - bold, italic, strikethrough, a block quote, and a code block (inline if no linebreak is selected). -- Formatting is applied as markdown syntax. -- Hitting ctrl/cmd+B also marks the selected text as bold -- Hitting ctrl/cmd+I also marks the selected text as italic -- Hitting ctrl/cmd+> also marks the selected text as a blockquote +- When selecting text, a formatting bar appears above the selection. +- The formatting bar allows to format the selected test as: + bold, italic, strikethrough, a block quote, and a code block (inline if no linebreak is selected). +- Formatting is applied as markdown syntax. +- Hitting ctrl/cmd+B also marks the selected text as bold +- Hitting ctrl/cmd+I also marks the selected text as italic +- Hitting ctrl/cmd+> also marks the selected text as a blockquote ## Misc -- When hitting the arrow-up button while having the caret at the start in the composer, - the last message sent by the syncing user is edited. -- Clicking a display name on an event in the timeline inserts a user pill into the composer -- Emoticons (like :-), >:-), :-/, ...) are replaced by emojis while typing if the relevant setting is enabled -- Typing in the composer sends typing notifications in the room -- Pressing ctrl/mod+z and ctrl/mod+y undoes/redoes modifications -- Pressing shift+enter inserts a line break -- Pressing enter sends the message. -- Choosing "Quote" in the context menu of an event inserts a quote of the event body in the composer. -- Choosing "Reply" in the context menu of an event shows a preview above the composer to reply to. -- Pressing alt+arrow up/arrow down navigates in previously sent messages, putting them in the composer. +- When hitting the arrow-up button while having the caret at the start in the composer, + the last message sent by the syncing user is edited. +- Clicking a display name on an event in the timeline inserts a user pill into the composer +- Emoticons (like :-), >:-), :-/, ...) are replaced by emojis while typing if the relevant setting is enabled +- Typing in the composer sends typing notifications in the room +- Pressing ctrl/mod+z and ctrl/mod+y undoes/redoes modifications +- Pressing shift+enter inserts a line break +- Pressing enter sends the message. +- Choosing "Quote" in the context menu of an event inserts a quote of the event body in the composer. +- Choosing "Reply" in the context menu of an event shows a preview above the composer to reply to. +- Pressing alt+arrow up/arrow down navigates in previously sent messages, putting them in the composer. diff --git a/docs/icons.md b/docs/icons.md index b0582356ce9..449663e24a4 100644 --- a/docs/icons.md +++ b/docs/icons.md @@ -8,9 +8,9 @@ Icons have `role="presentation"` and `aria-hidden` automatically applied. These SVG file recommendations: -- Colours should not be defined absolutely. Use `currentColor` instead. -- SVG files should be taken from the design compound as they are. Some icons contain special padding. - This means that there should be icons for each size, e.g. warning-16px and warning-32px. +- Colours should not be defined absolutely. Use `currentColor` instead. +- SVG files should be taken from the design compound as they are. Some icons contain special padding. + This means that there should be icons for each size, e.g. warning-16px and warning-32px. Example usage: diff --git a/docs/jitsi.md b/docs/jitsi.md index 48d1a7bf3eb..20e64db3798 100644 --- a/docs/jitsi.md +++ b/docs/jitsi.md @@ -81,27 +81,27 @@ which takes several parameters: _Query string_: -- `widgetId`: The ID of the widget. This is needed for communication back to the - react-sdk. -- `parentUrl`: The URL of the parent window. This is also needed for - communication back to the react-sdk. +- `widgetId`: The ID of the widget. This is needed for communication back to the + react-sdk. +- `parentUrl`: The URL of the parent window. This is also needed for + communication back to the react-sdk. _Hash/fragment (formatted as a query string)_: -- `conferenceDomain`: The domain to connect Jitsi Meet to. -- `conferenceId`: The room or conference ID to connect Jitsi Meet to. -- `isAudioOnly`: Boolean for whether this is a voice-only conference. May not - be present, should default to `false`. -- `startWithAudioMuted`: Boolean for whether the calls start with audio - muted. May not be present. -- `startWithVideoMuted`: Boolean for whether the calls start with video - muted. May not be present. -- `displayName`: The display name of the user viewing the widget. May not - be present or could be null. -- `avatarUrl`: The HTTP(S) URL for the avatar of the user viewing the widget. May - not be present or could be null. -- `userId`: The MXID of the user viewing the widget. May not be present or could - be null. +- `conferenceDomain`: The domain to connect Jitsi Meet to. +- `conferenceId`: The room or conference ID to connect Jitsi Meet to. +- `isAudioOnly`: Boolean for whether this is a voice-only conference. May not + be present, should default to `false`. +- `startWithAudioMuted`: Boolean for whether the calls start with audio + muted. May not be present. +- `startWithVideoMuted`: Boolean for whether the calls start with video + muted. May not be present. +- `displayName`: The display name of the user viewing the widget. May not + be present or could be null. +- `avatarUrl`: The HTTP(S) URL for the avatar of the user viewing the widget. May + not be present or could be null. +- `userId`: The MXID of the user viewing the widget. May not be present or could + be null. The react-sdk will assume that `jitsi.html` is at the path of wherever it is currently being served. For example, `https://develop.element.io/jitsi.html` or `vector://webapp/jitsi.html`. diff --git a/docs/playwright.md b/docs/playwright.md index 7eae8e783dc..4af3194220a 100644 --- a/docs/playwright.md +++ b/docs/playwright.md @@ -2,10 +2,10 @@ ## Contents -- How to run the tests -- How the tests work -- How to write great Playwright tests -- Visual testing +- How to run the tests +- How the tests work +- How to write great Playwright tests +- Visual testing ## Running the Tests @@ -123,15 +123,15 @@ When a Synapse instance is started, it's given a config generated from one of th templates in `playwright/plugins/homeserver/synapse/templates`. There are a couple of special files in these templates: -- `homeserver.yaml`: - Template substitution happens in this file. Template variables are: - - `REGISTRATION_SECRET`: The secret used to register users via the REST API. - - `MACAROON_SECRET_KEY`: Generated each time for security - - `FORM_SECRET`: Generated each time for security - - `PUBLIC_BASEURL`: The localhost url + port combination the synapse is accessible at -- `localhost.signing.key`: A signing key is auto-generated and saved to this file. - Config templates should not contain a signing key and instead assume that one will exist - in this file. +- `homeserver.yaml`: + Template substitution happens in this file. Template variables are: + - `REGISTRATION_SECRET`: The secret used to register users via the REST API. + - `MACAROON_SECRET_KEY`: Generated each time for security + - `FORM_SECRET`: Generated each time for security + - `PUBLIC_BASEURL`: The localhost url + port combination the synapse is accessible at +- `localhost.signing.key`: A signing key is auto-generated and saved to this file. + Config templates should not contain a signing key and instead assume that one will exist + in this file. All other files in the template are copied recursively to `/data/`, so the file `foo.html` in a template can be referenced in the config as `/data/foo.html`. @@ -217,3 +217,10 @@ instead of the native `toHaveScreenshot`. If you are running Linux and are unfortunate that the screenshots are not rendering identically, you may wish to specify `--ignore-snapshots` and rely on Docker to render them for you. + +## Test Tags + +We use test tags to categorise tests for running subsets more efficiently. + +- `@mergequeue`: Tests that are slow or flaky and cover areas of the app we update seldom, should not be run on every PR commit but will be run in the Merge Queue. +- `@screenshot`: Tests that use `toMatchScreenshot` to speed up a run of `test:playwright:screenshots`. A test with this tag must not also have the `@mergequeue` tag as this would cause false positives in the stale screenshot detection. diff --git a/docs/release.md b/docs/release.md index 50740393747..b2c797b66b5 100644 --- a/docs/release.md +++ b/docs/release.md @@ -82,28 +82,28 @@ This label will automagically convert to `X-Release-Blocker` at the conclusion o This release process revolves around our main repositories: -- [Element Desktop](https://github.com/element-hq/element-desktop/) -- [Element Web](https://github.com/element-hq/element-web/) -- [Matrix JS SDK](https://github.com/matrix-org/matrix-js-sdk/) +- [Element Desktop](https://github.com/element-hq/element-desktop/) +- [Element Web](https://github.com/element-hq/element-web/) +- [Matrix JS SDK](https://github.com/matrix-org/matrix-js-sdk/) We own other repositories, but they have more ad-hoc releases and are not part of the bi-weekly cycle: -- https://github.com/matrix-org/matrix-web-i18n/ -- https://github.com/matrix-org/matrix-react-sdk-module-api +- https://github.com/matrix-org/matrix-web-i18n/ +- https://github.com/matrix-org/matrix-react-sdk-module-api

Prerequisites

-- You must be part of the 2 Releasers GitHub groups: - - - - -- You will need access to the **VPN** ([docs](https://gitlab.matrix.org/new-vector/internal/-/wikis/SRE/Tailscale)) to be able to follow the instructions under Deploy below. -- You will need the ability to **SSH** in to the production machines to be able to follow the instructions under Deploy below. Ensure that your SSH key has a non-empty passphrase, and you registered your SSH key with Ops. Log a ticket at https://github.com/matrix-org/matrix-ansible-private and ask for: - - Two-factor authentication to be set up on your SSH key. (This is needed to get access to production). - - SSH access to `horme` (staging.element.io and app.element.io) - - Permission to sudo on horme as the user `element` -- You need "**jumphost**" configuration in your local `~/.ssh/config`. This should have been set up as part of your onboarding. +- You must be part of the 2 Releasers GitHub groups: + - + - +- You will need access to the **VPN** ([docs](https://gitlab.matrix.org/new-vector/internal/-/wikis/SRE/Tailscale)) to be able to follow the instructions under Deploy below. +- You will need the ability to **SSH** in to the production machines to be able to follow the instructions under Deploy below. Ensure that your SSH key has a non-empty passphrase, and you registered your SSH key with Ops. Log a ticket at https://github.com/matrix-org/matrix-ansible-private and ask for: + - Two-factor authentication to be set up on your SSH key. (This is needed to get access to production). + - SSH access to `horme` (staging.element.io and app.element.io) + - Permission to sudo on horme as the user `element` +- You need "**jumphost**" configuration in your local `~/.ssh/config`. This should have been set up as part of your onboarding.
@@ -177,7 +177,7 @@ For security, you may wish to merge the security advisory private fork or apply It is worth noting that at the end of the Final/Hotfix/Security release `staging` is merged to `master` which is merged back into `develop` - this means that any commit which goes to `staging` will eventually make its way back to the default branch. -- [ ] The staging branch is prepared +- [ ] The staging branch is prepared # Releasing @@ -192,21 +192,21 @@ switched back to the version of the dependency from the master branch to not lea ### Matrix JS SDK -- [ ] Check the draft release which has been generated by [the automation](https://github.com/matrix-org/matrix-js-sdk/actions/workflows/release-drafter.yml) -- [ ] Make any changes to the release notes in the draft release as are necessary - **Do not click publish, only save draft** -- [ ] Kick off a release using [the automation](https://github.com/matrix-org/matrix-js-sdk/actions/workflows/release.yml) - making sure to select the right type of release. For anything other than an RC: choose final. You should not need to ever switch off either of the Publishing options. +- [ ] Check the draft release which has been generated by [the automation](https://github.com/matrix-org/matrix-js-sdk/actions/workflows/release-drafter.yml) +- [ ] Make any changes to the release notes in the draft release as are necessary - **Do not click publish, only save draft** +- [ ] Kick off a release using [the automation](https://github.com/matrix-org/matrix-js-sdk/actions/workflows/release.yml) - making sure to select the right type of release. For anything other than an RC: choose final. You should not need to ever switch off either of the Publishing options. ### Element Web -- [ ] Check the draft release which has been generated by [the automation](https://github.com/element-hq/element-web/actions/workflows/release-drafter.yml) -- [ ] Make any changes to the release notes in the draft release as are necessary - **Do not click publish, only save draft** -- [ ] Kick off a release using [the automation](https://github.com/element-hq/element-web/actions/workflows/release.yml) - making sure to select the right type of release. For anything other than an RC: choose final. You should not need to ever switch off either of the Publishing options. +- [ ] Check the draft release which has been generated by [the automation](https://github.com/element-hq/element-web/actions/workflows/release-drafter.yml) +- [ ] Make any changes to the release notes in the draft release as are necessary - **Do not click publish, only save draft** +- [ ] Kick off a release using [the automation](https://github.com/element-hq/element-web/actions/workflows/release.yml) - making sure to select the right type of release. For anything other than an RC: choose final. You should not need to ever switch off either of the Publishing options. ### Element Desktop -- [ ] Check the draft release which has been generated by [the automation](https://github.com/element-hq/element-desktop/actions/workflows/release-drafter.yml) -- [ ] Make any changes to the release notes in the draft release as are necessary - **Do not click publish, only save draft** -- [ ] Kick off a release using [the automation](https://github.com/element-hq/element-desktop/actions/workflows/release.yml) - making sure to select the right type of release. For anything other than an RC: choose final. You should not need to ever switch off either of the Publishing options. +- [ ] Check the draft release which has been generated by [the automation](https://github.com/element-hq/element-desktop/actions/workflows/release-drafter.yml) +- [ ] Make any changes to the release notes in the draft release as are necessary - **Do not click publish, only save draft** +- [ ] Kick off a release using [the automation](https://github.com/element-hq/element-desktop/actions/workflows/release.yml) - making sure to select the right type of release. For anything other than an RC: choose final. You should not need to ever switch off either of the Publishing options. # Deploying @@ -214,23 +214,23 @@ We ship the SDKs to npm, this happens as part of the release process. We ship Element Web to dockerhub, `*.element.io`, and packages.element.io. We ship Element Desktop to packages.element.io. -- [ ] Check that element-web has shipped to dockerhub -- [ ] Deploy staging.element.io. [See docs.](https://handbook.element.io/books/element-web-team/page/deploying-appstagingelementio) -- [ ] Test staging.element.io +- [ ] Check that element-web has shipped to dockerhub +- [ ] Deploy staging.element.io. [See docs.](https://handbook.element.io/books/element-web-team/page/deploying-appstagingelementio) +- [ ] Test staging.element.io For final releases additionally do these steps: -- [ ] Deploy app.element.io. [See docs.](https://handbook.element.io/books/element-web-team/page/deploying-appstagingelementio) -- [ ] Test app.element.io -- [ ] Ensure Element Web package has shipped to packages.element.io -- [ ] Ensure Element Desktop packages have shipped to packages.element.io +- [ ] Deploy app.element.io. [See docs.](https://handbook.element.io/books/element-web-team/page/deploying-appstagingelementio) +- [ ] Test app.element.io +- [ ] Ensure Element Web package has shipped to packages.element.io +- [ ] Ensure Element Desktop packages have shipped to packages.element.io # Housekeeping We have some manual housekeeping to do in order to prepare for the next release. -- [ ] Update topics using [the automation](https://github.com/element-hq/element-web/actions/workflows/update-topics.yaml). It will autodetect the current latest version. Don't forget the date you supply should be e.g. September 5th (including the "th") for the script to work. -- [ ] Announce the release in [#element-web-announcements:matrix.org](https://matrix.to/#/#element-web-announcements:matrix.org) +- [ ] Update topics using [the automation](https://github.com/element-hq/element-web/actions/workflows/update-topics.yaml). It will autodetect the current latest version. Don't forget the date you supply should be e.g. September 5th (including the "th") for the script to work. +- [ ] Announce the release in [#element-web-announcements:matrix.org](https://matrix.to/#/#element-web-announcements:matrix.org)
(show) @@ -246,15 +246,15 @@ With wording like: For the first RC of a given release cycle do these steps: -- [ ] Go to the [matrix-js-sdk Renovate dashboard](https://github.com/matrix-org/matrix-js-sdk/issues/2406) and click the checkbox to create/update its PRs. +- [ ] Go to the [matrix-js-sdk Renovate dashboard](https://github.com/matrix-org/matrix-js-sdk/issues/2406) and click the checkbox to create/update its PRs. -- [ ] Go to the [element-web Renovate dashboard](https://github.com/element-hq/element-web/issues/22941) and click the checkbox to create/update its PRs. +- [ ] Go to the [element-web Renovate dashboard](https://github.com/element-hq/element-web/issues/22941) and click the checkbox to create/update its PRs. -- [ ] Go to the [element-desktop Renovate dashboard](https://github.com/element-hq/element-desktop/issues/465) and click the checkbox to create/update its PRs. +- [ ] Go to the [element-desktop Renovate dashboard](https://github.com/element-hq/element-desktop/issues/465) and click the checkbox to create/update its PRs. -- [ ] Later, check back and merge the PRs that succeeded to build. The ones that failed will get picked up by the [maintainer](https://docs.google.com/document/d/1V5VINWXATMpz9UBw4IKmVVB8aw3CxM0Jt7igtHnDfSk/edit#). +- [ ] Later, check back and merge the PRs that succeeded to build. The ones that failed will get picked up by the [maintainer](https://docs.google.com/document/d/1V5VINWXATMpz9UBw4IKmVVB8aw3CxM0Jt7igtHnDfSk/edit#). For final releases additionally do these steps: -- [ ] Archive done column on the [team board](https://github.com/orgs/element-hq/projects/67/views/34) _Note: this should be automated_ -- [ ] Add entry to the [milestones diary](https://docs.google.com/document/d/1cpRFJdfNCo2Ps6jqzQmatzbYEToSrQpyBug0aP_iwZE/edit#heading=h.6y55fw4t283z). The document says only to add significant releases, but we add all of them just in case. +- [ ] Archive done column on the [team board](https://github.com/orgs/element-hq/projects/67/views/34) _Note: this should be automated_ +- [ ] Add entry to the [milestones diary](https://docs.google.com/document/d/1cpRFJdfNCo2Ps6jqzQmatzbYEToSrQpyBug0aP_iwZE/edit#heading=h.6y55fw4t283z). The document says only to add significant releases, but we add all of them just in case. diff --git a/docs/review.md b/docs/review.md index 8f8dc5f09bb..c565db52978 100644 --- a/docs/review.md +++ b/docs/review.md @@ -10,53 +10,53 @@ When reviewing code, here are some things we look for and also things we avoid: ### We review for -- Correctness -- Performance -- Accessibility -- Security -- Quality via automated and manual testing -- Comments and documentation where needed -- Sharing knowledge of different areas among the team -- Ensuring it's something we're comfortable maintaining for the long term -- Progress indicators and local echo where appropriate with network activity +- Correctness +- Performance +- Accessibility +- Security +- Quality via automated and manual testing +- Comments and documentation where needed +- Sharing knowledge of different areas among the team +- Ensuring it's something we're comfortable maintaining for the long term +- Progress indicators and local echo where appropriate with network activity ### We should avoid -- Style nits that are already handled by the linter -- Dramatically increasing scope +- Style nits that are already handled by the linter +- Dramatically increasing scope ### Good practices -- Use empathetic language - - See also [Mindful Communication in Code - Reviews](https://kickstarter.engineering/a-guide-to-mindful-communication-in-code-reviews-48aab5282e5e) - and [How to Do Code Reviews Like a Human](https://mtlynch.io/human-code-reviews-1/) -- Authors should prefer smaller commits for easier reviewing and bisection -- Reviewers should be explicit about required versus optional changes - - Reviews are conversations and the PR author should feel comfortable - discussing and pushing back on changes before making them -- Reviewers are encouraged to ask for tests where they believe it is reasonable -- Core team should lead by example through their tone and language -- Take the time to thank and point out good code changes -- Using softer language like "please" and "what do you think?" goes a long way - towards making others feel like colleagues working towards a common goal +- Use empathetic language + - See also [Mindful Communication in Code + Reviews](https://kickstarter.engineering/a-guide-to-mindful-communication-in-code-reviews-48aab5282e5e) + and [How to Do Code Reviews Like a Human](https://mtlynch.io/human-code-reviews-1/) +- Authors should prefer smaller commits for easier reviewing and bisection +- Reviewers should be explicit about required versus optional changes + - Reviews are conversations and the PR author should feel comfortable + discussing and pushing back on changes before making them +- Reviewers are encouraged to ask for tests where they believe it is reasonable +- Core team should lead by example through their tone and language +- Take the time to thank and point out good code changes +- Using softer language like "please" and "what do you think?" goes a long way + towards making others feel like colleagues working towards a common goal ### Workflow -- Authors should request review from the element-web team by default (if someone on - the team is clearly the expert in an area, a direct review request to them may - be more appropriate) -- Reviewers should remove the team review request and request review from - themselves when starting a review to avoid double review -- If there are multiple related PRs authors should reference each of the PRs in - the others before requesting review. Reviewers might start reviewing from - different places and could miss other required PRs. -- Avoid force pushing to a PR after the first round of review -- Use the GitHub default of merge commits when landing (avoid alternate options - like squash or rebase) -- PR author merges after review (assuming they have write access) -- Assign issues only when in progress to indicate to others what can be picked - up +- Authors should request review from the element-web team by default (if someone on + the team is clearly the expert in an area, a direct review request to them may + be more appropriate) +- Reviewers should remove the team review request and request review from + themselves when starting a review to avoid double review +- If there are multiple related PRs authors should reference each of the PRs in + the others before requesting review. Reviewers might start reviewing from + different places and could miss other required PRs. +- Avoid force pushing to a PR after the first round of review +- Use the GitHub default of merge commits when landing (avoid alternate options + like squash or rebase) +- PR author merges after review (assuming they have write access) +- Assign issues only when in progress to indicate to others what can be picked + up ## Code Quality @@ -64,10 +64,10 @@ In the past, we have occasionally written different kinds of tests for Element and the SDKs, but it hasn't been a consistent focus. Going forward, we'd like to change that. -- For new features, code reviewers will expect some form of automated testing to - be included by default -- For bug fixes, regression tests are of course great to have, but we don't want - to block fixes on this, so we won't require them at this time +- For new features, code reviewers will expect some form of automated testing to + be included by default +- For bug fixes, regression tests are of course great to have, but we don't want + to block fixes on this, so we won't require them at this time The above policy is not a strict rule, but instead it's meant to be a conversation between the author and reviewer. As an author, try to think about @@ -104,10 +104,10 @@ perspective. In more detail, our usual process for changes that affect the UI or alter user functionality is: -- For changes that will go live when merged, always flag Design and Product - teams as appropriate -- For changes guarded by a feature flag, Design and Product review is not - required (though may still be useful) since we can continue tweaking +- For changes that will go live when merged, always flag Design and Product + teams as appropriate +- For changes guarded by a feature flag, Design and Product review is not + required (though may still be useful) since we can continue tweaking As it can be difficult to review design work from looking at just the changed files in a PR, a [preview site](./pr-previews.md) that includes your changes diff --git a/docs/room-list-store.md b/docs/room-list-store.md index b87bf5f7bd2..4e131ee309d 100644 --- a/docs/room-list-store.md +++ b/docs/room-list-store.md @@ -6,11 +6,11 @@ It's so complicated it needs its own README. Legend: -- Orange = External event. -- Purple = Deterministic flow. -- Green = Algorithm definition. -- Red = Exit condition/point. -- Blue = Process definition. +- Orange = External event. +- Purple = Deterministic flow. +- Green = Algorithm definition. +- Red = Exit condition/point. +- Blue = Process definition. ## Algorithms involved @@ -68,14 +68,14 @@ simply get the manual sorting algorithm applied to them with no further involvem algorithm. There are 4 categories: Red, Grey, Bold, and Idle. Each has their own definition based off relative (perceived) importance to the user: -- **Red**: The room has unread mentions waiting for the user. -- **Grey**: The room has unread notifications waiting for the user. Notifications are simply unread - messages which cause a push notification or badge count. Typically, this is the default as rooms get - set to 'All Messages'. -- **Bold**: The room has unread messages waiting for the user. Essentially this is a grey room without - a badge/notification count (or 'Mentions Only'/'Muted'). -- **Idle**: No useful (see definition of useful above) activity has occurred in the room since the user - last read it. +- **Red**: The room has unread mentions waiting for the user. +- **Grey**: The room has unread notifications waiting for the user. Notifications are simply unread + messages which cause a push notification or badge count. Typically, this is the default as rooms get + set to 'All Messages'. +- **Bold**: The room has unread messages waiting for the user. Essentially this is a grey room without + a badge/notification count (or 'Mentions Only'/'Muted'). +- **Idle**: No useful (see definition of useful above) activity has occurred in the room since the user + last read it. Conveniently, each tag gets ordered by those categories as presented: red rooms appear above grey, grey above bold, etc. diff --git a/docs/settings.md b/docs/settings.md index 3f0636d3801..e555cd7c1e8 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -10,13 +10,13 @@ of dealing with the different levels and exposes easy to use getters and setters Granular Settings rely on a series of known levels in order to use the correct value for the scenario. These levels, in order of priority, are: -- `device` - The current user's device -- `room-device` - The current user's device, but only when in a specific room -- `room-account` - The current user's account, but only when in a specific room -- `account` - The current user's account -- `room` - A specific room (setting for all members of the room) -- `config` - Values are defined by the `setting_defaults` key (usually) in `config.json` -- `default` - The hardcoded default for the settings +- `device` - The current user's device +- `room-device` - The current user's device, but only when in a specific room +- `room-account` - The current user's account, but only when in a specific room +- `account` - The current user's account +- `room` - A specific room (setting for all members of the room) +- `config` - Values are defined by the `setting_defaults` key (usually) in `config.json` +- `default` - The hardcoded default for the settings Individual settings may control which levels are appropriate for them as part of the defaults. This is often to ensure that room administrators cannot force account-only settings upon participants. diff --git a/docs/translating-dev.md b/docs/translating-dev.md index e2a8e2c82a9..fd1ac232940 100644 --- a/docs/translating-dev.md +++ b/docs/translating-dev.md @@ -2,9 +2,9 @@ ## Requirements -- A working [Development Setup](../README.md#setting-up-a-dev-environment) -- Latest LTS version of Node.js installed -- Be able to understand English +- A working [Development Setup](../README.md#setting-up-a-dev-environment) +- Latest LTS version of Node.js installed +- Be able to understand English ## Translating strings vs. marking strings for translation @@ -65,17 +65,17 @@ There you can also require all translations to be redone if the meaning of the s 1. Add it to the array in `_t` for example `_t(TKEY, {variable: this.variable})` 1. Add the variable inside the string. The syntax for variables is `%(variable)s`. Please note the _s_ at the end. The name of the variable has to match the previous used name. -- You can use the special `count` variable to choose between multiple versions of the same string, in order to get the correct pluralization. E.g. `_t('You have %(count)s new messages', { count: 2 })` would show 'You have 2 new messages', while `_t('You have %(count)s new messages', { count: 1 })` would show 'You have one new message' (assuming a singular version of the string has been added to the translation file. See above). Passing in `count` is much preferred over having an if-statement choose the correct string to use, because some languages have much more complicated plural rules than english (e.g. they might need a completely different form if there are three things rather than two). -- If you want to translate text that includes e.g. hyperlinks or other HTML you have to also use tag substitution, e.g. `_t('Click here!', {}, { 'a': (sub) => {sub} })`. If you don't do the tag substitution you will end up showing literally '' rather than making a hyperlink. -- You can also use React components with normal variable substitution if you want to insert HTML markup, e.g. `_t('Your email address is %(emailAddress)s', { emailAddress: {userEmailAddress} })`. +- You can use the special `count` variable to choose between multiple versions of the same string, in order to get the correct pluralization. E.g. `_t('You have %(count)s new messages', { count: 2 })` would show 'You have 2 new messages', while `_t('You have %(count)s new messages', { count: 1 })` would show 'You have one new message' (assuming a singular version of the string has been added to the translation file. See above). Passing in `count` is much preferred over having an if-statement choose the correct string to use, because some languages have much more complicated plural rules than english (e.g. they might need a completely different form if there are three things rather than two). +- If you want to translate text that includes e.g. hyperlinks or other HTML you have to also use tag substitution, e.g. `_t('Click here!', {}, { 'a': (sub) => {sub} })`. If you don't do the tag substitution you will end up showing literally '' rather than making a hyperlink. +- You can also use React components with normal variable substitution if you want to insert HTML markup, e.g. `_t('Your email address is %(emailAddress)s', { emailAddress: {userEmailAddress} })`. ## Things to know/Style Guides -- Do not use `_t()` inside `getDefaultProps`: the translations aren't loaded when `getDefaultProps` is called, leading to missing translations. Use `_td()` to indicate that `_t()` will be called on the string later. -- If using translated strings as constants, translated strings can't be in constants loaded at class-load time since the translations won't be loaded. Mark the strings using `_td()` instead and perform the actual translation later. -- If a string is presented in the UI with punctuation like a full stop, include this in the translation strings, since punctuation varies between languages too. -- Avoid "translation in parts", i.e. concatenating translated strings or using translated strings in variable substitutions. Context is important for translations, and translating partial strings this way is simply not always possible. -- Concatenating strings often also introduces an implicit assumption about word order (e.g. that the subject of the sentence comes first), which is incorrect for many languages. -- Translation 'smell test': If you have a string that does not begin with a capital letter (is not the start of a sentence) or it ends with e.g. ':' or a preposition (e.g. 'to') you should recheck that you are not trying to translate a partial sentence. -- If you have multiple strings, that are almost identical, except some part (e.g. a word or two) it is still better to translate the full sentence multiple times. It may seem like inefficient repetition, but unlike programming where you try to minimize repetition, translation is much faster if you have many, full, clear, sentences to work with, rather than fewer, but incomplete sentence fragments. -- Don't forget curly braces when you assign an expression to JSX attributes in the render method) +- Do not use `_t()` inside `getDefaultProps`: the translations aren't loaded when `getDefaultProps` is called, leading to missing translations. Use `_td()` to indicate that `_t()` will be called on the string later. +- If using translated strings as constants, translated strings can't be in constants loaded at class-load time since the translations won't be loaded. Mark the strings using `_td()` instead and perform the actual translation later. +- If a string is presented in the UI with punctuation like a full stop, include this in the translation strings, since punctuation varies between languages too. +- Avoid "translation in parts", i.e. concatenating translated strings or using translated strings in variable substitutions. Context is important for translations, and translating partial strings this way is simply not always possible. +- Concatenating strings often also introduces an implicit assumption about word order (e.g. that the subject of the sentence comes first), which is incorrect for many languages. +- Translation 'smell test': If you have a string that does not begin with a capital letter (is not the start of a sentence) or it ends with e.g. ':' or a preposition (e.g. 'to') you should recheck that you are not trying to translate a partial sentence. +- If you have multiple strings, that are almost identical, except some part (e.g. a word or two) it is still better to translate the full sentence multiple times. It may seem like inefficient repetition, but unlike programming where you try to minimize repetition, translation is much faster if you have many, full, clear, sentences to work with, rather than fewer, but incomplete sentence fragments. +- Don't forget curly braces when you assign an expression to JSX attributes in the render method) diff --git a/docs/translating.md b/docs/translating.md index 657b8cebbcd..2b82453f933 100644 --- a/docs/translating.md +++ b/docs/translating.md @@ -2,9 +2,9 @@ ## Requirements -- Web Browser -- Be able to understand English -- Be able to understand the language you want to translate Element into +- Web Browser +- Be able to understand English +- Be able to understand the language you want to translate Element into ## Join #element-translations:matrix.org diff --git a/jest.config.ts b/jest.config.ts index 04f1a91e77c..326f2040d97 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -32,7 +32,6 @@ const config: Config = { "decoderWorker\\.min\\.wasm": "/__mocks__/empty.js", "waveWorker\\.min\\.js": "/__mocks__/empty.js", "context-filter-polyfill": "/__mocks__/empty.js", - "FontManager.ts": "/__mocks__/FontManager.js", "workers/(.+)Factory": "/__mocks__/workerFactoryMock.js", "^!!raw-loader!.*": "jest-raw-loader", "recorderWorkletFactory": "/__mocks__/empty.js", diff --git a/package.json b/package.json index 24a4858192d..4d839b37e15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "elecord-web", - "version": "1.11.87", + "version": "1.11.89", "description": "Privacy focused chat app for gamers", "author": "hazzuk", "repository": { @@ -64,7 +64,7 @@ "test:playwright:open": "yarn test:playwright --ui", "test:playwright:screenshots": "yarn test:playwright:screenshots:build && yarn test:playwright:screenshots:run", "test:playwright:screenshots:build": "docker build playwright -t element-web-playwright", - "test:playwright:screenshots:run": "docker run --rm --network host -e BASE_URL -e CI -v $(pwd):/work/ -v $(node -e 'console.log(require(`path`).dirname(require.resolve(`matrix-js-sdk/package.json`)))'):/work/node_modules/matrix-js-sdk -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/:/tmp/ -it element-web-playwright", + "test:playwright:screenshots:run": "docker run --rm --network host -e BASE_URL -e CI -v $(pwd):/work/ -v $(node -e 'console.log(require(`path`).dirname(require.resolve(`matrix-js-sdk/package.json`)))'):/work/node_modules/matrix-js-sdk -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/:/tmp/ -it element-web-playwright --grep @screenshot", "coverage": "yarn test --coverage", "analyse:unused-exports": "ts-node ./scripts/analyse_unused_exports.ts", "analyse:webpack-bundles": "webpack-bundle-analyzer webpack-stats.json webapp", @@ -73,12 +73,14 @@ "resolutions": { "oidc-client-ts": "3.1.0", "jwt-decode": "4.0.0", - "caniuse-lite": "1.0.30001679", + "caniuse-lite": "1.0.30001684", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0", "wrap-ansi": "npm:wrap-ansi@^7.0.0" }, "dependencies": { "@babel/runtime": "^7.12.5", + "@fontsource/inconsolata": "^5", + "@fontsource/inter": "^5", "@formatjs/intl-segmenter": "^11.5.7", "@matrix-org/analytics-events": "^0.29.0", "@matrix-org/emojibase-bindings": "^1.3.3", @@ -114,15 +116,15 @@ "jsrsasign": "^11.0.0", "jszip": "^3.7.0", "katex": "^0.16.0", - "linkify-element": "4.1.3", - "linkify-react": "4.1.3", - "linkify-string": "4.1.3", - "linkifyjs": "4.1.3", + "linkify-element": "4.2.0", + "linkify-react": "4.2.0", + "linkify-string": "4.2.0", + "linkifyjs": "4.2.0", "lodash": "^4.17.21", "maplibre-gl": "^4.0.0", "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "0.0.1", - "matrix-js-sdk": "34.13.0", + "matrix-js-sdk": "35.1.0", "matrix-widget-api": "^1.10.0", "memoize-one": "^6.0.0", "mime": "^4.0.4", @@ -214,7 +216,6 @@ "babel-loader": "^9.0.0", "babel-plugin-jsx-remove-data-test-id": "^3.0.0", "blob-polyfill": "^9.0.0", - "buffer": "^6.0.3", "chokidar": "^4.0.0", "concurrently": "^9.0.0", "copy-webpack-plugin": "^12.0.0", @@ -268,11 +269,12 @@ "postcss-preset-env": "^10.0.0", "postcss-scss": "^4.0.4", "postcss-simple-vars": "^7.0.1", - "prettier": "3.3.3", + "prettier": "3.4.1", "process": "^0.11.10", "raw-loader": "^4.0.2", "rimraf": "^6.0.0", "semver": "^7.5.2", + "source-map-loader": "^5.0.0", "stylelint": "^16.1.0", "stylelint-config-standard": "^36.0.0", "stylelint-scss": "^6.0.0", diff --git a/playwright.config.ts b/playwright.config.ts index 1c1d380042d..06c1b05322d 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -6,11 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { defineConfig } from "@playwright/test"; +import { defineConfig, devices } from "@playwright/test"; const baseURL = process.env["BASE_URL"] ?? "http://localhost:8080"; export default defineConfig({ + projects: [{ name: "Chrome", use: { ...devices["Desktop Chrome"], channel: "chromium" } }], use: { viewport: { width: 1280, height: 720 }, ignoreHTTPSErrors: true, diff --git a/playwright/Dockerfile b/playwright/Dockerfile index 9d478ff231a..021820984a9 100644 --- a/playwright/Dockerfile +++ b/playwright/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/playwright:v1.48.2-jammy +FROM mcr.microsoft.com/playwright:v1.49.0-noble WORKDIR /work diff --git a/playwright/e2e/app-loading/feature-detection.spec.ts b/playwright/e2e/app-loading/feature-detection.spec.ts index 16e17a80549..ee61fb56628 100644 --- a/playwright/e2e/app-loading/feature-detection.spec.ts +++ b/playwright/e2e/app-loading/feature-detection.spec.ts @@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details. import { test, expect } from "../../element-web-test"; -test(`shows error page if browser lacks Intl support`, async ({ page }) => { +test(`shows error page if browser lacks Intl support`, { tag: "@screenshot" }, async ({ page }) => { await page.addInitScript({ content: `delete window.Intl;` }); await page.goto("/"); @@ -21,7 +21,7 @@ test(`shows error page if browser lacks Intl support`, async ({ page }) => { await expect(page).toMatchScreenshot("unsupported-browser.png"); }); -test(`shows error page if browser lacks WebAssembly support`, async ({ page }) => { +test(`shows error page if browser lacks WebAssembly support`, { tag: "@screenshot" }, async ({ page }) => { await page.addInitScript({ content: `delete window.WebAssembly;` }); await page.goto("/"); diff --git a/playwright/e2e/audio-player/audio-player.spec.ts b/playwright/e2e/audio-player/audio-player.spec.ts index c2081dfcd80..2bb9ab0be45 100644 --- a/playwright/e2e/audio-player/audio-player.spec.ts +++ b/playwright/e2e/audio-player/audio-player.spec.ts @@ -134,18 +134,22 @@ test.describe("Audio player", () => { ).toBeVisible(); }); - test("should be correctly rendered - light theme", async ({ page, app }) => { + test("should be correctly rendered - light theme", { tag: "@screenshot" }, async ({ page, app }) => { await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg"); await takeSnapshots(page, app, "Selected EventTile of audio player (light theme)"); }); - test("should be correctly rendered - light theme with monospace font", async ({ page, app }) => { - await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg"); + test( + "should be correctly rendered - light theme with monospace font", + { tag: "@screenshot" }, + async ({ page, app }) => { + await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg"); - await takeSnapshots(page, app, "Selected EventTile of audio player (light theme, monospace font)", true); // Enable monospace - }); + await takeSnapshots(page, app, "Selected EventTile of audio player (light theme, monospace font)", true); // Enable monospace + }, + ); - test("should be correctly rendered - high contrast theme", async ({ page, app }) => { + test("should be correctly rendered - high contrast theme", { tag: "@screenshot" }, async ({ page, app }) => { // Disable system theme in case ThemeWatcher enables the theme automatically, // so that the high contrast theme can be enabled await app.settings.setValue("use_system_theme", null, SettingLevel.DEVICE, false); @@ -161,7 +165,7 @@ test.describe("Audio player", () => { await takeSnapshots(page, app, "Selected EventTile of audio player (high contrast)"); }); - test("should be correctly rendered - dark theme", async ({ page, app }) => { + test("should be correctly rendered - dark theme", { tag: "@screenshot" }, async ({ page, app }) => { // Enable dark theme await app.settings.setValue("theme", null, SettingLevel.ACCOUNT, "dark"); @@ -207,93 +211,101 @@ test.describe("Audio player", () => { expect(download.suggestedFilename()).toBe("1sec.ogg"); }); - test("should support replying to audio file with another audio file", async ({ page, app }) => { - await uploadFile(page, "playwright/sample-files/1sec.ogg"); + test( + "should support replying to audio file with another audio file", + { tag: "@screenshot" }, + async ({ page, app }) => { + await uploadFile(page, "playwright/sample-files/1sec.ogg"); - // Assert the audio player is rendered - await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible(); + // Assert the audio player is rendered + await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible(); - // Find and click "Reply" button on MessageActionBar - const tile = page.locator(".mx_EventTile_last"); - await tile.hover(); - await tile.getByRole("button", { name: "Reply", exact: true }).click(); + // Find and click "Reply" button on MessageActionBar + const tile = page.locator(".mx_EventTile_last"); + await tile.hover(); + await tile.getByRole("button", { name: "Reply", exact: true }).click(); - // Reply to the player with another audio file - await uploadFile(page, "playwright/sample-files/1sec.ogg"); + // Reply to the player with another audio file + await uploadFile(page, "playwright/sample-files/1sec.ogg"); - // Assert that the audio player is rendered - await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible(); + // Assert that the audio player is rendered + await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible(); - // Assert that replied audio file is rendered as file button inside ReplyChain - const button = tile.locator(".mx_ReplyChain_wrapper .mx_MFileBody_info[role='button']"); - // Assert that the file button has file name - await expect(button.locator(".mx_MFileBody_info_filename")).toBeVisible(); + // Assert that replied audio file is rendered as file button inside ReplyChain + const button = tile.locator(".mx_ReplyChain_wrapper .mx_MFileBody_info[role='button']"); + // Assert that the file button has file name + await expect(button.locator(".mx_MFileBody_info_filename")).toBeVisible(); - await takeSnapshots(page, app, "Selected EventTile of audio player with a reply"); - }); + await takeSnapshots(page, app, "Selected EventTile of audio player with a reply"); + }, + ); - test("should support creating a reply chain with multiple audio files", async ({ page, app, user }) => { - // Note: "mx_ReplyChain" element is used not only for replies which - // create a reply chain, but also for a single reply without a replied - // message. This test checks whether a reply chain which consists of - // multiple audio file replies is rendered properly. + test( + "should support creating a reply chain with multiple audio files", + { tag: "@screenshot" }, + async ({ page, app, user }) => { + // Note: "mx_ReplyChain" element is used not only for replies which + // create a reply chain, but also for a single reply without a replied + // message. This test checks whether a reply chain which consists of + // multiple audio file replies is rendered properly. - const tile = page.locator(".mx_EventTile_last"); + const tile = page.locator(".mx_EventTile_last"); - // Find and click "Reply" button - const clickButtonReply = async () => { - await tile.scrollIntoViewIfNeeded(); - await tile.hover(); - await tile.getByRole("button", { name: "Reply", exact: true }).click(); - }; + // Find and click "Reply" button + const clickButtonReply = async () => { + await tile.scrollIntoViewIfNeeded(); + await tile.hover(); + await tile.getByRole("button", { name: "Reply", exact: true }).click(); + }; - await uploadFile(page, "playwright/sample-files/upload-first.ogg"); + await uploadFile(page, "playwright/sample-files/upload-first.ogg"); - // Assert that the audio player is rendered - await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible(); + // Assert that the audio player is rendered + await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible(); - await clickButtonReply(); + await clickButtonReply(); - // Reply to the player with another audio file - await uploadFile(page, "playwright/sample-files/upload-second.ogg"); + // Reply to the player with another audio file + await uploadFile(page, "playwright/sample-files/upload-second.ogg"); - // Assert that the audio player is rendered - await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible(); + // Assert that the audio player is rendered + await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible(); - await clickButtonReply(); + await clickButtonReply(); - // Reply to the player with yet another audio file to create a reply chain - await uploadFile(page, "playwright/sample-files/upload-third.ogg"); + // Reply to the player with yet another audio file to create a reply chain + await uploadFile(page, "playwright/sample-files/upload-third.ogg"); - // Assert that the audio player is rendered - await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible(); + // Assert that the audio player is rendered + await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible(); - // Assert that there are two "mx_ReplyChain" elements - await expect(tile.locator(".mx_ReplyChain")).toHaveCount(2); + // Assert that there are two "mx_ReplyChain" elements + await expect(tile.locator(".mx_ReplyChain")).toHaveCount(2); - // Assert that one line contains the user name - await expect(tile.locator(".mx_ReplyChain .mx_ReplyTile_sender").getByText(user.displayName)).toBeVisible(); + // Assert that one line contains the user name + await expect(tile.locator(".mx_ReplyChain .mx_ReplyTile_sender").getByText(user.displayName)).toBeVisible(); - // Assert that the other line contains the file button - await expect(tile.locator(".mx_ReplyChain .mx_MFileBody")).toBeVisible(); + // Assert that the other line contains the file button + await expect(tile.locator(".mx_ReplyChain .mx_MFileBody")).toBeVisible(); - // Click "In reply to" - await tile.locator(".mx_ReplyChain .mx_ReplyChain_show", { hasText: "In reply to" }).click(); + // Click "In reply to" + await tile.locator(".mx_ReplyChain .mx_ReplyChain_show", { hasText: "In reply to" }).click(); - const replyChain = tile.locator(".mx_ReplyChain:first-of-type"); - // Assert that "In reply to" has disappeared - await expect(replyChain.getByText("In reply to")).not.toBeVisible(); + const replyChain = tile.locator(".mx_ReplyChain:first-of-type"); + // Assert that "In reply to" has disappeared + await expect(replyChain.getByText("In reply to")).not.toBeVisible(); - // Assert that the file button contains the name of the file sent at first - await expect( - replyChain - .locator(".mx_MFileBody_info[role='button']") - .locator(".mx_MFileBody_info_filename", { hasText: "upload-first.ogg" }), - ).toBeVisible(); + // Assert that the file button contains the name of the file sent at first + await expect( + replyChain + .locator(".mx_MFileBody_info[role='button']") + .locator(".mx_MFileBody_info_filename", { hasText: "upload-first.ogg" }), + ).toBeVisible(); - // Take snapshots - await takeSnapshots(page, app, "Selected EventTile of audio player with a reply chain"); - }); + // Take snapshots + await takeSnapshots(page, app, "Selected EventTile of audio player with a reply chain"); + }, + ); test("should be rendered, play, and support replying on a thread", async ({ page, app }) => { await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg"); diff --git a/playwright/e2e/chat-export/html-export.spec.ts b/playwright/e2e/chat-export/html-export.spec.ts index 9a66a4907a3..f914cccd96b 100644 --- a/playwright/e2e/chat-export/html-export.spec.ts +++ b/playwright/e2e/chat-export/html-export.spec.ts @@ -89,43 +89,47 @@ test.describe("HTML Export", () => { }, }); - test("should export html successfully and match screenshot", async ({ page, app, room }) => { - // Set a fixed time rather than masking off the line with the time in it: we don't need to worry - // about the width changing and we can actually test this line looks correct. - page.clock.setSystemTime(new Date("2024-01-01T00:00:00Z")); - - // Send a bunch of messages to populate the room - for (let i = 1; i < 10; i++) { - const respone = await app.client.sendMessage(room.roomId, { body: `Testing ${i}`, msgtype: "m.text" }); - if (i == 1) { - await app.client.reactToMessage(room.roomId, null, respone.event_id, "🙃"); + test( + "should export html successfully and match screenshot", + { tag: "@screenshot" }, + async ({ page, app, room }) => { + // Set a fixed time rather than masking off the line with the time in it: we don't need to worry + // about the width changing and we can actually test this line looks correct. + page.clock.setSystemTime(new Date("2024-01-01T00:00:00Z")); + + // Send a bunch of messages to populate the room + for (let i = 1; i < 10; i++) { + const respone = await app.client.sendMessage(room.roomId, { body: `Testing ${i}`, msgtype: "m.text" }); + if (i == 1) { + await app.client.reactToMessage(room.roomId, null, respone.event_id, "🙃"); + } } - } - - // Wait for all the messages to be displayed - await expect( - page.locator(".mx_EventTile_last .mx_MTextBody .mx_EventTile_body").getByText("Testing 9"), - ).toBeVisible(); - - await app.toggleRoomInfoPanel(); - await page.getByRole("menuitem", { name: "Export Chat" }).click(); - - const downloadPromise = page.waitForEvent("download"); - await page.getByRole("button", { name: "Export", exact: true }).click(); - const download = await downloadPromise; - - const dirPath = path.join(os.tmpdir(), "html-export-test"); - const zipPath = `${dirPath}.zip`; - await download.saveAs(zipPath); - - const zip = await extractZipFileToPath(zipPath, dirPath); - await page.goto(`file://${dirPath}/${Object.keys(zip.files)[0]}/messages.html`); - await expect(page).toMatchScreenshot("html-export.png", { - mask: [ - // We need to mask the whole thing because the width of the time part changes - page.locator(".mx_TimelineSeparator"), - page.locator(".mx_MessageTimestamp"), - ], - }); - }); + + // Wait for all the messages to be displayed + await expect( + page.locator(".mx_EventTile_last .mx_MTextBody .mx_EventTile_body").getByText("Testing 9"), + ).toBeVisible(); + + await app.toggleRoomInfoPanel(); + await page.getByRole("menuitem", { name: "Export Chat" }).click(); + + const downloadPromise = page.waitForEvent("download"); + await page.getByRole("button", { name: "Export", exact: true }).click(); + const download = await downloadPromise; + + const dirPath = path.join(os.tmpdir(), "html-export-test"); + const zipPath = `${dirPath}.zip`; + await download.saveAs(zipPath); + + const zip = await extractZipFileToPath(zipPath, dirPath); + await page.goto(`file://${dirPath}/${Object.keys(zip.files)[0]}/messages.html`); + await expect(page).toMatchScreenshot("html-export.png", { + mask: [ + // We need to mask the whole thing because the width of the time part changes + page.locator(".mx_TimelineSeparator"), + page.locator(".mx_MessageTimestamp"), + ], + }); + }, + ); }); diff --git a/playwright/e2e/crypto/crypto.spec.ts b/playwright/e2e/crypto/crypto.spec.ts index 2ab49e72ec9..668c17d931d 100644 --- a/playwright/e2e/crypto/crypto.spec.ts +++ b/playwright/e2e/crypto/crypto.spec.ts @@ -204,30 +204,29 @@ test.describe("Cryptography", function () { await expect(page.locator(".mx_Dialog")).toHaveCount(1); }); - test("creating a DM should work, being e2e-encrypted / user verification", async ({ - page, - app, - bot: bob, - user: aliceCredentials, - }) => { - await app.client.bootstrapCrossSigning(aliceCredentials); - await startDMWithBob(page, bob); - // send first message - await page.getByRole("textbox", { name: "Send a message…" }).fill("Hey!"); - await page.getByRole("textbox", { name: "Send a message…" }).press("Enter"); - await checkDMRoom(page); - const bobRoomId = await bobJoin(page, bob); - await testMessages(page, bob, bobRoomId); - await verify(app, bob); - - // Assert that verified icon is rendered - await page.getByTestId("base-card-back-button").click(); - await page.getByLabel("Room info").nth(1).click(); - await expect(page.locator('.mx_RoomSummaryCard_badges [data-kind="green"]')).toContainText("Encrypted"); - - // Take a snapshot of RoomSummaryCard with a verified E2EE icon - await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("RoomSummaryCard-with-verified-e2ee.png"); - }); + test( + "creating a DM should work, being e2e-encrypted / user verification", + { tag: "@screenshot" }, + async ({ page, app, bot: bob, user: aliceCredentials }) => { + await app.client.bootstrapCrossSigning(aliceCredentials); + await startDMWithBob(page, bob); + // send first message + await page.getByRole("textbox", { name: "Send a message…" }).fill("Hey!"); + await page.getByRole("textbox", { name: "Send a message…" }).press("Enter"); + await checkDMRoom(page); + const bobRoomId = await bobJoin(page, bob); + await testMessages(page, bob, bobRoomId); + await verify(app, bob); + + // Assert that verified icon is rendered + await page.getByTestId("base-card-back-button").click(); + await page.getByLabel("Room info").nth(1).click(); + await expect(page.locator('.mx_RoomSummaryCard_badges [data-kind="green"]')).toContainText("Encrypted"); + + // Take a snapshot of RoomSummaryCard with a verified E2EE icon + await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("RoomSummaryCard-with-verified-e2ee.png"); + }, + ); test("should allow verification when there is no existing DM", async ({ page, diff --git a/playwright/e2e/crypto/decryption-failure-messages.spec.ts b/playwright/e2e/crypto/decryption-failure-messages.spec.ts index ce7ca34d8e1..b2a1209a70a 100644 --- a/playwright/e2e/crypto/decryption-failure-messages.spec.ts +++ b/playwright/e2e/crypto/decryption-failure-messages.spec.ts @@ -67,6 +67,9 @@ test.describe("Cryptography", function () { await page.locator(".mx_AuthPage").getByRole("button", { name: "I'll verify later" }).click(); await app.viewRoomByName("Test room"); + // In this case, the call to cryptoApi.isEncryptionEnabledInRoom is taking a long time to resolve + await page.waitForTimeout(1000); + // There should be two historical events in the timeline const tiles = await page.locator(".mx_EventTile").all(); expect(tiles.length).toBeGreaterThanOrEqual(2); diff --git a/playwright/e2e/crypto/device-verification.spec.ts b/playwright/e2e/crypto/device-verification.spec.ts index e01e5bbd83d..83a81c260cd 100644 --- a/playwright/e2e/crypto/device-verification.spec.ts +++ b/playwright/e2e/crypto/device-verification.spec.ts @@ -102,7 +102,7 @@ test.describe("Device verification", () => { // feed the QR code into the verification request. const qrData = await readQrCode(infoDialog); const verifier = await verificationRequest.evaluateHandle( - (request, qrData) => request.scanQRCode(new Uint8Array(qrData)), + (request, qrData) => request.scanQRCode(new Uint8ClampedArray(qrData)), [...qrData], ); diff --git a/playwright/e2e/crypto/event-shields.spec.ts b/playwright/e2e/crypto/event-shields.spec.ts index b5d3790aaae..0beb8e36500 100644 --- a/playwright/e2e/crypto/event-shields.spec.ts +++ b/playwright/e2e/crypto/event-shields.spec.ts @@ -6,6 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ +import { Locator } from "@playwright/test"; + import { expect, test } from "../../element-web-test"; import { autoJoin, @@ -16,6 +18,8 @@ import { logOutOfElement, verify, } from "./utils"; +import { bootstrapCrossSigningForClient } from "../../pages/client.ts"; +import { ElementAppPage } from "../../pages/ElementAppPage.ts"; test.describe("Cryptography", function () { test.use({ @@ -276,6 +280,15 @@ test.describe("Cryptography", function () { bot: bob, homeserver, }) => { + // Workaround for https://github.com/element-hq/element-web/issues/28640: + // make sure that Alice has seen Bob's identity before she goes offline. We do this by opening + // his user info. + await app.toggleRoomInfoPanel(); + const rightPanel = page.locator(".mx_RightPanel"); + await rightPanel.getByRole("menuitem", { name: "People" }).click(); + await rightPanel.getByRole("button", { name: bob.credentials!.userId }).click(); + await expect(rightPanel.locator(".mx_UserInfo_devices")).toContainText("1 session"); + // Our app is blocked from syncing while Bob sends his messages. await app.client.network.goOffline(); @@ -305,7 +318,50 @@ test.describe("Cryptography", function () { ); const penultimate = page.locator(".mx_EventTile").filter({ hasText: "test encrypted from verified" }); - await expect(penultimate.locator(".mx_EventTile_e2eIcon")).not.toBeVisible(); + await assertNoE2EIcon(penultimate, app); + }); + + test("should show correct shields on events sent by users with changed identity", async ({ + page, + app, + bot: bob, + homeserver, + }) => { + // Verify Bob + await verify(app, bob); + + // Bob logs in a new device and resets cross-signing + const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob); + await bootstrapCrossSigningForClient(await bobSecondDevice.prepareClient(), bob.credentials, true); + + /* should show an error for a message from a previously verified device */ + await bobSecondDevice.sendMessage(testRoomId, "test encrypted from user that was previously verified"); + const last = page.locator(".mx_EventTile_last"); + await expect(last).toContainText("test encrypted from user that was previously verified"); + const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon"); + await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/); + await lastE2eIcon.focus(); + await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText( + "Sender's verified identity has changed", + ); }); }); }); + +/** + * Check that the given message doesn't have an E2E warning icon. + * + * If it does, throw an error. + */ +async function assertNoE2EIcon(messageLocator: Locator, app: ElementAppPage) { + // Make sure the message itself exists, before we check if it has any icons + await messageLocator.waitFor(); + + const e2eIcon = messageLocator.locator(".mx_EventTile_e2eIcon"); + if ((await e2eIcon.count()) > 0) { + // uh-oh, there is an e2e icon. Let's find out what it's about so that we can throw a helpful error. + await e2eIcon.focus(); + const tooltip = await app.getTooltipForElement(e2eIcon); + throw new Error(`Found an unexpected e2eIcon with tooltip '${await tooltip.textContent()}'`); + } +} diff --git a/playwright/e2e/crypto/user-verification.spec.ts b/playwright/e2e/crypto/user-verification.spec.ts index 4c8d641e6f7..bd3d8595267 100644 --- a/playwright/e2e/crypto/user-verification.spec.ts +++ b/playwright/e2e/crypto/user-verification.spec.ts @@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details. import { type Preset, type Visibility } from "matrix-js-sdk/src/matrix"; +import type { Page } from "@playwright/test"; import { test, expect } from "../../element-web-test"; import { doTwoWaySasVerification, awaitVerifier } from "./utils"; import { Client } from "../../pages/client"; @@ -38,6 +39,8 @@ test.describe("User verification", () => { toasts, room: { roomId: dmRoomId }, }) => { + await waitForDeviceKeys(page); + // once Alice has joined, Bob starts the verification const bobVerificationRequest = await bob.evaluateHandle( async (client, { dmRoomId, aliceCredentials }) => { @@ -87,6 +90,8 @@ test.describe("User verification", () => { toasts, room: { roomId: dmRoomId }, }) => { + await waitForDeviceKeys(page); + // once Alice has joined, Bob starts the verification const bobVerificationRequest = await bob.evaluateHandle( async (client, { dmRoomId, aliceCredentials }) => { @@ -149,3 +154,15 @@ async function createDMRoom(client: Client, userId: string): Promise { ], }); } + +/** + * Wait until we get the other user's device keys. + * In newer rust-crypto versions, the verification request will be ignored if we + * don't have the sender's device keys. + */ +async function waitForDeviceKeys(page: Page): Promise { + await expect(page.getByRole("button", { name: "Avatar" })).toBeVisible(); + const avatar = await page.getByRole("button", { name: "Avatar" }); + await avatar.click(); + await expect(page.getByText("1 session")).toBeVisible(); +} diff --git a/playwright/e2e/editing/editing.spec.ts b/playwright/e2e/editing/editing.spec.ts index 206d91982e0..e22ec250b98 100644 --- a/playwright/e2e/editing/editing.spec.ts +++ b/playwright/e2e/editing/editing.spec.ts @@ -66,126 +66,130 @@ test.describe("Editing", () => { botCreateOpts: { displayName: "Bob" }, }); - test("should render and interact with the message edit history dialog", async ({ page, user, app, room }) => { - // Click the "Remove" button on the message edit history dialog - const clickButtonRemove = async (locator: Locator) => { - const eventTileLine = locator.locator(".mx_EventTile_line"); - await eventTileLine.hover(); - await eventTileLine.getByRole("button", { name: "Remove" }).click(); - }; + test( + "should render and interact with the message edit history dialog", + { tag: "@screenshot" }, + async ({ page, user, app, room }) => { + // Click the "Remove" button on the message edit history dialog + const clickButtonRemove = async (locator: Locator) => { + const eventTileLine = locator.locator(".mx_EventTile_line"); + await eventTileLine.hover(); + await eventTileLine.getByRole("button", { name: "Remove" }).click(); + }; - await page.goto(`#/room/${room.roomId}`); - - // Send "Message" - await sendEvent(app, room.roomId); - - // Edit "Message" to "Massage" - await editLastMessage(page, "Massage"); - - // Assert that the edit label is visible - await expect(page.locator(".mx_EventTile_edited")).toBeVisible(); + await page.goto(`#/room/${room.roomId}`); - await clickEditedMessage(page, "Massage"); + // Send "Message" + await sendEvent(app, room.roomId); - // Assert that the message edit history dialog is rendered - const dialog = page.getByRole("dialog"); - const li = dialog.getByRole("listitem").last(); - // Assert CSS styles which are difficult or cannot be detected with snapshots are applied as expected - await expect(li).toHaveCSS("clear", "both"); + // Edit "Message" to "Massage" + await editLastMessage(page, "Massage"); - const timestamp = li.locator(".mx_EventTile .mx_MessageTimestamp"); - await expect(timestamp).toHaveCSS("position", "absolute"); - await expect(timestamp).toHaveCSS("inset-inline-start", "0px"); - await expect(timestamp).toHaveCSS("text-align", "center"); + // Assert that the edit label is visible + await expect(page.locator(".mx_EventTile_edited")).toBeVisible(); - // Assert that monospace characters can fill the content line as expected - await expect(li.locator(".mx_EventTile .mx_EventTile_content")).toHaveCSS("margin-inline-end", "0px"); + await clickEditedMessage(page, "Massage"); - // Assert that zero block start padding is applied to mx_EventTile as expected - // See: .mx_EventTile on _EventTile.pcss - await expect(li.locator(".mx_EventTile")).toHaveCSS("padding-block-start", "0px"); + // Assert that the message edit history dialog is rendered + const dialog = page.getByRole("dialog"); + const li = dialog.getByRole("listitem").last(); + // Assert CSS styles which are difficult or cannot be detected with snapshots are applied as expected + await expect(li).toHaveCSS("clear", "both"); - // Assert that the date separator is rendered at the top - await expect(dialog.getByRole("listitem").first().locator("h2", { hasText: "today" })).toHaveCSS( - "text-transform", - "capitalize", - ); + const timestamp = li.locator(".mx_EventTile .mx_MessageTimestamp"); + await expect(timestamp).toHaveCSS("position", "absolute"); + await expect(timestamp).toHaveCSS("inset-inline-start", "0px"); + await expect(timestamp).toHaveCSS("text-align", "center"); - { - // Assert that the edited message is rendered under the date separator - const tile = dialog.locator("li:nth-child(2) .mx_EventTile"); - // Assert that the edited message body consists of both deleted character and inserted character - // Above the first "e" of "Message" was replaced with "a" - await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage"); - - const body = tile.locator(".mx_EventTile_content .mx_EventTile_body"); - await expect(body.locator(".mx_EditHistoryMessage_deletion").getByText("e")).toBeVisible(); - await expect(body.locator(".mx_EditHistoryMessage_insertion").getByText("a")).toBeVisible(); - } + // Assert that monospace characters can fill the content line as expected + await expect(li.locator(".mx_EventTile .mx_EventTile_content")).toHaveCSS("margin-inline-end", "0px"); - // Assert that the original message is rendered at the bottom - await expect( - dialog - .locator("li:nth-child(3) .mx_EventTile") - .locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Message" }), - ).toBeVisible(); + // Assert that zero block start padding is applied to mx_EventTile as expected + // See: .mx_EventTile on _EventTile.pcss + await expect(li.locator(".mx_EventTile")).toHaveCSS("padding-block-start", "0px"); - // Take a snapshot of the dialog - await expect(dialog).toMatchScreenshot("message-edit-history-dialog.png", { - mask: [page.locator(".mx_MessageTimestamp")], - }); + // Assert that the date separator is rendered at the top + await expect(dialog.getByRole("listitem").first().locator("h2", { hasText: "today" })).toHaveCSS( + "text-transform", + "capitalize", + ); - { - const tile = dialog.locator("li:nth-child(2) .mx_EventTile"); - await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage"); - // Click the "Remove" button again - await clickButtonRemove(tile); - } + { + // Assert that the edited message is rendered under the date separator + const tile = dialog.locator("li:nth-child(2) .mx_EventTile"); + // Assert that the edited message body consists of both deleted character and inserted character + // Above the first "e" of "Message" was replaced with "a" + await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage"); + + const body = tile.locator(".mx_EventTile_content .mx_EventTile_body"); + await expect(body.locator(".mx_EditHistoryMessage_deletion").getByText("e")).toBeVisible(); + await expect(body.locator(".mx_EditHistoryMessage_insertion").getByText("a")).toBeVisible(); + } - // Do nothing and close the dialog to confirm that the message edit history dialog is rendered - await app.closeDialog(); + // Assert that the original message is rendered at the bottom + await expect( + dialog + .locator("li:nth-child(3) .mx_EventTile") + .locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Message" }), + ).toBeVisible(); - { - // Assert that the message edit history dialog is rendered again after it was closed - const tile = dialog.locator("li:nth-child(2) .mx_EventTile"); - await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage"); - // Click the "Remove" button again - await clickButtonRemove(tile); - } + // Take a snapshot of the dialog + await expect(dialog).toMatchScreenshot("message-edit-history-dialog.png", { + mask: [page.locator(".mx_MessageTimestamp")], + }); - // This time remove the message really - const textInputDialog = page.locator(".mx_TextInputDialog"); - await textInputDialog.getByRole("textbox", { name: "Reason (optional)" }).fill("This is a test."); // Reason - await textInputDialog.getByRole("button", { name: "Remove" }).click(); + { + const tile = dialog.locator("li:nth-child(2) .mx_EventTile"); + await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage"); + // Click the "Remove" button again + await clickButtonRemove(tile); + } - // Assert that the message edit history dialog is rendered again - const messageEditHistoryDialog = page.locator(".mx_MessageEditHistoryDialog"); - // Assert that the date is rendered - await expect( - messageEditHistoryDialog.getByRole("listitem").first().locator("h2", { hasText: "today" }), - ).toHaveCSS("text-transform", "capitalize"); + // Do nothing and close the dialog to confirm that the message edit history dialog is rendered + await app.closeDialog(); - // Assert that the original message is rendered under the date on the dialog - await expect( - messageEditHistoryDialog - .locator("li:nth-child(2) .mx_EventTile") - .locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Message" }), - ).toBeVisible(); + { + // Assert that the message edit history dialog is rendered again after it was closed + const tile = dialog.locator("li:nth-child(2) .mx_EventTile"); + await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage"); + // Click the "Remove" button again + await clickButtonRemove(tile); + } - // Assert that the edited message is gone - await expect( - messageEditHistoryDialog.locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Meassage" }), - ).not.toBeVisible(); + // This time remove the message really + const textInputDialog = page.locator(".mx_TextInputDialog"); + await textInputDialog.getByRole("textbox", { name: "Reason (optional)" }).fill("This is a test."); // Reason + await textInputDialog.getByRole("button", { name: "Remove" }).click(); + + // Assert that the message edit history dialog is rendered again + const messageEditHistoryDialog = page.locator(".mx_MessageEditHistoryDialog"); + // Assert that the date is rendered + await expect( + messageEditHistoryDialog.getByRole("listitem").first().locator("h2", { hasText: "today" }), + ).toHaveCSS("text-transform", "capitalize"); + + // Assert that the original message is rendered under the date on the dialog + await expect( + messageEditHistoryDialog + .locator("li:nth-child(2) .mx_EventTile") + .locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Message" }), + ).toBeVisible(); + + // Assert that the edited message is gone + await expect( + messageEditHistoryDialog.locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Meassage" }), + ).not.toBeVisible(); - await app.closeDialog(); + await app.closeDialog(); - // Assert that the redaction placeholder is rendered - await expect( - page - .locator(".mx_RoomView_MessageList") - .locator(".mx_EventTile_last .mx_RedactedBody", { hasText: "Message deleted" }), - ).toBeVisible(); - }); + // Assert that the redaction placeholder is rendered + await expect( + page + .locator(".mx_RoomView_MessageList") + .locator(".mx_EventTile_last .mx_RedactedBody", { hasText: "Message deleted" }), + ).toBeVisible(); + }, + ); test("should render 'View Source' button in developer mode on the message edit history dialog", async ({ page, diff --git a/playwright/e2e/file-upload/image-upload.spec.ts b/playwright/e2e/file-upload/image-upload.spec.ts index eb473d83b2a..76782e90e8b 100644 --- a/playwright/e2e/file-upload/image-upload.spec.ts +++ b/playwright/e2e/file-upload/image-upload.spec.ts @@ -25,7 +25,7 @@ test.describe("Image Upload", () => { ).toBeVisible(); }); - test("should show image preview when uploading an image", async ({ page, app }) => { + test("should show image preview when uploading an image", { tag: "@screenshot" }, async ({ page, app }) => { await page .locator(".mx_MessageComposer_actions input[type='file']") .setInputFiles("playwright/sample-files/riot.png"); diff --git a/playwright/e2e/forgot-password/forgot-password.spec.ts b/playwright/e2e/forgot-password/forgot-password.spec.ts index c148900afd4..0a12514d9ec 100644 --- a/playwright/e2e/forgot-password/forgot-password.spec.ts +++ b/playwright/e2e/forgot-password/forgot-password.spec.ts @@ -26,7 +26,7 @@ test.describe("Forgot Password", () => { }), }); - test("renders properly", async ({ page, homeserver }) => { + test("renders properly", { tag: "@screenshot" }, async ({ page, homeserver }) => { await page.goto("/"); await page.getByRole("link", { name: "Sign in" }).click(); @@ -39,7 +39,7 @@ test.describe("Forgot Password", () => { await expect(page.getByRole("main")).toMatchScreenshot("forgot-password.png"); }); - test("renders email verification dialog properly", async ({ page, homeserver }) => { + test("renders email verification dialog properly", { tag: "@screenshot" }, async ({ page, homeserver }) => { const user = await homeserver.registerUser(username, password); await homeserver.setThreepid(user.userId, "email", email); diff --git a/playwright/e2e/invite/invite-dialog.spec.ts b/playwright/e2e/invite/invite-dialog.spec.ts index c8bd8eb404b..eb434eb5b56 100644 --- a/playwright/e2e/invite/invite-dialog.spec.ts +++ b/playwright/e2e/invite/invite-dialog.spec.ts @@ -19,7 +19,7 @@ test.describe("Invite dialog", function () { const botName = "BotAlice"; - test("should support inviting a user to a room", async ({ page, app, user, bot }) => { + test("should support inviting a user to a room", { tag: "@screenshot" }, async ({ page, app, user, bot }) => { // Create and view a room await app.client.createRoom({ name: "Test Room" }); await app.viewRoomByName("Test Room"); @@ -73,52 +73,63 @@ test.describe("Invite dialog", function () { await expect(page.getByText(`${botName} joined the room`)).toBeVisible(); }); - test("should support inviting a user to Direct Messages", async ({ page, app, user, bot }) => { - await page.locator(".mx_RoomList").getByRole("button", { name: "Start chat" }).click(); + test( + "should support inviting a user to Direct Messages", + { tag: "@screenshot" }, + async ({ page, app, user, bot }) => { + await page.locator(".mx_RoomList").getByRole("button", { name: "Start chat" }).click(); - const other = page.locator(".mx_InviteDialog_other"); - // Assert that the header is rendered - await expect(other.locator(".mx_Dialog_header .mx_Dialog_title").getByText("Direct Messages")).toBeVisible(); + const other = page.locator(".mx_InviteDialog_other"); + // Assert that the header is rendered + await expect( + other.locator(".mx_Dialog_header .mx_Dialog_title").getByText("Direct Messages"), + ).toBeVisible(); - // Assert that the bar is rendered - await expect(other.locator(".mx_InviteDialog_addressBar")).toBeVisible(); + // Assert that the bar is rendered + await expect(other.locator(".mx_InviteDialog_addressBar")).toBeVisible(); - // Take a snapshot of the invite dialog - await expect(page.locator(".mx_Dialog")).toMatchScreenshot("invite-dialog-dm-without-user.png"); + // Take a snapshot of the invite dialog + await expect(page.locator(".mx_Dialog")).toMatchScreenshot("invite-dialog-dm-without-user.png"); - await other.getByTestId("invite-dialog-input").fill(bot.credentials.userId); + await other.getByTestId("invite-dialog-input").fill(bot.credentials.userId); - await expect(other.locator(".mx_InviteDialog_tile_nameStack").getByText(bot.credentials.userId)).toBeVisible(); - await other.locator(".mx_InviteDialog_tile_nameStack").getByText(botName).click(); + await expect( + other.locator(".mx_InviteDialog_tile_nameStack").getByText(bot.credentials.userId), + ).toBeVisible(); + await other.locator(".mx_InviteDialog_tile_nameStack").getByText(botName).click(); - await expect( - other.locator(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name").getByText(botName), - ).toBeVisible(); + await expect( + other.locator(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name").getByText(botName), + ).toBeVisible(); - // Take a snapshot of the invite dialog with a user pill - await expect(page.locator(".mx_Dialog")).toMatchScreenshot("invite-dialog-dm-with-user-pill.png"); + // Take a snapshot of the invite dialog with a user pill + await expect(page.locator(".mx_Dialog")).toMatchScreenshot("invite-dialog-dm-with-user-pill.png"); - // Open a direct message UI - await other.getByRole("button", { name: "Go" }).click(); + // Open a direct message UI + await other.getByRole("button", { name: "Go" }).click(); - // Assert that the invite dialog disappears - await expect(page.locator(".mx_InviteDialog_other")).not.toBeVisible(); + // Assert that the invite dialog disappears + await expect(page.locator(".mx_InviteDialog_other")).not.toBeVisible(); - // Assert that the hovered user name on invitation UI does not have background color - // TODO: implement the test on room-header.spec.ts - const roomHeader = page.locator(".mx_RoomHeader"); - await roomHeader.locator(".mx_RoomHeader_heading").hover(); - await expect(roomHeader.locator(".mx_RoomHeader_heading")).toHaveCSS("background-color", "rgba(0, 0, 0, 0)"); + // Assert that the hovered user name on invitation UI does not have background color + // TODO: implement the test on room-header.spec.ts + const roomHeader = page.locator(".mx_RoomHeader"); + await roomHeader.locator(".mx_RoomHeader_heading").hover(); + await expect(roomHeader.locator(".mx_RoomHeader_heading")).toHaveCSS( + "background-color", + "rgba(0, 0, 0, 0)", + ); - // Send a message to invite the bots - const composer = app.getComposer().locator("[contenteditable]"); - await composer.fill("Hello}"); - await composer.press("Enter"); + // Send a message to invite the bots + const composer = app.getComposer().locator("[contenteditable]"); + await composer.fill("Hello}"); + await composer.press("Enter"); - // Assert that they were invited and joined - await expect(page.getByText(`${botName} joined the room`)).toBeVisible(); + // Assert that they were invited and joined + await expect(page.getByText(`${botName} joined the room`)).toBeVisible(); - // Assert that the message is displayed at the bottom - await expect(page.locator(".mx_EventTile_last").getByText("Hello")).toBeVisible(); - }); + // Assert that the message is displayed at the bottom + await expect(page.locator(".mx_EventTile_last").getByText("Hello")).toBeVisible(); + }, + ); }); diff --git a/playwright/e2e/messages/messages.spec.ts b/playwright/e2e/messages/messages.spec.ts index 0d5a5da4728..1c518199a00 100644 --- a/playwright/e2e/messages/messages.spec.ts +++ b/playwright/e2e/messages/messages.spec.ts @@ -63,7 +63,7 @@ test.describe("Message rendering", () => { { direction: "ltr", displayName: "Quentin" }, { direction: "rtl", displayName: "كوينتين" }, ].forEach(({ direction, displayName }) => { - test.describe(`with ${direction} display name`, () => { + test.describe(`with ${direction} display name`, { tag: "@screenshot" }, () => { test.use({ displayName, room: async ({ user, app }, use) => { @@ -72,14 +72,18 @@ test.describe("Message rendering", () => { }, }); - test("should render a basic LTR text message", async ({ page, user, app, room }) => { - await page.goto(`#/room/${room.roomId}`); + test( + "should render a basic LTR text message", + { tag: "@screenshot" }, + async ({ page, user, app, room }) => { + await page.goto(`#/room/${room.roomId}`); - const msgTile = await sendMessage(page, "Hello, world!"); - await expect(msgTile).toMatchScreenshot(`basic-message-ltr-${direction}displayname.png`, { - mask: [page.locator(".mx_MessageTimestamp")], - }); - }); + const msgTile = await sendMessage(page, "Hello, world!"); + await expect(msgTile).toMatchScreenshot(`basic-message-ltr-${direction}displayname.png`, { + mask: [page.locator(".mx_MessageTimestamp")], + }); + }, + ); test("should render an LTR emote", async ({ page, user, app, room }) => { await page.goto(`#/room/${room.roomId}`); diff --git a/playwright/e2e/permalinks/permalinks.spec.ts b/playwright/e2e/permalinks/permalinks.spec.ts index bd7884ea78d..746e15d2880 100644 --- a/playwright/e2e/permalinks/permalinks.spec.ts +++ b/playwright/e2e/permalinks/permalinks.spec.ts @@ -24,7 +24,7 @@ test.describe("permalinks", () => { displayName: "Alice", }); - test("shoud render permalinks as expected", async ({ page, app, user, homeserver }) => { + test("shoud render permalinks as expected", { tag: "@screenshot" }, async ({ page, app, user, homeserver }) => { const bob = new Bot(page, homeserver, { displayName: "Bob" }); const charlotte = new Bot(page, homeserver, { displayName: "Charlotte" }); await bob.prepareClient(); diff --git a/playwright/e2e/pinned-messages/index.ts b/playwright/e2e/pinned-messages/index.ts index ac50b62294b..545d0e34389 100644 --- a/playwright/e2e/pinned-messages/index.ts +++ b/playwright/e2e/pinned-messages/index.ts @@ -129,6 +129,7 @@ export class Helpers { const timelineMessage = this.page.locator(".mx_MTextBody", { hasText: message }); await timelineMessage.click({ button: "right" }); await this.page.getByRole("menuitem", { name: "Pin", exact: true }).click(); + await this.assertMessageInBanner(message); } /** diff --git a/playwright/e2e/pinned-messages/pinned-messages.spec.ts b/playwright/e2e/pinned-messages/pinned-messages.spec.ts index ef2c1b27d4c..06d6db80580 100644 --- a/playwright/e2e/pinned-messages/pinned-messages.spec.ts +++ b/playwright/e2e/pinned-messages/pinned-messages.spec.ts @@ -10,35 +10,38 @@ import { test } from "./index"; import { expect } from "../../element-web-test"; test.describe("Pinned messages", () => { - test("should show the empty state when there are no pinned messages", async ({ page, app, room1, util }) => { - await util.goTo(room1); - await util.openRoomInfo(); - await util.assertPinnedCountInRoomInfo(0); - await util.openPinnedMessagesList(); - await util.assertEmptyPinnedMessagesList(); - }); - - test("should pin one message and to have the pinned message badge in the timeline", async ({ - page, - app, - room1, - util, - }) => { - await util.goTo(room1); - await util.receiveMessages(room1, ["Msg1"]); - await util.pinMessages(["Msg1"]); - - const tile = util.getEventTile("Msg1"); - await expect(tile).toMatchScreenshot("pinned-message-Msg1.png", { - mask: [tile.locator(".mx_MessageTimestamp")], - // Hide the jump to bottom button in the timeline to avoid flakiness - css: ` + test( + "should show the empty state when there are no pinned messages", + { tag: "@screenshot" }, + async ({ page, app, room1, util }) => { + await util.goTo(room1); + await util.openRoomInfo(); + await util.assertPinnedCountInRoomInfo(0); + await util.openPinnedMessagesList(); + await util.assertEmptyPinnedMessagesList(); + }, + ); + + test( + "should pin one message and to have the pinned message badge in the timeline", + { tag: "@screenshot" }, + async ({ page, app, room1, util }) => { + await util.goTo(room1); + await util.receiveMessages(room1, ["Msg1"]); + await util.pinMessages(["Msg1"]); + + const tile = util.getEventTile("Msg1"); + await expect(tile).toMatchScreenshot("pinned-message-Msg1.png", { + mask: [tile.locator(".mx_MessageTimestamp")], + // Hide the jump to bottom button in the timeline to avoid flakiness + css: ` .mx_JumpToBottomButton { display: none !important; } `, - }); - }); + }); + }, + ); test("should pin messages and show them in the room info panel", async ({ page, app, room1, util }) => { await util.goTo(room1); @@ -73,7 +76,7 @@ test.describe("Pinned messages", () => { await util.assertPinnedCountInRoomInfo(2); }); - test("should unpin all messages", async ({ page, app, room1, util }) => { + test("should unpin all messages", { tag: "@screenshot" }, async ({ page, app, room1, util }) => { await util.goTo(room1); await util.receiveMessages(room1, ["Msg1", "Msg2", "Msg3", "Msg4"]); await util.pinMessages(["Msg1", "Msg2", "Msg4"]); @@ -98,7 +101,7 @@ test.describe("Pinned messages", () => { await util.assertPinnedCountInRoomInfo(0); }); - test("should display one message in the banner", async ({ page, app, room1, util }) => { + test("should display one message in the banner", { tag: "@screenshot" }, async ({ page, app, room1, util }) => { await util.goTo(room1); await util.receiveMessages(room1, ["Msg1"]); await util.pinMessages(["Msg1"]); @@ -106,7 +109,7 @@ test.describe("Pinned messages", () => { await expect(util.getBanner()).toMatchScreenshot("pinned-message-banner-1-Msg1.png"); }); - test("should display 2 messages in the banner", async ({ page, app, room1, util }) => { + test("should display 2 messages in the banner", { tag: "@screenshot" }, async ({ page, app, room1, util }) => { await util.goTo(room1); await util.receiveMessages(room1, ["Msg1", "Msg2"]); await util.pinMessages(["Msg1", "Msg2"]); @@ -123,7 +126,7 @@ test.describe("Pinned messages", () => { await expect(util.getBanner()).toMatchScreenshot("pinned-message-banner-2-Msg2.png"); }); - test("should display 4 messages in the banner", async ({ page, app, room1, util }) => { + test("should display 4 messages in the banner", { tag: "@screenshot" }, async ({ page, app, room1, util }) => { await util.goTo(room1); await util.receiveMessages(room1, ["Msg1", "Msg2", "Msg3", "Msg4"]); await util.pinMessages(["Msg1", "Msg2", "Msg3", "Msg4"]); diff --git a/playwright/e2e/polls/polls.spec.ts b/playwright/e2e/polls/polls.spec.ts index 4fd81955810..e1d3ebe7e36 100644 --- a/playwright/e2e/polls/polls.spec.ts +++ b/playwright/e2e/polls/polls.spec.ts @@ -93,7 +93,7 @@ test.describe("Polls", () => { }); }); - test("should be creatable and votable", async ({ page, app, bot, user }) => { + test("should be creatable and votable", { tag: "@screenshot" }, async ({ page, app, bot, user }) => { const roomId: string = await app.client.createRoom({}); await app.client.inviteUser(roomId, bot.credentials.userId); await page.goto("/#/room/" + roomId); @@ -219,107 +219,121 @@ test.describe("Polls", () => { await expect(page.locator(".mx_ErrorDialog")).toBeAttached(); }); - test("should be displayed correctly in thread panel", async ({ page, app, user, bot, homeserver }) => { - const botCharlie = new Bot(page, homeserver, { displayName: "BotCharlie" }); - await botCharlie.prepareClient(); - - const roomId: string = await app.client.createRoom({}); - await app.client.inviteUser(roomId, bot.credentials.userId); - await app.client.inviteUser(roomId, botCharlie.credentials.userId); - await page.goto("/#/room/" + roomId); - - // wait until the bots joined - await expect(page.getByText("BotBob and one other were invited and joined")).toBeAttached({ timeout: 10000 }); - - const locator = await app.openMessageComposerOptions(); - await locator.getByRole("menuitem", { name: "Poll" }).click(); - - const pollParams = { - title: "Does the polls feature work?", - options: ["Yes", "No", "Maybe"], - }; - await createPoll(page, pollParams); - - // Wait for message to send, get its ID and save as @pollId - const pollId = await page - .locator(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]") - .filter({ hasText: pollParams.title }) - .getAttribute("data-scroll-tokens"); - - // Bob starts thread on the poll - await bot.sendMessage( - roomId, - { - body: "Hello there", - msgtype: "m.text", - }, - pollId, - ); - - // open the thread summary - await page.getByRole("button", { name: "Open thread" }).click(); - - // Bob votes 'Maybe' in the poll - await botVoteForOption(page, bot, roomId, pollId, pollParams.options[2]); - - // Charlie votes 'No' - await botVoteForOption(page, botCharlie, roomId, pollId, pollParams.options[1]); - - // no votes shown until I vote, check votes have arrived in main tl - await expect( - page - .locator(".mx_RoomView_body .mx_MPollBody_totalVotes") - .getByText("2 votes cast. Vote to see the results"), - ).toBeAttached(); - - // and thread view - await expect( - page.locator(".mx_ThreadView .mx_MPollBody_totalVotes").getByText("2 votes cast. Vote to see the results"), - ).toBeAttached(); - - // Take snapshots of poll on ThreadView - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); - await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='bubble']").first()).toBeVisible(); - await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("ThreadView_with_a_poll_on_bubble_layout.png", { - mask: [page.locator(".mx_MessageTimestamp")], - }); - - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group); - await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='group']").first()).toBeVisible(); - - await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("ThreadView_with_a_poll_on_group_layout.png", { - mask: [page.locator(".mx_MessageTimestamp")], - }); - - const roomViewLocator = page.locator(".mx_RoomView_body"); - // vote 'Maybe' in the main timeline poll - await getPollOption(page, pollId, pollParams.options[2], roomViewLocator).click(); - // both me and bob have voted Maybe - await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 2, roomViewLocator); - - const threadViewLocator = page.locator(".mx_ThreadView"); - // votes updated in thread view too - await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 2, threadViewLocator); - // change my vote to 'Yes' - await getPollOption(page, pollId, pollParams.options[0], threadViewLocator).click(); - - // Bob updates vote to 'No' - await botVoteForOption(page, bot, roomId, pollId, pollParams.options[1]); - - // me: yes, bob: no, charlie: no - const expectVoteCounts = async (optLocator: Locator) => { - // I voted yes - await expectPollOptionVoteCount(page, pollId, pollParams.options[0], 1, optLocator); - // Bob and Charlie voted no - await expectPollOptionVoteCount(page, pollId, pollParams.options[1], 2, optLocator); - // 0 for maybe - await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 0, optLocator); - }; - - // check counts are correct in main timeline tile - await expectVoteCounts(page.locator(".mx_RoomView_body")); - - // and in thread view tile - await expectVoteCounts(page.locator(".mx_ThreadView")); - }); + test( + "should be displayed correctly in thread panel", + { tag: "@screenshot" }, + async ({ page, app, user, bot, homeserver }) => { + const botCharlie = new Bot(page, homeserver, { displayName: "BotCharlie" }); + await botCharlie.prepareClient(); + + const roomId: string = await app.client.createRoom({}); + await app.client.inviteUser(roomId, bot.credentials.userId); + await app.client.inviteUser(roomId, botCharlie.credentials.userId); + await page.goto("/#/room/" + roomId); + + // wait until the bots joined + await expect(page.getByText("BotBob and one other were invited and joined")).toBeAttached({ + timeout: 10000, + }); + + const locator = await app.openMessageComposerOptions(); + await locator.getByRole("menuitem", { name: "Poll" }).click(); + + const pollParams = { + title: "Does the polls feature work?", + options: ["Yes", "No", "Maybe"], + }; + await createPoll(page, pollParams); + + // Wait for message to send, get its ID and save as @pollId + const pollId = await page + .locator(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]") + .filter({ hasText: pollParams.title }) + .getAttribute("data-scroll-tokens"); + + // Bob starts thread on the poll + await bot.sendMessage( + roomId, + { + body: "Hello there", + msgtype: "m.text", + }, + pollId, + ); + + // open the thread summary + await page.getByRole("button", { name: "Open thread" }).click(); + + // Bob votes 'Maybe' in the poll + await botVoteForOption(page, bot, roomId, pollId, pollParams.options[2]); + + // Charlie votes 'No' + await botVoteForOption(page, botCharlie, roomId, pollId, pollParams.options[1]); + + // no votes shown until I vote, check votes have arrived in main tl + await expect( + page + .locator(".mx_RoomView_body .mx_MPollBody_totalVotes") + .getByText("2 votes cast. Vote to see the results"), + ).toBeAttached(); + + // and thread view + await expect( + page + .locator(".mx_ThreadView .mx_MPollBody_totalVotes") + .getByText("2 votes cast. Vote to see the results"), + ).toBeAttached(); + + // Take snapshots of poll on ThreadView + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); + await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='bubble']").first()).toBeVisible(); + await expect(page.locator(".mx_ThreadView")).toMatchScreenshot( + "ThreadView_with_a_poll_on_bubble_layout.png", + { + mask: [page.locator(".mx_MessageTimestamp")], + }, + ); + + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group); + await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='group']").first()).toBeVisible(); + + await expect(page.locator(".mx_ThreadView")).toMatchScreenshot( + "ThreadView_with_a_poll_on_group_layout.png", + { + mask: [page.locator(".mx_MessageTimestamp")], + }, + ); + + const roomViewLocator = page.locator(".mx_RoomView_body"); + // vote 'Maybe' in the main timeline poll + await getPollOption(page, pollId, pollParams.options[2], roomViewLocator).click(); + // both me and bob have voted Maybe + await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 2, roomViewLocator); + + const threadViewLocator = page.locator(".mx_ThreadView"); + // votes updated in thread view too + await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 2, threadViewLocator); + // change my vote to 'Yes' + await getPollOption(page, pollId, pollParams.options[0], threadViewLocator).click(); + + // Bob updates vote to 'No' + await botVoteForOption(page, bot, roomId, pollId, pollParams.options[1]); + + // me: yes, bob: no, charlie: no + const expectVoteCounts = async (optLocator: Locator) => { + // I voted yes + await expectPollOptionVoteCount(page, pollId, pollParams.options[0], 1, optLocator); + // Bob and Charlie voted no + await expectPollOptionVoteCount(page, pollId, pollParams.options[1], 2, optLocator); + // 0 for maybe + await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 0, optLocator); + }; + + // check counts are correct in main timeline tile + await expectVoteCounts(page.locator(".mx_RoomView_body")); + + // and in thread view tile + await expectVoteCounts(page.locator(".mx_ThreadView")); + }, + ); }); diff --git a/playwright/e2e/read-receipts/editing-messages-in-threads.spec.ts b/playwright/e2e/read-receipts/editing-messages-in-threads.spec.ts index 277de628769..42191831c80 100644 --- a/playwright/e2e/read-receipts/editing-messages-in-threads.spec.ts +++ b/playwright/e2e/read-receipts/editing-messages-in-threads.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.describe("editing messages", () => { test.describe("in threads", () => { test("An edit of a threaded message makes the room unread", async ({ diff --git a/playwright/e2e/read-receipts/editing-messages-main-timeline.spec.ts b/playwright/e2e/read-receipts/editing-messages-main-timeline.spec.ts index 027ab08e2d2..a4648223050 100644 --- a/playwright/e2e/read-receipts/editing-messages-main-timeline.spec.ts +++ b/playwright/e2e/read-receipts/editing-messages-main-timeline.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.describe("editing messages", () => { test.describe("in the main timeline", () => { test("Editing a message leaves a room read", async ({ roomAlpha: room1, roomBeta: room2, util, msg }) => { diff --git a/playwright/e2e/read-receipts/editing-messages-thread-roots.spec.ts b/playwright/e2e/read-receipts/editing-messages-thread-roots.spec.ts index e653b5d9bd6..506ed603bda 100644 --- a/playwright/e2e/read-receipts/editing-messages-thread-roots.spec.ts +++ b/playwright/e2e/read-receipts/editing-messages-thread-roots.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.describe("editing messages", () => { test.describe("thread roots", () => { test("An edit of a thread root leaves the room read", async ({ diff --git a/playwright/e2e/read-receipts/high-level.spec.ts b/playwright/e2e/read-receipts/high-level.spec.ts index 7d4f4eb1330..457cf994814 100644 --- a/playwright/e2e/read-receipts/high-level.spec.ts +++ b/playwright/e2e/read-receipts/high-level.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { customEvent, many, test } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.describe("Ignored events", () => { test("If all events after receipt are unimportant, the room is read", async ({ roomAlpha: room1, diff --git a/playwright/e2e/read-receipts/message-ordering.spec.ts b/playwright/e2e/read-receipts/message-ordering.spec.ts index 65875cf4a9e..d7f77fae5f5 100644 --- a/playwright/e2e/read-receipts/message-ordering.spec.ts +++ b/playwright/e2e/read-receipts/message-ordering.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.describe("Message ordering", () => { test.describe("in the main timeline", () => { test.fixme( diff --git a/playwright/e2e/read-receipts/missing-referents.spec.ts b/playwright/e2e/read-receipts/missing-referents.spec.ts index f798d7d4552..a1741851e2c 100644 --- a/playwright/e2e/read-receipts/missing-referents.spec.ts +++ b/playwright/e2e/read-receipts/missing-referents.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.describe("messages with missing referents", () => { test.fixme( "A message in an unknown thread is not visible and the room is read", diff --git a/playwright/e2e/read-receipts/new-messages-in-threads.spec.ts b/playwright/e2e/read-receipts/new-messages-in-threads.spec.ts index 91d850fac89..5407f3cb44a 100644 --- a/playwright/e2e/read-receipts/new-messages-in-threads.spec.ts +++ b/playwright/e2e/read-receipts/new-messages-in-threads.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { many, test } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.describe("new messages", () => { test.describe("in threads", () => { test("Receiving a message makes a room unread", async ({ diff --git a/playwright/e2e/read-receipts/new-messages-main-timeline.spec.ts b/playwright/e2e/read-receipts/new-messages-main-timeline.spec.ts index 2000e444d64..92f7b10cdd7 100644 --- a/playwright/e2e/read-receipts/new-messages-main-timeline.spec.ts +++ b/playwright/e2e/read-receipts/new-messages-main-timeline.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { many, test } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.describe("new messages", () => { test.describe("in the main timeline", () => { test("Receiving a message makes a room unread", async ({ diff --git a/playwright/e2e/read-receipts/new-messages-thread-roots.spec.ts b/playwright/e2e/read-receipts/new-messages-thread-roots.spec.ts index 878d0d4419f..3c8ed7849f4 100644 --- a/playwright/e2e/read-receipts/new-messages-thread-roots.spec.ts +++ b/playwright/e2e/read-receipts/new-messages-thread-roots.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { many, test } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.describe("new messages", () => { test.describe("thread roots", () => { test("Reading a thread root does not mark the thread as read", async ({ diff --git a/playwright/e2e/read-receipts/notifications.spec.ts b/playwright/e2e/read-receipts/notifications.spec.ts index 3050987be77..46edc9a7a32 100644 --- a/playwright/e2e/read-receipts/notifications.spec.ts +++ b/playwright/e2e/read-receipts/notifications.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.describe("Notifications", () => { test.describe("in the main timeline", () => { test.fixme("A new message that mentions me shows a notification", () => {}); diff --git a/playwright/e2e/read-receipts/reactions-in-threads.spec.ts b/playwright/e2e/read-receipts/reactions-in-threads.spec.ts index bc4a1847441..45b5e071ecc 100644 --- a/playwright/e2e/read-receipts/reactions-in-threads.spec.ts +++ b/playwright/e2e/read-receipts/reactions-in-threads.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test, expect } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.describe("reactions", () => { test.describe("in threads", () => { test("A reaction to a threaded message does not make the room unread", async ({ diff --git a/playwright/e2e/read-receipts/reactions-main-timeline.spec.ts b/playwright/e2e/read-receipts/reactions-main-timeline.spec.ts index 59d6eaea400..16d5c92eca0 100644 --- a/playwright/e2e/read-receipts/reactions-main-timeline.spec.ts +++ b/playwright/e2e/read-receipts/reactions-main-timeline.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.describe("reactions", () => { test.describe("in the main timeline", () => { test("Receiving a reaction to a message does not make a room unread", async ({ diff --git a/playwright/e2e/read-receipts/reactions-thread-roots.spec.ts b/playwright/e2e/read-receipts/reactions-thread-roots.spec.ts index 219a73d5e46..817597a27e0 100644 --- a/playwright/e2e/read-receipts/reactions-thread-roots.spec.ts +++ b/playwright/e2e/read-receipts/reactions-thread-roots.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.describe("reactions", () => { test.describe("thread roots", () => { test("A reaction to a thread root does not make the room unread", async ({ diff --git a/playwright/e2e/read-receipts/read-receipts.spec.ts b/playwright/e2e/read-receipts/read-receipts.spec.ts index 3056cc4a54b..f6515361f23 100644 --- a/playwright/e2e/read-receipts/read-receipts.spec.ts +++ b/playwright/e2e/read-receipts/read-receipts.spec.ts @@ -13,7 +13,7 @@ import { ElementAppPage } from "../../pages/ElementAppPage"; import { Bot } from "../../pages/bot"; import { test } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.use({ displayName: "Mae", botCreateOpts: { displayName: "Other User" }, diff --git a/playwright/e2e/read-receipts/readme.md b/playwright/e2e/read-receipts/readme.md index 4e4dce297f5..33bcfeb93d7 100644 --- a/playwright/e2e/read-receipts/readme.md +++ b/playwright/e2e/read-receipts/readme.md @@ -2,19 +2,19 @@ Tips for writing these tests: -- Break up your tests into the smallest test case possible. The purpose of - these tests is to understand hard-to-find bugs, so small tests are necessary. - We know that Playwright recommends combining tests together for performance, but - that will frustrate our goals here. (We will need to find a different way to - reduce CI time.) +- Break up your tests into the smallest test case possible. The purpose of + these tests is to understand hard-to-find bugs, so small tests are necessary. + We know that Playwright recommends combining tests together for performance, but + that will frustrate our goals here. (We will need to find a different way to + reduce CI time.) -- Try to assert something after every action, to make sure it has completed. - E.g.: - markAsRead(room2); - assertRead(room2); - You should especially follow this rule if you are jumping to a different - room or similar straight afterward. +- Try to assert something after every action, to make sure it has completed. + E.g.: + markAsRead(room2); + assertRead(room2); + You should especially follow this rule if you are jumping to a different + room or similar straight afterward. -- Use assertStillRead() if you are asserting something is read when it was - also read before. This waits a little while to make sure you're not getting a - false positive. +- Use assertStillRead() if you are asserting something is read when it was + also read before. This waits a little while to make sure you're not getting a + false positive. diff --git a/playwright/e2e/read-receipts/redactions-in-threads.spec.ts b/playwright/e2e/read-receipts/redactions-in-threads.spec.ts index 715bb4e9fc8..25c19a4f975 100644 --- a/playwright/e2e/read-receipts/redactions-in-threads.spec.ts +++ b/playwright/e2e/read-receipts/redactions-in-threads.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.describe("redactions", () => { test.describe("in threads", () => { test("Redacting the threaded message pointed to by my receipt leaves the room read", async ({ diff --git a/playwright/e2e/read-receipts/redactions-main-timeline.spec.ts b/playwright/e2e/read-receipts/redactions-main-timeline.spec.ts index c1dceda6a00..143d9685d8f 100644 --- a/playwright/e2e/read-receipts/redactions-main-timeline.spec.ts +++ b/playwright/e2e/read-receipts/redactions-main-timeline.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.describe("redactions", () => { test.describe("in the main timeline", () => { test("Redacting the message pointed to by my receipt leaves the room read", async ({ diff --git a/playwright/e2e/read-receipts/redactions-thread-roots.spec.ts b/playwright/e2e/read-receipts/redactions-thread-roots.spec.ts index a8dc38c47ee..01f296075c9 100644 --- a/playwright/e2e/read-receipts/redactions-thread-roots.spec.ts +++ b/playwright/e2e/read-receipts/redactions-thread-roots.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.describe("redactions", () => { test.describe("thread roots", () => { test("Redacting a thread root after it was read leaves the room read", async ({ diff --git a/playwright/e2e/read-receipts/room-list-order.spec.ts b/playwright/e2e/read-receipts/room-list-order.spec.ts index 052e2d756a3..80dda202a3a 100644 --- a/playwright/e2e/read-receipts/room-list-order.spec.ts +++ b/playwright/e2e/read-receipts/room-list-order.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.describe("Room list order", () => { test("Rooms with unread messages appear at the top of room list if 'unread first' is selected", async ({ roomAlpha: room1, diff --git a/playwright/e2e/register/email.spec.ts b/playwright/e2e/register/email.spec.ts index 7a80f0bbf7a..665e20ef01f 100644 --- a/playwright/e2e/register/email.spec.ts +++ b/playwright/e2e/register/email.spec.ts @@ -38,34 +38,33 @@ test.describe("Email Registration", async () => { await page.goto("/#/register"); }); - test("registers an account and lands on the use case selection screen", async ({ - page, - mailhog, - request, - checkA11y, - }) => { - await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible(); - // Hide the server text as it contains the randomly allocated Homeserver port - const screenshotOptions = { mask: [page.locator(".mx_ServerPicker_server")] }; + test( + "registers an account and lands on the use case selection screen", + { tag: "@screenshot" }, + async ({ page, mailhog, request, checkA11y }) => { + await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible(); + // Hide the server text as it contains the randomly allocated Homeserver port + const screenshotOptions = { mask: [page.locator(".mx_ServerPicker_server")] }; - await page.getByRole("textbox", { name: "Username" }).fill("alice"); - await page.getByPlaceholder("Password", { exact: true }).fill("totally a great password"); - await page.getByPlaceholder("Confirm password").fill("totally a great password"); - await page.getByPlaceholder("Email").fill("alice@email.com"); - await page.getByRole("button", { name: "Register" }).click(); + await page.getByRole("textbox", { name: "Username" }).fill("alice"); + await page.getByPlaceholder("Password", { exact: true }).fill("totally a great password"); + await page.getByPlaceholder("Confirm password").fill("totally a great password"); + await page.getByPlaceholder("Email").fill("alice@email.com"); + await page.getByRole("button", { name: "Register" }).click(); - await expect(page.getByText("Check your email to continue")).toBeVisible(); - await expect(page).toMatchScreenshot("registration_check_your_email.png", screenshotOptions); - await checkA11y(); + await expect(page.getByText("Check your email to continue")).toBeVisible(); + await expect(page).toMatchScreenshot("registration_check_your_email.png", screenshotOptions); + await checkA11y(); - await expect(page.getByText("An error was encountered when sending the email")).not.toBeVisible(); + await expect(page.getByText("An error was encountered when sending the email")).not.toBeVisible(); - const messages = await mailhog.api.messages(); - expect(messages.items).toHaveLength(1); - expect(messages.items[0].to).toEqual("alice@email.com"); - const [emailLink] = messages.items[0].text.match(/http.+/); - await request.get(emailLink); // "Click" the link in the email + const messages = await mailhog.api.messages(); + expect(messages.items).toHaveLength(1); + expect(messages.items[0].to).toEqual("alice@email.com"); + const [emailLink] = messages.items[0].text.match(/http.+/); + await request.get(emailLink); // "Click" the link in the email - await expect(page.locator(".mx_UseCaseSelection_skip")).toBeVisible(); - }); + await expect(page.locator(".mx_UseCaseSelection_skip")).toBeVisible(); + }, + ); }); diff --git a/playwright/e2e/register/register.spec.ts b/playwright/e2e/register/register.spec.ts index 2dd3779573d..19608ee174d 100644 --- a/playwright/e2e/register/register.spec.ts +++ b/playwright/e2e/register/register.spec.ts @@ -15,66 +15,73 @@ test.describe("Registration", () => { await page.goto("/#/register"); }); - test("registers an account and lands on the home screen", async ({ homeserver, page, checkA11y, crypto }) => { - await page.getByRole("button", { name: "Edit", exact: true }).click(); - await expect(page.getByRole("button", { name: "Continue", exact: true })).toBeVisible(); - - await expect(page.locator(".mx_Dialog")).toMatchScreenshot("server-picker.png"); - await checkA11y(); - - await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl); - await page.getByRole("button", { name: "Continue", exact: true }).click(); - // wait for the dialog to go away - await expect(page.getByRole("dialog")).not.toBeVisible(); - - await expect(page.getByRole("textbox", { name: "Username", exact: true })).toBeVisible(); - // Hide the server text as it contains the randomly allocated Homeserver port - const screenshotOptions = { mask: [page.locator(".mx_ServerPicker_server")], includeDialogBackground: true }; - await expect(page).toMatchScreenshot("registration.png", screenshotOptions); - await checkA11y(); - - await page.getByRole("textbox", { name: "Username", exact: true }).fill("alice"); - await page.getByPlaceholder("Password", { exact: true }).fill("totally a great password"); - await page.getByPlaceholder("Confirm password", { exact: true }).fill("totally a great password"); - await page.getByRole("button", { name: "Register", exact: true }).click(); - - const dialog = page.getByRole("dialog"); - await expect(dialog).toBeVisible(); - await expect(page).toMatchScreenshot("email-prompt.png", screenshotOptions); - await checkA11y(); - await dialog.getByRole("button", { name: "Continue", exact: true }).click(); - - await expect(page.locator(".mx_InteractiveAuthEntryComponents_termsPolicy")).toBeVisible(); - await expect(page).toMatchScreenshot("terms-prompt.png", screenshotOptions); - await checkA11y(); - - const termsPolicy = page.locator(".mx_InteractiveAuthEntryComponents_termsPolicy"); - await termsPolicy.getByRole("checkbox").click(); // Click the checkbox before terms of service anchor link - await expect(termsPolicy.getByLabel("Privacy Policy")).toBeVisible(); - - await page.getByRole("button", { name: "Accept", exact: true }).click(); - - await expect(page.locator(".mx_UseCaseSelection_skip")).toBeVisible(); - await expect(page).toMatchScreenshot("use-case-selection.png", screenshotOptions); - await checkA11y(); - await page.getByRole("button", { name: "Skip", exact: true }).click(); - - await expect(page).toHaveURL(/\/#\/home$/); - - /* - * Cross-signing checks - */ - // check that the device considers itself verified - await page.getByRole("button", { name: "User menu", exact: true }).click(); - await page.getByRole("menuitem", { name: "All settings", exact: true }).click(); - await page.getByRole("tab", { name: "Sessions", exact: true }).click(); - await expect(page.getByTestId("current-session-section").getByTestId("device-metadata-isVerified")).toHaveText( - "Verified", - ); - - // check that cross-signing keys have been uploaded. - await crypto.assertDeviceIsCrossSigned(); - }); + test( + "registers an account and lands on the home screen", + { tag: "@screenshot" }, + async ({ homeserver, page, checkA11y, crypto }) => { + await page.getByRole("button", { name: "Edit", exact: true }).click(); + await expect(page.getByRole("button", { name: "Continue", exact: true })).toBeVisible(); + + await expect(page.locator(".mx_Dialog")).toMatchScreenshot("server-picker.png"); + await checkA11y(); + + await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl); + await page.getByRole("button", { name: "Continue", exact: true }).click(); + // wait for the dialog to go away + await expect(page.getByRole("dialog")).not.toBeVisible(); + + await expect(page.getByRole("textbox", { name: "Username", exact: true })).toBeVisible(); + // Hide the server text as it contains the randomly allocated Homeserver port + const screenshotOptions = { + mask: [page.locator(".mx_ServerPicker_server")], + includeDialogBackground: true, + }; + await expect(page).toMatchScreenshot("registration.png", screenshotOptions); + await checkA11y(); + + await page.getByRole("textbox", { name: "Username", exact: true }).fill("alice"); + await page.getByPlaceholder("Password", { exact: true }).fill("totally a great password"); + await page.getByPlaceholder("Confirm password", { exact: true }).fill("totally a great password"); + await page.getByRole("button", { name: "Register", exact: true }).click(); + + const dialog = page.getByRole("dialog"); + await expect(dialog).toBeVisible(); + await expect(page).toMatchScreenshot("email-prompt.png", screenshotOptions); + await checkA11y(); + await dialog.getByRole("button", { name: "Continue", exact: true }).click(); + + await expect(page.locator(".mx_InteractiveAuthEntryComponents_termsPolicy")).toBeVisible(); + await expect(page).toMatchScreenshot("terms-prompt.png", screenshotOptions); + await checkA11y(); + + const termsPolicy = page.locator(".mx_InteractiveAuthEntryComponents_termsPolicy"); + await termsPolicy.getByRole("checkbox").click(); // Click the checkbox before terms of service anchor link + await expect(termsPolicy.getByLabel("Privacy Policy")).toBeVisible(); + + await page.getByRole("button", { name: "Accept", exact: true }).click(); + + await expect(page.locator(".mx_UseCaseSelection_skip")).toBeVisible(); + await expect(page).toMatchScreenshot("use-case-selection.png", screenshotOptions); + await checkA11y(); + await page.getByRole("button", { name: "Skip", exact: true }).click(); + + await expect(page).toHaveURL(/\/#\/home$/); + + /* + * Cross-signing checks + */ + // check that the device considers itself verified + await page.getByRole("button", { name: "User menu", exact: true }).click(); + await page.getByRole("menuitem", { name: "All settings", exact: true }).click(); + await page.getByRole("tab", { name: "Sessions", exact: true }).click(); + await expect( + page.getByTestId("current-session-section").getByTestId("device-metadata-isVerified"), + ).toHaveText("Verified"); + + // check that cross-signing keys have been uploaded. + await crypto.assertDeviceIsCrossSigned(); + }, + ); test("should require username to fulfil requirements and be available", async ({ homeserver, page }) => { await page.getByRole("button", { name: "Edit", exact: true }).click(); diff --git a/playwright/e2e/release-announcement/releaseAnnouncement.spec.ts b/playwright/e2e/release-announcement/releaseAnnouncement.spec.ts index 57c27caf1a5..e18d72ddba9 100644 --- a/playwright/e2e/release-announcement/releaseAnnouncement.spec.ts +++ b/playwright/e2e/release-announcement/releaseAnnouncement.spec.ts @@ -18,7 +18,7 @@ test.describe("Release announcement", () => { labsFlags: ["threadsActivityCentre"], }); - test("should display the release announcement process", async ({ page, app, util }) => { + test("should display the release announcement process", { tag: "@screenshot" }, async ({ page, app, util }) => { // The TAC release announcement should be displayed await util.assertReleaseAnnouncementIsVisible("Threads Activity Centre"); // Hide the release announcement diff --git a/playwright/e2e/right-panel/file-panel.spec.ts b/playwright/e2e/right-panel/file-panel.spec.ts index 1cb39aad256..c535bcdfbb6 100644 --- a/playwright/e2e/right-panel/file-panel.spec.ts +++ b/playwright/e2e/right-panel/file-panel.spec.ts @@ -40,7 +40,7 @@ test.describe("FilePanel", () => { }); test.describe("render", () => { - test("should render empty state", async ({ page }) => { + test("should render empty state", { tag: "@screenshot" }, async ({ page }) => { // Wait until the information about the empty state is rendered await expect(page.locator(".mx_EmptyState")).toBeVisible(); @@ -48,7 +48,7 @@ test.describe("FilePanel", () => { await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("empty.png"); }); - test("should list tiles on the panel", async ({ page }) => { + test("should list tiles on the panel", { tag: "@screenshot" }, async ({ page }) => { // Upload multiple files await uploadFile(page, "playwright/sample-files/riot.png"); // Image await uploadFile(page, "playwright/sample-files/1sec.ogg"); // Audio diff --git a/playwright/e2e/right-panel/notification-panel.spec.ts b/playwright/e2e/right-panel/notification-panel.spec.ts index 1d10af97988..55a6be04505 100644 --- a/playwright/e2e/right-panel/notification-panel.spec.ts +++ b/playwright/e2e/right-panel/notification-panel.spec.ts @@ -21,7 +21,7 @@ test.describe("NotificationPanel", () => { await app.client.createRoom({ name: ROOM_NAME }); }); - test("should render empty state", async ({ page, app }) => { + test("should render empty state", { tag: "@screenshot" }, async ({ page, app }) => { await app.viewRoomByName(ROOM_NAME); await page.getByRole("button", { name: "Notifications" }).click(); diff --git a/playwright/e2e/right-panel/right-panel.spec.ts b/playwright/e2e/right-panel/right-panel.spec.ts index 110f24a5019..1e9b8ebe1d4 100644 --- a/playwright/e2e/right-panel/right-panel.spec.ts +++ b/playwright/e2e/right-panel/right-panel.spec.ts @@ -38,7 +38,7 @@ test.describe("RightPanel", () => { }); test.describe("in rooms", () => { - test("should handle long room address and long room name", async ({ page, app }) => { + test("should handle long room address and long room name", { tag: "@screenshot" }, async ({ page, app }) => { await app.client.createRoom({ name: ROOM_NAME_LONG }); await viewRoomSummaryByName(page, app, ROOM_NAME_LONG); diff --git a/playwright/e2e/room-directory/room-directory.spec.ts b/playwright/e2e/room-directory/room-directory.spec.ts index f078a858a2a..f299a929bb7 100644 --- a/playwright/e2e/room-directory/room-directory.spec.ts +++ b/playwright/e2e/room-directory/room-directory.spec.ts @@ -47,34 +47,40 @@ test.describe("Room Directory", () => { expect(resp.chunk[0].room_id).toEqual(roomId); }); - test("should allow finding published rooms in directory", async ({ page, app, user, bot }) => { - const name = "This is a public room"; - await bot.createRoom({ - visibility: "public" as Visibility, - name, - room_alias_name: "test1234", - }); - - await page.getByRole("button", { name: "Explore rooms" }).click(); - - const dialog = page.locator(".mx_SpotlightDialog"); - await dialog.getByRole("textbox", { name: "Search" }).fill("Unknown Room"); - await expect( - dialog.getByText("If you can't find the room you're looking for, ask for an invite or create a new room."), - ).toHaveClass("mx_SpotlightDialog_otherSearches_messageSearchText"); - - await expect(page.locator(".mx_Dialog")).toMatchScreenshot("filtered-no-results.png"); - - await dialog.getByRole("textbox", { name: "Search" }).fill("test1234"); - await expect(dialog.getByText(name)).toHaveClass("mx_SpotlightDialog_result_publicRoomName"); - - await expect(page.locator(".mx_Dialog")).toMatchScreenshot("filtered-one-result.png"); - - await page - .locator(".mx_SpotlightDialog .mx_SpotlightDialog_option") - .getByRole("button", { name: "Join" }) - .click(); - - await expect(page).toHaveURL("/#/room/#test1234:localhost"); - }); + test( + "should allow finding published rooms in directory", + { tag: "@screenshot" }, + async ({ page, app, user, bot }) => { + const name = "This is a public room"; + await bot.createRoom({ + visibility: "public" as Visibility, + name, + room_alias_name: "test1234", + }); + + await page.getByRole("button", { name: "Explore rooms" }).click(); + + const dialog = page.locator(".mx_SpotlightDialog"); + await dialog.getByRole("textbox", { name: "Search" }).fill("Unknown Room"); + await expect( + dialog.getByText( + "If you can't find the room you're looking for, ask for an invite or create a new room.", + ), + ).toHaveClass("mx_SpotlightDialog_otherSearches_messageSearchText"); + + await expect(page.locator(".mx_Dialog")).toMatchScreenshot("filtered-no-results.png"); + + await dialog.getByRole("textbox", { name: "Search" }).fill("test1234"); + await expect(dialog.getByText(name)).toHaveClass("mx_SpotlightDialog_result_publicRoomName"); + + await expect(page.locator(".mx_Dialog")).toMatchScreenshot("filtered-one-result.png"); + + await page + .locator(".mx_SpotlightDialog .mx_SpotlightDialog_option") + .getByRole("button", { name: "Join" }) + .click(); + + await expect(page).toHaveURL("/#/room/#test1234:localhost"); + }, + ); }); diff --git a/playwright/e2e/room/room-header.spec.ts b/playwright/e2e/room/room-header.spec.ts index 6ecf4b3b33b..971508b25bb 100644 --- a/playwright/e2e/room/room-header.spec.ts +++ b/playwright/e2e/room/room-header.spec.ts @@ -20,7 +20,7 @@ test.describe("Room Header", () => { test.use({ labsFlags: ["feature_notifications"], }); - test("should render default buttons properly", async ({ page, app, user }) => { + test("should render default buttons properly", { tag: "@screenshot" }, async ({ page, app, user }) => { await app.client.createRoom({ name: "Test Room" }); await app.viewRoomByName("Test Room"); @@ -51,34 +51,38 @@ test.describe("Room Header", () => { await expect(header).toMatchScreenshot("room-header.png"); }); - test("should render a very long room name without collapsing the buttons", async ({ page, app, user }) => { - const LONG_ROOM_NAME = - "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore " + - "et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut " + - "aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum " + - "dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui " + - "officia deserunt mollit anim id est laborum."; + test( + "should render a very long room name without collapsing the buttons", + { tag: "@screenshot" }, + async ({ page, app, user }) => { + const LONG_ROOM_NAME = + "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore " + + "et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut " + + "aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum " + + "dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui " + + "officia deserunt mollit anim id est laborum."; - await app.client.createRoom({ name: LONG_ROOM_NAME }); - await app.viewRoomByName(LONG_ROOM_NAME); + await app.client.createRoom({ name: LONG_ROOM_NAME }); + await app.viewRoomByName(LONG_ROOM_NAME); - const header = page.locator(".mx_RoomHeader"); - // Wait until the room name is set - await expect(page.locator(".mx_RoomHeader_heading").getByText(LONG_ROOM_NAME)).toBeVisible(); - - // Assert the size of buttons on RoomHeader are specified and the buttons are not compressed - // Note these assertions do not check the size of mx_LegacyRoomHeader_name button - const buttons = header.locator(".mx_Flex").getByRole("button"); - await expect(buttons).toHaveCount(5); - - for (const button of await buttons.all()) { - await expect(button).toBeVisible(); - await expect(button).toHaveCSS("height", "32px"); - await expect(button).toHaveCSS("width", "32px"); - } - - await expect(header).toMatchScreenshot("room-header-long-name.png"); - }); + const header = page.locator(".mx_RoomHeader"); + // Wait until the room name is set + await expect(page.locator(".mx_RoomHeader_heading").getByText(LONG_ROOM_NAME)).toBeVisible(); + + // Assert the size of buttons on RoomHeader are specified and the buttons are not compressed + // Note these assertions do not check the size of mx_LegacyRoomHeader_name button + const buttons = header.locator(".mx_Flex").getByRole("button"); + await expect(buttons).toHaveCount(5); + + for (const button of await buttons.all()) { + await expect(button).toBeVisible(); + await expect(button).toHaveCSS("height", "32px"); + await expect(button).toHaveCSS("width", "32px"); + } + + await expect(header).toMatchScreenshot("room-header-long-name.png"); + }, + ); }); test.describe("with a video room", () => { @@ -99,30 +103,34 @@ test.describe("Room Header", () => { test.describe("and with feature_notifications enabled", () => { test.use({ labsFlags: ["feature_video_rooms", "feature_notifications"] }); - test("should render buttons for chat, room info, threads and facepile", async ({ page, app, user }) => { - await createVideoRoom(page, app); + test( + "should render buttons for chat, room info, threads and facepile", + { tag: "@screenshot" }, + async ({ page, app, user }) => { + await createVideoRoom(page, app); - const header = page.locator(".mx_RoomHeader"); + const header = page.locator(".mx_RoomHeader"); - // There's two room info button - the header itself and the i button - const infoButtons = header.getByRole("button", { name: "Room info" }); - await expect(infoButtons).toHaveCount(2); - await expect(infoButtons.first()).toBeVisible(); - await expect(infoButtons.last()).toBeVisible(); + // There's two room info button - the header itself and the i button + const infoButtons = header.getByRole("button", { name: "Room info" }); + await expect(infoButtons).toHaveCount(2); + await expect(infoButtons.first()).toBeVisible(); + await expect(infoButtons.last()).toBeVisible(); - // Facepile - await expect(header.locator(".mx_FacePile")).toBeVisible(); + // Facepile + await expect(header.locator(".mx_FacePile")).toBeVisible(); - // Chat, Threads and Notification buttons - await expect(header.getByRole("button", { name: "Chat" })).toBeVisible(); - await expect(header.getByRole("button", { name: "Threads" })).toBeVisible(); - await expect(header.getByRole("button", { name: "Notifications" })).toBeVisible(); + // Chat, Threads and Notification buttons + await expect(header.getByRole("button", { name: "Chat" })).toBeVisible(); + await expect(header.getByRole("button", { name: "Threads" })).toBeVisible(); + await expect(header.getByRole("button", { name: "Notifications" })).toBeVisible(); - // Assert that there is not a button except those buttons - await expect(header.getByRole("button")).toHaveCount(7); + // Assert that there is not a button except those buttons + await expect(header.getByRole("button")).toHaveCount(7); - await expect(header).toMatchScreenshot("room-header-video-room.png"); - }); + await expect(header).toMatchScreenshot("room-header-video-room.png"); + }, + ); }); test("should render a working chat button which opens the timeline on a right panel", async ({ diff --git a/playwright/e2e/settings/account-user-settings-tab.spec.ts b/playwright/e2e/settings/account-user-settings-tab.spec.ts index 5492094f937..7390ccfd8d1 100644 --- a/playwright/e2e/settings/account-user-settings-tab.spec.ts +++ b/playwright/e2e/settings/account-user-settings-tab.spec.ts @@ -23,7 +23,7 @@ test.describe("Account user settings tab", () => { }, }); - test("should be rendered properly", async ({ uut, user }) => { + test("should be rendered properly", { tag: "@screenshot" }, async ({ uut, user }) => { await expect(uut).toMatchScreenshot("account.png"); // Assert that the top heading is rendered @@ -71,7 +71,7 @@ test.describe("Account user settings tab", () => { ); }); - test("should respond to small screen sizes", async ({ page, uut }) => { + test("should respond to small screen sizes", { tag: "@screenshot" }, async ({ page, uut }) => { await page.setViewportSize({ width: 700, height: 600 }); await expect(uut).toMatchScreenshot("account-smallscreen.png"); }); diff --git a/playwright/e2e/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts b/playwright/e2e/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts index de6c9c527a6..c60ecb99d25 100644 --- a/playwright/e2e/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts +++ b/playwright/e2e/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts @@ -13,7 +13,7 @@ test.describe("Appearance user settings tab", () => { displayName: "Hanako", }); - test("should be rendered properly", async ({ page, user, app }) => { + test("should be rendered properly", { tag: "@screenshot" }, async ({ page, user, app }) => { const tab = await app.settings.openUserSettings("Appearance"); // Click "Show advanced" link button @@ -25,19 +25,23 @@ test.describe("Appearance user settings tab", () => { await expect(tab).toMatchScreenshot("appearance-tab.png"); }); - test("should support changing font size by using the font size dropdown", async ({ page, app, user }) => { - await app.settings.openUserSettings("Appearance"); + test( + "should support changing font size by using the font size dropdown", + { tag: "@screenshot" }, + async ({ page, app, user }) => { + await app.settings.openUserSettings("Appearance"); - const tab = page.getByTestId("mx_AppearanceUserSettingsTab"); - const fontDropdown = tab.locator(".mx_FontScalingPanel_Dropdown"); - await expect(fontDropdown.getByLabel("Font size")).toBeVisible(); + const tab = page.getByTestId("mx_AppearanceUserSettingsTab"); + const fontDropdown = tab.locator(".mx_FontScalingPanel_Dropdown"); + await expect(fontDropdown.getByLabel("Font size")).toBeVisible(); - // Default browser font size is 16px and the select value is 0 - // -4 value is 12px - await fontDropdown.getByLabel("Font size").selectOption({ value: "-4" }); + // Default browser font size is 16px and the select value is 0 + // -4 value is 12px + await fontDropdown.getByLabel("Font size").selectOption({ value: "-4" }); - await expect(page).toMatchScreenshot("window-12px.png", { includeDialogBackground: true }); - }); + await expect(page).toMatchScreenshot("window-12px.png", { includeDialogBackground: true }); + }, + ); test("should support enabling system font", async ({ page, app, user }) => { await app.settings.openUserSettings("Appearance"); diff --git a/playwright/e2e/settings/appearance-user-settings-tab/message-layout-panel.ts b/playwright/e2e/settings/appearance-user-settings-tab/message-layout-panel.ts index a0288baf1dd..157942a5853 100644 --- a/playwright/e2e/settings/appearance-user-settings-tab/message-layout-panel.ts +++ b/playwright/e2e/settings/appearance-user-settings-tab/message-layout-panel.ts @@ -20,20 +20,24 @@ test.describe("Appearance user settings tab", () => { await util.openAppearanceTab(); }); - test("should change the message layout from modern to bubble", async ({ page, app, user, util }) => { - await util.assertScreenshot(util.getMessageLayoutPanel(), "message-layout-panel-modern.png"); - - await util.getBubbleLayout().click(); - - // Assert that modern are irc layout are not selected - await expect(util.getBubbleLayout()).toBeChecked(); - await expect(util.getModernLayout()).not.toBeChecked(); - await expect(util.getIRCLayout()).not.toBeChecked(); - - // Assert that the room layout is set to bubble layout - await util.assertBubbleLayout(); - await util.assertScreenshot(util.getMessageLayoutPanel(), "message-layout-panel-bubble.png"); - }); + test( + "should change the message layout from modern to bubble", + { tag: "@screenshot" }, + async ({ page, app, user, util }) => { + await util.assertScreenshot(util.getMessageLayoutPanel(), "message-layout-panel-modern.png"); + + await util.getBubbleLayout().click(); + + // Assert that modern are irc layout are not selected + await expect(util.getBubbleLayout()).toBeChecked(); + await expect(util.getModernLayout()).not.toBeChecked(); + await expect(util.getIRCLayout()).not.toBeChecked(); + + // Assert that the room layout is set to bubble layout + await util.assertBubbleLayout(); + await util.assertScreenshot(util.getMessageLayoutPanel(), "message-layout-panel-bubble.png"); + }, + ); test("should enable compact layout when the modern layout is selected", async ({ page, app, user, util }) => { await expect(util.getCompactLayoutCheckbox()).not.toBeChecked(); diff --git a/playwright/e2e/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts b/playwright/e2e/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts index 4f3b75b5baa..63b53caa23e 100644 --- a/playwright/e2e/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts +++ b/playwright/e2e/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts @@ -20,31 +20,39 @@ test.describe("Appearance user settings tab", () => { await util.openAppearanceTab(); }); - test("should be rendered with the light theme selected", async ({ page, app, util }) => { - // Assert that 'Match system theme' is not checked - await expect(util.getMatchSystemThemeCheckbox()).not.toBeChecked(); - - // Assert that the light theme is selected - await expect(util.getLightTheme()).toBeChecked(); - // Assert that the dark and high contrast themes are not selected - await expect(util.getDarkTheme()).not.toBeChecked(); - await expect(util.getHighContrastTheme()).not.toBeChecked(); - - await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-light.png"); - }); - - test("should disable the themes when the system theme is clicked", async ({ page, app, util }) => { - await util.getMatchSystemThemeCheckbox().click(); - - // Assert that the themes are disabled - await expect(util.getLightTheme()).toBeDisabled(); - await expect(util.getDarkTheme()).toBeDisabled(); - await expect(util.getHighContrastTheme()).toBeDisabled(); - - await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-match-system-enabled.png"); - }); - - test("should change the theme to dark", async ({ page, app, util }) => { + test( + "should be rendered with the light theme selected", + { tag: "@screenshot" }, + async ({ page, app, util }) => { + // Assert that 'Match system theme' is not checked + await expect(util.getMatchSystemThemeCheckbox()).not.toBeChecked(); + + // Assert that the light theme is selected + await expect(util.getLightTheme()).toBeChecked(); + // Assert that the dark and high contrast themes are not selected + await expect(util.getDarkTheme()).not.toBeChecked(); + await expect(util.getHighContrastTheme()).not.toBeChecked(); + + await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-light.png"); + }, + ); + + test( + "should disable the themes when the system theme is clicked", + { tag: "@screenshot" }, + async ({ page, app, util }) => { + await util.getMatchSystemThemeCheckbox().click(); + + // Assert that the themes are disabled + await expect(util.getLightTheme()).toBeDisabled(); + await expect(util.getDarkTheme()).toBeDisabled(); + await expect(util.getHighContrastTheme()).toBeDisabled(); + + await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-match-system-enabled.png"); + }, + ); + + test("should change the theme to dark", { tag: "@screenshot" }, async ({ page, app, util }) => { // Assert that the light theme is selected await expect(util.getLightTheme()).toBeChecked(); @@ -63,19 +71,23 @@ test.describe("Appearance user settings tab", () => { labsFlags: ["feature_custom_themes"], }); - test("should render the custom theme section", async ({ page, app, util }) => { + test("should render the custom theme section", { tag: "@screenshot" }, async ({ page, app, util }) => { await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme.png"); }); - test("should be able to add and remove a custom theme", async ({ page, app, util }) => { - await util.addCustomTheme(); + test( + "should be able to add and remove a custom theme", + { tag: "@screenshot" }, + async ({ page, app, util }) => { + await util.addCustomTheme(); - await expect(util.getCustomTheme()).not.toBeChecked(); - await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme-added.png"); + await expect(util.getCustomTheme()).not.toBeChecked(); + await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme-added.png"); - await util.removeCustomTheme(); - await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme-removed.png"); - }); + await util.removeCustomTheme(); + await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme-removed.png"); + }, + ); }); }); }); diff --git a/playwright/e2e/settings/general-room-settings-tab.spec.ts b/playwright/e2e/settings/general-room-settings-tab.spec.ts index 47582bf0c0e..828ba5285bb 100644 --- a/playwright/e2e/settings/general-room-settings-tab.spec.ts +++ b/playwright/e2e/settings/general-room-settings-tab.spec.ts @@ -20,7 +20,7 @@ test.describe("General room settings tab", () => { await app.viewRoomByName(roomName); }); - test("should be rendered properly", async ({ page, app }) => { + test("should be rendered properly", { tag: "@screenshot" }, async ({ page, app }) => { const settings = await app.settings.openRoomSettings("General"); // Assert that "Show less" details element is rendered diff --git a/playwright/e2e/settings/preferences-user-settings-tab.spec.ts b/playwright/e2e/settings/preferences-user-settings-tab.spec.ts index 0880853ee81..8dc2570b426 100644 --- a/playwright/e2e/settings/preferences-user-settings-tab.spec.ts +++ b/playwright/e2e/settings/preferences-user-settings-tab.spec.ts @@ -23,7 +23,7 @@ test.describe("Preferences user settings tab", () => { }, }); - test("should be rendered properly", async ({ app, page, user }) => { + test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user }) => { page.setViewportSize({ width: 1024, height: 3300 }); const tab = await app.settings.openUserSettings("Preferences"); // Assert that the top heading is rendered diff --git a/playwright/e2e/settings/security-user-settings-tab.spec.ts b/playwright/e2e/settings/security-user-settings-tab.spec.ts index 6eab8306232..e7562698c33 100644 --- a/playwright/e2e/settings/security-user-settings-tab.spec.ts +++ b/playwright/e2e/settings/security-user-settings-tab.spec.ts @@ -36,7 +36,7 @@ test.describe("Security user settings tab", () => { }); test.describe("AnalyticsLearnMoreDialog", () => { - test("should be rendered properly", async ({ app, page }) => { + test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page }) => { const tab = await app.settings.openUserSettings("Security"); await tab.getByRole("button", { name: "Learn more" }).click(); await expect(page.locator(".mx_AnalyticsLearnMoreDialog_wrapper .mx_Dialog")).toMatchScreenshot( diff --git a/playwright/e2e/share-dialog/share-dialog.spec.ts b/playwright/e2e/share-dialog/share-dialog.spec.ts new file mode 100644 index 00000000000..e0993dd1bc4 --- /dev/null +++ b/playwright/e2e/share-dialog/share-dialog.spec.ts @@ -0,0 +1,67 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only + * Please see LICENSE files in the repository root for full details. + */ + +import { test, expect } from "../../element-web-test"; + +test.describe("Share dialog", () => { + test.use({ + displayName: "Alice", + room: async ({ app, user, bot }, use) => { + const roomId = await app.client.createRoom({ name: "Alice room" }); + await use({ roomId }); + }, + }); + + test("should share a room", { tag: "@screenshot" }, async ({ page, app, room }) => { + await app.viewRoomById(room.roomId); + await app.toggleRoomInfoPanel(); + await page.getByRole("menuitem", { name: "Copy link" }).click(); + + const dialog = page.getByRole("dialog", { name: "Share room" }); + await expect(dialog.getByText(`https://matrix.to/#/${room.roomId}`)).toBeVisible(); + expect(dialog).toMatchScreenshot("share-dialog-room.png", { + // QRCode and url changes at every run + mask: [page.locator(".mx_QRCode"), page.locator(".mx_ShareDialog_top > span")], + }); + }); + + test("should share a room member", { tag: "@screenshot" }, async ({ page, app, room, user }) => { + await app.viewRoomById(room.roomId); + await app.client.sendMessage(room.roomId, { body: "hello", msgtype: "m.text" }); + + const rightPanel = await app.toggleRoomInfoPanel(); + await rightPanel.getByRole("menuitem", { name: "People" }).click(); + await rightPanel.getByRole("button", { name: `${user.userId} (power 100)` }).click(); + await rightPanel.getByRole("button", { name: "Share profile" }).click(); + + const dialog = page.getByRole("dialog", { name: "Share User" }); + await expect(dialog.getByText(`https://matrix.to/#/${user.userId}`)).toBeVisible(); + expect(dialog).toMatchScreenshot("share-dialog-user.png", { + // QRCode changes at every run + mask: [page.locator(".mx_QRCode")], + }); + }); + + test("should share an event", { tag: "@screenshot" }, async ({ page, app, room }) => { + await app.viewRoomById(room.roomId); + await app.client.sendMessage(room.roomId, { body: "hello", msgtype: "m.text" }); + + const timelineMessage = page.locator(".mx_MTextBody", { hasText: "hello" }); + await timelineMessage.hover(); + await page.getByRole("button", { name: "Options", exact: true }).click(); + await page.getByRole("menuitem", { name: "Share" }).click(); + + const dialog = page.getByRole("dialog", { name: "Share Room Message" }); + await expect(dialog.getByRole("checkbox", { name: "Link to selected message" })).toBeChecked(); + expect(dialog).toMatchScreenshot("share-dialog-event.png", { + // QRCode and url changes at every run + mask: [page.locator(".mx_QRCode"), page.locator(".mx_ShareDialog_top > span")], + }); + await dialog.getByRole("checkbox", { name: "Link to selected message" }).click(); + await expect(dialog.getByRole("checkbox", { name: "Link to selected message" })).not.toBeChecked(); + }); +}); diff --git a/playwright/e2e/spaces/spaces.spec.ts b/playwright/e2e/spaces/spaces.spec.ts index 575450c6414..233cdee3b4b 100644 --- a/playwright/e2e/spaces/spaces.spec.ts +++ b/playwright/e2e/spaces/spaces.spec.ts @@ -55,7 +55,7 @@ test.describe("Spaces", () => { botCreateOpts: { displayName: "BotBob" }, }); - test("should allow user to create public space", async ({ page, app, user }) => { + test("should allow user to create public space", { tag: "@screenshot" }, async ({ page, app, user }) => { const contextMenu = await openSpaceCreateMenu(page); await expect(contextMenu).toMatchScreenshot("space-create-menu.png"); @@ -88,7 +88,7 @@ test.describe("Spaces", () => { await expect(page.getByRole("treeitem", { name: "Jokes" })).toBeVisible(); }); - test("should allow user to create private space", async ({ page, app, user }) => { + test("should allow user to create private space", { tag: "@screenshot" }, async ({ page, app, user }) => { const menu = await openSpaceCreateMenu(page); await menu.getByRole("button", { name: "Private" }).click(); @@ -216,49 +216,47 @@ test.describe("Spaces", () => { await expect(hierarchyList.getByRole("treeitem", { name: "Gaming" }).getByRole("button")).toBeVisible(); }); - test("should render subspaces in the space panel only when expanded", async ({ - page, - app, - user, - axe, - checkA11y, - }) => { - axe.disableRules([ - // Disable this check as it triggers on nested roving tab index elements which are in practice fine - "nested-interactive", - // XXX: We have some known contrast issues here - "color-contrast", - ]); - - const childSpaceId = await app.client.createSpace({ - name: "Child Space", - initial_state: [], - }); - await app.client.createSpace({ - name: "Root Space", - initial_state: [spaceChildInitialState(childSpaceId)], - }); - - // Find collapsed Space panel - const spaceTree = page.getByRole("tree", { name: "Spaces" }); - await expect(spaceTree.getByRole("button", { name: "Root Space" })).toBeVisible(); - await expect(spaceTree.getByRole("button", { name: "Child Space" })).not.toBeVisible(); - - await checkA11y(); - await expect(page.locator(".mx_SpacePanel")).toMatchScreenshot("space-panel-collapsed.png"); - - // This finds the expand button with the class name "mx_SpaceButton_toggleCollapse". Note there is another - // button with the same name with different class name "mx_SpacePanel_toggleCollapse". - await spaceTree.getByRole("button", { name: "Expand" }).click(); - await expect(page.locator(".mx_SpacePanel:not(.collapsed)")).toBeVisible(); // TODO: replace :not() selector - - const item = page.locator(".mx_SpaceItem", { hasText: "Root Space" }); - await expect(item).toBeVisible(); - await expect(item.locator(".mx_SpaceItem", { hasText: "Child Space" })).toBeVisible(); - - await checkA11y(); - await expect(page.locator(".mx_SpacePanel")).toMatchScreenshot("space-panel-expanded.png"); - }); + test( + "should render subspaces in the space panel only when expanded", + { tag: "@screenshot" }, + async ({ page, app, user, axe, checkA11y }) => { + axe.disableRules([ + // Disable this check as it triggers on nested roving tab index elements which are in practice fine + "nested-interactive", + // XXX: We have some known contrast issues here + "color-contrast", + ]); + + const childSpaceId = await app.client.createSpace({ + name: "Child Space", + initial_state: [], + }); + await app.client.createSpace({ + name: "Root Space", + initial_state: [spaceChildInitialState(childSpaceId)], + }); + + // Find collapsed Space panel + const spaceTree = page.getByRole("tree", { name: "Spaces" }); + await expect(spaceTree.getByRole("button", { name: "Root Space" })).toBeVisible(); + await expect(spaceTree.getByRole("button", { name: "Child Space" })).not.toBeVisible(); + + await checkA11y(); + await expect(page.locator(".mx_SpacePanel")).toMatchScreenshot("space-panel-collapsed.png"); + + // This finds the expand button with the class name "mx_SpaceButton_toggleCollapse". Note there is another + // button with the same name with different class name "mx_SpacePanel_toggleCollapse". + await spaceTree.getByRole("button", { name: "Expand" }).click(); + await expect(page.locator(".mx_SpacePanel:not(.collapsed)")).toBeVisible(); // TODO: replace :not() selector + + const item = page.locator(".mx_SpaceItem", { hasText: "Root Space" }); + await expect(item).toBeVisible(); + await expect(item.locator(".mx_SpaceItem", { hasText: "Child Space" })).toBeVisible(); + + await checkA11y(); + await expect(page.locator(".mx_SpacePanel")).toMatchScreenshot("space-panel-expanded.png"); + }, + ); test("should not soft crash when joining a room from space hierarchy which has a link in its topic", async ({ page, diff --git a/playwright/e2e/spaces/threads-activity-centre/index.ts b/playwright/e2e/spaces/threads-activity-centre/index.ts index f6ac7f95d69..b2b8473640e 100644 --- a/playwright/e2e/spaces/threads-activity-centre/index.ts +++ b/playwright/e2e/spaces/threads-activity-centre/index.ts @@ -276,7 +276,7 @@ export class Helpers { * Assert that the threads activity centre button has no indicator */ async assertNoTacIndicator() { - // Assert by checkng neither of the known indicators are visible first. This will wait + // Assert by checking neither of the known indicators are visible first. This will wait // if it takes a little time to disappear, but the screenshot comparison won't. await expect(this.getTacButton().locator("[data-indicator='success']")).not.toBeVisible(); await expect(this.getTacButton().locator("[data-indicator='critical']")).not.toBeVisible(); @@ -376,7 +376,7 @@ export class Helpers { * Clicks the button to mark all threads as read in the current room */ clickMarkAllThreadsRead() { - return this.page.getByLabel("Mark all as read").click(); + return this.page.locator("#thread-panel").getByRole("button", { name: "Mark all as read" }).click(); } } diff --git a/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts b/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts index fc66c13520d..ecf458c0600 100644 --- a/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts +++ b/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts @@ -16,16 +16,18 @@ test.describe("Threads Activity Centre", () => { labsFlags: ["threadsActivityCentre"], }); - test("should have the button correctly aligned and displayed in the space panel when expanded", async ({ - util, - }) => { - // Open the space panel - await util.expandSpacePanel(); - // The buttons in the space panel should be aligned when expanded - await expect(util.getSpacePanel()).toMatchScreenshot("tac-button-expanded.png"); - }); - - test("should not show indicator when there is no thread", async ({ room1, util }) => { + test( + "should have the button correctly aligned and displayed in the space panel when expanded", + { tag: "@screenshot" }, + async ({ util }) => { + // Open the space panel + await util.expandSpacePanel(); + // The buttons in the space panel should be aligned when expanded + await expect(util.getSpacePanel()).toMatchScreenshot("tac-button-expanded.png"); + }, + ); + + test("should not show indicator when there is no thread", { tag: "@screenshot" }, async ({ room1, util }) => { // No indicator should be shown await util.assertNoTacIndicator(); @@ -62,7 +64,7 @@ test.describe("Threads Activity Centre", () => { await util.assertHighlightIndicator(); }); - test("should show the rooms with unread threads", async ({ room1, room2, util, msg }) => { + test("should show the rooms with unread threads", { tag: "@screenshot" }, async ({ room1, room2, util, msg }) => { await util.goTo(room2); await util.populateThreads(room1, room2, msg); // The indicator should be shown @@ -79,7 +81,7 @@ test.describe("Threads Activity Centre", () => { await expect(util.getTacPanel()).toMatchScreenshot("tac-panel-mix-unread.png"); }); - test("should update with a thread is read", async ({ room1, room2, util, msg }) => { + test("should update with a thread is read", { tag: "@screenshot" }, async ({ room1, room2, util, msg }) => { await util.goTo(room2); await util.populateThreads(room1, room2, msg); @@ -128,7 +130,7 @@ test.describe("Threads Activity Centre", () => { await expect(page.locator(".mx_SpotlightDialog")).not.toBeVisible(); }); - test("should have the correct hover state", async ({ util, page }) => { + test("should have the correct hover state", { tag: "@screenshot" }, async ({ util, page }) => { await util.hoverTacButton(); await expect(util.getSpacePanel()).toMatchScreenshot("tac-hovered.png"); @@ -138,7 +140,7 @@ test.describe("Threads Activity Centre", () => { await expect(util.getSpacePanel()).toMatchScreenshot("tac-hovered-expanded.png"); }); - test("should mark all threads as read", async ({ room1, room2, util, msg, page }) => { + test("should mark all threads as read", { tag: "@screenshot" }, async ({ room1, room2, util, msg, page }) => { await util.receiveMessages(room1, ["Msg1", msg.threadedOff("Msg1", "Resp1")]); await util.assertNotificationTac(); @@ -146,7 +148,7 @@ test.describe("Threads Activity Centre", () => { await util.openTac(); await util.clickRoomInTac(room1.name); - util.clickMarkAllThreadsRead(); + await util.clickMarkAllThreadsRead(); await util.assertNoTacIndicator(); }); diff --git a/playwright/e2e/threads/threads.spec.ts b/playwright/e2e/threads/threads.spec.ts index a2642a49d16..06ec57653c7 100644 --- a/playwright/e2e/threads/threads.spec.ts +++ b/playwright/e2e/threads/threads.spec.ts @@ -25,7 +25,7 @@ test.describe("Threads", () => { }); // Flaky: https://github.com/vector-im/element-web/issues/26452 - test.skip("should be usable for a conversation", async ({ page, app, bot }) => { + test.skip("should be usable for a conversation", { tag: "@screenshot" }, async ({ page, app, bot }) => { const roomId = await app.client.createRoom({}); await app.client.inviteUser(roomId, bot.credentials.userId); await bot.joinRoom(roomId); @@ -150,7 +150,7 @@ test.describe("Threads", () => { ).toHaveCSS("padding-inline-start", ThreadViewGroupSpacingStart); // Take snapshot of group layout (IRC layout is not available on ThreadView) - expect(page.locator(".mx_ThreadView")).toMatchScreenshot( + await expect(page.locator(".mx_ThreadView")).toMatchScreenshot( "ThreadView_with_reaction_and_a_hidden_event_on_group_layout.png", { mask: mask, @@ -174,7 +174,7 @@ test.describe("Threads", () => { .toHaveCSS("margin-inline-start", "0px"); // Take snapshot of bubble layout - expect(page.locator(".mx_ThreadView")).toMatchScreenshot( + await expect(page.locator(".mx_ThreadView")).toMatchScreenshot( "ThreadView_with_reaction_and_a_hidden_event_on_bubble_layout.png", { mask: mask, @@ -351,57 +351,61 @@ test.describe("Threads", () => { }); }); - test("should send location and reply to the location on ThreadView", async ({ page, app, bot }) => { - const roomId = await app.client.createRoom({}); - await app.client.inviteUser(roomId, bot.credentials.userId); - await bot.joinRoom(roomId); - await page.goto("/#/room/" + roomId); - - // Exclude timestamp, read marker, and maplibregl-map from snapshots - const css = - ".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker, .maplibregl-map { visibility: hidden !important; }"; + test( + "should send location and reply to the location on ThreadView", + { tag: "@screenshot" }, + async ({ page, app, bot }) => { + const roomId = await app.client.createRoom({}); + await app.client.inviteUser(roomId, bot.credentials.userId); + await bot.joinRoom(roomId); + await page.goto("/#/room/" + roomId); - let locator = page.locator(".mx_RoomView_body"); - // User sends message - let textbox = locator.getByRole("textbox", { name: "Send a message…" }); - await textbox.fill("Hello Mr. Bot"); - await textbox.press("Enter"); - // Wait for message to send, get its ID and save as @threadId - const threadId = await locator - .locator(".mx_EventTile[data-scroll-tokens]") - .filter({ hasText: "Hello Mr. Bot" }) - .getAttribute("data-scroll-tokens"); + // Exclude timestamp, read marker, and maplibregl-map from snapshots + const css = + ".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker, .maplibregl-map { visibility: hidden !important; }"; - // Bot starts thread - await bot.sendMessage(roomId, "Hello there", threadId); - - // User clicks thread summary - await page.locator(".mx_RoomView_body .mx_ThreadSummary").click(); - - // User sends location on ThreadView - await expect(page.locator(".mx_ThreadView")).toBeAttached(); - await (await app.openMessageComposerOptions(true)).getByRole("menuitem", { name: "Location" }).click(); - await page.getByTestId(`share-location-option-Pin`).click(); - await page.locator("#mx_LocationPicker_map").click(); - await page.getByRole("button", { name: "Share location" }).click(); - await expect(page.locator(".mx_ThreadView .mx_EventTile_last .mx_MLocationBody")).toBeAttached({ - timeout: 10000, - }); + let locator = page.locator(".mx_RoomView_body"); + // User sends message + let textbox = locator.getByRole("textbox", { name: "Send a message…" }); + await textbox.fill("Hello Mr. Bot"); + await textbox.press("Enter"); + // Wait for message to send, get its ID and save as @threadId + const threadId = await locator + .locator(".mx_EventTile[data-scroll-tokens]") + .filter({ hasText: "Hello Mr. Bot" }) + .getAttribute("data-scroll-tokens"); + + // Bot starts thread + await bot.sendMessage(roomId, "Hello there", threadId); + + // User clicks thread summary + await page.locator(".mx_RoomView_body .mx_ThreadSummary").click(); + + // User sends location on ThreadView + await expect(page.locator(".mx_ThreadView")).toBeAttached(); + await (await app.openMessageComposerOptions(true)).getByRole("menuitem", { name: "Location" }).click(); + await page.getByTestId(`share-location-option-Pin`).click(); + await page.locator("#mx_LocationPicker_map").click(); + await page.getByRole("button", { name: "Share location" }).click(); + await expect(page.locator(".mx_ThreadView .mx_EventTile_last .mx_MLocationBody")).toBeAttached({ + timeout: 10000, + }); - // User replies to the location - locator = page.locator(".mx_ThreadView"); - await locator.locator(".mx_EventTile_last").hover(); - await locator.locator(".mx_EventTile_last").getByRole("button", { name: "Reply" }).click(); - textbox = locator.getByRole("textbox", { name: "Reply to thread…" }); - await textbox.fill("Please come here"); - await textbox.press("Enter"); - // Wait until the reply is sent - await expect(locator.locator(".mx_EventTile_last .mx_EventTile_receiptSent")).toBeVisible(); + // User replies to the location + locator = page.locator(".mx_ThreadView"); + await locator.locator(".mx_EventTile_last").hover(); + await locator.locator(".mx_EventTile_last").getByRole("button", { name: "Reply" }).click(); + textbox = locator.getByRole("textbox", { name: "Reply to thread…" }); + await textbox.fill("Please come here"); + await textbox.press("Enter"); + // Wait until the reply is sent + await expect(locator.locator(".mx_EventTile_last .mx_EventTile_receiptSent")).toBeVisible(); - // Take a snapshot of reply to the shared location - await page.addStyleTag({ content: css }); - await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("Reply_to_the_location_on_ThreadView.png"); - }); + // Take a snapshot of reply to the shared location + await page.addStyleTag({ content: css }); + await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("Reply_to_the_location_on_ThreadView.png"); + }, + ); test("right panel behaves correctly", async ({ page, app, user }) => { // Create room diff --git a/playwright/e2e/timeline/timeline.spec.ts b/playwright/e2e/timeline/timeline.spec.ts index e8ef0e577c2..7aaabb9759d 100644 --- a/playwright/e2e/timeline/timeline.spec.ts +++ b/playwright/e2e/timeline/timeline.spec.ts @@ -137,182 +137,190 @@ test.describe("Timeline", () => { }); test.describe("configure room", () => { - test("should create and configure a room on IRC layout", async ({ page, app, room }) => { - await page.goto(`/#/room/${room.roomId}`); - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); - await expect( - page.locator( - ".mx_RoomView_body .mx_GenericEventListSummary[data-layout='irc'] .mx_GenericEventListSummary_summary", - { hasText: `${OLD_NAME} created and configured the room.` }, - ), - ).toBeVisible(); - - // wait for the date separator to appear to have a stable screenshot - await expect(page.locator(".mx_TimelineSeparator")).toHaveText("today"); - - await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("configured-room-irc-layout.png"); - }); - - test("should have an expanded generic event list summary (GELS) on IRC layout", async ({ page, app, room }) => { - await page.goto(`/#/room/${room.roomId}`); - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); - - // Wait until configuration is finished - await expect( - page.locator( - ".mx_RoomView_body .mx_GenericEventListSummary[data-layout='irc'] .mx_GenericEventListSummary_summary", - { hasText: `${OLD_NAME} created and configured the room.` }, - ), - ).toBeVisible(); - - const gels = page.locator(".mx_GenericEventListSummary"); - // Click "expand" link button - await gels.getByRole("button", { name: "Expand" }).click(); - // Assert that the "expand" link button worked - await expect(gels.getByRole("button", { name: "Collapse" })).toBeVisible(); + test( + "should create and configure a room on IRC layout", + { tag: "@screenshot" }, + async ({ page, app, room }) => { + await page.goto(`/#/room/${room.roomId}`); + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); + await expect( + page.locator( + ".mx_RoomView_body .mx_GenericEventListSummary[data-layout='irc'] .mx_GenericEventListSummary_summary", + { hasText: `${OLD_NAME} created and configured the room.` }, + ), + ).toBeVisible(); + + // wait for the date separator to appear to have a stable screenshot + await expect(page.locator(".mx_TimelineSeparator")).toHaveText("today"); + + await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("configured-room-irc-layout.png"); + }, + ); + + test( + "should have an expanded generic event list summary (GELS) on IRC layout", + { tag: "@screenshot" }, + async ({ page, app, room }) => { + await page.goto(`/#/room/${room.roomId}`); + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); - await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-irc-layout.png", { - mask: [page.locator(".mx_MessageTimestamp")], - css: ` + // Wait until configuration is finished + await expect( + page.locator( + ".mx_RoomView_body .mx_GenericEventListSummary[data-layout='irc'] .mx_GenericEventListSummary_summary", + { hasText: `${OLD_NAME} created and configured the room.` }, + ), + ).toBeVisible(); + + const gels = page.locator(".mx_GenericEventListSummary"); + // Click "expand" link button + await gels.getByRole("button", { name: "Expand" }).click(); + // Assert that the "expand" link button worked + await expect(gels.getByRole("button", { name: "Collapse" })).toBeVisible(); + + await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-irc-layout.png", { + mask: [page.locator(".mx_MessageTimestamp")], + css: ` .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker { display: none !important; } `, - }); - }); - - test("should have an expanded generic event list summary (GELS) on compact modern/group layout", async ({ - page, - app, - room, - }) => { - await page.goto(`/#/room/${room.roomId}`); - - // Set compact modern layout - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group); - await app.settings.setValue("useCompactLayout", null, SettingLevel.DEVICE, true); + }); + }, + ); - // Wait until configuration is finished - await expect( - page.locator(".mx_RoomView_body .mx_GenericEventListSummary[data-layout='group']", { - hasText: `${OLD_NAME} created and configured the room.`, - }), - ).toBeVisible(); + test( + "should have an expanded generic event list summary (GELS) on compact modern/group layout", + { tag: "@screenshot" }, + async ({ page, app, room }) => { + await page.goto(`/#/room/${room.roomId}`); - const gels = page.locator(".mx_GenericEventListSummary"); - // Click "expand" link button - await gels.getByRole("button", { name: "Expand" }).click(); - // Assert that the "expand" link button worked - await expect(gels.getByRole("button", { name: "Collapse" })).toBeVisible(); + // Set compact modern layout + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group); + await app.settings.setValue("useCompactLayout", null, SettingLevel.DEVICE, true); - await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-modern-layout.png", { - mask: [page.locator(".mx_MessageTimestamp")], - css: ` + // Wait until configuration is finished + await expect( + page.locator(".mx_RoomView_body .mx_GenericEventListSummary[data-layout='group']", { + hasText: `${OLD_NAME} created and configured the room.`, + }), + ).toBeVisible(); + + const gels = page.locator(".mx_GenericEventListSummary"); + // Click "expand" link button + await gels.getByRole("button", { name: "Expand" }).click(); + // Assert that the "expand" link button worked + await expect(gels.getByRole("button", { name: "Collapse" })).toBeVisible(); + + await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-modern-layout.png", { + mask: [page.locator(".mx_MessageTimestamp")], + css: ` .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker { display: none !important; } `, - }); - }); - - test("should click 'collapse' on the first hovered info event line inside GELS on bubble layout", async ({ - page, - app, - room, - }) => { - // This test checks clickability of the "Collapse" link button, which had been covered with - // MessageActionBar's safe area - https://github.com/vector-im/element-web/issues/22864 - - await page.goto(`/#/room/${room.roomId}`); - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); - await expect( - page.locator( - ".mx_RoomView_body .mx_GenericEventListSummary[data-layout='bubble'] .mx_GenericEventListSummary_summary", - { hasText: `${OLD_NAME} created and configured the room.` }, - ), - ).toBeVisible(); - - const gels = page.locator(".mx_GenericEventListSummary"); - // Click "expand" link button - await gels.getByRole("button", { name: "Expand" }).click(); - // Assert that the "expand" link button worked - await expect(gels.getByRole("button", { name: "Collapse" })).toBeVisible(); - - // Make sure spacer is not visible on bubble layout - await expect( - page.locator(".mx_GenericEventListSummary[data-layout=bubble] .mx_GenericEventListSummary_spacer"), - ).not.toBeVisible(); // See: _GenericEventListSummary.pcss - - // Save snapshot of expanded generic event list summary on bubble layout - await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-bubble-layout.png", { - // Exclude timestamp from snapshot - mask: [page.locator(".mx_MessageTimestamp")], - }); - - // Click "collapse" link button on the first hovered info event line - const firstTile = gels.locator(".mx_GenericEventListSummary_unstyledList .mx_EventTile_info:first-of-type"); - await firstTile.hover(); - await expect(firstTile.getByRole("toolbar", { name: "Message Actions" })).toBeVisible(); - await gels.getByRole("button", { name: "Collapse" }).click(); + }); + }, + ); - // Assert that "collapse" link button worked - await expect(gels.getByRole("button", { name: "Expand" })).toBeVisible(); + test( + "should click 'collapse' on the first hovered info event line inside GELS on bubble layout", + { tag: "@screenshot" }, + async ({ page, app, room }) => { + // This test checks clickability of the "Collapse" link button, which had been covered with + // MessageActionBar's safe area - https://github.com/vector-im/element-web/issues/22864 - // Save snapshot of collapsed generic event list summary on bubble layout - await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("collapsed-gels-bubble-layout.png", { - mask: [page.locator(".mx_MessageTimestamp")], - }); - }); + await page.goto(`/#/room/${room.roomId}`); + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); + await expect( + page.locator( + ".mx_RoomView_body .mx_GenericEventListSummary[data-layout='bubble'] .mx_GenericEventListSummary_summary", + { hasText: `${OLD_NAME} created and configured the room.` }, + ), + ).toBeVisible(); + + const gels = page.locator(".mx_GenericEventListSummary"); + // Click "expand" link button + await gels.getByRole("button", { name: "Expand" }).click(); + // Assert that the "expand" link button worked + await expect(gels.getByRole("button", { name: "Collapse" })).toBeVisible(); + + // Make sure spacer is not visible on bubble layout + await expect( + page.locator(".mx_GenericEventListSummary[data-layout=bubble] .mx_GenericEventListSummary_spacer"), + ).not.toBeVisible(); // See: _GenericEventListSummary.pcss - test("should add inline start margin to an event line on IRC layout", async ({ - page, - app, - room, - axe, - checkA11y, - }) => { - axe.disableRules("color-contrast"); + // Save snapshot of expanded generic event list summary on bubble layout + await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-bubble-layout.png", { + // Exclude timestamp from snapshot + mask: [page.locator(".mx_MessageTimestamp")], + }); - await page.goto(`/#/room/${room.roomId}`); - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); + // Click "collapse" link button on the first hovered info event line + const firstTile = gels.locator( + ".mx_GenericEventListSummary_unstyledList .mx_EventTile_info:first-of-type", + ); + await firstTile.hover(); + await expect(firstTile.getByRole("toolbar", { name: "Message Actions" })).toBeVisible(); + await gels.getByRole("button", { name: "Collapse" }).click(); - // Wait until configuration is finished - await expect( - page.locator( - ".mx_RoomView_body .mx_GenericEventListSummary[data-layout='irc'] .mx_GenericEventListSummary_summary", - { hasText: `${OLD_NAME} created and configured the room.` }, - ), - ).toBeVisible(); + // Assert that "collapse" link button worked + await expect(gels.getByRole("button", { name: "Expand" })).toBeVisible(); - // Click "expand" link button - await page.locator(".mx_GenericEventListSummary").getByRole("button", { name: "Expand" }).click(); + // Save snapshot of collapsed generic event list summary on bubble layout + await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("collapsed-gels-bubble-layout.png", { + mask: [page.locator(".mx_MessageTimestamp")], + }); + }, + ); - // Check the event line has margin instead of inset property - // cf. _EventTile.pcss - // --EventTile_irc_line_info-margin-inline-start - // = calc(var(--name-width) + var(--icon-width) + 1 * var(--right-padding)) - // = 80 + 14 + 5 = 99px + test( + "should add inline start margin to an event line on IRC layout", + { tag: "@screenshot" }, + async ({ page, app, room, axe, checkA11y }) => { + axe.disableRules("color-contrast"); - const firstEventLineIrc = page.locator( - ".mx_EventTile_info[data-layout=irc]:first-of-type .mx_EventTile_line", - ); - await expect(firstEventLineIrc).toHaveCSS("margin-inline-start", "99px"); - await expect(firstEventLineIrc).toHaveCSS("inset-inline-start", "0px"); + await page.goto(`/#/room/${room.roomId}`); + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); - await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( - "event-line-inline-start-margin-irc-layout.png", - { - // Exclude timestamp and read marker from snapshot - mask: [page.locator(".mx_MessageTimestamp")], - css: ` + // Wait until configuration is finished + await expect( + page.locator( + ".mx_RoomView_body .mx_GenericEventListSummary[data-layout='irc'] .mx_GenericEventListSummary_summary", + { hasText: `${OLD_NAME} created and configured the room.` }, + ), + ).toBeVisible(); + + // Click "expand" link button + await page.locator(".mx_GenericEventListSummary").getByRole("button", { name: "Expand" }).click(); + + // Check the event line has margin instead of inset property + // cf. _EventTile.pcss + // --EventTile_irc_line_info-margin-inline-start + // = calc(var(--name-width) + var(--icon-width) + 1 * var(--right-padding)) + // = 80 + 14 + 5 = 99px + + const firstEventLineIrc = page.locator( + ".mx_EventTile_info[data-layout=irc]:first-of-type .mx_EventTile_line", + ); + await expect(firstEventLineIrc).toHaveCSS("margin-inline-start", "99px"); + await expect(firstEventLineIrc).toHaveCSS("inset-inline-start", "0px"); + + await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( + "event-line-inline-start-margin-irc-layout.png", + { + // Exclude timestamp and read marker from snapshot + mask: [page.locator(".mx_MessageTimestamp")], + css: ` .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker { display: none !important; } `, - }, - ); - await checkA11y(); - }); + }, + ); + await checkA11y(); + }, + ); }); test.describe("message displaying", () => { @@ -332,289 +340,311 @@ test.describe("Timeline", () => { ).toBeVisible(); }; - test("should align generic event list summary with messages and emote on IRC layout", async ({ - page, - app, - room, - }) => { - // This test aims to check: - // 1. Alignment of collapsed GELS (generic event list summary) and messages - // 2. Alignment of expanded GELS and messages - // 3. Alignment of expanded GELS and placeholder of deleted message - // 4. Alignment of expanded GELS, placeholder of deleted message, and emote + test( + "should align generic event list summary with messages and emote on IRC layout", + { tag: "@screenshot" }, + async ({ page, app, room }) => { + // This test aims to check: + // 1. Alignment of collapsed GELS (generic event list summary) and messages + // 2. Alignment of expanded GELS and messages + // 3. Alignment of expanded GELS and placeholder of deleted message + // 4. Alignment of expanded GELS, placeholder of deleted message, and emote - await page.goto(`/#/room/${room.roomId}`); - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); + await page.goto(`/#/room/${room.roomId}`); + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); - // Wait until configuration is finished - await expect( - page - .locator(".mx_GenericEventListSummary_summary") - .getByText(`${OLD_NAME} created and configured the room.`), - ).toBeVisible(); + // Wait until configuration is finished + await expect( + page + .locator(".mx_GenericEventListSummary_summary") + .getByText(`${OLD_NAME} created and configured the room.`), + ).toBeVisible(); + + // Send messages + const composer = app.getComposerField(); + await composer.fill("Hello Mr. Bot"); + await composer.press("Enter"); + await composer.fill("Hello again, Mr. Bot"); + await composer.press("Enter"); + + // Make sure the second message was sent + await expect( + page.locator(".mx_RoomView_MessageList > .mx_EventTile_last .mx_EventTile_receiptSent"), + ).toBeVisible(); + + // 1. Alignment of collapsed GELS (generic event list summary) and messages + // Check inline start spacing of collapsed GELS + // See: _EventTile.pcss + // .mx_GenericEventListSummary[data-layout="irc"] > .mx_EventTile_line + // = var(--name-width) + var(--icon-width) + var(--MessageTimestamp-width) + 2 * var(--right-padding) + // = 80 + 14 + 46 + 2 * 5 + // = 150px + await expect( + page.locator(".mx_GenericEventListSummary[data-layout=irc] > .mx_EventTile_line"), + ).toHaveCSS("padding-inline-start", "150px"); + // Check width and spacing values of elements in .mx_EventTile, which should be equal to 150px + // --right-padding should be applied + for (const locator of await page.locator(".mx_EventTile > a").all()) { + if (await locator.isVisible()) { + await expect(locator).toHaveCSS("margin-right", "5px"); + } + } + // --name-width width zero inline end margin should be applied + for (const locator of await page.locator(".mx_EventTile .mx_DisambiguatedProfile").all()) { + await expect(locator).toHaveCSS("width", "80px"); + await expect(locator).toHaveCSS("margin-inline-end", "0px"); + } + // --icon-width should be applied + for (const locator of await page.locator(".mx_EventTile .mx_EventTile_avatar > .mx_BaseAvatar").all()) { + await expect(locator).toHaveCSS("width", "14px"); + } + // var(--MessageTimestamp-width) should be applied + for (const locator of await page.locator(".mx_EventTile > a").all()) { + await expect(locator).toHaveCSS("min-width", "46px"); + } + // Record alignment of collapsed GELS and messages on messagePanel + await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( + "collapsed-gels-and-messages-irc-layout.png", + { + // Exclude timestamp from snapshot of mx_MainSplit + mask: [page.locator(".mx_MessageTimestamp")], + }, + ); - // Send messages - const composer = app.getComposerField(); - await composer.fill("Hello Mr. Bot"); - await composer.press("Enter"); - await composer.fill("Hello again, Mr. Bot"); - await composer.press("Enter"); + // 2. Alignment of expanded GELS and messages + // Click "expand" link button + await page.locator(".mx_GenericEventListSummary").getByRole("button", { name: "Expand" }).click(); + // Check inline start spacing of info line on expanded GELS + // See: _EventTile.pcss + // --EventTile_irc_line_info-margin-inline-start + // = 80 + 14 + 1 * 5 + await expect( + page.locator(".mx_EventTile[data-layout=irc].mx_EventTile_info:first-of-type .mx_EventTile_line"), + ).toHaveCSS("margin-inline-start", "99px"); + // Record alignment of expanded GELS and messages on messagePanel + await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( + "expanded-gels-and-messages-irc-layout.png", + { + // Exclude timestamp from snapshot of mx_MainSplit + mask: [page.locator(".mx_MessageTimestamp")], + }, + ); - // Make sure the second message was sent - await expect( - page.locator(".mx_RoomView_MessageList > .mx_EventTile_last .mx_EventTile_receiptSent"), - ).toBeVisible(); + // 3. Alignment of expanded GELS and placeholder of deleted message + // Delete the second (last) message + const lastTile = page.locator(".mx_RoomView_MessageList > .mx_EventTile_last"); + await lastTile.hover(); + await lastTile.getByRole("button", { name: "Options" }).click(); + await page.getByRole("menuitem", { name: "Remove" }).click(); + // Confirm deletion + await page.locator(".mx_Dialog_buttons").getByRole("button", { name: "Remove" }).click(); + // Make sure the dialog was closed and the second (last) message was redacted + await expect(page.locator(".mx_Dialog")).not.toBeVisible(); + await expect( + page.locator(".mx_GenericEventListSummary .mx_EventTile_last .mx_RedactedBody"), + ).toBeVisible(); + await expect( + page.locator(".mx_GenericEventListSummary .mx_EventTile_last .mx_EventTile_receiptSent"), + ).toBeVisible(); + // Record alignment of expanded GELS and placeholder of deleted message on messagePanel + await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( + "expanded-gels-redaction-placeholder.png", + { + // Exclude timestamp from snapshot of mx_MainSplit + mask: [page.locator(".mx_MessageTimestamp")], + }, + ); - // 1. Alignment of collapsed GELS (generic event list summary) and messages - // Check inline start spacing of collapsed GELS - // See: _EventTile.pcss - // .mx_GenericEventListSummary[data-layout="irc"] > .mx_EventTile_line - // = var(--name-width) + var(--icon-width) + var(--MessageTimestamp-width) + 2 * var(--right-padding) - // = 80 + 14 + 46 + 2 * 5 - // = 150px - await expect(page.locator(".mx_GenericEventListSummary[data-layout=irc] > .mx_EventTile_line")).toHaveCSS( - "padding-inline-start", - "150px", - ); - // Check width and spacing values of elements in .mx_EventTile, which should be equal to 150px - // --right-padding should be applied - for (const locator of await page.locator(".mx_EventTile > a").all()) { - if (await locator.isVisible()) { - await expect(locator).toHaveCSS("margin-right", "5px"); - } - } - // --name-width width zero inline end margin should be applied - for (const locator of await page.locator(".mx_EventTile .mx_DisambiguatedProfile").all()) { - await expect(locator).toHaveCSS("width", "80px"); - await expect(locator).toHaveCSS("margin-inline-end", "0px"); - } - // --icon-width should be applied - for (const locator of await page.locator(".mx_EventTile .mx_EventTile_avatar > .mx_BaseAvatar").all()) { - await expect(locator).toHaveCSS("width", "14px"); - } - // var(--MessageTimestamp-width) should be applied - for (const locator of await page.locator(".mx_EventTile > a").all()) { - await expect(locator).toHaveCSS("min-width", "46px"); - } - // Record alignment of collapsed GELS and messages on messagePanel - await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( - "collapsed-gels-and-messages-irc-layout.png", - { + // 4. Alignment of expanded GELS, placeholder of deleted message, and emote + // Send a emote + await page + .locator(".mx_RoomView_body") + .getByRole("textbox", { name: "Send a message…" }) + .fill("/me says hello to Mr. Bot"); + await page + .locator(".mx_RoomView_body") + .getByRole("textbox", { name: "Send a message…" }) + .press("Enter"); + // Check inline start margin of its avatar + // Here --right-padding is for the avatar on the message line + // See: _IRCLayout.pcss + // .mx_IRCLayout .mx_EventTile_emote .mx_EventTile_avatar + // = calc(var(--name-width) + var(--icon-width) + 1 * var(--right-padding)) + // = 80 + 14 + 1 * 5 + await expect(page.locator(".mx_EventTile_emote .mx_EventTile_avatar")).toHaveCSS("margin-left", "99px"); + // Make sure emote was sent + await expect( + page.locator(".mx_EventTile_last.mx_EventTile_emote .mx_EventTile_receiptSent"), + ).toBeVisible(); + // Record alignment of expanded GELS, placeholder of deleted message, and emote + await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-emote-irc-layout.png", { // Exclude timestamp from snapshot of mx_MainSplit mask: [page.locator(".mx_MessageTimestamp")], - }, - ); - - // 2. Alignment of expanded GELS and messages - // Click "expand" link button - await page.locator(".mx_GenericEventListSummary").getByRole("button", { name: "Expand" }).click(); - // Check inline start spacing of info line on expanded GELS - // See: _EventTile.pcss - // --EventTile_irc_line_info-margin-inline-start - // = 80 + 14 + 1 * 5 - await expect( - page.locator(".mx_EventTile[data-layout=irc].mx_EventTile_info:first-of-type .mx_EventTile_line"), - ).toHaveCSS("margin-inline-start", "99px"); - // Record alignment of expanded GELS and messages on messagePanel - await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-and-messages-irc-layout.png", { - // Exclude timestamp from snapshot of mx_MainSplit - mask: [page.locator(".mx_MessageTimestamp")], - }); - - // 3. Alignment of expanded GELS and placeholder of deleted message - // Delete the second (last) message - const lastTile = page.locator(".mx_RoomView_MessageList > .mx_EventTile_last"); - await lastTile.hover(); - await lastTile.getByRole("button", { name: "Options" }).click(); - await page.getByRole("menuitem", { name: "Remove" }).click(); - // Confirm deletion - await page.locator(".mx_Dialog_buttons").getByRole("button", { name: "Remove" }).click(); - // Make sure the dialog was closed and the second (last) message was redacted - await expect(page.locator(".mx_Dialog")).not.toBeVisible(); - await expect(page.locator(".mx_GenericEventListSummary .mx_EventTile_last .mx_RedactedBody")).toBeVisible(); - await expect( - page.locator(".mx_GenericEventListSummary .mx_EventTile_last .mx_EventTile_receiptSent"), - ).toBeVisible(); - // Record alignment of expanded GELS and placeholder of deleted message on messagePanel - await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-redaction-placeholder.png", { - // Exclude timestamp from snapshot of mx_MainSplit - mask: [page.locator(".mx_MessageTimestamp")], - }); - - // 4. Alignment of expanded GELS, placeholder of deleted message, and emote - // Send a emote - await page - .locator(".mx_RoomView_body") - .getByRole("textbox", { name: "Send a message…" }) - .fill("/me says hello to Mr. Bot"); - await page.locator(".mx_RoomView_body").getByRole("textbox", { name: "Send a message…" }).press("Enter"); - // Check inline start margin of its avatar - // Here --right-padding is for the avatar on the message line - // See: _IRCLayout.pcss - // .mx_IRCLayout .mx_EventTile_emote .mx_EventTile_avatar - // = calc(var(--name-width) + var(--icon-width) + 1 * var(--right-padding)) - // = 80 + 14 + 1 * 5 - await expect(page.locator(".mx_EventTile_emote .mx_EventTile_avatar")).toHaveCSS("margin-left", "99px"); - // Make sure emote was sent - await expect(page.locator(".mx_EventTile_last.mx_EventTile_emote .mx_EventTile_receiptSent")).toBeVisible(); - // Record alignment of expanded GELS, placeholder of deleted message, and emote - await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-emote-irc-layout.png", { - // Exclude timestamp from snapshot of mx_MainSplit - mask: [page.locator(".mx_MessageTimestamp")], - }); - }); - - test("should render EventTiles on IRC, modern (group), and bubble layout", async ({ page, app, room }) => { - const screenshotOptions = { - // Hide because flaky - See https://github.com/vector-im/element-web/issues/24957 - mask: [page.locator(".mx_MessageTimestamp")], - css: ` + }); + }, + ); + + test( + "should render EventTiles on IRC, modern (group), and bubble layout", + { tag: "@screenshot" }, + async ({ page, app, room }) => { + const screenshotOptions = { + // Hide because flaky - See https://github.com/vector-im/element-web/issues/24957 + mask: [page.locator(".mx_MessageTimestamp")], + css: ` .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker { display: none !important; } `, - }; - - await sendEvent(app.client, room.roomId); - await sendEvent(app.client, room.roomId); // check continuation - await sendEvent(app.client, room.roomId); // check the last EventTile + }; - await page.goto(`/#/room/${room.roomId}`); - const composer = app.getComposerField(); - // Send a plain text message - await composer.fill("Hello"); - await composer.press("Enter"); - // Send a big emoji - await composer.fill("🏀"); - await composer.press("Enter"); - // Send an inline emoji - await composer.fill("This message has an inline emoji 👒"); - await composer.press("Enter"); + await sendEvent(app.client, room.roomId); + await sendEvent(app.client, room.roomId); // check continuation + await sendEvent(app.client, room.roomId); // check the last EventTile - await expect(page.locator(".mx_RoomView").getByText("This message has an inline emoji 👒")).toBeVisible(); + await page.goto(`/#/room/${room.roomId}`); + const composer = app.getComposerField(); + // Send a plain text message + await composer.fill("Hello"); + await composer.press("Enter"); + // Send a big emoji + await composer.fill("🏀"); + await composer.press("Enter"); + // Send an inline emoji + await composer.fill("This message has an inline emoji 👒"); + await composer.press("Enter"); - //////////////////////////////////////////////////////////////////////////////////////////////////////////// - // IRC layout - //////////////////////////////////////////////////////////////////////////////////////////////////////////// + await expect( + page.locator(".mx_RoomView").getByText("This message has an inline emoji 👒"), + ).toBeVisible(); - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // IRC layout + //////////////////////////////////////////////////////////////////////////////////////////////////////////// - // Wait until configuration is finished - await expect( - page - .locator(".mx_GenericEventListSummary_summary") - .getByText(`${OLD_NAME} created and configured the room.`), - ).toBeVisible(); + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); - await app.timeline.scrollToBottom(); - await expect( - page.locator(".mx_RoomView").getByText("This message has an inline emoji 👒"), - ).toBeInViewport(); - await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( - "event-tiles-irc-layout.png", - screenshotOptions, - ); + // Wait until configuration is finished + await expect( + page + .locator(".mx_GenericEventListSummary_summary") + .getByText(`${OLD_NAME} created and configured the room.`), + ).toBeVisible(); - //////////////////////////////////////////////////////////////////////////////////////////////////////////// - // Group/modern layout - //////////////////////////////////////////////////////////////////////////////////////////////////////////// + await app.timeline.scrollToBottom(); + await expect( + page.locator(".mx_RoomView").getByText("This message has an inline emoji 👒"), + ).toBeInViewport(); + await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( + "event-tiles-irc-layout.png", + screenshotOptions, + ); - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group); + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Group/modern layout + //////////////////////////////////////////////////////////////////////////////////////////////////////////// - // Check that the last EventTile is rendered - await app.timeline.scrollToBottom(); - await expect( - page.locator(".mx_RoomView").getByText("This message has an inline emoji 👒"), - ).toBeInViewport(); - await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( - "event-tiles-modern-layout.png", - screenshotOptions, - ); + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group); - // Check the same thing for compact layout - await app.settings.setValue("useCompactLayout", null, SettingLevel.DEVICE, true); + // Check that the last EventTile is rendered + await app.timeline.scrollToBottom(); + await expect( + page.locator(".mx_RoomView").getByText("This message has an inline emoji 👒"), + ).toBeInViewport(); + await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( + "event-tiles-modern-layout.png", + screenshotOptions, + ); - // Check that the last EventTile is rendered - await app.timeline.scrollToBottom(); - await expect( - page.locator(".mx_RoomView").getByText("This message has an inline emoji 👒"), - ).toBeInViewport(); - await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( - "event-tiles-compact-modern-layout.png", - screenshotOptions, - ); + // Check the same thing for compact layout + await app.settings.setValue("useCompactLayout", null, SettingLevel.DEVICE, true); - //////////////////////////////////////////////////////////////////////////////////////////////////////////// - // Message bubble layout - //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Check that the last EventTile is rendered + await app.timeline.scrollToBottom(); + await expect( + page.locator(".mx_RoomView").getByText("This message has an inline emoji 👒"), + ).toBeInViewport(); + await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( + "event-tiles-compact-modern-layout.png", + screenshotOptions, + ); - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Message bubble layout + //////////////////////////////////////////////////////////////////////////////////////////////////////////// - await app.timeline.scrollToBottom(); - await expect( - page.locator(".mx_RoomView").getByText("This message has an inline emoji 👒"), - ).toBeInViewport(); - await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( - "event-tiles-bubble-layout.png", - screenshotOptions, - ); - }); + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); - test("should set inline start padding to a hidden event line", async ({ page, app, room }) => { - test.skip( - true, - "Disabled due to screenshot test being flaky - https://github.com/element-hq/element-web/issues/26890", - ); - await sendEvent(app.client, room.roomId); - await page.goto(`/#/room/${room.roomId}`); - await app.settings.setValue("showHiddenEventsInTimeline", null, SettingLevel.DEVICE, true); - await expect( - page - .locator(".mx_GenericEventListSummary_summary") - .getByText(`${OLD_NAME} created and configured the room.`), - ).toBeVisible(); + await app.timeline.scrollToBottom(); + await expect( + page.locator(".mx_RoomView").getByText("This message has an inline emoji 👒"), + ).toBeInViewport(); + await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( + "event-tiles-bubble-layout.png", + screenshotOptions, + ); + }, + ); + + test( + "should set inline start padding to a hidden event line", + { tag: "@screenshot" }, + async ({ page, app, room }) => { + test.skip( + true, + "Disabled due to screenshot test being flaky - https://github.com/element-hq/element-web/issues/26890", + ); + await sendEvent(app.client, room.roomId); + await page.goto(`/#/room/${room.roomId}`); + await app.settings.setValue("showHiddenEventsInTimeline", null, SettingLevel.DEVICE, true); + await expect( + page + .locator(".mx_GenericEventListSummary_summary") + .getByText(`${OLD_NAME} created and configured the room.`), + ).toBeVisible(); - // Edit message - await messageEdit(page); + // Edit message + await messageEdit(page); - // Click timestamp to highlight hidden event line - await page.locator(".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp").click(); + // Click timestamp to highlight hidden event line + await page.locator(".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp").click(); - // should not add inline start padding to a hidden event line on IRC layout - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); - await expect( - page.locator(".mx_EventTile[data-layout=irc].mx_EventTile_info .mx_EventTile_line").first(), - ).toHaveCSS("padding-inline-start", "0px"); + // should not add inline start padding to a hidden event line on IRC layout + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); + await expect( + page.locator(".mx_EventTile[data-layout=irc].mx_EventTile_info .mx_EventTile_line").first(), + ).toHaveCSS("padding-inline-start", "0px"); - // Exclude timestamp and read marker from snapshot - const screenshotOptions = { - mask: [page.locator(".mx_MessageTimestamp")], - css: ` + // Exclude timestamp and read marker from snapshot + const screenshotOptions = { + mask: [page.locator(".mx_MessageTimestamp")], + css: ` .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker { display: none !important; } `, - }; + }; - await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( - "hidden-event-line-zero-padding-irc-layout.png", - screenshotOptions, - ); + await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( + "hidden-event-line-zero-padding-irc-layout.png", + screenshotOptions, + ); - // should add inline start padding to a hidden event line on modern layout - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group); - // calc(var(--EventTile_group_line-spacing-inline-start) + 20px) = 64 + 20 = 84px - await expect( - page.locator(".mx_EventTile[data-layout=group].mx_EventTile_info .mx_EventTile_line").first(), - ).toHaveCSS("padding-inline-start", "84px"); + // should add inline start padding to a hidden event line on modern layout + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group); + // calc(var(--EventTile_group_line-spacing-inline-start) + 20px) = 64 + 20 = 84px + await expect( + page.locator(".mx_EventTile[data-layout=group].mx_EventTile_info .mx_EventTile_line").first(), + ).toHaveCSS("padding-inline-start", "84px"); - await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( - "hidden-event-line-padding-modern-layout.png", - screenshotOptions, - ); - }); + await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( + "hidden-event-line-padding-modern-layout.png", + screenshotOptions, + ); + }, + ); - test("should click view source event toggle", async ({ page, app, room }) => { + test("should click view source event toggle", { tag: "@screenshot" }, async ({ page, app, room }) => { // This test checks: // 1. clickability of top left of view source event toggle // 2. clickability of view source toggle on IRC layout @@ -712,89 +742,97 @@ test.describe("Timeline", () => { ).toBeVisible(); }); - test("should render url previews", async ({ page, app, room, axe, checkA11y, context }) => { - axe.disableRules("color-contrast"); - - // Element Web uses a Service Worker to rewrite unauthenticated media requests to authenticated ones, but - // the page can't see this happening. We intercept the route at the BrowserContext to ensure we get it - // post-worker, but we can't waitForResponse on that, so the page context is still used there. Because - // the page doesn't see the rewrite, it waits for the unauthenticated route. This is only confusing until - // the js-sdk (and thus the app as a whole) switches to using authenticated endpoints by default, hopefully. - await context.route( - "**/_matrix/client/v1/media/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*", - async (route) => { - await route.fulfill({ - path: "playwright/sample-files/riot.png", - }); - }, - ); - await page.route( - "**/_matrix/media/v3/preview_url?url=https%3A%2F%2Fcall.element.io%2F&ts=*", - async (route) => { - await route.fulfill({ - json: { - "og:title": "Element Call", - "og:description": null, - "og:image:width": 48, - "og:image:height": 48, - "og:image": "mxc://matrix.org/2022-08-16_yaiSVSRIsNFfxDnV", - "og:image:type": "image/png", - "matrix:image:size": 2121, - }, - }); - }, - ); + test( + "should render url previews", + { tag: "@screenshot" }, + async ({ page, app, room, axe, checkA11y, context }) => { + axe.disableRules("color-contrast"); + + // Element Web uses a Service Worker to rewrite unauthenticated media requests to authenticated ones, but + // the page can't see this happening. We intercept the route at the BrowserContext to ensure we get it + // post-worker, but we can't waitForResponse on that, so the page context is still used there. Because + // the page doesn't see the rewrite, it waits for the unauthenticated route. This is only confusing until + // the js-sdk (and thus the app as a whole) switches to using authenticated endpoints by default, hopefully. + await context.route( + "**/_matrix/client/v1/media/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*", + async (route) => { + await route.fulfill({ + path: "playwright/sample-files/riot.png", + }); + }, + ); + await page.route( + "**/_matrix/media/v3/preview_url?url=https%3A%2F%2Fcall.element.io%2F&ts=*", + async (route) => { + await route.fulfill({ + json: { + "og:title": "Element Call", + "og:description": null, + "og:image:width": 48, + "og:image:height": 48, + "og:image": "mxc://matrix.org/2022-08-16_yaiSVSRIsNFfxDnV", + "og:image:type": "image/png", + "matrix:image:size": 2121, + }, + }); + }, + ); - const requestPromises: Promise[] = [ - page.waitForResponse("**/_matrix/media/v3/preview_url?url=https%3A%2F%2Fcall.element.io%2F&ts=*"), - // see context.route above for why we listen for the unauthenticated endpoint - page.waitForResponse("**/_matrix/media/v3/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*"), - ]; + const requestPromises: Promise[] = [ + page.waitForResponse("**/_matrix/media/v3/preview_url?url=https%3A%2F%2Fcall.element.io%2F&ts=*"), + // see context.route above for why we listen for the unauthenticated endpoint + page.waitForResponse("**/_matrix/media/v3/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*"), + ]; - await app.client.sendMessage(room.roomId, "https://call.element.io/"); - await page.goto(`/#/room/${room.roomId}`); + await app.client.sendMessage(room.roomId, "https://call.element.io/"); + await page.goto(`/#/room/${room.roomId}`); - await expect(page.locator(".mx_LinkPreviewWidget").getByText("Element Call")).toBeVisible(); - await Promise.all(requestPromises); + await expect(page.locator(".mx_LinkPreviewWidget").getByText("Element Call")).toBeVisible(); + await Promise.all(requestPromises); - await checkA11y(); + await checkA11y(); - await app.timeline.scrollToBottom(); - await expect(page.locator(".mx_EventTile_last")).toMatchScreenshot("url-preview.png", { - // Exclude timestamp and read marker from snapshot - mask: [page.locator(".mx_MessageTimestamp")], - css: ` + await app.timeline.scrollToBottom(); + await expect(page.locator(".mx_EventTile_last")).toMatchScreenshot("url-preview.png", { + // Exclude timestamp and read marker from snapshot + mask: [page.locator(".mx_MessageTimestamp")], + css: ` .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker { display: none !important; } `, - }); - }); + }); + }, + ); test.describe("on search results panel", () => { - test("should highlight search result words regardless of formatting", async ({ page, app, room }) => { - await sendEvent(app.client, room.roomId); - await sendEvent(app.client, room.roomId, true); - await page.goto(`/#/room/${room.roomId}`); + test( + "should highlight search result words regardless of formatting", + { tag: "@screenshot" }, + async ({ page, app, room }) => { + await sendEvent(app.client, room.roomId); + await sendEvent(app.client, room.roomId, true); + await page.goto(`/#/room/${room.roomId}`); - await app.toggleRoomInfoPanel(); + await app.toggleRoomInfoPanel(); - await page.locator(".mx_RoomSummaryCard_search").getByRole("searchbox").fill("Message"); - await page.locator(".mx_RoomSummaryCard_search").getByRole("searchbox").press("Enter"); + await page.locator(".mx_RoomSummaryCard_search").getByRole("searchbox").fill("Message"); + await page.locator(".mx_RoomSummaryCard_search").getByRole("searchbox").press("Enter"); - await expect(page.locator(".mx_RoomSearchAuxPanel")).toMatchScreenshot("search-aux-panel.png"); + await expect(page.locator(".mx_RoomSearchAuxPanel")).toMatchScreenshot("search-aux-panel.png"); - for (const locator of await page - .locator(".mx_EventTile:not(.mx_EventTile_contextual) .mx_EventTile_searchHighlight") - .all()) { - await expect(locator).toBeVisible(); - } - await expect(page.locator(".mx_RoomView_searchResultsPanel")).toMatchScreenshot( - "highlighted-search-results.png", - ); - }); + for (const locator of await page + .locator(".mx_EventTile:not(.mx_EventTile_contextual) .mx_EventTile_searchHighlight") + .all()) { + await expect(locator).toBeVisible(); + } + await expect(page.locator(".mx_RoomView_searchResultsPanel")).toMatchScreenshot( + "highlighted-search-results.png", + ); + }, + ); - test("should render a fully opaque textual event", async ({ page, app, room }) => { + test("should render a fully opaque textual event", { tag: "@screenshot" }, async ({ page, app, room }) => { const stringToSearch = "Message"; // Same with string sent with sendEvent() await sendEvent(app.client, room.roomId); @@ -918,7 +956,7 @@ test.describe("Timeline", () => { ).toHaveCount(4); }); - test("should display a reply chain", async ({ page, app, room, homeserver }) => { + test("should display a reply chain", { tag: "@screenshot" }, async ({ page, app, room, homeserver }) => { const reply2 = "Reply again"; await page.goto(`/#/room/${room.roomId}`); @@ -1031,122 +1069,121 @@ test.describe("Timeline", () => { ); }); - test("should send, reply, and display long strings without overflowing", async ({ - page, - app, - room, - homeserver, - }) => { - // Max 256 characters for display name - const LONG_STRING = - "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut " + - "et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut " + - "aliquip"; - - const newDisplayName = `${LONG_STRING} 2`; - - // Set the display name to "LONG_STRING 2" in order to avoid screenshot tests from failing - // due to the generated random mxid being displayed inside the GELS summary. - // Note that we set it here as the test was failing on CI (but not locally!) if the name - // was changed afterwards. This is quite concerning, but maybe better than just disabling the - // whole test? - // https://github.com/element-hq/element-web/issues/27109 - await app.client.setDisplayName(newDisplayName); - - // Create a bot with a long display name - const bot = new Bot(page, homeserver, { - displayName: LONG_STRING, - autoAcceptInvites: false, - }); - await bot.prepareClient(); + test( + "should send, reply, and display long strings without overflowing", + { tag: "@screenshot" }, + async ({ page, app, room, homeserver }) => { + // Max 256 characters for display name + const LONG_STRING = + "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut " + + "et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut " + + "aliquip"; + + const newDisplayName = `${LONG_STRING} 2`; + + // Set the display name to "LONG_STRING 2" in order to avoid screenshot tests from failing + // due to the generated random mxid being displayed inside the GELS summary. + // Note that we set it here as the test was failing on CI (but not locally!) if the name + // was changed afterwards. This is quite concerning, but maybe better than just disabling the + // whole test? + // https://github.com/element-hq/element-web/issues/27109 + await app.client.setDisplayName(newDisplayName); + + // Create a bot with a long display name + const bot = new Bot(page, homeserver, { + displayName: LONG_STRING, + autoAcceptInvites: false, + }); + await bot.prepareClient(); - // Create another room with a long name, invite the bot, and open the room - const testRoomId = await app.client.createRoom({ name: LONG_STRING }); - await app.client.inviteUser(testRoomId, bot.credentials.userId); - await bot.joinRoom(testRoomId); - await page.goto(`/#/room/${testRoomId}`); + // Create another room with a long name, invite the bot, and open the room + const testRoomId = await app.client.createRoom({ name: LONG_STRING }); + await app.client.inviteUser(testRoomId, bot.credentials.userId); + await bot.joinRoom(testRoomId); + await page.goto(`/#/room/${testRoomId}`); - // Wait until configuration is finished - await expect( - page - .locator(".mx_GenericEventListSummary_summary") - .getByText(newDisplayName + " created and configured the room."), - ).toBeVisible(); - - // Have the bot send a long message - await bot.sendMessage(testRoomId, { - body: LONG_STRING, - msgtype: "m.text", - }); + // Wait until configuration is finished + await expect( + page + .locator(".mx_GenericEventListSummary_summary") + .getByText(newDisplayName + " created and configured the room."), + ).toBeVisible(); + + // Have the bot send a long message + await bot.sendMessage(testRoomId, { + body: LONG_STRING, + msgtype: "m.text", + }); - // Wait until the message is rendered - await expect( - page.locator(".mx_EventTile_last .mx_MTextBody .mx_EventTile_body").getByText(LONG_STRING), - ).toBeVisible(); + // Wait until the message is rendered + await expect( + page.locator(".mx_EventTile_last .mx_MTextBody .mx_EventTile_body").getByText(LONG_STRING), + ).toBeVisible(); - // Reply to the message - await clickButtonReply(page); - await app.getComposerField().fill(reply); - await app.getComposerField().press("Enter"); + // Reply to the message + await clickButtonReply(page); + await app.getComposerField().fill(reply); + await app.getComposerField().press("Enter"); - // Make sure the reply tile is rendered - const eventTileLine = page.locator(".mx_EventTile_last .mx_EventTile_line"); - await expect(eventTileLine.locator(".mx_ReplyTile .mx_MTextBody").getByText(LONG_STRING)).toBeVisible(); + // Make sure the reply tile is rendered + const eventTileLine = page.locator(".mx_EventTile_last .mx_EventTile_line"); + await expect(eventTileLine.locator(".mx_ReplyTile .mx_MTextBody").getByText(LONG_STRING)).toBeVisible(); - await expect(eventTileLine.getByText(reply)).toHaveCount(1); + await expect(eventTileLine.getByText(reply)).toHaveCount(1); - // Change the viewport size - await page.setViewportSize({ width: 1600, height: 1200 }); + // Change the viewport size + await page.setViewportSize({ width: 1600, height: 1200 }); - // Exclude timestamp and read marker from snapshot - const screenshotOptions = { - mask: [page.locator(".mx_MessageTimestamp")], - css: ` + // Exclude timestamp and read marker from snapshot + const screenshotOptions = { + mask: [page.locator(".mx_MessageTimestamp")], + css: ` .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker { display: none !important; } `, - }; - - // Make sure the strings do not overflow on IRC layout - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); - // Scroll to the bottom to take a snapshot of the whole viewport - await app.timeline.scrollToBottom(); - // Assert that both avatar in the introduction and the last message are visible at the same time - await expect(page.locator(".mx_NewRoomIntro .mx_BaseAvatar")).toBeVisible(); - const lastEventTileIrc = page.locator(".mx_EventTile_last[data-layout='irc']"); - await expect(lastEventTileIrc.locator(".mx_MTextBody").first()).toBeVisible(); - await expect(lastEventTileIrc.locator(".mx_EventTile_receiptSent")).toBeVisible(); // rendered at the bottom of EventTile - // Take a snapshot in IRC layout - await expect(page.locator(".mx_ScrollPanel")).toMatchScreenshot( - "long-strings-with-reply-irc-layout.png", - screenshotOptions, - ); + }; + + // Make sure the strings do not overflow on IRC layout + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); + // Scroll to the bottom to take a snapshot of the whole viewport + await app.timeline.scrollToBottom(); + // Assert that both avatar in the introduction and the last message are visible at the same time + await expect(page.locator(".mx_NewRoomIntro .mx_BaseAvatar")).toBeVisible(); + const lastEventTileIrc = page.locator(".mx_EventTile_last[data-layout='irc']"); + await expect(lastEventTileIrc.locator(".mx_MTextBody").first()).toBeVisible(); + await expect(lastEventTileIrc.locator(".mx_EventTile_receiptSent")).toBeVisible(); // rendered at the bottom of EventTile + // Take a snapshot in IRC layout + await expect(page.locator(".mx_ScrollPanel")).toMatchScreenshot( + "long-strings-with-reply-irc-layout.png", + screenshotOptions, + ); - // Make sure the strings do not overflow on modern layout - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group); - await app.timeline.scrollToBottom(); // Scroll again in case - await expect(page.locator(".mx_NewRoomIntro .mx_BaseAvatar")).toBeVisible(); - const lastEventTileGroup = page.locator(".mx_EventTile_last[data-layout='group']"); - await expect(lastEventTileGroup.locator(".mx_MTextBody").first()).toBeVisible(); - await expect(lastEventTileGroup.locator(".mx_EventTile_receiptSent")).toBeVisible(); - await expect(page.locator(".mx_ScrollPanel")).toMatchScreenshot( - "long-strings-with-reply-modern-layout.png", - screenshotOptions, - ); + // Make sure the strings do not overflow on modern layout + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group); + await app.timeline.scrollToBottom(); // Scroll again in case + await expect(page.locator(".mx_NewRoomIntro .mx_BaseAvatar")).toBeVisible(); + const lastEventTileGroup = page.locator(".mx_EventTile_last[data-layout='group']"); + await expect(lastEventTileGroup.locator(".mx_MTextBody").first()).toBeVisible(); + await expect(lastEventTileGroup.locator(".mx_EventTile_receiptSent")).toBeVisible(); + await expect(page.locator(".mx_ScrollPanel")).toMatchScreenshot( + "long-strings-with-reply-modern-layout.png", + screenshotOptions, + ); - // Make sure the strings do not overflow on bubble layout - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); - await app.timeline.scrollToBottom(); // Scroll again in case - await expect(page.locator(".mx_NewRoomIntro .mx_BaseAvatar")).toBeVisible(); - const lastEventTileBubble = page.locator(".mx_EventTile_last[data-layout='bubble']"); - await expect(lastEventTileBubble.locator(".mx_MTextBody").first()).toBeVisible(); - await expect(lastEventTileBubble.locator(".mx_EventTile_receiptSent")).toBeVisible(); - await expect(page.locator(".mx_ScrollPanel")).toMatchScreenshot( - "long-strings-with-reply-bubble-layout.png", - screenshotOptions, - ); - }); + // Make sure the strings do not overflow on bubble layout + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); + await app.timeline.scrollToBottom(); // Scroll again in case + await expect(page.locator(".mx_NewRoomIntro .mx_BaseAvatar")).toBeVisible(); + const lastEventTileBubble = page.locator(".mx_EventTile_last[data-layout='bubble']"); + await expect(lastEventTileBubble.locator(".mx_MTextBody").first()).toBeVisible(); + await expect(lastEventTileBubble.locator(".mx_EventTile_receiptSent")).toBeVisible(); + await expect(page.locator(".mx_ScrollPanel")).toMatchScreenshot( + "long-strings-with-reply-bubble-layout.png", + screenshotOptions, + ); + }, + ); async function testImageRendering(page: Page, app: ElementAppPage, room: { roomId: string }) { await app.viewRoomById(room.roomId); @@ -1176,7 +1213,7 @@ test.describe("Timeline", () => { ); } - test("should render images in the timeline", async ({ page, app, room, context }) => { + test("should render images in the timeline", { tag: "@screenshot" }, async ({ page, app, room, context }) => { await testImageRendering(page, app, room); }); @@ -1188,50 +1225,54 @@ test.describe("Timeline", () => { // In practice, this means this test will *always* succeed because it ends up relying on fallback behaviour tested // above (unless of course the above tests are also broken). test.describe("MSC3916 - Authenticated Media", () => { - test("should render authenticated images in the timeline", async ({ page, app, room, context }) => { - // Note: we have to use `context` instead of `page` for routing, otherwise we'll miss Service Worker events. - // See https://playwright.dev/docs/service-workers-experimental#network-events-and-routing - - // Install our mocks and preventative measures - await context.route("**/_matrix/client/versions", async (route) => { - // Force enable MSC3916/Matrix 1.11, which may require the service worker's internal cache to be cleared later. - const json = await (await route.fetch()).json(); - if (!json["versions"]) json["versions"] = []; - json["versions"].push("v1.11"); - await route.fulfill({ json }); - }); - await context.route("**/_matrix/media/*/download/**", async (route) => { - // should not be called. We don't use `abort` so that it's clearer in the logs what happened. - await route.fulfill({ - status: 500, - json: { errcode: "M_UNKNOWN", error: "Unexpected route called." }, + test( + "should render authenticated images in the timeline", + { tag: "@screenshot" }, + async ({ page, app, room, context }) => { + // Note: we have to use `context` instead of `page` for routing, otherwise we'll miss Service Worker events. + // See https://playwright.dev/docs/service-workers-experimental#network-events-and-routing + + // Install our mocks and preventative measures + await context.route("**/_matrix/client/versions", async (route) => { + // Force enable MSC3916/Matrix 1.11, which may require the service worker's internal cache to be cleared later. + const json = await (await route.fetch()).json(); + if (!json["versions"]) json["versions"] = []; + json["versions"].push("v1.11"); + await route.fulfill({ json }); }); - }); - await context.route("**/_matrix/media/*/thumbnail/**", async (route) => { - // should not be called. We don't use `abort` so that it's clearer in the logs what happened. - await route.fulfill({ - status: 500, - json: { errcode: "M_UNKNOWN", error: "Unexpected route called." }, + await context.route("**/_matrix/media/*/download/**", async (route) => { + // should not be called. We don't use `abort` so that it's clearer in the logs what happened. + await route.fulfill({ + status: 500, + json: { errcode: "M_UNKNOWN", error: "Unexpected route called." }, + }); }); - }); - await context.route("**/_matrix/client/v1/download/**", async (route) => { - expect(route.request().headers()["Authorization"]).toBeDefined(); - // we can't use route.continue() because no configured homeserver supports MSC3916 yet - await route.fulfill({ - body: NEW_AVATAR, + await context.route("**/_matrix/media/*/thumbnail/**", async (route) => { + // should not be called. We don't use `abort` so that it's clearer in the logs what happened. + await route.fulfill({ + status: 500, + json: { errcode: "M_UNKNOWN", error: "Unexpected route called." }, + }); }); - }); - await context.route("**/_matrix/client/v1/thumbnail/**", async (route) => { - expect(route.request().headers()["Authorization"]).toBeDefined(); - // we can't use route.continue() because no configured homeserver supports MSC3916 yet - await route.fulfill({ - body: NEW_AVATAR, + await context.route("**/_matrix/client/v1/download/**", async (route) => { + expect(route.request().headers()["Authorization"]).toBeDefined(); + // we can't use route.continue() because no configured homeserver supports MSC3916 yet + await route.fulfill({ + body: NEW_AVATAR, + }); + }); + await context.route("**/_matrix/client/v1/thumbnail/**", async (route) => { + expect(route.request().headers()["Authorization"]).toBeDefined(); + // we can't use route.continue() because no configured homeserver supports MSC3916 yet + await route.fulfill({ + body: NEW_AVATAR, + }); }); - }); - // We check the same screenshot because there should be no user-visible impact to using authentication. - await testImageRendering(page, app, room); - }); + // We check the same screenshot because there should be no user-visible impact to using authentication. + await testImageRendering(page, app, room); + }, + ); }); }); }); diff --git a/playwright/e2e/user-menu/user-menu.spec.ts b/playwright/e2e/user-menu/user-menu.spec.ts index 0ad21dbded9..268da00f30f 100644 --- a/playwright/e2e/user-menu/user-menu.spec.ts +++ b/playwright/e2e/user-menu/user-menu.spec.ts @@ -11,7 +11,7 @@ import { test, expect } from "../../element-web-test"; test.describe("User Menu", () => { test.use({ displayName: "Jeff" }); - test("should contain our name & userId", async ({ page, user }) => { + test("should contain our name & userId", { tag: "@screenshot" }, async ({ page, user }) => { await page.getByRole("button", { name: "User menu", exact: true }).click(); const menu = page.getByRole("menu"); diff --git a/playwright/e2e/user-onboarding/user-onboarding-new.spec.ts b/playwright/e2e/user-onboarding/user-onboarding-new.spec.ts index f561eb9615e..b89fa3ac70a 100644 --- a/playwright/e2e/user-onboarding/user-onboarding-new.spec.ts +++ b/playwright/e2e/user-onboarding/user-onboarding-new.spec.ts @@ -26,7 +26,7 @@ test.describe("User Onboarding (new user)", () => { await expect(page.locator(".mx_UserOnboardingList")).toBeVisible(); }); - test("page is shown and preference exists", async ({ page, app }) => { + test("page is shown and preference exists", { tag: "@screenshot" }, async ({ page, app }) => { await expect(page.locator(".mx_UserOnboardingPage")).toMatchScreenshot( "User-Onboarding-new-user-page-is-shown-and-preference-exists-1.png", ); @@ -34,7 +34,7 @@ test.describe("User Onboarding (new user)", () => { await expect(page.getByText("Show shortcut to welcome checklist above the room list")).toBeVisible(); }); - test("app download dialog", async ({ page }) => { + test("app download dialog", { tag: "@screenshot" }, async ({ page }) => { await page.getByRole("button", { name: "Download apps" }).click(); await expect( page.getByRole("dialog").getByRole("heading", { level: 1, name: "Download Element" }), diff --git a/playwright/e2e/user-view/user-view.spec.ts b/playwright/e2e/user-view/user-view.spec.ts index 218c63fe743..ff8e9684e93 100644 --- a/playwright/e2e/user-view/user-view.spec.ts +++ b/playwright/e2e/user-view/user-view.spec.ts @@ -14,7 +14,7 @@ test.describe("UserView", () => { botCreateOpts: { displayName: "Usman" }, }); - test("should render the user view as expected", async ({ page, homeserver, user, bot }) => { + test("should render the user view as expected", { tag: "@screenshot" }, async ({ page, homeserver, user, bot }) => { await page.goto(`/#/user/${bot.credentials.userId}`); const rightPanel = page.locator("#mx_RightPanel"); diff --git a/playwright/e2e/widgets/layout.spec.ts b/playwright/e2e/widgets/layout.spec.ts index 41cfece6e80..c80ea44078e 100644 --- a/playwright/e2e/widgets/layout.spec.ts +++ b/playwright/e2e/widgets/layout.spec.ts @@ -70,7 +70,7 @@ test.describe("Widget Layout", () => { await app.viewRoomByName(ROOM_NAME); }); - test("should be set properly", async ({ page }) => { + test("should be set properly", { tag: "@screenshot" }, async ({ page }) => { await expect(page.locator(".mx_AppsDrawer")).toMatchScreenshot("apps-drawer.png"); }); diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts index 8d5229a5100..76e57e33f70 100644 --- a/playwright/element-web-test.ts +++ b/playwright/element-web-test.ts @@ -314,6 +314,10 @@ export const expect = baseExpect.extend({ const testInfo = test.info(); if (!testInfo) throw new Error(`toMatchScreenshot() must be called during the test`); + if (!testInfo.tags.includes("@screenshot")) { + throw new Error("toMatchScreenshot() must be used in a test tagged with @screenshot"); + } + const page = "page" in receiver ? receiver.page() : receiver; let css = ` diff --git a/playwright/plugins/homeserver/synapse/index.ts b/playwright/plugins/homeserver/synapse/index.ts index f11d94a7031..078ca2848f3 100644 --- a/playwright/plugins/homeserver/synapse/index.ts +++ b/playwright/plugins/homeserver/synapse/index.ts @@ -20,7 +20,7 @@ import { randB64Bytes } from "../../utils/rand"; // Docker tag to use for synapse docker image. // We target a specific digest as every now and then a Synapse update will break our CI. // This digest is updated by the playwright-image-updates.yaml workflow periodically. -const DOCKER_TAG = "develop@sha256:e163b15bf4905e4067dece856cca00e6ac8d1d655f4f1307978eee256b3ea775"; +const DOCKER_TAG = "develop@sha256:6b82dba715fa7ae641010b4cc5e71edaeb9cc05a50ac5b9e4ff09afa9cd2a80d"; async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise> { const templateDir = path.join(__dirname, "templates", opts.template); diff --git a/playwright/plugins/oauth_server/README.md b/playwright/plugins/oauth_server/README.md index 541756384f9..5260704a663 100644 --- a/playwright/plugins/oauth_server/README.md +++ b/playwright/plugins/oauth_server/README.md @@ -4,16 +4,16 @@ A very simple OAuth identity provider server. The following endpoints are exposed: -- `/oauth/auth.html`: An OAuth2 [authorization endpoint](https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint). - In a proper OAuth2 system, this would prompt the user to log in; we just give a big "Submit" button (and an - auth code that can be changed if we want the next step to fail). It redirects back to the calling application - with a "code". +- `/oauth/auth.html`: An OAuth2 [authorization endpoint](https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint). + In a proper OAuth2 system, this would prompt the user to log in; we just give a big "Submit" button (and an + auth code that can be changed if we want the next step to fail). It redirects back to the calling application + with a "code". -- `/oauth/token`: An OAuth2 [token endpoint](https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint). - Receives the code issued by "auth.html" and, if it is valid, exchanges it for an OAuth2 access token. +- `/oauth/token`: An OAuth2 [token endpoint](https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint). + Receives the code issued by "auth.html" and, if it is valid, exchanges it for an OAuth2 access token. -- `/oauth/userinfo`: An OAuth2 [userinfo endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo). - Returns details about the owner of the offered access token. +- `/oauth/userinfo`: An OAuth2 [userinfo endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo). + Returns details about the owner of the offered access token. To start the server, do: diff --git a/playwright/snapshots/app-loading/feature-detection.spec.ts/unsupported-browser-CompatibilityView-linux.png b/playwright/snapshots/app-loading/feature-detection.spec.ts/unsupported-browser-CompatibilityView-linux.png index dd8b24beeae..6524a45a672 100644 Binary files a/playwright/snapshots/app-loading/feature-detection.spec.ts/unsupported-browser-CompatibilityView-linux.png and b/playwright/snapshots/app-loading/feature-detection.spec.ts/unsupported-browser-CompatibilityView-linux.png differ diff --git a/playwright/snapshots/app-loading/feature-detection.spec.ts/unsupported-browser-linux.png b/playwright/snapshots/app-loading/feature-detection.spec.ts/unsupported-browser-linux.png index 96ad96e3e11..9a7760bfd01 100644 Binary files a/playwright/snapshots/app-loading/feature-detection.spec.ts/unsupported-browser-linux.png and b/playwright/snapshots/app-loading/feature-detection.spec.ts/unsupported-browser-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--bubble-layout-linux.png index 0887d95a0cf..165033dbe97 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--bubble-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--bubble-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--group-layout-linux.png index 1a082fc9bf3..f309d57bc0c 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--group-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--group-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--irc-layout-linux.png index 4600e591f8c..bd02a2f21a6 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--irc-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--irc-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--bubble-layout-linux.png index 48781632328..16e0624b83c 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--bubble-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--bubble-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--group-layout-linux.png index 1167c0672f2..1e789302563 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--group-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--group-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--irc-layout-linux.png index bdddc786c54..6a43aac7ef0 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--irc-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--irc-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--bubble-layout-linux.png index e9feb85d107..014b8dbaecb 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--bubble-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--bubble-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--group-layout-linux.png index f59e2d7ab19..156d89053ca 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--group-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--group-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--irc-layout-linux.png index 078eae3de4b..caf6e1e6980 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--irc-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--irc-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--bubble-layout-linux.png index 0f0df017fd4..c9591ebf49a 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--bubble-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--bubble-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--group-layout-linux.png index 6cc3f5506f4..794ac11b01c 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--group-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--group-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--irc-layout-linux.png index f92f43c947d..2b6475fbdf3 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--irc-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--irc-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-bubble-layout-linux.png index 61051aa9392..0f643ee43ad 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-bubble-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-bubble-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-bubble-layout-linux.png index 3dded6ef828..ffe1b0be50e 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-bubble-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-bubble-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-group-layout-linux.png index 6866b164758..2b867170ae7 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-group-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-group-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-irc-layout-linux.png index a340f5c1040..459ebd3584d 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-irc-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-irc-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-group-layout-linux.png index 80927840ae3..da97c280299 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-group-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-group-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-irc-layout-linux.png index 346642f84d2..009ea38f7ba 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-irc-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-irc-layout-linux.png differ diff --git a/playwright/snapshots/chat-export/html-export.spec.ts/html-export-linux.png b/playwright/snapshots/chat-export/html-export.spec.ts/html-export-linux.png index 8519e162f2f..465e58ca39e 100644 Binary files a/playwright/snapshots/chat-export/html-export.spec.ts/html-export-linux.png and b/playwright/snapshots/chat-export/html-export.spec.ts/html-export-linux.png differ diff --git a/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png b/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png index 3e684150175..14cb5ce372f 100644 Binary files a/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png and b/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png differ diff --git a/playwright/snapshots/editing/editing.spec.ts/message-edit-history-dialog-linux.png b/playwright/snapshots/editing/editing.spec.ts/message-edit-history-dialog-linux.png index 3504b1ddfa0..81939514ffe 100644 Binary files a/playwright/snapshots/editing/editing.spec.ts/message-edit-history-dialog-linux.png and b/playwright/snapshots/editing/editing.spec.ts/message-edit-history-dialog-linux.png differ diff --git a/playwright/snapshots/file-upload/image-upload.spec.ts/image-upload-preview-linux.png b/playwright/snapshots/file-upload/image-upload.spec.ts/image-upload-preview-linux.png index d2afccb9905..e3c37e79c9b 100644 Binary files a/playwright/snapshots/file-upload/image-upload.spec.ts/image-upload-preview-linux.png and b/playwright/snapshots/file-upload/image-upload.spec.ts/image-upload-preview-linux.png differ diff --git a/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-linux.png b/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-linux.png index 6ce8d95e595..377e1931be8 100644 Binary files a/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-linux.png and b/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-linux.png differ diff --git a/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-verify-email-linux.png b/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-verify-email-linux.png index 5fa7969c574..ec7b9a174d5 100644 Binary files a/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-verify-email-linux.png and b/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-verify-email-linux.png differ diff --git a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-with-user-pill-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-with-user-pill-linux.png index 554400123fe..cc64e8bf781 100644 Binary files a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-with-user-pill-linux.png and b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-with-user-pill-linux.png differ diff --git a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-without-user-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-without-user-linux.png index 9a888fb65c1..a634e876bc6 100644 Binary files a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-without-user-linux.png and b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-without-user-linux.png differ diff --git a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png index 1342f5bf274..d01aae804c9 100644 Binary files a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png and b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png differ diff --git a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png index de476106e98..6e8e2fbddff 100644 Binary files a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png and b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-ltrdisplayname-linux.png index fe924436948..98310517688 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-ltrdisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-ltrdisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-rtldisplayname-linux.png index a0a5dbb8b0d..4bf016b8255 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-ltrdisplayname-linux.png index cf2da6f023f..9f31f518fc0 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-ltrdisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-ltrdisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-rtldisplayname-linux.png index e9aded5a5fb..ee699905239 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-ltrdisplayname-linux.png index 1e29c40c734..d8b02a028ba 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-ltrdisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-ltrdisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-rtldisplayname-linux.png index 104b8f469ef..eea2b474693 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-ltrdisplayname-linux.png index f15894f2b3f..85a3c69c0eb 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-ltrdisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-ltrdisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-rtldisplayname-linux.png index bec538f32da..4d2e9c593dc 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-ltr-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-ltr-ltrdisplayname-linux.png index 772bbbbeec1..5ab0a7e88f6 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/emote-ltr-ltrdisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/emote-ltr-ltrdisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-ltr-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-ltr-rtldisplayname-linux.png index 04f4e0d1f5a..1bb35825782 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/emote-ltr-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/emote-ltr-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-ltrdisplayname-linux.png index 8cc8d6e0887..c3501583f89 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-ltrdisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-ltrdisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-rtldisplayname-linux.png index feb0651650b..a4b7d0a9922 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-ltrdisplayname-linux.png index 181e1fb9cab..537f1dd2c48 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-ltrdisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-ltrdisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-rtldisplayname-linux.png index ffae099bdc6..26bd9f7523c 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-rtl-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-rtl-ltrdisplayname-linux.png index ee9d8b8a43e..27357dc5035 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/emote-rtl-ltrdisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/emote-rtl-ltrdisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-rtl-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-rtl-rtldisplayname-linux.png index 19075ea869f..3f50a1406f1 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/emote-rtl-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/emote-rtl-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/reply-message-ltr-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/reply-message-ltr-ltrdisplayname-linux.png index eebd5330793..a4de383fbac 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/reply-message-ltr-ltrdisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/reply-message-ltr-ltrdisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/reply-message-ltr-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/reply-message-ltr-rtldisplayname-linux.png index ed0c69fb8dd..37096f025c1 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/reply-message-ltr-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/reply-message-ltr-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-ltrdisplayname-linux.png index 1a0f5577b68..a3acc741f3d 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-ltrdisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-ltrdisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-rtldisplayname-linux.png index 587170ee1ce..52ced56c83e 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/permalinks/permalinks.spec.ts/permalink-rendering-linux.png b/playwright/snapshots/permalinks/permalinks.spec.ts/permalink-rendering-linux.png index 0ec66fa4ef1..62ffcead998 100644 Binary files a/playwright/snapshots/permalinks/permalinks.spec.ts/permalink-rendering-linux.png and b/playwright/snapshots/permalinks/permalinks.spec.ts/permalink-rendering-linux.png differ diff --git a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-Msg1-linux.png b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-Msg1-linux.png index 97b751ec6a1..f39caf654a2 100644 Binary files a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-Msg1-linux.png and b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-Msg1-linux.png differ diff --git a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-1-Msg1-linux.png b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-1-Msg1-linux.png index c3c4d57acf4..ac13a2152d9 100644 Binary files a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-1-Msg1-linux.png and b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-1-Msg1-linux.png differ diff --git a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-2-Msg1-linux.png b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-2-Msg1-linux.png index 3c87967137a..20917ae16f3 100644 Binary files a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-2-Msg1-linux.png and b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-2-Msg1-linux.png differ diff --git a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-2-Msg2-linux.png b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-2-Msg2-linux.png index 996c289bccb..aaf8c720eb5 100644 Binary files a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-2-Msg2-linux.png and b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-2-Msg2-linux.png differ diff --git a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg1-linux.png b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg1-linux.png index 35c7e6fb25c..7ef356c589d 100644 Binary files a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg1-linux.png and b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg1-linux.png differ diff --git a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg2-linux.png b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg2-linux.png index 610a44adf1c..a62d8182b1a 100644 Binary files a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg2-linux.png and b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg2-linux.png differ diff --git a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg3-linux.png b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg3-linux.png index 62d1f3401b1..fa548aaadcc 100644 Binary files a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg3-linux.png and b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg3-linux.png differ diff --git a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg4-linux.png b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg4-linux.png index 2c27dd00296..bc2062f98d7 100644 Binary files a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg4-linux.png and b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg4-linux.png differ diff --git a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-messages-list-empty-linux.png b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-messages-list-empty-linux.png index 16c694de957..84ff71a49ca 100644 Binary files a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-messages-list-empty-linux.png and b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-messages-list-empty-linux.png differ diff --git a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/unpin-all-dialog-linux.png b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/unpin-all-dialog-linux.png index e6f1005395c..39a8905711b 100644 Binary files a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/unpin-all-dialog-linux.png and b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/unpin-all-dialog-linux.png differ diff --git a/playwright/snapshots/polls/polls.spec.ts/Polls-Timeline-tile-no-votes-linux.png b/playwright/snapshots/polls/polls.spec.ts/Polls-Timeline-tile-no-votes-linux.png index 1ade373ba8f..6818add73e4 100644 Binary files a/playwright/snapshots/polls/polls.spec.ts/Polls-Timeline-tile-no-votes-linux.png and b/playwright/snapshots/polls/polls.spec.ts/Polls-Timeline-tile-no-votes-linux.png differ diff --git a/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png b/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png index bbc29d4be3c..a842b686dda 100644 Binary files a/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png and b/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png differ diff --git a/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png b/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png index 120b80320b2..d9d12951df0 100644 Binary files a/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png and b/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png differ diff --git a/playwright/snapshots/register/email.spec.ts/registration-check-your-email-linux.png b/playwright/snapshots/register/email.spec.ts/registration-check-your-email-linux.png index a8709dfd99c..4b724d77725 100644 Binary files a/playwright/snapshots/register/email.spec.ts/registration-check-your-email-linux.png and b/playwright/snapshots/register/email.spec.ts/registration-check-your-email-linux.png differ diff --git a/playwright/snapshots/register/register.spec.ts/email-prompt-linux.png b/playwright/snapshots/register/register.spec.ts/email-prompt-linux.png index 5d32d5b6196..f78a2a0b165 100644 Binary files a/playwright/snapshots/register/register.spec.ts/email-prompt-linux.png and b/playwright/snapshots/register/register.spec.ts/email-prompt-linux.png differ diff --git a/playwright/snapshots/register/register.spec.ts/registration-linux.png b/playwright/snapshots/register/register.spec.ts/registration-linux.png index a4f3be59a9f..481c74fcda4 100644 Binary files a/playwright/snapshots/register/register.spec.ts/registration-linux.png and b/playwright/snapshots/register/register.spec.ts/registration-linux.png differ diff --git a/playwright/snapshots/register/register.spec.ts/server-picker-linux.png b/playwright/snapshots/register/register.spec.ts/server-picker-linux.png index c0e398debb9..da924bae82c 100644 Binary files a/playwright/snapshots/register/register.spec.ts/server-picker-linux.png and b/playwright/snapshots/register/register.spec.ts/server-picker-linux.png differ diff --git a/playwright/snapshots/register/register.spec.ts/terms-prompt-linux.png b/playwright/snapshots/register/register.spec.ts/terms-prompt-linux.png index 310dcf25d9d..f5326e9d2d4 100644 Binary files a/playwright/snapshots/register/register.spec.ts/terms-prompt-linux.png and b/playwright/snapshots/register/register.spec.ts/terms-prompt-linux.png differ diff --git a/playwright/snapshots/register/register.spec.ts/use-case-selection-linux.png b/playwright/snapshots/register/register.spec.ts/use-case-selection-linux.png index efde30d4fa5..1dd98b51e15 100644 Binary files a/playwright/snapshots/register/register.spec.ts/use-case-selection-linux.png and b/playwright/snapshots/register/register.spec.ts/use-case-selection-linux.png differ diff --git a/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-Threads-Activity-Centre-linux.png b/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-Threads-Activity-Centre-linux.png index 8a0b738a1b8..fee99165ab9 100644 Binary files a/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-Threads-Activity-Centre-linux.png and b/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-Threads-Activity-Centre-linux.png differ diff --git a/playwright/snapshots/right-panel/file-panel.spec.ts/empty-linux.png b/playwright/snapshots/right-panel/file-panel.spec.ts/empty-linux.png index 9edd65a6336..0566a21175a 100644 Binary files a/playwright/snapshots/right-panel/file-panel.spec.ts/empty-linux.png and b/playwright/snapshots/right-panel/file-panel.spec.ts/empty-linux.png differ diff --git a/playwright/snapshots/right-panel/file-panel.spec.ts/file-tiles-list-linux.png b/playwright/snapshots/right-panel/file-panel.spec.ts/file-tiles-list-linux.png index 8b248e52404..1c75a923732 100644 Binary files a/playwright/snapshots/right-panel/file-panel.spec.ts/file-tiles-list-linux.png and b/playwright/snapshots/right-panel/file-panel.spec.ts/file-tiles-list-linux.png differ diff --git a/playwright/snapshots/right-panel/notification-panel.spec.ts/empty-linux.png b/playwright/snapshots/right-panel/notification-panel.spec.ts/empty-linux.png index 26481bfe5d7..cd228be8ca7 100644 Binary files a/playwright/snapshots/right-panel/notification-panel.spec.ts/empty-linux.png and b/playwright/snapshots/right-panel/notification-panel.spec.ts/empty-linux.png differ diff --git a/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png b/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png index 4e3c67393b6..8ba6f98e115 100644 Binary files a/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png and b/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png differ diff --git a/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-no-results-linux.png b/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-no-results-linux.png index 82779d4cc98..783e468c6e7 100644 Binary files a/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-no-results-linux.png and b/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-no-results-linux.png differ diff --git a/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-one-result-linux.png b/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-one-result-linux.png index 666dbe689ad..aaa504f4e37 100644 Binary files a/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-one-result-linux.png and b/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-one-result-linux.png differ diff --git a/playwright/snapshots/room/room-header.spec.ts/room-header-linux.png b/playwright/snapshots/room/room-header.spec.ts/room-header-linux.png index ea591089b43..afc5d53fab3 100644 Binary files a/playwright/snapshots/room/room-header.spec.ts/room-header-linux.png and b/playwright/snapshots/room/room-header.spec.ts/room-header-linux.png differ diff --git a/playwright/snapshots/room/room-header.spec.ts/room-header-long-name-linux.png b/playwright/snapshots/room/room-header.spec.ts/room-header-long-name-linux.png index 742ed777128..ce15e3e1516 100644 Binary files a/playwright/snapshots/room/room-header.spec.ts/room-header-long-name-linux.png and b/playwright/snapshots/room/room-header.spec.ts/room-header-long-name-linux.png differ diff --git a/playwright/snapshots/room/room-header.spec.ts/room-header-video-room-linux.png b/playwright/snapshots/room/room-header.spec.ts/room-header-video-room-linux.png index bb10e28aba6..bd31e502d7f 100644 Binary files a/playwright/snapshots/room/room-header.spec.ts/room-header-video-room-linux.png and b/playwright/snapshots/room/room-header.spec.ts/room-header-video-room-linux.png differ diff --git a/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-linux.png b/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-linux.png index 702dab55f9e..c641f2a0aaf 100644 Binary files a/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-linux.png and b/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-linux.png differ diff --git a/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-smallscreen-linux.png b/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-smallscreen-linux.png index d1f28308f16..b9d81c5d5a8 100644 Binary files a/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-smallscreen-linux.png and b/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-smallscreen-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png index a30e9969b62..75a4852f9b9 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png index ba0b6f24b8d..b2b71375bd2 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-added-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-added-linux.png index d44c1073072..0d18bff1c26 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-added-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-added-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-linux.png index 76a0befd339..9cadcde4155 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-removed-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-removed-linux.png index cdafb6e9ee8..1ec17661fe4 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-removed-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-removed-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-dark-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-dark-linux.png index 3b9c2431380..75db794a1a3 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-dark-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-dark-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-light-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-light-linux.png index ca909171166..357790598d0 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-light-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-light-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-match-system-enabled-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-match-system-enabled-linux.png index 1aed777c8d6..42f27d10bf5 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-match-system-enabled-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-match-system-enabled-linux.png differ diff --git a/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png b/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png index b9b39690001..c08a36a808c 100644 Binary files a/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png and b/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png differ diff --git a/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png b/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png index 082650056f9..7d952052519 100644 Binary files a/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png and b/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png differ diff --git a/playwright/snapshots/settings/security-user-settings-tab.spec.ts/Security-user-settings-tab-with-posthog-enable-b5d89-csLearnMoreDialog-should-be-rendered-properly-1-linux.png b/playwright/snapshots/settings/security-user-settings-tab.spec.ts/Security-user-settings-tab-with-posthog-enable-b5d89-csLearnMoreDialog-should-be-rendered-properly-1-linux.png index e858838ab98..720cc415480 100644 Binary files a/playwright/snapshots/settings/security-user-settings-tab.spec.ts/Security-user-settings-tab-with-posthog-enable-b5d89-csLearnMoreDialog-should-be-rendered-properly-1-linux.png and b/playwright/snapshots/settings/security-user-settings-tab.spec.ts/Security-user-settings-tab-with-posthog-enable-b5d89-csLearnMoreDialog-should-be-rendered-properly-1-linux.png differ diff --git a/playwright/snapshots/share-dialog/share-dialog.spec.ts/share-dialog-event-linux.png b/playwright/snapshots/share-dialog/share-dialog.spec.ts/share-dialog-event-linux.png new file mode 100644 index 00000000000..541eaa2fa55 Binary files /dev/null and b/playwright/snapshots/share-dialog/share-dialog.spec.ts/share-dialog-event-linux.png differ diff --git a/playwright/snapshots/share-dialog/share-dialog.spec.ts/share-dialog-room-linux.png b/playwright/snapshots/share-dialog/share-dialog.spec.ts/share-dialog-room-linux.png new file mode 100644 index 00000000000..f6fbd2fa430 Binary files /dev/null and b/playwright/snapshots/share-dialog/share-dialog.spec.ts/share-dialog-room-linux.png differ diff --git a/playwright/snapshots/share-dialog/share-dialog.spec.ts/share-dialog-user-linux.png b/playwright/snapshots/share-dialog/share-dialog.spec.ts/share-dialog-user-linux.png new file mode 100644 index 00000000000..2fb39b9f5f0 Binary files /dev/null and b/playwright/snapshots/share-dialog/share-dialog.spec.ts/share-dialog-user-linux.png differ diff --git a/playwright/snapshots/spaces/spaces.spec.ts/invite-teammates-dialog-linux.png b/playwright/snapshots/spaces/spaces.spec.ts/invite-teammates-dialog-linux.png index ef7536d455d..2244dc7cf91 100644 Binary files a/playwright/snapshots/spaces/spaces.spec.ts/invite-teammates-dialog-linux.png and b/playwright/snapshots/spaces/spaces.spec.ts/invite-teammates-dialog-linux.png differ diff --git a/playwright/snapshots/spaces/spaces.spec.ts/space-create-menu-linux.png b/playwright/snapshots/spaces/spaces.spec.ts/space-create-menu-linux.png index 3dae06ad52f..d178f257ca7 100644 Binary files a/playwright/snapshots/spaces/spaces.spec.ts/space-create-menu-linux.png and b/playwright/snapshots/spaces/spaces.spec.ts/space-create-menu-linux.png differ diff --git a/playwright/snapshots/spaces/spaces.spec.ts/space-panel-collapsed-linux.png b/playwright/snapshots/spaces/spaces.spec.ts/space-panel-collapsed-linux.png index 8485df9e3ae..312ca2580c6 100644 Binary files a/playwright/snapshots/spaces/spaces.spec.ts/space-panel-collapsed-linux.png and b/playwright/snapshots/spaces/spaces.spec.ts/space-panel-collapsed-linux.png differ diff --git a/playwright/snapshots/spaces/spaces.spec.ts/space-panel-expanded-linux.png b/playwright/snapshots/spaces/spaces.spec.ts/space-panel-expanded-linux.png index 296563e8708..f0606ce47ab 100644 Binary files a/playwright/snapshots/spaces/spaces.spec.ts/space-panel-expanded-linux.png and b/playwright/snapshots/spaces/spaces.spec.ts/space-panel-expanded-linux.png differ diff --git a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-button-expanded-linux.png b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-button-expanded-linux.png index 85e483aca01..2ad6315d9eb 100644 Binary files a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-button-expanded-linux.png and b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-button-expanded-linux.png differ diff --git a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-expanded-linux.png b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-expanded-linux.png index 85e483aca01..2ad6315d9eb 100644 Binary files a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-expanded-linux.png and b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-expanded-linux.png differ diff --git a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-linux.png b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-linux.png index 5d9490c1d1a..591d22c3c43 100644 Binary files a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-linux.png and b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-linux.png differ diff --git a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-no-indicator-linux.png b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-no-indicator-linux.png index c7a1f9fea15..fedc5cd7500 100644 Binary files a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-no-indicator-linux.png and b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-no-indicator-linux.png differ diff --git a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-mix-unread-linux.png b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-mix-unread-linux.png index ec5a8193d25..0785ecbe97d 100644 Binary files a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-mix-unread-linux.png and b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-mix-unread-linux.png differ diff --git a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-notification-unread-linux.png b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-notification-unread-linux.png index f0f6cee3e6a..8b6af97d6e6 100644 Binary files a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-notification-unread-linux.png and b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-notification-unread-linux.png differ diff --git a/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png b/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png index d8a4f542d42..e43e41dd795 100644 Binary files a/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png and b/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png index b0b7efb95b6..348db69cfc3 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-bubble-layout-linux.png index d05e7432b31..42ee5a0acb3 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-bubble-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-bubble-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/configured-room-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/configured-room-irc-layout-linux.png index 375f2e96404..92532e3d9c8 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/configured-room-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/configured-room-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-line-inline-start-margin-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-line-inline-start-margin-irc-layout-linux.png index 334221670d0..1e50cd3c0fb 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-line-inline-start-margin-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-line-inline-start-margin-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-bubble-layout-linux.png index c0a01c99fb0..167f9b6855a 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-bubble-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-bubble-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-compact-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-compact-modern-layout-linux.png index 87e65a86ae5..0682bf760f4 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-compact-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-compact-modern-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-layout-linux.png index 98ec9e0cf66..b9899fb177e 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-modern-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-modern-linux.png index 445d616ea40..ca4ce5933f6 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-modern-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-modern-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-bubble-layout-linux.png index 5be5e1ec05a..b0960a11887 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-bubble-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-bubble-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-compact-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-compact-modern-layout-linux.png index 01ed4d5b0c9..a7637b6b94d 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-compact-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-compact-modern-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-irc-layout-linux.png index 74817fced2b..a609a4cd0df 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-modern-layout-linux.png index d2d5b2cf73b..fe50abef0cc 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-modern-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-and-messages-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-and-messages-irc-layout-linux.png index eaa1e70db58..ac6dadc9625 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-and-messages-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-and-messages-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-bubble-layout-linux.png index e8a60625915..8e833be3085 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-bubble-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-bubble-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-emote-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-emote-irc-layout-linux.png index 7ce18d56e28..3e9e78ca997 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-emote-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-emote-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-irc-layout-linux.png index 334221670d0..1e50cd3c0fb 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-modern-layout-linux.png index a98369e7b65..b81a9d68a86 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-modern-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-redaction-placeholder-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-redaction-placeholder-linux.png index 91926e1c15a..58ba6c57033 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-redaction-placeholder-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-redaction-placeholder-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/highlighted-search-results-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/highlighted-search-results-linux.png index 008ebc82e3e..a99cfb68cfc 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/highlighted-search-results-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/highlighted-search-results-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/hovered-hidden-event-line-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/hovered-hidden-event-line-irc-layout-linux.png index 100dc86c7a5..5fb0c283b5f 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/hovered-hidden-event-line-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/hovered-hidden-event-line-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/image-in-timeline-default-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/image-in-timeline-default-layout-linux.png index 1d05e321052..b8a24fb3a47 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/image-in-timeline-default-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/image-in-timeline-default-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-bubble-layout-linux.png index 7a918a959ab..879e647f5e3 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-bubble-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-bubble-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-irc-layout-linux.png index e52b371fe70..b52a56a22e3 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-modern-layout-linux.png index ec66bf20138..bbaba1dbea7 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-modern-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/search-aux-panel-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/search-aux-panel-linux.png index b50f1762224..125c47ee35b 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/search-aux-panel-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/search-aux-panel-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/search-results-with-TextualEvent-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/search-results-with-TextualEvent-linux.png index cf489b18363..5aa6bcea193 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/search-results-with-TextualEvent-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/search-results-with-TextualEvent-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/url-preview-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/url-preview-linux.png index a0c833115c6..56220d88d1d 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/url-preview-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/url-preview-linux.png differ diff --git a/playwright/snapshots/user-menu/user-menu.spec.ts/user-menu-linux.png b/playwright/snapshots/user-menu/user-menu.spec.ts/user-menu-linux.png index edc0ad17ad0..bfaa18d4c3e 100644 Binary files a/playwright/snapshots/user-menu/user-menu.spec.ts/user-menu-linux.png and b/playwright/snapshots/user-menu/user-menu.spec.ts/user-menu-linux.png differ diff --git a/playwright/snapshots/user-onboarding/user-onboarding-new.spec.ts/User-Onboarding-new-user-app-download-dialog-1-linux.png b/playwright/snapshots/user-onboarding/user-onboarding-new.spec.ts/User-Onboarding-new-user-app-download-dialog-1-linux.png index 0c7fc94a0ed..024886d01e6 100644 Binary files a/playwright/snapshots/user-onboarding/user-onboarding-new.spec.ts/User-Onboarding-new-user-app-download-dialog-1-linux.png and b/playwright/snapshots/user-onboarding/user-onboarding-new.spec.ts/User-Onboarding-new-user-app-download-dialog-1-linux.png differ diff --git a/playwright/snapshots/user-onboarding/user-onboarding-new.spec.ts/User-Onboarding-new-user-page-is-shown-and-preference-exists-1-linux.png b/playwright/snapshots/user-onboarding/user-onboarding-new.spec.ts/User-Onboarding-new-user-page-is-shown-and-preference-exists-1-linux.png index 3112b0fcf22..1042d92e764 100644 Binary files a/playwright/snapshots/user-onboarding/user-onboarding-new.spec.ts/User-Onboarding-new-user-page-is-shown-and-preference-exists-1-linux.png and b/playwright/snapshots/user-onboarding/user-onboarding-new.spec.ts/User-Onboarding-new-user-page-is-shown-and-preference-exists-1-linux.png differ diff --git a/playwright/snapshots/user-view/user-view.spec.ts/user-info-linux.png b/playwright/snapshots/user-view/user-view.spec.ts/user-info-linux.png index 0612fe1ba72..30206f1a255 100644 Binary files a/playwright/snapshots/user-view/user-view.spec.ts/user-info-linux.png and b/playwright/snapshots/user-view/user-view.spec.ts/user-info-linux.png differ diff --git a/playwright/snapshots/widgets/layout.spec.ts/apps-drawer-linux.png b/playwright/snapshots/widgets/layout.spec.ts/apps-drawer-linux.png index 203991bc325..6331d373b36 100644 Binary files a/playwright/snapshots/widgets/layout.spec.ts/apps-drawer-linux.png and b/playwright/snapshots/widgets/layout.spec.ts/apps-drawer-linux.png differ diff --git a/res/css/_common.pcss b/res/css/_common.pcss index 15ba02b6b88..74328af39b2 100644 --- a/res/css/_common.pcss +++ b/res/css/_common.pcss @@ -596,7 +596,7 @@ legend { .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button), + ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button), .mx_Dialog input[type="submit"], .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton), .mx_Dialog_buttons input[type="submit"] { @@ -616,14 +616,16 @@ legend { .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):last-child { + ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not( + .mx_ShareDialog button + ):last-child { margin-right: 0px; } .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):focus, + ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button):focus, .mx_Dialog input[type="submit"]:focus, .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):focus, .mx_Dialog_buttons input[type="submit"]:focus { @@ -635,7 +637,7 @@ legend { .mx_Dialog_buttons button.mx_Dialog_primary:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button), + ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button), .mx_Dialog_buttons input[type="submit"].mx_Dialog_primary { color: var(--cpd-color-text-on-solid-primary); background-color: var(--cpd-color-bg-action-primary-rest); @@ -648,7 +650,7 @@ legend { .mx_Dialog_buttons button.danger:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(.mx_UserProfileSettings button):not( .mx_ThemeChoicePanel_CustomTheme button - ):not(.mx_UnpinAllDialog button), + ):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button), .mx_Dialog_buttons input[type="submit"].danger { background-color: var(--cpd-color-bg-critical-primary); border: solid 1px var(--cpd-color-bg-critical-primary); @@ -664,7 +666,7 @@ legend { .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):disabled, + ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button):disabled, .mx_Dialog input[type="submit"]:disabled, .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):disabled, .mx_Dialog_buttons input[type="submit"]:disabled { diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 0fcdf6dee6e..e9a53cd43cc 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -393,9 +393,3 @@ @import "./views/voip/_LegacyCallViewHeader.pcss"; @import "./views/voip/_LegacyCallViewSidebar.pcss"; @import "./views/voip/_VideoFeed.pcss"; -@import "./voice-broadcast/atoms/_LiveBadge.pcss"; -@import "./voice-broadcast/atoms/_VoiceBroadcastControl.pcss"; -@import "./voice-broadcast/atoms/_VoiceBroadcastHeader.pcss"; -@import "./voice-broadcast/atoms/_VoiceBroadcastRecordingConnectionError.pcss"; -@import "./voice-broadcast/atoms/_VoiceBroadcastRoomSubtitle.pcss"; -@import "./voice-broadcast/molecules/_VoiceBroadcastBody.pcss"; diff --git a/res/css/components/views/settings/shared/_SettingsSubsection.pcss b/res/css/components/views/settings/shared/_SettingsSubsection.pcss index 14261a59c86..ec20b86d98e 100644 --- a/res/css/components/views/settings/shared/_SettingsSubsection.pcss +++ b/res/css/components/views/settings/shared/_SettingsSubsection.pcss @@ -13,7 +13,16 @@ Please see LICENSE files in the repository root for full details. &.mx_SettingsSubsection_newUi { display: flex; flex-direction: column; - gap: var(--cpd-space-8x); + gap: var(--cpd-space-6x); + } + + *[role="separator"] { + /** + * The gap between subsections is 32px and inside the subsection is 24px. + * The separator separates the subsections, so it should have the same gap as the subsections. + * We add 12px and the separator spacing to the top margin to make the separator visually centered between the subsections. + */ + margin-top: calc(var(--cpd-space-3x) + var(--cpd-separator-spacing)); } } diff --git a/res/css/structures/_UserMenu.pcss b/res/css/structures/_UserMenu.pcss index 741a4e90dca..d24a6e4ac7a 100644 --- a/res/css/structures/_UserMenu.pcss +++ b/res/css/structures/_UserMenu.pcss @@ -22,20 +22,6 @@ Please see LICENSE files in the repository root for full details. pointer-events: none; /* makes the avatar non-draggable */ } } - - .mx_UserMenu_userAvatarLive { - align-items: center; - background-color: $alert; - border-radius: 6px; - color: $live-badge-color; - display: flex; - height: 12px; - justify-content: center; - left: 25px; - position: absolute; - top: 20px; - width: 12px; - } } .mx_UserMenu_contextMenuButton { diff --git a/res/css/views/dialogs/_ShareDialog.pcss b/res/css/views/dialogs/_ShareDialog.pcss index 086222af31b..cfede43aae7 100644 --- a/res/css/views/dialogs/_ShareDialog.pcss +++ b/res/css/views/dialogs/_ShareDialog.pcss @@ -5,50 +5,73 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -.mx_ShareDialog hr { - margin-top: 25px; - margin-bottom: 25px; - border-color: $light-fg-color; -} +.mx_ShareDialog { + /* Value from figma design */ + width: 416px; + + .mx_Dialog_header { + text-align: center; + margin-bottom: var(--cpd-space-6x); + /* Override dialog header padding to able to center it */ + padding-inline-end: 0; + } -.mx_ShareDialog .mx_ShareDialog_content { - margin: 10px 0; + .mx_ShareDialog_content { + display: flex; + flex-direction: column; + gap: var(--cpd-space-6x); + align-items: center; - .mx_CopyableText { - width: unset; /* full width */ + .mx_ShareDialog_top { + display: flex; + flex-direction: column; + gap: var(--cpd-space-4x); + align-items: center; + width: 100%; - > a { - text-decoration: none; - flex-shrink: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + span { + text-align: center; + font: var(--cpd-font-body-sm-semibold); + color: var(--cpd-color-text-secondary); + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + width: 100%; + } } - } -} -.mx_ShareDialog_split { - display: flex; - flex-wrap: wrap; -} + label { + display: inline-flex; + gap: var(--cpd-space-3x); + justify-content: center; + align-items: center; + font: var(--cpd-font-body-md-medium); + } -.mx_ShareDialog_qrcode_container { - float: left; - height: 256px; - width: 256px; - margin-right: 64px; -} + button { + width: 100%; + } -.mx_ShareDialog_qrcode_container + .mx_ShareDialog_social_container { - width: 299px; -} + .mx_ShareDialog_social { + display: flex; + gap: var(--cpd-space-3x); + justify-content: center; -.mx_ShareDialog_social_container { - display: inline-block; -} + a { + width: 48px; + height: 48px; + border-radius: 99px; + box-sizing: border-box; + border: 1px solid var(--cpd-color-border-interactive-secondary); + display: flex; + justify-content: center; + align-items: center; -.mx_ShareDialog_social_icon { - display: inline-grid; - margin-right: 10px; - margin-bottom: 10px; + img { + width: 24px; + height: 24px; + } + } + } + } } diff --git a/res/css/views/rooms/_MessageComposer.pcss b/res/css/views/rooms/_MessageComposer.pcss index 3f11e9fa6c8..73ac15c9c9e 100644 --- a/res/css/views/rooms/_MessageComposer.pcss +++ b/res/css/views/rooms/_MessageComposer.pcss @@ -256,10 +256,6 @@ Please see LICENSE files in the repository root for full details. mask-image: url("@vector-im/compound-design-tokens/icons/mic-on-solid.svg"); } -.mx_MessageComposer_voiceBroadcast::before { - mask-image: url("$(res)/img/element-icons/live.svg"); -} - .mx_MessageComposer_plain_text::before { mask-image: url("$(res)/img/element-icons/room/composer/plain_text.svg"); } diff --git a/res/css/voice-broadcast/atoms/_LiveBadge.pcss b/res/css/voice-broadcast/atoms/_LiveBadge.pcss deleted file mode 100644 index 7d5f23819be..00000000000 --- a/res/css/voice-broadcast/atoms/_LiveBadge.pcss +++ /dev/null @@ -1,23 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -.mx_LiveBadge { - align-items: center; - background-color: $alert; - border-radius: 2px; - color: $live-badge-color; - display: inline-flex; - font-size: $font-12px; - font-weight: var(--cpd-font-weight-semibold); - gap: $spacing-4; - padding: 2px 4px; -} - -.mx_LiveBadge--grey { - background-color: $quaternary-content; -} diff --git a/res/css/voice-broadcast/atoms/_VoiceBroadcastControl.pcss b/res/css/voice-broadcast/atoms/_VoiceBroadcastControl.pcss deleted file mode 100644 index 5bd7bfe0982..00000000000 --- a/res/css/voice-broadcast/atoms/_VoiceBroadcastControl.pcss +++ /dev/null @@ -1,28 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -.mx_VoiceBroadcastControl { - align-items: center; - background-color: $background; - border-radius: 50%; - color: $secondary-content; - display: flex; - flex: 0 0 32px; - height: 32px; - justify-content: center; - width: 32px; -} - -.mx_VoiceBroadcastControl-recording { - color: $alert; -} - -.mx_VoiceBroadcastControl-play .mx_Icon { - left: 1px; - position: relative; -} diff --git a/res/css/voice-broadcast/atoms/_VoiceBroadcastHeader.pcss b/res/css/voice-broadcast/atoms/_VoiceBroadcastHeader.pcss deleted file mode 100644 index c5e21233b7c..00000000000 --- a/res/css/voice-broadcast/atoms/_VoiceBroadcastHeader.pcss +++ /dev/null @@ -1,60 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -.mx_VoiceBroadcastHeader { - align-items: flex-start; - display: flex; - gap: $spacing-8; - line-height: 20px; - margin-bottom: $spacing-16; - min-width: 0; -} - -.mx_VoiceBroadcastHeader_content { - flex-grow: 1; - min-width: 0; -} - -.mx_VoiceBroadcastHeader_room_wrapper { - align-items: center; - display: flex; - gap: 4px; - justify-content: flex-start; -} - -.mx_VoiceBroadcastHeader_room { - font-size: $font-12px; - font-weight: var(--cpd-font-weight-semibold); - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.mx_VoiceBroadcastHeader_line { - align-items: center; - color: $secondary-content; - font-size: $font-12px; - display: flex; - gap: $spacing-4; - - .mx_Spinner { - flex: 0 0 14px; - padding: 1px; - } - - span { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } -} - -.mx_VoiceBroadcastHeader_mic--clickable { - cursor: pointer; -} diff --git a/res/css/voice-broadcast/atoms/_VoiceBroadcastRecordingConnectionError.pcss b/res/css/voice-broadcast/atoms/_VoiceBroadcastRecordingConnectionError.pcss deleted file mode 100644 index f21c0bb7331..00000000000 --- a/res/css/voice-broadcast/atoms/_VoiceBroadcastRecordingConnectionError.pcss +++ /dev/null @@ -1,18 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -.mx_VoiceBroadcastRecordingConnectionError { - align-items: center; - color: $alert; - display: flex; - gap: $spacing-12; - - svg path { - fill: $alert; - } -} diff --git a/res/css/voice-broadcast/atoms/_VoiceBroadcastRoomSubtitle.pcss b/res/css/voice-broadcast/atoms/_VoiceBroadcastRoomSubtitle.pcss deleted file mode 100644 index e0748e76269..00000000000 --- a/res/css/voice-broadcast/atoms/_VoiceBroadcastRoomSubtitle.pcss +++ /dev/null @@ -1,14 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -.mx_RoomTile .mx_RoomTile_titleContainer .mx_RoomTile_subtitle.mx_RoomTile_subtitle--voice-broadcast { - align-items: center; - color: $alert; - display: flex; - gap: $spacing-4; -} diff --git a/res/css/voice-broadcast/molecules/_VoiceBroadcastBody.pcss b/res/css/voice-broadcast/molecules/_VoiceBroadcastBody.pcss deleted file mode 100644 index 45ed0e98f97..00000000000 --- a/res/css/voice-broadcast/molecules/_VoiceBroadcastBody.pcss +++ /dev/null @@ -1,75 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -.mx_VoiceBroadcastBody { - background-color: $quinary-content; - border-radius: 8px; - color: $secondary-content; - display: inline-block; - font-size: $font-12px; - padding: $spacing-12; - width: 271px; - - .mx_Clock { - line-height: 1; - } -} - -.mx_VoiceBroadcastBody--pip { - background-color: $system; - box-shadow: 0 2px 8px 0 #0000004a; -} - -.mx_VoiceBroadcastBody--small { - display: flex; - gap: $spacing-8; - width: 192px; - - .mx_VoiceBroadcastHeader { - margin-bottom: 0; - } - - .mx_VoiceBroadcastControl { - align-self: center; - } - - .mx_LiveBadge { - margin-top: 4px; - } -} - -.mx_VoiceBroadcastBody_divider { - background-color: $quinary-content; - border: 0; - height: 1px; - margin: $spacing-12 0; -} - -.mx_VoiceBroadcastBody_controls { - align-items: center; - display: flex; - gap: $spacing-32; - justify-content: center; - margin-bottom: $spacing-8; -} - -.mx_VoiceBroadcastBody_timerow { - display: flex; - justify-content: space-between; -} - -.mx_AccessibleButton.mx_VoiceBroadcastBody_blockButton { - display: flex; - gap: $spacing-8; -} - -.mx_VoiceBroadcastBody__small-close { - right: 8px; - position: absolute; - top: 8px; -} diff --git a/res/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlX5qhExfHwNJU.woff2 b/res/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlX5qhExfHwNJU.woff2 deleted file mode 100644 index 880f06af785..00000000000 Binary files a/res/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlX5qhExfHwNJU.woff2 and /dev/null differ diff --git a/res/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlZ5qhExfHw.woff2 b/res/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlZ5qhExfHw.woff2 deleted file mode 100644 index 9fe96559d1f..00000000000 Binary files a/res/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlZ5qhExfHw.woff2 and /dev/null differ diff --git a/res/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71n5_zaDpwm80E.woff2 b/res/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71n5_zaDpwm80E.woff2 deleted file mode 100644 index cd79590d99b..00000000000 Binary files a/res/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71n5_zaDpwm80E.woff2 and /dev/null differ diff --git a/res/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71p5_zaDpwm.woff2 b/res/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71p5_zaDpwm.woff2 deleted file mode 100644 index cf26d38db4b..00000000000 Binary files a/res/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71p5_zaDpwm.woff2 and /dev/null differ diff --git a/res/fonts/Inter/Inter-Bold.woff b/res/fonts/Inter/Inter-Bold.woff deleted file mode 100644 index 2ec7ac3d213..00000000000 Binary files a/res/fonts/Inter/Inter-Bold.woff and /dev/null differ diff --git a/res/fonts/Inter/Inter-Bold.woff2 b/res/fonts/Inter/Inter-Bold.woff2 deleted file mode 100644 index 6989c99229e..00000000000 Binary files a/res/fonts/Inter/Inter-Bold.woff2 and /dev/null differ diff --git a/res/fonts/Inter/Inter-BoldItalic.woff b/res/fonts/Inter/Inter-BoldItalic.woff deleted file mode 100644 index aa35b797455..00000000000 Binary files a/res/fonts/Inter/Inter-BoldItalic.woff and /dev/null differ diff --git a/res/fonts/Inter/Inter-BoldItalic.woff2 b/res/fonts/Inter/Inter-BoldItalic.woff2 deleted file mode 100644 index 18b4c1ce5ec..00000000000 Binary files a/res/fonts/Inter/Inter-BoldItalic.woff2 and /dev/null differ diff --git a/res/fonts/Inter/Inter-Italic.woff b/res/fonts/Inter/Inter-Italic.woff deleted file mode 100644 index 4b765bd5929..00000000000 Binary files a/res/fonts/Inter/Inter-Italic.woff and /dev/null differ diff --git a/res/fonts/Inter/Inter-Italic.woff2 b/res/fonts/Inter/Inter-Italic.woff2 deleted file mode 100644 index bd5f255a989..00000000000 Binary files a/res/fonts/Inter/Inter-Italic.woff2 and /dev/null differ diff --git a/res/fonts/Inter/Inter-Medium.woff b/res/fonts/Inter/Inter-Medium.woff deleted file mode 100644 index 7d55f34ccab..00000000000 Binary files a/res/fonts/Inter/Inter-Medium.woff and /dev/null differ diff --git a/res/fonts/Inter/Inter-Medium.woff2 b/res/fonts/Inter/Inter-Medium.woff2 deleted file mode 100644 index a916b47fc84..00000000000 Binary files a/res/fonts/Inter/Inter-Medium.woff2 and /dev/null differ diff --git a/res/fonts/Inter/Inter-MediumItalic.woff b/res/fonts/Inter/Inter-MediumItalic.woff deleted file mode 100644 index 422ab0576ad..00000000000 Binary files a/res/fonts/Inter/Inter-MediumItalic.woff and /dev/null differ diff --git a/res/fonts/Inter/Inter-MediumItalic.woff2 b/res/fonts/Inter/Inter-MediumItalic.woff2 deleted file mode 100644 index f623924aeab..00000000000 Binary files a/res/fonts/Inter/Inter-MediumItalic.woff2 and /dev/null differ diff --git a/res/fonts/Inter/Inter-Regular.woff b/res/fonts/Inter/Inter-Regular.woff deleted file mode 100644 index 7ff51b7d8fb..00000000000 Binary files a/res/fonts/Inter/Inter-Regular.woff and /dev/null differ diff --git a/res/fonts/Inter/Inter-Regular.woff2 b/res/fonts/Inter/Inter-Regular.woff2 deleted file mode 100644 index 554aed66127..00000000000 Binary files a/res/fonts/Inter/Inter-Regular.woff2 and /dev/null differ diff --git a/res/fonts/Inter/Inter-SemiBold.woff b/res/fonts/Inter/Inter-SemiBold.woff deleted file mode 100644 index 76e507a515b..00000000000 Binary files a/res/fonts/Inter/Inter-SemiBold.woff and /dev/null differ diff --git a/res/fonts/Inter/Inter-SemiBold.woff2 b/res/fonts/Inter/Inter-SemiBold.woff2 deleted file mode 100644 index 9307998993f..00000000000 Binary files a/res/fonts/Inter/Inter-SemiBold.woff2 and /dev/null differ diff --git a/res/fonts/Inter/Inter-SemiBoldItalic.woff b/res/fonts/Inter/Inter-SemiBoldItalic.woff deleted file mode 100644 index 382181212d4..00000000000 Binary files a/res/fonts/Inter/Inter-SemiBoldItalic.woff and /dev/null differ diff --git a/res/fonts/Inter/Inter-SemiBoldItalic.woff2 b/res/fonts/Inter/Inter-SemiBoldItalic.woff2 deleted file mode 100644 index f19f5505ec1..00000000000 Binary files a/res/fonts/Inter/Inter-SemiBoldItalic.woff2 and /dev/null differ diff --git a/res/fonts/Open_Sans/LICENSE.txt b/res/fonts/Open_Sans/LICENSE.txt deleted file mode 100755 index 75b52484ea4..00000000000 --- a/res/fonts/Open_Sans/LICENSE.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/res/fonts/Open_Sans/OpenSans-Bold.ttf b/res/fonts/Open_Sans/OpenSans-Bold.ttf deleted file mode 100755 index fd79d43bea0..00000000000 Binary files a/res/fonts/Open_Sans/OpenSans-Bold.ttf and /dev/null differ diff --git a/res/fonts/Open_Sans/OpenSans-BoldItalic.ttf b/res/fonts/Open_Sans/OpenSans-BoldItalic.ttf deleted file mode 100755 index 9bc800958a4..00000000000 Binary files a/res/fonts/Open_Sans/OpenSans-BoldItalic.ttf and /dev/null differ diff --git a/res/fonts/Open_Sans/OpenSans-Italic.ttf b/res/fonts/Open_Sans/OpenSans-Italic.ttf deleted file mode 100755 index c90da48ff3b..00000000000 Binary files a/res/fonts/Open_Sans/OpenSans-Italic.ttf and /dev/null differ diff --git a/res/fonts/Open_Sans/OpenSans-Regular.ttf b/res/fonts/Open_Sans/OpenSans-Regular.ttf deleted file mode 100755 index db433349b70..00000000000 Binary files a/res/fonts/Open_Sans/OpenSans-Regular.ttf and /dev/null differ diff --git a/res/fonts/Open_Sans/OpenSans-Semibold.ttf b/res/fonts/Open_Sans/OpenSans-Semibold.ttf deleted file mode 100755 index 1a7679e3949..00000000000 Binary files a/res/fonts/Open_Sans/OpenSans-Semibold.ttf and /dev/null differ diff --git a/res/fonts/Open_Sans/OpenSans-SemiboldItalic.ttf b/res/fonts/Open_Sans/OpenSans-SemiboldItalic.ttf deleted file mode 100755 index 59b6d16b065..00000000000 Binary files a/res/fonts/Open_Sans/OpenSans-SemiboldItalic.ttf and /dev/null differ diff --git a/res/fonts/Twemoji_Mozilla/TwemojiMozilla-sbix.woff2 b/res/fonts/Twemoji_Mozilla/TwemojiMozilla-sbix.woff2 deleted file mode 100644 index 90f444b1a10..00000000000 Binary files a/res/fonts/Twemoji_Mozilla/TwemojiMozilla-sbix.woff2 and /dev/null differ diff --git a/res/themes/dark/css/_dark.pcss b/res/themes/dark/css/_dark.pcss index 8b0673f692b..2d3ea2e4f42 100644 --- a/res/themes/dark/css/_dark.pcss +++ b/res/themes/dark/css/_dark.pcss @@ -240,11 +240,6 @@ $location-live-secondary-color: #deddfd; } /* ******************** */ -/* Voice Broadcast */ -/* ******************** */ -$live-badge-color: #ffffff; -/* ******************** */ - /* One-off colors */ /* ******************** */ $progressbar-bg-color: var(--cpd-color-gray-200); diff --git a/res/themes/legacy-dark/css/_legacy-dark.pcss b/res/themes/legacy-dark/css/_legacy-dark.pcss index 45bb1870f1b..ea5228b6c74 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.pcss +++ b/res/themes/legacy-dark/css/_legacy-dark.pcss @@ -226,11 +226,6 @@ $location-live-color: #5c56f5; $location-live-secondary-color: #deddfd; /* ******************** */ -/* Voice Broadcast */ -/* ******************** */ -$live-badge-color: #ffffff; -/* ******************** */ - body { color-scheme: dark; } diff --git a/res/themes/legacy-light/css/_legacy-light.pcss b/res/themes/legacy-light/css/_legacy-light.pcss index 76e0eec588a..32ca7d3d1a0 100644 --- a/res/themes/legacy-light/css/_legacy-light.pcss +++ b/res/themes/legacy-light/css/_legacy-light.pcss @@ -325,11 +325,6 @@ $location-live-color: #5c56f5; $location-live-secondary-color: #deddfd; /* ******************** */ -/* Voice Broadcast */ -/* ******************** */ -$live-badge-color: #ffffff; -/* ******************** */ - body { color-scheme: light; } diff --git a/res/themes/light/css/_fonts.pcss b/res/themes/light/css/_fonts.pcss index 62613fcee5a..8044f47b219 100644 --- a/res/themes/light/css/_fonts.pcss +++ b/res/themes/light/css/_fonts.pcss @@ -1,145 +1,20 @@ /* the 'src' links are relative to the bundle.css, which is in a subdirectory. */ -/* Inter unexpectedly contains various codepoints which collide with emoji, even - when variation-16 is applied to request the emoji variant. From eyeballing - the emoji picker, these are: 20e3, 23cf, 24c2, 25a0-25c1, 2665, 2764, 2b06, 2b1c. - Therefore we define a unicode-range to load which excludes the glyphs - (to avoid having to maintain a fork of Inter). */ - -$inter-unicode-range: U+0000-20e2, U+20e4-23ce, U+23d0-24c1, U+24c3-259f, U+25c2-2664, U+2666-2763, U+2765-2b05, - U+2b07-2b1b, U+2b1d-10FFFF; - -@font-face { - font-family: "Inter"; - font-style: normal; - font-weight: 400; - font-display: swap; - unicode-range: $inter-unicode-range; - src: - url("$(res)/fonts/Inter/Inter-Regular.woff2?v=3.18") format("woff2"), - url("$(res)/fonts/Inter/Inter-Regular.woff?v=3.18") format("woff"); -} +/* Twemoji COLR */ @font-face { - font-family: "Inter"; - font-style: italic; + font-family: "Twemoji"; font-weight: 400; - font-display: swap; - unicode-range: $inter-unicode-range; - src: - url("$(res)/fonts/Inter/Inter-Italic.woff2?v=3.18") format("woff2"), - url("$(res)/fonts/Inter/Inter-Italic.woff?v=3.18") format("woff"); -} - -@font-face { - font-family: "Inter"; - font-style: normal; - font-weight: 500; - font-display: swap; - unicode-range: $inter-unicode-range; - src: - url("$(res)/fonts/Inter/Inter-Medium.woff2?v=3.18") format("woff2"), - url("$(res)/fonts/Inter/Inter-Medium.woff?v=3.18") format("woff"); + src: url("$(res)/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2") format("woff2"); } +/* For at least Chrome on Windows 10, we have to explictly add extra weights for the emoji to appear in bold messages, etc. */ @font-face { - font-family: "Inter"; - font-style: italic; - font-weight: 500; - font-display: swap; - unicode-range: $inter-unicode-range; - src: - url("$(res)/fonts/Inter/Inter-MediumItalic.woff2?v=3.18") format("woff2"), - url("$(res)/fonts/Inter/Inter-MediumItalic.woff?v=3.18") format("woff"); -} - -@font-face { - font-family: "Inter"; - font-style: normal; - font-weight: 600; - font-display: swap; - unicode-range: $inter-unicode-range; - src: - url("$(res)/fonts/Inter/Inter-SemiBold.woff2?v=3.18") format("woff2"), - url("$(res)/fonts/Inter/Inter-SemiBold.woff?v=3.18") format("woff"); -} -@font-face { - font-family: "Inter"; - font-style: italic; + font-family: "Twemoji"; font-weight: 600; - font-display: swap; - unicode-range: $inter-unicode-range; - src: - url("$(res)/fonts/Inter/Inter-SemiBoldItalic.woff2?v=3.18") format("woff2"), - url("$(res)/fonts/Inter/Inter-SemiBoldItalic.woff?v=3.18") format("woff"); -} - -@font-face { - font-family: "Inter"; - font-style: normal; - font-weight: 700; - font-display: swap; - unicode-range: $inter-unicode-range; - src: - url("$(res)/fonts/Inter/Inter-Bold.woff2?v=3.18") format("woff2"), - url("$(res)/fonts/Inter/Inter-Bold.woff?v=3.18") format("woff"); -} -@font-face { - font-family: "Inter"; - font-style: italic; - font-weight: 700; - font-display: swap; - unicode-range: $inter-unicode-range; - src: - url("$(res)/fonts/Inter/Inter-BoldItalic.woff2?v=3.18") format("woff2"), - url("$(res)/fonts/Inter/Inter-BoldItalic.woff?v=3.18") format("woff"); -} - -/* latin-ext */ -@font-face { - font-family: "Inconsolata"; - font-style: normal; - font-weight: 400; - src: - local("Inconsolata Regular"), - local("Inconsolata-Regular"), - url("$(res)/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlX5qhExfHwNJU.woff2") format("woff2"); - unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; -} -/* latin */ -@font-face { - font-family: "Inconsolata"; - font-style: normal; - font-weight: 400; - font-display: swap; - src: - local("Inconsolata Regular"), - local("Inconsolata-Regular"), - url("$(res)/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlZ5qhExfHw.woff2") format("woff2"); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, - U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} -/* latin-ext */ -@font-face { - font-family: "Inconsolata"; - font-style: normal; - font-weight: 700; - font-display: swap; - src: - local("Inconsolata Bold"), - local("Inconsolata-Bold"), - url("$(res)/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71n5_zaDpwm80E.woff2") format("woff2"); - unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; + src: url("$(res)/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2") format("woff2"); } -/* latin */ @font-face { - font-family: "Inconsolata"; - font-style: normal; + font-family: "Twemoji"; font-weight: 700; - font-display: swap; - src: - local("Inconsolata Bold"), - local("Inconsolata-Bold"), - url("$(res)/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71p5_zaDpwm.woff2") format("woff2"); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, - U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; + src: url("$(res)/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2") format("woff2"); } diff --git a/res/themes/light/css/_light.pcss b/res/themes/light/css/_light.pcss index 5f278c6f160..1a1705a9c15 100644 --- a/res/themes/light/css/_light.pcss +++ b/res/themes/light/css/_light.pcss @@ -10,8 +10,8 @@ /* Noto Color Emoji contains digits, in fixed-width, therefore causing digits in flowed text to stand out. TODO: Consider putting all emoji fonts to the end rather than the front. */ -$font-family: "Inter", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Arial", "Helvetica", sans-serif, - "Noto Color Emoji"; +$font-family: "Inter", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Arial", "Helvetica", + sans-serif, "Noto Color Emoji"; $monospace-font-family: "Inconsolata", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Courier", monospace, "Noto Color Emoji"; @@ -355,11 +355,6 @@ $location-live-color: var(--cpd-color-purple-900); $location-live-secondary-color: var(--cpd-color-purple-600); /* ******************** */ -/* Voice Broadcast */ -/* ******************** */ -$live-badge-color: var(--cpd-color-icon-on-solid-primary); -/* ******************** */ - body { color-scheme: light; } diff --git a/scripts/copy-i18n.py b/scripts/copy-i18n.py deleted file mode 100755 index 07b12712397..00000000000 --- a/scripts/copy-i18n.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python - -import json -import sys -import os - -if len(sys.argv) < 3: - print "Usage: %s " % (sys.argv[0],) - print "eg. %s pt_BR.json pt.json" % (sys.argv[0],) - print - print "Adds any translations to that exist in but not " - sys.exit(1) - -srcpath = sys.argv[1] -dstpath = sys.argv[2] -tmppath = dstpath + ".tmp" - -with open(srcpath) as f: - src = json.load(f) - -with open(dstpath) as f: - dst = json.load(f) - -toAdd = {} -for k,v in src.iteritems(): - if k not in dst: - print "Adding %s" % (k,) - toAdd[k] = v - -# don't just json.dumps as we'll probably re-order all the keys (and they're -# not in any given order so we can't just sort_keys). Append them to the end. -with open(dstpath) as ifp: - with open(tmppath, 'w') as ofp: - for line in ifp: - strippedline = line.strip() - if strippedline in ('{', '}'): - ofp.write(line) - elif strippedline.endswith(','): - ofp.write(line) - else: - ofp.write(' '+strippedline+',') - toAddStr = json.dumps(toAdd, indent=4, separators=(',', ': '), ensure_ascii=False, encoding="utf8").strip("{}\n") - ofp.write("\n") - ofp.write(toAddStr.encode('utf8')) - ofp.write("\n") - -os.rename(tmppath, dstpath) diff --git a/scripts/fetch-develop.deps.sh b/scripts/fetch-develop.deps.sh deleted file mode 100755 index 5814b43ff74..00000000000 --- a/scripts/fetch-develop.deps.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env bash - -# Fetches the js-sdk dependency for development or testing purposes -# If there exists a branch of that dependency with the same name as -# the branch the current checkout is on, use that branch. Otherwise, -# use develop. - -set -x - -GIT_CLONE_ARGS=("$@") -[ -z "$defbranch" ] && defbranch="develop" - -# clone a specific branch of a github repo -function clone() { - org=$1 - repo=$2 - branch=$3 - - # Chop 'origin' off the start as jenkins ends up using - # branches on the origin, but this doesn't work if we - # specify the branch when cloning. - branch=${branch#origin/} - - if [ -n "$branch" ] - then - echo "Trying to use $org/$repo#$branch" - # Disable auth prompts: https://serverfault.com/a/665959 - GIT_TERMINAL_PROMPT=0 git clone https://github.com/$org/$repo.git $repo --branch $branch \ - "${GIT_CLONE_ARGS[@]}" - return $? - fi - return 1 -} - -function dodep() { - deforg=$1 - defrepo=$2 - rm -rf $defrepo - - # Try the PR author's branch in case it exists on the deps as well. - # Try the target branch of the push or PR. - # Use the default branch as the last resort. - if [[ "$BUILDKITE" == true ]]; then - # If BUILDKITE_BRANCH is set, it will contain either: - # * "branch" when the author's branch and target branch are in the same repo - # * "author:branch" when the author's branch is in their fork - # We can split on `:` into an array to check. - BUILDKITE_BRANCH_ARRAY=(${BUILDKITE_BRANCH//:/ }) - if [[ "${#BUILDKITE_BRANCH_ARRAY[@]}" == "2" ]]; then - prAuthor=${BUILDKITE_BRANCH_ARRAY[0]} - prBranch=${BUILDKITE_BRANCH_ARRAY[1]} - else - prAuthor=$deforg - prBranch=$BUILDKITE_BRANCH - fi - clone $prAuthor $defrepo $prBranch || - clone $deforg $defrepo $BUILDKITE_PULL_REQUEST_BASE_BRANCH || - clone $deforg $defrepo $defbranch || - return $? - else - clone $deforg $defrepo $ghprbSourceBranch || - clone $deforg $defrepo $GIT_BRANCH || - clone $deforg $defrepo `git rev-parse --abbrev-ref HEAD` || - clone $deforg $defrepo $defbranch || - return $? - fi - - echo "$defrepo set to branch "`git -C "$defrepo" rev-parse --abbrev-ref HEAD` -} - -############################## - -echo 'Setting up matrix-js-sdk' - -dodep matrix-org matrix-js-sdk - -pushd matrix-js-sdk -yarn link -yarn install --frozen-lockfile -popd - -yarn link matrix-js-sdk - -############################## diff --git a/scripts/genflags.sh b/scripts/genflags.sh deleted file mode 100755 index aa882a99b4c..00000000000 --- a/scripts/genflags.sh +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright 2017-2024 New Vector Ltd. - -# SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -# Please see LICENSE in the repository root for full details. - - -# genflags.sh - Generates pngs for use with CountryDropdown.js -# -# Dependencies: -# - imagemagick --with-rsvg (because default imagemagick SVG -# renderer does not produce accurate results) -# -# on macOS, this is most easily done with: -# brew install imagemagick --with-librsvg -# -# This will clone the googlei18n flag repo before converting -# all phonenumber.js-supported country flags (as SVGs) into -# PNGs that can be used by CountryDropdown.js. - -set -e - -# Allow CTRL+C to terminate the script -trap "echo Exited!; exit;" SIGINT SIGTERM - -# git clone the google repo to get flag SVGs -git clone git@github.com:googlei18n/region-flags -for f in region-flags/svg/*.svg; do - # Skip state flags - if [[ $f =~ [A-Z]{2}-[A-Z]{2,3}.svg ]] ; then - echo "Skipping state flag "$f - continue - fi - - # Skip countries not included in phonenumber.js - if [[ $f =~ (AC|CP|DG|EA|EU|IC|TA|UM|UN|XK).svg ]] ; then - echo "Skipping non-phonenumber supported flag "$f - continue - fi - - # Run imagemagick convert - # -background none : transparent background - # -resize 50x30 : resize the flag to have a height of 15px (2x) - # By default, aspect ratio is respected so the width will - # be correct and not necessarily 25px. - # -filter Lanczos : use sharper resampling to avoid muddiness - # -gravity Center : keep the image central when adding an -extent - # -border 1 : add a 1px border around the flag - # -bordercolor : set the border colour - # -extent 54x54 : surround the image with padding so that it - # has the dimensions 27x27px (2x). - convert $f -background none -filter Lanczos -resize 50x30 \ - -gravity Center -border 1 -bordercolor \#e0e0e0 \ - -extent 54x54 $f.png - - # $f.png will be region-flags/svg/XX.svg.png at this point - - # Extract filename from path $f - newname=${f##*/} - # Replace .svg with .png - newname=${newname%.svg}.png - # Move the file to flags directory - mv $f.png ../res/flags/$newname - echo "Generated res/flags/"$newname -done diff --git a/src/@types/matrix-js-sdk.d.ts b/src/@types/matrix-js-sdk.d.ts index 73366f2fee3..41ccfcbb3b2 100644 --- a/src/@types/matrix-js-sdk.d.ts +++ b/src/@types/matrix-js-sdk.d.ts @@ -10,7 +10,6 @@ import type { IWidget } from "matrix-widget-api"; import type { BLURHASH_FIELD } from "../utils/image-media"; import type { JitsiCallMemberEventType, JitsiCallMemberContent } from "../call-types"; import type { ILayoutStateEvent, WIDGET_LAYOUT_EVENT_TYPE } from "../stores/widgets/types"; -import type { VoiceBroadcastInfoEventContent, VoiceBroadcastInfoEventType } from "../voice-broadcast/types"; import type { EncryptedFile } from "matrix-js-sdk/src/types"; // Extend Matrix JS SDK types via Typescript declaration merging to support unspecced event fields and types @@ -37,9 +36,6 @@ declare module "matrix-js-sdk/src/types" { "im.vector.modular.widgets": IWidget | {}; [WIDGET_LAYOUT_EVENT_TYPE]: ILayoutStateEvent; - // Unstable voice broadcast state events - [VoiceBroadcastInfoEventType]: VoiceBroadcastInfoEventContent; - // Element custom state events "im.vector.web.settings": Record; "org.matrix.room.preview_urls": { disable: boolean }; @@ -78,7 +74,5 @@ declare module "matrix-js-sdk/src/types" { waveform?: number[]; }; "org.matrix.msc3245.voice"?: {}; - - "io.element.voice_broadcast_chunk"?: { sequence: number }; } } diff --git a/src/@types/png-chunks-extract.d.ts b/src/@types/png-chunks-extract.d.ts index 38ea84c32af..c767b40655b 100644 --- a/src/@types/png-chunks-extract.d.ts +++ b/src/@types/png-chunks-extract.d.ts @@ -12,7 +12,7 @@ declare module "png-chunks-extract" { data: Uint8Array; } - function extractPngChunks(data: Uint8Array | Buffer): IChunk[]; + function extractPngChunks(data: Uint8Array): IChunk[]; export default extractPngChunks; } diff --git a/src/DecryptionFailureTracker.ts b/src/DecryptionFailureTracker.ts index 1e07ba252b5..bfcd126eeaa 100644 --- a/src/DecryptionFailureTracker.ts +++ b/src/DecryptionFailureTracker.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { ScalableBloomFilter } from "bloom-filters"; +import ScalableBloomFilter from "bloom-filters/dist/bloom/scalable-bloom-filter"; import { HttpApiEvent, MatrixClient, MatrixEventEvent, MatrixEvent } from "matrix-js-sdk/src/matrix"; import { Error as ErrorEvent } from "@matrix-org/analytics-events/types/typescript/Error"; import { DecryptionFailureCode, CryptoEvent } from "matrix-js-sdk/src/crypto-api"; diff --git a/src/IConfigOptions.ts b/src/IConfigOptions.ts index 72bee5d0abc..5dd500402d4 100644 --- a/src/IConfigOptions.ts +++ b/src/IConfigOptions.ts @@ -175,13 +175,6 @@ export interface IConfigOptions { sync_timeline_limit?: number; dangerously_allow_unsafe_and_insecure_passwords?: boolean; // developer option - voice_broadcast?: { - // length per voice chunk in seconds - chunk_length?: number; - // max voice broadcast length in seconds - max_length?: number; - }; - user_notice?: { title: string; description: string; diff --git a/src/LegacyCallHandler.tsx b/src/LegacyCallHandler.tsx index a06480e9cd7..b804ca0084d 100644 --- a/src/LegacyCallHandler.tsx +++ b/src/LegacyCallHandler.tsx @@ -55,8 +55,6 @@ import { OpenInviteDialogPayload } from "./dispatcher/payloads/OpenInviteDialogP import { findDMForUser } from "./utils/dm/findDMForUser"; import { getJoinedNonFunctionalMembers } from "./utils/room/getJoinedNonFunctionalMembers"; import { localNotificationsAreSilenced } from "./utils/notifications"; -import { SdkContextClass } from "./contexts/SDKContext"; -import { showCantStartACallDialog } from "./voice-broadcast/utils/showCantStartACallDialog"; import { isNotNull } from "./Typeguards"; import { BackgroundAudio } from "./audio/BackgroundAudio"; import { Jitsi } from "./widgets/Jitsi.ts"; @@ -859,15 +857,6 @@ export default class LegacyCallHandler extends EventEmitter { return; } - // Pause current broadcast, if any - SdkContextClass.instance.voiceBroadcastPlaybacksStore.getCurrent()?.pause(); - - if (SdkContextClass.instance.voiceBroadcastRecordingsStore.getCurrent()) { - // Do not start a call, if recording a broadcast - showCantStartACallDialog(); - return; - } - // We might be using managed hybrid widgets if (isManagedHybridWidgetEnabled(room)) { await addManagedHybridWidget(room); diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 9804ab5d82d..ce879531185 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -35,13 +35,11 @@ import IdentityAuthClient from "./IdentityAuthClient"; import { crossSigningCallbacks } from "./SecurityManager"; import { SlidingSyncManager } from "./SlidingSyncManager"; import { _t, UserFriendlyError } from "./languageHandler"; -import { SettingLevel } from "./settings/SettingLevel"; import MatrixClientBackedController from "./settings/controllers/MatrixClientBackedController"; import ErrorDialog from "./components/views/dialogs/ErrorDialog"; import PlatformPeg from "./PlatformPeg"; import { formatList } from "./utils/FormattingUtils"; import SdkConfig from "./SdkConfig"; -import { Features } from "./settings/Settings"; import { setDeviceIsolationMode } from "./settings/controllers/DeviceIsolationModeController.ts"; export interface IMatrixClientCreds { @@ -333,11 +331,6 @@ class MatrixClientPegClass implements IMatrixClientPeg { logger.error("Warning! Not using an encryption key for rust crypto store."); } - // Record the fact that we used the Rust crypto stack with this client. This just guards against people - // rolling back to versions of EW that did not default to Rust crypto (which would lead to an error, since - // we cannot migrate from Rust to Legacy crypto). - await SettingsStore.setValue(Features.RustCrypto, null, SettingLevel.DEVICE, true); - await this.matrixClient.initRustCrypto({ storageKey: rustCryptoStoreKey, storagePassword: rustCryptoStorePassword, diff --git a/src/Notifier.ts b/src/Notifier.ts index 961d2171a86..c724c4780cd 100644 --- a/src/Notifier.ts +++ b/src/Notifier.ts @@ -49,8 +49,6 @@ import { SdkContextClass } from "./contexts/SDKContext"; import { localNotificationsAreSilenced, createLocalNotificationSettingsIfNeeded } from "./utils/notifications"; import { getIncomingCallToastKey, IncomingCallToast } from "./toasts/IncomingCallToast"; import ToastStore from "./stores/ToastStore"; -import { VoiceBroadcastChunkEventType, VoiceBroadcastInfoEventType } from "./voice-broadcast"; -import { getSenderName } from "./utils/event/getSenderName"; import { stripPlainReply } from "./utils/Reply"; import { BackgroundAudio } from "./audio/BackgroundAudio"; @@ -81,17 +79,6 @@ const msgTypeHandlers: Record string | null> = { return TextForEvent.textForLocationEvent(event)(); }, [MsgType.Audio]: (event: MatrixEvent): string | null => { - if (event.getContent()?.[VoiceBroadcastChunkEventType]) { - if (event.getContent()?.[VoiceBroadcastChunkEventType]?.sequence === 1) { - // Show a notification for the first broadcast chunk. - // At this point a user received something to listen to. - return _t("notifier|io.element.voice_broadcast_chunk", { senderName: getSenderName(event) }); - } - - // Mute other broadcast chunks - return null; - } - return TextForEvent.textForEvent(event, MatrixClientPeg.safeGet()); }, }; @@ -460,8 +447,6 @@ class NotifierClass extends TypedEventEmitter m.sender === cli.getUserId()); - if (EventType.CallNotify === ev.getType() && (ev.getAge() ?? 0) < 10000 && !thisUserHasConnectedDevice) { + // Check maximum age (<= 15 seconds) of a call notify event that will trigger a ringing notification + if (EventType.CallNotify === ev.getType() && (ev.getAge() ?? 0) < 15000 && !thisUserHasConnectedDevice) { const content = ev.getContent(); const roomId = ev.getRoomId(); if (typeof content.call_id !== "string") { diff --git a/src/SdkConfig.ts b/src/SdkConfig.ts index cbec03381ac..c9a0010b0ff 100644 --- a/src/SdkConfig.ts +++ b/src/SdkConfig.ts @@ -46,10 +46,6 @@ export const DEFAULTS: DeepReadonly = { logo: require("../res/img/element-desktop-logo.svg").default, url: "https://element.io/get-started", }, - voice_broadcast: { - chunk_length: 2 * 60, // two minutes - max_length: 4 * 60 * 60, // four hours - }, feedback: { existing_issues_url: diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index f97dff786fa..e8122b2dbf2 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -7,8 +7,8 @@ Please see LICENSE files in the repository root for full details. */ import { lazy } from "react"; -import { ICryptoCallbacks, SecretStorage } from "matrix-js-sdk/src/matrix"; -import { deriveRecoveryKeyFromPassphrase, decodeRecoveryKey } from "matrix-js-sdk/src/crypto-api"; +import { SecretStorage } from "matrix-js-sdk/src/matrix"; +import { deriveRecoveryKeyFromPassphrase, decodeRecoveryKey, CryptoCallbacks } from "matrix-js-sdk/src/crypto-api"; import { logger } from "matrix-js-sdk/src/logger"; import Modal from "./Modal"; @@ -159,7 +159,7 @@ function cacheSecretStorageKey( } } -export const crossSigningCallbacks: ICryptoCallbacks = { +export const crossSigningCallbacks: CryptoCallbacks = { getSecretStorageKey, cacheSecretStorageKey, }; diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 5fc4d8c3576..b739cec12fc 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -49,7 +49,6 @@ import VoipUserMapper from "./VoipUserMapper"; import { htmlSerializeFromMdIfNeeded } from "./editor/serialize"; import { leaveRoomBehaviour } from "./utils/leave-behaviour"; import { MatrixClientPeg } from "./MatrixClientPeg"; -import { getDeviceCryptoInfo } from "./utils/crypto/deviceInfo"; import { isCurrentLocalRoom, reject, singleMxcUpload, success, successSync } from "./slash-commands/utils"; import { deop, op } from "./slash-commands/op"; import { CommandCategories } from "./slash-commands/interface"; @@ -658,69 +657,6 @@ export const Commands = [ category: CommandCategories.admin, renderingTypes: [TimelineRenderingType.Room], }), - new Command({ - command: "verify", - args: " ", - description: _td("slash_command|verify"), - runFn: function (cli, roomId, threadId, args) { - if (args) { - const matches = args.match(/^(\S+) +(\S+) +(\S+)$/); - if (matches) { - const userId = matches[1]; - const deviceId = matches[2]; - const fingerprint = matches[3]; - - return success( - (async (): Promise => { - const device = await getDeviceCryptoInfo(cli, userId, deviceId); - if (!device) { - throw new UserFriendlyError("slash_command|verify_unknown_pair", { - userId, - deviceId, - cause: undefined, - }); - } - const deviceTrust = await cli.getCrypto()?.getDeviceVerificationStatus(userId, deviceId); - - if (deviceTrust?.isVerified()) { - if (device.getFingerprint() === fingerprint) { - throw new UserFriendlyError("slash_command|verify_nop"); - } else { - throw new UserFriendlyError("slash_command|verify_nop_warning_mismatch"); - } - } - - if (device.getFingerprint() !== fingerprint) { - const fprint = device.getFingerprint(); - throw new UserFriendlyError("slash_command|verify_mismatch", { - fprint, - userId, - deviceId, - fingerprint, - cause: undefined, - }); - } - - await cli.setDeviceVerified(userId, deviceId, true); - - // Tell the user we verified everything - Modal.createDialog(InfoDialog, { - title: _t("slash_command|verify_success_title"), - description: ( -
-

{_t("slash_command|verify_success_description", { userId, deviceId })}

-
- ), - }); - })(), - ); - } - } - return reject(this.getUsage()); - }, - category: CommandCategories.advanced, - renderingTypes: [TimelineRenderingType.Room], - }), new Command({ command: "discardsession", description: _td("slash_command|discardsession"), diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx index 1ffae62aea3..49d8b739b73 100644 --- a/src/TextForEvent.tsx +++ b/src/TextForEvent.tsx @@ -36,7 +36,6 @@ import AccessibleButton from "./components/views/elements/AccessibleButton"; import RightPanelStore from "./stores/right-panel/RightPanelStore"; import { highlightEvent, isLocationEvent } from "./utils/EventUtils"; import { ElementCall } from "./models/Call"; -import { textForVoiceBroadcastStoppedEvent, VoiceBroadcastInfoEventType } from "./voice-broadcast"; import { getSenderName } from "./utils/event/getSenderName"; import PosthogTrackers from "./PosthogTrackers.ts"; @@ -906,7 +905,6 @@ const stateHandlers: IHandlers = { // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) "im.vector.modular.widgets": textForWidgetEvent, [WIDGET_LAYOUT_EVENT_TYPE]: textForWidgetLayoutEvent, - [VoiceBroadcastInfoEventType]: textForVoiceBroadcastStoppedEvent, }; // Add all the Mjolnir stuff to the renderer diff --git a/src/accessibility/context_menu/ContextMenuButton.tsx b/src/accessibility/context_menu/ContextMenuButton.tsx index d8c7d912c1b..9d8b5585e3b 100644 --- a/src/accessibility/context_menu/ContextMenuButton.tsx +++ b/src/accessibility/context_menu/ContextMenuButton.tsx @@ -8,25 +8,24 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { ComponentProps, forwardRef, Ref } from "react"; +import React, { forwardRef, Ref } from "react"; -import AccessibleButton from "../../components/views/elements/AccessibleButton"; +import AccessibleButton, { ButtonProps } from "../../components/views/elements/AccessibleButton"; -type Props = ComponentProps> & { +type Props = ButtonProps & { label?: string; // whether the context menu is currently open isExpanded: boolean; }; // Semantic component for representing the AccessibleButton which launches a -export const ContextMenuButton = forwardRef(function ( - { label, isExpanded, children, onClick, onContextMenu, element, ...props }: Props, - ref: Ref, +export const ContextMenuButton = forwardRef(function ( + { label, isExpanded, children, onClick, onContextMenu, ...props }: Props, + ref: Ref, ) { return ( = ComponentProps> & { +type Props = ButtonProps & { // whether the context menu is currently open isExpanded: boolean; }; // Semantic component for representing the AccessibleButton which launches a -export const ContextMenuTooltipButton = forwardRef(function ( - { isExpanded, children, onClick, onContextMenu, element, ...props }: Props, - ref: Ref, +export const ContextMenuTooltipButton = forwardRef(function ( + { isExpanded, children, onClick, onContextMenu, ...props }: Props, + ref: Ref, ) { return ( = Omit< - ComponentProps>, - "inputRef" | "tabIndex" -> & { - inputRef?: Ref; +type Props = Omit, "tabIndex"> & { + inputRef?: RefObject; focusOnMouseOver?: boolean; }; // Wrapper to allow use of useRovingTabIndex for simple AccessibleButtons outside of React Functional Components. -export const RovingAccessibleButton = ({ +export const RovingAccessibleButton = ({ inputRef, onFocus, onMouseOver, focusOnMouseOver, - element, ...props }: Props): JSX.Element => { - const [onFocusInternal, isActive, ref] = useRovingTabIndex(inputRef); + const [onFocusInternal, isActive, ref] = useRovingTabIndex(inputRef); return ( { + onFocus={(event: React.FocusEvent) => { onFocusInternal(); onFocus?.(event); }} - onMouseOver={(event: React.MouseEvent) => { + onMouseOver={(event: React.MouseEvent) => { if (focusOnMouseOver) onFocusInternal(); onMouseOver?.(event); }} diff --git a/src/components/structures/EmbeddedPage.tsx b/src/components/structures/EmbeddedPage.tsx index 5c7e81caf54..c471565d912 100644 --- a/src/components/structures/EmbeddedPage.tsx +++ b/src/components/structures/EmbeddedPage.tsx @@ -36,7 +36,7 @@ interface IState { export default class EmbeddedPage extends React.PureComponent { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private unmounted = false; private dispatcherRef?: string; diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx index 74a91d8cbc5..4c580cb9fe3 100644 --- a/src/components/structures/FilePanel.tsx +++ b/src/components/structures/FilePanel.tsx @@ -34,6 +34,7 @@ import { Layout } from "../../settings/enums/Layout"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import Measured from "../views/elements/Measured"; import EmptyState from "../views/right_panel/EmptyState"; +import { ScopedRoomContextProvider } from "../../contexts/ScopedRoomContext.tsx"; interface IProps { roomId: string; @@ -51,7 +52,7 @@ interface IState { */ class FilePanel extends React.Component { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; // This is used to track if a decrypted event was a live event and should be // added to the timeline. @@ -104,7 +105,11 @@ class FilePanel extends React.Component { } if (!this.state.timelineSet.eventIdToTimeline(ev.getId()!)) { - this.state.timelineSet.addEventToTimeline(ev, timeline, false); + this.state.timelineSet.addEventToTimeline(ev, timeline, { + fromCache: false, + addToState: false, + toStartOfTimeline: false, + }); } } @@ -269,12 +274,10 @@ class FilePanel extends React.Component { if (this.state.timelineSet) { return ( - { layout={Layout.Group} /> - + ); } else { return ( - + { > - + ); } } diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index f9bc5b34100..5b86d097518 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -23,7 +23,6 @@ import classNames from "classnames"; import { isOnlyCtrlOrCmdKeyEvent, Key } from "../../Keyboard"; import PageTypes from "../../PageTypes"; import MediaDeviceHandler from "../../MediaDeviceHandler"; -import { fixupColorFonts } from "../../utils/FontManager"; import dis from "../../dispatcher/dispatcher"; import { IMatrixClientCreds } from "../../MatrixClientPeg"; import SettingsStore from "../../settings/SettingsStore"; @@ -149,8 +148,6 @@ class LoggedInView extends React.Component { MediaDeviceHandler.loadDevices(); - fixupColorFonts(); - this._roomView = React.createRef(); this._resizeContainer = React.createRef(); this.resizeHandler = React.createRef(); diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 9f9e2253523..548dbff983c 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -119,7 +119,6 @@ import { ValidatedServerConfig } from "../../utils/ValidatedServerConfig"; import { isLocalRoom } from "../../utils/localRoom/isLocalRoom"; import { SDKContext, SdkContextClass } from "../../contexts/SDKContext"; import { viewUserDeviceSettings } from "../../actions/handlers/viewUserDeviceSettings"; -import { cleanUpBroadcasts, VoiceBroadcastResumer } from "../../voice-broadcast"; import GenericToast from "../views/toasts/GenericToast"; import RovingSpotlightDialog from "../views/dialogs/spotlight/SpotlightDialog"; import { findDMForUser } from "../../utils/dm/findDMForUser"; @@ -227,7 +226,6 @@ export default class MatrixChat extends React.PureComponent { private focusNext: FocusNextType; private subTitleStatus: string; private prevWindowWidth: number; - private voiceBroadcastResumer?: VoiceBroadcastResumer; private readonly loggedInView = createRef(); private dispatcherRef?: string; @@ -501,7 +499,6 @@ export default class MatrixChat extends React.PureComponent { window.removeEventListener("resize", this.onWindowResized); this.stores.accountPasswordStore.clearPassword(); - this.voiceBroadcastResumer?.destroy(); } private onWindowResized = (): void => { @@ -651,10 +648,9 @@ export default class MatrixChat extends React.PureComponent { break; case "logout": LegacyCallHandler.instance.hangupAllCalls(); - Promise.all([ - ...[...CallStore.instance.connectedCalls].map((call) => call.disconnect()), - cleanUpBroadcasts(this.stores), - ]).finally(() => Lifecycle.logout(this.stores.oidcClientStore)); + Promise.all([...[...CallStore.instance.connectedCalls].map((call) => call.disconnect())]).finally(() => + Lifecycle.logout(this.stores.oidcClientStore), + ); break; case "require_registration": startAnyRegistrationFlow(payload as any); @@ -1679,8 +1675,6 @@ export default class MatrixChat extends React.PureComponent { }); } }); - - this.voiceBroadcastResumer = new VoiceBroadcastResumer(cli); } /** diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index b26de2e645d..d2133f4f130 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -196,7 +196,7 @@ interface IReadReceiptForUser { */ export default class MessagePanel extends React.Component { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public static defaultProps = { disableGrouping: false, diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx index edec675b140..236da25409b 100644 --- a/src/components/structures/NotificationPanel.tsx +++ b/src/components/structures/NotificationPanel.tsx @@ -19,6 +19,7 @@ import { Layout } from "../../settings/enums/Layout"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import Measured from "../views/elements/Measured"; import EmptyState from "../views/right_panel/EmptyState"; +import { ScopedRoomContextProvider } from "../../contexts/ScopedRoomContext.tsx"; interface IProps { onClose(): void; @@ -33,7 +34,7 @@ interface IState { */ export default class NotificationPanel extends React.PureComponent { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private card = React.createRef(); @@ -79,12 +80,10 @@ export default class NotificationPanel extends React.PureComponent } {content} - + ); } } diff --git a/src/components/structures/PipContainer.tsx b/src/components/structures/PipContainer.tsx index 731e720b127..c9fabfe0c9e 100644 --- a/src/components/structures/PipContainer.tsx +++ b/src/components/structures/PipContainer.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { MutableRefObject, ReactNode, useContext, useRef } from "react"; +import React, { MutableRefObject, ReactNode, useRef } from "react"; import { CallEvent, CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call"; import { logger } from "matrix-js-sdk/src/logger"; import { Optional } from "matrix-events-sdk"; @@ -21,19 +21,7 @@ import { WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore"; import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../stores/ActiveWidgetStore"; import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; import { UPDATE_EVENT } from "../../stores/AsyncStore"; -import { SDKContext, SdkContextClass } from "../../contexts/SDKContext"; -import { - useCurrentVoiceBroadcastPreRecording, - useCurrentVoiceBroadcastRecording, - VoiceBroadcastPlayback, - VoiceBroadcastPlaybackBody, - VoiceBroadcastPreRecording, - VoiceBroadcastPreRecordingPip, - VoiceBroadcastRecording, - VoiceBroadcastRecordingPip, - VoiceBroadcastSmallPlaybackBody, -} from "../../voice-broadcast"; -import { useCurrentVoiceBroadcastPlayback } from "../../voice-broadcast/hooks/useCurrentVoiceBroadcastPlayback"; +import { SdkContextClass } from "../../contexts/SDKContext"; import { WidgetPip } from "../views/pips/WidgetPip"; const SHOW_CALL_IN_STATES = [ @@ -46,9 +34,6 @@ const SHOW_CALL_IN_STATES = [ ]; interface IProps { - voiceBroadcastRecording: Optional; - voiceBroadcastPreRecording: Optional; - voiceBroadcastPlayback: Optional; movePersistedElement: MutableRefObject<(() => void) | undefined>; } @@ -245,52 +230,9 @@ class PipContainerInner extends React.Component { this.setState({ showWidgetInPip, persistentWidgetId, persistentRoomId }); } - private createVoiceBroadcastPlaybackPipContent(voiceBroadcastPlayback: VoiceBroadcastPlayback): CreatePipChildren { - const content = - this.state.viewedRoomId === voiceBroadcastPlayback.infoEvent.getRoomId() ? ( - - ) : ( - - ); - - return ({ onStartMoving }) => ( -
- {content} -
- ); - } - - private createVoiceBroadcastPreRecordingPipContent( - voiceBroadcastPreRecording: VoiceBroadcastPreRecording, - ): CreatePipChildren { - return ({ onStartMoving }) => ( -
- -
- ); - } - - private createVoiceBroadcastRecordingPipContent( - voiceBroadcastRecording: VoiceBroadcastRecording, - ): CreatePipChildren { - return ({ onStartMoving }) => ( -
- -
- ); - } - public render(): ReactNode { const pipMode = true; - let pipContent: Array = []; - - if (this.props.voiceBroadcastRecording) { - pipContent = [this.createVoiceBroadcastRecordingPipContent(this.props.voiceBroadcastRecording)]; - } else if (this.props.voiceBroadcastPreRecording) { - pipContent = [this.createVoiceBroadcastPreRecordingPipContent(this.props.voiceBroadcastPreRecording)]; - } else if (this.props.voiceBroadcastPlayback) { - pipContent = [this.createVoiceBroadcastPlaybackPipContent(this.props.voiceBroadcastPlayback)]; - } + const pipContent: Array = []; if (this.state.primaryCall) { // get a ref to call inside the current scope @@ -338,24 +280,7 @@ class PipContainerInner extends React.Component { } export const PipContainer: React.FC = () => { - const sdkContext = useContext(SDKContext); - const voiceBroadcastPreRecordingStore = sdkContext.voiceBroadcastPreRecordingStore; - const { currentVoiceBroadcastPreRecording } = useCurrentVoiceBroadcastPreRecording(voiceBroadcastPreRecordingStore); - - const voiceBroadcastRecordingsStore = sdkContext.voiceBroadcastRecordingsStore; - const { currentVoiceBroadcastRecording } = useCurrentVoiceBroadcastRecording(voiceBroadcastRecordingsStore); - - const voiceBroadcastPlaybacksStore = sdkContext.voiceBroadcastPlaybacksStore; - const { currentVoiceBroadcastPlayback } = useCurrentVoiceBroadcastPlayback(voiceBroadcastPlaybacksStore); - const movePersistedElement = useRef<() => void>(); - return ( - - ); + return ; }; diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index 9a9f29f82e5..a1f20162430 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -63,7 +63,7 @@ interface IState { export default class RightPanel extends React.Component { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public constructor(props: Props, context: React.ContextType) { super(props, context); @@ -109,10 +109,10 @@ export default class RightPanel extends React.Component { } // redraw the badge on the membership list - if (this.state.phase === RightPanelPhases.RoomMemberList) { + if (this.state.phase === RightPanelPhases.MemberList) { this.delayedUpdate(); } else if ( - this.state.phase === RightPanelPhases.RoomMemberInfo && + this.state.phase === RightPanelPhases.MemberInfo && member.userId === this.state.cardState?.member?.userId ) { // refresh the member info (e.g. new power level) @@ -157,7 +157,7 @@ export default class RightPanel extends React.Component { const phase = this.props.overwriteCard?.phase ?? this.state.phase; const cardState = this.props.overwriteCard?.state ?? this.state.cardState; switch (phase) { - case RightPanelPhases.RoomMemberList: + case RightPanelPhases.MemberList: if (!!roomId) { card = ( { ); } break; - case RightPanelPhases.SpaceMemberList: - if (!!cardState?.spaceId || !!roomId) { - card = ( - - ); - } - break; - case RightPanelPhases.RoomMemberInfo: - case RightPanelPhases.SpaceMemberInfo: + case RightPanelPhases.MemberInfo: case RightPanelPhases.EncryptionPanel: { if (!!cardState?.member) { const roomMember = cardState.member instanceof RoomMember ? cardState.member : undefined; @@ -203,8 +189,7 @@ export default class RightPanel extends React.Component { } break; } - case RightPanelPhases.Room3pidMemberInfo: - case RightPanelPhases.Space3pidMemberInfo: + case RightPanelPhases.ThreePidMemberInfo: if (!!cardState?.memberInfoEvent) { card = ( diff --git a/src/components/structures/RoomSearchView.tsx b/src/components/structures/RoomSearchView.tsx index de2d9d2198e..82146bcc5e0 100644 --- a/src/components/structures/RoomSearchView.tsx +++ b/src/components/structures/RoomSearchView.tsx @@ -26,7 +26,7 @@ import ErrorDialog from "../views/dialogs/ErrorDialog"; import ResizeNotifier from "../../utils/ResizeNotifier"; import MatrixClientContext from "../../contexts/MatrixClientContext"; import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; -import RoomContext from "../../contexts/RoomContext"; +import { useScopedRoomContext } from "../../contexts/ScopedRoomContext.tsx"; const DEBUG = false; let debuglog = function (msg: string): void {}; @@ -53,7 +53,7 @@ interface Props { export const RoomSearchView = forwardRef( ({ term, scope, promise, abortController, resizeNotifier, className, onUpdate, inProgress }: Props, ref) => { const client = useContext(MatrixClientContext); - const roomContext = useContext(RoomContext); + const roomContext = useScopedRoomContext("showHiddenEvents"); const [highlights, setHighlights] = useState(null); const [results, setResults] = useState(null); const aborted = useRef(false); diff --git a/src/components/structures/RoomStatusBar.tsx b/src/components/structures/RoomStatusBar.tsx index 76f3b0c2298..3bd69148aee 100644 --- a/src/components/structures/RoomStatusBar.tsx +++ b/src/components/structures/RoomStatusBar.tsx @@ -89,7 +89,7 @@ interface IState { export default class RoomStatusBar extends React.PureComponent { private unmounted = false; public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public constructor(props: IProps, context: React.ContextType) { super(props, context); diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 470b73de7c6..772d5698a30 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { ChangeEvent, ComponentProps, createRef, ReactElement, ReactNode, RefObject, useContext } from "react"; +import React, { ChangeEvent, ComponentProps, createRef, ReactElement, ReactNode, RefObject, JSX } from "react"; import classNames from "classnames"; import { IRecommendedVersion, @@ -29,6 +29,7 @@ import { MatrixError, ISearchResults, THREAD_RELATION_TYPE, + MatrixClient, } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; import { logger } from "matrix-js-sdk/src/logger"; @@ -54,7 +55,7 @@ import WidgetEchoStore from "../../stores/WidgetEchoStore"; import SettingsStore from "../../settings/SettingsStore"; import { Layout } from "../../settings/enums/Layout"; import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton"; -import RoomContext, { TimelineRenderingType, MainSplitContentType } from "../../contexts/RoomContext"; +import { TimelineRenderingType, MainSplitContentType } from "../../contexts/RoomContext"; import { E2EStatus, shieldStatusForRoom } from "../../utils/ShieldUtils"; import { Action } from "../../dispatcher/actions"; import { IMatrixClientCreds } from "../../MatrixClientPeg"; @@ -126,6 +127,7 @@ import RightPanelStore from "../../stores/right-panel/RightPanelStore"; import { onView3pidInvite } from "../../stores/right-panel/action-handlers"; import RoomSearchAuxPanel from "../views/rooms/RoomSearchAuxPanel"; import { PinnedMessageBanner } from "../views/rooms/PinnedMessageBanner"; +import { ScopedRoomContextProvider, useScopedRoomContext } from "../../contexts/ScopedRoomContext"; const DEBUG = false; const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000; @@ -233,6 +235,11 @@ export interface IRoomState { liveTimeline?: EventTimeline; narrow: boolean; msc3946ProcessDynamicPredecessor: boolean; + /** + * Whether the room is encrypted or not. + * If null, we are still determining the encryption status. + */ + isRoomEncrypted: boolean | null; canAskToJoin: boolean; promptAskToJoin: boolean; @@ -246,6 +253,7 @@ interface LocalRoomViewProps { permalinkCreator: RoomPermalinkCreator; roomView: RefObject; onFileDrop: (dataTransfer: DataTransfer) => Promise; + mainSplitContentType: MainSplitContentType; } /** @@ -255,7 +263,7 @@ interface LocalRoomViewProps { * @returns {ReactElement} */ function LocalRoomView(props: LocalRoomViewProps): ReactElement { - const context = useContext(RoomContext); + const context = useScopedRoomContext("room"); const room = context.room as LocalRoom; const encryptionEvent = props.localRoom.currentState.getStateEvents(EventType.RoomEncryption)[0]; let encryptionTile: ReactNode; @@ -323,6 +331,7 @@ interface ILocalRoomCreateLoaderProps { localRoom: LocalRoom; names: string; resizeNotifier: ResizeNotifier; + mainSplitContentType: MainSplitContentType; } /** @@ -363,7 +372,7 @@ export class RoomView extends React.Component { private roomViewBody = createRef(); public static contextType = SDKContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public constructor(props: IRoomProps, context: React.ContextType) { super(props, context); @@ -417,6 +426,7 @@ export class RoomView extends React.Component { canAskToJoin: this.askToJoinEnabled, promptAskToJoin: false, viewRoomOpts: { buttons: [] }, + isRoomEncrypted: null, }; } @@ -655,6 +665,7 @@ export class RoomView extends React.Component { // the RoomView instance if (initial) { newState.room = this.context.client!.getRoom(newState.roomId) || undefined; + newState.isRoomEncrypted = null; if (newState.room) { newState.showApps = this.shouldShowApps(newState.room); this.onRoomLoaded(newState.room); @@ -697,6 +708,14 @@ export class RoomView extends React.Component { if (initial) { this.setupRoom(newState.room, newState.roomId, !!newState.joining, !!newState.shouldPeek); } + + // We don't block the initial setup but we want to make it early to not block the timeline rendering + const isRoomEncrypted = await this.getIsRoomEncrypted(newState.roomId); + this.setState({ + isRoomEncrypted, + ...(isRoomEncrypted && + newState.roomId && { e2eStatus: RoomView.e2eStatusCache.get(newState.roomId) ?? E2EStatus.Warning }), + }); }; private onConnectedCalls = (): void => { @@ -1214,18 +1233,18 @@ export class RoomView extends React.Component { if (payload.member) { if (payload.push) { RightPanelStore.instance.pushCard({ - phase: RightPanelPhases.RoomMemberInfo, + phase: RightPanelPhases.MemberInfo, state: { member: payload.member }, }); } else { RightPanelStore.instance.setCards([ { phase: RightPanelPhases.RoomSummary }, - { phase: RightPanelPhases.RoomMemberList }, - { phase: RightPanelPhases.RoomMemberInfo, state: { member: payload.member } }, + { phase: RightPanelPhases.MemberList }, + { phase: RightPanelPhases.MemberInfo, state: { member: payload.member } }, ]); } } else { - RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomMemberList); + RightPanelStore.instance.showOrHidePhase(RightPanelPhases.MemberList); } break; case Action.View3pidInvite: @@ -1342,13 +1361,12 @@ export class RoomView extends React.Component { this.context.widgetLayoutStore.on(WidgetLayoutStore.emissionForRoom(room), this.onWidgetLayoutChange); this.calculatePeekRules(room); - this.updatePreviewUrlVisibility(room); this.loadMembersIfJoined(room); this.calculateRecommendedVersion(room); - this.updateE2EStatus(room); this.updatePermissions(room); this.checkWidgets(room); this.loadVirtualRoom(room); + this.updateRoomEncrypted(room); if ( this.getMainSplitContentType(room) !== MainSplitContentType.Timeline && @@ -1377,6 +1395,13 @@ export class RoomView extends React.Component { return room?.currentState.getStateEvents(EventType.RoomTombstone, "") ?? undefined; } + private async getIsRoomEncrypted(roomId = this.state.roomId): Promise { + const crypto = this.context.client?.getCrypto(); + if (!crypto || !roomId) return false; + + return await crypto.isEncryptionEnabledInRoom(roomId); + } + private async calculateRecommendedVersion(room: Room): Promise { const upgradeRecommendation = await room.getRecommendedVersion(); if (this.unmounted) return; @@ -1409,12 +1434,15 @@ export class RoomView extends React.Component { }); } - private updatePreviewUrlVisibility({ roomId }: Room): void { - // URL Previews in E2EE rooms can be a privacy leak so use a different setting which is per-room explicit - const key = this.context.client?.isRoomEncrypted(roomId) ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled"; - this.setState({ - showUrlPreview: SettingsStore.getValue(key, roomId), - }); + private updatePreviewUrlVisibility(room: Room): void { + this.setState(({ isRoomEncrypted }) => ({ + showUrlPreview: this.getPreviewUrlVisibility(room, isRoomEncrypted), + })); + } + + private getPreviewUrlVisibility({ roomId }: Room, isRoomEncrypted: boolean | null): boolean { + const key = isRoomEncrypted ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled"; + return SettingsStore.getValue(key, roomId); } private onRoom = (room: Room): void => { @@ -1456,22 +1484,20 @@ export class RoomView extends React.Component { }; private async updateE2EStatus(room: Room): Promise { - if (!this.context.client?.isRoomEncrypted(room.roomId)) return; + if (!this.context.client || !this.state.isRoomEncrypted) return; + const e2eStatus = await this.cacheAndGetE2EStatus(room, this.context.client); + if (this.unmounted) return; + this.setState({ e2eStatus }); + } - // If crypto is not currently enabled, we aren't tracking devices at all, - // so we don't know what the answer is. Let's error on the safe side and show - // a warning for this case. - let e2eStatus = RoomView.e2eStatusCache.get(room.roomId) ?? E2EStatus.Warning; + private async cacheAndGetE2EStatus(room: Room, client: MatrixClient): Promise { + let e2eStatus = RoomView.e2eStatusCache.get(room.roomId); // set the state immediately then update, so we don't scare the user into thinking the room is unencrypted - this.setState({ e2eStatus }); + if (e2eStatus) this.setState({ e2eStatus }); - if (this.context.client.getCrypto()) { - /* At this point, the user has encryption on and cross-signing on */ - e2eStatus = await shieldStatusForRoom(this.context.client, room); - RoomView.e2eStatusCache.set(room.roomId, e2eStatus); - if (this.unmounted) return; - this.setState({ e2eStatus }); - } + e2eStatus = await shieldStatusForRoom(client, room); + RoomView.e2eStatusCache.set(room.roomId, e2eStatus); + return e2eStatus; } private onUrlPreviewsEnabledChange = (): void => { @@ -1480,20 +1506,36 @@ export class RoomView extends React.Component { } }; - private onRoomStateEvents = (ev: MatrixEvent, state: RoomState): void => { + private onRoomStateEvents = async (ev: MatrixEvent, state: RoomState): Promise => { // ignore if we don't have a room yet - if (!this.state.room || this.state.room.roomId !== state.roomId) return; + if (!this.state.room || this.state.room.roomId !== state.roomId || !this.context.client) return; switch (ev.getType()) { case EventType.RoomTombstone: this.setState({ tombstone: this.getRoomTombstone() }); break; - + case EventType.RoomEncryption: { + await this.updateRoomEncrypted(); + break; + } default: this.updatePermissions(this.state.room); } }; + private async updateRoomEncrypted(room = this.state.room): Promise { + if (!room || !this.context.client) return; + + const isRoomEncrypted = await this.getIsRoomEncrypted(room.roomId); + const newE2EStatus = isRoomEncrypted ? await this.cacheAndGetE2EStatus(room, this.context.client) : null; + + this.setState({ + isRoomEncrypted, + showUrlPreview: this.getPreviewUrlVisibility(room, isRoomEncrypted), + ...(newE2EStatus && { e2eStatus: newE2EStatus }), + }); + } + private onRoomStateUpdate = (state: RoomState): void => { // ignore members in other rooms if (state.roomId !== this.state.room?.roomId) { @@ -1959,35 +2001,41 @@ export class RoomView extends React.Component { if (!this.state.room || !this.context?.client) return null; const names = this.state.room.getDefaultRoomName(this.context.client.getSafeUserId()); return ( - - - + + + ); } private renderLocalRoomView(localRoom: LocalRoom): ReactNode { return ( - + - + ); } private renderWaitingForThirdPartyRoomView(inviteEvent: MatrixEvent): ReactNode { return ( - + - + ); } @@ -2027,6 +2075,8 @@ export class RoomView extends React.Component { public render(): ReactNode { if (!this.context.client) return null; + const { isRoomEncrypted } = this.state; + const isRoomEncryptionLoading = isRoomEncrypted === null; if (this.state.room instanceof LocalRoom) { if (this.state.room.state === LocalRoomState.CREATING) { @@ -2242,14 +2292,16 @@ export class RoomView extends React.Component { let aux: JSX.Element | undefined; let previewBar; if (this.state.timelineRenderingType === TimelineRenderingType.Search) { - aux = ( - - ); + if (!isRoomEncryptionLoading) { + aux = ( + + ); + } } else if (showRoomUpgradeBar) { aux = ; } else if (myMembership !== KnownMembership.Join) { @@ -2320,13 +2372,19 @@ export class RoomView extends React.Component { ); const pinnedMessageBanner = ( - + ); let messageComposer; const showComposer = + !isRoomEncryptionLoading && // joined and not showing search results - myMembership === KnownMembership.Join && !this.state.search; + myMembership === KnownMembership.Join && + !this.state.search; if (showComposer) { messageComposer = ( { highlightedEventId = this.state.initialEventId; } - const messagePanel = ( -
+ {SOCIALS.map((social) => ( + + {social.name} + + ))} +
+ ); +} - const matrixToUrl = this.getUrl(); - const encodedUrl = encodeURIComponent(matrixToUrl); - - const showQrCode = SettingsStore.getValue(UIFeature.ShareQRCode); - const showSocials = SettingsStore.getValue(UIFeature.ShareSocial); - - let qrSocialSection; - if (showQrCode || showSocials) { - qrSocialSection = ( - <> -
-
- {showQrCode && ( -
- -
- )} - {showSocials && ( -
- {socials.map((social) => ( - - {social.name} - - ))} -
- )} -
- - ); +/** + * Get the title, url and checkbox label for the dialog based on the target. + * @param target + * @param linkToSpecificEvent + * @param permalinkCreator + */ +function useTargetValues( + target: ShareDialogProps["target"], + linkToSpecificEvent: boolean, + permalinkCreator?: RoomPermalinkCreator, +): { title: string; url: string; checkboxLabel?: string } { + return useMemo(() => { + if (target instanceof URL) return { title: _t("share|title_link"), url: target.toString() }; + if (target instanceof User || target instanceof RoomMember) + return { + title: _t("share|title_user"), + url: makeUserPermalink(target.userId), + }; + + if (target instanceof Room) { + const title = _t("share|title_room"); + const newPermalinkCreator = new RoomPermalinkCreator(target); + newPermalinkCreator.load(); + + const events = target.getLiveTimeline().getEvents(); + return { + title, + url: linkToSpecificEvent + ? newPermalinkCreator.forEvent(events[events.length - 1].getId()!) + : newPermalinkCreator.forShareableRoom(), + ...(events.length > 0 && { checkboxLabel: _t("share|permalink_most_recent") }), + }; } - return ( - - {this.props.subtitle &&

{this.props.subtitle}

} -
- matrixToUrl}> - - {matrixToUrl} - - - {checkbox} - {qrSocialSection} -
-
- ); - } + // MatrixEvent is remaining and should have a permalinkCreator + const url = linkToSpecificEvent + ? permalinkCreator!.forEvent(target.getId()!) + : permalinkCreator!.forShareableRoom(); + return { + title: _t("share|title_message"), + url, + checkboxLabel: _t("share|permalink_message"), + }; + }, [target, linkToSpecificEvent, permalinkCreator]); } diff --git a/src/components/views/dialogs/UntrustedDeviceDialog.tsx b/src/components/views/dialogs/UntrustedDeviceDialog.tsx index c2bc92e7510..4b37032207b 100644 --- a/src/components/views/dialogs/UntrustedDeviceDialog.tsx +++ b/src/components/views/dialogs/UntrustedDeviceDialog.tsx @@ -17,9 +17,20 @@ import BaseDialog from "./BaseDialog"; import { IDevice } from "../right_panel/UserInfo"; interface IProps { + /** + * The user whose device is untrusted. + */ user: User; + /** + * The device that is untrusted. + */ device: IDevice; - onFinished(mode?: "legacy" | "sas" | false): void; + /** + * Callback for when the dialog is dismissed. + * If mode is "sas", the user wants to verify the device with SAS. Otherwise, the dialog was dismissed normally. + * @param mode The mode of dismissal. + */ + onFinished(mode?: "sas"): void; } const UntrustedDeviceDialog: React.FC = ({ device, user, onFinished }) => { @@ -56,13 +67,10 @@ const UntrustedDeviceDialog: React.FC = ({ device, user, onFinished }) =

{askToVerifyText}

- onFinished("legacy")}> - {_t("encryption|udd|manual_verification_button")} - onFinished("sas")}> {_t("encryption|udd|interactive_verification_button")} - onFinished(false)}> + onFinished()}> {_t("action|done")}
diff --git a/src/components/views/dialogs/devtools/SettingExplorer.tsx b/src/components/views/dialogs/devtools/SettingExplorer.tsx index ed4b64d870d..ae37fa3e1c9 100644 --- a/src/components/views/dialogs/devtools/SettingExplorer.tsx +++ b/src/components/views/dialogs/devtools/SettingExplorer.tsx @@ -298,7 +298,7 @@ const SettingsList: React.FC = ({ onBack, onView, onEdit }) {i} onEdit(i)} className="mx_DevTools_SettingsExplorer_edit" > diff --git a/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx b/src/components/views/dialogs/security/InitialCryptoSetupDialog.tsx similarity index 83% rename from src/components/views/dialogs/security/CreateCrossSigningDialog.tsx rename to src/components/views/dialogs/security/InitialCryptoSetupDialog.tsx index 73da6b178c9..4ee69f17a48 100644 --- a/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx +++ b/src/components/views/dialogs/security/InitialCryptoSetupDialog.tsx @@ -25,14 +25,19 @@ interface Props { } /* - * Walks the user through the process of creating a cross-signing keys. In most - * cases, only a spinner is shown, but for more complex auth like SSO, the user - * may need to complete some steps to proceed. + * Walks the user through the process of creating a cross-signing keys. + * In most cases, only a spinner is shown, but for more + * complex auth like SSO, the user may need to complete some steps to proceed. */ -const CreateCrossSigningDialog: React.FC = ({ matrixClient, accountPassword, tokenLogin, onFinished }) => { +export const InitialCryptoSetupDialog: React.FC = ({ + matrixClient, + accountPassword, + tokenLogin, + onFinished, +}) => { const [error, setError] = useState(false); - const bootstrapCrossSigning = useCallback(async () => { + const doSetup = useCallback(async () => { const cryptoApi = matrixClient.getCrypto(); if (!cryptoApi) return; @@ -40,6 +45,7 @@ const CreateCrossSigningDialog: React.FC = ({ matrixClient, accountPasswo try { await createCrossSigning(matrixClient, tokenLogin, accountPassword); + onFinished(true); } catch (e) { if (tokenLogin) { @@ -58,8 +64,8 @@ const CreateCrossSigningDialog: React.FC = ({ matrixClient, accountPasswo }, [onFinished]); useEffect(() => { - bootstrapCrossSigning(); - }, [bootstrapCrossSigning]); + doSetup(); + }, [doSetup]); let content; if (error) { @@ -69,7 +75,7 @@ const CreateCrossSigningDialog: React.FC = ({ matrixClient, accountPasswo
@@ -95,5 +101,3 @@ const CreateCrossSigningDialog: React.FC = ({ matrixClient, accountPasswo ); }; - -export default CreateCrossSigningDialog; diff --git a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx index 20d6825b9b7..a87b7341e7d 100644 --- a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx +++ b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx @@ -1253,7 +1253,7 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n {filterToLabel(filter)} = ButtonProps & { +type TooltipOptionProps = ButtonProps & { + className?: string; endAdornment?: ReactNode; inputRef?: Ref; }; -export const TooltipOption = ({ +export const TooltipOption = ({ inputRef, className, - element, ...props }: TooltipOptionProps): JSX.Element => { const [onFocus, isActive, ref] = useRovingTabIndex(inputRef); @@ -34,7 +34,6 @@ export const TooltipOption = ({ tabIndex={-1} aria-selected={isActive} role="option" - element={element as keyof JSX.IntrinsicElements} /> ); }; diff --git a/src/components/views/directory/NetworkDropdown.tsx b/src/components/views/directory/NetworkDropdown.tsx index 43d123676b0..a1b1986f471 100644 --- a/src/components/views/directory/NetworkDropdown.tsx +++ b/src/components/views/directory/NetworkDropdown.tsx @@ -168,7 +168,7 @@ export const NetworkDropdown: React.FC = ({ protocols, config, setConfig adornment: ( setUserDefinedServers(without(userDefinedServers, roomServer))} /> ), diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx index b8b5297384c..8b58f251c36 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -6,7 +6,15 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { ComponentProps, forwardRef, FunctionComponent, HTMLAttributes, InputHTMLAttributes, Ref } from "react"; +import React, { + ComponentProps, + ComponentPropsWithoutRef, + forwardRef, + FunctionComponent, + ReactElement, + KeyboardEvent, + Ref, +} from "react"; import classnames from "classnames"; import { Tooltip } from "@vector-im/compound-web"; @@ -38,20 +46,8 @@ export type AccessibleButtonKind = | "icon_primary" | "icon_primary_outline"; -/** - * This type construct allows us to specifically pass those props down to the element we’re creating that the element - * actually supports. - * - * e.g., if element is set to "a", we’ll support href and target, if it’s set to "input", we support type. - * - * To remain compatible with existing code, we’ll continue to support InputHTMLAttributes - */ -type DynamicHtmlElementProps = - JSX.IntrinsicElements[T] extends HTMLAttributes<{}> ? DynamicElementProps : DynamicElementProps<"div">; -type DynamicElementProps = Partial< - Omit -> & - Omit, "onClick">; +type ElementType = keyof HTMLElementTagNameMap; +const defaultElement = "div"; type TooltipProps = ComponentProps; @@ -60,7 +56,7 @@ type TooltipProps = ComponentProps; * * Extends props accepted by the underlying element specified using the `element` prop. */ -type Props = DynamicHtmlElementProps & { +type Props = { /** * The base element type. "div" by default. */ @@ -105,14 +101,12 @@ type Props = DynamicHtmlElementProps & disableTooltip?: TooltipProps["disabled"]; }; -export type ButtonProps = Props; +export type ButtonProps = Props & Omit, keyof Props>; /** * Type of the props passed to the element that is rendered by AccessibleButton. */ -interface RenderedElementProps extends React.InputHTMLAttributes { - ref?: React.Ref; -} +type RenderedElementProps = React.InputHTMLAttributes & RefProp; /** * AccessibleButton is a generic wrapper for any element that should be treated @@ -124,9 +118,9 @@ interface RenderedElementProps extends React.InputHTMLAttributes { * @param {Object} props react element properties * @returns {Object} rendered react */ -const AccessibleButton = forwardRef(function ( +const AccessibleButton = forwardRef(function ( { - element = "div" as T, + element, onClick, children, kind, @@ -141,10 +135,10 @@ const AccessibleButton = forwardRef(function , - ref: Ref, + }: ButtonProps, + ref: Ref, ): JSX.Element { - const newProps: RenderedElementProps = restProps; + const newProps = restProps as RenderedElementProps; newProps["aria-label"] = newProps["aria-label"] ?? title; if (disabled) { newProps["aria-disabled"] = true; @@ -162,7 +156,7 @@ const AccessibleButton = forwardRef(function { + newProps.onKeyDown = (e: KeyboardEvent) => { const action = getKeyBindingsManager().getAccessibilityAction(e); switch (action) { @@ -178,7 +172,7 @@ const AccessibleButton = forwardRef(function { + newProps.onKeyUp = (e: KeyboardEvent) => { const action = getKeyBindingsManager().getAccessibilityAction(e); switch (action) { @@ -207,7 +201,7 @@ const AccessibleButton = forwardRef(function { + ref?: Ref; +} + +interface ButtonComponent { + // With the explicit `element` prop + (props: { element?: C } & ButtonProps & RefProp): ReactElement; + // Without the explicit `element` prop + (props: ButtonProps<"div"> & RefProp<"div">): ReactElement; +} + +export default AccessibleButton as ButtonComponent; diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx index dae452fd5d0..56754f14a6b 100644 --- a/src/components/views/elements/AppTile.tsx +++ b/src/components/views/elements/AppTile.tsx @@ -116,7 +116,7 @@ interface IState { export default class AppTile extends React.Component { public static contextType = MatrixClientContext; - public declare context: ContextType; + declare public context: ContextType; public static defaultProps: Partial = { waitForIframeLoad: true, diff --git a/src/components/views/elements/EditableItemList.tsx b/src/components/views/elements/EditableItemList.tsx index dc6e6c09a16..ad2d9aceee7 100644 --- a/src/components/views/elements/EditableItemList.tsx +++ b/src/components/views/elements/EditableItemList.tsx @@ -133,12 +133,7 @@ export default class EditableItemList

extends React.PureComponent - + {_t("action|add")} diff --git a/src/components/views/elements/EventListSummary.tsx b/src/components/views/elements/EventListSummary.tsx index 7562f992c10..776908375a1 100644 --- a/src/components/views/elements/EventListSummary.tsx +++ b/src/components/views/elements/EventListSummary.tsx @@ -73,7 +73,7 @@ export default class EventListSummary extends React.Component< IProps & Required> > { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public static defaultProps = { summaryLength: 1, diff --git a/src/components/views/elements/MiniAvatarUploader.tsx b/src/components/views/elements/MiniAvatarUploader.tsx index cf5a2398148..452b206bef3 100644 --- a/src/components/views/elements/MiniAvatarUploader.tsx +++ b/src/components/views/elements/MiniAvatarUploader.tsx @@ -12,12 +12,12 @@ import React, { useContext, useRef, useState, MouseEvent, ReactNode } from "reac import { Tooltip } from "@vector-im/compound-web"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import RoomContext from "../../../contexts/RoomContext"; import { useTimeout } from "../../../hooks/useTimeout"; import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; import AccessibleButton from "./AccessibleButton"; import Spinner from "./Spinner"; import { getFileChanged } from "../settings/AvatarSetting.tsx"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; export const AVATAR_SIZE = "52px"; @@ -56,7 +56,7 @@ const MiniAvatarUploader: React.FC = ({ const label = hasAvatar || busy ? hasAvatarLabel : noAvatarLabel; - const { room } = useContext(RoomContext); + const { room } = useScopedRoomContext("room"); const canSetAvatar = isUserAvatar || room?.currentState?.maySendStateEvent(EventType.RoomAvatar, cli.getSafeUserId()); if (!canSetAvatar) return {children}; diff --git a/src/components/views/elements/PersistentApp.tsx b/src/components/views/elements/PersistentApp.tsx index 6fbf0d31bc8..5f720dc85e8 100644 --- a/src/components/views/elements/PersistentApp.tsx +++ b/src/components/views/elements/PersistentApp.tsx @@ -25,7 +25,7 @@ interface IProps { export default class PersistentApp extends React.Component { public static contextType = MatrixClientContext; - public declare context: ContextType; + declare public context: ContextType; private room: Room; public constructor(props: IProps, context: ContextType) { diff --git a/src/components/views/elements/ReplyChain.tsx b/src/components/views/elements/ReplyChain.tsx index 71846d60658..8e10ca3af90 100644 --- a/src/components/views/elements/ReplyChain.tsx +++ b/src/components/views/elements/ReplyChain.tsx @@ -65,7 +65,7 @@ interface IState { // be low as each event being loaded (after the first) is triggered by an explicit user action. export default class ReplyChain extends React.Component { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private unmounted = false; private room: Room; diff --git a/src/components/views/elements/RoomAliasField.tsx b/src/components/views/elements/RoomAliasField.tsx index f092f9d25f9..faa0ccf1a64 100644 --- a/src/components/views/elements/RoomAliasField.tsx +++ b/src/components/views/elements/RoomAliasField.tsx @@ -33,7 +33,7 @@ interface IState { // Controlled form component wrapping Field for inputting a room alias scoped to a given domain export default class RoomAliasField extends React.PureComponent { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private fieldRef = createRef(); diff --git a/src/components/views/elements/crypto/VerificationQRCode.tsx b/src/components/views/elements/crypto/VerificationQRCode.tsx index 82f4ca20d3b..021457bc006 100644 --- a/src/components/views/elements/crypto/VerificationQRCode.tsx +++ b/src/components/views/elements/crypto/VerificationQRCode.tsx @@ -12,7 +12,7 @@ import QRCode from "../QRCode"; interface IProps { /** The data for the QR code. If `undefined`, a spinner is shown. */ - qrCodeBytes: undefined | Buffer; + qrCodeBytes: undefined | Uint8ClampedArray; } export default class VerificationQRCode extends React.PureComponent { diff --git a/src/components/views/emojipicker/Emoji.tsx b/src/components/views/emojipicker/Emoji.tsx index c3dfb24bd13..a852122b750 100644 --- a/src/components/views/emojipicker/Emoji.tsx +++ b/src/components/views/emojipicker/Emoji.tsx @@ -31,7 +31,7 @@ class Emoji extends React.PureComponent { return ( onClick(ev, emoji)} + onClick={(ev: ButtonEvent) => onClick(ev, emoji)} onMouseEnter={() => onMouseEnter(emoji)} onMouseLeave={() => onMouseLeave(emoji)} className="mx_EmojiPicker_item_wrapper" diff --git a/src/components/views/emojipicker/ReactionPicker.tsx b/src/components/views/emojipicker/ReactionPicker.tsx index b62df99e254..bd166344904 100644 --- a/src/components/views/emojipicker/ReactionPicker.tsx +++ b/src/components/views/emojipicker/ReactionPicker.tsx @@ -29,7 +29,7 @@ interface IState { class ReactionPicker extends React.Component { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public constructor(props: IProps, context: React.ContextType) { super(props, context); diff --git a/src/components/views/emojipicker/Search.tsx b/src/components/views/emojipicker/Search.tsx index 87397b6d4b2..bce045cb8c6 100644 --- a/src/components/views/emojipicker/Search.tsx +++ b/src/components/views/emojipicker/Search.tsx @@ -23,7 +23,7 @@ interface IProps { class Search extends React.PureComponent { public static contextType = RovingTabIndexContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private inputRef = React.createRef(); diff --git a/src/components/views/location/LocationPicker.tsx b/src/components/views/location/LocationPicker.tsx index c45521830d6..e812f1c6bd9 100644 --- a/src/components/views/location/LocationPicker.tsx +++ b/src/components/views/location/LocationPicker.tsx @@ -42,7 +42,7 @@ const isSharingOwnLocation = (shareType: LocationShareType): boolean => class LocationPicker extends React.Component { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private map?: maplibregl.Map; private geolocate?: maplibregl.GeolocateControl; private marker?: maplibregl.Marker; diff --git a/src/components/views/messages/EditHistoryMessage.tsx b/src/components/views/messages/EditHistoryMessage.tsx index 8316d0835b3..fb6f04c08f0 100644 --- a/src/components/views/messages/EditHistoryMessage.tsx +++ b/src/components/views/messages/EditHistoryMessage.tsx @@ -45,7 +45,7 @@ interface IState { export default class EditHistoryMessage extends React.PureComponent { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private content = createRef(); private pills = new ReactRootManager(); diff --git a/src/components/views/messages/MAudioBody.tsx b/src/components/views/messages/MAudioBody.tsx index 326b1c38c8b..bf0cc9ee541 100644 --- a/src/components/views/messages/MAudioBody.tsx +++ b/src/components/views/messages/MAudioBody.tsx @@ -30,7 +30,7 @@ interface IState { export default class MAudioBody extends React.PureComponent { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public state: IState = {}; diff --git a/src/components/views/messages/MFileBody.tsx b/src/components/views/messages/MFileBody.tsx index fde3ea01841..1235b73b4bd 100644 --- a/src/components/views/messages/MFileBody.tsx +++ b/src/components/views/messages/MFileBody.tsx @@ -102,7 +102,7 @@ interface IState { export default class MFileBody extends React.Component { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public state: IState = {}; diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index 7d45e71a4b7..c3aeee1a54a 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -51,13 +51,14 @@ interface IState { naturalHeight: number; }; hover: boolean; + focus: boolean; showImage: boolean; placeholder: Placeholder; } export default class MImageBody extends React.Component { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private unmounted = false; private image = createRef(); @@ -71,6 +72,7 @@ export default class MImageBody extends React.Component { imgError: false, imgLoaded: false, hover: false, + focus: false, showImage: SettingsStore.getValue("showImages"), placeholder: Placeholder.NoImage, }; @@ -120,30 +122,29 @@ export default class MImageBody extends React.Component { } }; - protected onImageEnter = (e: React.MouseEvent): void => { - this.setState({ hover: true }); - - if ( + private get shouldAutoplay(): boolean { + return !( !this.state.contentUrl || !this.state.showImage || !this.state.isAnimated || SettingsStore.getValue("autoplayGifs") - ) { - return; - } - const imgElement = e.currentTarget; - imgElement.src = this.state.contentUrl; + ); + } + + protected onImageEnter = (): void => { + this.setState({ hover: true }); }; - protected onImageLeave = (e: React.MouseEvent): void => { + protected onImageLeave = (): void => { this.setState({ hover: false }); + }; - const url = this.state.thumbUrl ?? this.state.contentUrl; - if (!url || !this.state.showImage || !this.state.isAnimated || SettingsStore.getValue("autoplayGifs")) { - return; - } - const imgElement = e.currentTarget; - imgElement.src = url; + private onFocus = (): void => { + this.setState({ focus: true }); + }; + + private onBlur = (): void => { + this.setState({ focus: false }); }; private reconnectedListener = createReconnectedListener((): void => { @@ -470,14 +471,20 @@ export default class MImageBody extends React.Component { let showPlaceholder = Boolean(placeholder); + const hoverOrFocus = this.state.hover || this.state.focus; if (thumbUrl && !this.state.imgError) { + let url = thumbUrl; + if (hoverOrFocus && this.shouldAutoplay) { + url = this.state.contentUrl!; + } + // Restrict the width of the thumbnail here, otherwise it will fill the container // which has the same width as the timeline // mx_MImageBody_thumbnail resizes img to exactly container size img = ( {content.body} { showPlaceholder = false; // because we're hiding the image, so don't show the placeholder. } - if (this.state.isAnimated && !SettingsStore.getValue("autoplayGifs") && !this.state.hover) { + if (this.state.isAnimated && !SettingsStore.getValue("autoplayGifs") && !hoverOrFocus) { // XXX: Arguably we may want a different label when the animated image is WEBP and not GIF gifLabel =

GIF

; } let banner: ReactNode | undefined; - if (this.state.showImage && this.state.hover) { + if (this.state.showImage && hoverOrFocus) { banner = this.getBanner(content); } @@ -568,7 +575,13 @@ export default class MImageBody extends React.Component { protected wrapImage(contentUrl: string | null | undefined, children: JSX.Element): ReactNode { if (contentUrl) { return ( - + {children} ); @@ -657,17 +670,14 @@ export default class MImageBody extends React.Component { } interface PlaceholderIProps { - hover?: boolean; maxWidth?: number; } export class HiddenImagePlaceholder extends React.PureComponent { public render(): React.ReactNode { const maxWidth = this.props.maxWidth ? this.props.maxWidth + "px" : null; - let className = "mx_HiddenImagePlaceholder"; - if (this.props.hover) className += " mx_HiddenImagePlaceholder_hover"; return ( -
+
{_t("timeline|m.image|show_image")} diff --git a/src/components/views/messages/MLocationBody.tsx b/src/components/views/messages/MLocationBody.tsx index b226476fa85..7735e64b031 100644 --- a/src/components/views/messages/MLocationBody.tsx +++ b/src/components/views/messages/MLocationBody.tsx @@ -30,7 +30,7 @@ interface IState { export default class MLocationBody extends React.Component { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private unmounted = false; private mapId: string; diff --git a/src/components/views/messages/MPollBody.tsx b/src/components/views/messages/MPollBody.tsx index 9e173e5f4a3..ba3962779f3 100644 --- a/src/components/views/messages/MPollBody.tsx +++ b/src/components/views/messages/MPollBody.tsx @@ -139,7 +139,7 @@ export function launchPollEditor(mxEvent: MatrixEvent, getRelationsForEvent?: Ge export default class MPollBody extends React.Component { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private seenEventIds: string[] = []; // Events we have already seen public constructor(props: IBodyProps, context: React.ContextType) { diff --git a/src/components/views/messages/MPollEndBody.tsx b/src/components/views/messages/MPollEndBody.tsx index 94671fea12e..1129b3538ed 100644 --- a/src/components/views/messages/MPollEndBody.tsx +++ b/src/components/views/messages/MPollEndBody.tsx @@ -90,7 +90,7 @@ export const MPollEndBody = React.forwardRef(({ mxEvent, ...pro const { pollStartEvent, isLoadingPollStartEvent } = usePollStartEvent(mxEvent); if (!pollStartEvent) { - const pollEndFallbackMessage = M_TEXT.findIn(mxEvent.getContent()) || textForEvent(mxEvent, cli); + const pollEndFallbackMessage = M_TEXT.findIn(mxEvent.getContent()) || textForEvent(mxEvent, cli); return ( <> diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx index 4036b9ddecf..822d2c3f593 100644 --- a/src/components/views/messages/MVideoBody.tsx +++ b/src/components/views/messages/MVideoBody.tsx @@ -34,7 +34,7 @@ interface IState { export default class MVideoBody extends React.PureComponent { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private videoRef = React.createRef(); private sizeWatcher?: string; diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx index fdd02004292..579db054e9e 100644 --- a/src/components/views/messages/MessageActionBar.tsx +++ b/src/components/views/messages/MessageActionBar.tsx @@ -58,7 +58,6 @@ import { ALTERNATE_KEY_NAME } from "../../../accessibility/KeyboardShortcuts"; import { Action } from "../../../dispatcher/actions"; import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload"; import { GetRelationsForEvent, IEventTileType } from "../rooms/EventTile"; -import { VoiceBroadcastInfoEventType } from "../../../voice-broadcast/types"; import { ButtonEvent } from "../elements/AccessibleButton"; import PinningUtils from "../../../utils/PinningUtils"; import PosthogTrackers from "../../../PosthogTrackers.ts"; @@ -262,7 +261,7 @@ interface IMessageActionBarProps { export default class MessageActionBar extends React.PureComponent { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public componentDidMount(): void { if (this.props.mxEvent.status && this.props.mxEvent.status !== EventStatus.SENT) { @@ -354,8 +353,7 @@ export default class MessageActionBar extends React.PureComponent this.onPinClick(e, isPinned)} + onClick={(e: ButtonEvent) => this.onPinClick(e, isPinned)} onContextMenu={(e: ButtonEvent) => this.onPinClick(e, isPinned)} key="pin" placement="left" diff --git a/src/components/views/messages/MessageEvent.tsx b/src/components/views/messages/MessageEvent.tsx index 60fcce6493e..d1a1c59141d 100644 --- a/src/components/views/messages/MessageEvent.tsx +++ b/src/components/views/messages/MessageEvent.tsx @@ -41,7 +41,6 @@ import MjolnirBody from "./MjolnirBody"; import MBeaconBody from "./MBeaconBody"; import { DecryptionFailureBody } from "./DecryptionFailureBody"; import { GetRelationsForEvent, IEventTileOps } from "../rooms/EventTile"; -import { VoiceBroadcastBody, VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../../../voice-broadcast"; // onMessageAllowed is handled internally interface IProps extends Omit { @@ -85,7 +84,7 @@ export default class MessageEvent extends React.Component implements IMe private evTypes = new Map>(baseEvTypes.entries()); public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public constructor(props: IProps, context: React.ContextType) { super(props, context); @@ -276,10 +275,6 @@ export default class MessageEvent extends React.Component implements IMe if (M_LOCATION.matches(type) || (type === EventType.RoomMessage && msgtype === MsgType.Location)) { BodyType = MLocationBody; } - - if (type === VoiceBroadcastInfoEventType && content?.state === VoiceBroadcastInfoState.Started) { - BodyType = VoiceBroadcastBody; - } } if (SettingsStore.getValue("feature_mjolnir")) { diff --git a/src/components/views/messages/ReactionsRow.tsx b/src/components/views/messages/ReactionsRow.tsx index eba9499606b..605e6a7dfe0 100644 --- a/src/components/views/messages/ReactionsRow.tsx +++ b/src/components/views/messages/ReactionsRow.tsx @@ -75,7 +75,7 @@ interface IState { export default class ReactionsRow extends React.PureComponent { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public constructor(props: IProps, context: React.ContextType) { super(props, context); diff --git a/src/components/views/messages/ReactionsRowButton.tsx b/src/components/views/messages/ReactionsRowButton.tsx index 4a1d8d67fed..709edeffd82 100644 --- a/src/components/views/messages/ReactionsRowButton.tsx +++ b/src/components/views/messages/ReactionsRowButton.tsx @@ -38,7 +38,7 @@ export interface IProps { export default class ReactionsRowButton extends React.PureComponent { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public onClick = (): void => { const { mxEvent, myReactionEvent, content } = this.props; diff --git a/src/components/views/messages/ReactionsRowButtonTooltip.tsx b/src/components/views/messages/ReactionsRowButtonTooltip.tsx index 9790356762e..5f407e2e208 100644 --- a/src/components/views/messages/ReactionsRowButtonTooltip.tsx +++ b/src/components/views/messages/ReactionsRowButtonTooltip.tsx @@ -28,7 +28,7 @@ interface IProps { export default class ReactionsRowButtonTooltip extends React.PureComponent> { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public render(): React.ReactNode { const { content, reactionEvents, mxEvent, children } = this.props; diff --git a/src/components/views/messages/RoomPredecessorTile.tsx b/src/components/views/messages/RoomPredecessorTile.tsx index 2e8633febde..afc81422345 100644 --- a/src/components/views/messages/RoomPredecessorTile.tsx +++ b/src/components/views/messages/RoomPredecessorTile.tsx @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { useCallback, useContext } from "react"; +import React, { useCallback } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import { MatrixEvent, Room, RoomState } from "matrix-js-sdk/src/matrix"; @@ -18,10 +18,10 @@ import { _t } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import EventTileBubble from "./EventTileBubble"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; -import RoomContext from "../../../contexts/RoomContext"; import { useRoomState } from "../../../hooks/useRoomState"; import SettingsStore from "../../../settings/SettingsStore"; import MatrixToPermalinkConstructor from "../../../utils/permalinks/MatrixToPermalinkConstructor"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; interface IProps { /** The m.room.create MatrixEvent that this tile represents */ @@ -40,7 +40,7 @@ export const RoomPredecessorTile: React.FC = ({ mxEvent, timestamp }) => // the information inside mxEvent. This allows us the flexibility later to // use a different predecessor (e.g. through MSC3946) and still display it // in the timeline location of the create event. - const roomContext = useContext(RoomContext); + const roomContext = useScopedRoomContext("room"); const predecessor = useRoomState( roomContext.room, useCallback( diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 0c05236176f..242feff6d4b 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -52,10 +52,8 @@ export default class TextualBody extends React.Component { private tooltips = new ReactRootManager(); private reactRoots = new ReactRootManager(); - private ref = createRef(); - public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public state = { links: [], @@ -86,7 +84,7 @@ export default class TextualBody extends React.Component { if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") { // Handle expansion and add buttons - const pres = this.ref.current?.getElementsByTagName("pre"); + const pres = [...content.getElementsByTagName("pre")]; if (pres && pres.length > 0) { for (let i = 0; i < pres.length; i++) { // If there already is a div wrapping the codeblock we want to skip this. @@ -115,13 +113,14 @@ export default class TextualBody extends React.Component { root.className = "mx_EventTile_pre_container"; // Insert containing div in place of
 block
-        pre.parentNode?.replaceChild(root, pre);
+        pre.replaceWith(root);
 
         this.reactRoots.render(
             
                 {pre}
             ,
             root,
+            pre,
         );
     }
 
@@ -129,7 +128,8 @@ export default class TextualBody extends React.Component {
         if (!this.props.editState) {
             const stoppedEditing = prevProps.editState && !this.props.editState;
             const messageWasEdited = prevProps.replacingEventId !== this.props.replacingEventId;
-            if (messageWasEdited || stoppedEditing) {
+            const urlPreviewChanged = prevProps.showUrlPreview !== this.props.showUrlPreview;
+            if (messageWasEdited || stoppedEditing || urlPreviewChanged) {
                 this.applyFormatting();
             }
         }
@@ -196,10 +196,9 @@ export default class TextualBody extends React.Component {
                     
                 );
 
-                this.reactRoots.render(spoiler, spoilerContainer);
-
-                node.parentNode?.replaceChild(spoilerContainer, node);
+                this.reactRoots.render(spoiler, spoilerContainer, node);
 
+                node.replaceWith(spoilerContainer);
                 node = spoilerContainer;
             }
 
@@ -479,12 +478,7 @@ export default class TextualBody extends React.Component {
 
         if (isEmote) {
             return (
-                
+
{mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()} @@ -497,7 +491,7 @@ export default class TextualBody extends React.Component { } if (isNotice) { return ( -
+
{body} {widgets}
@@ -505,14 +499,14 @@ export default class TextualBody extends React.Component { } if (isCaption) { return ( -
+
{body} {widgets}
); } return ( -
+
{body} {widgets}
diff --git a/src/components/views/messages/TextualEvent.tsx b/src/components/views/messages/TextualEvent.tsx index 1c1ba26d080..1c54963f76f 100644 --- a/src/components/views/messages/TextualEvent.tsx +++ b/src/components/views/messages/TextualEvent.tsx @@ -19,7 +19,7 @@ interface IProps { export default class TextualEvent extends React.Component { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public render(): React.ReactNode { const text = TextForEvent.textForEvent( diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx index af7106f9c5d..d6161e9434d 100644 --- a/src/components/views/right_panel/PinnedMessagesCard.tsx +++ b/src/components/views/right_panel/PinnedMessagesCard.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { useCallback, useEffect, JSX } from "react"; +import React, { useCallback, useEffect, JSX, useContext } from "react"; import { Room, MatrixEvent } from "matrix-js-sdk/src/matrix"; import { Button, Separator } from "@vector-im/compound-web"; import classNames from "classnames"; @@ -18,7 +18,7 @@ import Spinner from "../elements/Spinner"; import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; import { PinnedEventTile } from "../rooms/PinnedEventTile"; import { useRoomState } from "../../../hooks/useRoomState"; -import RoomContext, { TimelineRenderingType, useRoomContext } from "../../../contexts/RoomContext"; +import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext"; import { ReadPinsEventId } from "./types"; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import { filterBoolean } from "../../../utils/arrays"; @@ -27,6 +27,7 @@ import { UnpinAllDialog } from "../dialogs/UnpinAllDialog"; import EmptyState from "./EmptyState"; import { usePinnedEvents, useReadPinnedEvents, useSortedFetchedPinnedEvents } from "../../../hooks/usePinnedEvents"; import PinningUtils from "../../../utils/PinningUtils.ts"; +import { ScopedRoomContextProvider } from "../../../contexts/ScopedRoomContext.tsx"; /** * List the pinned messages in a room inside a Card. @@ -48,7 +49,7 @@ interface PinnedMessagesCardProps { export function PinnedMessagesCard({ room, onClose, permalinkCreator }: PinnedMessagesCardProps): JSX.Element { const cli = useMatrixClientContext(); - const roomContext = useRoomContext(); + const roomContext = useContext(RoomContext); const pinnedEventIds = usePinnedEvents(room); const readPinnedEvents = useReadPinnedEvents(room); const pinnedEvents = useSortedFetchedPinnedEvents(room, pinnedEventIds); @@ -89,14 +90,9 @@ export function PinnedMessagesCard({ room, onClose, permalinkCreator }: PinnedMe className="mx_PinnedMessagesCard" onClose={onClose} > - + {content} - + ); } diff --git a/src/components/views/right_panel/RoomSummaryCard.tsx b/src/components/views/right_panel/RoomSummaryCard.tsx index 664977bbe27..c8dd0b97387 100644 --- a/src/components/views/right_panel/RoomSummaryCard.tsx +++ b/src/components/views/right_panel/RoomSummaryCard.tsx @@ -47,11 +47,11 @@ import RoomAvatar from "../avatars/RoomAvatar"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases"; import Modal from "../../../Modal"; -import ShareDialog from "../dialogs/ShareDialog"; +import { ShareDialog } from "../dialogs/ShareDialog"; import { useEventEmitterState } from "../../../hooks/useEventEmitter"; import { E2EStatus } from "../../../utils/ShieldUtils"; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; -import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext"; +import { TimelineRenderingType } from "../../../contexts/RoomContext"; import RoomName from "../elements/RoomName"; import ExportDialog from "../dialogs/ExportDialog"; import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; @@ -76,6 +76,7 @@ import { useTransition } from "../../../hooks/useTransition"; import { isVideoRoom as calcIsVideoRoom } from "../../../utils/video-rooms"; import { usePinnedEvents } from "../../../hooks/usePinnedEvents"; import { ReleaseAnnouncement } from "../../structures/ReleaseAnnouncement.tsx"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; interface IProps { room: Room; @@ -86,7 +87,7 @@ interface IProps { } const onRoomMembersClick = (): void => { - RightPanelStore.instance.pushCard({ phase: RightPanelPhases.RoomMemberList }, true); + RightPanelStore.instance.pushCard({ phase: RightPanelPhases.MemberList }, true); }; const onRoomThreadsClick = (): void => { @@ -232,7 +233,7 @@ const RoomSummaryCard: React.FC = ({ }; const isRoomEncrypted = useIsEncrypted(cli, room); - const roomContext = useContext(RoomContext); + const roomContext = useScopedRoomContext("e2eStatus", "timelineRenderingType"); const e2eStatus = roomContext.e2eStatus; const isVideoRoom = calcIsVideoRoom(room); diff --git a/src/components/views/right_panel/TimelineCard.tsx b/src/components/views/right_panel/TimelineCard.tsx index e0988eeaa5d..f62319f3cda 100644 --- a/src/components/views/right_panel/TimelineCard.tsx +++ b/src/components/views/right_panel/TimelineCard.tsx @@ -38,6 +38,7 @@ import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import Measured from "../elements/Measured"; import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import { SdkContextClass } from "../../../contexts/SDKContext"; +import { ScopedRoomContextProvider } from "../../../contexts/ScopedRoomContext.tsx"; interface IProps { room: Room; @@ -68,7 +69,7 @@ interface IState { export default class TimelineCard extends React.Component { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private dispatcherRef?: string; private layoutWatcherRef?: string; @@ -199,13 +200,11 @@ export default class TimelineCard extends React.Component { const showComposer = myMembership === KnownMembership.Join; return ( - { /> )} - + ); } } diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index d07b3566e2e..591e2327ae4 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -63,7 +63,7 @@ import PowerSelector from "../elements/PowerSelector"; import MemberAvatar from "../avatars/MemberAvatar"; import PresenceLabel from "../rooms/PresenceLabel"; import BulkRedactDialog from "../dialogs/BulkRedactDialog"; -import ShareDialog from "../dialogs/ShareDialog"; +import { ShareDialog } from "../dialogs/ShareDialog"; import ErrorDialog from "../dialogs/ErrorDialog"; import QuestionDialog from "../dialogs/QuestionDialog"; import ConfirmUserActionDialog from "../dialogs/ConfirmUserActionDialog"; @@ -1739,13 +1739,13 @@ export const UserInfoHeader: React.FC<{ interface IProps { user: Member; room?: Room; - phase: RightPanelPhases.RoomMemberInfo | RightPanelPhases.SpaceMemberInfo | RightPanelPhases.EncryptionPanel; + phase: RightPanelPhases.MemberInfo | RightPanelPhases.EncryptionPanel; onClose(): void; verificationRequest?: VerificationRequest; verificationRequestPromise?: Promise; } -const UserInfo: React.FC = ({ user, room, onClose, phase = RightPanelPhases.RoomMemberInfo, ...props }) => { +const UserInfo: React.FC = ({ user, room, onClose, phase = RightPanelPhases.MemberInfo, ...props }) => { const cli = useContext(MatrixClientContext); // fetch latest room member if we have a room, so we don't show historical information, falling back to user @@ -1767,8 +1767,6 @@ const UserInfo: React.FC = ({ user, room, onClose, phase = RightPanelPha // We have no previousPhase for when viewing a UserInfo without a Room at this time if (room && phase === RightPanelPhases.EncryptionPanel) { cardState = { member }; - } else if (room?.isSpaceRoom()) { - cardState = { spaceId: room.roomId }; } const onEncryptionPanelClose = (): void => { @@ -1777,8 +1775,7 @@ const UserInfo: React.FC = ({ user, room, onClose, phase = RightPanelPha let content: JSX.Element | undefined; switch (phase) { - case RightPanelPhases.RoomMemberInfo: - case RightPanelPhases.SpaceMemberInfo: + case RightPanelPhases.MemberInfo: content = ( = ({ user, room, onClose, phase = RightPanelPha closeLabel={closeLabel} cardState={cardState} onBack={(ev: ButtonEvent) => { - if (RightPanelStore.instance.previousCard.phase === RightPanelPhases.RoomMemberList) { + if (RightPanelStore.instance.previousCard.phase === RightPanelPhases.MemberList) { PosthogTrackers.trackInteraction("WebRightPanelRoomUserInfoBackButton", ev); } }} diff --git a/src/components/views/right_panel/VerificationPanel.tsx b/src/components/views/right_panel/VerificationPanel.tsx index 7e04af78249..6f8295dece2 100644 --- a/src/components/views/right_panel/VerificationPanel.tsx +++ b/src/components/views/right_panel/VerificationPanel.tsx @@ -46,7 +46,7 @@ interface IState { * We attempt to calculate this once the verification request transitions into the "Ready" phase. If the other * side cannot scan QR codes, it will remain `undefined`. */ - qrCodeBytes: Buffer | undefined; + qrCodeBytes: Uint8ClampedArray | undefined; sasEvent: ShowSasCallbacks | null; emojiButtonClicked?: boolean; diff --git a/src/components/views/room_settings/AliasSettings.tsx b/src/components/views/room_settings/AliasSettings.tsx index 3c1a745530f..0bb29b7f895 100644 --- a/src/components/views/room_settings/AliasSettings.tsx +++ b/src/components/views/room_settings/AliasSettings.tsx @@ -94,7 +94,7 @@ interface IState { export default class AliasSettings extends React.Component { public static contextType = MatrixClientContext; - public declare context: ContextType; + declare public context: ContextType; public static defaultProps = { canSetAliases: false, diff --git a/src/components/views/rooms/Autocomplete.tsx b/src/components/views/rooms/Autocomplete.tsx index 423b5c62723..3ffd6648ea8 100644 --- a/src/components/views/rooms/Autocomplete.tsx +++ b/src/components/views/rooms/Autocomplete.tsx @@ -49,7 +49,7 @@ export default class Autocomplete extends React.PureComponent { private completionRefs: Record> = {}; public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public constructor(props: IProps, context: React.ContextType) { super(props, context); diff --git a/src/components/views/rooms/EditMessageComposer.tsx b/src/components/views/rooms/EditMessageComposer.tsx index d62a451b8b0..f4b5c3698eb 100644 --- a/src/components/views/rooms/EditMessageComposer.tsx +++ b/src/components/views/rooms/EditMessageComposer.tsx @@ -43,25 +43,6 @@ import { attachMentions, attachRelation } from "./SendMessageComposer"; import { filterBoolean } from "../../../utils/arrays"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -function getHtmlReplyFallback(mxEvent: MatrixEvent): string { - const html = mxEvent.getContent().formatted_body; - if (!html) { - return ""; - } - const rootNode = new DOMParser().parseFromString(html, "text/html").body; - const mxReply = rootNode.querySelector("mx-reply"); - return (mxReply && mxReply.outerHTML) || ""; -} - -function getTextReplyFallback(mxEvent: MatrixEvent): string { - const body: string = mxEvent.getContent().body; - const lines = body.split("\n").map((l) => l.trim()); - if (lines.length > 2 && lines[0].startsWith("> ") && lines[1].length === 0) { - return `${lines[0]}\n\n`; - } - return ""; -} - // exported for tests export function createEditContent( model: EditorModel, @@ -72,15 +53,6 @@ export function createEditContent( if (isEmote) { model = stripEmoteCommand(model); } - const isReply = !!editedEvent.replyEventId; - let plainPrefix = ""; - let htmlPrefix = ""; - - if (isReply) { - plainPrefix = getTextReplyFallback(editedEvent); - htmlPrefix = getHtmlReplyFallback(editedEvent); - } - const body = textSerialize(model); const newContent: RoomMessageEventContent = { @@ -89,19 +61,18 @@ export function createEditContent( }; const contentBody: RoomMessageTextEventContent & Omit, "m.relates_to"> = { "msgtype": newContent.msgtype, - "body": `${plainPrefix} * ${body}`, + "body": `* ${body}`, "m.new_content": newContent, }; const formattedBody = htmlSerializeIfNeeded(model, { - forceHTML: isReply, useMarkdown: SettingsStore.getValue("MessageComposerInput.useMarkdown"), }); if (formattedBody) { newContent.format = "org.matrix.custom.html"; newContent.formatted_body = formattedBody; contentBody.format = newContent.format; - contentBody.formatted_body = `${htmlPrefix} * ${formattedBody}`; + contentBody.formatted_body = `* ${formattedBody}`; } // Build the mentions properties for both the content and new_content. @@ -121,7 +92,7 @@ interface IState { class EditMessageComposer extends React.Component { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private readonly editorRef = createRef(); private dispatcherRef?: string; diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 22da73bef7f..8c755f00bda 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -296,7 +296,7 @@ export class UnwrappedEventTile extends React.Component }; public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private unmounted = false; @@ -757,6 +757,14 @@ export class UnwrappedEventTile extends React.Component case EventShieldReason.MISMATCHED_SENDER_KEY: shieldReasonMessage = _t("encryption|event_shield_reason_mismatched_sender_key"); break; + + case EventShieldReason.SENT_IN_CLEAR: + shieldReasonMessage = _t("common|unencrypted"); + break; + + case EventShieldReason.VERIFICATION_VIOLATION: + shieldReasonMessage = _t("timeline|decryption_failure|sender_identity_previously_verified"); + break; } if (this.state.shieldColour === EventShieldColour.GREY) { @@ -767,7 +775,7 @@ export class UnwrappedEventTile extends React.Component } } - if (MatrixClientPeg.safeGet().isRoomEncrypted(ev.getRoomId()!)) { + if (this.context.isRoomEncrypted) { // else if room is encrypted // and event is being encrypted or is not_sent (Unknown Devices/Network Error) if (ev.status === EventStatus.ENCRYPTING) { diff --git a/src/components/views/rooms/HistoryTile.tsx b/src/components/views/rooms/HistoryTile.tsx index c52ab044a7f..3aa74b8b0c7 100644 --- a/src/components/views/rooms/HistoryTile.tsx +++ b/src/components/views/rooms/HistoryTile.tsx @@ -6,15 +6,15 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { useContext } from "react"; +import React from "react"; import { EventTimeline } from "matrix-js-sdk/src/matrix"; import EventTileBubble from "../messages/EventTileBubble"; -import RoomContext from "../../../contexts/RoomContext"; import { _t } from "../../../languageHandler"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; const HistoryTile: React.FC = () => { - const { room } = useContext(RoomContext); + const { room } = useScopedRoomContext("room"); const oldState = room?.getLiveTimeline().getState(EventTimeline.BACKWARDS); const historyState = oldState?.getStateEvents("m.room.history_visibility")[0]?.getContent().history_visibility; diff --git a/src/components/views/rooms/MemberList.tsx b/src/components/views/rooms/MemberList.tsx index e503ce23633..5587b56bf82 100644 --- a/src/components/views/rooms/MemberList.tsx +++ b/src/components/views/rooms/MemberList.tsx @@ -75,7 +75,7 @@ export default class MemberList extends React.Component { private unmounted = false; public static contextType = SDKContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private tiles: Map = new Map(); public constructor(props: IProps, context: React.ContextType) { diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 27189000d1c..f5716d728b6 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -48,14 +48,9 @@ import MessageComposerButtons from "./MessageComposerButtons"; import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom"; -import { Features } from "../../../settings/Settings"; import { VoiceMessageRecording } from "../../../audio/VoiceMessageRecording"; import { SendWysiwygComposer, sendMessage, getConversionFunctions } from "./wysiwyg_composer/"; import { MatrixClientProps, withMatrixClientHOC } from "../../../contexts/MatrixClientContext"; -import { setUpVoiceBroadcastPreRecording } from "../../../voice-broadcast/utils/setUpVoiceBroadcastPreRecording"; -import { SdkContextClass } from "../../../contexts/SDKContext"; -import { VoiceBroadcastInfoState } from "../../../voice-broadcast"; -import { createCantStartVoiceMessageBroadcastDialog } from "../dialogs/CantStartVoiceMessageBroadcastDialog"; import { UIFeature } from "../../../settings/UIFeature"; import { formatTimeLeft } from "../../../DateUtils"; import RoomReplacedSvg from "../../../../res/img/room_replaced.svg"; @@ -101,7 +96,6 @@ interface IState { isStickerPickerOpen: boolean; showStickersButton: boolean; showPollsButton: boolean; - showVoiceBroadcastButton: boolean; isWysiwygLabEnabled: boolean; isRichTextEnabled: boolean; initialComposerContent: string; @@ -123,11 +117,10 @@ export class MessageComposer extends React.Component { private _voiceRecording: Optional; public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public static defaultProps = { compact: false, - showVoiceBroadcastButton: false, isRichTextEnabled: true, }; @@ -155,7 +148,6 @@ export class MessageComposer extends React.Component { isStickerPickerOpen: false, showStickersButton: SettingsStore.getValue("MessageComposerInput.showStickersButton"), showPollsButton: SettingsStore.getValue("MessageComposerInput.showPollsButton"), - showVoiceBroadcastButton: SettingsStore.getValue(Features.VoiceBroadcast), isWysiwygLabEnabled: isWysiwygLabEnabled, isRichTextEnabled: isRichTextEnabled, initialComposerContent: initialComposerContent, @@ -250,7 +242,6 @@ export class MessageComposer extends React.Component { SettingsStore.monitorSetting("MessageComposerInput.showStickersButton", null); SettingsStore.monitorSetting("MessageComposerInput.showPollsButton", null); - SettingsStore.monitorSetting(Features.VoiceBroadcast, null); SettingsStore.monitorSetting("feature_wysiwyg_composer", null); this.dispatcherRef = dis.register(this.onAction); @@ -301,12 +292,6 @@ export class MessageComposer extends React.Component { } break; } - case Features.VoiceBroadcast: { - if (this.state.showVoiceBroadcastButton !== settingUpdatedPayload.newValue) { - this.setState({ showVoiceBroadcastButton: !!settingUpdatedPayload.newValue }); - } - break; - } case "feature_wysiwyg_composer": { if (this.state.isWysiwygLabEnabled !== settingUpdatedPayload.newValue) { this.setState({ isWysiwygLabEnabled: Boolean(settingUpdatedPayload.newValue) }); @@ -533,13 +518,7 @@ export class MessageComposer extends React.Component { } private onRecordStartEndClick = (): void => { - const currentBroadcastRecording = SdkContextClass.instance.voiceBroadcastRecordingsStore.getCurrent(); - - if (currentBroadcastRecording && currentBroadcastRecording.getState() !== VoiceBroadcastInfoState.Stopped) { - createCantStartVoiceMessageBroadcastDialog(); - } else { - this.voiceRecordingButton.current?.onRecordStartEndClick(); - } + this.voiceRecordingButton.current?.onRecordStartEndClick(); if (this.context.narrow) { this.toggleButtonMenu(); @@ -698,17 +677,6 @@ export class MessageComposer extends React.Component { isRichTextEnabled={this.state.isRichTextEnabled} onComposerModeClick={this.onRichTextToggle} toggleButtonMenu={this.toggleButtonMenu} - showVoiceBroadcastButton={this.state.showVoiceBroadcastButton} - onStartVoiceBroadcastClick={() => { - setUpVoiceBroadcastPreRecording( - this.props.room, - MatrixClientPeg.safeGet(), - SdkContextClass.instance.voiceBroadcastPlaybacksStore, - SdkContextClass.instance.voiceBroadcastRecordingsStore, - SdkContextClass.instance.voiceBroadcastPreRecordingStore, - ); - this.toggleButtonMenu(); - }} /> )} {showSendButton && ( diff --git a/src/components/views/rooms/MessageComposerButtons.tsx b/src/components/views/rooms/MessageComposerButtons.tsx index 003c2afed96..19b86834dd7 100644 --- a/src/components/views/rooms/MessageComposerButtons.tsx +++ b/src/components/views/rooms/MessageComposerButtons.tsx @@ -21,7 +21,6 @@ import PollCreateDialog from "../elements/PollCreateDialog"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import ContentMessages from "../../../ContentMessages"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import RoomContext from "../../../contexts/RoomContext"; import { useDispatcher } from "../../../hooks/useDispatcher"; import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; import IconizedContextMenu, { IconizedContextMenuOptionList } from "../context_menus/IconizedContextMenu"; @@ -29,6 +28,7 @@ import { EmojiButton } from "./EmojiButton"; import { filterBoolean } from "../../../utils/arrays"; import { useSettingValue } from "../../../hooks/useSettings"; import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; interface IProps { addEmoji: (emoji: string) => boolean; @@ -43,8 +43,6 @@ interface IProps { showPollsButton: boolean; showStickersButton: boolean; toggleButtonMenu: () => void; - showVoiceBroadcastButton: boolean; - onStartVoiceBroadcastClick: () => void; isRichTextEnabled: boolean; onComposerModeClick: () => void; } @@ -54,7 +52,7 @@ export const OverflowMenuContext = createContext(null const MessageComposerButtons: React.FC = (props: IProps) => { const matrixClient = useContext(MatrixClientContext); - const { room, narrow } = useContext(RoomContext); + const { room, narrow } = useScopedRoomContext("room", "narrow"); const isWysiwygLabEnabled = useSettingValue("feature_wysiwyg_composer"); @@ -80,7 +78,6 @@ const MessageComposerButtons: React.FC = (props: IProps) => { uploadButton(), // props passed via UploadButtonContext showStickersButton(props), voiceRecordingButton(props, narrow), - startVoiceBroadcastButton(props), props.showPollsButton ? pollButton(room, props.relation) : null, showLocationButton(props, room, matrixClient), ]; @@ -100,7 +97,6 @@ const MessageComposerButtons: React.FC = (props: IProps) => { moreButtons = [ showStickersButton(props), voiceRecordingButton(props, narrow), - startVoiceBroadcastButton(props), props.showPollsButton ? pollButton(room, props.relation) : null, showLocationButton(props, room, matrixClient), ]; @@ -168,7 +164,7 @@ interface IUploadButtonProps { // We put the file input outside the UploadButton component so that it doesn't get killed when the context menu closes. const UploadButtonContextProvider: React.FC = ({ roomId, relation, children }) => { const cli = useContext(MatrixClientContext); - const roomContext = useContext(RoomContext); + const roomContext = useScopedRoomContext("timelineRenderingType"); const uploadInput = useRef(null); const onUploadClick = (): void => { @@ -254,18 +250,6 @@ function showStickersButton(props: IProps): ReactElement | null { ) : null; } -const startVoiceBroadcastButton: React.FC = (props: IProps): ReactElement | null => { - return props.showVoiceBroadcastButton ? ( - - ) : null; -}; - function voiceRecordingButton(props: IProps, narrow: boolean): ReactElement | null { // XXX: recording UI does not work well in narrow mode, so hide for now return narrow ? null : ( @@ -290,7 +274,7 @@ interface IPollButtonProps { class PollButton extends React.PureComponent { public static contextType = OverflowMenuContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private onCreateClick = (): void => { this.context?.(); // close overflow menu diff --git a/src/components/views/rooms/MessageComposerFormatBar.tsx b/src/components/views/rooms/MessageComposerFormatBar.tsx index 34798cc6089..0ab359d9ddd 100644 --- a/src/components/views/rooms/MessageComposerFormatBar.tsx +++ b/src/components/views/rooms/MessageComposerFormatBar.tsx @@ -33,6 +33,12 @@ interface IState { export default class MessageComposerFormatBar extends React.PureComponent { private readonly formatBarRef = createRef(); + /** + * The height of the format bar in pixels. + * Height 32px + 2px border + * @private + */ + private readonly BAR_HEIGHT = 34; public constructor(props: IProps) { super(props); @@ -96,7 +102,7 @@ export default class MessageComposerFormatBar extends React.PureComponent { const cli = useContext(MatrixClientContext); - const { room, roomId } = useContext(RoomContext); + const { room, roomId } = useScopedRoomContext("room", "roomId"); if (!room || !roomId) { throw new Error("Unable to create a NewRoomIntro without room and roomId"); diff --git a/src/components/views/rooms/PinnedMessageBanner.tsx b/src/components/views/rooms/PinnedMessageBanner.tsx index f44b4417c99..32000d57925 100644 --- a/src/components/views/rooms/PinnedMessageBanner.tsx +++ b/src/components/views/rooms/PinnedMessageBanner.tsx @@ -6,10 +6,10 @@ * Please see LICENSE files in the repository root for full details. */ -import React, { JSX, useEffect, useState } from "react"; +import React, { JSX, useEffect, useRef, useState } from "react"; import PinIcon from "@vector-im/compound-design-tokens/assets/web/icons/pin-solid"; import { Button } from "@vector-im/compound-web"; -import { Room } from "matrix-js-sdk/src/matrix"; +import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; import classNames from "classnames"; import { usePinnedEvents, useSortedFetchedPinnedEvents } from "../../../hooks/usePinnedEvents"; @@ -25,6 +25,7 @@ import { Action } from "../../../dispatcher/actions"; import MessageEvent from "../messages/MessageEvent"; import PosthogTrackers from "../../../PosthogTrackers.ts"; import { EventPreview } from "./EventPreview.tsx"; +import ResizeNotifier from "../../../utils/ResizeNotifier"; /** * The props for the {@link PinnedMessageBanner} component. @@ -38,12 +39,20 @@ interface PinnedMessageBannerProps { * The room where the banner is displayed */ room: Room; + /** + * The resize notifier to notify the timeline to resize itself when the banner is displayed or hidden. + */ + resizeNotifier: ResizeNotifier; } /** * A banner that displays the pinned messages in a room. */ -export function PinnedMessageBanner({ room, permalinkCreator }: PinnedMessageBannerProps): JSX.Element | null { +export function PinnedMessageBanner({ + room, + permalinkCreator, + resizeNotifier, +}: PinnedMessageBannerProps): JSX.Element | null { const pinnedEventIds = usePinnedEvents(room); const pinnedEvents = useSortedFetchedPinnedEvents(room, pinnedEventIds); const eventCount = pinnedEvents.length; @@ -56,6 +65,8 @@ export function PinnedMessageBanner({ room, permalinkCreator }: PinnedMessageBan }, [eventCount]); const pinnedEvent = pinnedEvents[currentEventIndex]; + useNotifyTimeline(pinnedEvent, resizeNotifier); + if (!pinnedEvent) return null; const shouldUseMessageEvent = pinnedEvent.isRedacted() || pinnedEvent.isDecryptionFailure(); @@ -128,6 +139,23 @@ export function PinnedMessageBanner({ room, permalinkCreator }: PinnedMessageBan ); } +/** + * When the banner is displayed or hidden, we want to notify the timeline to resize itself. + * @param pinnedEvent + * @param resizeNotifier + */ +function useNotifyTimeline(pinnedEvent: MatrixEvent | null, resizeNotifier: ResizeNotifier): void { + const previousEvent = useRef(null); + useEffect(() => { + // If we switch from a pinned message to no pinned message or the opposite, we want to resize the timeline + if ((previousEvent.current && !pinnedEvent) || (!previousEvent.current && pinnedEvent)) { + resizeNotifier.notifyTimelineHeightChanged(); + } + + previousEvent.current = pinnedEvent; + }, [pinnedEvent, resizeNotifier]); +} + const MAX_INDICATORS = 3; /** diff --git a/src/components/views/rooms/ReplyPreview.tsx b/src/components/views/rooms/ReplyPreview.tsx index c820154b2b5..7851f7914d6 100644 --- a/src/components/views/rooms/ReplyPreview.tsx +++ b/src/components/views/rooms/ReplyPreview.tsx @@ -31,7 +31,7 @@ interface IProps { export default class ReplyPreview extends React.Component { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public render(): JSX.Element | null { if (!this.props.replyToEvent) return null; diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index c2642ea733a..d133587fc92 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { useCallback, useContext, useMemo, useState } from "react"; +import React, { useCallback, useMemo, useState } from "react"; import { Body as BodyText, Button, IconButton, Menu, MenuItem, Tooltip } from "@vector-im/compound-web"; import VideoCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/video-call-solid"; import VoiceCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/voice-call"; @@ -48,10 +48,10 @@ import { CallGuestLinkButton } from "./RoomHeader/CallGuestLinkButton"; import { ButtonEvent } from "../elements/AccessibleButton"; import WithPresenceIndicator, { useDmMember } from "../avatars/WithPresenceIndicator"; import { IOOBData } from "../../../stores/ThreepidInviteStore"; -import RoomContext from "../../../contexts/RoomContext"; import { MainSplitContentType } from "../../structures/RoomView"; import defaultDispatcher from "../../../dispatcher/dispatcher.ts"; import { RoomSettingsTab } from "../dialogs/RoomSettingsDialog.tsx"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; export default function RoomHeader({ room, @@ -229,7 +229,7 @@ export default function RoomHeader({ voiceCallButton = undefined; } - const roomContext = useContext(RoomContext); + const roomContext = useScopedRoomContext("mainSplitContentType"); const isVideoRoom = calcIsVideoRoom(room); const showChatButton = isVideoRoom || @@ -392,7 +392,7 @@ export default function RoomHeader({ viewUserOnClick={false} tooltipLabel={_t("room|header_face_pile_tooltip")} onClick={(e: ButtonEvent) => { - RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomMemberList); + RightPanelStore.instance.showOrHidePhase(RightPanelPhases.MemberList); e.stopPropagation(); }} aria-label={_t("common|n_members", { count: memberCount })} diff --git a/src/components/views/rooms/RoomHeader/CallGuestLinkButton.tsx b/src/components/views/rooms/RoomHeader/CallGuestLinkButton.tsx index ae8e7be16bf..8c000bdf3bc 100644 --- a/src/components/views/rooms/RoomHeader/CallGuestLinkButton.tsx +++ b/src/components/views/rooms/RoomHeader/CallGuestLinkButton.tsx @@ -12,7 +12,7 @@ import { logger } from "matrix-js-sdk/src/logger"; import { EventType, JoinRule, Room } from "matrix-js-sdk/src/matrix"; import Modal from "../../../../Modal"; -import ShareDialog from "../../dialogs/ShareDialog"; +import { ShareDialog } from "../../dialogs/ShareDialog"; import { _t } from "../../../../languageHandler"; import SettingsStore from "../../../../settings/SettingsStore"; import { calculateRoomVia } from "../../../../utils/permalinks/Permalinks"; diff --git a/src/components/views/rooms/RoomInfoLine.tsx b/src/components/views/rooms/RoomInfoLine.tsx index 710ef617583..1487bfe15b7 100644 --- a/src/components/views/rooms/RoomInfoLine.tsx +++ b/src/components/views/rooms/RoomInfoLine.tsx @@ -64,7 +64,7 @@ const RoomInfoLine: FC = ({ room }) => { // summary is not still loading const viewMembers = (): void => RightPanelStore.instance.setCard({ - phase: room.isSpaceRoom() ? RightPanelPhases.SpaceMemberList : RightPanelPhases.RoomMemberList, + phase: RightPanelPhases.MemberList, }); members = ( diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index 853bebc4fef..f3bde66af97 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -424,7 +424,7 @@ export default class RoomList extends React.PureComponent { private treeRef = createRef(); public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public constructor(props: IProps, context: React.ContextType) { super(props, context); diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index 8351c176ff0..7953c5068db 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -39,7 +39,6 @@ import { getKeyBindingsManager } from "../../../KeyBindingsManager"; import { RoomGeneralContextMenu } from "../context_menus/RoomGeneralContextMenu"; import { CallStore, CallStoreEvent } from "../../../stores/CallStore"; import { SdkContextClass } from "../../../contexts/SDKContext"; -import { useHasRoomLiveVoiceBroadcast } from "../../../voice-broadcast"; import { RoomTileSubtitle } from "./RoomTileSubtitle"; import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; import { UIComponent } from "../../../settings/UIFeature"; @@ -53,10 +52,6 @@ interface Props { tag: TagID; } -interface ClassProps extends Props { - hasLiveVoiceBroadcast: boolean; -} - type PartialDOMRect = Pick; interface State { @@ -77,13 +72,13 @@ export const contextMenuBelow = (elementRect: PartialDOMRect): MenuProps => { return { left, top, chevronFace }; }; -export class RoomTile extends React.PureComponent { +class RoomTile extends React.PureComponent { private dispatcherRef?: string; private roomTileRef = createRef(); private notificationState: NotificationState; private roomProps: RoomEchoChamber; - public constructor(props: ClassProps) { + public constructor(props: Props) { super(props); this.state = { @@ -370,15 +365,10 @@ export class RoomTile extends React.PureComponent { /** * RoomTile has a subtile if one of the following applies: * - there is a call - * - there is a live voice broadcast * - message previews are enabled and there is a previewable message */ private get shouldRenderSubtitle(): boolean { - return ( - !!this.state.call || - this.props.hasLiveVoiceBroadcast || - (this.props.showMessagePreview && !!this.state.messagePreview) - ); + return !!this.state.call || (this.props.showMessagePreview && !!this.state.messagePreview); } public render(): React.ReactElement { @@ -409,7 +399,6 @@ export class RoomTile extends React.PureComponent { const subtitle = this.shouldRenderSubtitle ? ( { } } -const RoomTileHOC: React.FC = (props: Props) => { - const hasLiveVoiceBroadcast = useHasRoomLiveVoiceBroadcast(props.room); - return ; -}; - -export default RoomTileHOC; +export default RoomTile; diff --git a/src/components/views/rooms/RoomTileSubtitle.tsx b/src/components/views/rooms/RoomTileSubtitle.tsx index ea4a96d2593..479b9c4f717 100644 --- a/src/components/views/rooms/RoomTileSubtitle.tsx +++ b/src/components/views/rooms/RoomTileSubtitle.tsx @@ -13,11 +13,9 @@ import { ThreadsIcon } from "@vector-im/compound-design-tokens/assets/web/icons" import { MessagePreview } from "../../../stores/room-list/MessagePreviewStore"; import { Call } from "../../../models/Call"; import { RoomTileCallSummary } from "./RoomTileCallSummary"; -import { VoiceBroadcastRoomSubtitle } from "../../../voice-broadcast"; interface Props { call: Call | null; - hasLiveVoiceBroadcast: boolean; messagePreview: MessagePreview | null; roomId: string; showMessagePreview: boolean; @@ -25,13 +23,7 @@ interface Props { const messagePreviewId = (roomId: string): string => `mx_RoomTile_messagePreview_${roomId}`; -export const RoomTileSubtitle: React.FC = ({ - call, - hasLiveVoiceBroadcast, - messagePreview, - roomId, - showMessagePreview, -}) => { +export const RoomTileSubtitle: React.FC = ({ call, messagePreview, roomId, showMessagePreview }) => { if (call) { return (
@@ -40,10 +32,6 @@ export const RoomTileSubtitle: React.FC = ({ ); } - if (hasLiveVoiceBroadcast) { - return ; - } - if (showMessagePreview && messagePreview) { const className = classNames("mx_RoomTile_subtitle", { "mx_RoomTile_subtitle--thread-reply": messagePreview.isThreadReply, diff --git a/src/components/views/rooms/RoomUpgradeWarningBar.tsx b/src/components/views/rooms/RoomUpgradeWarningBar.tsx index 66519fa7669..e92be96cb20 100644 --- a/src/components/views/rooms/RoomUpgradeWarningBar.tsx +++ b/src/components/views/rooms/RoomUpgradeWarningBar.tsx @@ -25,7 +25,7 @@ interface IState { export default class RoomUpgradeWarningBar extends React.PureComponent { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public constructor(props: IProps, context: React.ContextType) { super(props, context); diff --git a/src/components/views/rooms/SearchResultTile.tsx b/src/components/views/rooms/SearchResultTile.tsx index 5ebbaffdd92..94f5e6da9db 100644 --- a/src/components/views/rooms/SearchResultTile.tsx +++ b/src/components/views/rooms/SearchResultTile.tsx @@ -36,7 +36,7 @@ interface IProps { export default class SearchResultTile extends React.Component { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; // A map of private callEventGroupers = new Map(); diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 252957c2c77..b3767cbd2a0 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -193,7 +193,6 @@ export function createMessageContent( body: body, }; const formattedBody = htmlSerializeIfNeeded(model, { - forceHTML: !!replyToEvent, useMarkdown: SettingsStore.getValue("MessageComposerInput.useMarkdown"), }); if (formattedBody) { @@ -241,7 +240,7 @@ interface ISendMessageComposerProps extends MatrixClientProps { export class SendMessageComposer extends React.Component { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private readonly prepareToEncrypt?: DebouncedFunc<() => void>; private readonly editorRef = createRef(); diff --git a/src/components/views/rooms/ThreadSummary.tsx b/src/components/views/rooms/ThreadSummary.tsx index 4a3032d6411..ac23331f66d 100644 --- a/src/components/views/rooms/ThreadSummary.tsx +++ b/src/components/views/rooms/ThreadSummary.tsx @@ -16,7 +16,6 @@ import { CardContext } from "../right_panel/context"; import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; import PosthogTrackers from "../../../PosthogTrackers"; import { useTypedEventEmitterState } from "../../../hooks/useEventEmitter"; -import RoomContext from "../../../contexts/RoomContext"; import MemberAvatar from "../avatars/MemberAvatar"; import { Action } from "../../../dispatcher/actions"; import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload"; @@ -24,6 +23,7 @@ import defaultDispatcher from "../../../dispatcher/dispatcher"; import { useUnreadNotifications } from "../../../hooks/useUnreadNotifications"; import { notificationLevelToIndicator } from "../../../utils/notifications"; import { EventPreviewTile, useEventPreview } from "./EventPreview.tsx"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; interface IProps { mxEvent: MatrixEvent; @@ -31,7 +31,7 @@ interface IProps { } const ThreadSummary: React.FC = ({ mxEvent, thread, ...props }) => { - const roomContext = useContext(RoomContext); + const roomContext = useScopedRoomContext("narrow"); const cardContext = useContext(CardContext); const count = useTypedEventEmitterState(thread, ThreadEvent.Update, () => thread.length); const { level } = useUnreadNotifications(thread.room, thread.id); diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index a6d4a2fc276..a8335a9902b 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -53,7 +53,7 @@ interface IState { */ export default class VoiceRecordComposerTile extends React.PureComponent { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private voiceRecordingId: string; public constructor(props: IProps, context: React.ContextType) { diff --git a/src/components/views/rooms/wysiwyg_composer/components/Emoji.tsx b/src/components/views/rooms/wysiwyg_composer/components/Emoji.tsx index b7a6d65e235..9ab3d210abc 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/Emoji.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/Emoji.tsx @@ -13,14 +13,14 @@ import { EmojiButton } from "../../EmojiButton"; import dis from "../../../../../dispatcher/dispatcher"; import { ComposerInsertPayload } from "../../../../../dispatcher/payloads/ComposerInsertPayload"; import { Action } from "../../../../../dispatcher/actions"; -import { useRoomContext } from "../../../../../contexts/RoomContext"; +import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx"; interface EmojiProps { menuPosition: MenuProps; } export function Emoji({ menuPosition }: EmojiProps): JSX.Element { - const roomContext = useRoomContext(); + const roomContext = useScopedRoomContext("timelineRenderingType"); return ( , ): JSX.Element | null => { - const { room } = useRoomContext(); + const { room } = useScopedRoomContext("room"); const client = useMatrixClientContext(); function handleConfirm(completion: ICompletion): void { diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx index 5b6361a58e8..f1e42ce091f 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx @@ -19,12 +19,12 @@ import { Editor } from "./Editor"; import { useInputEventProcessor } from "../hooks/useInputEventProcessor"; import { useSetCursorPosition } from "../hooks/useSetCursorPosition"; import { useIsFocused } from "../hooks/useIsFocused"; -import { useRoomContext } from "../../../../../contexts/RoomContext"; import defaultDispatcher from "../../../../../dispatcher/dispatcher"; import { Action } from "../../../../../dispatcher/actions"; import { parsePermalink } from "../../../../../utils/permalinks/Permalinks"; import { isNotNull } from "../../../../../Typeguards"; import { useSettingValue } from "../../../../../hooks/useSettings"; +import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx"; interface WysiwygComposerProps { disabled?: boolean; @@ -56,7 +56,7 @@ export const WysiwygComposer = memo(function WysiwygComposer({ children, eventRelation, }: WysiwygComposerProps) { - const { room } = useRoomContext(); + const { room } = useScopedRoomContext("room"); const autocompleteRef = useRef(null); const inputEventProcessor = useInputEventProcessor(onSend, autocompleteRef, initialContent, eventRelation); diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useEditing.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useEditing.ts index 5d1c3b867e2..20f394e8a3e 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useEditing.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useEditing.ts @@ -10,10 +10,10 @@ import { ISendEventResponse } from "matrix-js-sdk/src/matrix"; import { useCallback, useState } from "react"; import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext"; -import { useRoomContext } from "../../../../../contexts/RoomContext"; import EditorStateTransfer from "../../../../../utils/EditorStateTransfer"; import { endEditing } from "../utils/editing"; import { editMessage } from "../utils/message"; +import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx"; export function useEditing( editorStateTransfer: EditorStateTransfer, @@ -24,7 +24,7 @@ export function useEditing( editMessage(): Promise; endEditing(): void; } { - const roomContext = useRoomContext(); + const roomContext = useScopedRoomContext("timelineRenderingType"); const mxClient = useMatrixClientContext(); const [isSaveDisabled, setIsSaveDisabled] = useState(true); diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useInitialContent.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useInitialContent.ts index 2e0ddd3ccd1..3a3799496b8 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useInitialContent.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useInitialContent.ts @@ -10,11 +10,11 @@ import { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; import { useMemo } from "react"; import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext"; -import { useRoomContext } from "../../../../../contexts/RoomContext"; import { parseEvent } from "../../../../../editor/deserialize"; import { CommandPartCreator, Part } from "../../../../../editor/parts"; import SettingsStore from "../../../../../settings/SettingsStore"; import EditorStateTransfer from "../../../../../utils/EditorStateTransfer"; +import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx"; function getFormattedContent(editorStateTransfer: EditorStateTransfer): string { return ( @@ -60,12 +60,12 @@ export function parseEditorStateTransfer( } export function useInitialContent(editorStateTransfer: EditorStateTransfer): string | undefined { - const roomContext = useRoomContext(); + const { room } = useScopedRoomContext("room"); const mxClient = useMatrixClientContext(); return useMemo(() => { - if (editorStateTransfer && roomContext.room && mxClient) { - return parseEditorStateTransfer(editorStateTransfer, roomContext.room, mxClient); + if (editorStateTransfer && room && mxClient) { + return parseEditorStateTransfer(editorStateTransfer, room, mxClient); } - }, [editorStateTransfer, roomContext, mxClient]); + }, [editorStateTransfer, room, mxClient]); } diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts index 8eac63eb366..cab3bdefb85 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts @@ -16,7 +16,6 @@ import { KeyBindingAction } from "../../../../../accessibility/KeyboardShortcuts import { findEditableEvent } from "../../../../../utils/EventUtils"; import dis from "../../../../../dispatcher/dispatcher"; import { Action } from "../../../../../dispatcher/actions"; -import { useRoomContext } from "../../../../../contexts/RoomContext"; import { IRoomState } from "../../../../structures/RoomView"; import { ComposerContextState, useComposerContext } from "../ComposerContext"; import EditorStateTransfer from "../../../../../utils/EditorStateTransfer"; @@ -26,6 +25,7 @@ import { getEventsFromEditorStateTransfer, getEventsFromRoom } from "../utils/ev import { endEditing } from "../utils/editing"; import Autocomplete from "../../Autocomplete"; import { handleClipboardEvent, handleEventWithAutocomplete, isEventToHandleAsClipboardEvent } from "./utils"; +import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx"; export function useInputEventProcessor( onSend: () => void, @@ -33,7 +33,7 @@ export function useInputEventProcessor( initialContent?: string, eventRelation?: IEventRelation, ): (event: WysiwygEvent, composer: Wysiwyg, editor: HTMLElement) => WysiwygEvent | null { - const roomContext = useRoomContext(); + const roomContext = useScopedRoomContext("liveTimeline", "room", "replyToEvent", "timelineRenderingType"); const composerContext = useComposerContext(); const mxClient = useMatrixClientContext(); const isCtrlEnterToSend = useSettingValue("MessageComposerInput.ctrlEnterToSend"); @@ -94,7 +94,7 @@ function handleKeyboardEvent( initialContent: string | undefined, composer: Wysiwyg, editor: HTMLElement, - roomContext: IRoomState, + roomContext: Pick, composerContext: ComposerContextState, mxClient: MatrixClient | undefined, autocompleteRef: React.RefObject, @@ -175,7 +175,7 @@ function dispatchEditEvent( isForward: boolean, editorStateTransfer: EditorStateTransfer | undefined, composerContext: ComposerContextState, - roomContext: IRoomState, + roomContext: Pick, mxClient: MatrixClient, ): boolean { const foundEvents = editorStateTransfer diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts index 742d24fe341..1dc23cc274a 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts @@ -16,8 +16,8 @@ import Autocomplete from "../../Autocomplete"; import { handleClipboardEvent, handleEventWithAutocomplete, isEventToHandleAsClipboardEvent } from "./utils"; import { useSuggestion } from "./useSuggestion"; import { isNotNull, isNotUndefined } from "../../../../../Typeguards"; -import { useRoomContext } from "../../../../../contexts/RoomContext"; import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext"; +import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx"; function isDivElement(target: EventTarget): target is HTMLDivElement { return target instanceof HTMLDivElement; @@ -63,7 +63,7 @@ export function usePlainTextListeners( onSelect: (event: SyntheticEvent) => void; suggestion: MappedSuggestion | null; } { - const roomContext = useRoomContext(); + const roomContext = useScopedRoomContext("room", "timelineRenderingType", "replyToEvent"); const mxClient = useMatrixClientContext(); const ref = useRef(null); diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts index e1e34623c85..eb76d77af5d 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts @@ -11,20 +11,21 @@ import { RefObject, useCallback, useRef } from "react"; import defaultDispatcher from "../../../../../dispatcher/dispatcher"; import { Action } from "../../../../../dispatcher/actions"; import { ActionPayload } from "../../../../../dispatcher/payloads"; -import { TimelineRenderingType, useRoomContext } from "../../../../../contexts/RoomContext"; +import { TimelineRenderingType } from "../../../../../contexts/RoomContext"; import { useDispatcher } from "../../../../../hooks/useDispatcher"; import { focusComposer } from "./utils"; import { ComposerType } from "../../../../../dispatcher/payloads/ComposerInsertPayload"; import { ComposerFunctions } from "../types"; import { setSelection } from "../utils/selection"; import { useComposerContext } from "../ComposerContext"; +import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx"; export function useWysiwygEditActionHandler( disabled: boolean, composerElement: RefObject, composerFunctions: ComposerFunctions, ): void { - const roomContext = useRoomContext(); + const roomContext = useScopedRoomContext("timelineRenderingType"); const composerContext = useComposerContext(); const timeoutId = useRef(null); diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts index 16e1e608ec2..d11f3498fd7 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts @@ -11,20 +11,21 @@ import { MutableRefObject, useCallback, useRef } from "react"; import defaultDispatcher from "../../../../../dispatcher/dispatcher"; import { Action } from "../../../../../dispatcher/actions"; import { ActionPayload } from "../../../../../dispatcher/payloads"; -import { TimelineRenderingType, useRoomContext } from "../../../../../contexts/RoomContext"; +import { TimelineRenderingType } from "../../../../../contexts/RoomContext"; import { useDispatcher } from "../../../../../hooks/useDispatcher"; import { focusComposer } from "./utils"; import { ComposerFunctions } from "../types"; import { ComposerType } from "../../../../../dispatcher/payloads/ComposerInsertPayload"; import { useComposerContext } from "../ComposerContext"; import { setSelection } from "../utils/selection"; +import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx"; export function useWysiwygSendActionHandler( disabled: boolean, composerElement: MutableRefObject, composerFunctions: ComposerFunctions, ): void { - const roomContext = useRoomContext(); + const roomContext = useScopedRoomContext("timelineRenderingType"); const composerContext = useComposerContext(); const timeoutId = useRef(null); diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts b/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts index 39317ea88cd..3345c9f474a 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts @@ -22,7 +22,7 @@ import { isNotNull } from "../../../../../Typeguards"; export function focusComposer( composerElement: MutableRefObject, renderingType: TimelineRenderingType, - roomContext: IRoomState, + roomContext: Pick, timeoutId: MutableRefObject, ): void { if (renderingType === roomContext.timelineRenderingType) { @@ -123,7 +123,7 @@ export function handleEventWithAutocomplete( export function handleClipboardEvent( event: ClipboardEvent | InputEvent, data: DataTransfer | null, - roomContext: IRoomState, + roomContext: Pick, mxClient: MatrixClient, eventRelation?: IEventRelation, ): boolean { diff --git a/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts b/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts index 7f42ed2327a..58d09b3d128 100644 --- a/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts +++ b/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts @@ -27,28 +27,6 @@ function attachRelation(content: IContent, relation?: IEventRelation): void { } } -function getHtmlReplyFallback(mxEvent: MatrixEvent): string { - const html = mxEvent.getContent().formatted_body; - if (!html) { - return ""; - } - const rootNode = new DOMParser().parseFromString(html, "text/html").body; - const mxReply = rootNode.querySelector("mx-reply"); - return (mxReply && mxReply.outerHTML) || ""; -} - -function getTextReplyFallback(mxEvent: MatrixEvent): string { - const body = mxEvent.getContent().body; - if (typeof body !== "string") { - return ""; - } - const lines = body.split("\n").map((l) => l.trim()); - if (lines.length > 2 && lines[0].startsWith("> ") && lines[1].length === 0) { - return `${lines[0]}\n\n`; - } - return ""; -} - interface CreateMessageContentParams { relation?: IEventRelation; replyToEvent?: MatrixEvent; @@ -63,8 +41,6 @@ export async function createMessageContent( { relation, replyToEvent, editedEvent }: CreateMessageContentParams, ): Promise { const isEditing = isMatrixEvent(editedEvent); - const isReply = isEditing ? Boolean(editedEvent.replyEventId) : isMatrixEvent(replyToEvent); - const isReplyAndEditing = isEditing && isReply; const isEmote = message.startsWith(EMOTE_PREFIX); if (isEmote) { @@ -82,12 +58,10 @@ export async function createMessageContent( // if we're editing rich text, the message content is pure html // BUT if we're not, the message content will be plain text where we need to convert the mentions const body = isHTML ? await richToPlain(message, false) : convertPlainTextToBody(message); - const bodyPrefix = (isReplyAndEditing && getTextReplyFallback(editedEvent)) || ""; - const formattedBodyPrefix = (isReplyAndEditing && getHtmlReplyFallback(editedEvent)) || ""; const content = { msgtype: isEmote ? MsgType.Emote : MsgType.Text, - body: isEditing ? `${bodyPrefix} * ${body}` : body, + body: isEditing ? `* ${body}` : body, } as RoomMessageTextEventContent & ReplacementEvent; // TODO markdown support @@ -97,7 +71,7 @@ export async function createMessageContent( if (formattedBody) { content.format = "org.matrix.custom.html"; - content.formatted_body = isEditing ? `${formattedBodyPrefix} * ${formattedBody}` : formattedBody; + content.formatted_body = isEditing ? `* ${formattedBody}` : formattedBody; } if (isEditing) { diff --git a/src/components/views/rooms/wysiwyg_composer/utils/editing.ts b/src/components/views/rooms/wysiwyg_composer/utils/editing.ts index 58a9e244924..462763b8f46 100644 --- a/src/components/views/rooms/wysiwyg_composer/utils/editing.ts +++ b/src/components/views/rooms/wysiwyg_composer/utils/editing.ts @@ -13,7 +13,7 @@ import dis from "../../../../../dispatcher/dispatcher"; import { Action } from "../../../../../dispatcher/actions"; import EditorStateTransfer from "../../../../../utils/EditorStateTransfer"; -export function endEditing(roomContext: IRoomState): void { +export function endEditing(roomContext: Pick): void { // todo local storage // localStorage.removeItem(this.editorRoomKey); // localStorage.removeItem(this.editorStateKey); diff --git a/src/components/views/rooms/wysiwyg_composer/utils/event.ts b/src/components/views/rooms/wysiwyg_composer/utils/event.ts index 5fd37b3665b..45c6b1cac34 100644 --- a/src/components/views/rooms/wysiwyg_composer/utils/event.ts +++ b/src/components/views/rooms/wysiwyg_composer/utils/event.ts @@ -15,7 +15,7 @@ import { ComposerContextState } from "../ComposerContext"; // From EditMessageComposer private get events(): MatrixEvent[] export function getEventsFromEditorStateTransfer( editorStateTransfer: EditorStateTransfer, - roomContext: IRoomState, + roomContext: Pick, mxClient: MatrixClient, ): MatrixEvent[] | undefined { const liveTimelineEvents = roomContext.liveTimeline?.getEvents(); @@ -41,7 +41,7 @@ export function getEventsFromEditorStateTransfer( // From SendMessageComposer private onKeyDown = (event: KeyboardEvent): void export function getEventsFromRoom( composerContext: ComposerContextState, - roomContext: IRoomState, + roomContext: Pick, ): MatrixEvent[] | undefined { const isReplyingToThread = composerContext.eventRelation?.key === THREAD_RELATION_TYPE.name; return roomContext.liveTimeline diff --git a/src/components/views/rooms/wysiwyg_composer/utils/message.ts b/src/components/views/rooms/wysiwyg_composer/utils/message.ts index 44e10e3cc53..b7fca8ecb4a 100644 --- a/src/components/views/rooms/wysiwyg_composer/utils/message.ts +++ b/src/components/views/rooms/wysiwyg_composer/utils/message.ts @@ -39,7 +39,7 @@ export interface SendMessageParams { mxClient: MatrixClient; relation?: IEventRelation; replyToEvent?: MatrixEvent; - roomContext: IRoomState; + roomContext: Pick; } export async function sendMessage( @@ -177,7 +177,7 @@ export async function sendMessage( interface EditMessageParams { mxClient: MatrixClient; - roomContext: IRoomState; + roomContext: Pick; editorStateTransfer: EditorStateTransfer; } diff --git a/src/components/views/settings/AvatarSetting.tsx b/src/components/views/settings/AvatarSetting.tsx index b6ce5415903..ee47094cf93 100644 --- a/src/components/views/settings/AvatarSetting.tsx +++ b/src/components/views/settings/AvatarSetting.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { ReactNode, createRef, useCallback, useEffect, useState } from "react"; +import React, { ReactNode, createRef, useCallback, useEffect, useState, useId } from "react"; import EditIcon from "@vector-im/compound-design-tokens/assets/web/icons/edit"; import UploadIcon from "@vector-im/compound-design-tokens/assets/web/icons/share"; import DeleteIcon from "@vector-im/compound-design-tokens/assets/web/icons/delete"; @@ -16,7 +16,6 @@ import classNames from "classnames"; import { _t } from "../../../languageHandler"; import { mediaFromMxc } from "../../../customisations/Media"; import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; -import { useId } from "../../../utils/useId"; import AccessibleButton from "../elements/AccessibleButton"; import BaseAvatar from "../avatars/BaseAvatar"; import Modal from "../../../Modal.tsx"; diff --git a/src/components/views/settings/CryptographyPanel.tsx b/src/components/views/settings/CryptographyPanel.tsx index b418c0b05df..fbd696f243f 100644 --- a/src/components/views/settings/CryptographyPanel.tsx +++ b/src/components/views/settings/CryptographyPanel.tsx @@ -32,7 +32,7 @@ interface IState { export default class CryptographyPanel extends React.Component { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public constructor(props: IProps, context: React.ContextType) { super(props); diff --git a/src/components/views/settings/SetIdServer.tsx b/src/components/views/settings/SetIdServer.tsx index 8ed6461d0a3..6dc3ae48a2c 100644 --- a/src/components/views/settings/SetIdServer.tsx +++ b/src/components/views/settings/SetIdServer.tsx @@ -407,7 +407,6 @@ export default class SetIdServer extends React.Component { forceValidity={this.state.error ? false : undefined} /> = Omit< - ComponentProps>, - "aria-label" | "title" | "kind" | "className" | "onClick" | "element" +type Props = Omit< + ButtonProps, + "aria-label" | "title" | "kind" | "className" | "element" > & { isExpanded: boolean; - onClick: () => void; }; -export const DeviceExpandDetailsButton = ({ +export const DeviceExpandDetailsButton = ({ isExpanded, - onClick, ...rest }: Props): JSX.Element => { const label = isExpanded ? _t("settings|sessions|hide_details") : _t("settings|sessions|show_details"); @@ -36,7 +34,6 @@ export const DeviceExpandDetailsButton = className={classNames("mx_DeviceExpandDetailsButton", { mx_DeviceExpandDetailsButton_expanded: isExpanded, })} - onClick={onClick} > diff --git a/src/components/views/settings/devices/LoginWithQRSection.tsx b/src/components/views/settings/devices/LoginWithQRSection.tsx index a164ff894b4..033aa8e32ab 100644 --- a/src/components/views/settings/devices/LoginWithQRSection.tsx +++ b/src/components/views/settings/devices/LoginWithQRSection.tsx @@ -7,13 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; -import { - IServerVersions, - IClientWellKnown, - OidcClientConfig, - MatrixClient, - DEVICE_CODE_SCOPE, -} from "matrix-js-sdk/src/matrix"; +import { IServerVersions, OidcClientConfig, MatrixClient, DEVICE_CODE_SCOPE } from "matrix-js-sdk/src/matrix"; import QrCodeIcon from "@vector-im/compound-design-tokens/assets/web/icons/qr-code"; import { Text } from "@vector-im/compound-web"; @@ -25,7 +19,6 @@ import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext interface IProps { onShowQr: () => void; versions?: IServerVersions; - wellKnown?: IClientWellKnown; oidcClientConfig?: OidcClientConfig; isCrossSigningReady?: boolean; } @@ -35,10 +28,8 @@ export function shouldShowQr( isCrossSigningReady: boolean, oidcClientConfig?: OidcClientConfig, versions?: IServerVersions, - wellKnown?: IClientWellKnown, ): boolean { - const msc4108Supported = - !!versions?.unstable_features?.["org.matrix.msc4108"] || !!wellKnown?.["io.element.rendezvous"]?.server; + const msc4108Supported = !!versions?.unstable_features?.["org.matrix.msc4108"]; const deviceAuthorizationGrantSupported = oidcClientConfig?.metadata?.grant_types_supported.includes(DEVICE_CODE_SCOPE); @@ -51,15 +42,9 @@ export function shouldShowQr( ); } -const LoginWithQRSection: React.FC = ({ - onShowQr, - versions, - wellKnown, - oidcClientConfig, - isCrossSigningReady, -}) => { +const LoginWithQRSection: React.FC = ({ onShowQr, versions, oidcClientConfig, isCrossSigningReady }) => { const cli = useMatrixClientContext(); - const offerShowQr = shouldShowQr(cli, !!isCrossSigningReady, oidcClientConfig, versions, wellKnown); + const offerShowQr = shouldShowQr(cli, !!isCrossSigningReady, oidcClientConfig, versions); return ( diff --git a/src/components/views/settings/shared/SettingsSubsection.tsx b/src/components/views/settings/shared/SettingsSubsection.tsx index 3248a5eb906..ba8d9aba6d4 100644 --- a/src/components/views/settings/shared/SettingsSubsection.tsx +++ b/src/components/views/settings/shared/SettingsSubsection.tsx @@ -45,7 +45,7 @@ export const SettingsSubsection: React.FC = ({ mx_SettingsSubsection_newUi: !legacy, })} > - {typeof heading === "string" ? : <>{heading}} + {typeof heading === "string" ? : <>{heading}} {!!description && (
{description} diff --git a/src/components/views/settings/shared/SettingsSubsectionHeading.tsx b/src/components/views/settings/shared/SettingsSubsectionHeading.tsx index 25439d6a62e..8c51eac0c27 100644 --- a/src/components/views/settings/shared/SettingsSubsectionHeading.tsx +++ b/src/components/views/settings/shared/SettingsSubsectionHeading.tsx @@ -12,21 +12,13 @@ import Heading from "../../typography/Heading"; export interface SettingsSubsectionHeadingProps extends HTMLAttributes { heading: string; - legacy?: boolean; children?: React.ReactNode; } -export const SettingsSubsectionHeading: React.FC = ({ - heading, - legacy = true, - children, - ...rest -}) => { - const size = legacy ? "4" : "3"; - +export const SettingsSubsectionHeading: React.FC = ({ heading, children, ...rest }) => { return (
- + {heading} {children} diff --git a/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx b/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx index d29d82853aa..0da257607ee 100644 --- a/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx @@ -28,7 +28,7 @@ interface IProps { export default class BridgeSettingsTab extends React.Component { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private renderBridgeCard(event: MatrixEvent, room: Room | null): ReactNode { const content = event.getContent(); diff --git a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.tsx index 048fe5df9d9..31c361de1bd 100644 --- a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.tsx @@ -33,7 +33,7 @@ interface IState { export default class GeneralRoomSettingsTab extends React.Component { public static contextType = MatrixClientContext; - public declare context: ContextType; + declare public context: ContextType; public constructor(props: IProps, context: ContextType) { super(props, context); diff --git a/src/components/views/settings/tabs/room/NotificationSettingsTab.tsx b/src/components/views/settings/tabs/room/NotificationSettingsTab.tsx index f668b1ff076..9aabf1edb0d 100644 --- a/src/components/views/settings/tabs/room/NotificationSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/NotificationSettingsTab.tsx @@ -42,7 +42,7 @@ export default class NotificationsSettingsTab extends React.Component(); public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public constructor(props: IProps, context: React.ContextType) { super(props, context); diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx index 8261bfd3eb4..baf4b412539 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx @@ -19,7 +19,6 @@ import ErrorDialog from "../../../dialogs/ErrorDialog"; import PowerSelector from "../../../elements/PowerSelector"; import SettingsFieldset from "../../SettingsFieldset"; import SettingsStore from "../../../../../settings/SettingsStore"; -import { VoiceBroadcastInfoEventType } from "../../../../../voice-broadcast"; import { ElementCall } from "../../../../../models/Call"; import SdkConfig, { DEFAULTS } from "../../../../../SdkConfig"; import { AddPrivilegedUsers } from "../../AddPrivilegedUsers"; @@ -62,7 +61,6 @@ const plEventsToShow: Record = { // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) "im.vector.modular.widgets": { isState: true, hideForSpace: true }, - [VoiceBroadcastInfoEventType]: { isState: true, hideForSpace: true }, }; // parse a string as an integer; if the input is undefined, or cannot be parsed @@ -81,7 +79,7 @@ interface IBannedUserProps { export class BannedUser extends React.Component { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private onUnbanClick = (): void => { this.context.unban(this.props.member.roomId, this.props.member.userId).catch((err) => { @@ -134,7 +132,7 @@ interface RolesRoomSettingsTabState { export default class RolesRoomSettingsTab extends React.Component { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public constructor(props: IProps) { super(props); @@ -289,7 +287,6 @@ export default class RolesRoomSettingsTab extends React.Component { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public constructor(props: IProps, context: React.ContextType) { super(props, context); diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx index 87c44b23f26..f20a9feed0f 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx @@ -32,7 +32,7 @@ interface IState { export default class HelpUserSettingsTab extends React.Component { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public constructor(props: IProps, context: React.ContextType) { super(props, context); diff --git a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx index 9ad7df31e98..3e86d779ff3 100644 --- a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx @@ -268,7 +268,6 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState> onChange={this.onPersonalRuleChanged} /> value={this.state.newList} onChange={this.onNewListChanged} /> - + {_t("action|subscribe")} diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index 2e16f45762b..5e9445bb995 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { lazy, Suspense, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; +import React, { lazy, Suspense, useCallback, useContext, useEffect, useRef, useState } from "react"; import { discoverAndValidateOIDCIssuerWellKnown, MatrixClient } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { defer } from "matrix-js-sdk/src/utils"; @@ -184,7 +184,6 @@ const SessionManagerTab: React.FC<{ const userId = matrixClient?.getUserId(); const currentUserMember = (userId && matrixClient?.getUser(userId)) || undefined; const clientVersions = useAsyncMemo(() => matrixClient.getVersions(), [matrixClient]); - const wellKnown = useMemo(() => matrixClient?.getClientWellKnown(), [matrixClient]); const oidcClientConfig = useAsyncMemo(async () => { try { const authIssuer = await matrixClient?.getAuthIssuer(); @@ -305,7 +304,6 @@ const SessionManagerTab: React.FC<{ diff --git a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx index 36d336faa32..9711159a10e 100644 --- a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx @@ -51,7 +51,7 @@ const mapDeviceKindToHandlerValue = (deviceKind: MediaDeviceKindEnum): string | export default class VoiceUserSettingsTab extends React.Component<{}, IState> { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public constructor(props: {}, context: React.ContextType) { super(props, context); diff --git a/src/components/views/spaces/SpaceBasicSettings.tsx b/src/components/views/spaces/SpaceBasicSettings.tsx index 8311e6728ec..63a70a97cd4 100644 --- a/src/components/views/spaces/SpaceBasicSettings.tsx +++ b/src/components/views/spaces/SpaceBasicSettings.tsx @@ -71,7 +71,6 @@ export const SpaceAvatar: React.FC avatarUploadRef.current?.click()} - alt="" /> avatarUploadRef.current?.click()} diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index af484445b42..73bb66af380 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -221,7 +221,7 @@ const CreateSpaceButton: React.FC { - const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(); + const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(); useEffect(() => { if (!isPanelCollapsed && menuDisplayed) { diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index cee4cf54ecd..38329c39b73 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -30,7 +30,7 @@ import defaultDispatcher from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton"; import { toRightOf, useContextMenu } from "../../structures/ContextMenu"; -import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; +import AccessibleButton, { ButtonEvent, ButtonProps as AccessibleButtonProps } from "../elements/AccessibleButton"; import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; import { NotificationLevel } from "../../../stores/notifications/NotificationLevel"; import { getKeyBindingsManager } from "../../../KeyBindingsManager"; @@ -39,8 +39,8 @@ import SpaceContextMenu from "../context_menus/SpaceContextMenu"; import { useRovingTabIndex } from "../../../accessibility/RovingTabIndex"; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; -type ButtonProps = Omit< - ComponentProps>, +type ButtonProps = Omit< + AccessibleButtonProps, "title" | "onClick" | "size" | "element" > & { space?: Room; @@ -52,12 +52,12 @@ type ButtonProps = Omit< notificationState?: NotificationState; isNarrow?: boolean; size: string; - innerRef?: RefObject; + innerRef?: RefObject; ContextMenuComponent?: ComponentType>; onClick?(ev?: ButtonEvent): void; }; -export const SpaceButton = ({ +export const SpaceButton = ({ space, spaceKey: _spaceKey, className, @@ -72,8 +72,8 @@ export const SpaceButton = ({ ContextMenuComponent, ...props }: ButtonProps): JSX.Element => { - const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(innerRef); - const [onFocus, isActive, ref] = useRovingTabIndex(handle); + const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(innerRef); + const [onFocus, isActive, ref] = useRovingTabIndex(handle); const tabIndex = isActive ? 0 : -1; const spaceKey = _spaceKey ?? space?.roomId; diff --git a/src/components/views/toasts/VerificationRequestToast.tsx b/src/components/views/toasts/VerificationRequestToast.tsx index 54932a12edd..7d31aa67640 100644 --- a/src/components/views/toasts/VerificationRequestToast.tsx +++ b/src/components/views/toasts/VerificationRequestToast.tsx @@ -117,7 +117,7 @@ export default class VerificationRequestToast extends React.PureComponent this.state = { pending: false, }; - - // As this component is also used before login (during complete security), - // also make sure we have a working emoji font to display the SAS emojis here. - // This is also done from LoggedInView. - fixupColorFonts(); } private onMatchClick = (): void => { diff --git a/src/components/views/voip/LegacyCallView/LegacyCallViewButtons.tsx b/src/components/views/voip/LegacyCallView/LegacyCallViewButtons.tsx index bdcd3713cb5..105736d04e2 100644 --- a/src/components/views/voip/LegacyCallView/LegacyCallViewButtons.tsx +++ b/src/components/views/voip/LegacyCallView/LegacyCallViewButtons.tsx @@ -69,7 +69,7 @@ interface IDropdownButtonProps extends ButtonProps { } const LegacyCallViewDropdownButton: React.FC = ({ state, deviceKinds, ...props }) => { - const [menuDisplayed, buttonRef, openMenu, closeMenu] = useContextMenu(); + const [menuDisplayed, buttonRef, openMenu, closeMenu] = useContextMenu(); const [hoveringDropdown, setHoveringDropdown] = useState(false); const classes = classNames("mx_LegacyCallViewButtons_button", "mx_LegacyCallViewButtons_dropdownButton", { diff --git a/src/contexts/RoomContext.ts b/src/contexts/RoomContext.ts index acd4bfa6f41..4303c46a34e 100644 --- a/src/contexts/RoomContext.ts +++ b/src/contexts/RoomContext.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { createContext, useContext } from "react"; +import { createContext } from "react"; import { IRoomState } from "../components/structures/RoomView"; import { Layout } from "../settings/enums/Layout"; @@ -75,9 +75,7 @@ const RoomContext = createContext< canAskToJoin: false, promptAskToJoin: false, viewRoomOpts: { buttons: [] }, + isRoomEncrypted: null, }); RoomContext.displayName = "RoomContext"; export default RoomContext; -export function useRoomContext(): IRoomState { - return useContext(RoomContext); -} diff --git a/src/contexts/SDKContext.ts b/src/contexts/SDKContext.ts index 28e7e3aadb1..fe736615545 100644 --- a/src/contexts/SDKContext.ts +++ b/src/contexts/SDKContext.ts @@ -25,11 +25,6 @@ import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore"; import { WidgetPermissionStore } from "../stores/widgets/WidgetPermissionStore"; import { OidcClientStore } from "../stores/oidc/OidcClientStore"; import WidgetStore from "../stores/WidgetStore"; -import { - VoiceBroadcastPlaybacksStore, - VoiceBroadcastPreRecordingStore, - VoiceBroadcastRecordingsStore, -} from "../voice-broadcast"; // This context is available to components under MatrixChat, // the context must not be used by components outside a SdkContextClass tree. @@ -68,9 +63,6 @@ export class SdkContextClass { protected _SpaceStore?: SpaceStoreClass; protected _LegacyCallHandler?: LegacyCallHandler; protected _TypingStore?: TypingStore; - protected _VoiceBroadcastRecordingsStore?: VoiceBroadcastRecordingsStore; - protected _VoiceBroadcastPreRecordingStore?: VoiceBroadcastPreRecordingStore; - protected _VoiceBroadcastPlaybacksStore?: VoiceBroadcastPlaybacksStore; protected _AccountPasswordStore?: AccountPasswordStore; protected _UserProfilesStore?: UserProfilesStore; protected _OidcClientStore?: OidcClientStore; @@ -157,27 +149,6 @@ export class SdkContextClass { return this._TypingStore; } - public get voiceBroadcastRecordingsStore(): VoiceBroadcastRecordingsStore { - if (!this._VoiceBroadcastRecordingsStore) { - this._VoiceBroadcastRecordingsStore = new VoiceBroadcastRecordingsStore(); - } - return this._VoiceBroadcastRecordingsStore; - } - - public get voiceBroadcastPreRecordingStore(): VoiceBroadcastPreRecordingStore { - if (!this._VoiceBroadcastPreRecordingStore) { - this._VoiceBroadcastPreRecordingStore = new VoiceBroadcastPreRecordingStore(); - } - return this._VoiceBroadcastPreRecordingStore; - } - - public get voiceBroadcastPlaybacksStore(): VoiceBroadcastPlaybacksStore { - if (!this._VoiceBroadcastPlaybacksStore) { - this._VoiceBroadcastPlaybacksStore = new VoiceBroadcastPlaybacksStore(this.voiceBroadcastRecordingsStore); - } - return this._VoiceBroadcastPlaybacksStore; - } - public get accountPasswordStore(): AccountPasswordStore { if (!this._AccountPasswordStore) { this._AccountPasswordStore = new AccountPasswordStore(); diff --git a/src/contexts/ScopedRoomContext.tsx b/src/contexts/ScopedRoomContext.tsx new file mode 100644 index 00000000000..1222443d290 --- /dev/null +++ b/src/contexts/ScopedRoomContext.tsx @@ -0,0 +1,78 @@ +/* +Copyright 2024 New Vector Ltd. +Copyright 2019 The Matrix.org Foundation C.I.C. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import { TypedEventEmitter } from "matrix-js-sdk/src/matrix"; +import React, { ContextType, createContext, memo, ReactNode, useContext, useEffect, useRef, useState } from "react"; + +import { objectKeyChanges } from "../utils/objects.ts"; +import { useTypedEventEmitter } from "../hooks/useEventEmitter.ts"; +import RoomContext from "./RoomContext.ts"; + +// React Contexts with frequently changing values (like State where the object reference is changed on every update) +// cause performance issues by triggering a re-render on every component subscribed to that context. +// With ScopedRoomContext we're effectively setting up virtual contexts which are a subset of the overall context object +// and subscribers specify which fields they care about, and they will only be awoken on updates to those specific fields. + +type ContextValue = ContextType; + +export enum NotificationStateEvents { + Update = "update", +} + +type EventHandlerMap> = { + [NotificationStateEvents.Update]: (keys: Array) => void; +}; + +class EfficientContext> extends TypedEventEmitter< + NotificationStateEvents, + EventHandlerMap +> { + public constructor(public state: C) { + super(); + } + + public setState(state: C): void { + const changedKeys = objectKeyChanges(this.state ?? ({} as C), state); + this.state = state; + this.emit(NotificationStateEvents.Update, changedKeys); + } +} + +const ScopedRoomContext = createContext | undefined>(undefined); + +// Uses react memo and leverages splatting the value to ensure that the context is only updated when the state changes (shallow compare) +export const ScopedRoomContextProvider = memo( + ({ children, ...state }: { children: ReactNode } & ContextValue): JSX.Element => { + const contextRef = useRef(new EfficientContext(state)); + useEffect(() => { + contextRef.current.setState(state); + }, [state]); + + // Includes the legacy RoomContext provider for backwards compatibility with class components + return ( + + {children} + + ); + }, +); + +type ScopedRoomContext> = { [key in K[number]]: ContextValue[key] }; + +export function useScopedRoomContext>(...keys: K): ScopedRoomContext { + const context = useContext(ScopedRoomContext); + const [state, setState] = useState>(context?.state ?? ({} as ScopedRoomContext)); + + useTypedEventEmitter(context, NotificationStateEvents.Update, (updatedKeys: K): void => { + if (context?.state && updatedKeys.some((updatedKey) => keys.includes(updatedKey))) { + setState(context.state); + } + }); + + return state; +} diff --git a/src/events/EventTileFactory.tsx b/src/events/EventTileFactory.tsx index ff771b04676..ec1651872af 100644 --- a/src/events/EventTileFactory.tsx +++ b/src/events/EventTileFactory.tsx @@ -41,13 +41,7 @@ import { getMessageModerationState, MessageModerationState } from "../utils/Even import HiddenBody from "../components/views/messages/HiddenBody"; import ViewSourceEvent from "../components/views/messages/ViewSourceEvent"; import { shouldDisplayAsBeaconTile } from "../utils/beacon/timeline"; -import { shouldDisplayAsVoiceBroadcastTile } from "../voice-broadcast/utils/shouldDisplayAsVoiceBroadcastTile"; import { ElementCall } from "../models/Call"; -import { - isRelatedToVoiceBroadcast, - shouldDisplayAsVoiceBroadcastStoppedText, - VoiceBroadcastChunkEventType, -} from "../voice-broadcast"; // Subset of EventTile's IProps plus some mixins export interface EventTileTypeProps @@ -223,12 +217,6 @@ export function pickFactory( return MessageEventFactory; } - if (shouldDisplayAsVoiceBroadcastTile(mxEvent)) { - return MessageEventFactory; - } else if (shouldDisplayAsVoiceBroadcastStoppedText(mxEvent)) { - return TextualEventFactory; - } - if (SINGULAR_STATE_EVENTS.has(evType) && mxEvent.getStateKey() !== "") { return noEventFactoryFactory(); // improper event type to render } @@ -249,16 +237,6 @@ export function pickFactory( return noEventFactoryFactory(); } - if (mxEvent.getContent()[VoiceBroadcastChunkEventType]) { - // hide voice broadcast chunks - return noEventFactoryFactory(); - } - - if (!showHiddenEvents && mxEvent.isDecryptionFailure() && isRelatedToVoiceBroadcast(mxEvent, cli)) { - // hide utd events related to a broadcast - return noEventFactoryFactory(); - } - return EVENT_TILE_TYPES.get(evType) ?? noEventFactoryFactory(); } diff --git a/src/events/forward/getForwardableEvent.ts b/src/events/forward/getForwardableEvent.ts index 000a50f4eeb..2d37ebf6e97 100644 --- a/src/events/forward/getForwardableEvent.ts +++ b/src/events/forward/getForwardableEvent.ts @@ -9,7 +9,6 @@ Please see LICENSE files in the repository root for full details. import { M_POLL_END, M_POLL_START, M_BEACON_INFO, MatrixEvent, MatrixClient } from "matrix-js-sdk/src/matrix"; import { getShareableLocationEventForBeacon } from "../../utils/beacon/getShareableLocation"; -import { VoiceBroadcastInfoEventType } from "../../voice-broadcast/types"; /** * Get forwardable event for a given event @@ -20,8 +19,6 @@ export const getForwardableEvent = (event: MatrixEvent, cli: MatrixClient): Matr return null; } - if (event.getType() === VoiceBroadcastInfoEventType) return null; - // Live location beacons should forward their latest location as a static pin location // If the beacon is not live, or doesn't have a location forwarding is not allowed if (M_BEACON_INFO.matches(event.getType())) { diff --git a/src/hooks/room/useRoomMemberProfile.ts b/src/hooks/room/useRoomMemberProfile.ts index 57f72a722eb..b8bb44c50de 100644 --- a/src/hooks/room/useRoomMemberProfile.ts +++ b/src/hooks/room/useRoomMemberProfile.ts @@ -7,10 +7,11 @@ Please see LICENSE files in the repository root for full details. */ import { RoomMember } from "matrix-js-sdk/src/matrix"; -import { useContext, useMemo } from "react"; +import { useMemo } from "react"; -import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; +import { TimelineRenderingType } from "../../contexts/RoomContext"; import { useSettingValue } from "../useSettings"; +import { useScopedRoomContext } from "../../contexts/ScopedRoomContext.tsx"; export function useRoomMemberProfile({ userId = "", @@ -21,7 +22,7 @@ export function useRoomMemberProfile({ member?: RoomMember | null; forceHistorical?: boolean; }): RoomMember | undefined | null { - const context = useContext(RoomContext); + const context = useScopedRoomContext("room", "timelineRenderingType"); const useOnlyCurrentProfiles = useSettingValue("useOnlyCurrentProfiles"); const member = useMemo(() => { diff --git a/src/hooks/useAudioDeviceSelection.ts b/src/hooks/useAudioDeviceSelection.ts deleted file mode 100644 index 504eb10ea69..00000000000 --- a/src/hooks/useAudioDeviceSelection.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { useRef, useState } from "react"; - -import { _t } from "../languageHandler"; -import MediaDeviceHandler, { MediaDeviceKindEnum } from "../MediaDeviceHandler"; -import { requestMediaPermissions } from "../utils/media/requestMediaPermissions"; - -interface State { - devices: MediaDeviceInfo[]; - device: MediaDeviceInfo | null; -} - -export const useAudioDeviceSelection = ( - onDeviceChanged?: (device: MediaDeviceInfo) => void, -): { - currentDevice: MediaDeviceInfo | null; - currentDeviceLabel: string; - devices: MediaDeviceInfo[]; - setDevice(device: MediaDeviceInfo): void; -} => { - const shouldRequestPermissionsRef = useRef(true); - const [state, setState] = useState({ - devices: [], - device: null, - }); - - if (shouldRequestPermissionsRef.current) { - shouldRequestPermissionsRef.current = false; - requestMediaPermissions(false).then((stream: MediaStream | undefined) => { - MediaDeviceHandler.getDevices().then((devices) => { - if (!devices) return; - const { audioinput } = devices; - MediaDeviceHandler.getDefaultDevice(audioinput); - const deviceFromSettings = MediaDeviceHandler.getAudioInput(); - const device = - audioinput.find((d) => { - return d.deviceId === deviceFromSettings; - }) || audioinput[0]; - setState({ - ...state, - devices: audioinput, - device, - }); - stream?.getTracks().forEach((t) => t.stop()); - }); - }); - } - - const setDevice = (device: MediaDeviceInfo): void => { - const shouldNotify = device.deviceId !== state.device?.deviceId; - MediaDeviceHandler.instance.setDevice(device.deviceId, MediaDeviceKindEnum.AudioInput); - - setState({ - ...state, - device, - }); - - if (shouldNotify) { - onDeviceChanged?.(device); - } - }; - - return { - currentDevice: state.device, - currentDeviceLabel: state.device?.label || _t("voip|default_device"), - devices: state.devices, - setDevice, - }; -}; diff --git a/src/hooks/useEncryptionStatus.ts b/src/hooks/useEncryptionStatus.ts index 686f68f25e2..ed8cceb9f81 100644 --- a/src/hooks/useEncryptionStatus.ts +++ b/src/hooks/useEncryptionStatus.ts @@ -6,9 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { CryptoEvent, MatrixClient, Room, RoomStateEvent } from "matrix-js-sdk/src/matrix"; +import { MatrixClient, Room, RoomStateEvent } from "matrix-js-sdk/src/matrix"; import { useEffect, useMemo, useState } from "react"; import { throttle } from "lodash"; +import { CryptoEvent } from "matrix-js-sdk/src/crypto-api"; import { E2EStatus, shieldStatusForRoom } from "../utils/ShieldUtils"; import { useTypedEventEmitter } from "./useEventEmitter"; diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index 7647377196b..df8a243c730 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -882,7 +882,6 @@ }, "udd": { "interactive_verification_button": "Interaktivní ověření pomocí emoji", - "manual_verification_button": "Ruční ověření pomocí textu", "other_ask_verify_text": "Požádejte tohoto uživatele, aby ověřil svou relaci, nebo jí níže můžete ověřit manuálně.", "other_new_session_text": "%(name)s (%(userId)s) se přihlásil(a) do nové relace bez ověření:", "own_ask_verify_text": "Ověřte další relaci jedním z následujících způsobů.", @@ -917,12 +916,6 @@ "incoming_sas_dialog_waiting": "Čekání na potvrzení partnerem…", "incoming_sas_user_dialog_text_1": "Po ověření bude uživatel označen jako důvěryhodný. Ověřování uživatelů vám dává větší jistotu, že je komunikace důvěrná.", "incoming_sas_user_dialog_text_2": "Ověření uživatele označí jeho relace za důvěryhodné a vaše relace budou důvěryhodné pro něj.", - "manual_device_verification_device_id_label": "ID sezení", - "manual_device_verification_device_key_label": "Klíč relace", - "manual_device_verification_device_name_label": "Název relace", - "manual_device_verification_footer": "Pokud se neshodují, bezpečnost vaší komunikace může být kompromitována.", - "manual_device_verification_self_text": "Potvrďte porovnáním následujícího s uživatelským nastavením v jiné relaci:", - "manual_device_verification_user_text": "Potvrďte relaci tohoto uživatele porovnáním následujícího s jeho uživatelským nastavením:", "no_key_or_device": "Vypadá to, že nemáte bezpečnostní klíč ani žádné jiné zařízení, které byste mohli ověřit. Toto zařízení nebude mít přístup ke starým šifrovaným zprávám. Abyste mohli na tomto zařízení ověřit svou totožnost, budete muset resetovat ověřovací klíče.", "no_support_qr_emoji": "Zařízení, které se snažíte ověřit, neumožňuje ověření QR kódem ani pomocí emotikonů, které %(brand)s podporuje. Zkuste použít jiného klienta.", "other_party_cancelled": "Druhá strana ověření zrušila.", @@ -1046,10 +1039,6 @@ }, "error_user_not_logged_in": "Uživatel není přihlášen", "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s ukončil(a) hlasové vysílání", - "you": "Ukončili jste hlasové vysílání" - }, "m.call.answer": { "dm": "Probíhá hovor", "user": "%(senderName)s se připojil k hovoru", @@ -1423,8 +1412,6 @@ "video_rooms_faq2_answer": "Ano, časová osa chatu se zobrazuje vedle videa.", "video_rooms_faq2_question": "Mohu vedle videohovoru používat i textový chat?", "video_rooms_feedbackSubheading": "Děkujeme, že jste vyzkoušeli beta verzi, a prosíme vás o co nejpodrobnější informace, abychom ji mohli vylepšit.", - "voice_broadcast": "Hlasové vysílání", - "voice_broadcast_force_small_chunks": "Vynutit 15s délku bloku hlasového vysílání", "wysiwyg_composer": "Editor formátovaného textu" }, "labs_mjolnir": { @@ -1568,7 +1555,6 @@ "mute_description": "Nebudete dostávat žádná oznámení" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s zahájil(a) hlasové vysílání", "m.key.verification.request": "%(name)s žádá o ověření" }, "onboarding": { @@ -2145,7 +2131,6 @@ "error_unbanning": "Zrušení vykázání se nezdařilo", "events_default": "Posílat zprávy", "invite": "Zvát uživatele", - "io.element.voice_broadcast_info": "Hlasová vysílání", "kick": "Odebrat uživatele", "m.call": "Zahájit %(brand)s volání", "m.call.member": "Připojit se k %(brand)s volání", @@ -2788,7 +2773,6 @@ "warning": "UPOZORNĚNÍ: " }, "share": { - "link_title": "Odkaz na místnost", "permalink_message": "Odkaz na vybranou zprávu", "permalink_most_recent": "Odkaz na nejnovější zprávu", "share_call": "Odkaz na pozvánku na konferenci", @@ -2880,13 +2864,6 @@ "upgraderoom": "Aktualizuje místnost na novou verzi", "upgraderoom_permission_error": "Na provedení tohoto příkazu nemáte dostatečná oprávnění.", "usage": "Použití", - "verify": "Ověří uživatele, relaci a veřejné klíče", - "verify_mismatch": "VAROVÁNÍ: OVĚŘENÍ KLÍČE SE NEZDAŘILO! Podpisový klíč pro uživatele %(userId)s a relaci %(deviceId)s je „%(fprint)s“, což neodpovídá klíči „%(fingerprint)s“. To by mohlo znamenat, že vaše komunikace je zachycována!", - "verify_nop": "Relace je už ověřená!", - "verify_nop_warning_mismatch": "VAROVÁNÍ: relace již byla ověřena, ale klíče se NESHODUJÍ!", - "verify_success_description": "Zadaný podpisový klíč odpovídá klíči relace %(deviceId)s od uživatele %(userId)s. Relace byla označena jako ověřená.", - "verify_success_title": "Ověřený klíč", - "verify_unknown_pair": "Neznámý pár (uživatel, relace): (%(userId)s, %(deviceId)s)", "view": "Zobrazí místnost s danou adresou", "whois": "Zobrazuje informace o uživateli" }, @@ -3104,10 +3081,6 @@ "error_rendering_message": "Tuto zprávu nelze načíst", "historical_messages_unavailable": "Dřívější zprávy nelze zobrazit", "in_room_name": " v %(room)s", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s ukončil(a) hlasové vysílání", - "you": "Ukončili jste hlasové vysílání" - }, "io.element.widgets.layout": "%(senderName)s aktualizoval rozvržení místnosti", "late_event_separator": "Původně odesláno %(dateTime)s", "load_error": { @@ -3640,38 +3613,6 @@ "switch_theme_dark": "Přepnout do tmavého režimu", "switch_theme_light": "Přepnout do světlého režimu" }, - "voice_broadcast": { - "30s_backward": "30s zpět", - "30s_forward": "30s vpřed", - "action": "Hlasové vysílání", - "buffering": "Ukládání do vyrovnávací paměti…", - "confirm_listen_affirm": "Ano, ukončit nahrávání", - "confirm_listen_description": "Jakmile začnete poslouchat toto živé vysílání, aktuální záznam živého vysílání bude ukončen.", - "confirm_listen_title": "Poslouchat živé vysílání?", - "confirm_stop_affirm": "Ano, zastavit vysílání", - "confirm_stop_description": "Opravdu chcete ukončit živé vysílání? Tím se vysílání ukončí a v místnosti bude k dispozici celý záznam.", - "confirm_stop_title": "Ukončit živé vysílání?", - "connection_error": "Chyba připojení - nahrávání pozastaveno", - "failed_already_recording_description": "Již nahráváte hlasové vysílání. Ukončete prosím aktuální hlasové vysílání a spusťte nové.", - "failed_already_recording_title": "Nelze spustit nové hlasové vysílání", - "failed_decrypt": "Nelze dešifrovat hlasové vysílání", - "failed_generic": "Nelze přehrát toto hlasové vysílání", - "failed_insufficient_permission_description": "Nemáte potřebná oprávnění ke spuštění hlasového vysílání v této místnosti. Obraťte se na správce místnosti, aby vám zvýšil oprávnění.", - "failed_insufficient_permission_title": "Nelze spustit nové hlasové vysílání", - "failed_no_connection_description": "Bohužel nyní nemůžeme spustit nahrávání. Zkuste to prosím později.", - "failed_no_connection_title": "Chyba připojení", - "failed_others_already_recording_description": "Hlasové vysílání už nahrává někdo jiný. Počkejte, až jeho hlasové vysílání skončí, a spusťte nové.", - "failed_others_already_recording_title": "Nelze spustit nové hlasové vysílání", - "go_live": "Přejít naživo", - "live": "Živě", - "pause": "pozastavit hlasové vysílání", - "play": "přehrát hlasové vysílání", - "resume": "obnovit hlasové vysílání" - }, - "voice_message": { - "cant_start_broadcast_description": "Hlasovou zprávu nelze spustit, protože právě nahráváte živé vysílání. Ukončete prosím živé vysílání, abyste mohli začít nahrávat hlasovou zprávu.", - "cant_start_broadcast_title": "Nelze spustit hlasovou zprávu" - }, "voip": { "already_in_call": "Již máte hovor", "already_in_call_person": "S touto osobou již telefonujete.", @@ -3691,7 +3632,6 @@ "camera_disabled": "Vaše kamera je vypnutá", "camera_enabled": "Vaše kamera je stále zapnutá", "cannot_call_yourself_description": "Nemůžete volat sami sobě.", - "change_input_device": "Změnit vstupní zařízení", "close_lobby": "Zavřít lobby", "connecting": "Spojování", "connection_lost": "Došlo ke ztrátě připojení k serveru", @@ -3710,8 +3650,6 @@ "enable_camera": "Zapnout kameru", "enable_microphone": "Zrušit ztlumení mikrofonu", "expand": "Návrat do hovoru", - "failed_call_live_broadcast_description": "Nemůžete zahájit hovor, protože právě nahráváte živé vysílání. Ukončete prosím živé vysílání, abyste mohli zahájit hovor.", - "failed_call_live_broadcast_title": "Nelze zahájit hovor", "get_call_link": "Sdílet odkaz na hovor", "hangup": "Zavěsit", "hide_sidebar_button": "Skrýt postranní panel", diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index abe4566f8c9..5a4a6043fbc 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -2,6 +2,7 @@ "a11y": { "emoji_picker": "Emoji-Auswahl", "jump_first_invite": "Zur ersten Einladung springen.", + "message_composer": "Nachrichteneingabe-Feld", "n_unread_messages": { "other": "%(count)s ungelesene Nachrichten.", "one": "1 ungelesene Nachricht." @@ -10,11 +11,14 @@ "other": "%(count)s ungelesene Nachrichten einschließlich Erwähnungen.", "one": "1 ungelesene Erwähnung." }, + "recent_rooms": "Kürzlich besuchte Chatrooms", "room_name": "Raum %(name)s", + "room_status_bar": "Statusleiste des Chatrooms", + "seek_bar_label": "Audio-Suchleiste", "unread_messages": "Ungelesene Nachrichten.", "user_menu": "Benutzermenü" }, - "a11y_jump_first_unread_room": "Zum ersten ungelesenen Raum springen.", + "a11y_jump_first_unread_room": "Springen Sie zum ersten ungelesenen Chatroom.", "action": { "accept": "Annehmen", "add": "Hinzufügen", @@ -106,6 +110,7 @@ "save": "Speichern", "search": "Suchen", "send_report": "Bericht senden", + "set_avatar": "Profilbild festlegen", "share": "Teilen", "show": "Zeigen", "show_advanced": "Erweiterte Einstellungen", @@ -123,12 +128,13 @@ "trust": "Vertrauen", "try_again": "Erneut versuchen", "unban": "Verbannung aufheben", - "unignore": "Nicht mehr blockieren", + "unignore": "Freigeben", "unpin": "Nicht mehr anheften", "unsubscribe": "Deabonnieren", "update": "Aktualisieren", "upgrade": "Aktualisieren", "upload": "Hochladen", + "upload_file": "Datei hochladen", "verify": "Verifizieren", "view": "Ansicht", "view_all": "Alles anzeigen", @@ -143,7 +149,7 @@ "accept_button": "Das ist okay", "bullet_1": "Wir erfassen und analysieren keine Kontodaten", "bullet_2": "Wir teilen keine Informationen mit Dritten", - "consent_migration": "Sie haben zuvor zugestimmt, anonymisierte Nutzungsdaten mit uns zu teilen. Wir aktualisieren, wie das funktioniert.", + "consent_migration": "Du hast zugestimmt, anonymisierte Nutzungsdaten mit uns zu teilen. Wir aktualisieren die Funktionsweise.", "disable_prompt": "Du kannst dies jederzeit in den Einstellungen deaktivieren", "enable_prompt": "Hilf mit, %(analyticsOwner)s zu verbessern", "learn_more": "Teile Daten anonymisiert um uns zu helfen Probleme zu identifizieren. Nichts persönliches. Keine Dritten. Mehr dazu hier", @@ -204,11 +210,11 @@ "failed_soft_logout_auth": "Erneute Authentifizierung fehlgeschlagen", "failed_soft_logout_homeserver": "Erneute Authentifizierung aufgrund eines Problems des Heim-Servers fehlgeschlagen", "forgot_password_email_invalid": "E-Mail-Adresse scheint ungültig zu sein.", - "forgot_password_email_required": "Es muss die mit dem Benutzerkonto verbundene E-Mail-Adresse eingegeben werden.", + "forgot_password_email_required": "Es muss die mit dem Konto verbundene E-Mail-Adresse eingegeben werden.", "forgot_password_prompt": "Passwort vergessen?", "forgot_password_send_email": "E-Mail senden", "identifier_label": "Anmelden mit", - "incorrect_credentials": "Inkorrekter Nutzername und/oder Passwort.", + "incorrect_credentials": "Benutzername und/oder Passwort falsch.", "incorrect_credentials_detail": "Bitte beachte, dass du dich gerade auf %(hs)s anmeldest, nicht matrix.org.", "incorrect_password": "Ungültiges Passwort", "log_in_new_account": "Mit deinem neuen Konto anmelden.", @@ -223,6 +229,7 @@ }, "misconfigured_body": "Wende dich an deinen %(brand)s-Admin um deine Konfiguration auf ungültige oder doppelte Einträge zu überprüfen.", "misconfigured_title": "Dein %(brand)s ist falsch konfiguriert", + "mobile_create_account_title": "Du bist dabei, auf %(hsName)s ein Konto anzulegen", "msisdn_field_description": "Andere Personen können dich mit deinen Kontaktdaten in Räume einladen", "msisdn_field_label": "Telefon", "msisdn_field_number_invalid": "Diese Telefonummer sieht nicht ganz richtig aus. Bitte überprüfe deine Eingabe und versuche es erneut", @@ -241,12 +248,40 @@ "phone_label": "Telefon", "phone_optional_label": "Telefon (optional)", "qr_code_login": { + "check_code_explainer": "Hierdurch wird überprüft, ob die Verbindung zu Ihrem anderen Gerät sicher ist.", + "check_code_heading": "Gib die Nummer ein, die am anderen Gerät angezeigt wird", + "check_code_input_label": "zweistelliger Code", + "check_code_mismatch": "Die Zahlen stimmen nicht überein", "completing_setup": "Schließe Anmeldung deines neuen Gerätes ab", + "error_etag_missing": "Ein unerwarteter Fehler ist aufgetreten. Dies kann an einer Browsererweiterung, einem Proxyserver oder einer fehlerhaften Serverkonfiguration liegen.", + "error_expired": "Die Anmeldung ist abgelaufen. Bitte versuchen Sie es erneut.", + "error_expired_title": "Die Anmeldung wurde nicht rechtzeitig abgeschlossen", + "error_insecure_channel_detected": "Eine sichere Verbindung zum neuen Gerät konnte nicht hergestellt werden. Ihre vorhandenen Geräte sind weiterhin sicher und Sie müssen sich keine Sorgen um sie machen.", + "error_insecure_channel_detected_instructions": "Was jetzt?", + "error_insecure_channel_detected_instructions_1": "Versuchen Sie erneut, sich mit einem QR-Code auf dem anderen Gerät anzumelden, falls dies ein Netzwerkproblem war", + "error_insecure_channel_detected_instructions_2": "Falls das gleiche Problem auftritt, probiere Sie es bitte mit einem anderen WLAN-Netzwerk oder verwenden Sie Ihre mobilen Daten anstelle von WLAN", + "error_insecure_channel_detected_instructions_3": "Falls das nicht funktioniert, melden Sie sich bitte manuell an.", + "error_insecure_channel_detected_title": "Die Verbindung ist nicht sicher", + "error_other_device_already_signed_in": "Sie brauchen nichts weiter zu tun.", + "error_other_device_already_signed_in_title": "Ihr anderes Gerät ist bereits angemeldet", "error_rate_limited": "Zu viele Versuche in zu kurzer Zeit. Warte ein wenig, bevor du es erneut versuchst.", - "error_unexpected": "Ein unerwarteter Fehler ist aufgetreten.", - "scan_code_instruction": "Lese den folgenden QR-Code mit deinem nicht angemeldeten Gerät ein.", - "scan_qr_code": "QR-Code einlesen", - "select_qr_code": "Wähle „%(scanQRCode)s“", + "error_unexpected": "Es ist ein unerwarteter Fehler aufgetreten. Ihre Anfrage zum Verbinden des anderen Geräts wurde abgebrochen.", + "error_unsupported_protocol": "Dieses Gerät unterstützt die Anmeldung auf einem anderen Gerät mit einem QR-Code nicht.", + "error_unsupported_protocol_title": "Anderes Gerät nicht kompatibel", + "error_user_cancelled": "Der Anmeldevorgang wurde am anderen Gerät abgebrochen.", + "error_user_cancelled_title": "Anmeldungsanfrage abgebrochen", + "error_user_declined": "Sie oder der Kontoanbieter haben die Anmeldeanfrage abgelehnt.", + "error_user_declined_title": "Anmeldung abgelehnt", + "follow_remaining_instructions": "Folgen Sie den restlichen Anweisungen.", + "open_element_other_device": "Öffnen Sie %(brand)s auf Ihren anderen Gerät", + "point_the_camera": "Scanne den hier angezeigten QR-Code", + "scan_code_instruction": "Scanne den QR-Code mit einem weiteren Gerät.", + "scan_qr_code": "Anmeldung mit QR-Code", + "security_code": "Sicherheitscode", + "security_code_prompt": "Wenn Sie dazu aufgefordert werden, geben Sie den folgenden Code auf Ihrem anderen Gerät ein.", + "select_qr_code": "Wähle \"%(scanQRCode)s\"", + "unsupported_explainer": "Dein Kontoanbieter unterstützt keine Anmeldung bei einem neuen Gerät per QR-Code.", + "unsupported_heading": "QR-Code nicht unterstützt", "waiting_for_device": "Warte auf Anmeldung des Gerätes" }, "register_action": "Konto erstellen", @@ -258,8 +293,8 @@ "registration_disabled": "Registrierungen wurden auf diesem Heim-Server deaktiviert.", "registration_msisdn_field_required_invalid": "Telefonnummer eingeben (auf diesem Heim-Server erforderlich)", "registration_successful": "Registrierung erfolgreich", - "registration_username_in_use": "Jemand anderes nutzt diesen Benutzernamen schon. Probier einen anderen oder wenn du es bist, melde dich unten an.", - "registration_username_unable_check": "Es kann nicht überprüft werden, ob der Nutzername bereits vergeben ist. Bitte versuche es später erneut.", + "registration_username_in_use": "Dieser Benutzername wird bereits verwendet. Wählen Sie einen anderen Benutzernamen oder melden Sie sich unten an.", + "registration_username_unable_check": "Es kann nicht überprüft werden, ob der Benutzername bereits vergeben ist. Bitte versuchen Sie es später erneut.", "registration_username_validation": "Verwende nur Kleinbuchstaben, Zahlen, Bindestriche und Unterstriche", "reset_password": { "confirm_new_password": "Neues Passwort bestätigen", @@ -335,6 +370,8 @@ "email_resend_prompt": "Nicht angekommen? Erneut senden", "email_resent": "Verschickt!", "fallback_button": "Authentifizierung beginnen", + "mas_cross_signing_reset_cta": "Gehen Sie zu Ihren Konto", + "mas_cross_signing_reset_description": "Setzen Sie Ihre Identität über Ihren Kontoanbieter zurück. Kommen Sie dann zurück und klicken Sie auf „Wiederholen“.", "msisdn": "Eine Textnachricht wurde an %(msisdn)s gesendet", "msisdn_token_incorrect": "Token fehlerhaft", "msisdn_token_prompt": "Bitte gib den darin enthaltenen Code ein:", @@ -355,7 +392,7 @@ "unsupported_auth_email": "Dieser Heim-Server unterstützt die Anmeldung per E-Mail-Adresse nicht.", "unsupported_auth_msisdn": "Dieser Server unterstützt keine Authentifizierung per Telefonnummer.", "username_field_required_invalid": "Benutzername eingeben", - "username_in_use": "Dieser Benutzername wird bereits genutzt, bitte versuche es mit einem anderen.", + "username_in_use": "Dieser Benutzername wird bereits verwendet. Bitte wählen Sie einen anderen Benutzernamen.", "verify_email_explainer": "Wir müssen wissen, dass du es auch wirklich bist, bevor wir dein Passwort zurücksetzen. Klicke auf den Link in der E-Mail, die wir gerade an %(email)s gesendet haben", "verify_email_heading": "Verifiziere deine E-Mail, um fortzufahren" }, @@ -365,12 +402,12 @@ "collecting_information": "App-Versionsinformationen werden abgerufen", "collecting_logs": "Protokolle werden abgerufen", "create_new_issue": "Bitte erstelle ein neues Issue auf GitHub damit wir diesen Fehler untersuchen können.", - "description": "Fehlerberichte enthalten Nutzungsdaten wie Nutzernamen von dir und anderen Personen, Raum-IDs deiner beigetretenen Räume sowie mit welchen Elementen der Oberfläche du kürzlich interagiert hast. Sie enthalten keine Nachrichten.", + "description": "Fehlerberichte enthalten Applikationsnutzungsdaten wie Ihren Benutzernamen and Ihre Pseudonyme oder jene Ihrer Chatpartner, die IDs und Namen von Chaträumen, in denen Sie Mitglied sind und Elementen der Benutzeroberfläche mit denen Sie kürzlich interagiert haben. Fehlerberichte enthalten keine Nachrichten.", "download_logs": "Protokolle herunterladen", "downloading_logs": "Lade Protokolle herunter", "error_empty": "Bitte teile uns mit, was schief lief - oder besser, beschreibe das Problem auf GitHub in einem \"Issue\".", "failed_send_logs": "Senden von Protokolldateien fehlgeschlagen: ", - "github_issue": "\"Issue\" auf Github", + "github_issue": "GitHub-Problem", "introduction": "Wenn du uns einen Bug auf GitHub gemeldet hast, können uns Debug-Logs helfen, das Problem zu finden. ", "log_request": "Um uns zu helfen, dies in Zukunft zu vermeiden, sende uns bitte die Protokolldateien.", "logs_sent": "Protokolldateien gesendet", @@ -425,6 +462,7 @@ "beta": "Beta", "camera": "Kamera", "cameras": "Kameras", + "cancel": "Abbrechen", "capabilities": "Funktionen", "copied": "Kopiert!", "credits": "Danksagungen", @@ -460,11 +498,13 @@ "legal": "Rechtliches", "light": "Hell", "loading": "Lade …", + "lobby": "Lobby", "location": "Standort", "low_priority": "Niedrige Priorität", "matrix": "Matrix", "message": "Nachricht", "message_layout": "Nachrichtenlayout", + "message_timestamp_invalid": "Ungültiger Zeitstempel", "microphone": "Mikrofon", "model": "Modell", "modern": "Modern", @@ -488,7 +528,7 @@ "orphan_rooms": "Andere Räume", "password": "Passwort", "people": "Personen", - "preferences": "Einstellungen", + "preferences": "Präferenzen", "presence": "Anwesenheit", "preview_message": "Hey du. Du bist großartig!", "privacy": "Privatsphäre", @@ -506,6 +546,8 @@ "room": "Raum", "room_name": "Raumname", "rooms": "Räume", + "save": "Speichern", + "saved": "Gespeichert", "saving": "Speichere …", "secure_backup": "Verschlüsselte Sicherung", "security": "Sicherheit", @@ -516,7 +558,7 @@ "show_more": "Mehr zeigen", "someone": "Jemand", "space": "Raum", - "spaces": "Räume", + "spaces": "Spaces", "sticker": "Sticker", "stickerpack": "Sticker-Paket", "success": "Erfolg", @@ -534,6 +576,7 @@ "unnamed_room": "Unbenannter Raum", "unnamed_space": "Unbenannter Space", "unverified": "Nicht verifiziert", + "updating": "Aktualisieren...", "user": "Benutzer", "user_avatar": "Profilbild", "username": "Benutzername", @@ -661,7 +704,7 @@ "private_personal_heading": "Für wen ist dieser Space gedacht?", "private_space": "Für mich und meine Kollegen", "private_space_description": "Ein privater Space für dich und deine Kollegen", - "public_description": "Öffne den Space für alle - am besten für Communities", + "public_description": "Öffne den Space für alle - am besten für Communitys", "public_heading": "Dein öffentlicher Space", "search_public_button": "Öffentliche Spaces suchen", "setup_rooms_community_description": "Lass uns für jedes einen Raum erstellen.", @@ -687,6 +730,7 @@ "twemoji": "Die Twemoji-Emojis sind © Twitter, Inc und weitere Mitwirkende und wird unter den Bedingungen von CC-BY 4.0 verwendet.", "twemoji_colr": "Die Schriftart twemoji-colr ist © Mozilla Foundation und wird unter den Bedingungen von Apache 2.0 verwendet." }, + "desktop_default_device_name": "%(brand)s Desktop: %(platformName)s", "devtools": { "active_widgets": "Aktive Widgets", "category_other": "Sonstiges", @@ -732,6 +776,7 @@ "room_notifications_type": "Typ: ", "room_status": "Raumstatus", "room_unread_status_count": { + "one": "Ungelesen Status des ChatRooms: %(status)s, Anzahl: %(count)s", "other": "Ungelesen-Status im Raum: %(status)s, Anzahl: %(count)s" }, "save_setting_values": "Einstellungswerte speichern", @@ -761,7 +806,7 @@ "toolbox": "Werkzeugkasten", "use_at_own_risk": "Diese Benutzeroberfläche prüft nicht auf richtige Datentypen. Benutzung auf eigene Gefahr.", "user_read_up_to": "Der Benutzer hat gelesen bis: ", - "user_read_up_to_ignore_synthetic": "Der Benutzer hat gelesen bis (ignoreSynthetic): ", + "user_read_up_to_ignore_synthetic": "Der Benutzer hat bis (ignoreSynthetic) gelesen: ", "user_read_up_to_private": "Benutzer las bis (m.read.private): ", "user_read_up_to_private_ignore_synthetic": "Benutzer las bis (m.read.private;ignoreSynthetic): ", "value": "Wert", @@ -860,6 +905,8 @@ "warning": "Wenn du die neue Wiederherstellungsmethode nicht festgelegt hast, versucht ein Angreifer möglicherweise, auf dein Konto zuzugreifen. Ändere dein Kontopasswort und lege sofort eine neue Wiederherstellungsmethode in den Einstellungen fest." }, "not_supported": "", + "pinned_identity_changed": "Die Identität von %(displayName)s (%(userId)s)) hat sich geändert. Mehr erfahren", + "pinned_identity_changed_no_displayname": "Die Identität von %(userId)s hat sich geändert. Mehr erfahren", "recovery_method_removed": { "description_1": "In dieser Sitzung wurde festgestellt, dass deine Sicherheitsphrase und dein Schlüssel für sichere Nachrichten entfernt wurden.", "description_2": "Wenn du dies versehentlich getan hast, kannst du in dieser Sitzung \"sichere Nachrichten\" einrichten, die den Nachrichtenverlauf dieser Sitzung mit einer neuen Wiederherstellungsmethode erneut verschlüsseln.", @@ -875,7 +922,6 @@ }, "udd": { "interactive_verification_button": "Interaktiv per Emoji verifizieren", - "manual_verification_button": "Manuell per Text verifizieren", "other_ask_verify_text": "Bitte diesen Nutzer, seine Sitzung zu verifizieren, oder verifiziere diese unten manuell.", "other_new_session_text": "%(name)s (%(userId)s) hat sich zu einer neuen Sitzung angemeldet, ohne sie zu verifizieren:", "own_ask_verify_text": "Verifiziere deine andere Sitzung mit einer der folgenden Optionen.", @@ -910,12 +956,6 @@ "incoming_sas_dialog_waiting": "Warte auf Bestätigung des Gesprächspartners …", "incoming_sas_user_dialog_text_1": "Überprüfe diesen Benutzer, um ihn als vertrauenswürdig zu kennzeichnen. Benutzern zu vertrauen gibt dir zusätzliche Sicherheit bei der Verwendung von Ende-zu-Ende-verschlüsselten Nachrichten.", "incoming_sas_user_dialog_text_2": "Wenn du diesen Benutzer verifizierst werden seine Sitzungen für dich und deine Sitzungen für ihn als vertrauenswürdig markiert.", - "manual_device_verification_device_id_label": "Sitzungs-ID", - "manual_device_verification_device_key_label": "Sitzungsschlüssel", - "manual_device_verification_device_name_label": "Sitzungsname", - "manual_device_verification_footer": "Wenn sie nicht übereinstimmen kann die Sicherheit eurer Kommunikation kompromittiert sein.", - "manual_device_verification_self_text": "Bestätige indem du das folgende mit deinen Benutzereinstellungen in deiner anderen Sitzung vergleichst:", - "manual_device_verification_user_text": "Bestätige die Sitzung dieses Benutzers indem du das folgende mit seinen Benutzereinstellungen vergleichst:", "no_key_or_device": "Es sieht so aus, als hättest du keinen Sicherheitsschlüssel oder andere Geräte, mit denen du dich verifizieren könntest. Dieses Gerät wird keine alten verschlüsselten Nachrichten lesen können. Um deine Identität auf diesem Gerät zu verifizieren musst du deine Verifizierungsschlüssel zurücksetzen.", "no_support_qr_emoji": "Das Gerät unterstützt weder Verifizieren mittels QR-Code noch Emoji-Verifizierung. %(brand)s benötigt dies jedoch. Bitte verwende eine andere Anwendung.", "other_party_cancelled": "Die Gegenstelle hat die Überprüfung abgebrochen.", @@ -929,7 +969,8 @@ "qr_reciprocate_same_shield_device": "Fast geschafft! Zeigen beide Geräte das selbe Wappen an?", "qr_reciprocate_same_shield_user": "Fast geschafft! Wird bei %(displayName)s das gleiche Schild angezeigt?", "request_toast_accept": "Sitzung verifizieren", - "request_toast_decline_counter": "Ignorieren (%(counter)s)", + "request_toast_accept_user": "Benutzer verifizieren", + "request_toast_decline_counter": "Blockiert (%(counter)s)", "request_toast_detail": "%(deviceId)s von %(ip)s", "reset_proceed_prompt": "Mit Zurücksetzen fortfahren", "sas_caption_self": "Verifiziere dieses Gerät, indem du überprüfst, dass die folgende Zahl auf dem Bildschirm erscheint.", @@ -954,7 +995,7 @@ "unverified_sessions_toast_description": "Überprüfe sie, um ein sicheres Konto gewährleisten zu können", "unverified_sessions_toast_reject": "Später", "unverified_sessions_toast_title": "Du hast nicht verifizierte Sitzungen", - "verification_description": "Verifiziere diese Anmeldung, um auf verschlüsselte Nachrichten zuzugreifen und dich anderen gegenüber zu identifizieren.", + "verification_description": "Verifizieren Sie Ihre Identität, um auf verschlüsselte Nachrichten zuzugreifen und sich gegenüber anderen Benutzern zu verifizieren. Falls Sie einen Handy oder ein anderes mobiles Gerät verwenden, öffnen Sie die App dort and fahren sie mit Ihrer Identifikation fort.", "verification_dialog_title_device": "Anderes Gerät verifizieren", "verification_dialog_title_user": "Verifizierungsanfrage", "verification_skip_warning": "Ohne dich zu verifizieren wirst du keinen Zugriff auf alle deine Nachrichten haben und könntest für andere als nicht vertrauenswürdig erscheinen.", @@ -1020,6 +1061,10 @@ "error_app_open_in_another_tab": "Wechsle zu einem anderen Tab um mit %(brand)s zu verbinden. Dieser Tab kann jetzt geschlossen werden.", "error_app_open_in_another_tab_title": "%(brand)s läuft bereits in einem anderen Tab.", "error_app_opened_in_another_window": "%(brand)s läuft bereit in einem anderen Fenster. Klicke \"%(label)s um %(brand)s hier zu nutzen und beende das andere Fenster.", + "error_database_closed_description": { + "for_desktop": "Deine Festplatte scheint voll zu sein. Mache Speicherplatz frei und lade erneut.", + "for_web": "Diese Meldung wird erwartet, wenn Sie die Browserdaten gelöscht haben. %(brand)s ist möglicherweise auch in einem anderen Tab geöffnet oder Ihre Festplatte ist voll. Bitte machen Sie etwas Speicherplatz frei und laden Sie die Seite neu." + }, "error_database_closed_title": "%(brand)s funktioniert nicht mehr", "error_dialog": { "copy_room_link_failed": { @@ -1035,10 +1080,6 @@ }, "error_user_not_logged_in": "Benutzer ist nicht angemeldet", "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s beendete eine Sprachübertragung", - "you": "Du hast eine Sprachübertragung beendet" - }, "m.call.answer": { "dm": "Laufendes Gespräch", "user": "%(senderName)s ist dem Anruf beigetreten", @@ -1060,7 +1101,15 @@ "you": "Du reagiertest mit %(reaction)s auf %(message)s" }, "m.sticker": "%(senderName)s: %(stickerName)s", - "m.text": "%(senderName)s: %(message)s" + "m.text": "%(senderName)s: %(message)s", + "prefix": { + "audio": "Audio", + "file": "Datei", + "image": "Bild", + "poll": "Umfrage", + "video": "Video" + }, + "preview": "%(prefix)s%(preview)s" }, "export_chat": { "cancelled": "Exportieren abgebrochen", @@ -1183,7 +1232,19 @@ "other": "In %(spaceName)s und %(count)s weiteren Spaces." }, "incompatible_browser": { - "title": "Nicht unterstützter Browser" + "continue": "Trotzdem fortfahren", + "description": "%(brand)s verwendet einige Browser-Funktionen, die von deinem aktuellen Browser nicht unterstützt werden. %(detail)s", + "detail_can_continue": "Wenn Sie fortfahren, funktionieren einige Funktionen möglicherweise nicht mehr und es besteht das Risiko, dass Sie in Zukunft Daten verlieren.", + "detail_no_continue": "Versuchen Sie, diesen Browser zu aktualisieren, wenn Sie nicht die neueste Version verwenden, und versuchen Sie es erneut.", + "learn_more": "Mehr erfahren", + "linux": "Linux", + "macos": "Mac", + "supported_browsers": "Verwenden Sie Chrome, Firefox, Edge oder Safari, um das beste Erlebnis zu erzielen.", + "title": "Nicht unterstützter Browser", + "use_desktop_heading": "Verwende stattdessen %(brand)s Desktop", + "use_mobile_heading": "Stattdessen %(brand)s am Smartphone benutzen", + "use_mobile_heading_after_desktop": "Oder verwende die mobile App", + "windows": "Windows (%(bits)s-bit)" }, "info_tooltip_title": "Information", "integration_manager": { @@ -1198,7 +1259,7 @@ "integrations": { "disabled_dialog_description": "Aktiviere „%(manageIntegrations)s“ in den Einstellungen, um dies zu tun.", "disabled_dialog_title": "Integrationen sind deaktiviert", - "impossible_dialog_description": "%(brand)s erlaubt dir nicht, eine Integrationsverwaltung zu verwenden, um dies zu tun. Bitte kontaktiere einen Administrator.", + "impossible_dialog_description": "%(brand)s erlaubt es Ihnen nicht, eine Integrationsverwaltung zu verwenden. Bitte kontaktieren Sie einen Administrator.", "impossible_dialog_title": "Integrationen sind nicht erlaubt" }, "invite": { @@ -1307,12 +1368,14 @@ "navigate_next_message_edit": "Nächste Nachricht bearbeiten", "navigate_prev_history": "Vorheriger kürzlich besuchter Raum oder Space", "navigate_prev_message_edit": "Vorherige Nachricht bearbeiten", + "next_landmark": "Zur nächsten Landmark springen", "next_room": "Nächste Unterhaltung", "next_unread_room": "Nächste ungelesene Nachricht", "number": "[Nummer]", "open_user_settings": "Benutzereinstellungen öffnen", "page_down": "Bild runter", "page_up": "Bild hoch", + "prev_landmark": "Zur vorherigen Landmark springen", "prev_room": "Vorherige Unterhaltung", "prev_unread_room": "Vorherige ungelesene Nachricht", "room_list_collapse_section": "Raumliste einklappen", @@ -1357,8 +1420,11 @@ "dynamic_room_predecessors": "Veränderbare Raumvorgänger", "dynamic_room_predecessors_description": "MSC3946 aktivieren (zur Verknüpfung von Raumarchiven nach der Raumerstellung)", "element_call_video_rooms": "Element Call-Videoräume", + "exclude_insecure_devices": "Unsichere Geräte ausschließen beim senden/empfangen von Nachrichten", + "exclude_insecure_devices_description": "Bei Aktivierung dieses Modus werden verschlüsselte Nachrichten nicht mehr mit unverifizierten Geräten geteilt und Nachrichten von unverifizierten Geräten werden als Fehler angezeigt. Beachten Sie, dass bei Aktivierung dieses Modus es nicht möglich ist, mit Benutzern, die keine verifizierten Geräte haben, zu kommunizieren.", "experimental_description": "Experimentierfreudig? Probiere unsere neuesten, sich in Entwicklung befindlichen Ideen aus. Diese Funktionen sind nicht final; Sie könnten instabil sein, sich verändern oder sogar ganz entfernt werden. Erfahre mehr.", "experimental_section": "Frühe Vorschauen", + "extended_profiles_msc_support": "Erfordert die Unterstützung von MSC4133 durch den Server", "feature_disable_call_per_sender_encryption": "Verschlüsselung per-sender für Element Anruf abschalten", "feature_wysiwyg_composer_description": "Verwende Textverarbeitung (Rich-Text) statt Markdown im Eingabefeld.", "group_calls": "Neue Gruppenanruf-Erfahrung", @@ -1369,9 +1435,10 @@ "group_moderation": "Moderation", "group_profile": "Profil", "group_rooms": "Räume", - "group_spaces": "Räume", + "group_spaces": "Spaces", "group_themes": "Themen", "group_threads": "Themen", + "group_ui": "Benutzeroberfläche", "group_voip": "Anrufe", "group_widgets": "Widgets", "hidebold": "Benachrichtigungspunkt ausblenden (nur Zähler zeigen)", @@ -1393,6 +1460,7 @@ "notifications": "Benachrichtigungen in der Kopfleiste des Raums anschalten", "oidc_native_flow": "Native OIDC Authentifizierung", "oidc_native_flow_description": "⚠ Warnung: Experimentell. Nutze OIDC native Authentifizierung wenn dies vom Server unterstützt wird.", + "release_announcement": "Release Ankündigung", "render_reaction_images": "Benutzerdefinierte Bilder in Reaktionen anzeigen", "render_reaction_images_description": "Werden manchmal auch als „benutzerdefinierte Emojis“ bezeichnet.", "report_to_moderators": "An Raummoderation melden", @@ -1400,7 +1468,7 @@ "sliding_sync": "Sliding-Sync-Modus", "sliding_sync_description": "In aktiver Entwicklung, kann nicht deaktiviert werden.", "sliding_sync_disabled_notice": "Zum Deaktivieren, melde dich ab und erneut an", - "sliding_sync_server_no_support": "Dein Server unterstützt dies nicht nativ", + "sliding_sync_server_no_support": "Dein Server wird hier nicht unterstützt.", "under_active_development": "In aktiver Entwicklung.", "unrealiable_e2e": "Nicht zuverlässig in verschlüsselten Räumen", "video_rooms": "Videoräume", @@ -1412,8 +1480,6 @@ "video_rooms_faq2_answer": "Ja, der Verlauf wird neben dem Videoanruf angezeigt.", "video_rooms_faq2_question": "Kann ich während Videoanrufen auch Textnachrichten verschicken?", "video_rooms_feedbackSubheading": "Danke, dass Du die Beta ausprobierst. Bitte gehe soweit wie Du kannst ins Detail damit wird genau überprüfen können.", - "voice_broadcast": "Sprachübertragung", - "voice_broadcast_force_small_chunks": "Die Chunk-Länge der Sprachübertragungen auf 15 Sekunden erzwingen", "wysiwyg_composer": "Textverarbeitungs-Editor" }, "labs_mjolnir": { @@ -1454,6 +1520,8 @@ "last_person_warning": "Du bist die einzige Person im Raum. Sobald du ihn verlässt, wird niemand mehr hineingelangen, auch du nicht.", "leave_room_question": "Bist du sicher, dass du den Raum „%(roomName)s“ verlassen möchtest?", "leave_space_question": "Bist du sicher, dass du den Space „%(spaceName)s“ verlassen möchtest?", + "room_leave_admin_warning": "Sie sind der einzige Administrator in diesem Chatroom. Wenn Sie den Chatroom verlassen, wird niemand in der Lage sein, die Chatroommeinstellungen zu ändern oder andere wichtige Maßnahmen zu ergreifen.", + "room_leave_mod_warning": "Sie sind der einzige Moderator in diesem Chatroom. Wenn Sie den Chatroom verlassen, kann niemand die Raumeinstellungen ändern oder andere wichtige Maßnahmen ergreifen.", "room_rejoin_warning": "Dieser Raum ist nicht öffentlich. Du wirst ihn nicht ohne erneute Einladung betreten können.", "space_rejoin_warning": "Du wirst diesen privaten Space nur mit einer Einladung wieder betreten können." }, @@ -1518,6 +1586,7 @@ }, "member_list_back_action_label": "Raummitglieder", "message_edit_dialog_title": "Nachrichtenänderungen", + "migrating_crypto": "Bleib dran. Wir aktualisieren%(brand)s, um die Verschlüsselung schneller und zuverlässiger zu machen.", "mobile_guide": { "toast_accept": "App verwenden", "toast_description": "%(brand)s ist in mobilen Browsern experimentell. Für eine bessere Erfahrung nutze unsere App.", @@ -1543,8 +1612,10 @@ "keyword": "Schlüsselwort", "keyword_new": "Neues Schlüsselwort", "level_activity": "Aktivität", + "level_highlight": "Hervorhebung", "level_muted": "Stumm", "level_none": "Nichts", + "level_notification": "Benachrichtigung", "level_unsent": "Nicht gesendet", "mark_all_read": "Alle als gelesen markieren", "mentions_and_keywords": "@Erwähnungen und Schlüsselwörter", @@ -1554,7 +1625,6 @@ "mute_description": "Du wirst keine Benachrichtigungen erhalten" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s begann eine Sprachübertragung", "m.key.verification.request": "%(name)s fordert eine Verifizierung an" }, "onboarding": { @@ -1572,8 +1642,8 @@ "download_brand_desktop": "%(brand)s Desktop herunterladen", "download_f_droid": "In F-Droid erhältlich", "download_google_play": "In Google Play erhältlich", - "enable_notifications": "Benachrichtigungen einschalten", - "enable_notifications_action": "Benachrichtigungen aktivieren", + "enable_notifications": "Desktopbenachrichtigungen einschalten", + "enable_notifications_action": "Einstellungen öffnen", "enable_notifications_description": "Verpasse keine Antworten oder wichtigen Nachrichten", "explore_rooms": "Öffentliche Räume erkunden", "find_community_members": "Finde deine Community-Mitglieder und lade sie ein", @@ -1711,7 +1781,7 @@ "disagree": "Ablehnen", "error_create_room_moderation_bot": "Erstellen des Raums mit Moderations-Bot nicht möglich", "hide_messages_from_user": "Prüfe, ob du alle aktuellen und zukünftigen Nachrichten dieses Nutzers verstecken willst.", - "ignore_user": "Nutzer ignorieren", + "ignore_user": "Benutzer blockieren", "illegal_content": "Illegale Inhalte", "missing_reason": "Bitte gib an, weshalb du einen Fehler meldest.", "nature": "Bitte wähle eine Kategorie aus und beschreibe, was die Nachricht missbräuchlich macht.", @@ -1752,14 +1822,37 @@ "restore_failed_error": "Konnte Schlüsselsicherung nicht wiederherstellen" }, "right_panel": { - "add_integrations": "Widgets, Brücken und Bots hinzufügen", + "add_integrations": "Erweiterungen hinzufügen", + "add_topic": "Thema hinzufügen", + "extensions_button": "Erweiterungen", + "extensions_empty_description": "Wählen Sie \"%(addIntegrations)s\" um Erweiterungen zu suchen und diesem Chatroom hinzuzufügen", + "extensions_empty_title": "Steigern Sie die Produktivität mit mehr Tools, Widgets und Bots", "files_button": "Dateien", "pinned_messages": { + "empty_description": "Wählen Sie eine Nachricht aus und wählen Sie „%(pinAction)s “, um sie hier einzuschließen.", + "empty_title": "Hefte wichtige Nachrichten an, damit sie leicht gefunden werden können.", + "header": { + "one": "1 angeheftete Nachricht", + "other": "%(count)s angeheftete Nachrichten" + }, "limits": { "other": "Du kannst nur %(count)s Widgets anheften" - } + }, + "menu": "Menü öffnen", + "release_announcement": { + "close": "Ok", + "description": "Alle angepinnten Nachrichten finden Sie hier. Bewegen Sie den Mauszeiger über eine beliebige Nachricht und wählen Sie „Pin“, um die Nachricht hinzuzufügen.", + "title": "Alle neuen angehefteten Nachrichten" + }, + "reply_thread": "Auf Nachricht im Thread antworten", + "unpin_all": { + "button": "Alle Nachrichten lösen", + "content": "Stellen Sie sicher, dass Sie wirklich alle angehefteten Nachrichten entfernen möchten. Diese Aktion kann nicht rückgängig gemacht werden.", + "title": "Alle Nachrichten lösen?" + }, + "view": "Im Nachrichtenverlauf ansehen" }, - "pinned_messages_button": "Angeheftet", + "pinned_messages_button": "Angeheftete Nachrichten", "poll": { "active_heading": "Aktive Umfragen", "empty_active": "In diesem Raum gibt es keine aktiven Umfragen", @@ -1780,11 +1873,11 @@ }, "load_more": "Weitere Umfragen laden", "loading": "Lade Umfragen", - "past_heading": "Vergangene Umfragen", + "past_heading": "Abgeschlossene Umfragen", "view_in_timeline": "Umfrage im Verlauf anzeigen", "view_poll": "Umfrage ansehen" }, - "polls_button": "Umfrageverlauf", + "polls_button": "Umfragen", "room_summary_card": { "title": "Raum-Info" }, @@ -1813,6 +1906,7 @@ "forget": "Raum vergessen", "low_priority": "Niedrige Priorität", "mark_read": "Als gelesen markieren", + "mark_unread": "Als ungelesen markieren", "notifications_default": "Standardeinstellung verwenden", "notifications_mute": "Raum stumm stellen", "title": "Raumoptionen", @@ -1861,6 +1955,8 @@ }, "room_is_public": "Dieser Raum ist öffentlich" }, + "header_avatar_open_settings_label": "Chatroomeinstellungen öffnen", + "header_face_pile_tooltip": "Personen", "header_untrusted_label": "Nicht vertrauenswürdig", "inaccessible": "Dieser Raum oder Space ist im Moment nicht zugänglich.", "inaccessible_name": "Auf %(roomName)s kann momentan nicht zugegriffen werden.", @@ -1884,10 +1980,10 @@ "you_created": "Du hast diesen Raum erstellt." }, "invite_email_mismatch_suggestion": "Teile diese E-Mail-Adresse in den Einstellungen, um Einladungen direkt in %(brand)s zu erhalten.", - "invite_reject_ignore": "Ablehnen und Nutzer blockieren", + "invite_reject_ignore": "Ablehnen und Benutzer blockieren", "invite_sent_to_email": "Einladung an %(email)s gesendet", "invite_sent_to_email_room": "Diese Einladung zu %(roomName)s wurde an %(email)s gesendet", - "invite_subtitle": " hat dich eingeladen", + "invite_subtitle": "Eingeladen von ", "invite_this_room": "In diesen Raum einladen", "invite_title": "Möchtest du %(roomName)s betreten?", "inviter_unknown": "Unbekannt", @@ -1930,11 +2026,24 @@ "not_found_title": "Dieser Raum oder Space existiert nicht.", "not_found_title_name": "%(roomName)s existiert nicht.", "peek_join_prompt": "Du erkundest den Raum %(roomName)s. Willst du ihn betreten?", + "pinned_message_badge": "Fixierte Nachrichten", + "pinned_message_banner": { + "button_close_list": "Liste schließen", + "button_view_all": "Alle anzeigen", + "description": "In diesem Raum sind Nachrichten angeheftet. Klicken Sie hier, um sie anzusehen.", + "go_to_message": "Fixierte Nachrichten im Nachrichtenverlauf anzeigen.", + "title": "%(index)s of %(length)s Angeheftete Nachrichten" + }, "read_topic": "Klicke, um das Thema zu lesen", "rejecting": "Lehne Einladung ab …", "rejoin_button": "Erneut betreten", "search": { "all_rooms_button": "Alle Räume durchsuchen", + "placeholder": "Nachrichten durchsuchen...", + "summary": { + "one": "1 Ergebnis für \"\" gefunden", + "other": "%(count)s Ergebnisse für \"\" gefunden" + }, "this_room_button": "Diesen Raum durchsuchen" }, "status_bar": { @@ -2070,6 +2179,8 @@ "error_deleting_alias_description": "Beim Entfernen dieser Adresse ist ein Fehler aufgetreten. Vielleicht existiert sie nicht mehr oder es kam zu einem temporären Fehler.", "error_deleting_alias_description_forbidden": "Du hast nicht die Berechtigung, die Adresse zu löschen.", "error_deleting_alias_title": "Fehler beim Löschen der Adresse", + "error_publishing": "Chatroom konnte nicht auf öffentlich gestellt werden", + "error_publishing_detail": "Beim Veröffentlichen dieses Chatrooms ist ein Fehler aufgetreten", "error_save_space_settings": "Spaceeinstellungen konnten nicht gespeichert werden.", "error_updating_alias_description": "Es gab einen Fehler beim Ändern des Raumalias. Entweder erlaubt es der Server nicht oder es gab ein temporäres Problem.", "error_updating_canonical_alias_description": "Es gab ein Problem beim Aktualisieren der Raum-Hauptadresse. Es kann sein, dass der Server dies verbietet oder ein temporäres Problem aufgetreten ist.", @@ -2127,7 +2238,6 @@ "error_unbanning": "Aufheben der Verbannung fehlgeschlagen", "events_default": "Nachrichten senden", "invite": "Person einladen", - "io.element.voice_broadcast_info": "Sprachübertragungen", "kick": "Benutzer entfernen", "m.call": "Beginne %(brand)s-Anrufe", "m.call.member": "Trete %(brand)s-Anrufen bei", @@ -2140,7 +2250,7 @@ "m.room.history_visibility": "Sichtbarkeit des Verlaufs ändern", "m.room.name": "Raumname ändern", "m.room.name_space": "Name des Space ändern", - "m.room.pinned_events": "Angeheftete Ereignisse verwalten", + "m.room.pinned_events": "Angeheftete Nachrichten verwalten", "m.room.power_levels": "Berechtigungen ändern", "m.room.redaction": "Vom mir gesendete Nachrichten löschen", "m.room.server_acl": "Server-ACLs bearbeiten", @@ -2297,25 +2407,39 @@ "brand_version": "Version von %(brand)s:", "clear_cache_reload": "Zwischenspeicher löschen und neu laden", "crypto_version": "Krypto-Version:", + "dialog_title": "Einstellungen: Hilfe & Info", "help_link": "Um Hilfe zur Benutzung von %(brand)s zu erhalten, klicke hier.", "homeserver": "Heim-Server ist %(homeserverUrl)s", "identity_server": "Identitäts-Server ist %(identityServerUrl)s", - "title": "Hilfe und Info", + "title": "Hilfe & Info", "versions": "Versionen" } }, "settings": { + "account": { + "dialog_title": "Einstellunge: Konto", + "title": "Konto" + }, "all_rooms_home": "Alle Räume auf Startseite anzeigen", "all_rooms_home_description": "Alle Räume, denen du beigetreten bist, werden auf der Startseite erscheinen.", "always_show_message_timestamps": "Nachrichtenzeitstempel immer anzeigen", "appearance": { + "bundled_emoji_font": "Verwenden Sie den mitgelieferten Emoji Font.", + "compact_layout": "Kompakten Text und Nachrichten anzeigen", + "compact_layout_description": "Um diese Funktion nutzen zu können, muss das moderne Nachrichtenlayout ausgewählt sein.", "custom_font": "Systemschriftart verwenden", "custom_font_description": "Setze den Schriftnamen auf eine in deinem System installierte Schriftart und %(brand)s wird versuchen, sie zu verwenden.", "custom_font_name": "Systemschriftart", "custom_font_size": "Andere Schriftgröße verwenden", - "custom_theme_error_downloading": "Fehler beim herunterladen des Themas.", + "custom_theme_add": "Benutzerdefiniertes Design hinzufügen", + "custom_theme_downloading": "Benutzerdefiniertes Design wird heruntergeladen...", + "custom_theme_error_downloading": "Fehler beim Herunterladen des Designs.", + "custom_theme_help": "Geben Sie die URL eines benutzerdefinierten Designs ein, das Sie anwenden möchten.", "custom_theme_invalid": "Ungültiges Designschema.", + "dialog_title": "Einstellungen: Erscheinungsbild", "font_size": "Schriftgröße", + "font_size_default": "%(fontSize)s (Standard)", + "high_contrast": "Hochkontrast", "image_size_default": "Standard", "image_size_large": "Groß", "layout_bubbles": "Nachrichtenblasen", @@ -2330,6 +2454,9 @@ "code_block_expand_default": "Quelltextblöcke standardmäßig erweitern", "code_block_line_numbers": "Zeilennummern in Quelltextblöcken", "disable_historical_profile": "Aktuelle Profilbilder und Anzeigenamen im Verlauf anzeigen", + "discovery": { + "title": "Wie man Sie findet" + }, "emoji_autocomplete": "Emoji-Vorschläge während Eingabe", "enable_markdown": "Markdown aktivieren", "enable_markdown_description": "Beginne Nachrichten mit /plain, um sie ohne Markdown zu senden.", @@ -2345,6 +2472,14 @@ "add_msisdn_dialog_title": "Telefonnummer hinzufügen", "add_msisdn_instructions": "Gib den per SMS an +%(msisdn)s gesendeten Bestätigungscode ein.", "add_msisdn_misconfigured": "Das MSISDN-Verknüpfungsverfahren ist falsch konfiguriert", + "allow_spellcheck": "Rechtschreibprüfung zulassen", + "application_language": "Anwendungssprache", + "application_language_reload_hint": "Nach Änderung der Sprache wird die App neu gestartet", + "avatar_remove_progress": "Bild wird entfernt...", + "avatar_save_progress": "Bild wird hochgeladen...", + "avatar_upload_error_text": "Das Dateiformat wird nicht unterstützt oder das Bild ist größer als %(size)s.", + "avatar_upload_error_text_generic": "Das Dateiformat wird möglicherweise nicht unterstützt.", + "avatar_upload_error_title": "Profilbild konnte nicht hochgeladen werden", "confirm_adding_email_body": "Klicke unten auf den Knopf, um die hinzugefügte E-Mail-Adresse zu bestätigen.", "confirm_adding_email_title": "Hinzugefügte E-Mail-Addresse bestätigen", "deactivate_confirm_body": "Willst du dein Konto wirklich deaktivieren? Du kannst dies nicht rückgängig machen.", @@ -2352,7 +2487,7 @@ "deactivate_confirm_content": "Bestätige, dass du dein Konto deaktivieren möchtest. Wenn du fortfährst, tritt folgendes ein:", "deactivate_confirm_content_1": "Du wirst dein Konto nicht reaktivieren können", "deactivate_confirm_content_2": "Du wirst dich nicht mehr anmelden können", - "deactivate_confirm_content_3": "Niemand wird in der Lage sein deinen Benutzernamen (MXID) wiederzuverwenden, dich eingeschlossen: Der Benutzername wird nicht verfügbar bleiben", + "deactivate_confirm_content_3": "Niemand wird in der Lage sein, Ihren Benutzernamen (MXID) wiederzuverwenden, Sie eingeschlossen: Der Benutzername wird insgesamt nur einmal vergeben.", "deactivate_confirm_content_4": "Du wirst alle Unterhaltungen verlassen, in denen du dich befindest", "deactivate_confirm_content_5": "Du wirst vom Identitäts-Server entfernt: Deine Freunde werden nicht mehr in der Lage sein, dich über deine E-Mail-Adresse oder Telefonnummer zu finden", "deactivate_confirm_content_6": "Deine alten Nachrichten werden weiterhin für Personen sichtbar bleiben, die sie erhalten haben, so wie es bei E-Mails der Fall ist. Möchtest du deine Nachrichten vor Personen verbergen, die Räume in der Zukunft betreten?", @@ -2360,10 +2495,13 @@ "deactivate_confirm_erase_label": "Meine Nachrichten vor neuen Teilnehmern verstecken", "deactivate_section": "Benutzerkonto deaktivieren", "deactivate_warning": "Die Deaktivierung deines Kontos ist unwiderruflich — sei vorsichtig!", - "discovery_email_empty": "Entdeckungsoptionen werden angezeigt, sobald du eine E-Mail-Adresse hinzugefügt hast.", + "discovery_email_empty": "Optionen zum Entdecken anderer Benutzer werden angezeigt, sobald Sie Ihre E-Mail Adresse zu Ihrem Konto hinzugefügt haben,", "discovery_email_verification_instructions": "Verifiziere den Link in deinem Posteingang", - "discovery_msisdn_empty": "Entdeckungsoptionen werden angezeigt, sobald du eine Telefonnummer hinzugefügt hast.", + "discovery_msisdn_empty": "Optionen zum Entdecken anderer Benutzer werden angezeigt, sobald Sie Ihre Telefonnummer zu Ihrem Konto hinzugefügt haben", "discovery_needs_terms": "Stimme den Nutzungsbedingungen des Identitäts-Servers %(serverName)s zu, um per E-Mail-Adresse oder Telefonnummer auffindbar zu werden.", + "discovery_needs_terms_title": "Lassen Sie sich von anderen finden", + "display_name": "Anzeigename", + "display_name_error": "Anzeigename konnte nicht gesetzt werden", "email_address_in_use": "Diese E-Mail-Adresse wird bereits verwendet", "email_address_label": "E-Mail-Adresse", "email_not_verified": "Deine E-Mail-Adresse wurde noch nicht verifiziert", @@ -2388,18 +2526,24 @@ "error_share_msisdn_discovery": "Teilen der Telefonnummer nicht möglich", "identity_server_no_token": "Kein Identitäts-Zugangs-Token gefunden", "identity_server_not_set": "Kein Identitäts-Server festgelegt", - "language_section": "Sprache und Region", + "language_section": "Sprache", "msisdn_in_use": "Diese Telefonnummer wird bereits verwendet", "msisdn_label": "Telefonnummer", "msisdn_verification_field_label": "Bestätigungscode", "msisdn_verification_instructions": "Gib den Bestätigungscode ein, den du empfangen hast.", "msisdns_heading": "Telefonnummern", "oidc_manage_button": "Konto verwalten", - "password_change_section": "Setze neues Kontopasswort …", + "password_change_section": "Passwort des Nutzerkontos ändern...", "password_change_success": "Dein Passwort wurde erfolgreich geändert.", + "personal_info": "Persönliche Daten", + "profile_subtitle": "So werden Sie von anderen in der App gesehen", + "profile_subtitle_oidc": "Ihr Konto wird separat durch einen Identitätsanbieter verwaltet. Einige persönliche Daten können deswegen hier nicht geändert werden.", "remove_email_prompt": "%(email)s entfernen?", "remove_msisdn_prompt": "%(phone)s entfernen?", - "spell_check_locale_placeholder": "Wähle ein Gebietsschema" + "spell_check_locale_placeholder": "Wähle ein Gebietsschema", + "unable_to_load_emails": "E-Mail Adresse konnte nicht geladen werden", + "unable_to_load_msisdns": "Telefonnummern können nicht geladen werden", + "username": "Benutzername" }, "image_thumbnails": "Vorschauen für Bilder", "inline_url_previews_default": "URL-Vorschau standardmäßig aktivieren", @@ -2455,12 +2599,20 @@ "phrase_strong_enough": "Super! Diese Passphrase wirkt stark genug" }, "keyboard": { + "dialog_title": "Einstellungen: Tastatur", "title": "Tastatur" }, + "labs": { + "dialog_title": "Einstellungen: Labore" + }, + "labs_mjolnir": { + "dialog_title": "Einstellungen: Blockierte Benutzer" + }, "notifications": { "default_setting_description": "Diese Einstellung wird standardmäßig für all deine Räume übernommen.", "default_setting_section": "Ich möchte benachrichtigt werden für (Standardeinstellung)", "desktop_notification_message_preview": "Nachrichtenvorschau in der Desktopbenachrichtigung anzeigen", + "dialog_title": "Einstellungen: Benachrichtigungen", "email_description": "E-Mail-Zusammenfassung für verpasste Benachrichtigungen erhalten", "email_section": "E-Mail-Zusammenfassung", "email_select": "Wähle, an welche E-Mail-Adresse die Zusammenfassungen gesendet werden. Verwalte deine E-Mail-Adressen unter .", @@ -2519,12 +2671,15 @@ "code_blocks_heading": "Quelltextblöcke", "compact_modern": "Modernes kompaktes Layout verwenden", "composer_heading": "Nachrichteneingabe", + "default_timezone": "Browser-Standard (%(timezone)s )", + "dialog_title": "Einstellungen: Präferenzen", "enable_hardware_acceleration": "Aktiviere die Hardwarebeschleunigung", "enable_tray_icon": "Fenster beim Schließen in die Symbolleiste minimieren", "keyboard_heading": "Tastenkombinationen", "keyboard_view_shortcuts_button": "Um alle Tastenkombinationen anzuzeigen, klicke hier.", "media_heading": "Mediendateien", "presence_description": "Teile anderen deine Aktivität und deinen Status mit.", + "publish_timezone": "Zeitzone auf öffentlichem Profil anzeigen lassen", "rm_lifetime": "Gültigkeitsdauer der Gelesen-Markierung (ms)", "rm_lifetime_offscreen": "Gültigkeitsdauer der Gelesen-Markierung außerhalb des Bildschirms (ms)", "room_directory_heading": "Raumverzeichnis", @@ -2533,7 +2688,8 @@ "show_checklist_shortcuts": "Verknüpfung zu ersten Schritten (Willkommen) anzeigen", "show_polls_button": "Zeige Pol button", "surround_text": "Sonderzeichen automatisch vor und hinter Textauswahl setzen", - "time_heading": "Zeitanzeige" + "time_heading": "Zeitanzeige", + "user_timezone": "Zeitzone festlegen" }, "prompt_invite": "Warnen, bevor du Einladungen zu ungültigen Matrix-IDs sendest", "replace_plain_emoji": "Klartext-Emoji automatisch ersetzen", @@ -2564,14 +2720,17 @@ "cross_signing_self_signing_private_key": "Selbst signierter privater Schlüssel:", "cross_signing_user_signing_private_key": "Privater Benutzerschlüssel:", "cryptography_section": "Verschlüsselung", + "dehydrated_device_description": "Die Offline-Gerätefunktion ermöglicht es Ihnen, verschlüsselte Nachrichten zu empfangen, auch wenn Sie an keinem Gerät angemeldet sind", + "dehydrated_device_enabled": "Offline-Gerät aktiviert", "delete_backup": "Lösche Sicherung", "delete_backup_confirm_description": "Bist du sicher? Du wirst alle deine verschlüsselten Nachrichten verlieren, wenn deine Schlüssel nicht gut gesichert sind.", + "dialog_title": "Einstellungen Sicherheit & Datenschutz", "e2ee_default_disabled_warning": "Deine Server-Administration hat die Ende-zu-Ende-Verschlüsselung für private Räume und Direktnachrichten standardmäßig deaktiviert.", "enable_message_search": "Nachrichtensuche in verschlüsselten Räumen aktivieren", "encryption_section": "Verschlüsselung", "error_loading_key_backup_status": "Konnte Status der Schlüsselsicherung nicht laden", "export_megolm_keys": "E2E-Raumschlüssel exportieren", - "ignore_users_empty": "Du ignorierst keine Benutzer.", + "ignore_users_empty": "Sie haben keinen Benutzer blockiert.", "ignore_users_section": "Blockierte Benutzer", "import_megolm_keys": "E2E-Raumschlüssel importieren", "key_backup_active": "Diese Sitzung sichert deine Schlüssel.", @@ -2643,6 +2802,7 @@ "device_unverified_description_current": "Verifiziere deine aktuelle Sitzung für besonders sichere Kommunikation.", "device_verified_description": "Diese Sitzung ist für sichere Kommunikation bereit.", "device_verified_description_current": "Deine aktuelle Sitzung ist für sichere Kommunikation bereit.", + "dialog_title": "Einstellungen: Sitzungen", "error_pusher_state": "Konfigurieren des Push-Dienstes fehlgeschlagen", "error_set_name": "Es konnte kein Sitzungsname gesetzt werden", "filter_all": "Alle", @@ -2682,9 +2842,10 @@ "security_recommendations_description": "Verbessere deine Kontosicherheit, indem du diese Empfehlungen beherzigst.", "session_id": "Sitzungs-ID", "show_details": "Details anzeigen", - "sign_in_with_qr": "Mit QR-Code anmelden", + "sign_in_with_qr": "Neues Gerät verknüpfen", "sign_in_with_qr_button": "QR-Code anzeigen", - "sign_in_with_qr_description": "Du kannst dieses Gerät verwenden, um ein neues Gerät per QR-Code anzumelden. Dazu musst du den auf diesem Gerät angezeigten QR-Code mit deinem nicht angemeldeten Gerät einlesen.", + "sign_in_with_qr_description": "Sie können dieses Gerät dazu verwenden, um ein neues Gerät per QR-Code anzumelden und verschlüsselte Nachrichtenübermittlung einzurichten.", + "sign_in_with_qr_unsupported": "Wird von Ihrem Kontoanbieter nicht unterstützt", "sign_out": "Von dieser Sitzung abmelden", "sign_out_all_other_sessions": "Von allen anderen Sitzungen abmelden (%(otherSessionsCount)s)", "sign_out_confirm_description": { @@ -2724,7 +2885,9 @@ "show_redaction_placeholder": "Platzhalter für gelöschte Nachrichten", "show_stickers_button": "Sticker-Schaltfläche", "show_typing_notifications": "Tippbenachrichtigungen anzeigen", + "showbold": "Alle Aktivitäten in der Chatroommliste anzeigen (Ausführungspunkte oder Anzahl ungelesener Nachrichten)", "sidebar": { + "dialog_title": "Einstellungen: Seitenleiste", "metaspaces_favourites_description": "Gruppiere all deine favorisierten Unterhaltungen an einem Ort.", "metaspaces_home_all_rooms": "Alle Räume anzeigen", "metaspaces_home_all_rooms_description": "Alle Räume auf der Startseite anzeigen, auch wenn sie Teil eines Space sind.", @@ -2733,10 +2896,14 @@ "metaspaces_orphans_description": "Gruppiere all deine Räume, die nicht Teil eines Spaces sind, an einem Ort.", "metaspaces_people_description": "Gruppiere all deine Direktnachrichten an einem Ort.", "metaspaces_subsection": "Anzuzeigende Spaces", - "spaces_explainer": "Räume sind Möglichkeiten, Personen zu gruppieren. Neben den Räumen, in denen du dich befindest, kannst du auch einige vorgefertigte verwenden.", + "metaspaces_video_rooms": "Videoräume und -konferenzen", + "metaspaces_video_rooms_description": "Gruppiere alle privaten Videoräume und -konferenzen.", + "metaspaces_video_rooms_description_invite_extension": "Sie können Personen außerhalb von Matrix zu Konferenzen einladen.", + "spaces_explainer": "Räume sind eine Möglichkeit, Chatrooms und Personen zu gruppieren. Abgesehen von den Räumen, in denen Sie sich gerade befinden, können Sie auch einige vorgefertigte Räume verwenden.", "title": "Seitenleiste" }, "start_automatically": "Nach Systemstart automatisch starten", + "tac_only_notifications": "Benachrichtigungen nur im Thread Aktivitätszentrum anzeigen", "use_12_hour_format": "Uhrzeiten im 12-Stundenformat (z. B. 2:30 p. m.)", "use_command_enter_send_message": "Benutze Betriebssystemtaste + Eingabe um eine Nachricht zu senden", "use_command_f_search": "Nutze Command + F um den Verlauf zu durchsuchen", @@ -2750,6 +2917,7 @@ "audio_output_empty": "Keine Audioausgabe erkannt", "auto_gain_control": "Automatische Lautstärkeregelung", "connection_section": "Verbindung", + "dialog_title": "Einstellungen: Sprache und Video", "echo_cancellation": "Echounterdrückung", "enable_fallback_ice_server": "Ersatz-Anrufassistenz-Server erlauben (%(server)s)", "enable_fallback_ice_server_description": "Dieser wird nur verwendet, sollte dein Heim-Server keinen bieten. Deine IP-Adresse würde während eines Anrufs geteilt werden.", @@ -2768,9 +2936,12 @@ "warning": "WARNUNG: " }, "share": { - "link_title": "Link zum Raum", + "link_copied": "Link kopiert", "permalink_message": "Link zur ausgewählten Nachricht", "permalink_most_recent": "Link zur aktuellsten Nachricht", + "share_call": "Konferenzeinladungslink", + "share_call_subtitle": "Link für externe Benutzer, um dem Anruf ohne Matrixkonto beizutreten:", + "title_link": "Link teilen", "title_message": "Raumnachricht teilen", "title_room": "Raum teilen", "title_user": "Teile Benutzer" @@ -2803,7 +2974,7 @@ "help_dialog_title": "Befehl Hilfe", "holdcall": "Den aktuellen Anruf halten", "html": "Sendet eine Nachricht als HTML, ohne sie als Markdown darzustellen", - "ignore": "Nutzer blockieren und dessen Nachrichten ausblenden", + "ignore": "Benutzer blockieren und dessen Nachrichten ausblenden", "ignore_dialog_description": "%(userId)s ist jetzt blockiert", "ignore_dialog_title": "Benutzer blockiert", "invite": "Lädt den Benutzer mit der angegebenen ID in den aktuellen Raum ein", @@ -2846,7 +3017,7 @@ "unban": "Entbannt den Benutzer mit der angegebenen ID", "unflip": "Stellt ┬──┬ ノ( ゜-゜ノ) einer Klartextnachricht voran", "unholdcall": "Beendet das Halten des Anrufs", - "unignore": "Benutzer nicht mehr ignorieren und neue Nachrichten wieder anzeigen", + "unignore": "Benutzer freigeben und ihre neuen Nachrichten wieder anzeigen", "unignore_dialog_description": "%(userId)s wird nicht mehr blockiert", "unignore_dialog_title": "Benutzer nicht mehr blockiert", "unknown_command": "Unbekannter Befehl", @@ -2857,13 +3028,6 @@ "upgraderoom": "Aktualisiert den Raum auf eine neue Version", "upgraderoom_permission_error": "Du hast nicht die erforderlichen Berechtigungen, diesen Befehl zu verwenden.", "usage": "Verwendung", - "verify": "Verifiziert Benutzer, Sitzung und öffentlichen Schlüsselpaare", - "verify_mismatch": "ACHTUNG: SCHLÜSSELVERIFIZIERUNG FEHLGESCHLAGEN! Der Signierschlüssel für %(userId)s und Sitzung %(deviceId)s ist \"%(fprint)s\", was nicht mit dem bereitgestellten Schlüssel \"%(fingerprint)s\" übereinstimmt. Das könnte bedeuten, dass deine Kommunikation abgehört wird!", - "verify_nop": "Sitzung bereits verifiziert!", - "verify_nop_warning_mismatch": "ACHTUNG: Sitzung bereits verifiziert, aber die Schlüssel PASSEN NICHT!", - "verify_success_description": "Dein bereitgestellter Signaturschlüssel passt zum von der Sitzung %(deviceId)s von %(userId)s empfangendem Schlüssel. Sitzung wurde als verifiziert markiert.", - "verify_success_title": "Verifizierter Schlüssel", - "verify_unknown_pair": "Unbekanntes Paar (Nutzer, Sitzung): (%(userId)s, %(deviceId)s)", "view": "Raum mit angegebener Adresse betrachten", "whois": "Zeigt Informationen über Benutzer" }, @@ -2971,7 +3135,7 @@ "heading_with_query": "Nutze \"%(query)s\" zum Suchen", "heading_without_query": "Suche nach", "join_button_text": "%(roomAddress)s betreten", - "keyboard_scroll_hint": "Benutze zum scrollen", + "keyboard_scroll_hint": "Benutze zum Scrollen", "message_search_section_title": "Andere Suchen", "other_rooms_in_space": "Andere Räume in %(spaceName)s", "public_rooms_label": "Öffentliche Räume", @@ -3019,12 +3183,22 @@ "one": "%(count)s Antwort", "other": "%(count)s Antworten" }, + "empty_description": "Verwende \"%(replyInThread)s\" beim Hovern über eine Nachricht.", + "empty_title": "Threads helfen Ihnen, Ihre Unterhaltungen beim Thema zu halten, and sie sind auch leichter zu verfolgen.", "error_start_thread_existing_relation": "Du kannst keinen Thread in einem Thread starten", + "mark_all_read": "Alle als gelesen markieren", "my_threads": "Meine Threads", "my_threads_description": "Zeigt alle Threads, an denen du teilgenommen hast", "open_thread": "Thread anzeigen", "show_thread_filter": "Zeige:" }, + "threads_activity_centre": { + "header": "Thread-Aktivität", + "no_rooms_with_threads_notifs": "Sie haben noch keine Chatrooms mit Thread-Benachrichtigungen.", + "no_rooms_with_unread_threads": "Sie haben noch keine Chatrooms mit ungelesenen Threads.", + "release_announcement_description": "Die Thread-Benachrichtigungen wurden verschoben. Sie finden sie ab sofort hier.", + "release_announcement_header": "Thread Aktivitätszentrum" + }, "time": { "about_day_ago": "vor etwa einem Tag", "about_hour_ago": "vor etwa einer Stunde", @@ -3066,9 +3240,20 @@ }, "creation_summary_dm": "%(creator)s hat diese Direktnachricht erstellt.", "creation_summary_room": "%(creator)s hat den Raum erstellt und konfiguriert.", + "decryption_failure": { + "blocked": "Der Absender hat den Empfang dieser Nachricht blockiert, da Ihr Gerät nicht verifiziert ist.", + "historical_event_no_key_backup": "Der historische Nachrichtenverlauf ist auf diesem Gerät nicht verfügbar.", + "historical_event_unverified_device": "Sie müssen dieses Gerät verifizieren, um auf den Nachrichtenverlauf zugreifen zu können", + "historical_event_user_not_joined": "Sie haben keinen Zugriff auf diese Nachricht", + "sender_identity_previously_verified": "Die verifizierte Identität des Absenders hat sich geändert", + "sender_unsigned_device": "Von einem unsicheren Gerät verschickt.", + "unable_to_decrypt": "Entschlüsselung der Nachricht nicht möglich" + }, "disambiguated_profile": "%(displayName)s (%(matrixId)s)", "download_action_decrypting": "Entschlüsseln", "download_action_downloading": "Herunterladen", + "download_failed": "Herunterladen fehlgeschlagen", + "download_failed_description": "Beim Herunterladen dieser Datei ist ein Fehler aufgetreten", "e2e_state": "Status der Ende zu Ende Verschlüssellung", "edits": { "tooltip_label": "Am %(date)s geändert. Klicke, um Änderungen anzuzeigen.", @@ -3079,10 +3264,6 @@ "error_rendering_message": "Diese Nachricht kann nicht geladen werden", "historical_messages_unavailable": "Du kannst keine älteren Nachrichten lesen", "in_room_name": " in %(room)s", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s beendete eine Sprachübertragung", - "you": "Du hast eine Sprachübertragung beendet" - }, "io.element.widgets.layout": "%(senderName)s hat das Raumlayout geändert", "late_event_separator": "Ursprünglich gesendet %(dateTime)s", "load_error": { @@ -3127,7 +3308,7 @@ }, "m.file": { "error_decrypting": "Fehler beim Entschlüsseln des Anhangs", - "error_invalid": "Ungültige Datei%(extra)s" + "error_invalid": "Ungültige Datei" }, "m.image": { "error": "Kann Bild aufgrund eines Fehlers nicht anzeigen", @@ -3242,12 +3423,12 @@ "set": "%(senderDisplayName)s hat den Raumnamen geändert zu %(roomName)s." }, "m.room.pinned_events": { - "changed": "%(senderName)s hat die angehefteten Nachrichten für diesen Raum geändert.", - "changed_link": "%(senderName)s hat die angehefteten Nachrichten geändert.", - "pinned": "%(senderName)s hat eine Nachricht angeheftet. Alle angehefteten Nachrichten anzeigen.", - "pinned_link": "%(senderName)s hat eine Nachricht angeheftet. Alle angehefteten Nachrichten anzeigen.", - "unpinned": "%(senderName)s hat eine Nachricht losgelöst. Alle angepinnten Nachrichten anzeigen.", - "unpinned_link": "%(senderName)s hat eine Nachricht losgeheftet. Alle angehefteten Nachrichten anzeigen." + "changed": "%(senderName)s hat die fixierte Nachrichten für diesen Chatroom geändert.", + "changed_link": "%(senderName)s hat die fixierten Nachrichten geändert.", + "pinned": "%(senderName)s hat eine Nachricht fixiert. Alle fixierten Nachrichten anzeigen.", + "pinned_link": "%(senderName)s hat eine Nachricht fixiert. Alle fixierten Nachrichten anzeigen.", + "unpinned": "%(senderName)s hat eine Nachricht losgelöst. Alle fixierten Nachrichten anzeigen.", + "unpinned_link": "%(senderName)s hat eine Nachricht gelöst. Alle fixierten Nachrichten anzeigen." }, "m.room.power_levels": { "changed": "%(senderName)s hat das Berechtigungslevel von %(powerLevelDiffText)s geändert.", @@ -3314,7 +3495,8 @@ "reactions": { "add_reaction_prompt": "Reaktion hinzufügen", "custom_reaction_fallback_label": "Benutzerdefinierte Reaktion", - "label": "%(reactors)s hat mit %(content)s reagiert" + "label": "%(reactors)s hat mit %(content)s reagiert", + "tooltip_caption": "hat reagiert mit %(shortName)s" }, "read_receipt_title": { "one": "Von %(count)s Person gesehen", @@ -3499,6 +3681,10 @@ "truncated_list_n_more": { "other": "Und %(count)s weitere …" }, + "unsupported_browser": { + "description": "Wenn Sie fortfahren, funktionieren einige Funktionen möglicherweise nicht mehr und es besteht das Risiko, dass Sie in Zukunft Daten verlieren. Aktualisieren Sie Ihren Browser, um die Nutzung von %(brand)s fortzusetzen.", + "title": "%(brand)s unterstützt diesen Browser nicht" + }, "unsupported_server_description": "Dieser Server nutzt eine ältere Matrix-Version. Aktualisiere auf Matrix %(version)s, um %(brand)s fehlerfrei nutzen zu können.", "unsupported_server_title": "Dein Server wird nicht unterstützt", "update": { @@ -3516,6 +3702,12 @@ "toast_title": "Aktualisiere %(brand)s", "unavailable": "Nicht verfügbar" }, + "update_room_access_modal": { + "description": "Um einen Link zum Teilen zu erstellen, müssen Sie Gästen erlauben, diesem Chatroom beizutreten. Dadurch kann der Chatroom weniger sicher werden. Wenn Sie mit dem Anruf fertig sind, können Sie den Raum wieder privat machen.", + "dont_change_description": "Sie können den Anruf auch in einem separaten Chatroom führen.", + "no_change": "Ich möchte die Zugriffsebene nicht ändern.", + "title": "Ändern Sie die Zugriffsebene des Chatrooms." + }, "upload_failed_generic": "Die Datei „%(fileName)s“ konnte nicht hochgeladen werden.", "upload_failed_size": "Die Datei „%(fileName)s“ überschreitet das Hochladelimit deines Heim-Servers", "upload_failed_title": "Hochladen fehlgeschlagen", @@ -3525,6 +3717,7 @@ "error_files_too_large": "Die Datei ist zu groß, um hochgeladen zu werden. Die maximale Dateigröße ist %(limit)s.", "error_some_files_too_large": "Einige Dateien sind zu groß, um hochgeladen zu werden. Die maximale Dateigröße ist %(limit)s.", "error_title": "Fehler beim Hochladen", + "not_image": "Die von Ihnen ausgewählte Datei ist keine gültige Bilddatei.", "title": "Dateien hochladen", "title_progress": "Dateien hochladen (%(current)s von %(total)s)", "upload_all_button": "Alle hochladen", @@ -3551,6 +3744,7 @@ "deactivate_confirm_action": "Konto deaktivieren", "deactivate_confirm_description": "Beim Deaktivieren wirst du abgemeldet und ein erneutes Anmelden verhindert. Zusätzlich wirst du aus allen Räumen entfernt. Diese Aktion kann nicht rückgängig gemacht werden. Bist du sicher, dass du dieses Konto deaktivieren willst?", "deactivate_confirm_title": "Konto deaktivieren?", + "dehydrated_device_enabled": "Offline-Gerät aktiviert", "demote_button": "Zurückstufen", "demote_self_confirm_description_space": "Das Entfernen von Rechten kann nicht rückgängig gemacht werden. Falls sie dir niemand anderer zurückgeben kann, kannst du sie nie wieder erhalten.", "demote_self_confirm_room": "Du wirst nicht in der Lage sein, die Änderung zurückzusetzen, da du dich degradierst. Wenn du der letze Nutzer mit Berechtigungen bist, wird es unmöglich sein die Privilegien zurückzubekommen.", @@ -3567,6 +3761,7 @@ "error_revoke_3pid_invite_title": "Einladung konnte nicht zurückgezogen werden", "hide_sessions": "Sitzungen ausblenden", "hide_verified_sessions": "Verifizierte Sitzungen ausblenden", + "ignore_button": "Blockieren", "ignore_confirm_description": "Alle Nachrichten und Einladungen der Person werden verborgen. Bist du sicher, dass du sie ignorieren möchtest?", "ignore_confirm_title": "%(user)s ignorieren", "invited_by": "%(sender)s eingeladen", @@ -3594,59 +3789,30 @@ "no_recent_messages_description": "Versuche nach oben zu scrollen, um zu sehen ob sich dort frühere Nachrichten befinden.", "no_recent_messages_title": "Keine neuen Nachrichten von %(user)s gefunden" }, - "redact_button": "Kürzlich gesendete Nachrichten entfernen", + "redact_button": "Nachrichten entfernen", "revoke_invite": "Einladung zurückziehen", "room_encrypted": "Nachrichten in diesem Raum sind Ende-zu-Ende verschlüsselt.", "room_encrypted_detail": "Diese Nachricht ist verschlüsselt. Nur Sie und der Empfänger haben den Schlüssel, um die Nachricht zu entschlüsseln.", "room_unencrypted": "Nachrichten in diesem Raum sind nicht Ende-zu-Ende verschlüsselt.", "room_unencrypted_detail": "Nachrichten in verschlüsselten Räumen können nur von dir und vom Empfänger gelesen werden.", - "share_button": "Link zu Benutzer teilen", + "send_message": "Nachricht senden", + "share_button": "Profil teilen", "unban_button_room": "Entbannen", "unban_button_space": "Entbannen", "unban_room_confirm_title": "Von %(roomName)s entbannen", "unban_space_everything": "Überall wo ich die Rechte dazu habe, entbannen", "unban_space_specific": "In ausgewählten Räumen und Spaces entbannen", "unban_space_warning": "Die Person wird keinen Zutritt zu Bereichen haben, in denen du nicht administrierst.", + "unignore_button": "Nicht mehr ignorieren", "verify_button": "Nutzer verifizieren", "verify_explainer": "Für zusätzliche Sicherheit, verifiziere diesen Nutzer, durch Vergleichen eines Einmal-Codes auf euren beiden Geräten." }, "user_menu": { + "link_new_device": "Neues Gerät verknüpfen", "settings": "Alle Einstellungen", "switch_theme_dark": "Zum dunklen Thema wechseln", "switch_theme_light": "Zum hellen Thema wechseln" }, - "voice_broadcast": { - "30s_backward": "30s zurückspulen", - "30s_forward": "30s vorspulen", - "action": "Sprachübertragung", - "buffering": "Puffere …", - "confirm_listen_affirm": "Ja, beende meine Aufzeichnung", - "confirm_listen_description": "Wenn du beginnst, diese Echtzeitübertragung anzuhören, wird deine aktuelle Echtzeitübertragungsaufzeichnung beendet.", - "confirm_listen_title": "Echtzeitübertragung anhören?", - "confirm_stop_affirm": "Ja, Übertragung beenden", - "confirm_stop_description": "Möchtest du deine Übertragung wirklich beenden? Dies wird die Übertragung abschließen und die vollständige Aufnahme im Raum bereitstellen.", - "confirm_stop_title": "Live-Übertragung beenden?", - "connection_error": "Verbindungsfehler − Aufnahme pausiert", - "failed_already_recording_description": "Du zeichnest bereits eine Sprachübertragung auf. Bitte beende die laufende Übertragung, um eine neue zu beginnen.", - "failed_already_recording_title": "Sprachübertragung kann nicht gestartet werden", - "failed_decrypt": "Entschlüsseln der Sprachübertragung nicht möglich", - "failed_generic": "Wiedergabe der Sprachübertragung nicht möglich", - "failed_insufficient_permission_description": "Du hast nicht die nötigen Berechtigungen, um eine Sprachübertragung in diesem Raum zu starten. Kontaktiere einen Raumadministrator, um deine Berechtigungen anzupassen.", - "failed_insufficient_permission_title": "Sprachübertragung kann nicht gestartet werden", - "failed_no_connection_description": "Leider ist es aktuell nicht möglich, eine Aufnahme zu beginnen. Bitte versuche es später erneut.", - "failed_no_connection_title": "Verbindungsfehler", - "failed_others_already_recording_description": "Jemand anderes nimmt bereits eine Sprachübertragung auf. Warte auf das Ende der Übertragung, bevor du eine neue startest.", - "failed_others_already_recording_title": "Sprachübertragung kann nicht gestartet werden", - "go_live": "Live schalten", - "live": "Live", - "pause": "Sprachübertragung pausieren", - "play": "Sprachübertragung wiedergeben", - "resume": "Sprachübertragung fortsetzen" - }, - "voice_message": { - "cant_start_broadcast_description": "Du kannst keine Sprachnachricht beginnen, da du im Moment eine Echtzeitübertragung aufzeichnest. Bitte beende deine Sprachübertragung, um ein Gespräch zu beginnen.", - "cant_start_broadcast_title": "Kann Sprachnachricht nicht beginnen" - }, "voip": { "already_in_call": "Schon im Anruf", "already_in_call_person": "Du bist schon in einem Anruf mit dieser Person.", @@ -3666,10 +3832,10 @@ "camera_disabled": "Deine Kamera ist ausgeschaltet", "camera_enabled": "Deine Kamera ist noch aktiv", "cannot_call_yourself_description": "Du kannst keinen Anruf mit dir selbst starten.", - "change_input_device": "Eingabegerät wechseln", + "close_lobby": "Lobby schließen", "connecting": "Verbinden", "connection_lost": "Verbindung zum Server unterbrochen", - "connection_lost_description": "Sie können keine Anrufe starten ohne Verbindung zum Server.", + "connection_lost_description": "Ohne Verbindung zum Server können Sie keine Anrufe tätigen.", "consulting": "%(transferTarget)s wird angefragt. Übertragung zu %(transferee)s", "default_device": "Standardgerät", "dial": "Wählen", @@ -3680,17 +3846,24 @@ "disabled_no_perms_start_video_call": "Dir fehlt die Berechtigung, um Videoanrufe zu beginnen", "disabled_no_perms_start_voice_call": "Dir fehlt die Berechtigung, um Audioanrufe zu beginnen", "disabled_ongoing_call": "laufender Anruf", + "element_call": "Element Anruf", "enable_camera": "Kamera aktivieren", "enable_microphone": "Mikrofon aktivieren", "expand": "Zurück zum Anruf", - "failed_call_live_broadcast_description": "Du kannst keinen Anruf beginnen, da du im Moment eine Sprachübertragung aufzeichnest. Bitte beende deine Sprachübertragung, um ein Gespräch zu beginnen.", - "failed_call_live_broadcast_title": "Kann keinen Anruf beginnen", + "get_call_link": "Anruflink teilen", "hangup": "Auflegen", "hide_sidebar_button": "Seitenleiste verbergen", "input_devices": "Eingabegeräte", + "jitsi_call": "Jitsi-Konferenz", "join_button_tooltip_call_full": "Entschuldigung — dieser Anruf ist aktuell besetzt", "join_button_tooltip_connecting": "Verbinden", + "legacy_call": "Legacy-Anruf", "maximise": "Bildschirm füllen", + "maximise_call": "Anruf maximieren", + "metaspace_video_rooms": { + "conference_room_section": "Konferenzen" + }, + "minimise_call": "Anruf minimieren", "misconfigured_server": "Anruf aufgrund eines falsch konfigurierten Servers fehlgeschlagen", "misconfigured_server_description": "Bitte frage die Administration deines Heim-Servers (%(homeserverDomain)s) darum, einen TURN-Server einzurichten, damit Anrufe zuverlässig funktionieren.", "misconfigured_server_fallback": "Alternativ kannst du versuchen, den öffentlichen Server unter zu verwenden. Dieser wird nicht so zuverlässig sein und deine IP-Adresse wird mit ihm geteilt. Du kannst dies auch in den Einstellungen konfigurieren.", @@ -3732,12 +3905,13 @@ "unknown_person": "unbekannte Person", "unsilence": "Ton an", "unsupported": "Anrufe werden nicht unterstützt", - "unsupported_browser": "Sie können in diesem Browser keien Anrufe durchführen.", + "unsupported_browser": "Du kannst in diesem Browser keine Anrufe tätigen.", "user_busy": "Person beschäftigt", "user_busy_description": "Die angerufene Person ist momentan beschäftigt.", "user_is_presenting": "%(sharerName)s präsentiert", "video_call": "Videoanruf", "video_call_started": "Videoanruf hat begonnen", + "video_call_using": "Videoanruf mit:", "voice_call": "Sprachanruf", "you_are_presenting": "Du präsentierst" }, @@ -3846,7 +4020,7 @@ "title": "Erlaube diesem Widget deine Identität zu überprüfen" }, "popout": "Widget in eigenem Fenster öffnen", - "set_room_layout": "Dein Raumlayout für alle setzen", + "set_room_layout": "Layout für alle festlegen", "shared_data_avatar": "Deine Profilbild-URL", "shared_data_device_id": "Deine Geräte-ID", "shared_data_lang": "Deine Sprache", @@ -3872,7 +4046,7 @@ "l33t": "Vorhersagbare Ersetzungen wie „@“ anstelle von „a“ helfen nicht besonders", "longerKeyboardPattern": "Nutze ein längeres Tastaturmuster mit mehr Abwechslung", "noNeed": "Kein Bedarf an Symbolen, Zahlen oder Großbuchstaben", - "pwned": "Wenn Sie dieses Passwort woanders verwenden, sollten Sie es ändern.", + "pwned": "Falls Sie dieses Passwort anderweitig verwenden, sollten Sie es ändern.", "recentYears": "Vermeide die letzten Jahre", "repeated": "Vermeide wiederholte Worte und Zeichen", "reverseWords": "Umgedrehte Worte sind nicht schwerer zu erraten", @@ -3894,6 +4068,7 @@ "straightRow": "Gerade Reihen von Tasten sind einfach zu erraten", "topHundred": "Dies ist unter den Top 100 der häufigsten Passwörter", "topTen": "Dies ist unter den Top 10 der häufigsten Passwörter", + "userInputs": "Personenbezogene oder seitenbezogene Daten sollten hier nicht vorhanden sein.", "wordByItself": "Ein einzelnes Wort ist einfach zu erraten" } } diff --git a/src/i18n/strings/el.json b/src/i18n/strings/el.json index 2c042c0dc36..808aeb02eb2 100644 --- a/src/i18n/strings/el.json +++ b/src/i18n/strings/el.json @@ -763,12 +763,6 @@ "incoming_sas_dialog_title": "Εισερχόμενο Αίτημα Επαλήθευσης", "incoming_sas_user_dialog_text_1": "Επαληθεύστε αυτόν τον χρήστη για να τον επισημάνετε ως αξιόπιστο. Η εμπιστοσύνη των χρηστών σάς προσφέρει επιπλέον ηρεμία όταν χρησιμοποιείτε μηνύματα με κρυπτογράφηση από άκρο σε άκρο.", "incoming_sas_user_dialog_text_2": "Η επαλήθευση αυτού του χρήστη θα επισημάνει τη συνεδρία του ως αξιόπιστη και θα επισημάνει επίσης τη συνεδρία σας ως αξιόπιστη σε αυτόν.", - "manual_device_verification_device_id_label": "Αναγνωριστικό συνεδρίας", - "manual_device_verification_device_key_label": "Κλειδί συνεδρίας", - "manual_device_verification_device_name_label": "Όνομα συνεδρίας", - "manual_device_verification_footer": "Εάν δεν ταιριάζουν, η ασφάλεια της επικοινωνίας σας μπορεί να τεθεί σε κίνδυνο.", - "manual_device_verification_self_text": "Επιβεβαιώστε συγκρίνοντας τα ακόλουθα με τις Ρυθμίσεις χρήστη στην άλλη συνεδρία σας:", - "manual_device_verification_user_text": "Επιβεβαιώστε την συνεδρία αυτού του χρήστη συγκρίνοντας τα ακόλουθα με τις Ρυθμίσεις του:", "no_key_or_device": "Φαίνεται ότι δε διαθέτετε Κλειδί Ασφαλείας ή άλλες συσκευές με τις οποίες μπορείτε να επαληθεύσετε. Αυτή η συσκευή δε θα έχει πρόσβαση σε παλιά κρυπτογραφημένα μηνύματα. Για να επαληθεύσετε την ταυτότητά σας σε αυτήν τη συσκευή, θα πρέπει να επαναφέρετε τα κλειδιά επαλήθευσης.", "no_support_qr_emoji": "Η συσκευή που προσπαθείτε να επαληθεύσετε δεν υποστηρίζει τη σάρωση κωδικού QR ή επαλήθευσης emoji, κάτι που υποστηρίζει το %(brand)s. Δοκιμάστε με διαφορετικό πρόγραμμα-πελάτη.", "other_party_cancelled": "Το άλλο μέρος ακύρωσε την επαλήθευση.", @@ -2164,7 +2158,6 @@ "warn_quit": "Προειδοποιήστε πριν την παραίτηση" }, "share": { - "link_title": "Σύνδεσμος στο δωμάτιο", "permalink_message": "Σύνδεσμος στο επιλεγμένο μήνυμα", "permalink_most_recent": "Σύνδεσμος προς το πιο πρόσφατο μήνυμα", "title_message": "Κοινή χρήση Μηνύματος Δωματίου", @@ -2246,12 +2239,6 @@ "upgraderoom": "Αναβαθμίζει το δωμάτιο σε μια καινούργια έκδοση", "upgraderoom_permission_error": "Δεν διαθέτετε τις απαιτούμενες άδειες για να χρησιμοποιήσετε αυτήν την εντολή.", "usage": "Χρήση", - "verify": "Επιβεβαιώνει έναν χρήστη, συνεδρία, και pubkey tuple", - "verify_mismatch": "ΠΡΟΕΙΔΟΠΟΙΗΣΗ: Η ΕΠΑΛΗΘΕΥΣΗ ΚΛΕΙΔΙΟΥ ΑΠΕΤΥΧΕ! Το κλειδί σύνδεσης για %(userId)s και συνεδρίας %(deviceId)s είναι \"%(fprint)s\" που δεν ταιριάζει με το παρεχόμενο κλειδί\"%(fingerprint)s\". Αυτό μπορεί να σημαίνει ότι υπάρχει υποκλοπή στις επικοινωνίες σας!", - "verify_nop": "Η συνεδρία έχει ήδη επιβεβαιωθεί!", - "verify_success_description": "Το κλειδί υπογραφής που παρείχατε ταιριάζει με το κλειδί που λάβατε από την συνεδρία %(userId)s's %(deviceId)s. Η συνεδρία σημειώνεται ως επιβεβαιωμένη.", - "verify_success_title": "Επιβεβαιωμένο κλειδί", - "verify_unknown_pair": "Άγνωστο ζευγάρι (χρήστης, συνεδρία): (%(userId)s, %(deviceId)s)", "whois": "Εμφανίζει πληροφορίες για έναν χρήστη" }, "space": { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8c5ab0b2e3a..2132530500a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -398,7 +398,7 @@ }, "bug_reporting": { "additional_context": "If there is additional context that would help in analysing the issue, such as what you were doing at the time, room IDs, user IDs, etc., please include those things here.", - "before_submitting": "Before submitting logs, you must create a GitHub issue to describe your problem.", + "before_submitting": "We recommend creating a GitHub issue to ensure that your report is reviewed.", "collecting_information": "Collecting app version information", "collecting_logs": "Collecting logs", "create_new_issue": "Please create a new issue on GitHub so that we can investigate this bug.", @@ -924,7 +924,6 @@ }, "udd": { "interactive_verification_button": "Interactively verify by emoji", - "manual_verification_button": "Manually verify by text", "other_ask_verify_text": "Ask this user to verify their session, or manually verify it below.", "other_new_session_text": "%(name)s (%(userId)s) signed in to a new session without verifying it:", "own_ask_verify_text": "Verify your other session using one of the options below.", @@ -959,12 +958,6 @@ "incoming_sas_dialog_waiting": "Waiting for partner to confirm…", "incoming_sas_user_dialog_text_1": "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.", "incoming_sas_user_dialog_text_2": "Verifying this user will mark their session as trusted, and also mark your session as trusted to them.", - "manual_device_verification_device_id_label": "Session ID", - "manual_device_verification_device_key_label": "Session key", - "manual_device_verification_device_name_label": "Session name", - "manual_device_verification_footer": "If they don't match, the security of your communication may be compromised.", - "manual_device_verification_self_text": "Confirm by comparing the following with the User Settings in your other session:", - "manual_device_verification_user_text": "Confirm this user's session by comparing the following with their User Settings:", "no_key_or_device": "It looks like you don't have a Security Key or any other devices you can verify against. This device will not be able to access old encrypted messages. In order to verify your identity on this device, you'll need to reset your verification keys.", "no_support_qr_emoji": "The device you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.", "other_party_cancelled": "The other party cancelled the verification.", @@ -1004,7 +997,7 @@ "unverified_sessions_toast_description": "Review to ensure your account is safe", "unverified_sessions_toast_reject": "Later", "unverified_sessions_toast_title": "You have unverified sessions", - "verification_description": "Verify your identity to access encrypted messages and prove your identity to others.", + "verification_description": "Verify your identity to access encrypted messages and prove your identity to others. If you also use a mobile device, please open the app there before you proceed.", "verification_dialog_title_device": "Verify other device", "verification_dialog_title_user": "Verification Request", "verification_skip_warning": "Without verifying, you won't have access to all your messages and may appear as untrusted to others.", @@ -1089,10 +1082,6 @@ }, "error_user_not_logged_in": "User is not logged in", "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s ended a voice broadcast", - "you": "You ended a voice broadcast" - }, "m.call.answer": { "dm": "Call in progress", "user": "%(senderName)s joined the call", @@ -1493,8 +1482,6 @@ "video_rooms_faq2_answer": "Yes, the chat timeline is displayed alongside the video.", "video_rooms_faq2_question": "Can I use text chat alongside the video call?", "video_rooms_feedbackSubheading": "Thank you for trying the beta, please go into as much detail as you can so we can improve it.", - "voice_broadcast": "Voice broadcast", - "voice_broadcast_force_small_chunks": "Force 15s voice broadcast chunk length", "wysiwyg_composer": "Rich text editor" }, "labs_mjolnir": { @@ -1640,7 +1627,6 @@ "mute_description": "You won't get any notifications" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s started a voice broadcast", "m.key.verification.request": "%(name)s is requesting verification" }, "onboarding": { @@ -2255,7 +2241,6 @@ "error_unbanning": "Failed to unban", "events_default": "Send messages", "invite": "Invite users", - "io.element.voice_broadcast_info": "Voice broadcasts", "kick": "Remove users", "m.call": "Start %(brand)s calls", "m.call.member": "Join %(brand)s calls", @@ -2954,7 +2939,7 @@ "warning": "WARNING: " }, "share": { - "link_title": "Link to room", + "link_copied": "Link copied", "permalink_message": "Link to selected message", "permalink_most_recent": "Link to most recent message", "share_call": "Conference invite link", @@ -3046,13 +3031,6 @@ "upgraderoom": "Upgrades a room to a new version", "upgraderoom_permission_error": "You do not have the required permissions to use this command.", "usage": "Usage", - "verify": "Verifies a user, session, and pubkey tuple", - "verify_mismatch": "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!", - "verify_nop": "Session already verified!", - "verify_nop_warning_mismatch": "WARNING: session already verified, but keys do NOT MATCH!", - "verify_success_description": "The signing key you provided matches the signing key you received from %(userId)s's session %(deviceId)s. Session marked as verified.", - "verify_success_title": "Verified key", - "verify_unknown_pair": "Unknown (user, session) pair: (%(userId)s, %(deviceId)s)", "view": "Views room with given address", "whois": "Displays information about a user" }, @@ -3289,10 +3267,6 @@ "error_rendering_message": "Can't load this message", "historical_messages_unavailable": "You can't see earlier messages", "in_room_name": " in %(room)s", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s ended a voice broadcast", - "you": "You ended a voice broadcast" - }, "io.element.widgets.layout": "%(senderName)s has updated the room layout", "late_event_separator": "Originally sent %(dateTime)s", "load_error": { @@ -3842,38 +3816,6 @@ "switch_theme_dark": "Switch to dark mode", "switch_theme_light": "Switch to light mode" }, - "voice_broadcast": { - "30s_backward": "30s backward", - "30s_forward": "30s forward", - "action": "Voice broadcast", - "buffering": "Buffering…", - "confirm_listen_affirm": "Yes, end my recording", - "confirm_listen_description": "If you start listening to this live broadcast, your current live broadcast recording will be ended.", - "confirm_listen_title": "Listen to live broadcast?", - "confirm_stop_affirm": "Yes, stop broadcast", - "confirm_stop_description": "Are you sure you want to stop your live broadcast? This will end the broadcast and the full recording will be available in the room.", - "confirm_stop_title": "Stop live broadcasting?", - "connection_error": "Connection error - Recording paused", - "failed_already_recording_description": "You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.", - "failed_already_recording_title": "Can't start a new voice broadcast", - "failed_decrypt": "Unable to decrypt voice broadcast", - "failed_generic": "Unable to play this voice broadcast", - "failed_insufficient_permission_description": "You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.", - "failed_insufficient_permission_title": "Can't start a new voice broadcast", - "failed_no_connection_description": "Unfortunately we're unable to start a recording right now. Please try again later.", - "failed_no_connection_title": "Connection error", - "failed_others_already_recording_description": "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.", - "failed_others_already_recording_title": "Can't start a new voice broadcast", - "go_live": "Go live", - "live": "Live", - "pause": "pause voice broadcast", - "play": "play voice broadcast", - "resume": "resume voice broadcast" - }, - "voice_message": { - "cant_start_broadcast_description": "You can't start a voice message as you are currently recording a live broadcast. Please end your live broadcast in order to start recording a voice message.", - "cant_start_broadcast_title": "Can't start voice message" - }, "voip": { "already_in_call": "Already in call", "already_in_call_person": "You're already in a call with this person.", @@ -3893,7 +3835,6 @@ "camera_disabled": "Your camera is turned off", "camera_enabled": "Your camera is still enabled", "cannot_call_yourself_description": "You cannot place a call with yourself.", - "change_input_device": "Change input device", "close_lobby": "Close lobby", "connecting": "Connecting", "connection_lost": "Connectivity to the server has been lost", @@ -3912,8 +3853,6 @@ "enable_camera": "Turn on camera", "enable_microphone": "Unmute microphone", "expand": "Return to call", - "failed_call_live_broadcast_description": "You can’t start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call.", - "failed_call_live_broadcast_title": "Can’t start a call", "get_call_link": "Share call link", "hangup": "Hangup", "hide_sidebar_button": "Hide sidebar", diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json index 33d8e57f488..88694a3a580 100644 --- a/src/i18n/strings/eo.json +++ b/src/i18n/strings/eo.json @@ -693,12 +693,6 @@ "incoming_sas_dialog_title": "Venas kontrolpeto", "incoming_sas_user_dialog_text_1": "Kontrolu ĉi tiun uzanton por marki ĝin fidata. Fidado devas vin trankviligi dum uzado de tutvoja ĉifrado.", "incoming_sas_user_dialog_text_2": "Kontrolo de tiu ĉi uzanto markos ĝian salutaĵon fidata, kaj ankaŭ markos vian salutaĵon fidata por ĝi.", - "manual_device_verification_device_id_label": "Identigilo de salutaĵo", - "manual_device_verification_device_key_label": "Ŝlosilo de salutaĵo", - "manual_device_verification_device_name_label": "Nomo de salutaĵo", - "manual_device_verification_footer": "Se ili ne akordas, la sekureco de via komunikado eble estas rompita.", - "manual_device_verification_self_text": "Konfirmu per komparo de la sekva kun la agardoj de uzanto en via alia salutaĵo:", - "manual_device_verification_user_text": "Konfirmu la salutaĵon de ĉi tiu uzanto per komparo de la sekva kun ĝiaj agordoj de uzanto:", "other_party_cancelled": "La alia kontrolano nuligis la kontrolon.", "prompt_encrypted": "Kontrolu ĉiujn uzantojn en ĉambro por certigi, ke ĝi sekuras.", "prompt_self": "Rekomencu kontroladon el la sciigo.", @@ -795,10 +789,6 @@ } }, "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s finis voĉan elsendon", - "you": "Vi finis voĉan elsendon" - }, "m.call.answer": { "dm": "Voko okazas", "user": "%(senderName)s aliĝis al la voko", @@ -1065,7 +1055,6 @@ "video_rooms_always_on_voip_channels": "Videoĉambroj estas ĉiam ŝaltitaj VoIP-kanaloj enkonstruitaj en ĉambro en %(brand)s.", "video_rooms_faq1_answer": "Uzu la \"+\" butonon en la ĉambro sekcio de la maldekstra panelo.", "video_rooms_faq1_question": "Kiel mi povas krei videoĉambron?", - "voice_broadcast": "Voĉan elsendo", "wysiwyg_composer": "Riĉa tekstoredaktilo" }, "labs_mjolnir": { @@ -1167,7 +1156,6 @@ "message_didnt_send": "Mesaĝo ne sendiĝis. Klaku por akiri informojn." }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s komencis voĉan elsendon", "m.key.verification.request": "%(name)s petas kontrolon" }, "onboarding": { @@ -2050,13 +2038,6 @@ "upgraderoom": "Gradaltigas ĉambron al nova versio", "upgraderoom_permission_error": "Vi ne havas sufiĉajn permesojn por uzi ĉi tiun komandon.", "usage": "Uzo", - "verify": "Kontrolas opon de uzanto, salutaĵo, kaj publika ŝlosilo", - "verify_mismatch": "AVERTO: MALSUKCESIS KONTROLO DE ŜLOSILOJ! La subskriba ŝlosilo de %(userId)s kaj session %(deviceId)s estas «%(fprint)s», kiu ne akordas la donitan ŝlosilon «%(fingerprint)s». Tio povus signifi, ke via komunikado estas spionata!", - "verify_nop": "Salutaĵo jam estas kontrolita!", - "verify_nop_warning_mismatch": "AVERTO: Salutaĵo jam estas kontrolita, sed la ŝlosiloj NE AKORDAS!", - "verify_success_description": "La subskriba ŝlosilo, kiun vi donis, akordas la subskribas ŝlosilon, kinu vi ricevis de la salutaĵo %(deviceId)s de la uzanto %(userId)s. Salutaĵo estis markita kontrolita.", - "verify_success_title": "Kontrolita ŝlosilo", - "verify_unknown_pair": "Nekonata (uzanto, salutaĵo) duopo: (%(userId)s, %(deviceId)s)", "whois": "Montras informojn pri uzanto" }, "space": { @@ -2208,10 +2189,6 @@ }, "error_no_renderer": "Ĉi tiu okazo ne povis montriĝi", "error_rendering_message": "Ne povas enlegi ĉi tiun mesaĝon", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s finis voĉan elsendon", - "you": "Vi finis voĉan elsendon" - }, "io.element.widgets.layout": "%(senderName)s ĝisdatigis la aranĝon de ĉambro", "load_error": { "no_permission": "Provis enlegi certan parton de ĉi tiu historio, sed vi ne havas permeson vidi ĝin.", @@ -2626,32 +2603,6 @@ "switch_theme_dark": "Ŝalti malhelan reĝimon", "switch_theme_light": "Ŝalti helan reĝimon" }, - "voice_broadcast": { - "30s_backward": "30s. reen", - "30s_forward": "30s. antaŭen", - "action": "Voĉan elsendo", - "confirm_listen_affirm": "Jes, ĉesigu mian registradon", - "confirm_listen_description": "Se vi komencas aŭskulti ĉi tiun vivan elsendon, via nuna viva elsendo registrado estos finita.", - "confirm_listen_title": "Aŭskulti vivan elsendon?", - "confirm_stop_affirm": "Jes, ĉesu elsendon", - "confirm_stop_description": "Ĉu vi certas, ke vi volas fini la elsendon? Ĉi tio finos la transdonon kaj provizos la plenan registradon en la ĉambro.", - "confirm_stop_title": "Ĉu ĉesi rekta elsendo?", - "failed_already_recording_description": "Vi jam registras voĉan elsendon. Bonvolu fini vian nunan voĉelsendon por komenci novan.", - "failed_already_recording_title": "Ne povas komenci novan voĉan elsendon", - "failed_decrypt": "Ne eblas deĉifri voĉan elsendon", - "failed_generic": "Ne eblas ludi ĉi tiun voĉan elsendon", - "failed_insufficient_permission_description": "Vi ne havas la bezonatajn permesojn por komenci voĉan elsendon en ĉi tiu ĉambro. Kontaktu ĉambran administranton por ĝisdatigi viajn permesojn.", - "failed_insufficient_permission_title": "Ne povas komenci novan voĉan elsendon", - "failed_no_connection_description": "Bedaŭrinde ni ne povas komenci registradon nun. Bonvolu reprovi poste.", - "failed_no_connection_title": "eraro de konekto", - "failed_others_already_recording_description": "Iu alia jam registras voĉan elsendon. Atendu, ke ilia voĉa elsendo finiĝos por komenci novan.", - "failed_others_already_recording_title": "Ne povas komenci novan voĉan elsendon", - "go_live": "Iru vivi", - "live": "Vivi", - "pause": "paŭzi voĉan elsendon", - "play": "ludu voĉan elsendon", - "resume": "rekomenci voĉan elsendon" - }, "voip": { "already_in_call": "Jam vokanta", "already_in_call_person": "Vi jam vokas ĉi tiun personon.", @@ -2671,7 +2622,6 @@ "camera_disabled": "Via filmilo estas malŝaltita", "camera_enabled": "Via filmilo ankoraŭ estas ŝaltita", "cannot_call_yourself_description": "Vi ne povas voki vin mem.", - "change_input_device": "Ŝanĝu enigan aparaton", "connecting": "Konektante", "connection_lost": "Konektebleco al la servilo estas perdita", "connection_lost_description": "Vi ne povas voki sen konektaĵo al la servilo.", @@ -2683,8 +2633,6 @@ "enable_camera": "Ŝalti la filmilon", "enable_microphone": "Malsilentigi la mikrofonon", "expand": "Reveni al voko", - "failed_call_live_broadcast_description": "Vi ne povas komenci vokon ĉar vi nuntempe registras vivan elsendon. Bonvolu fini vian vivan elsendon por komenci vokon.", - "failed_call_live_broadcast_title": "Ne povas komenci vokon", "hangup": "Fini vokon", "hide_sidebar_button": "Kaŝi flankan breton", "join_button_tooltip_connecting": "Konektante", diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json index cb6a8557b39..0f54ccca700 100644 --- a/src/i18n/strings/es.json +++ b/src/i18n/strings/es.json @@ -813,7 +813,6 @@ }, "udd": { "interactive_verification_button": "Verificar interactivamente usando emojis", - "manual_verification_button": "Verificar manualmente usando un texto", "other_ask_verify_text": "Pídele al usuario que verifique su sesión, o verifícala manualmente a continuación.", "other_new_session_text": "%(name)s (%(userId)s) inició una nueva sesión sin verificarla:", "own_ask_verify_text": "Verifica la otra sesión utilizando una de las siguientes opciones.", @@ -848,12 +847,6 @@ "incoming_sas_dialog_waiting": "Esperando a que la otra persona confirme…", "incoming_sas_user_dialog_text_1": "Verifica a este usuario para marcarlo como de confianza. Confiar en usuarios aporta tranquilidad en los mensajes cifrados de extremo a extremo.", "incoming_sas_user_dialog_text_2": "Verificar este usuario marcará su sesión como de confianza, y también marcará tu sesión como de confianza para él.", - "manual_device_verification_device_id_label": "ID de Sesión", - "manual_device_verification_device_key_label": "Código de sesión", - "manual_device_verification_device_name_label": "Nombre de sesión", - "manual_device_verification_footer": "Si no coinciden, la seguridad de su comunicación puede estar comprometida.", - "manual_device_verification_self_text": "Confirme comparando lo siguiente con los ajustes de usuario de su otra sesión:", - "manual_device_verification_user_text": "Confirma la sesión de este usuario comparando lo siguiente con su configuración:", "no_key_or_device": "Parece que no tienes una clave de seguridad u otros dispositivos para la verificación. Este dispositivo no podrá acceder los mensajes cifrados antiguos. Para verificar tu identidad en este dispositivo, tendrás que restablecer tus claves de verificación.", "no_support_qr_emoji": "El dispositivo que estás intentando verificar no es compatible con el escaneo de códigos QR o la verificación con emojis, que son las opciones que %(brand)s ofrece. Prueba con otra aplicación distinta.", "other_party_cancelled": "El otro lado canceló la verificación.", @@ -1304,7 +1297,6 @@ "video_rooms_faq1_question": "Cómo crear una sala de vídeo", "video_rooms_faq2_answer": "Sí, el historial de la sala aparece al lado del vídeo.", "video_rooms_faq2_question": "¿Puedo mandar mensajes de texto en la videollamada?", - "voice_broadcast": "Retransmisión de voz", "wysiwyg_composer": "Editor de texto enriquecido" }, "labs_mjolnir": { @@ -1955,7 +1947,6 @@ "error_unbanning": "No se pudo quitar veto", "events_default": "Enviar mensajes", "invite": "Invitar usuarios", - "io.element.voice_broadcast_info": "Retransmisiones de voz", "kick": "Sacar usuarios", "m.call": "Empezar llamadas de %(brand)s", "m.call.member": "Unirte a llamadas de %(brand)s", @@ -2529,7 +2520,6 @@ "warning": "ADVERTENCIA : " }, "share": { - "link_title": "Enlace a la sala", "permalink_message": "Enlazar al mensaje seleccionado", "permalink_most_recent": "Enlazar al mensaje más reciente", "title_message": "Compartir un mensaje de esta sala", @@ -2612,13 +2602,6 @@ "upgraderoom": "Actualiza una sala a una nueva versión", "upgraderoom_permission_error": "No tienes los permisos requeridos para usar este comando.", "usage": "Uso", - "verify": "Verifica a un usuario, sesión y tupla de clave pública", - "verify_mismatch": "¡ATENCIÓN: LA VERIFICACIÓN DE LA CLAVE HA FALLADO! La clave de firma para %(userId)s y sesión %(deviceId)s es \"%(fprint)s\", la cual no coincide con la clave proporcionada \"%(fingerprint)s\". ¡Esto podría significar que tus comunicaciones están siendo interceptadas!", - "verify_nop": "¡La sesión ya ha sido verificada!", - "verify_nop_warning_mismatch": "ADVERTENCIA: la sesión ya está verificada, pero las claves NO COINCIDEN", - "verify_success_description": "La clave de firma que proporcionaste coincide con la clave de firma que recibiste de la sesión %(deviceId)s de %(userId)s. Sesión marcada como verificada.", - "verify_success_title": "Clave verificada", - "verify_unknown_pair": "Pareja (usuario, sesión) desconocida: (%(userId)s, %(deviceId)s)", "whois": "Muestra información sobre un usuario" }, "space": { @@ -3336,29 +3319,6 @@ "switch_theme_dark": "Cambiar al tema oscuro", "switch_theme_light": "Cambiar al tema claro" }, - "voice_broadcast": { - "30s_backward": "retroceder 30s", - "30s_forward": "avanzar 30s", - "action": "Retransmisión de voz", - "buffering": "Cargando…", - "confirm_listen_affirm": "Sí, terminar grabación", - "confirm_stop_affirm": "Sí, detener retransmisión", - "confirm_stop_title": "¿Dejar de retransmitir?", - "connection_error": "Error de conexión, grabación detenida", - "failed_already_recording_title": "No se ha podido iniciar una nueva difusión de voz", - "failed_insufficient_permission_title": "No se ha podido iniciar una nueva difusión de voz", - "failed_no_connection_description": "Lamentablemente, no hemos podido empezar a grabar ahora mismo. Inténtalo de nuevo más tarde.", - "failed_no_connection_title": "Error de conexión", - "failed_others_already_recording_title": "No se ha podido iniciar una nueva difusión de voz", - "go_live": "Empezar directo", - "live": "En directo", - "pause": "pausar retransmisión de voz", - "play": "reproducir difusión de voz", - "resume": "reanudar retransmisión de voz" - }, - "voice_message": { - "cant_start_broadcast_title": "No se ha podido empezar el mensaje de voz" - }, "voip": { "already_in_call": "Ya en una llamada", "already_in_call_person": "Ya estás en una llamada con esta persona.", @@ -3378,7 +3338,6 @@ "camera_disabled": "Tu cámara está apagada", "camera_enabled": "Tu cámara todavía está encendida", "cannot_call_yourself_description": "No puedes llamarte a ti mismo.", - "change_input_device": "Cambiar dispositivo de entrada", "connecting": "Conectando", "connection_lost": "Se ha perdido la conexión con el servidor", "connection_lost_description": "No puedes llamar porque no hay conexión con el servidor.", @@ -3395,8 +3354,6 @@ "enable_camera": "Encender cámara", "enable_microphone": "Activar micrófono", "expand": "Volver a la llamada", - "failed_call_live_broadcast_description": "No puedes empezar una llamada, porque estás grabando una retransmisión en directo. Por favor, finaliza tu retransmisión en directo para empezar la llamada.", - "failed_call_live_broadcast_title": "No se ha podido empezar la llamada", "hangup": "Colgar", "hide_sidebar_button": "Ocultar menú lateral", "input_devices": "Dispositivos de entrada", diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index b05d9024c05..093dcf861ef 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -880,7 +880,6 @@ }, "udd": { "interactive_verification_button": "Verifitseeri interaktiivselt emoji abil", - "manual_verification_button": "Verifitseeri käsitsi etteantud teksti abil", "other_ask_verify_text": "Palu nimetatud kasutajal verifitseerida see sessioon või tee seda alljärgnevaga käsitsi.", "other_new_session_text": "%(name)s (%(userId)s) logis sisse uude sessiooni ilma seda verifitseerimata:", "own_ask_verify_text": "Verifitseeri oma teine sessioon kasutades üht alljärgnevatest võimalustest.", @@ -915,12 +914,6 @@ "incoming_sas_dialog_waiting": "Ootan teise osapoole kinnitust…", "incoming_sas_user_dialog_text_1": "Selle kasutaja usaldamiseks peaksid ta verifitseerima. Kui sa pruugid läbivalt krüptitud sõnumeid, siis kasutajate verifitseerimine tagab sulle täiendava meelerahu.", "incoming_sas_user_dialog_text_2": "Selle kasutaja verifitseerimisel märgitakse tema sessioon usaldusväärseks ning samuti märgitakse sinu sessioon tema jaoks usaldusväärseks.", - "manual_device_verification_device_id_label": "Sessiooni tunnus", - "manual_device_verification_device_key_label": "Sessiooni võti", - "manual_device_verification_device_name_label": "Sessiooni nimi", - "manual_device_verification_footer": "Kui nad omavahel ei klapi, siis teie suhtluse turvalisus võib olla ohus.", - "manual_device_verification_self_text": "Kinnita seda võrreldes järgnevaid andmeid oma teise sessiooni kasutajaseadetes:", - "manual_device_verification_user_text": "Kinnita selle kasutaja sessioon võrreldes järgnevaid andmeid tema kasutajaseadetes:", "no_key_or_device": "Tundub, et sul ei ole ei turvavõtit ega muid seadmeid, mida saaksid verifitseerimiseks kasutada. Siin seadmes ei saa lugeda vanu krüptitud sõnumeid. Enda tuvastamiseks selles seadmed pead oma vanad verifitseerimisvõtmed kustutama.", "no_support_qr_emoji": "See seade, mida sa tahad verifitseerida ei toeta QR-koodi ega emoji-põhist verifitseerimist, aga just neid %(brand)s oskab kasutada. Proovi mõne muu Matrix'i kliendiga.", "other_party_cancelled": "Teine osapool tühistas verifitseerimise.", @@ -1043,10 +1036,6 @@ }, "error_user_not_logged_in": "Kasutaja pole võrku loginud", "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s lõpetas ringhäälingukõne", - "you": "Sa lõpetasid ringhäälingukõne" - }, "m.call.answer": { "dm": "Kõne on pooleli", "user": "%(senderName)s liitus kõnega", @@ -1415,8 +1404,6 @@ "video_rooms_faq1_question": "Kuidas ma saan luua videotoa?", "video_rooms_faq2_answer": "Jah, tekstivestluse ajajoon on kuvatud videovaate kõrval.", "video_rooms_faq2_question": "Kas ma saan videokõne ajal ka tekstisõnumeid saata?", - "voice_broadcast": "Ringhäälingukõne", - "voice_broadcast_force_small_chunks": "Kasuta ringhäälingusõnumi puhul 15-sekundilist blokipikkust", "wysiwyg_composer": "Kujundatud teksti toimeti" }, "labs_mjolnir": { @@ -1556,7 +1543,6 @@ "mute_description": "Sa ei saa üldse teavitusi" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s alustas ringhäälingukõnet", "m.key.verification.request": "%(name)s soovib verifitseerimist" }, "onboarding": { @@ -2112,7 +2098,6 @@ "error_unbanning": "Ligipääsu taastamine ei õnnestunud", "events_default": "Saada sõnumeid", "invite": "Kutsu kasutajaid", - "io.element.voice_broadcast_info": "Ringhäälingukõned", "kick": "Eemalda kasutajaid", "m.call": "Alusta helistamist %(brand)s abil", "m.call.member": "Liitu %(brand)s kõnedega", @@ -2744,7 +2729,6 @@ "warning": "HOIATUS: " }, "share": { - "link_title": "Link jututoale", "permalink_message": "Viide valitud sõnumile", "permalink_most_recent": "Viide kõige viimasele sõnumile", "title_message": "Jaga jututoa sõnumit", @@ -2831,13 +2815,6 @@ "upgraderoom": "Uuendab jututoa uue versioonini", "upgraderoom_permission_error": "Sul ei ole piisavalt õigusi selle käsu käivitamiseks.", "usage": "Kasutus", - "verify": "Verifitseerib kasutaja, sessiooni ja avalikud võtmed", - "verify_mismatch": "HOIATUS: VÕTMETE VERIFITSEERIMINE EI ÕNNESTUNUD! Kasutaja %(userId)s ja sessiooni %(deviceId)s allkirjastamise võti on „%(fprint)s“, aga see ei vasta antud sõrmejäljele „%(fingerprint)s“. See võib tähendada, et sinu kasutatavad ühendused võivad olla kolmanda osapoole poolt vahelt lõigatud!", - "verify_nop": "Sessioon on juba verifitseeritud!", - "verify_nop_warning_mismatch": "HOIATUS: Sessioon on juba verifitseeritud, aga võtmed ei klapi!", - "verify_success_description": "Sinu antud allkirjavõti vastab allkirjavõtmele, mille sa said kasutaja %(userId)s sessioonist %(deviceId)s. Sessioon on märgitud verifitseerituks.", - "verify_success_title": "Verifitseeritud võti", - "verify_unknown_pair": "Tundmatu kasutaja ja sessiooni kombinatsioon: (%(userId)s, %(deviceId)s)", "view": "Vaata sellise aadressiga jututuba", "whois": "Näitab teavet kasutaja kohta" }, @@ -3049,10 +3026,6 @@ "error_rendering_message": "Selle sõnumi laadimine ei õnnestu", "historical_messages_unavailable": "Sa ei saa näha varasemaid sõnumeid", "in_room_name": " %(room)s jututoas", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s lõpetas ringhäälingukõne", - "you": "Sa lõpetasid ringhäälingukõne" - }, "io.element.widgets.layout": "%(senderName)s on uuendanud jututoa välimust", "load_error": { "no_permission": "Üritasin laadida teatud hetke selle jututoa ajajoonelt, kuid sul ei ole õigusi selle sõnumi nägemiseks.", @@ -3580,38 +3553,6 @@ "switch_theme_dark": "Kasuta tumedat teemat", "switch_theme_light": "Kasuta heledat teemat" }, - "voice_broadcast": { - "30s_backward": "30s tagasi", - "30s_forward": "30s edasi", - "action": "Ringhäälingukõne", - "buffering": "Andmed on puhverdamisel…", - "confirm_listen_affirm": "Jah, lõpeta salvestamine", - "confirm_listen_description": "Kui hakkad kuulama seda ringhäälingukõnet, siis hetkel toimuv ringhäälingukõne salvestamine lõppeb.", - "confirm_listen_title": "Kas soovid kuulata ringhäälingukõnet?", - "confirm_stop_affirm": "Jah, lõpeta", - "confirm_stop_description": "Kas sa oled kindel, et soovid otseeetri lõpetada? Sellega ringhäälingukõne salvestamine lõppeb ja salvestis on kättesaadav kõigile jututoas.", - "confirm_stop_title": "Kas lõpetame otseeetri?", - "connection_error": "Viga võrguühenduses - salvestamine on peatatud", - "failed_already_recording_description": "Sa juba salvestad ringhäälingukõnet. Uue alustamiseks palun lõpeta eelmine salvestus.", - "failed_already_recording_title": "Uue ringhäälingukõne alustamine pole võimalik", - "failed_decrypt": "Ringhäälingukõne dekrüptimine ei õnnestu", - "failed_generic": "Selle ringhäälingukõne esitamine ei õnnestu", - "failed_insufficient_permission_description": "Sul pole piisavalt õigusi selles jututoas ringhäälingukõne algatamiseks. Õiguste lisamiseks palun võta ühendust jututoa haldajaga.", - "failed_insufficient_permission_title": "Uue ringhäälingukõne alustamine pole võimalik", - "failed_no_connection_description": "Kahjuks me ei saa hetkel salvestamist alustada. Palun proovi hiljem uuesti.", - "failed_no_connection_title": "Ühenduse viga", - "failed_others_already_recording_description": "Keegi juba salvestab ringhäälingukõnet. Uue ringhäälingukõne salvestamiseks palun oota, kuni see teine ringhäälingukõne on lõppenud.", - "failed_others_already_recording_title": "Uue ringhäälingukõne alustamine pole võimalik", - "go_live": "Alusta otseeetrit", - "live": "Otseeeter", - "pause": "peata ringhäälingukõne", - "play": "esita ringhäälingukõnet", - "resume": "jätka ringhäälingukõnet" - }, - "voice_message": { - "cant_start_broadcast_description": "Kuna sa hetkel salvestad ringhäälingukõnet, siis häälsõnumi salvestamine või esitamine ei õnnestu. Selleks palun lõpeta ringhäälingukõne.", - "cant_start_broadcast_title": "Häälsõnumi salvestamine või esitamine ei õnnestu" - }, "voip": { "already_in_call": "Kõne on juba pooleli", "already_in_call_person": "Sinul juba kõne käsil selle osapoolega.", @@ -3631,7 +3572,6 @@ "camera_disabled": "Sinu seadme kaamera on välja lülitatud", "camera_enabled": "Sinu seadme kaamera on jätkuvalt kasutusel", "cannot_call_yourself_description": "Sa ei saa iseendale helistada.", - "change_input_device": "Vaheta sisendseadet", "connecting": "Kõne on ühendamisel", "connection_lost": "Ühendus sinu serveriga on katkenud", "connection_lost_description": "Kui ühendus sinu serveriga on katkenud, siis sa ei saa helistada.", @@ -3648,8 +3588,6 @@ "enable_camera": "Lülita kaamera sisse", "enable_microphone": "Eemalda mikrofoni summutamine", "expand": "Pöördu tagasi kõne juurde", - "failed_call_live_broadcast_description": "Kuna sa hetkel salvestad ringhäälingukõnet, siis tavakõne algatamine ei õnnestu. Kõne alustamiseks palun lõpeta ringhäälingukõne.", - "failed_call_live_broadcast_title": "Kõne algatamine ei õnnestu", "hangup": "Katkesta kõne", "hide_sidebar_button": "Peida külgpaan", "input_devices": "Sisendseadmed", diff --git a/src/i18n/strings/fa.json b/src/i18n/strings/fa.json index 5541bbbfbd0..4fc031ac43d 100644 --- a/src/i18n/strings/fa.json +++ b/src/i18n/strings/fa.json @@ -654,12 +654,6 @@ "incoming_sas_dialog_title": "درخواست تأیید دریافتی", "incoming_sas_user_dialog_text_1": "این کاربر را تأیید کنید تا به عنوان کاربر مورد اعتماد علامت‌گذاری شود. اعتماد به کاربران آرامش و اطمینان بیشتری به شما در استفاده از رمزنگاری سرتاسر می‌دهد.", "incoming_sas_user_dialog_text_2": "با تأیید این کاربر ، نشست وی به عنوان مورد اعتماد علامت‌گذاری شده و همچنین نشست شما به عنوان مورد اعتماد برای وی علامت‌گذاری خواهد شد.", - "manual_device_verification_device_id_label": "شناسه‌ی نشست", - "manual_device_verification_device_key_label": "کلید نشست", - "manual_device_verification_device_name_label": "نام نشست", - "manual_device_verification_footer": "اگر آنها مطابقت نداشته‌باشند ، ممکن است امنیت ارتباطات شما به خطر افتاده باشد.", - "manual_device_verification_self_text": "از طریق مقایسه‌ی این با تنظیمات کاربری در نشست‌های دیگرتان، تائيد کنید:", - "manual_device_verification_user_text": "این نشست کاربر را از طریق مقایسه‌ی این با تنظیمات کاربری تائيد کنید:", "other_party_cancelled": "طرف مقابل فرآیند تائید را لغو کرد.", "prompt_encrypted": "برای اطمینان از امنیت اتاق، هویت همه‌ی کاربران حاضر در اتاق را تأیید کنید.", "prompt_self": "از اعلان دوباره تأیید را شروع کنید.", @@ -981,8 +975,7 @@ "latex_maths": "نمایش لاتکس ریاضیات در پیام‌ها", "leave_beta": "ترک نسخه‌ی بتا", "video_rooms": "اتاق های تصویری", - "video_rooms_a_new_way_to_chat": "راهکار جدیدی برای گفتگوی صوتی و تصویری در%(brand)sوجود دارد.", - "voice_broadcast": "صدای جمعی" + "video_rooms_a_new_way_to_chat": "راهکار جدیدی برای گفتگوی صوتی و تصویری در%(brand)sوجود دارد." }, "labs_mjolnir": { "advanced_warning": "⚠ این تنظیمات برای کاربران حرفه‌ای قرار داده شده‌است.", @@ -1073,7 +1066,6 @@ "mentions_keywords": "منشن ها و کلمات کلیدی" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s یک پخش صوتی را شروع کرد", "m.key.verification.request": "%(name)s درخواست تائید دارد" }, "onboarding": { @@ -1806,12 +1798,6 @@ "upgraderoom": "یک اتاق را به نسخه جدید ارتقا دهید", "upgraderoom_permission_error": "شما مجوزهای لازم را برای استفاده از این دستور ندارید.", "usage": "استفاده", - "verify": "یک کاربر، نشست و عبارت کلید عمومی را تائید می‌کند", - "verify_mismatch": "هشدار: تایید کلید ناموفق بود! کلید امضا کننده %(userId)s در نشست %(deviceId)s برابر %(fprint)s است که با کلید %(fingerprint)s تطابق ندارد. این می تواند به معنی رهگیری ارتباطات شما باشد!", - "verify_nop": "نشست پیش از این تائید شده‌است!", - "verify_success_description": "کلید امضای ارائه شده با کلید امضای دریافت شده از جلسه %(deviceId)s کاربر %(userId)s مطابقت دارد. نشست به عنوان تأیید شده علامت گذاری شد.", - "verify_success_title": "کلید تأیید شده", - "verify_unknown_pair": "دوتایی (کاربر و نشست) ناشناخته : ( %(userId)sو%(deviceId)s )", "whois": "اطلاعات مربوط به کاربر را نمایش می دهد" }, "space": { @@ -2312,22 +2298,6 @@ "switch_theme_dark": "انتخاب حالت تاریک", "switch_theme_light": "انتخاب حالت روشن" }, - "voice_broadcast": { - "action": "صدای جمعی", - "confirm_stop_affirm": "بله، توقف ارسال جمعی", - "confirm_stop_title": "آیا ارسال جمعی زنده متوقف شود؟", - "failed_already_recording_description": "شما در حال ضبط یک صدا برای ارسال جمعی هستید. برای تولید یک صدای جمعی دیگر ضبط فعلی را متوقف نمایید.", - "failed_already_recording_title": "امکان ارسال یک صدای جدید به صورت جمعی نیست", - "failed_insufficient_permission_description": "شما دسترسی لازم برای ارسال صدای جمعی در این اتاق را ندارید. لطفا با مدیر اتاق تماس بگیرید.", - "failed_insufficient_permission_title": "امکان ارسال یک صدای جدید به صورت جمعی نیست", - "failed_others_already_recording_description": "شخص دیگری در حال ضبط صدا برای ارسال جمعی است. برای ارسال صدای جمعی باید منتظر بمانید تا کار ایشان به پایان برسد.", - "failed_others_already_recording_title": "امکان ارسال یک صدای جدید به صورت جمعی نیست", - "go_live": "برو به زنده", - "live": "زنده", - "pause": "توقف صدای جمعی", - "play": "پخش صدای جمعی", - "resume": "بازگشت به صدای جمعی" - }, "voip": { "already_in_call": "هم‌اکنون در تماس هستید", "already_in_call_person": "شما هم‌اکنون با این فرد در تماس هستید.", diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index 091761af4b8..48fc132f002 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -778,7 +778,6 @@ }, "udd": { "interactive_verification_button": "Vahvista vuorovaikutteisesti emojilla", - "manual_verification_button": "Vahvista manuaalisesti tekstillä", "other_ask_verify_text": "Pyydä tätä käyttäjää vahvistamaan istuntonsa, tai vahvista se manuaalisesti alla.", "other_new_session_text": "%(name)s (%(userId)s) kirjautui uudella istunnolla varmentamatta sitä:", "own_ask_verify_text": "Varmenna toinen istuntosi käyttämällä yhtä seuraavista tavoista.", @@ -807,10 +806,6 @@ "incoming_sas_dialog_title": "Saapuva varmennuspyyntö", "incoming_sas_user_dialog_text_1": "Varmenna tämä käyttäjä merkitäksesi hänet luotetuksi. Käyttäjiin luottaminen antaa sinulle ylimääräistä mielenrauhaa käyttäessäsi päästä päähän -salausta.", "incoming_sas_user_dialog_text_2": "Tämän käyttäjän varmentaminen merkitsee hänen istuntonsa luotetuksi, ja myös merkkaa sinun istuntosi luotetuksi hänen laitteissaan.", - "manual_device_verification_device_id_label": "Istuntotunniste", - "manual_device_verification_device_key_label": "Istunnon tunnus", - "manual_device_verification_device_name_label": "Istunnon nimi", - "manual_device_verification_footer": "Jos ne eivät täsmää, viestinnän turvallisuus saattaa olla vaarantunut.", "other_party_cancelled": "Toinen osapuoli perui varmennuksen.", "prompt_encrypted": "Varmenna kaikki huoneen käyttäjät varmistaaksesi, että se on turvallinen.", "prompt_self": "Aloita varmennus uudelleen ilmoituksesta.", @@ -917,10 +912,6 @@ }, "error_user_not_logged_in": "Käyttäjä ei ole sisäänkirjautunut", "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s lopetti äänen yleislähetyksen", - "you": "Lopetit äänen yleislähetyksen" - }, "m.call.answer": { "dm": "Puhelu käynnissä", "user": "%(senderName)s liittyi puheluun", @@ -1239,8 +1230,7 @@ "video_rooms_faq1_answer": "Käytä ”+”-painiketta vasemman paneelin huoneosiossa.", "video_rooms_faq1_question": "Miten voin luoda videohuoneen?", "video_rooms_faq2_answer": "Kyllä, keskustelun aikajana esitetään videon yhteydessä.", - "video_rooms_faq2_question": "Voinko käyttää tekstikeskustelua videopuhelussa?", - "voice_broadcast": "Äänen yleislähetys" + "video_rooms_faq2_question": "Voinko käyttää tekstikeskustelua videopuhelussa?" }, "labs_mjolnir": { "advanced_warning": "⚠ Nämä asetukset on tarkoitettu edistyneille käyttäjille.", @@ -1356,7 +1346,6 @@ "mute_description": "Et saa ilmoituksia" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s aloitti äänen yleislähetyksen", "m.key.verification.request": "%(name)s pyytää varmennusta" }, "onboarding": { @@ -1856,7 +1845,6 @@ "error_unbanning": "Porttikiellon poistaminen epäonnistui", "events_default": "Lähetä viestejä", "invite": "Kutsu käyttäjiä", - "io.element.voice_broadcast_info": "Äänen yleislähetykset", "kick": "Poista käyttäjiä", "m.call": "Aloita %(brand)s-puheluja", "m.call.member": "Liity %(brand)s-puheluihin", @@ -2415,7 +2403,6 @@ "warning": "VAROITUS: " }, "share": { - "link_title": "Linkitä huoneeseen", "permalink_message": "Linkitä valittuun viestiin", "permalink_most_recent": "Linkitä viimeisimpään viestiin", "title_message": "Jaa huoneviesti", @@ -2499,13 +2486,6 @@ "upgraderoom": "Päivittää huoneen uuteen versioon", "upgraderoom_permission_error": "Sinulla ei ole vaadittavia oikeuksia tämän komennon käyttämiseksi.", "usage": "Käyttö", - "verify": "Varmentaa käyttäjän, istunnon ja julkiset avaimet", - "verify_mismatch": "VAROITUS: AVAIMEN VARMENTAMINEN EPÄONNISTUI! Käyttäjän %(userId)s ja laitteen %(deviceId)s istunnon allekirjoitusavain on ”%(fprint)s”, mikä ei täsmää annettuun avaimeen ”%(fingerprint)s”. Tämä voi tarkoittaa, että viestintäänne siepataan!", - "verify_nop": "Istunto on jo vahvistettu!", - "verify_nop_warning_mismatch": "VAROITUS: istunto on jo vahvistettu, mutta avaimet EIVÄT TÄSMÄÄ!", - "verify_success_description": "Antamasi allekirjoitusavain täsmää käyttäjältä %(userId)s saamaasi istunnon %(deviceId)s allekirjoitusavaimeen. Istunto on varmennettu.", - "verify_success_title": "Varmennettu avain", - "verify_unknown_pair": "Tuntematon (käyttäjä, laite) (%(userId)s, %(deviceId)s)", "whois": "Näyttää tietoa käyttäjästä" }, "space": { @@ -2710,10 +2690,6 @@ "error_rendering_message": "Tätä viestiä ei voi ladata", "historical_messages_unavailable": "Et voi nähdä aiempia viestejä", "in_room_name": " huoneessa %(room)s", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s lopetti äänen yleislähetyksen", - "you": "Lopetit äänen yleislähetyksen" - }, "io.element.widgets.layout": "%(senderName)s on päivittänyt huoneen asettelun", "load_error": { "no_permission": "Aikajanan tietty hetki yritettiin ladata, mutta sinulla ei ole oikeutta nähdä kyseistä viestiä.", @@ -3202,23 +3178,6 @@ "switch_theme_dark": "Vaihda tummaan teemaan", "switch_theme_light": "Vaihda vaaleaan teemaan" }, - "voice_broadcast": { - "30s_backward": "30 s taaksepäin", - "30s_forward": "30 s eteenpäin", - "action": "Äänen yleislähetys", - "buffering": "Puskuroidaan…", - "confirm_stop_affirm": "Kyllä, pysäytä yleislähetys", - "confirm_stop_title": "Pysäytetäänkö liveyleislähetys?", - "failed_already_recording_description": "Tallennat jo äänen yleislähetystä. Lopeta nykyinen äänen yleislähetys aloittaaksesi uuden.", - "failed_already_recording_title": "Uutta äänen yleislähetystä ei voi käynnistää", - "failed_insufficient_permission_title": "Uutta äänen yleislähetystä ei voi käynnistää", - "failed_no_connection_title": "Yhteysvirhe", - "failed_others_already_recording_description": "Joku toinen tallentaa jo äänen yleislähetystä. Odota äänen yleislähetyksen päättymistä, jotta voit aloittaa uuden.", - "failed_others_already_recording_title": "Uutta äänen yleislähetystä ei voi käynnistää", - "pause": "keskeytä äänen yleislähetys", - "play": "toista äänen yleislähetys", - "resume": "palaa äänen yleislähetykseen" - }, "voip": { "already_in_call": "Olet jo puhelussa", "already_in_call_person": "Olet jo puhelussa tämän henkilön kanssa.", @@ -3238,7 +3197,6 @@ "camera_disabled": "Kamerasi on pois päältä", "camera_enabled": "Kamerasi on edelleen päällä", "cannot_call_yourself_description": "Et voi soittaa itsellesi.", - "change_input_device": "Vaihda sisääntulolaitetta", "connecting": "Yhdistetään", "connection_lost": "Yhteys palvelimeen on katkennut", "connection_lost_description": "Et voi soittaa puheluja ilman yhteyttä palvelimeen.", @@ -3254,7 +3212,6 @@ "enable_camera": "Laita kamera päälle", "enable_microphone": "Poista mikrofonin mykistys", "expand": "Palaa puheluun", - "failed_call_live_broadcast_title": "Puhelua ei voi aloittaa", "hangup": "Lopeta", "hide_sidebar_button": "Piilota sivupalkki", "input_devices": "Sisääntulolaitteet", diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 7d209a8a45b..08348ba29d6 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -229,6 +229,7 @@ }, "misconfigured_body": "Demandez à votre administrateur %(brand)s de vérifier que votre configuration ne contient pas d’entrées incorrectes ou en double.", "misconfigured_title": "Votre %(brand)s est mal configuré", + "mobile_create_account_title": "Vous êtes sur le point de créer un compte sur %(hsName)s", "msisdn_field_description": "D’autres utilisateurs peuvent vous inviter à des salons grâce à vos informations de contact", "msisdn_field_label": "Numéro de téléphone", "msisdn_field_number_invalid": "Ce numéro de téléphone ne semble pas correct, merci de vérifier et réessayer", @@ -279,6 +280,8 @@ "security_code": "Code de sécurité", "security_code_prompt": "Si vous y êtes invité, saisissez le code ci-dessous sur votre autre appareil.", "select_qr_code": "Sélectionnez « %(scanQRCode)s »", + "unsupported_explainer": "Votre fournisseur de compte ne prend pas en charge la connexion à un nouvel appareil à l’aide d’un code QR.", + "unsupported_heading": "Le code QR n'est pas pris en charge", "waiting_for_device": "En attente de connexion de l’appareil" }, "register_action": "Créer un compte", @@ -367,6 +370,8 @@ "email_resend_prompt": "Vous ne l’avez pas reçu ? Le renvoyer", "email_resent": "Ré-envoyé !", "fallback_button": "Commencer l’authentification", + "mas_cross_signing_reset_cta": "Accédez à votre compte", + "mas_cross_signing_reset_description": "Réinitialisez votre identité par l’intermédiaire de votre fournisseur de compte, puis revenez et cliquez sur « Réessayer ».", "msisdn": "Un message a été envoyé à %(msisdn)s", "msisdn_token_incorrect": "Jeton incorrect", "msisdn_token_prompt": "Merci de saisir le code qu’il contient :", @@ -499,6 +504,7 @@ "matrix": "Matrix", "message": "Message", "message_layout": "Mise en page des messages", + "message_timestamp_invalid": "Horodatage non valide", "microphone": "Micro", "model": "Modèle", "modern": "Moderne", @@ -899,6 +905,8 @@ "warning": "Si vous n’avez pas activé de nouvelle méthode de récupération, un attaquant essaye peut-être d’accéder à votre compte. Changez immédiatement le mot de passe de votre compte et configurez une nouvelle méthode de récupération dans les paramètres." }, "not_supported": "", + "pinned_identity_changed": "%(displayName)sL'identité de (%(userId)s) semble avoir changé. En savoir plus ", + "pinned_identity_changed_no_displayname": "%(userId)ssemble avoir changé d'identité. En savoir plus", "recovery_method_removed": { "description_1": "Cette session a détecté que votre phrase secrète et clé de sécurité pour les messages sécurisés ont été supprimées.", "description_2": "Si vous l’avez fait accidentellement, vous pouvez configurer les messages sécurisés sur cette session ce qui re-chiffrera l’historique des messages de cette session avec une nouvelle méthode de récupération.", @@ -914,7 +922,6 @@ }, "udd": { "interactive_verification_button": "Vérifier de façon interactive avec des émojis", - "manual_verification_button": "Vérifier manuellement avec un texte", "other_ask_verify_text": "Demandez à cet utilisateur de vérifier sa session, ou vérifiez-la manuellement ci-dessous.", "other_new_session_text": "%(name)s (%(userId)s) s’est connecté à une nouvelle session sans la vérifier :", "own_ask_verify_text": "Vérifiez votre autre session en utilisant une des options ci-dessous.", @@ -949,12 +956,6 @@ "incoming_sas_dialog_waiting": "Attente de la confirmation du partenaire…", "incoming_sas_user_dialog_text_1": "Vérifier cet utilisateur pour le marquer comme fiable. Faire confiance aux utilisateurs vous permet d’être tranquille lorsque vous utilisez des messages chiffrés de bout en bout.", "incoming_sas_user_dialog_text_2": "Vérifier cet utilisateur marquera sa session comme fiable, et marquera aussi votre session comme fiable pour lui.", - "manual_device_verification_device_id_label": "Identifiant de session", - "manual_device_verification_device_key_label": "Clé de la session", - "manual_device_verification_device_name_label": "Nom de la session", - "manual_device_verification_footer": "S’ils ne correspondent pas, la sécurité de vos communications est peut-être compromise.", - "manual_device_verification_self_text": "Confirmez en comparant ceci avec les paramètres utilisateurs de votre autre session :", - "manual_device_verification_user_text": "Confirmez la session de cet utilisateur en comparant ceci avec ses paramètres utilisateur :", "no_key_or_device": "Il semblerait que vous n’avez pas de clé de sécurité ou d’autres appareils pour faire la vérification. Cet appareil ne pourra pas accéder aux anciens messages chiffrés. Afin de vérifier votre identité sur cet appareil, vous devrez réinitialiser vos clés de vérifications.", "no_support_qr_emoji": "L’appareil que vous essayez de vérifier ne prend pas en charge les QR codes ou la vérification d’émojis, qui sont les méthodes prises en charge par %(brand)s. Essayez avec un autre client.", "other_party_cancelled": "L’autre personne a annulé la vérification.", @@ -994,7 +995,7 @@ "unverified_sessions_toast_description": "Vérifiez pour assurer la sécurité de votre compte", "unverified_sessions_toast_reject": "Plus tard", "unverified_sessions_toast_title": "Vous avez des sessions non vérifiées", - "verification_description": "Vérifiez votre identité pour accéder aux messages chiffrés et prouver votre identité aux autres.", + "verification_description": "Vérifiez votre identité pour accéder aux messages chiffrés et prouver votre identité aux autres. Si vous utilisez également un appareil mobile, veuillez ouvrir l’application avant de continuer.", "verification_dialog_title_device": "Vérifier un autre appareil", "verification_dialog_title_user": "Demande de vérification", "verification_skip_warning": "Sans vérification, vous n’aurez pas accès à tous vos messages et vous n’apparaîtrez pas comme de confiance aux autres.", @@ -1079,10 +1080,6 @@ }, "error_user_not_logged_in": "L’utilisateur n’est pas identifié", "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s a terminé une diffusion audio", - "you": "Vous avez terminé une diffusion audio" - }, "m.call.answer": { "dm": "Appel en cours", "user": "%(senderName)s a rejoint l’appel", @@ -1104,7 +1101,15 @@ "you": "Vous avez réagi avec %(reaction)s à %(message)s" }, "m.sticker": "%(senderName)s : %(stickerName)s", - "m.text": "%(senderName)s : %(message)s" + "m.text": "%(senderName)s : %(message)s", + "prefix": { + "audio": "Audio", + "file": "Fichier", + "image": "Image", + "poll": "Sondage", + "video": "Vidéo" + }, + "preview": "%(prefix)s: %(preview)s" }, "export_chat": { "cancelled": "Export annulé", @@ -1227,7 +1232,19 @@ "other": "Dans %(spaceName)s et %(count)s autres espaces." }, "incompatible_browser": { - "title": "Navigateur non pris en charge" + "continue": "Continuez quand même", + "description": "%(brand)s utilise certaines fonctionnalités du navigateur qui ne sont pas disponibles dans votre navigateur actuel. %(detail)s", + "detail_can_continue": "Si vous continuez, certaines fonctionnalités pourraient cesser de fonctionner et vous risquez de perdre des données à l'avenir.", + "detail_no_continue": "Essayez de mettre à jour ce navigateur si vous n'utilisez pas la dernière version, puis réessayez.", + "learn_more": "En savoir plus", + "linux": "Linux", + "macos": "MAC", + "supported_browsers": "Pour une expérience optimale, utilisez Chrome, Firefox, Edge, ou Safari.", + "title": "Navigateur non pris en charge", + "use_desktop_heading": "Utilisez plutôt %(brand)s Desktop", + "use_mobile_heading": "Utilisez plutôt %(brand)s sur votre appareil mobile", + "use_mobile_heading_after_desktop": "Ou utilisez notre application mobile", + "windows": "Windows (%(bits)s-bit)" }, "info_tooltip_title": "Informations", "integration_manager": { @@ -1351,12 +1368,14 @@ "navigate_next_message_edit": "Aller vers le prochain message à modifier", "navigate_prev_history": "Salon ou espace précédemment visité", "navigate_prev_message_edit": "Allez vers le précédent message à modifier", + "next_landmark": "Aller au prochain point de repère", "next_room": "Prochain salon ou conversation privée", "next_unread_room": "Prochain salon ou conversation privée non lu", "number": "[numéro]", "open_user_settings": "Ouvrir les paramètres de l'utilisateur", "page_down": "Page Bas", "page_up": "Page Haut", + "prev_landmark": "Aller au point de repère précédent", "prev_room": "Précédent salon ou conversation privée", "prev_unread_room": "Précédent salon ou conversation privée non lu", "room_list_collapse_section": "Réduire la section de la liste des salons", @@ -1401,8 +1420,11 @@ "dynamic_room_predecessors": "Prédécesseurs de salon dynamique", "dynamic_room_predecessors_description": "Active MSC3946 (pour prendre en charge les archives de salon après création)", "element_call_video_rooms": "Salons vidéo Element Call", + "exclude_insecure_devices": "Exclure les appareils non sécurisés lors de l'envoi/de la réception de messages", + "exclude_insecure_devices_description": "Lorsque ce mode est activé, les messages chiffrés ne seront pas partagés avec des appareils non vérifiés et les messages provenant d'appareils non vérifiés seront affichés comme une erreur. Notez que si vous activez ce mode, il se peut que vous ne puissiez pas communiquer avec les utilisateurs qui n'ont pas vérifié leurs appareils.", "experimental_description": "Envie d’expériences ? Essayez nos dernières idées en développement. Ces fonctionnalités ne sont pas terminées ; elles peuvent changer, être instables, ou être complètement abandonnées. En savoir plus.", "experimental_section": "Avant-premières", + "extended_profiles_msc_support": "Nécessite que votre serveur prenne en charge MSC4133", "feature_disable_call_per_sender_encryption": "Désactiver le chiffrement de chaque expéditeur pour Element Call", "feature_wysiwyg_composer_description": "Utilise le texte formaté au lieu de Markdown dans le compositeur de message.", "group_calls": "Nouvelle expérience d’appel de groupe", @@ -1458,8 +1480,6 @@ "video_rooms_faq2_answer": "Oui, l’historique de conversation est affiché à côté de la vidéo.", "video_rooms_faq2_question": "Est-il possible d’utiliser les messages textuels en même temps que l’appel vidéo ?", "video_rooms_feedbackSubheading": "Merci d’essayer la version bêta. Veuillez l’utiliser au maximum pour que nous puissions l’améliorer.", - "voice_broadcast": "Diffusion audio", - "voice_broadcast_force_small_chunks": "Forcer la diffusion audio à utiliser des morceaux de 15s", "wysiwyg_composer": "Éditeur de texte formaté" }, "labs_mjolnir": { @@ -1605,7 +1625,6 @@ "mute_description": "Vous n’aurez aucune notification" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s a démarré une diffusion audio", "m.key.verification.request": "%(name)s demande une vérification" }, "onboarding": { @@ -1805,8 +1824,12 @@ "right_panel": { "add_integrations": "Ajouter des extensions", "add_topic": "Ajouter un sujet", + "extensions_button": "Extensions", + "extensions_empty_description": "Sélectionnez « %(addIntegrations)s » pour explorer et ajouter des extensions à ce salon", + "extensions_empty_title": "Augmentez votre productivité avec plus d’outils, de widgets et de bots", "files_button": "Fichiers", "pinned_messages": { + "empty_description": "Sélectionnez un message et choisissez « %(pinAction)s » pour l'inclure ici.", "empty_title": "Épingler des messages importants afin qu'ils puissent être facilement découverts", "header": { "one": "1 Message épinglé", @@ -1817,11 +1840,17 @@ }, "menu": "Ouvrir le menu", "release_announcement": { + "close": "Ok", + "description": "Retrouvez tous les messages épinglés ici. Survolez n'importe quel message et sélectionnez « Épingler » pour l'ajouter.", "title": "Tous les nouveaux messages épinglés" }, + "reply_thread": "Répondre à un un message de fil de discussion", "unpin_all": { - "button": "Désépingler tous les messages" - } + "button": "Désépingler tous les messages", + "content": "Assurez-vous que vous voulez vraiment supprimer tous les messages épinglés. Cette action ne peut pas être annulée.", + "title": "Désépingler tous les messages ?" + }, + "view": "Voir dans la discussion" }, "pinned_messages_button": "Messages épinglés", "poll": { @@ -1926,6 +1955,7 @@ }, "room_is_public": "Ce salon est public" }, + "header_avatar_open_settings_label": "Ouvrir les paramètres du salon", "header_face_pile_tooltip": "Personnes", "header_untrusted_label": "Non fiable", "inaccessible": "Ce salon ou cet espace n’est pas accessible en ce moment.", @@ -1996,8 +2026,13 @@ "not_found_title": "Ce salon ou cet espace n’existe pas.", "not_found_title_name": "%(roomName)s n’existe pas.", "peek_join_prompt": "Ceci est un aperçu de %(roomName)s. Voulez-vous rejoindre le salon ?", + "pinned_message_badge": "Message épinglé", "pinned_message_banner": { - "description": "Ce salon contient des messages épinglés. Cliquez pour les consulter." + "button_close_list": "Fermer la liste", + "button_view_all": "Voir tout", + "description": "Ce salon contient des messages épinglés. Cliquez pour les consulter.", + "go_to_message": "Afficher le message épinglé dans la discussion.", + "title": "%(index)s de %(length)s messages épinglés" }, "read_topic": "Cliquer pour lire le sujet", "rejecting": "Rejet de l’invitation…", @@ -2005,6 +2040,10 @@ "search": { "all_rooms_button": "Rechercher dans tous les salons", "placeholder": "Rechercher des messages…", + "summary": { + "one": "1 résultat trouvé pour «  »", + "other": "%(count)srésultats trouvés pour «  »" + }, "this_room_button": "Rechercher dans ce salon" }, "status_bar": { @@ -2140,6 +2179,8 @@ "error_deleting_alias_description": "Une erreur est survenue lors de la suppression de cette adresse. Elle n’existe peut-être plus ou une erreur temporaire est survenue.", "error_deleting_alias_description_forbidden": "Vous n’avez pas la permission de supprimer cette adresse.", "error_deleting_alias_title": "Erreur lors de la suppression de l’adresse", + "error_publishing": "Impossible de publier le salon", + "error_publishing_detail": "Une erreur s'est produite lors de la publication du salon", "error_save_space_settings": "Échec de l’enregistrement des paramètres.", "error_updating_alias_description": "Une erreur est survenue lors de la mise à jour des adresses alternatives du salon. Ce n’est peut-être pas permis par le serveur ou une défaillance temporaire est survenue.", "error_updating_canonical_alias_description": "Une erreur est survenue lors de la mise à jour de l’adresse principale de salon. Ce n’est peut-être pas autorisé par le serveur ou une erreur temporaire est survenue.", @@ -2197,7 +2238,6 @@ "error_unbanning": "Échec de la révocation du bannissement", "events_default": "Envoyer des messages", "invite": "Inviter des utilisateurs", - "io.element.voice_broadcast_info": "Diffusions audio", "kick": "Expulser des utilisateurs", "m.call": "Démarrer des appels %(brand)s", "m.call.member": "Rejoindre des appels %(brand)s", @@ -2376,16 +2416,25 @@ } }, "settings": { + "account": { + "dialog_title": "Paramètres : Compte", + "title": "Compte" + }, "all_rooms_home": "Afficher tous les salons dans Accueil", "all_rooms_home_description": "Tous les salons dans lesquels vous vous trouvez apparaîtront sur l’Accueil.", "always_show_message_timestamps": "Toujours afficher l’heure des messages", "appearance": { "bundled_emoji_font": "Utilise la police d’émoji interne", + "compact_layout": "Afficher le texte et les messages compacts", + "compact_layout_description": "La mise en page moderne doit être sélectionnée pour utiliser cette fonctionnalité.", "custom_font": "Utiliser une police du système", "custom_font_description": "Définissez le nom d’une police de caractères installée sur votre système et %(brand)s essaiera de l’utiliser.", "custom_font_name": "Nom de la police du système", "custom_font_size": "Utiliser une taille personnalisée", + "custom_theme_add": "Ajouter un thème personnalisé", + "custom_theme_downloading": "Téléchargement du thème personnalisé…", "custom_theme_error_downloading": "Erreur lors du téléchargement du thème", + "custom_theme_help": "Entrez l'URL du thème personnalisé que vous souhaitez appliquer.", "custom_theme_invalid": "Schéma du thème invalide.", "dialog_title": "Paramètres : Apparence", "font_size": "Taille de la police", @@ -2405,6 +2454,9 @@ "code_block_expand_default": "Développer les blocs de code par défaut", "code_block_line_numbers": "Afficher les numéros de ligne dans les blocs de code", "disable_historical_profile": "Afficher l’image de profil et le nom actuels des utilisateurs dans l’historique des messages", + "discovery": { + "title": "Comment vous trouver" + }, "emoji_autocomplete": "Activer la suggestion d’émojis lors de la saisie", "enable_markdown": "Activer Markdown", "enable_markdown_description": "Commencez les messages avec /plain pour les envoyer sans markdown.", @@ -2420,10 +2472,14 @@ "add_msisdn_dialog_title": "Ajouter un numéro de téléphone", "add_msisdn_instructions": "Un SMS a été envoyé à +%(msisdn)s. Saisissez le code de vérification qu’il contient.", "add_msisdn_misconfigured": "L’ajout / liaison avec le flux MSISDN est mal configuré", + "allow_spellcheck": "Autoriser la vérification orthographique", + "application_language": "Langue de l'application", "application_language_reload_hint": "L’application se rechargera après avoir sélectionné une autre langue", "avatar_remove_progress": "Suppression de l'image...", "avatar_save_progress": "Chargement de l'image...", + "avatar_upload_error_text": "Le format de fichier n'est pas pris en charge ou l'image est plus grande que%(size)s.", "avatar_upload_error_text_generic": "Le format de fichier n'est peut-être pas pris en charge.", + "avatar_upload_error_title": "L'image de l'avatar n'a pas pu être téléchargée", "confirm_adding_email_body": "Cliquez sur le bouton ci-dessous pour confirmer l’ajout de l’adresse e-mail.", "confirm_adding_email_title": "Confirmer l’ajout de l’adresse e-mail", "deactivate_confirm_body": "Voulez-vous vraiment désactiver votre compte ? Ceci est irréversible.", @@ -2443,6 +2499,7 @@ "discovery_email_verification_instructions": "Vérifiez le lien dans votre boîte de réception", "discovery_msisdn_empty": "Les options de découverte apparaîtront quand vous aurez ajouté un numéro de téléphone ci-dessus.", "discovery_needs_terms": "Acceptez les conditions de service du serveur d’identité (%(serverName)s) pour vous permettre d’être découvrable par votre adresse e-mail ou votre numéro de téléphone.", + "discovery_needs_terms_title": "Laissez les gens vous trouver", "display_name": "Nom d'affichage", "display_name_error": "Impossible de définir le nom d'affichage", "email_address_in_use": "Cette adresse e-mail est déjà utilisée", @@ -2479,10 +2536,13 @@ "password_change_section": "Définir un nouveau mot de passe de compte…", "password_change_success": "Votre mot de passe a été mis à jour.", "personal_info": "Informations personnelles", + "profile_subtitle": "Voici comment vous apparaissez aux autres utilisateurs de l'application.", "profile_subtitle_oidc": "Votre compte est géré séparément par un fournisseur d'identité et certaines de vos informations personnelles ne peuvent donc pas être modifiées ici.", "remove_email_prompt": "Supprimer %(email)s ?", "remove_msisdn_prompt": "Supprimer %(phone)s ?", "spell_check_locale_placeholder": "Choisir une langue", + "unable_to_load_emails": "Impossible de charger les adresses e-mail", + "unable_to_load_msisdns": "Impossible de charger les numéros de téléphone", "username": "Nom d’utilisateur" }, "image_thumbnails": "Afficher les aperçus/vignettes pour les images", @@ -2611,6 +2671,7 @@ "code_blocks_heading": "Blocs de code", "compact_modern": "Utiliser une mise en page « moderne » plus compacte", "composer_heading": "Compositeur", + "default_timezone": "Navigateur par défaut (%(timezone)s)", "dialog_title": "Paramètres : Préférences", "enable_hardware_acceleration": "Activer l’accélération matérielle", "enable_tray_icon": "Afficher l’icône dans la barre d’état et minimiser la fenêtre lors de la fermeture", @@ -2618,6 +2679,7 @@ "keyboard_view_shortcuts_button": "Pour voir tous les raccourcis claviers, cliquez ici.", "media_heading": "Images, GIF et vidéos", "presence_description": "Partager votre activité et votre statut avec les autres.", + "publish_timezone": "Publier le fuseau horaire sur le profil public", "rm_lifetime": "Durée de vie du repère de lecture (ms)", "rm_lifetime_offscreen": "Durée de vie du repère de lecture en dehors de l’écran (ms)", "room_directory_heading": "Répertoire des salons", @@ -2626,7 +2688,8 @@ "show_checklist_shortcuts": "Afficher le raccourci vers la liste de vérification de bienvenue au-dessus de la liste des salons", "show_polls_button": "Afficher le bouton des sondages", "surround_text": "Entourer le texte sélectionné lors de la saisie de certains caractères", - "time_heading": "Affichage de l’heure" + "time_heading": "Affichage de l’heure", + "user_timezone": "Définir le fuseau horaire" }, "prompt_invite": "Demander avant d’envoyer des invitations à des identifiants matrix potentiellement non valides", "replace_plain_emoji": "Remplacer automatiquement le texte par des émojis", @@ -2782,6 +2845,7 @@ "sign_in_with_qr": "Associer un nouvel appareil", "sign_in_with_qr_button": "Afficher le QR code", "sign_in_with_qr_description": "Utilisez un code QR pour vous connecter à un autre appareil et configurer votre messagerie sécurisée.", + "sign_in_with_qr_unsupported": "Non pris en charge par votre fournisseur de compte", "sign_out": "Se déconnecter de cette session", "sign_out_all_other_sessions": "Déconnecter toutes les autres sessions (%(otherSessionsCount)s)", "sign_out_confirm_description": { @@ -2872,7 +2936,7 @@ "warning": "ATTENTION : " }, "share": { - "link_title": "Lien vers le salon", + "link_copied": "Lien copié", "permalink_message": "Lien vers le message sélectionné", "permalink_most_recent": "Lien vers le message le plus récent", "share_call": "Lien d'invitation à la conférence", @@ -2964,13 +3028,6 @@ "upgraderoom": "Met à niveau un salon vers une nouvelle version", "upgraderoom_permission_error": "Vous n’avez pas les autorisations nécessaires pour utiliser cette commande.", "usage": "Utilisation", - "verify": "Vérifie un utilisateur, une session et une collection de clés publiques", - "verify_mismatch": "ATTENTION : ÉCHEC DE LA VÉRIFICATION DE CLÉ ! La clé de signature pour %(userId)s et la session %(deviceId)s est « %(fprint)s  ce qui ne correspond pas à la clé fournie « %(fingerprint)s ». Cela pourrait signifier que vos communications sont interceptées !", - "verify_nop": "Session déjà vérifiée !", - "verify_nop_warning_mismatch": "ATTENTION : session déjà vérifiée, mais les clés ne CORRESPONDENT PAS !", - "verify_success_description": "La clé de signature que vous avez fournie correspond à celle que vous avez reçue de la session %(deviceId)s de %(userId)s. Session marquée comme vérifiée.", - "verify_success_title": "Clé vérifiée", - "verify_unknown_pair": "Paire (utilisateur, session) inconnue : (%(userId)s, %(deviceId)s)", "view": "Affiche le salon avec cette adresse", "whois": "Affiche des informations à propos de l’utilisateur" }, @@ -3188,6 +3245,8 @@ "historical_event_no_key_backup": "L'historique des messages n'est pas disponible sur cet appareil", "historical_event_unverified_device": "Vous devez vérifier cet appareil pour accéder à l'historique des messages", "historical_event_user_not_joined": "Vous n'avez pas accès à ce message", + "sender_identity_previously_verified": "L'identité vérifiée de l'expéditeur a changé", + "sender_unsigned_device": "Envoyé depuis un appareil non sécurisé.", "unable_to_decrypt": "Impossible de déchiffrer le message" }, "disambiguated_profile": "%(displayName)s (%(matrixId)s)", @@ -3195,6 +3254,7 @@ "download_action_downloading": "Téléchargement en cours", "download_failed": "Échec du téléchargement", "download_failed_description": "Une erreur s'est produite lors du téléchargement de ce fichier", + "e2e_state": "État du chiffrement de bout en bout", "edits": { "tooltip_label": "Modifié le %(date)s. Cliquer pour voir les modifications.", "tooltip_sub": "Cliquez pour voir les modifications", @@ -3204,10 +3264,6 @@ "error_rendering_message": "Impossible de charger ce message", "historical_messages_unavailable": "Vous ne pouvez pas voir les messages plus anciens", "in_room_name": " dans %(room)s", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s a terminé une diffusion audio", - "you": "Vous avez terminé une diffusion audio" - }, "io.element.widgets.layout": "%(senderName)s a mis à jour la mise en page du salon", "late_event_separator": "Initialement envoyé%(dateTime)s", "load_error": { @@ -3439,7 +3495,8 @@ "reactions": { "add_reaction_prompt": "Ajouter une réaction", "custom_reaction_fallback_label": "Réaction personnalisée", - "label": "%(reactors)s ont réagi avec %(content)s" + "label": "%(reactors)s ont réagi avec %(content)s", + "tooltip_caption": "a réagi avec %(shortName)s" }, "read_receipt_title": { "one": "Vu par %(count)s personne", @@ -3624,6 +3681,10 @@ "truncated_list_n_more": { "other": "Et %(count)s autres…" }, + "unsupported_browser": { + "description": "Si vous continuez, certaines fonctionnalités risquent de cesser de fonctionner et vous risquez de perdre des données à l'avenir. Mettez à jour votre navigateur pour continuer à utiliser%(brand)s .", + "title": "%(brand)sne prend pas en charge ce navigateur" + }, "unsupported_server_description": "Ce serveur utilise une ancienne version de Matrix. Mettez-le à jour vers Matrix %(version)s pour utiliser %(brand)s sans erreurs.", "unsupported_server_title": "Votre serveur n’est pas pris en charge", "update": { @@ -3656,6 +3717,7 @@ "error_files_too_large": "Ces fichiers sont trop lourds pour être envoyés. La taille limite des fichiers est de %(limit)s.", "error_some_files_too_large": "Certains fichiers sont trop lourds pour être envoyés. La taille limite des fichiers est de %(limit)s.", "error_title": "Erreur d’envoi", + "not_image": "Le fichier que vous avez choisi n'est pas un fichier image valide.", "title": "Envoyer les fichiers", "title_progress": "Envoi des fichiers (%(current)s sur %(total)s)", "upload_all_button": "Tout envoyer", @@ -3682,6 +3744,7 @@ "deactivate_confirm_action": "Désactiver l’utilisateur", "deactivate_confirm_description": "Désactiver cet utilisateur le déconnectera et l’empêchera de se reconnecter. De plus, il quittera tous les salons qu’il a rejoints. Cette action ne peut pas être annulée. Voulez-vous vraiment désactiver cet utilisateur ?", "deactivate_confirm_title": "Désactiver l’utilisateur ?", + "dehydrated_device_enabled": "Appareil hors ligne activé", "demote_button": "Rétrograder", "demote_self_confirm_description_space": "Vous ne pourrez pas annuler ce changement puisque vous vous rétrogradez. Si vous êtes le dernier utilisateur a privilèges de cet espace, il deviendra impossible d’en reprendre contrôle.", "demote_self_confirm_room": "Vous ne pourrez pas annuler cette modification car vous vous rétrogradez. Si vous êtes le dernier utilisateur privilégié de ce salon, il sera impossible de récupérer les privilèges.", @@ -3698,6 +3761,7 @@ "error_revoke_3pid_invite_title": "Échec de la révocation de l’invitation", "hide_sessions": "Masquer les sessions", "hide_verified_sessions": "Masquer les sessions vérifiées", + "ignore_button": "Ignorer", "ignore_confirm_description": "Tous les messages et invitations de cette utilisateur seront cachés. Êtes-vous sûr de vouloir les ignorer ?", "ignore_confirm_title": "Ignorer %(user)s", "invited_by": "Invité par %(sender)s", @@ -3731,6 +3795,7 @@ "room_encrypted_detail": "Vos messages sont sécurisés et seuls vous et le destinataire avez les clés uniques pour les déchiffrer.", "room_unencrypted": "Les messages dans ce salon ne sont pas chiffrés de bout en bout.", "room_unencrypted_detail": "Dans les salons chiffrés, vos messages sont sécurisés et seuls vous et le destinataire avez les clés uniques pour les déchiffrer.", + "send_message": "Envoyer un message", "share_button": "Partager le profil", "unban_button_room": "Révoquer le bannissement du salon", "unban_button_space": "Révoquer le bannissement de l’espace", @@ -3738,6 +3803,7 @@ "unban_space_everything": "Annuler le bannissement de partout où j’ai le droit de le faire", "unban_space_specific": "Annuler le bannissement de certains endroits où j’ai le droit de le faire", "unban_space_warning": "Ils ne pourront plus accéder aux endroits dans lesquels vous n’êtes pas administrateur.", + "unignore_button": "Ne plus ignorer", "verify_button": "Vérifier l’utilisateur", "verify_explainer": "Pour une sécurité supplémentaire, vérifiez cet utilisateur en comparant un code à usage unique sur vos deux appareils." }, @@ -3747,38 +3813,6 @@ "switch_theme_dark": "Passer au mode sombre", "switch_theme_light": "Passer au mode clair" }, - "voice_broadcast": { - "30s_backward": "30s en arrière", - "30s_forward": "30s en avant", - "action": "Diffusion audio", - "buffering": "Mise en mémoire tampon…", - "confirm_listen_affirm": "Oui, terminer mon enregistrement", - "confirm_listen_description": "En commençant à écouter cette diffusion en direct, votre enregistrement de diffusion en direct actuel sera interrompu.", - "confirm_listen_title": "Écouter la diffusion en direct ?", - "confirm_stop_affirm": "Oui, arrêter la diffusion", - "confirm_stop_description": "Êtes-vous sûr de vouloir arrêter votre diffusion en direct ? Cela terminera la diffusion et l’enregistrement complet sera disponible dans le salon.", - "confirm_stop_title": "Arrêter la diffusion en direct ?", - "connection_error": "Erreur de connexion – Enregistrement en pause", - "failed_already_recording_description": "Vous êtes déjà en train de réaliser une diffusion audio. Veuillez terminer votre diffusion audio actuelle pour en démarrer une nouvelle.", - "failed_already_recording_title": "Impossible de commencer une nouvelle diffusion audio", - "failed_decrypt": "Impossible de décrypter la diffusion audio", - "failed_generic": "Impossible de lire cette diffusion audio", - "failed_insufficient_permission_description": "Vous n’avez pas les permissions requises pour démarrer une nouvelle diffusion audio dans ce salon. Contactez un administrateur du salon pour mettre-à-jour vos permissions.", - "failed_insufficient_permission_title": "Impossible de commencer une nouvelle diffusion audio", - "failed_no_connection_description": "Malheureusement, nous ne pouvons pas démarrer l’enregistrement pour le moment. Veuillez réessayer plus tard.", - "failed_no_connection_title": "Erreur de connexion", - "failed_others_already_recording_description": "Une autre personne est déjà en train de réaliser une diffusion audio. Attendez que sa diffusion audio soit terminée pour en démarrer une nouvelle.", - "failed_others_already_recording_title": "Impossible de commencer une nouvelle diffusion audio", - "go_live": "Passer en direct", - "live": "Direct", - "pause": "mettre en pause la diffusion audio", - "play": "lire la diffusion audio", - "resume": "continuer la diffusion audio" - }, - "voice_message": { - "cant_start_broadcast_description": "Vous ne pouvez pas commencer un message vocal car vous êtes en train d’enregistrer une diffusion en direct. Veuillez terminer cette diffusion pour commencer un message vocal.", - "cant_start_broadcast_title": "Impossible de commencer un message vocal" - }, "voip": { "already_in_call": "Déjà en cours d’appel", "already_in_call_person": "Vous êtes déjà en cours d’appel avec cette personne.", @@ -3798,7 +3832,6 @@ "camera_disabled": "Votre caméra est éteinte", "camera_enabled": "Votre caméra est toujours allumée", "cannot_call_yourself_description": "Vous ne pouvez pas passer d’appel avec vous-même.", - "change_input_device": "Change de périphérique d’entrée", "close_lobby": "Fermer la salle d'attente", "connecting": "Connexion", "connection_lost": "La connexion au serveur a été perdue", @@ -3817,8 +3850,6 @@ "enable_camera": "Activer la caméra", "enable_microphone": "Activer le microphone", "expand": "Revenir à l’appel", - "failed_call_live_broadcast_description": "Vous ne pouvez pas démarrer un appel car vous êtes en train d’enregistrer une diffusion en direct. Veuillez terminer cette diffusion pour démarrer un appel.", - "failed_call_live_broadcast_title": "Impossible de démarrer un appel", "get_call_link": "Partager le lien de l'appel", "hangup": "Raccrocher", "hide_sidebar_button": "Masquer la barre latérale", @@ -3826,6 +3857,7 @@ "jitsi_call": "Conférence Jitsi", "join_button_tooltip_call_full": "Désolé — Cet appel est actuellement complet", "join_button_tooltip_connecting": "Connexion", + "legacy_call": "Appel vidéo", "maximise": "Remplir l’écran", "maximise_call": "Plein écran", "metaspace_video_rooms": { diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index 00277a5f3e4..50c551d6a2d 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -749,7 +749,6 @@ }, "udd": { "interactive_verification_button": "Verificar interactivamente usando emoji", - "manual_verification_button": "Verificar manualmente con texto", "other_ask_verify_text": "Pídelle a usuaria que verifique a súa sesión, ou verificaa manualmente aquí.", "other_new_session_text": "%(name)s (%(userId)s) conectouse a unha nova sesión sen verificala:", "own_ask_verify_text": "Verifica a túa outra sesión usando unha das opcións inferiores.", @@ -781,12 +780,6 @@ "incoming_sas_dialog_title": "Solicitude entrante de verificación", "incoming_sas_user_dialog_text_1": "Verifica esta usuaria para marcala como confiable. Ao confiar nas usuarias proporcionache tranquilidade extra cando usas cifrado de extremo-a-extremo.", "incoming_sas_user_dialog_text_2": "Ao verificar esta usuaria marcarás a súa sesión como confiable, e tamén marcará a túa sesión como confiable para elas.", - "manual_device_verification_device_id_label": "ID de sesión", - "manual_device_verification_device_key_label": "Chave da sesión", - "manual_device_verification_device_name_label": "Nome da sesión", - "manual_device_verification_footer": "Se non concordan, a seguridade da comunicación podería estar comprometida.", - "manual_device_verification_self_text": "Corfirma comparando o seguinte cos Axustes de Usuaria na outra sesión:", - "manual_device_verification_user_text": "Confirma a sesión desta usuaria comparando o seguinte cos seus Axustes de Usuaria:", "no_key_or_device": "Semella que non tes unha Chave de Seguridade ou outros dispositivos cos que verificar. Este dispositivo non poderá acceder a mensaxes antigas cifradas. Para poder verificar a túa identidade neste dispositivo tes que restablecer as chaves de verificación.", "no_support_qr_emoji": "Este dispositivo que intentas verificar non ten soporte para código QR nin verificación por emoji, que é o que %(brand)s soporta. Inténtao cun cliente diferente.", "other_party_cancelled": "A outra parte cancelou a verificación.", @@ -1200,8 +1193,7 @@ "video_rooms_faq1_answer": "Usa o botón \"+\" na sección da sala do panel esquerdo.", "video_rooms_faq1_question": "Como creo unha sala de vídeo?", "video_rooms_faq2_answer": "Si, a cronoloxía de texto móstrase xunto co vídeo.", - "video_rooms_faq2_question": "Podo usar chat de texto xunto á chamada de vídeo?", - "voice_broadcast": "Emisión de voz" + "video_rooms_faq2_question": "Podo usar chat de texto xunto á chamada de vídeo?" }, "labs_mjolnir": { "advanced_warning": "⚠ Estos axustes van dirixidos a usuarias avanzadas.", @@ -1811,7 +1803,6 @@ "error_unbanning": "Fallou eliminar a prohibición", "events_default": "Enviar mensaxes", "invite": "Convidar usuarias", - "io.element.voice_broadcast_info": "Emisións de voz", "kick": "Eliminar usuarias", "m.reaction": "Enviar reaccións", "m.room.avatar": "Cambiar avatar da sala", @@ -2315,7 +2306,6 @@ "warn_quit": "Aviso antes de saír" }, "share": { - "link_title": "Ligazón á sala", "permalink_message": "Ligazón á mensaxe escollida", "permalink_most_recent": "Ligazón ás mensaxes máis recentes", "title_message": "Compartir unha mensaxe da sala", @@ -2397,12 +2387,6 @@ "upgraderoom": "Subir a sala de versión", "upgraderoom_permission_error": "Non tes os permisos suficientes para usar este comando.", "usage": "Uso", - "verify": "Verifica unha usuaria, sesión e chave pública", - "verify_mismatch": "AVISO: FALLOU A VERIFICACIÓN DAS CHAVES! A chave de firma para %(userId)s na sesión %(deviceId)s é \"%(fprint)s\" que non concordan coa chave proporcionada \"%(fingerprint)s\". Esto podería significar que as túas comunicacións foron interceptadas!", - "verify_nop": "A sesión xa está verificada!", - "verify_success_description": "A chave de firma proporcionada concorda coa chave de firma recibida desde a sesión %(deviceId)s de %(userId)s. Sesión marcada como verificada.", - "verify_success_title": "Chave verificada", - "verify_unknown_pair": "Parella (usuaria, sesión) descoñecida: (%(userId)s, %(deviceId)s)", "whois": "Mostra información acerca da usuaria" }, "space": { @@ -3102,9 +3086,6 @@ "switch_theme_dark": "Cambiar a decorado escuro", "switch_theme_light": "Cambiar a decorado claro" }, - "voice_broadcast": { - "action": "Emisión de voz" - }, "voip": { "already_in_call": "Xa estás nunha chamada", "already_in_call_person": "Xa estás nunha conversa con esta persoa.", diff --git a/src/i18n/strings/he.json b/src/i18n/strings/he.json index c98d59c6597..724e17efd91 100644 --- a/src/i18n/strings/he.json +++ b/src/i18n/strings/he.json @@ -662,12 +662,6 @@ "incoming_sas_dialog_title": "בקשת אימות נכנסת", "incoming_sas_user_dialog_text_1": "אמתו את המשתמש הזה כדי לסמן אותו כאמין. אמון במשתמשים מעניק לכם שקט נפשי נוסף בשימוש בהודעות מוצפנות מקצה לקצה.", "incoming_sas_user_dialog_text_2": "אימות משתמש זה יסמן את ההפעלה שלו כאמינה, וגם יסמן את ההפעלה שלכם כאמינה להם.", - "manual_device_verification_device_id_label": "זהות מושב", - "manual_device_verification_device_key_label": "מפתח מושב", - "manual_device_verification_device_name_label": "שם מושב", - "manual_device_verification_footer": "אם הם לא תואמים, אבטחת התקשורת שלך עלולה להיפגע.", - "manual_device_verification_self_text": "אשר על ידי השוואה בין הדברים הבאים להגדרות המשתמש בפגישה האחרת שלך:", - "manual_device_verification_user_text": "אשר את הפעלת המשתמש הזה על ידי השוואה בין הדברים הבאים להגדרות המשתמש שלהם:", "other_party_cancelled": "הצד השני ביטל את האימות.", "prompt_encrypted": "אמת את כל המשתמשים בחדר כדי לוודא שהוא מאובטח.", "prompt_self": "התחל אימות שוב מההודעה.", @@ -1454,7 +1448,6 @@ "error_unbanning": "שגיאה בהסרת חסימה", "events_default": "שלח הודעות", "invite": "הזמנת משתמשים", - "io.element.voice_broadcast_info": "שליחת הקלטות קוליות", "kick": "הסר משתמשים", "m.reaction": "שלח תגובות", "m.room.avatar": "שנה אווטר של החדר", @@ -1958,11 +1951,6 @@ "upgraderoom": "משדרג את החדר לגרסא חדשה", "upgraderoom_permission_error": "אין לכם הרשאות להשתמש בפקודה זו.", "usage": "שימוש", - "verify": "מוודא משתמש, התחברות וצמד מפתח ציבורי", - "verify_mismatch": "אזהרה: אימות מפתח נכשל! חתימת המפתח של %(userId)s ושל ההתחברות של מכשיר %(deviceId)s הינו \"%(fprint)s\" אשר אינו תואם למפתח הנתון \"%(fingerprint)s\". דבר זה יכול להעיר על כך שישנו נסיון להאזין לתקשורת שלכם!", - "verify_nop": "ההתחברות כבר אושרה!", - "verify_success_description": "המפתח החתום שנתתם תואם את המפתח שקבלתם מ %(userId)s מהתחברות %(deviceId)s. ההתחברות סומנה כמאושרת.", - "verify_success_title": "מפתח מאושר", "whois": "מציג מידע אודות משתמש" }, "space": { diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index cf410fb82f3..349c4a39cdb 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -872,7 +872,6 @@ }, "udd": { "interactive_verification_button": "Interaktív ellenőrzés emodzsikkal", - "manual_verification_button": "Kézi szöveges ellenőrzés", "other_ask_verify_text": "Kérje meg a felhasználót, hogy hitelesítse a munkamenetét, vagy ellenőrizze kézzel lentebb.", "other_new_session_text": "%(name)s (%(userId)s) új munkamenetbe lépett be, anélkül, hogy ellenőrizte volna:", "own_ask_verify_text": "Ellenőrizze a másik munkamenetét a lenti lehetőségek egyikével.", @@ -907,12 +906,6 @@ "incoming_sas_dialog_waiting": "Várakozás a partner megerősítésére…", "incoming_sas_user_dialog_text_1": "Ellenőrizd ezt a felhasználót, hogy megbízhatónak lehessen tekinteni. Megbízható felhasználók további nyugalmat jelenthetnek ha végpontól végpontig titkosítást használsz.", "incoming_sas_user_dialog_text_2": "A felhasználó ellenőrzése által az ő munkamenete megbízhatónak lesz jelölve, és a te munkameneted is megbízhatónak lesz jelölve nála.", - "manual_device_verification_device_id_label": "Kapcsolat azonosító", - "manual_device_verification_device_key_label": "Munkamenetkulcs", - "manual_device_verification_device_name_label": "Munkamenet neve", - "manual_device_verification_footer": "Ha nem egyeznek akkor a kommunikációtok biztonsága veszélyben lehet.", - "manual_device_verification_self_text": "Erősítsd meg a felhasználói beállítások összehasonlításával a többi munkamenetedben:", - "manual_device_verification_user_text": "Ezt a munkamenetet hitelesítsd az ő felhasználói beállításának az összehasonlításával:", "no_key_or_device": "Úgy tűnik, hogy nem rendelkezik biztonsági kulccsal, vagy másik eszközzel, amelyikkel ellenőrizhetné. Ezzel az eszközzel nem fér majd hozzá a régi titkosított üzenetekhez. Ahhoz, hogy a személyazonosságát ezen az eszközön ellenőrizni lehessen, az ellenőrzédi kulcsokat alaphelyzetbe kell állítani.", "no_support_qr_emoji": "Az ellenőrizni kívánt eszköz nem támogatja se a QR kód beolvasást se az emodzsi ellenőrzést, amit a %(brand)s támogat. Próbálja meg egy másik klienssel.", "other_party_cancelled": "A másik fél megszakította az ellenőrzést.", @@ -1029,10 +1022,6 @@ }, "error_user_not_logged_in": "A felhasználó nincs bejelentkezve", "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s befejezte a hangközvetítést", - "you": "A hangközvetítés befejeződött" - }, "m.call.answer": { "dm": "Folyamatban lévő hívás", "user": "%(senderName)s csatlakozott a hívásba", @@ -1392,8 +1381,6 @@ "video_rooms_faq1_question": "Hogy lehet videószobát készíteni?", "video_rooms_faq2_answer": "Igen, a szöveges idővonal a videóval együtt megjelenik.", "video_rooms_faq2_question": "Lehet a videóhívás közben szövegesen is csevegni?", - "voice_broadcast": "Hangközvetítés", - "voice_broadcast_force_small_chunks": "Hangközvetítések 15 másodperces darabolásának kényszerítése", "wysiwyg_composer": "Szövegszerkesztő használata" }, "labs_mjolnir": { @@ -1528,7 +1515,6 @@ "mute_description": "Nem kap semmilyen értesítést" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s hangos közvetítést indított", "m.key.verification.request": "%(name)s ellenőrzést kér" }, "onboarding": { @@ -2077,7 +2063,6 @@ "error_unbanning": "A kitiltás visszavonása sikertelen", "events_default": "Üzenetek küldése", "invite": "Felhasználók meghívása", - "io.element.voice_broadcast_info": "Hangközvetítés", "kick": "Felhasználók eltávolítása", "m.call": "%(brand)s hívás indítása", "m.call.member": "Csatlakozás ebbe a hívásba: %(brand)s", @@ -2711,7 +2696,6 @@ "warning": "FIGYELEM: " }, "share": { - "link_title": "Hivatkozás a szobához", "permalink_message": "Hivatkozás a kijelölt üzenethez", "permalink_most_recent": "Hivatkozás a legfrissebb üzenethez", "title_message": "Szoba üzenetének megosztása", @@ -2800,13 +2784,6 @@ "upgraderoom": "Új verzióra fejleszti a szobát", "upgraderoom_permission_error": "A parancs használatához nincs meg a megfelelő jogosultsága.", "usage": "Használat", - "verify": "Felhasználó, munkamenet és nyilvános kulcs hármas ellenőrzése", - "verify_mismatch": "FIGYELEM: A KULCSELLENŐRZÉS SIKERTELEN! %(userId)s aláírási kulcsa és a(z) %(deviceId)s munkamenet ujjlenyomata „%(fprint)s”, amely nem egyezik meg a megadott ujjlenyomattal: „%(fingerprint)s”. Ez azt is jelentheti, hogy a kommunikációt lehallgatják.", - "verify_nop": "A munkamenet már ellenőrzött.", - "verify_nop_warning_mismatch": "FIGYELEM: a munkamenet már ellenőrizve van, de a kulcsok NEM EGYEZNEK.", - "verify_success_description": "A megadott aláírási kulcs megegyezik %(userId)s felhasználótól kapott aláírási kulccsal ebben a munkamenetben: %(deviceId)s. A munkamenet ellenőrzöttnek lett jelölve.", - "verify_success_title": "Ellenőrzött kulcs", - "verify_unknown_pair": "Ismeretlen (felhasználó, munkamenet) páros: (%(userId)s, %(deviceId)s)", "view": "Megadott címmel rendelkező szobák megjelenítése", "whois": "Információt jelenít meg a felhasználóról" }, @@ -3018,10 +2995,6 @@ "error_rendering_message": "Ezt az üzenetet nem sikerült betölteni", "historical_messages_unavailable": "Nem tekintheted meg a régebbi üzeneteket", "in_room_name": " itt: %(room)s", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s befejezte a hangközvetítést", - "you": "Befejezte a hangközvetítést" - }, "io.element.widgets.layout": "%(senderName)s frissítette a szoba kinézetét", "load_error": { "no_permission": "Megpróbálta betölteni a szoba megadott időpontjának megfelelő adatait, de nincs joga a kérdéses üzenetek megjelenítéséhez.", @@ -3551,38 +3524,6 @@ "switch_theme_dark": "Sötét módra váltás", "switch_theme_light": "Világos módra váltás" }, - "voice_broadcast": { - "30s_backward": "vissza 30 másodperccel", - "30s_forward": "előre 30 másodperccel", - "action": "Hangközvetítés", - "buffering": "Pufferelés…", - "confirm_listen_affirm": "Igen, a felvétel befejezése", - "confirm_listen_description": "Ha hallgatja ezt az élő közvetítést, akkor a jelenlegi élő közvetítésének a felvétele befejeződik.", - "confirm_listen_title": "Élő közvetítés hallgatása?", - "confirm_stop_affirm": "Igen, közvetítés megállítása", - "confirm_stop_description": "Biztos, hogy befejezi az élő közvetítést? Ez megállítja a közvetítést és a felvétel az egész szoba számára elérhető lesz.", - "confirm_stop_title": "Megszakítja az élő közvetítést?", - "connection_error": "Kapcsolódási hiba – Felvétel szüneteltetve", - "failed_already_recording_description": "Egy hangközvetítés már folyamatban van. Először fejezze be a jelenlegi közvetítést egy új indításához.", - "failed_already_recording_title": "Az új hangközvetítés nem indítható el", - "failed_decrypt": "A hangközvetítést nem lehet visszafejteni", - "failed_generic": "A hangközvetítés nem játszható le", - "failed_insufficient_permission_description": "Nincs jogosultsága hangközvetítést indítani ebben a szobában. Vegye fel a kapcsolatot a szoba adminisztrátorával a szükséges jogosultság megszerzéséhez.", - "failed_insufficient_permission_title": "Az új hangközvetítés nem indítható el", - "failed_no_connection_description": "Sajnos most nem lehet elindítani a felvételt. Próbálja meg később.", - "failed_no_connection_title": "Kapcsolati hiba", - "failed_others_already_recording_description": "Valaki már elindított egy hangközvetítést. Várja meg a közvetítés végét az új indításához.", - "failed_others_already_recording_title": "Az új hangközvetítés nem indítható el", - "go_live": "Élő közvetítés indítása", - "live": "Élő közvetítés", - "pause": "hangközvetítés szüneteltetése", - "play": "hangközvetítés lejátszása", - "resume": "hangközvetítés folytatása" - }, - "voice_message": { - "cant_start_broadcast_description": "Nem lehet hang üzenetet indítani élő közvetítés felvétele közben. Az élő közvetítés bejezése szükséges a hang üzenet indításához.", - "cant_start_broadcast_title": "Hang üzenetet nem lehet elindítani" - }, "voip": { "already_in_call": "A hívás már folyamatban van", "already_in_call_person": "Már hívásban van ezzel a személlyel.", @@ -3602,7 +3543,6 @@ "camera_disabled": "A kamerája ki van kapcsolva", "camera_enabled": "A kamerája még mindig be van kapcsolva", "cannot_call_yourself_description": "Nem hívhatja fel saját magát.", - "change_input_device": "Bemeneti eszköz megváltoztatása", "connecting": "Kapcsolódás", "connection_lost": "Megszakadt a kapcsolat a kiszolgálóval", "connection_lost_description": "Nem kezdeményezhet hívást a kiszolgálóval való kapcsolat nélkül.", @@ -3619,8 +3559,6 @@ "enable_camera": "Kamera bekapcsolása", "enable_microphone": "Mikrofon némításának feloldása", "expand": "Visszatérés a híváshoz", - "failed_call_live_broadcast_description": "Nem lehet hívást kezdeményezni élő közvetítés felvétele közben. Az élő közvetítés bejezése szükséges a hívás indításához.", - "failed_call_live_broadcast_title": "Nem sikerült hívást indítani", "hangup": "Bontás", "hide_sidebar_button": "Oldalsáv elrejtése", "input_devices": "Beviteli eszközök", diff --git a/src/i18n/strings/id.json b/src/i18n/strings/id.json index 77cdd8a78ff..9eade90bc21 100644 --- a/src/i18n/strings/id.json +++ b/src/i18n/strings/id.json @@ -870,7 +870,6 @@ }, "udd": { "interactive_verification_button": "Verifikasi secara interaktif sengan emoji", - "manual_verification_button": "Verifikasi secara manual dengan teks", "other_ask_verify_text": "Tanyakan pengguna ini untuk memverifikasi sesinya, atau verifikasi secara manual di bawah.", "other_new_session_text": "%(name)s (%(userId)s) masuk ke sesi yang baru tanpa memverifikasinya:", "own_ask_verify_text": "Verifikasi sesi Anda lainnya dengan menggunakan salah satu pilihan di bawah.", @@ -905,12 +904,6 @@ "incoming_sas_dialog_waiting": "Menunggu pengguna untuk konfirmasi…", "incoming_sas_user_dialog_text_1": "Verifikasi pengguna ini untuk menandainya sebagai terpercaya. Mempercayai pengguna memberikan Anda ketenangan saat menggunakan pesan terenkripsi secara ujung ke ujung.", "incoming_sas_user_dialog_text_2": "Memverifikasi pengguna ini akan menandai sesinya sebagai terpercaya, dan juga menandai sesi Anda sebagai terpercaya kepadanya.", - "manual_device_verification_device_id_label": "ID Sesi", - "manual_device_verification_device_key_label": "Kunci sesi", - "manual_device_verification_device_name_label": "Nama sesi", - "manual_device_verification_footer": "Jika mereka tidak cocok, keamanan komunikasi Anda mungkin dikompromikan.", - "manual_device_verification_self_text": "Konfirmasi dengan membandingkan berikut ini dengan Pengaturan Pengguna di sesi Anda yang lain:", - "manual_device_verification_user_text": "Konfirmasi sesi pengguna ini dengan membandingkan berikut ini dengan Pengaturan Pengguna:", "no_key_or_device": "Sepertinya Anda tidak memiliki Kunci Keamanan atau perangkat lainnya yang Anda dapat gunakan untuk memverifikasi. Perangkat ini tidak dapat mengakses ke pesan terenkripsi lama. Untuk membuktikan identitas Anda, kunci verifikasi harus diatur ulang.", "no_support_qr_emoji": "Perangkat yang Anda sedang verifikasi tidak mendukung pemindaian kode QR atau verifikasi emoji, yang didukung oleh %(brand)s. Coba menggunakan klien yang lain.", "other_party_cancelled": "Pengguna yang lain membatalkan proses verifikasi ini.", @@ -1027,10 +1020,6 @@ }, "error_user_not_logged_in": "Pengguna belum masuk", "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s mdngakhiri sebuah siaran suara", - "you": "Anda mengakhiri sebuah siaran suara" - }, "m.call.answer": { "dm": "Panggilan sedang berjalan", "user": "%(senderName)s bergabung dengan panggilan saat ini", @@ -1399,8 +1388,6 @@ "video_rooms_faq2_answer": "Ya, lini masa obrolan akan ditampilkan di sebelah videonya.", "video_rooms_faq2_question": "Bisakah saya mengobrol dengan teks saat ada panggilan video?", "video_rooms_feedbackSubheading": "Terima kasih telah mencoba fitur beta, mohon berikan masukan sedetail mungkin supaya kami dapat menyempurnakannya.", - "voice_broadcast": "Siaran suara", - "voice_broadcast_force_small_chunks": "Paksakan panjang bagian siaran suara 15d", "wysiwyg_composer": "Editor teks kaya" }, "labs_mjolnir": { @@ -1537,7 +1524,6 @@ "mute_description": "Anda tidak akan mendapatkan notifikasi apa pun" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s memulai sebuah siaran suara", "m.key.verification.request": "%(name)s meminta verifikasi" }, "onboarding": { @@ -2104,7 +2090,6 @@ "error_unbanning": "Gagal untuk menghapus cekalan", "events_default": "Kirim pesan", "invite": "Undang pengguna", - "io.element.voice_broadcast_info": "Siaran suara", "kick": "Keluarkan pengguna", "m.call": "Mulai panggilan %(brand)s", "m.call.member": "Bergabung panggilan %(brand)s", @@ -2743,7 +2728,6 @@ "warning": "PERINGATAN: " }, "share": { - "link_title": "Tautan ke ruangan", "permalink_message": "Tautan ke pesan yang dipilih", "permalink_most_recent": "Tautan ke pesan terkini", "title_message": "Bagikan Pesan Ruangan", @@ -2832,13 +2816,6 @@ "upgraderoom": "Meningkatkan ruangan ke versi yang baru", "upgraderoom_permission_error": "Anda tidak memiliki izin yang dibutuhkan untuk menggunakan perintah ini.", "usage": "Penggunaan", - "verify": "Memverifikasi sebuah pengguna, sesi, dan tupel pubkey", - "verify_mismatch": "PERINGATAN: VERIFIKASI KUNCI GAGAL! Kunci penandatanganan untuk %(userId)s dan sesi %(deviceId)s adalah \"%(fprint)s\" yang tidak cocok dengan kunci \"%(fingerprint)s\" yang disediakan. Ini bisa saja berarti komunikasi Anda sedang disadap!", - "verify_nop": "Sesi telah diverifikasi!", - "verify_nop_warning_mismatch": "PERINGATAN: sesi telah diverifikasi, tetapi kuncinya TIDAK COCOK!", - "verify_success_description": "Kunci penandatanganan yang Anda sediakan cocok dengan kunci penandatanganan yang Anda terima dari sesi %(userId)s %(deviceId)s. Sesi ditandai sebagai terverifikasi.", - "verify_success_title": "Kunci terverifikasi", - "verify_unknown_pair": "Pasangan tidak diketahui (pengguna, sesi): (%(userId)s, %(deviceId)s)", "view": "Menampilkan ruangan dengan alamat yang ditentukan", "whois": "Menampilkan informasi tentang sebuah pengguna" }, @@ -3051,10 +3028,6 @@ "error_rendering_message": "Tidak dapat memuat pesan ini", "historical_messages_unavailable": "Anda tidak dapat melihat pesan-pesan awal", "in_room_name": " di %(room)s", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s mengakhiri sebuah siaran suara", - "you": "Anda mengakhiri sebuah siaran suara" - }, "io.element.widgets.layout": "%(senderName)s telah memperbarui tata letak ruangan", "load_error": { "no_permission": "Mencoba memuat titik spesifik di lini masa ruangan ini, tetapi Anda tidak memiliki izin untuk menampilkan pesannya.", @@ -3584,38 +3557,6 @@ "switch_theme_dark": "Ubah ke mode gelap", "switch_theme_light": "Ubah ke mode terang" }, - "voice_broadcast": { - "30s_backward": "30d sebelumnya", - "30s_forward": "30d selanjutnya", - "action": "Siaran suara", - "buffering": "Memuat…", - "confirm_listen_affirm": "Ya, hentikan rekaman saya", - "confirm_listen_description": "Jika Anda mendengarkan siaran langsung ini, rekaman siaran langsung Anda saat ini akan dihentikan.", - "confirm_listen_title": "Dengarkan siaran langsung?", - "confirm_stop_affirm": "Iya, hentikan siaran", - "confirm_stop_description": "Apakah Anda ingin menghentikan siaran langsung Anda? Ini akan mengakhiri siarannya, dan rekamanan lengkap akan tersedia dalam ruangan.", - "confirm_stop_title": "Hentikan siaran langsung?", - "connection_error": "Kesalahan koneksi - Perekaman dijeda", - "failed_already_recording_description": "Anda saat ini merekam sebuah siaran suara. Mohon akhiri siaran suara Anda saat ini untuk memulai yang baru.", - "failed_already_recording_title": "Tidak dapat memulai sebuah siaran suara baru", - "failed_decrypt": "Tidak dapat mendekripsi siaran suara", - "failed_generic": "Tidak dapat memutar siaran suara ini", - "failed_insufficient_permission_description": "Anda tidak memiliki izin untuk memulai sebuah siaran suara di ruangan ini. Hubungi sebuah administrator ruangan untuk meningkatkan izin Anda.", - "failed_insufficient_permission_title": "Tidak dapat memulai sebuah siaran suara baru", - "failed_no_connection_description": "Sayangnya kami saat ini tidak dapat memulai sebuah rekaman. Silakan mencoba lagi nanti.", - "failed_no_connection_title": "Terjadi kesalahan koneksi", - "failed_others_already_recording_description": "Ada orang lain yang saat ini merekam sebuah siaran suara. Tunggu siaran suaranya berakhir untuk memulai yang baru.", - "failed_others_already_recording_title": "Tidak dapat memulai sebuah siaran suara baru", - "go_live": "Mulai siaran langsung", - "live": "Langsung", - "pause": "jeda siaran suara", - "play": "mainkan siaran suara", - "resume": "lanjutkan siaran suara" - }, - "voice_message": { - "cant_start_broadcast_description": "Anda tidak dapat memulai sebuah pesan suara karena Anda saat ini merekam sebuah siaran langsung. Silakan mengakhiri siaran langsung Anda untuk memulai merekam sebuah pesan suara.", - "cant_start_broadcast_title": "Tidak dapat memulai pesan suara" - }, "voip": { "already_in_call": "Sudah ada di panggilan", "already_in_call_person": "Anda sudah ada di panggilan dengan orang itu.", @@ -3635,7 +3576,6 @@ "camera_disabled": "Kamera Anda dimatikan", "camera_enabled": "Kamera Anda masih nyala", "cannot_call_yourself_description": "Anda tidak dapat melakukan panggilan dengan diri sendiri.", - "change_input_device": "Ubah perangkat masukan", "connecting": "Menghubungkan", "connection_lost": "Koneksi ke server telah hilang", "connection_lost_description": "Anda tidak dapat membuat panggilan tanpa terhubung ke server.", @@ -3652,8 +3592,6 @@ "enable_camera": "Nyalakan kamera", "enable_microphone": "Suarakan mikrofon", "expand": "Kembali ke panggilan", - "failed_call_live_broadcast_description": "Anda tidak dapat memulai sebuah panggilan karena Anda saat ini merekam sebuah siaran langsung. Mohon akhiri siaran langsung Anda untuk memulai sebuah panggilan.", - "failed_call_live_broadcast_title": "Tidak dapat memulai panggilan", "hangup": "Akhiri", "hide_sidebar_button": "Sembunyikan sisi bilah", "input_devices": "Perangkat masukan", diff --git a/src/i18n/strings/is.json b/src/i18n/strings/is.json index c746caef1d2..21ff743f2db 100644 --- a/src/i18n/strings/is.json +++ b/src/i18n/strings/is.json @@ -737,7 +737,6 @@ }, "udd": { "interactive_verification_button": "Sannprófa gagnvirkt með táknmyndum", - "manual_verification_button": "Sannreyna handvirkt með texta", "other_ask_verify_text": "Biddu þennan notanda að sannreyna setuna sína, eða sannreyndu hana handvirkt hér fyrir neðan.", "other_new_session_text": "%(name)s (%(userId)s) skráði sig inn í nýja setu án þess að sannvotta hana:", "own_new_session_text": "Þú skráðir inn í nýja setu án þess að sannvotta hana:", @@ -764,11 +763,6 @@ "explainer": "Örugg skilaboð við þennan notanda eru enda-í-enda dulrituð þannig að enginn annar getur lesið þau.", "in_person": "Til öryggis, gerðu þetta í eigin persónu eða notaðu einhverja samskiptaleið sem þú treystir.", "incoming_sas_dialog_title": "Innkomin beiðni um sannvottun", - "manual_device_verification_device_id_label": "Auðkenni setu", - "manual_device_verification_device_key_label": "Dulritunarlykill setu", - "manual_device_verification_device_name_label": "Nafn á setu", - "manual_device_verification_footer": "Ef þetta samsvarar ekki, getur verið að samskiptin þín séu berskjölduð.", - "manual_device_verification_self_text": "Staðfestu með því að bera eftirfarandi saman við 'Stillingar notanda' í hinni setunni þinni:", "other_party_cancelled": "Hinn aðilinn hætti við sannvottunina.", "prompt_encrypted": "Sannreyndu alla notendur á spjallrás til að tryggja að hún sé örugg.", "prompt_self": "Hefja sannvottun aftur úr tilkynningunni.", @@ -862,10 +856,6 @@ } }, "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s endaði talútsendingu", - "you": "Þú endaðir talútsendingu" - }, "m.call.answer": { "dm": "Símtal í gangi", "user": "%(senderName)s kom inn í símtalið", @@ -1164,7 +1154,6 @@ "video_rooms_faq1_question": "Hvernig bý ég til myndspjallrás?", "video_rooms_faq2_answer": "Já, tímalína spjallsins birtist við hlið myndmerkisins.", "video_rooms_faq2_question": "Get ég notað textaspjall samhliða myndsímtali?", - "voice_broadcast": "Útvörpun tals", "wysiwyg_composer": "Þróaður textaritill" }, "labs_mjolnir": { @@ -1285,7 +1274,6 @@ "mute_description": "Þú munt ekki fá neinar tilkynningar" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s hóf talútsendingu", "m.key.verification.request": "%(name)s biður um sannvottun" }, "onboarding": { @@ -1730,7 +1718,6 @@ "error_unbanning": "Tókst ekki að taka úr banni", "events_default": "Senda skilaboð", "invite": "Bjóða notendum", - "io.element.voice_broadcast_info": "Útsendingar tals", "kick": "Fjarlægja notendur", "m.call": "Byrja %(brand)s samtal", "m.call.member": "Taka þátt í %(brand)s samtali", @@ -2240,7 +2227,6 @@ "warning": "AÐVÖRUN: " }, "share": { - "link_title": "Tengill á spjallrás", "permalink_message": "Tengill í valin skilaboð", "permalink_most_recent": "Tengill í nýjustu skilaboðin", "title_message": "Deila skilaboðum spjallrásar", @@ -2321,12 +2307,6 @@ "upgraderoom": "Uppfærir spjallrás í nýja útgáfu", "upgraderoom_permission_error": "Þú hefur ekki nauðsynlegar heimildir til að nota þessa skipun.", "usage": "Notkun", - "verify": "Sannreynir auðkenni notanda, setu og dreifilykils", - "verify_mismatch": "AÐVÖRUN: SANNVOTTUN LYKILS MISTÓKST! Undirritunarlykillinn fyrir %(userId)s og setuna %(deviceId)s er \"%(fprint)s\" sem samsvarar ekki uppgefna lyklinum \"%(fingerprint)s\". Þetta gæti þýtt að einhver hafi komist inn í samskiptin þín!", - "verify_nop": "Seta er þegar sannreynd!", - "verify_success_description": "Undirritunarlykillinn sem þú gafst upp samsvarar lyklinum sem þú fékkst frá %(userId)s og setunni %(deviceId)s. Setan er því merkt sem sannreynd.", - "verify_success_title": "Staðfestur dulritunarlykill", - "verify_unknown_pair": "Óþekkt pörun (notandi, seta): (%(userId)s, %(deviceId)s)", "whois": "Birtir upplýsingar um notanda" }, "space": { @@ -2523,10 +2503,6 @@ "error_rendering_message": "Gat ekki hlaðið inn þessum skilaboðum", "historical_messages_unavailable": "Þú getur ekki séð eldri skilaboð", "in_room_name": " í %(room)s", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s endaði talútsendingu", - "you": "Þú endaðir talútsendingu" - }, "io.element.widgets.layout": "%(senderName)s hefur uppfært framsetningu spjallrásarinnar", "load_error": { "no_permission": "Reyndi að hlaða inn tilteknum punkti úr tímalínu þessarar spjallrásar, en þú ert ekki með heimild til að skoða tilteknu skilaboðin.", @@ -2998,27 +2974,6 @@ "switch_theme_dark": "Skiptu yfir í dökkan ham", "switch_theme_light": "Skiptu yfir í ljósan ham" }, - "voice_broadcast": { - "30s_backward": "30s afturábak", - "30s_forward": "30s áfram", - "action": "Útvörpun tals", - "buffering": "Hleð í biðminni…", - "confirm_listen_affirm": "Já, stöðva upptökuna mína", - "confirm_listen_title": "Hlusta á beina útsendingu?", - "confirm_stop_affirm": "Já, stöðva útsendingu", - "confirm_stop_title": "Stöðva beina útsendingu?", - "connection_error": "Villa í tengingu - Upptaka í bið", - "failed_already_recording_title": "Get ekki byrjað nýja talútsendingu", - "failed_insufficient_permission_title": "Get ekki byrjað nýja talútsendingu", - "failed_no_connection_description": "Því miður tókst ekki að setja aðra upptöku í gang. Reyndu aftur síðar.", - "failed_no_connection_title": "Villa í tengingu", - "failed_others_already_recording_title": "Get ekki byrjað nýja talútsendingu", - "go_live": "Fara í beina útsendingu", - "live": "Beint", - "pause": "setja talútsendingu í bið", - "play": "spila talútsendingu", - "resume": "halda áfram með talútsendingu" - }, "voip": { "already_in_call": "Nú þegar í símtali", "already_in_call_person": "Þú ert nú þegar í símtali við þennan aðila.", @@ -3038,7 +2993,6 @@ "camera_disabled": "Slökkt er á myndavélinni þinni", "camera_enabled": "Enn er kveikt á myndavélinni þinni", "cannot_call_yourself_description": "Þú getur ekki byrjað símtal með sjálfum þér.", - "change_input_device": "skipta um inntakstæki", "connecting": "Tengist", "connection_lost": "Tenging við vefþjón hefur rofnað", "connection_lost_description": "Þú getur ekki hringt símtöl án tengingar við netþjóninn.", @@ -3055,7 +3009,6 @@ "enable_camera": "Kveikja á myndavél", "enable_microphone": "Kveikja á hljóðnema", "expand": "Fara til baka í símtal", - "failed_call_live_broadcast_title": "Get ekki hafið símtal", "hangup": "Leggja á", "hide_sidebar_button": "Fela hliðarspjald", "input_devices": "Inntakstæki", diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 9f0ab6d4304..1ddf88e8327 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -881,7 +881,6 @@ }, "udd": { "interactive_verification_button": "Verifica interattivamente con emoji", - "manual_verification_button": "Verifica manualmente con testo", "other_ask_verify_text": "Chiedi a questo utente di verificare la sua sessione o verificala manualmente sotto.", "other_new_session_text": "%(name)s (%(userId)s) ha fatto l'accesso con una nuova sessione senza verificarla:", "own_ask_verify_text": "Verifica la tua altra sessione usando una delle opzioni sotto.", @@ -916,12 +915,6 @@ "incoming_sas_dialog_waiting": "In attesa che il partner confermi…", "incoming_sas_user_dialog_text_1": "Verifica questo utente per contrassegnarlo come affidabile. La fiducia degli utenti offre una maggiore tranquillità quando si utilizzano messaggi cifrati end-to-end.", "incoming_sas_user_dialog_text_2": "La verifica di questo utente contrassegnerà come fidata la sua sessione a te e viceversa.", - "manual_device_verification_device_id_label": "ID sessione", - "manual_device_verification_device_key_label": "Chiave sessione", - "manual_device_verification_device_name_label": "Nome sessione", - "manual_device_verification_footer": "Se non corrispondono, la sicurezza delle tue comunicazioni potrebbe essere compromessa.", - "manual_device_verification_self_text": "Conferma confrontando il seguente con le impostazioni utente nell'altra sessione:", - "manual_device_verification_user_text": "Conferma questa sessione confrontando il seguente con le sue impostazioni utente:", "no_key_or_device": "Pare che tu non abbia una chiave di sicurezza o altri dispositivi con cui poterti verificare. Questo dispositivo non potrà accedere ai vecchi messaggi cifrati. Per potere verificare la tua ideintità su questo dispositivo, dovrai reimpostare le chiavi di verifica.", "no_support_qr_emoji": "Il dispositivo che stai cercando di verificare non supporta la scansione di un codice QR o la verifica emoji, che sono supportate da %(brand)s. Prova con un client diverso.", "other_party_cancelled": "L'altra parte ha annullato la verifica.", @@ -1045,10 +1038,6 @@ }, "error_user_not_logged_in": "Utente non connesso", "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s ha terminato una trasmissione vocale", - "you": "Hai terminato una trasmissione vocale" - }, "m.call.answer": { "dm": "Chiamata in corso", "user": "%(senderName)s si è unito alla chiamata", @@ -1422,8 +1411,6 @@ "video_rooms_faq2_answer": "Sì, la cronologia della chat viene mostrata assieme al video.", "video_rooms_faq2_question": "Posso usare la chat testuale assieme alla chiamata video?", "video_rooms_feedbackSubheading": "Grazie per aver provato la beta, ti preghiamo di fornire tutti i dettagli possibili in modo da poterla migliorare.", - "voice_broadcast": "Trasmissione vocale", - "voice_broadcast_force_small_chunks": "Forza lunghezza pezzo trasmissione vocale a 15s", "wysiwyg_composer": "Editor in rich text" }, "labs_mjolnir": { @@ -1567,7 +1554,6 @@ "mute_description": "Non riceverai alcuna notifica" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s ha iniziato una trasmissione vocale", "m.key.verification.request": "%(name)s sta richiedendo la verifica" }, "onboarding": { @@ -2141,7 +2127,6 @@ "error_unbanning": "Rimozione ban fallita", "events_default": "Invia messaggi", "invite": "Invita utenti", - "io.element.voice_broadcast_info": "Trasmissioni vocali", "kick": "Rimuovi utenti", "m.call": "Inizia chiamate di %(brand)s", "m.call.member": "Entra in chiamate di %(brand)s", @@ -2784,7 +2769,6 @@ "warning": "ATTENZIONE: " }, "share": { - "link_title": "Collegamento alla stanza", "permalink_message": "Link al messaggio selezionato", "permalink_most_recent": "Link al messaggio più recente", "title_message": "Condividi messaggio stanza", @@ -2873,13 +2857,6 @@ "upgraderoom": "Aggiorna una stanza ad una nuova versione", "upgraderoom_permission_error": "Non hai l'autorizzazione necessaria per usare questo comando.", "usage": "Utilizzo", - "verify": "Verifica un utente, una sessione e una tupla pubblica", - "verify_mismatch": "ATTENZIONE: VERIFICA CHIAVI FALLITA! La chiave per %(userId)s e per la sessione %(deviceId)s è \"%(fprint)s\" la quale non corriponde con la chiave \"%(fingerprint)s\" fornita. Ciò può significare che le comunicazioni vengono intercettate!", - "verify_nop": "Sessione già verificata!", - "verify_nop_warning_mismatch": "ATTENZIONE: sessione già verificata, ma le chiavi NON CORRISPONDONO!", - "verify_success_description": "La chiave che hai fornito corrisponde alla chiave che hai ricevuto dalla sessione di %(userId)s %(deviceId)s. Sessione contrassegnata come verificata.", - "verify_success_title": "Chiave verificata", - "verify_unknown_pair": "Coppia (utente, sessione) sconosciuta: (%(userId)s, %(deviceId)s)", "view": "Visualizza la stanza con l'indirizzo dato", "whois": "Mostra le informazioni di un utente" }, @@ -3097,10 +3074,6 @@ "error_rendering_message": "Impossibile caricare questo messaggio", "historical_messages_unavailable": "Non puoi vedere i messaggi precedenti", "in_room_name": " in %(room)s", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s ha terminato una trasmissione vocale", - "you": "Hai terminato una trasmissione vocale" - }, "io.element.widgets.layout": "%(senderName)s ha aggiornato la disposizione della stanza", "late_event_separator": "Inviato originariamente il %(dateTime)s", "load_error": { @@ -3633,38 +3606,6 @@ "switch_theme_dark": "Passa alla modalità scura", "switch_theme_light": "Passa alla modalità chiara" }, - "voice_broadcast": { - "30s_backward": "30s indietro", - "30s_forward": "30s avanti", - "action": "Trasmissione vocale", - "buffering": "Buffer…", - "confirm_listen_affirm": "Sì, termina la mia registrazione", - "confirm_listen_description": "Se inizi ad ascoltare questa trasmissione in diretta, l'attuale registrazione della tua trasmissione in diretta finirà.", - "confirm_listen_title": "Ascoltare la trasmissione in diretta?", - "confirm_stop_affirm": "Sì, ferma la trasmissione", - "confirm_stop_description": "Vuoi davvero fermare la tua trasmissione in diretta? Verrà terminata la trasmissione e la registrazione completa sarà disponibile nella stanza.", - "confirm_stop_title": "Fermare la trasmissione in diretta?", - "connection_error": "Errore di connessione - Registrazione in pausa", - "failed_already_recording_description": "Stai già registrando una trasmissione vocale. Termina quella in corso per iniziarne una nuova.", - "failed_already_recording_title": "Impossibile iniziare una nuova trasmissione vocale", - "failed_decrypt": "Impossibile decifrare la trasmissione vocale", - "failed_generic": "Impossibile avviare questa trasmissione vocale", - "failed_insufficient_permission_description": "Non hai l'autorizzazione necessaria per iniziare una trasmissione vocale in questa stanza. Contatta un amministratore della stanza per aggiornare le tue autorizzazioni.", - "failed_insufficient_permission_title": "Impossibile iniziare una nuova trasmissione vocale", - "failed_no_connection_description": "Sfortunatamente non riusciamo ad iniziare una registrazione al momento. Riprova più tardi.", - "failed_no_connection_title": "Errore di connessione", - "failed_others_already_recording_description": "Qualcun altro sta già registrando una trasmissione vocale. Aspetta che finisca prima di iniziarne una nuova.", - "failed_others_already_recording_title": "Impossibile iniziare una nuova trasmissione vocale", - "go_live": "Vai in diretta", - "live": "In diretta", - "pause": "sospendi trasmissione vocale", - "play": "avvia trasmissione vocale", - "resume": "riprendi trasmissione vocale" - }, - "voice_message": { - "cant_start_broadcast_description": "Non puoi iniziare un messaggio vocale perché stai registrando una trasmissione in diretta. Termina la trasmissione per potere iniziare un messaggio vocale.", - "cant_start_broadcast_title": "Impossibile iniziare il messaggio vocale" - }, "voip": { "already_in_call": "Già in una chiamata", "already_in_call_person": "Sei già in una chiamata con questa persona.", @@ -3684,7 +3625,6 @@ "camera_disabled": "La tua fotocamera è spenta", "camera_enabled": "La tua fotocamera è ancora attiva", "cannot_call_yourself_description": "Non puoi chiamare te stesso.", - "change_input_device": "Cambia dispositivo di input", "close_lobby": "Chiudi sala d'attesa", "connecting": "In connessione", "connection_lost": "La connessione al server è stata persa", @@ -3703,8 +3643,6 @@ "enable_camera": "Accendi la fotocamera", "enable_microphone": "Riaccendi il microfono", "expand": "Torna alla chiamata", - "failed_call_live_broadcast_description": "Non puoi avviare una chiamata perché stai registrando una trasmissione in diretta. Termina la trasmissione per potere iniziare una chiamata.", - "failed_call_live_broadcast_title": "Impossibile avviare una chiamata", "hangup": "Riaggancia", "hide_sidebar_button": "Nascondi barra laterale", "input_devices": "Dispositivi di input", diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json index 963de355ac6..4ac7672e4c8 100644 --- a/src/i18n/strings/ja.json +++ b/src/i18n/strings/ja.json @@ -833,7 +833,6 @@ }, "udd": { "interactive_verification_button": "絵文字で認証", - "manual_verification_button": "テキストを使って手動で認証", "other_ask_verify_text": "このユーザーにセッションを認証するよう依頼するか、以下から手動で認証してください。", "other_new_session_text": "%(name)s(%(userId)s)は未認証のセッションにサインインしました:", "own_ask_verify_text": "以下のどれか一つを使って他のセッションを認証します。", @@ -868,12 +867,6 @@ "incoming_sas_dialog_waiting": "相手の承認を待機しています…", "incoming_sas_user_dialog_text_1": "このユーザーを認証すると、信頼済として表示します。ユーザーを信頼すると、より一層安心してエンドツーエンド暗号化を使用することができます。", "incoming_sas_user_dialog_text_2": "このユーザーを認証すると、相手のセッションと自分のセッションを信頼済として表示します。", - "manual_device_verification_device_id_label": "セッションID", - "manual_device_verification_device_key_label": "セッションキー", - "manual_device_verification_device_name_label": "セッション名", - "manual_device_verification_footer": "一致していない場合は、コミュニケーションのセキュリティーが損なわれている可能性があります。", - "manual_device_verification_self_text": "他のセッションのユーザー設定で、以下を比較して承認してください:", - "manual_device_verification_user_text": "ユーザー設定画面で以下を比較し、このユーザーのセッションを承認してください:", "no_key_or_device": "セキュリティーキーもしくは認証可能な端末が設定されていません。この端末では、以前暗号化されたメッセージにアクセスすることができません。この端末で本人確認を行うには、認証用の鍵を再設定する必要があります。", "no_support_qr_emoji": "認証しようとしている端末は、QRコードのスキャンや絵文字による認証をサポートしていませんが、%(brand)sではサポートされています。異なるクライアントで試してください。", "other_party_cancelled": "相手が認証をキャンセルしました。", @@ -983,10 +976,6 @@ } }, "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)sが音声配信を終了しました", - "you": "音声配信を終了しました" - }, "m.call.answer": { "dm": "通話しています", "user": "%(senderName)sが通話に参加しました", @@ -1318,8 +1307,6 @@ "video_rooms_faq1_question": "ビデオ通話ルームの作成方法", "video_rooms_faq2_answer": "はい、会話のタイムラインが動画と並んで表示されます。", "video_rooms_faq2_question": "テキストによる会話も行えますか?", - "voice_broadcast": "音声配信", - "voice_broadcast_force_small_chunks": "音声配信のチャンク長を15秒に強制", "wysiwyg_composer": "リッチテキストエディター" }, "labs_mjolnir": { @@ -1455,7 +1442,6 @@ "mute_description": "通知を送信しません" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)sが音声配信を開始しました", "m.key.verification.request": "%(name)sは認証を要求しています" }, "onboarding": { @@ -1953,7 +1939,6 @@ "error_unbanning": "ブロック解除に失敗しました", "events_default": "メッセージの送信", "invite": "ユーザーの招待", - "io.element.voice_broadcast_info": "音声配信", "kick": "ユーザーの追放", "m.call": "%(brand)s通話を開始", "m.call.member": "%(brand)s通話に参加", @@ -2532,7 +2517,6 @@ "warning": "警告:" }, "share": { - "link_title": "ルームへのリンク", "permalink_message": "選択したメッセージにリンク", "permalink_most_recent": "最新のメッセージにリンク", "title_message": "ルームのメッセージを共有", @@ -2614,13 +2598,6 @@ "upgraderoom": "ルームを新しいバージョンにアップグレード", "upgraderoom_permission_error": "このコマンドを実行するのに必要な権限がありません。", "usage": "用法", - "verify": "ユーザー、セッション、およびpubkeyタプルを認証", - "verify_mismatch": "警告:鍵の認証に失敗しました!提供された鍵「%(fingerprint)s」は、%(userId)sおよびセッション %(deviceId)s の署名鍵「%(fprint)s」と一致しません。通信が傍受されているおそれがあります!", - "verify_nop": "このセッションは認証済です!", - "verify_nop_warning_mismatch": "警告:このセッションは認証済ですが、鍵が一致しません!", - "verify_success_description": "指定された署名鍵は%(userId)sのセッション %(deviceId)s から受け取った鍵と一致します。セッションは認証済です。", - "verify_success_title": "認証済の鍵", - "verify_unknown_pair": "不明な(ユーザー、セッション)ペア:(%(userId)s、%(deviceId)s)", "whois": "ユーザーの情報を表示" }, "space": { @@ -2831,10 +2808,6 @@ "error_rendering_message": "このメッセージを読み込めません", "historical_messages_unavailable": "以前のメッセージは表示できません", "in_room_name": " %(room)s内で", - "io.element.voice_broadcast_info": { - "user": "%(senderName)sが音声配信を終了しました", - "you": "音声配信を終了しました" - }, "io.element.widgets.layout": "%(senderName)sがルームのレイアウトを更新しました", "load_error": { "no_permission": "このルームのタイムラインの特定の地点を読み込もうとしましたが、問題のメッセージを閲覧する権限がありません。", @@ -3345,38 +3318,6 @@ "switch_theme_dark": "ダークテーマに切り替える", "switch_theme_light": "ライトテーマに切り替える" }, - "voice_broadcast": { - "30s_backward": "30秒戻す", - "30s_forward": "30秒進める", - "action": "音声配信", - "buffering": "バッファリングしています…", - "confirm_listen_affirm": "はい、録音を終了してください", - "confirm_listen_description": "このライブ配信の視聴を開始すると、現在のライブ配信の録音は終了します。", - "confirm_listen_title": "ライブ配信を視聴しますか?", - "confirm_stop_affirm": "はい、配信を停止します", - "confirm_stop_description": "ライブ配信を終了してよろしいですか?配信を終了し、録音をこのルームで利用できるよう設定します。", - "confirm_stop_title": "ライブ配信を停止しますか?", - "connection_error": "接続エラー - 録音を停止しました", - "failed_already_recording_description": "既に音声配信を録音しています。新しく始めるには現在の音声配信を終了してください。", - "failed_already_recording_title": "新しい音声配信を開始できません", - "failed_decrypt": "音声配信を復号化できません", - "failed_generic": "この音声配信を再生できません", - "failed_insufficient_permission_description": "このルームで音声配信を開始する権限がありません。ルームの管理者に連絡して権限の付与を依頼してください。", - "failed_insufficient_permission_title": "新しい音声配信を開始できません", - "failed_no_connection_description": "録音を開始できません。後でもう一度やり直してください。", - "failed_no_connection_title": "接続エラー", - "failed_others_already_recording_description": "他の人が既に音声配信を録音しています。新しく始めるには音声配信が終わるまで待機してください。", - "failed_others_already_recording_title": "新しい音声配信を開始できません", - "go_live": "ライブ配信", - "live": "ライブ", - "pause": "音声配信を一時停止", - "play": "音声配信を再生", - "resume": "音声配信を再開" - }, - "voice_message": { - "cant_start_broadcast_description": "ライブ配信を録音しているため、音声メッセージを開始できません。音声メッセージの録音を開始するには、ライブ配信を終了してください。", - "cant_start_broadcast_title": "音声メッセージを開始できません" - }, "voip": { "already_in_call": "既に通話中です", "already_in_call_person": "既にこの人と通話中です。", @@ -3396,7 +3337,6 @@ "camera_disabled": "カメラが無効です", "camera_enabled": "カメラがまだ有効です", "cannot_call_yourself_description": "自分自身に通話を発信することはできません。", - "change_input_device": "入力端末を変更", "connecting": "接続しています", "connection_lost": "サーバーとの接続が失われました", "connection_lost_description": "サーバーに接続していないため、通話を発信できません。", @@ -3413,8 +3353,6 @@ "enable_camera": "カメラを有効にする", "enable_microphone": "マイクのミュートを解除", "expand": "通話に戻る", - "failed_call_live_broadcast_description": "ライブ配信を録音しているため、通話を開始できません。通話を開始するには、ライブ配信を終了してください。", - "failed_call_live_broadcast_title": "通話を開始できません", "hangup": "電話を切る", "hide_sidebar_button": "サイドバーを表示しない", "input_devices": "入力装置", diff --git a/src/i18n/strings/lo.json b/src/i18n/strings/lo.json index 6310faf2005..ddbe3d17f83 100644 --- a/src/i18n/strings/lo.json +++ b/src/i18n/strings/lo.json @@ -769,12 +769,6 @@ "incoming_sas_dialog_title": "ການຮ້ອງຂໍການຢັ້ງຢືນຂາເຂົ້າ", "incoming_sas_user_dialog_text_1": "ຢັ້ງຢືນຜູ້ໃຊ້ນີ້ເພື່ອສ້າງເຄື່ອງທີ່ເຊື່ອຖືໄດ້. ຜູ້ໃຊ້ທີ່ເຊື່ອຖືໄດ້ ເຮັດໃຫ້ທ່ານອຸ່ນໃຈຂື້ນເມື່ຶຶອເຂົ້າລະຫັດຂໍ້ຄວາມແຕ່ຕົ້ນທາງເຖິງປາຍທາງ.", "incoming_sas_user_dialog_text_2": "ການຢືນຢັນຜູ້ໃຊ້ນີ້ຈະເປັນເຄື່ອງໝາຍໃນລະບົບຂອງເຂົາເຈົ້າໜ້າເຊື່ອຖືໄດ້ ແລະ ເປັນເຄື່ອງໝາຍເຖິງລະບົບຂອງທ່ານ ເປັນທີ່ເຊື່ອຖືໄດ້ຕໍ່ກັບເຂົາເຈົ້າ.", - "manual_device_verification_device_id_label": "ID ລະບົບ", - "manual_device_verification_device_key_label": "ລະຫັດລະບົບ", - "manual_device_verification_device_name_label": "ຊື່ລະບົບ", - "manual_device_verification_footer": "ຖ້າລະຫັດບໍ່ກົງກັນ, ຄວາມປອດໄພຂອງການສື່ສານຂອງທ່ານອາດຈະຖືກທໍາລາຍ.", - "manual_device_verification_self_text": "ຢືນຢັນໂດຍການປຽບທຽບສິ່ງຕໍ່ໄປນີ້ກັບການຕັ້ງຄ່າຜູ້ໃຊ້ໃນລະບົບອື່ນຂອງທ່ານ:", - "manual_device_verification_user_text": "ຢືນຢັນລະບົບຂອງຜູ້ໃຊ້ນີ້ໂດຍການປຽບທຽບສິ່ງຕໍ່ໄປນີ້ກັບການຕັ້ງຄ່າຜູ້ໃຊ້ຂອງເຂົາເຈົ້າ:", "no_key_or_device": "ເບິ່ງຄືວ່າທ່ານບໍ່ມີກະແຈຄວາມປອດໄພ ຫຼື ອຸປະກອນອື່ນໆທີ່ທ່ານສາມາດຢືນຢັນໄດ້. ອຸປະກອນນີ້ຈະບໍ່ສາມາດເຂົ້າເຖິງຂໍ້ຄວາມທີ່ເຂົ້າລະຫັດເກົ່າໄດ້. ເພື່ອຢືນຢັນຕົວຕົນຂອງທ່ານໃນອຸປະກອນນີ້, ທ່ານຈຳເປັນຕ້ອງຕັ້ງລະຫັດຢືນຢັນຂອງທ່ານ.", "no_support_qr_emoji": "ອຸປະກອນທີ່ທ່ານພະຍາຍາມກວດສອບບໍ່ຮອງຮັບການສະແກນລະຫັດ QR ຫຼື ການຢັ້ງຢືນ emoji, ຊຶ່ງເປັນສິ່ງທີ່%(brand)sສະຫນັບສະຫນູນ. ລອງໃຊ້ກັບລູກຄ້າອື່ນ.", "other_party_cancelled": "ອີກຝ່າຍໄດ້ຍົກເລີກການຢັ້ງຢືນ.", @@ -2191,7 +2185,6 @@ "warn_quit": "ເຕືອນກ່ອນຢຸດຕິ" }, "share": { - "link_title": "ເຊື່ອມຕໍ່ທີ່ຫ້ອງ", "permalink_message": "ເຊື່ອມຕໍ່ກັບຂໍ້ຄວາມທີ່ເລືອກ", "permalink_most_recent": "ເຊື່ອມຕໍ່ກັບຂໍ້ຄວາມຫຼ້າສຸດ", "title_message": "ແບ່ງປັນຂໍ້ຄວາມໃນຫ້ອງ", @@ -2273,12 +2266,6 @@ "upgraderoom": "ຍົກລະດັບຫ້ອງເປັນລຸ້ນໃໝ່", "upgraderoom_permission_error": "ທ່ານບໍ່ມີສິດໃຊ້ຄໍາສັ່ງນີ້.", "usage": "ການນໍາໃຊ້", - "verify": "ຢືນຢັນຜູ້ໃຊ້, ລະບົບ, ແລະ pubkey tuple", - "verify_mismatch": "ຄຳເຕືອນ: ການຢືນຢັນບໍ່ສຳເລັັດ! ປຸ່ມເຊັນຊື່ສຳລັບ %(userId)s ແລະ ລະບົບ %(deviceId)s ແມ່ນ \"%(fprint)s\" ບໍ່ກົງກັບລະຫັດທີ່ລະບຸໄວ້ \"%(fingerprint)s\". ນີ້ອາດຈະຫມາຍຄວາມວ່າການສື່ສານຂອງທ່ານຖືກຂັດຂວາງ!", - "verify_nop": "ການຢັ້ງຢືນລະບົບແລ້ວ!", - "verify_success_description": "ກະແຈໄຂລະຫັດທີ່ທ່ານໃຊ້ກົງກັບກະແຈໄຂລະຫັດທີ່ທ່ານໄດ້ຮັບຈາກ %(userId)s ເທິງອຸປະກອນ %(deviceId)s. ລະບົບຢັ້ງຢືນສຳເລັດແລ້ວ.", - "verify_success_title": "ກະແຈທີ່ຢືນຢັນແລ້ວ", - "verify_unknown_pair": "ບໍ່ຮູ້ຈັກ (ຜູ້ໃຊ້, ລະບົບ) ຄູ່: (%(userId)s, %(deviceId)s)", "whois": "ສະແດງຂໍ້ມູນກ່ຽວກັບຜູ້ໃຊ້" }, "space": { diff --git a/src/i18n/strings/lt.json b/src/i18n/strings/lt.json index 8f503bacdd8..1e0679bae48 100644 --- a/src/i18n/strings/lt.json +++ b/src/i18n/strings/lt.json @@ -597,12 +597,6 @@ "incoming_sas_dialog_title": "Įeinantis Patikrinimo Prašymas", "incoming_sas_user_dialog_text_1": "Patvirtinkite šį vartotoją, kad pažymėtumėte jį kaip patikimą. Vartotojų pažymėjimas patikimais suteikia jums papildomos ramybės naudojant visapusiškai užšifruotas žinutes.", "incoming_sas_user_dialog_text_2": "Patvirtinant šį vartotoją, jo seansas bus pažymėtas kaip patikimas, taip pat jūsų seansas bus pažymėtas kaip patikimas jam.", - "manual_device_verification_device_id_label": "Seanso ID", - "manual_device_verification_device_key_label": "Seanso raktas", - "manual_device_verification_device_name_label": "Seanso pavadinimas", - "manual_device_verification_footer": "Jei jie nesutampa, gali būti pažeistas jūsų komunikacijos saugumas.", - "manual_device_verification_self_text": "Patvirtinkite, palygindami tai, kas nurodyta toliau, su Vartotojo Nustatymais kitame jūsų seanse:", - "manual_device_verification_user_text": "Patvirtinkite šio vartotojo seansą, palygindami tai, kas nurodyta toliau, su jo Vartotojo Nustatymais:", "no_support_qr_emoji": "Įrenginys, kurį bandote patvirtinti, nepalaiko QR kodo nuskaitymo arba jaustukų patikrinimo, kurį palaiko %(brand)s. Pabandykite naudoti kitą klientą.", "other_party_cancelled": "Kita šalis atšaukė patvirtinimą.", "prompt_encrypted": "Patvirtinkite visus vartotojus kambaryje, kad užtikrintumėte jo saugumą.", @@ -886,8 +880,7 @@ "video_rooms_faq1_answer": "Kairiajame skydelyje esančioje kambarių skiltyje naudokite mygtuką “+”.", "video_rooms_faq1_question": "Kaip galiu sukurti vaizdo kambarį?", "video_rooms_faq2_answer": "Taip, pokalbių laiko juosta rodoma kartu su vaizdu.", - "video_rooms_faq2_question": "Ar galiu naudoti teksto pokalbius kartu su vaizdo skambučiu?", - "voice_broadcast": "Balso transliacija" + "video_rooms_faq2_question": "Ar galiu naudoti teksto pokalbius kartu su vaizdo skambučiu?" }, "labs_mjolnir": { "advanced_warning": "⚠ Šie nustatymai yra skirti pažengusiems vartotojams.", @@ -1859,11 +1852,6 @@ "upgraderoom": "Atnaujina kambarį į naują versiją", "upgraderoom_permission_error": "Jūs neturite reikalingų leidimų naudoti šią komandą.", "usage": "Naudojimas", - "verify": "Patvirtina vartotojo, seanso ir pubkey daugiadalę duomenų struktūrą", - "verify_mismatch": "ĮSPĖJIMAS: RAKTŲ PATIKRINIMAS NEPAVYKO! Pasirašymo raktas vartotojui %(userId)s ir seansui %(deviceId)s yra \"%(fprint)s\", kuris nesutampa su pateiktu raktu \"%(fingerprint)s\". Tai gali reikšti, kad jūsų komunikacijos yra perimamos!", - "verify_nop": "Seansas jau patvirtintas!", - "verify_success_description": "Jūsų pateiktas pasirašymo raktas sutampa su pasirašymo raktu, gautu iš vartotojo %(userId)s seanso %(deviceId)s. Seansas pažymėtas kaip patikrintas.", - "verify_success_title": "Patvirtintas raktas", "whois": "Parodo informaciją apie vartotoją" }, "space": { @@ -2385,15 +2373,6 @@ "user_menu": { "settings": "Visi nustatymai" }, - "voice_broadcast": { - "action": "Balso transliacija", - "confirm_stop_affirm": "Taip, sustabdyti transliaciją", - "confirm_stop_title": "Sustabdyti transliaciją gyvai?", - "live": "Gyvai", - "pause": "pristabdyti balso transliaciją", - "play": "paleisti balso transliaciją", - "resume": "tęsti balso transliaciją" - }, "voip": { "already_in_call": "Jau pokalbyje", "already_in_call_person": "Jūs jau esate pokalbyje su šiuo asmeniu.", diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index 1231a9a3303..ebc2f396232 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -774,7 +774,6 @@ }, "udd": { "interactive_verification_button": "Interactief verifiëren door emoji", - "manual_verification_button": "Handmatig verifiëren via tekst", "other_ask_verify_text": "Vraag deze persoon de sessie te verifiëren, of verifieer het handmatig hieronder.", "other_new_session_text": "%(name)s%(userId)s heeft zich aangemeld bij een nieuwe sessie zonder deze te verifiëren:", "own_ask_verify_text": "Verifieer je andere sessie op een van onderstaande wijzen.", @@ -806,12 +805,6 @@ "incoming_sas_dialog_title": "Inkomend verificatieverzoek", "incoming_sas_user_dialog_text_1": "Verifieer deze persoon om als vertrouwd te markeren. Personen vertrouwen geeft je extra zekerheid bij het gebruik van eind-tot-eind-versleutelde berichten.", "incoming_sas_user_dialog_text_2": "Deze persoon verifiëren zal de sessie als vertrouwd markeren voor jullie beide.", - "manual_device_verification_device_id_label": "Sessie-ID", - "manual_device_verification_device_key_label": "Sessiesleutel", - "manual_device_verification_device_name_label": "Sessienaam", - "manual_device_verification_footer": "Als deze niet overeenkomen, dan wordt deze sessie mogelijk door iemand anders onderschept.", - "manual_device_verification_self_text": "Bevestig door het volgende te vergelijken met de persoonsinstellingen in je andere sessie:", - "manual_device_verification_user_text": "Bevestig de sessie van deze persoon door het volgende te vergelijken met zijn persoonsinstellingen:", "no_key_or_device": "Het lijkt erop dat je geen veiligheidssleutel hebt of andere apparaten waarmee je kunt verifiëren. Dit apparaat heeft geen toegang tot oude versleutelde berichten. Om je identiteit op dit apparaat te verifiëren, moet je jouw verificatiesleutels opnieuw instellen.", "no_support_qr_emoji": "Het apparaat dat je probeert te verifiëren ondersteund niet de door %(brand)s ondersteunde methodes: scannen van een QR-code of emoji verificatie. Probeer het met een andere app.", "other_party_cancelled": "De tegenpartij heeft de verificatie geannuleerd.", @@ -1214,8 +1207,7 @@ "video_rooms_faq1_answer": "Gebruik de knop \"+\" in het kamergedeelte van het linkerpaneel.", "video_rooms_faq1_question": "Hoe kan ik een videokamer maken?", "video_rooms_faq2_answer": "Ja, de gesprekstijdslijn wordt naast de video weergegeven.", - "video_rooms_faq2_question": "Kan ik tekstberichten gebruiken naast het videogesprek?", - "voice_broadcast": "Spraakuitzending" + "video_rooms_faq2_question": "Kan ik tekstberichten gebruiken naast het videogesprek?" }, "labs_mjolnir": { "advanced_warning": "⚠ Deze instellingen zijn bedoeld voor gevorderde personen.", @@ -1824,7 +1816,6 @@ "error_unbanning": "Ontbannen mislukt", "events_default": "Berichten versturen", "invite": "Personen uitnodigen", - "io.element.voice_broadcast_info": "Spraakuitzendingen", "kick": "Personen verwijderen", "m.call": "%(brand)s oproepen starten", "m.call.member": "Deelnemen aan %(brand)s gesprekken", @@ -2363,7 +2354,6 @@ "warn_quit": "Waarschuwen voordat je afsluit" }, "share": { - "link_title": "Link naar kamer", "permalink_message": "Koppeling naar geselecteerd bericht", "permalink_most_recent": "Koppeling naar meest recente bericht", "title_message": "Bericht uit kamer delen", @@ -2445,12 +2435,6 @@ "upgraderoom": "Upgrade deze kamer naar een nieuwere versie", "upgraderoom_permission_error": "Je beschikt niet over de vereiste machtigingen om deze opdracht uit te voeren.", "usage": "Gebruik", - "verify": "Verifieert de combinatie van persoon, sessie en publieke sleutel", - "verify_mismatch": "PAS OP: sleutelverificatie MISLUKT! De combinatie %(userId)s + sessie %(deviceId)s is ondertekend met ‘%(fprint)s’ - maar de opgegeven sleutel is ‘%(fingerprint)s’. Wellicht worden jouw berichten onderschept!", - "verify_nop": "Sessie al geverifieerd!", - "verify_success_description": "De door jou verschafte sleutel en de van %(userId)ss sessie %(deviceId)s verkregen sleutels komen overeen. De sessie is daarmee geverifieerd.", - "verify_success_title": "Geverifieerde sleutel", - "verify_unknown_pair": "Onbekend paar (persoon, sessie): (%(userId)s, %(deviceId)s)", "whois": "Geeft informatie weer over een persoon" }, "space": { @@ -3155,20 +3139,6 @@ "switch_theme_dark": "Naar donkere modus wisselen", "switch_theme_light": "Naar lichte modus wisselen" }, - "voice_broadcast": { - "action": "Spraakuitzending", - "confirm_stop_affirm": "Ja, stop uitzending", - "confirm_stop_title": "Live uitzending stoppen?", - "failed_already_recording_description": "U neemt al een spraakuitzending op. Beëindig uw huidige spraakuitzending om een nieuwe te starten.", - "failed_already_recording_title": "Kan geen nieuwe spraakuitzending starten", - "failed_insufficient_permission_description": "U heeft niet de vereiste rechten om een spraakuitzending in deze kamer te starten. Neem contact op met een kamer beheerder om uw machtiging aan te passen.", - "failed_insufficient_permission_title": "Kan geen nieuwe spraakuitzending starten", - "failed_others_already_recording_description": "Iemand anders neemt al een spraakuitzending op. Wacht tot de spraakuitzending is afgelopen om een nieuwe te starten.", - "failed_others_already_recording_title": "Kan geen nieuwe spraakuitzending starten", - "pause": "spraakuitzending pauzeren", - "play": "spraakuitzending afspelen", - "resume": "hervat spraakuitzending" - }, "voip": { "already_in_call": "Al in de oproep", "already_in_call_person": "Je bent al in gesprek met deze persoon.", diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index e148c873f12..66ac8070804 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -340,7 +340,7 @@ "set_email_prompt": "Czy chcesz ustawić adres e-mail?", "sign_in_description": "Użyj swojego konta, aby kontynuować.", "sign_in_instead": "Zamiast tego zaloguj się", - "sign_in_instead_prompt": "Zamiast tego zaloguj się", + "sign_in_instead_prompt": "Masz już konto? Zaloguj się tutaj", "sign_in_or_register": "Zaloguj się lub utwórz konto", "sign_in_or_register_description": "Użyj konta lub utwórz nowe, aby kontynuować.", "sign_in_prompt": "Posiadasz już konto? Zaloguj się", @@ -505,6 +505,7 @@ "matrix": "Matrix", "message": "Wiadomość", "message_layout": "Wygląd wiadomości", + "message_timestamp_invalid": "Nieprawidłowy znacznik czasu", "microphone": "Mikrofon", "model": "Model", "modern": "Współczesny", @@ -908,6 +909,8 @@ "warning": "Jeżeli nie ustawiłeś nowej metody odzyskiwania, atakujący może uzyskać dostęp do Twojego konta. Zmień hasło konta i natychmiast ustaw nową metodę odzyskiwania w Ustawieniach." }, "not_supported": "", + "pinned_identity_changed": "Tożsamość użytkownika %(displayName)s (%(userId)s) uległa zmianie. Dowiedz się więcej", + "pinned_identity_changed_no_displayname": "Tożsamość użytkownika %(userId)s uległa zmianie Dowiedz się więcej", "recovery_method_removed": { "description_1": "Ta sesja wykryła, że Twoja fraza bezpieczeństwa i klucz dla bezpiecznych wiadomości zostały usunięte.", "description_2": "Jeśli zrobiłeś to przez pomyłkę, możesz ustawić bezpieczne wiadomości w tej sesji, co zaszyfruje ponownie historię wiadomości za pomocą nowej metody odzyskiwania.", @@ -923,7 +926,6 @@ }, "udd": { "interactive_verification_button": "Zweryfikuj interaktywnie za pomocą emoji", - "manual_verification_button": "Zweryfikuj ręcznie za pomocą tekstu", "other_ask_verify_text": "Poproś go/ją o zweryfikowanie tej sesji bądź zweryfikuj ją osobiście poniżej.", "other_new_session_text": "%(name)s%(userId)s zalogował się do nowej sesji bez zweryfikowania jej:", "own_ask_verify_text": "Zweryfikuj swoje pozostałe sesje używając jednej z opcji poniżej.", @@ -958,12 +960,6 @@ "incoming_sas_dialog_waiting": "Oczekiwanie na potwierdzenie partnera…", "incoming_sas_user_dialog_text_1": "Zweryfikuj tego użytkownika, aby oznaczyć go jako zaufanego. Użytkownicy zaufani dodają większej pewności, gdy korzystasz z wiadomości szyfrowanych end-to-end.", "incoming_sas_user_dialog_text_2": "Weryfikacja tego użytkownika oznaczy Twoją i jego sesję jako zaufaną.", - "manual_device_verification_device_id_label": "Identyfikator sesji", - "manual_device_verification_device_key_label": "Klucz sesji", - "manual_device_verification_device_name_label": "Nazwa sesji", - "manual_device_verification_footer": "Jeśli nie pasują, bezpieczeństwo twojego konta mogło zostać zdradzone.", - "manual_device_verification_self_text": "Potwierdź porównując następujące elementy w ustawieniach użytkownika w drugiej sesji:", - "manual_device_verification_user_text": "Potwierdź sesję tego użytkownika, porównując następujące elementy w jego ustawieniach użytkownika:", "no_key_or_device": "Wygląda na to, że nie masz klucza bezpieczeństwa ani żadnych innych urządzeń, które mogą weryfikować Twoją tożsamość. To urządzenie nie będzie mogło uzyskać dostępu do wcześniejszych zaszyfrowanych wiadomości. Aby zweryfikować swoją tożsamość na tym urządzeniu, należy zresetować klucze weryfikacyjne.", "no_support_qr_emoji": "Urządzenie, które próbujesz zweryfikować nie wspiera skanowania kodu QR lub weryfikacji emoji, czyli tego co obsługuje %(brand)s. Spróbuj użyć innego klienta.", "other_party_cancelled": "Druga strona anulowała weryfikację.", @@ -1003,7 +999,7 @@ "unverified_sessions_toast_description": "Sprawdź, by upewnić się że Twoje konto jest bezpieczne", "unverified_sessions_toast_reject": "Później", "unverified_sessions_toast_title": "Masz niezweryfikowane sesje", - "verification_description": "Zweryfikuj swoją tożsamość, aby uzyskać dostęp do wiadomości szyfrowanych i potwierdzić swoją tożsamość innym.", + "verification_description": "Zweryfikuj swoją tożsamość, aby uzyskać dostęp do wiadomości szyfrowanych i potwierdzić swoją tożsamość innym. Jeśli korzystasz z urządzenia mobilnego, otwórz na niej aplikację.", "verification_dialog_title_device": "Zweryfikuj drugie urządzenie", "verification_dialog_title_user": "Żądanie weryfikacji", "verification_skip_warning": "Bez weryfikacji, nie będziesz posiadać dostępu do wszystkich swoich wiadomości, a inni będą Cię widzieć jako niezaufanego.", @@ -1088,10 +1084,6 @@ }, "error_user_not_logged_in": "Użytkownik nie jest zalogowany", "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s zakończył transmisję głosową", - "you": "Zakończyłeś transmisje na żywo" - }, "m.call.answer": { "dm": "Połączenie w trakcie", "user": "%(senderName)s dołączył do połączenia", @@ -1113,7 +1105,15 @@ "you": "Dodano reakcję %(reaction)s do %(message)s" }, "m.sticker": "%(senderName)s: %(stickerName)s", - "m.text": "%(senderName)s: %(message)s" + "m.text": "%(senderName)s: %(message)s", + "prefix": { + "audio": "Audio", + "file": "Plik", + "image": "Obraz", + "poll": "Ankieta", + "video": "Wideo" + }, + "preview": "%(prefix)s: %(preview)s" }, "export_chat": { "cancelled": "Eksport został anulowany", @@ -1490,8 +1490,6 @@ "video_rooms_faq2_answer": "Tak, oś czasu czatu jest wyświetlana wraz z wideo.", "video_rooms_faq2_question": "Czy mogę używać kanału tekstowego jednocześnie rozmawiając na kanale wideo?", "video_rooms_feedbackSubheading": "Dziękujemy za wypróbowanie wersji beta, opisz wnikliwie swoje doświadczenia i pomóż nam ulepszyć nasz produkt.", - "voice_broadcast": "Transmisja głosowa", - "voice_broadcast_force_small_chunks": "Wymuś 15s długość kawałków dla transmisji głosowej", "wysiwyg_composer": "Bogaty edytor tekstu" }, "labs_mjolnir": { @@ -1637,7 +1635,6 @@ "mute_description": "Nie otrzymasz żadnych powiadomień" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s rozpoczął transmisję głosową", "m.key.verification.request": "%(name)s prosi o weryfikację" }, "onboarding": { @@ -2255,7 +2252,6 @@ "error_unbanning": "Nie udało się odbanować", "events_default": "Wysyłanie wiadomości", "invite": "Zapraszanie użytkowników", - "io.element.voice_broadcast_info": "Transmisje głosowe", "kick": "Usuń użytkowników", "m.call": "Rozpocznij połączenie %(brand)s", "m.call.member": "Dołącz do połączeń %(brand)s", @@ -2954,7 +2950,7 @@ "warning": "OSTRZEŻENIE: " }, "share": { - "link_title": "Link do pokoju", + "link_copied": "Skopiowano link", "permalink_message": "Link do zaznaczonej wiadomości", "permalink_most_recent": "Link do najnowszej wiadomości", "share_call": "Link zaproszenia do konferencji", @@ -3046,13 +3042,6 @@ "upgraderoom": "Ulepsza pokój do nowej wersji", "upgraderoom_permission_error": "Nie posiadasz wymaganych uprawnień do użycia tego polecenia.", "usage": "Użycie", - "verify": "Weryfikuje użytkownika, sesję oraz klucz publiczny", - "verify_mismatch": "OSTRZEŻENIE: WERYFIKACJA KLUCZY NIE POWIODŁA SIĘ! Klucz podpisujący dla %(userId)s oraz sesji %(deviceId)s to \"%(fprint)s\", nie pasuje on do podanego klucza \"%(fingerprint)s\". To może oznaczać że Twoja komunikacja jest przechwytywana!", - "verify_nop": "Sesja już zweryfikowana!", - "verify_nop_warning_mismatch": "OSTRZEŻENIE: sesja została już zweryfikowana, ale klucze NIE PASUJĄ!", - "verify_success_description": "Klucz podpisujący, który podano jest taki sam jak klucz podpisujący otrzymany od %(userId)s oraz sesji %(deviceId)s. Sesja została oznaczona jako zweryfikowana.", - "verify_success_title": "Zweryfikowany klucz", - "verify_unknown_pair": "Nieznana para (użytkownik, sesja): (%(userId)s, %(deviceId)s)", "view": "Przegląda pokój z podanym adresem", "whois": "Pokazuje informacje na temat użytkownika" }, @@ -3270,8 +3259,8 @@ "historical_event_no_key_backup": "Historia wiadomości nie jest dostępna na tym urządzeniu", "historical_event_unverified_device": "Musisz zweryfikować to urządzenie, aby wyświetlić historię wiadomości", "historical_event_user_not_joined": "Nie masz dostępu do tej wiadomości", - "sender_identity_previously_verified": "Zweryfikowana tożsamość uległa zmianie", - "sender_unsigned_device": "Zaszyfrowano przez urządzenie niezweryfikowane przez właściciela.", + "sender_identity_previously_verified": "Zweryfikowana tożsamość nadawcy uległa zmianie", + "sender_unsigned_device": "Wysłano z niezabezpieczonego urządzenia.", "unable_to_decrypt": "Nie można rozszyfrować wiadomości" }, "disambiguated_profile": "%(displayName)s (%(matrixId)s)", @@ -3289,10 +3278,6 @@ "error_rendering_message": "Nie można wczytać tej wiadomości", "historical_messages_unavailable": "Nie możesz widzieć poprzednich wiadomości", "in_room_name": " w %(room)s", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s zakończył transmisję głosową", - "you": "Zakończyłeś transmisje głosową" - }, "io.element.widgets.layout": "%(senderName)s zmienił układ pokoju", "late_event_separator": "Pierwotnie wysłano %(dateTime)s", "load_error": { @@ -3747,6 +3732,7 @@ "error_files_too_large": "Te pliki są zbyt duże do wysłania. Ograniczenie wielkości plików to %(limit)s.", "error_some_files_too_large": "Niektóre pliki są zbyt duże do wysłania. Ograniczenie wielkości plików to %(limit)s.", "error_title": "Błąd wysyłania", + "not_image": "Wybrany plik nie jest prawidłowym plikiem obrazu.", "title": "Prześlij pliki", "title_progress": "Prześlij pliki (%(current)s z %(total)s)", "upload_all_button": "Prześlij wszystko", @@ -3842,38 +3828,6 @@ "switch_theme_dark": "Przełącz na tryb ciemny", "switch_theme_light": "Przełącz na tryb jasny" }, - "voice_broadcast": { - "30s_backward": "30s do tyłu", - "30s_forward": "30s do przodu", - "action": "Transmisja głosowa", - "buffering": "Buforowanie…", - "confirm_listen_affirm": "Tak, zakończ moje nagranie", - "confirm_listen_description": "Jeśli zaczniesz słuchać tej transmisji na żywo, twoja bieżąca transmisja na żywo zostanie zakończona.", - "confirm_listen_title": "Zacząć słuchać transmisji na żywo?", - "confirm_stop_affirm": "Tak, zakończ transmisję", - "confirm_stop_description": "Czy na pewno chcesz zakończyć transmisję na żywo? Transmisja zostanie zakończona, a całe nagranie będzie dostępne w pokoju.", - "confirm_stop_title": "Zakończyć transmisję na żywo?", - "connection_error": "Błąd połączenia - Nagrywanie wstrzymane", - "failed_already_recording_description": "Już nagrywasz transmisję głosową. Zakończ bieżącą transmisję głosową, aby rozpocząć nową.", - "failed_already_recording_title": "Nie można rozpocząć nowej transmisji głosowej", - "failed_decrypt": "Nie można rozszyfrować transmisji głosowej", - "failed_generic": "Nie można odtworzyć tej transmisji głosowej", - "failed_insufficient_permission_description": "Nie posiadasz wymaganych uprawnień, aby rozpocząć transmisję głosową w tym pokoju. Skontaktuj się z administratorem pokoju, aby zwiększyć swoje uprawnienia.", - "failed_insufficient_permission_title": "Nie można rozpocząć nowej transmisji głosowej", - "failed_no_connection_description": "Niestety, nie jesteśmy w stanie rozpocząć nowego nagrania. Spróbuj ponownie później.", - "failed_no_connection_title": "Błąd połączenia", - "failed_others_already_recording_description": "Ktoś już nagrywa transmisję głosową. Aby rozpocząć nową, poczekaj aż bieżąca się skończy.", - "failed_others_already_recording_title": "Nie można rozpocząć nowej transmisji głosowej", - "go_live": "Rozpocznij transmisję", - "live": "Na żywo", - "pause": "wstrzymaj transmisję głosową", - "play": "odtwórz transmisję głosową", - "resume": "wznów transmisję głosową" - }, - "voice_message": { - "cant_start_broadcast_description": "Nie możesz rozpocząć wiadomości głosowej, ponieważ już nagrywasz transmisję na żywo. Zakończ transmisję na żywo, aby rozpocząć nagrywanie wiadomości głosowej.", - "cant_start_broadcast_title": "Nie można rozpocząć wiadomości głosowej" - }, "voip": { "already_in_call": "Już dzwoni", "already_in_call_person": "Prowadzisz już rozmowę z tą osobą.", @@ -3893,7 +3847,6 @@ "camera_disabled": "Twoja kamera jest wyłączona", "camera_enabled": "Twoja kamera jest nadal włączona", "cannot_call_yourself_description": "Nie możesz wykonać połączenia do siebie.", - "change_input_device": "Zmień urządzenie wejściowe", "close_lobby": "Zamknij poczekalnię", "connecting": "Łączenie", "connection_lost": "Połączenie z serwerem zostało przerwane", @@ -3912,8 +3865,6 @@ "enable_camera": "Włącz kamerę", "enable_microphone": "Wyłącz wyciszenie mikrofonu", "expand": "Wróć do połączenia", - "failed_call_live_broadcast_description": "Nie możesz rozpocząć połączenia, ponieważ już nagrywasz transmisję na żywo. Zakończ transmisję na żywo, aby rozpocząć połączenie.", - "failed_call_live_broadcast_title": "Nie można rozpocząć połączenia", "get_call_link": "Udostępnij link do połączenia", "hangup": "Rozłącz", "hide_sidebar_button": "Ukryj pasek boczny", diff --git a/src/i18n/strings/pt_BR.json b/src/i18n/strings/pt_BR.json index 6789fb4ee66..7813d6146c0 100644 --- a/src/i18n/strings/pt_BR.json +++ b/src/i18n/strings/pt_BR.json @@ -674,12 +674,6 @@ "incoming_sas_dialog_title": "Recebendo solicitação de confirmação", "incoming_sas_user_dialog_text_1": "Confirme este usuário para torná-lo confiável. Confiar nos usuários fornece segurança adicional ao trocar mensagens criptografadas de ponta a ponta.", "incoming_sas_user_dialog_text_2": "Se você confirmar esse usuário, a sessão será marcada como confiável para você e para ele.", - "manual_device_verification_device_id_label": "Identificador de sessão", - "manual_device_verification_device_key_label": "Chave da sessão", - "manual_device_verification_device_name_label": "Nome da sessão", - "manual_device_verification_footer": "Se eles não corresponderem, a segurança da sua comunicação pode estar comprometida.", - "manual_device_verification_self_text": "Para confirmar, compare a seguinte informação com aquela apresentada em sua outra sessão:", - "manual_device_verification_user_text": "Confirme a sessão deste usuário comparando o seguinte com as configurações deste usuário:", "other_party_cancelled": "Seu contato cancelou a confirmação.", "prompt_encrypted": "Verifique todos os usuários em uma sala para se certificar de que ela está segura.", "prompt_self": "Iniciar a confirmação novamente, após a notificação.", @@ -765,10 +759,6 @@ } }, "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s encerrou uma transmissão de voz", - "you": "Você encerrou uma transmissão de voz" - }, "m.call.answer": { "dm": "Chamada em andamento", "user": "%(senderName)s entrou na chamada", @@ -1082,7 +1072,6 @@ "mute_description": "Você não receberá nenhuma notificação" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s iniciou uma transmissão de voz", "m.key.verification.request": "%(name)s está solicitando confirmação" }, "onboarding": { @@ -1970,12 +1959,6 @@ "upgraderoom": "Atualiza a sala para uma nova versão", "upgraderoom_permission_error": "Você não tem as permissões necessárias para usar este comando.", "usage": "Uso", - "verify": "Confirma um usuário, sessão, e chave criptografada pública", - "verify_mismatch": "ATENÇÃO: A CONFIRMAÇÃO DA CHAVE FALHOU! A chave de assinatura para %(userId)s e sessão %(deviceId)s é \"%(fprint)s\", o que não corresponde à chave fornecida \"%(fingerprint)s\". Isso pode significar que suas comunicações estejam sendo interceptadas por terceiros!", - "verify_nop": "Sessão já confirmada!", - "verify_success_description": "A chave de assinatura que você forneceu corresponde à chave de assinatura que você recebeu da sessão %(deviceId)s do usuário %(userId)s. Esta sessão foi marcada como confirmada.", - "verify_success_title": "Chave confirmada", - "verify_unknown_pair": "Par desconhecido (usuário, sessão): (%(userId)s, %(deviceId)s)", "whois": "Exibe informação sobre um usuário" }, "space": { @@ -2101,10 +2084,6 @@ "error_no_renderer": "Este evento não pôde ser exibido", "error_rendering_message": "Não foi possível carregar esta mensagem", "historical_messages_unavailable": "Você não pode ver as mensagens anteriores", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s encerrou uma transmissão de voz", - "you": "Você encerrou uma transmissão de voz" - }, "io.element.widgets.layout": "%(senderName)s atualizou o layout da sala", "load_error": { "no_permission": "Não foi possível carregar um trecho específico da conversa desta sala, porque parece que você não tem permissão para ler a mensagem em questão.", @@ -2537,21 +2516,6 @@ "switch_theme_dark": "Alternar para o modo escuro", "switch_theme_light": "Alternar para o modo claro" }, - "voice_broadcast": { - "confirm_listen_description": "Se você começar a ouvir esta tramissão ao vivo, a gravação desta transmissão, será encerrada.", - "confirm_listen_title": "Ouvir transmissão ao vivo?", - "confirm_stop_affirm": "Sim, interromper a transmissão", - "confirm_stop_title": "Parar a transmissão ao vivo?", - "failed_already_recording_description": "Você já está gravando uma transmissão de voz. Encerre sua transmissão de voz atual para iniciar uma nova.", - "failed_already_recording_title": "Não é possível iniciar uma nova transmissão de voz", - "failed_insufficient_permission_description": "Você não tem as permissões necessárias para iniciar uma transmissão de voz nesta sala. Entre em contato com um administrador de sala para atualizar suas permissões.", - "failed_insufficient_permission_title": "Não é possível iniciar uma nova transmissão de voz", - "failed_no_connection_description": "Infelizmente, não podemos iniciar uma gravação agora. Por favor, tente novamente mais tarde.", - "failed_no_connection_title": "Erro de conexão", - "failed_others_already_recording_description": "Outra pessoa já está gravando uma transmissão de voz. Aguarde o término da transmissão de voz para iniciar uma nova.", - "failed_others_already_recording_title": "Não é possível iniciar uma nova transmissão de voz", - "live": "Ao vivo" - }, "voip": { "already_in_call": "Já em um chamada", "already_in_call_person": "Você já está em uma chamada com essa pessoa.", @@ -2582,8 +2546,6 @@ "enable_camera": "Ligar câmera", "enable_microphone": "Habilitar microfone", "expand": "Retornar para a chamada", - "failed_call_live_broadcast_description": "Você não pode iniciar uma chamada porque está gravando uma transmissão ao vivo. Termine sua transmissão ao vivo para iniciar uma chamada.", - "failed_call_live_broadcast_title": "Não é possível iniciar uma chamada", "hangup": "Desligar", "hide_sidebar_button": "Esconder a barra lateral", "join_button_tooltip_connecting": "Conectando", diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index 50a3ee50e97..555cb3d1f9b 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -877,7 +877,6 @@ }, "udd": { "interactive_verification_button": "Интерактивная сверка по смайлам", - "manual_verification_button": "Ручная сверка по тексту", "other_ask_verify_text": "Попросите этого пользователя подтвердить сеанс или подтвердите его вручную ниже.", "other_new_session_text": "%(name)s (%(userId)s) произвел(а) вход через новый сеанс без подтверждения:", "own_ask_verify_text": "Подтвердите ваш другой сеанс, используя один из вариантов ниже.", @@ -912,12 +911,6 @@ "incoming_sas_dialog_waiting": "Ожидаем подтверждения от партнера…", "incoming_sas_user_dialog_text_1": "Проверить этого пользователя, чтобы отметить его, как доверенного. Доверенные пользователи дают вам больше уверенности при использовании шифрованных сообщений.", "incoming_sas_user_dialog_text_2": "Подтверждение этого пользователя сделает его сеанс доверенным у вас, а также сделает ваш сеанс доверенным у него.", - "manual_device_verification_device_id_label": "ID сеанса", - "manual_device_verification_device_key_label": "Ключ сеанса", - "manual_device_verification_device_name_label": "Название сеанса", - "manual_device_verification_footer": "Если они не совпадают, безопасность вашего общения может быть поставлена под угрозу.", - "manual_device_verification_self_text": "Сравните следующие параметры с \"Пользовательскими настройками\" в другом вашем сеансе:", - "manual_device_verification_user_text": "Подтвердите сеанс этого пользователя, сравнив следующие параметры с его \"Пользовательскими настройками\":", "no_key_or_device": "Похоже, у вас нет бумажного ключа, или других сеансов, с которыми вы могли бы свериться. В этом сеансе вы не сможете получить доступ к старым зашифрованным сообщениям. Чтобы подтвердить свою личность в этом сеансе, вам нужно будет сбросить свои ключи шифрования.", "no_support_qr_emoji": "Устройство, которое вы пытаетесь проверить, не поддерживает сканирование QR-кода или проверку смайликов, которые поддерживает %(brand)s. Попробуйте использовать другой клиент.", "other_party_cancelled": "Другая сторона отменила проверку.", @@ -1036,10 +1029,6 @@ }, "error_user_not_logged_in": "Пользователь не вошел в систему", "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s завершил(а) голосовую трансляцию", - "you": "Вы завершили голосовую трансляцию" - }, "m.call.answer": { "dm": "Звонок в процессе", "user": "%(senderName)s присоединился(лась) к звонку", @@ -1412,8 +1401,6 @@ "video_rooms_faq2_answer": "Да, лента сообщений отображается рядом с видео.", "video_rooms_faq2_question": "Можно ли использовать текстовый чат одновременно с видеозвонком?", "video_rooms_feedbackSubheading": "Спасибо, что попробовали бета-версию. Пожалуйста, расскажите как можно подробнее, чтобы мы могли ее улучшить.", - "voice_broadcast": "Голосовая трансляция", - "voice_broadcast_force_small_chunks": "Длина фрагмента голосовой трансляции 15s", "wysiwyg_composer": "Наглядный текстовый редактор" }, "labs_mjolnir": { @@ -1550,7 +1537,6 @@ "mute_description": "Вы не будете получать никаких уведомлений" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s начал(а) голосовую трансляцию", "m.key.verification.request": "%(name)s запрашивает проверку" }, "onboarding": { @@ -2126,7 +2112,6 @@ "error_unbanning": "Не удалось разблокировать", "events_default": "Отправить сообщения", "invite": "Пригласить пользователей", - "io.element.voice_broadcast_info": "Голосовые трансляции", "kick": "Удалять пользователей", "m.call": "Начать %(brand)s звонок", "m.call.member": "Присоединяйтесь к %(brand)s звонку", @@ -2769,7 +2754,6 @@ "warning": "ВНИМАНИЕ: " }, "share": { - "link_title": "Ссылка на комнату", "permalink_message": "Ссылка на выбранное сообщение", "permalink_most_recent": "Ссылка на последнее сообщение", "title_message": "Поделиться сообщением", @@ -2858,13 +2842,6 @@ "upgraderoom": "Обновляет комнату до новой версии", "upgraderoom_permission_error": "У вас нет необходимых разрешений для использования этой команды.", "usage": "Использование", - "verify": "Проверяет пользователя, сеанс и публичные ключи", - "verify_mismatch": "ВНИМАНИЕ: ПРОВЕРКА КЛЮЧА НЕ ПРОШЛА! Ключом подписи для %(userId)s и сеанса %(deviceId)s является \"%(fprint)s\", что не соответствует указанному ключу \"%(fingerprint)s\". Это может означать, что ваши сообщения перехватываются!", - "verify_nop": "Сеанс уже подтверждён!", - "verify_nop_warning_mismatch": "ВНИМАНИЕ: сеанс уже заверен, но ключи НЕ СОВПАДАЮТ!", - "verify_success_description": "Ключ подписи, который вы предоставили, соответствует ключу подписи, который вы получили от пользователя %(userId)s через сеанс %(deviceId)s. Сеанс отмечен как подтверждённый.", - "verify_success_title": "Ключ проверен", - "verify_unknown_pair": "Неизвестная пара (пользователь, сеанс): (%(userId)s, %(deviceId)s)", "view": "Просмотр комнаты с указанным адресом", "whois": "Показать информацию о пользователе" }, @@ -3080,10 +3057,6 @@ "error_rendering_message": "Не удалось загрузить это сообщение", "historical_messages_unavailable": "Вы не можете просматривать более старые сообщения", "in_room_name": " в %(room)s", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s завершил(а) голосовую трансляцию", - "you": "Вы завершили голосовую трансляцию" - }, "io.element.widgets.layout": "%(senderName)s обновил(а) макет комнаты", "late_event_separator": "Первоначально отправлено %(dateTime)s", "load_error": { @@ -3618,38 +3591,6 @@ "switch_theme_dark": "Переключить в тёмный режим", "switch_theme_light": "Переключить в светлый режим" }, - "voice_broadcast": { - "30s_backward": "30с назад", - "30s_forward": "30с вперёд", - "action": "Голосовая трансляция", - "buffering": "Буферизация…", - "confirm_listen_affirm": "Да, закончить мою запись", - "confirm_listen_description": "Если вы начнете слушать данную трансляцию, то текущая запись прямой трансляции будет завершена.", - "confirm_listen_title": "Слушать прямой эфир?", - "confirm_stop_affirm": "Да, остановить трансляцию", - "confirm_stop_description": "Вы действительно хотите прекратить прямую трансляцию? На этом трансляция закончится и полная запись будет доступна в комнате.", - "confirm_stop_title": "Закончить голосовую трансляцию?", - "connection_error": "Ошибка подключения — запись приостановлена", - "failed_already_recording_description": "Вы уже записываете голосовую трансляцию. Пожалуйста, завершите текущую голосовую трансляцию, чтобы начать новую.", - "failed_already_recording_title": "Не получилось начать новую голосовую трансляцию", - "failed_decrypt": "Невозможно расшифровать голосовую трансляцию", - "failed_generic": "Невозможно воспроизвести эту голосовую трансляцию", - "failed_insufficient_permission_description": "У вас нет необходимых разрешений, чтобы начать голосовую трансляцию в этой комнате. Свяжитесь с администратором комнаты для получения разрешений.", - "failed_insufficient_permission_title": "Не получилось начать новую голосовую трансляцию", - "failed_no_connection_description": "К сожалению, сейчас мы не можем начать запись. Пожалуйста, попробуйте позже.", - "failed_no_connection_title": "Ошибка подключения", - "failed_others_already_recording_description": "Кто-то уже записывает голосовую трансляцию. Ждите окончания их голосовой трансляции, чтобы начать новую.", - "failed_others_already_recording_title": "Не получилось начать новую голосовую трансляцию", - "go_live": "Начать эфир", - "live": "В эфире", - "pause": "приостановить голосовую трансляцию", - "play": "проиграть голосовую трансляцию", - "resume": "продолжить голосовую трансляцию" - }, - "voice_message": { - "cant_start_broadcast_description": "Вы не можете начать голосовое сообщение, так как вы сейчас записываете прямую трансляцию. Завершите прямую трансляцию, чтобы начать запись голосового сообщения.", - "cant_start_broadcast_title": "Не удается запустить голосовое сообщение" - }, "voip": { "already_in_call": "Уже в вызове", "already_in_call_person": "Вы уже разговариваете с этим человеком.", @@ -3669,7 +3610,6 @@ "camera_disabled": "Ваша камера выключена", "camera_enabled": "Ваша камера всё ещё включена", "cannot_call_yourself_description": "Вы не можете позвонить самому себе.", - "change_input_device": "Смените устройство ввода", "connecting": "Подключение", "connection_lost": "Соединение с сервером потеряно", "connection_lost_description": "Вы не можете совершать вызовы без подключения к серверу.", @@ -3686,8 +3626,6 @@ "enable_camera": "Включить камеру", "enable_microphone": "Включить микрофон", "expand": "Вернуться к звонку", - "failed_call_live_broadcast_description": "Вы не можете начать звонок, так как вы производите живое вещание. Пожалуйста, остановите вещание, чтобы начать звонок.", - "failed_call_live_broadcast_title": "Невозможно начать звонок", "hangup": "Повесить трубку", "hide_sidebar_button": "Скрыть боковую панель", "input_devices": "Устройства ввода", diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json index 34ba4789bf5..eab25be92bb 100644 --- a/src/i18n/strings/sk.json +++ b/src/i18n/strings/sk.json @@ -877,7 +877,6 @@ }, "udd": { "interactive_verification_button": "Interaktívne overte pomocou emotikonov", - "manual_verification_button": "Manuálne overte pomocou textu", "other_ask_verify_text": "Poproste tohto používateľa, aby si overil svoju reláciu alebo ju nižšie manuálne overte.", "other_new_session_text": "%(name)s (%(userId)s) sa prihlásil do novej relácie bez jej overenia:", "own_ask_verify_text": "Overte svoje ostatné relácie pomocou jednej z nižšie uvedených možností.", @@ -912,12 +911,6 @@ "incoming_sas_dialog_waiting": "Čakanie na potvrdenie od partnera…", "incoming_sas_user_dialog_text_1": "Overte tohto používateľa a označte ho ako dôveryhodného. Dôveryhodní používatelia vám poskytujú dodatočný pokoj na duši pri používaní end-to-end šifrovaných správ.", "incoming_sas_user_dialog_text_2": "Overenie tohto používateľa označí jeho reláciu ako dôveryhodnú a zároveň označí vašu reláciu ako dôveryhodnú pre neho.", - "manual_device_verification_device_id_label": "ID relácie", - "manual_device_verification_device_key_label": "Kľúč relácie", - "manual_device_verification_device_name_label": "Názov relácie", - "manual_device_verification_footer": "Ak sa nezhodujú, môže byť ohrozená bezpečnosť vašej komunikácie.", - "manual_device_verification_self_text": "Potvrďte to porovnaním nasledujúcich údajov s nastaveniami používateľa v inej vašej relácii:", - "manual_device_verification_user_text": "Potvrďte reláciu tohto používateľa porovnaním nasledujúcich údajov s jeho nastaveniami používateľa:", "no_key_or_device": "Vyzerá to, že nemáte bezpečnostný kľúč ani žiadne iné zariadenie, pomocou ktorého by ste to mohli overiť. Toto zariadenie nebude mať prístup k starým zašifrovaným správam. Ak chcete overiť svoju totožnosť na tomto zariadení, budete musieť obnoviť svoje overovacie kľúče.", "no_support_qr_emoji": "Zariadenie, ktoré sa snažíte overiť, nepodporuje overenie skenovaním QR kódu ani overenie pomocou emotikonov, ktoré podporuje aplikácia %(brand)s. Skúste použiť iného klienta.", "other_party_cancelled": "Proti strana zrušila overovanie.", @@ -1037,10 +1030,6 @@ }, "error_user_not_logged_in": "Používateľ nie je prihlásený", "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s ukončil/a hlasové vysielanie", - "you": "Ukončili ste hlasové vysielanie" - }, "m.call.answer": { "dm": "Práve prebieha hovor", "user": "%(senderName)s sa pridal/a do hovoru", @@ -1417,8 +1406,6 @@ "video_rooms_faq2_answer": "Áno, časová os konverzácie sa zobrazuje spolu s videom.", "video_rooms_faq2_question": "Môžem popri videohovore používať aj textovú konverzáciu?", "video_rooms_feedbackSubheading": "Ďakujeme, že ste vyskúšali beta verziu, prosím, uveďte čo najviac podrobností, aby sme ju mohli vylepšiť.", - "voice_broadcast": "Hlasové vysielanie", - "voice_broadcast_force_small_chunks": "Vynútiť 15s dĺžku sekcie hlasového vysielania", "wysiwyg_composer": "Rozšírený textový editor" }, "labs_mjolnir": { @@ -1555,7 +1542,6 @@ "mute_description": "Nebudete dostávať žiadne oznámenia" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s začal/a hlasové vysielanie", "m.key.verification.request": "%(name)s žiada o overenie" }, "onboarding": { @@ -2130,7 +2116,6 @@ "error_unbanning": "Nepodarilo sa povoliť vstup", "events_default": "Odoslať správy", "invite": "Pozvať používateľov", - "io.element.voice_broadcast_info": "Hlasové vysielania", "kick": "Odstrániť používateľov", "m.call": "Spustiť %(brand)s hovory", "m.call.member": "Pripojiť sa k %(brand)s hovorom", @@ -2772,7 +2757,6 @@ "warning": "UPOZORNENIE: " }, "share": { - "link_title": "Odkaz na miestnosť", "permalink_message": "Odkaz na vybratú správu", "permalink_most_recent": "Odkaz na najnovšiu správu", "title_message": "Zdieľať správu z miestnosti", @@ -2861,13 +2845,6 @@ "upgraderoom": "Aktualizuje miestnosť na novšiu verziu", "upgraderoom_permission_error": "Na použitie tohoto príkazu nemáte dostatočné povolenia.", "usage": "Použitie", - "verify": "Overí používateľa, reláciu a verejné kľúče", - "verify_mismatch": "VAROVANIE: OVERENIE KĽÚČOV ZLYHALO! Podpisový kľúč používateľa %(userId)s a relácia %(deviceId)s je \"%(fprint)s\" čo nezodpovedá zadanému kľúču \"%(fingerprint)s\". Môže to znamenať, že vaša komunikácia je odpočúvaná!", - "verify_nop": "Relácia je už overená!", - "verify_nop_warning_mismatch": "VAROVANIE: Relácia je už overená, ale kľúče sa NEZHODUJÚ!", - "verify_success_description": "Zadaný podpisový kľúč sa zhoduje s podpisovým kľúčom, ktorý ste dostali z relácie používateľa %(userId)s %(deviceId)s. Relácia označená ako overená.", - "verify_success_title": "Kľúč overený", - "verify_unknown_pair": "Neznámy pár (používateľ, relácia): (%(userId)s, %(deviceId)s)", "view": "Zobrazí miestnosti s danou adresou", "whois": "Zobrazuje informácie o používateľovi" }, @@ -3082,10 +3059,6 @@ "error_rendering_message": "Nemožno načítať túto správu", "historical_messages_unavailable": "Nemôžete vidieť predchádzajúce správy", "in_room_name": " v %(room)s", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s ukončil/a hlasové vysielanie", - "you": "Ukončili ste hlasové vysielanie" - }, "io.element.widgets.layout": "%(senderName)s aktualizoval usporiadanie miestnosti", "late_event_separator": "Pôvodne odoslané %(dateTime)s", "load_error": { @@ -3647,38 +3620,6 @@ "switch_theme_dark": "Prepnúť na tmavý režim", "switch_theme_light": "Prepnúť na svetlý režim" }, - "voice_broadcast": { - "30s_backward": "30s späť", - "30s_forward": "30s dopredu", - "action": "Hlasové vysielanie", - "buffering": "Načítavanie do vyrovnávacej pamäte…", - "confirm_listen_affirm": "Áno, ukončiť moje nahrávanie", - "confirm_listen_description": "Ak začnete počúvať toto živé vysielanie, váš aktuálny záznam živého vysielania sa ukončí.", - "confirm_listen_title": "Počúvať živé vysielanie?", - "confirm_stop_affirm": "Áno, zastaviť vysielanie", - "confirm_stop_description": "Určite chcete zastaviť vysielanie naživo? Tým sa vysielanie ukončí a v miestnosti bude k dispozícii celý záznam.", - "confirm_stop_title": "Zastaviť vysielanie naživo?", - "connection_error": "Chyba pripojenia - nahrávanie pozastavené", - "failed_already_recording_description": "Už nahrávate hlasové vysielanie. Ukončite aktuálne hlasové vysielanie a spustite nové.", - "failed_already_recording_title": "Nemôžete spustiť nové hlasové vysielanie", - "failed_decrypt": "Hlasové vysielanie sa nedá dešifrovať", - "failed_generic": "Toto hlasové vysielanie nie je možné prehrať", - "failed_insufficient_permission_description": "Nemáte požadované oprávnenia na spustenie hlasového vysielania v tejto miestnosti. Obráťte sa na správcu miestnosti, aby vám rozšíril oprávnenia.", - "failed_insufficient_permission_title": "Nemôžete spustiť nové hlasové vysielanie", - "failed_no_connection_description": "Bohužiaľ teraz nemôžeme spustiť nahrávanie. Skúste to prosím neskôr.", - "failed_no_connection_title": "Chyba pripojenia", - "failed_others_already_recording_description": "Niekto iný už nahráva hlasové vysielanie. Počkajte, kým sa skončí jeho hlasové vysielanie, a potom spustite nové.", - "failed_others_already_recording_title": "Nemôžete spustiť nové hlasové vysielanie", - "go_live": "Prejsť naživo", - "live": "Naživo", - "pause": "pozastaviť hlasové vysielanie", - "play": "spustiť hlasové vysielanie", - "resume": "obnoviť hlasové vysielanie" - }, - "voice_message": { - "cant_start_broadcast_description": "Nemôžete spustiť hlasovú správu, pretože práve nahrávate živé vysielanie. Ukončite prosím živé vysielanie, aby ste mohli začať nahrávať hlasovú správu.", - "cant_start_broadcast_title": "Nemožno spustiť hlasovú správu" - }, "voip": { "already_in_call": "Hovor už prebieha", "already_in_call_person": "S touto osobou už hovor prebieha.", @@ -3698,7 +3639,6 @@ "camera_disabled": "Váš fotoaparát je vypnutý", "camera_enabled": "Fotoaparát je stále zapnutý", "cannot_call_yourself_description": "Nemôžete zavolať samému sebe.", - "change_input_device": "Zmeniť vstupné zariadenie", "connecting": "Pripájanie", "connection_lost": "Spojenie so serverom bolo prerušené", "connection_lost_description": "Bez pripojenia k serveru nie je možné uskutočňovať hovory.", @@ -3715,8 +3655,6 @@ "enable_camera": "Zapnúť kameru", "enable_microphone": "Zrušiť stlmenie mikrofónu", "expand": "Návrat k hovoru", - "failed_call_live_broadcast_description": "Nemôžete spustiť hovor, pretože práve nahrávate živé vysielanie. Ukončite živé vysielanie, aby ste mohli začať hovor.", - "failed_call_live_broadcast_title": "Nie je možné začať hovor", "hangup": "Zavesiť", "hide_sidebar_button": "Skryť bočný panel", "input_devices": "Vstupné zariadenia", diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index b7258c26cbf..356480535fd 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -835,7 +835,6 @@ }, "udd": { "interactive_verification_button": "Verifikojeni në mënyrë ndërvepruese përmes emoji-sh", - "manual_verification_button": "Verifikojeni dorazi përmes teksti", "other_ask_verify_text": "Kërkojini këtij përdoruesi të verifikojë sesionin e vet, ose ta verifikojë më poshtë dorazi.", "other_new_session_text": "%(name)s (%(userId)s) bëri hyrjen në një sesion të ri pa e verifikuar:", "own_ask_verify_text": "Verifikoni sesionit tuaj tjetër duke përdorur një nga mundësitë më poshtë.", @@ -870,12 +869,6 @@ "incoming_sas_dialog_waiting": "Po pritet ripohimi nga partneri…", "incoming_sas_user_dialog_text_1": "Verifikojeni këtë përdorues që t’i vihet shenjë si i besuar. Përdoruesit e besuar ju më tepër siguri kur përdorni mesazhe të fshehtëzuar skaj-më-skaj.", "incoming_sas_user_dialog_text_2": "Verifikimi i këtij përdoruesi do t’i vërë shenjë sesionit të tij si të besuar dhe sesionit tuaj si të besuar për ta.", - "manual_device_verification_device_id_label": "ID sesioni", - "manual_device_verification_device_key_label": "Kyç sesioni", - "manual_device_verification_device_name_label": "Emër sesioni", - "manual_device_verification_footer": "Nëse s’përputhen, siguria e komunikimeve tuaja mund të jetë komprometuar.", - "manual_device_verification_self_text": "Ripohojeni duke krahasuar sa vijon me Rregullimet e Përdoruesit te sesioni juaj tjetër:", - "manual_device_verification_user_text": "Ripohojeni këtë sesion përdoruesi duke krahasuar sa vijon me Rregullimet e tij të Përdoruesit:", "no_key_or_device": "Duket sikur s’keni Kyç Sigurie ose ndonjë pajisje tjetër nga e cila mund të bëni verifikimin. Kjo pajisje s’do të jetë në gjendje të hyjë te mesazhe të dikurshëm të fshehtëzuar. Që të mund të verifikohet identiteti juaj në këtë pajisje, ju duhet të riujdisni kyçet tuaj të verifikimit.", "no_support_qr_emoji": "Pajisja që po provoni të verifikoni nuk mbulon skanim të një kodi QR, apo verifikim me emoji, çka janë ato që mbulohen prej %(brand)s. Provoni me një klient tjetër.", "other_party_cancelled": "Pala tjetër e anuloi verifikimin.", @@ -991,10 +984,6 @@ } }, "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s përfundoi një transmetim zanor", - "you": "Përfunduat një transmetim zanor" - }, "m.call.answer": { "dm": "Thirrje në ecuri e sipër", "user": "%(senderName)s u bë pjesë e thirrjes", @@ -1329,8 +1318,6 @@ "video_rooms_faq1_question": "Si mund të krijoj një dhomë me video?", "video_rooms_faq2_answer": "Po, rrjedha kohore e fjalosjes shfaqet tok me videon.", "video_rooms_faq2_question": "A mund të përdor fjalosje me tekst në krah të thirrjes video?", - "voice_broadcast": "Transmetim zanor", - "voice_broadcast_force_small_chunks": "Detyro gjatësi copëzash transmetimi zanor prej 15s", "wysiwyg_composer": "Përpunues teksti të pasur" }, "labs_mjolnir": { @@ -1465,7 +1452,6 @@ "mute_description": "S’do të merrni ndonjë njoftim" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s nisi një transmetim zanor", "m.key.verification.request": "%(name)s po kërkon verifikim" }, "onboarding": { @@ -2005,7 +1991,6 @@ "error_unbanning": "S’u arrit t’i hiqej dëbimi", "events_default": "Dërgoni mesazhe", "invite": "Ftoni përdorues", - "io.element.voice_broadcast_info": "Transmetime zanore", "kick": "Hiqni përdorues", "m.call": "Nisni thirrje %(brand)s", "m.call.member": "Merrni pjesë në thirrje %(brand)s", @@ -2599,7 +2584,6 @@ "warning": "KUJDES: " }, "share": { - "link_title": "Lidhje për te dhoma", "permalink_message": "Lidhje për te mesazhi i përzgjedhur", "permalink_most_recent": "Lidhje për te mesazhet më të freskët", "title_message": "Ndani Me të Tjerë Mesazh Dhome", @@ -2684,13 +2668,6 @@ "upgraderoom": "E kalon një dhomë te një version i ri i përmirësuar", "upgraderoom_permission_error": "S’keni lejet e domosdoshme për përdorimin e këtij urdhri.", "usage": "Përdorim", - "verify": "Verifikon një përdorues, sesion dhe një set kyçesh publikë", - "verify_mismatch": "KUJDES: VERIFIKIMI I KYÇIT DËSHTOI! Kyçi i nënshkrimit për %(userId)s dhe sesionin %(deviceId)s është \"%(fprint)s\", që nuk përputhet me kyçin e dhënë \"%(fingerprint)s\". Kjo mund të jetë shenjë se komunikimet tuaja po përgjohen!", - "verify_nop": "Sesion i tashmë i verifikuar!", - "verify_nop_warning_mismatch": "KUJDES: sesion tashmë i verifikuar, por kyçet NUK PËRPUTHEN!", - "verify_success_description": "Kyçi i nënshkrimit që dhatë përputhet me kyçin e nënshkrimit që morët nga sesioni i %(userId)s %(deviceId)s. Sesionit iu vu shenjë si i verifikuar.", - "verify_success_title": "Kyç i verifikuar", - "verify_unknown_pair": "Çift (përdorues, sesion) i pavlefshëm: (%(userId)s, %(deviceId)s)", "whois": "Shfaq të dhëna rreth një përdoruesi" }, "space": { @@ -2893,10 +2870,6 @@ "error_rendering_message": "S’ngarkohet dot ky mesazh", "historical_messages_unavailable": "S’mund të shihni mesazhe më të hershëm", "in_room_name": " në %(room)s", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s përfundoi një transmetim zanor", - "you": "Përfunduat një transmetim zanor" - }, "io.element.widgets.layout": "%(senderName)s ka përditësuar skemën e dhomës", "load_error": { "no_permission": "U provua të ngarkohej një pikë e caktuar në kronologjinë e kësaj dhome, por nuk keni leje për ta parë mesazhin në fjalë.", @@ -3411,36 +3384,6 @@ "switch_theme_dark": "Kalo nën mënyrën e errët", "switch_theme_light": "Kalo nën mënyrën e çelët" }, - "voice_broadcast": { - "30s_backward": "30s mbrapsht", - "30s_forward": "30s përpara", - "action": "Transmetim zanor", - "confirm_listen_affirm": "Po, përfundoje regjistrimin tim", - "confirm_listen_description": "Nëse filloni të dëgjoni te ky transmetim i drejtpërdrejtë, regjistrimi juaj i tanishëm i një transmetimi të drejtpërdrejtë do të përfundojë.", - "confirm_listen_title": "Të dëgjohet te transmetimi i drejtpërdrejtë?", - "confirm_stop_affirm": "Po, ndale transmetimin zanor", - "confirm_stop_description": "Jeni i sigurt se doni të ndalet transmetimi juaj i drejtpërdrejtë? Kjo do të përfundojë transmetimin dhe regjistrimi i plotë do të jetë i passhëm te dhoma.", - "confirm_stop_title": "Të ndalet transmetimi i drejtpërdrejtë?", - "connection_error": "Gabim lidhjeje - Regjistrimi u ndal", - "failed_already_recording_description": "Po incizoni tashmë një transmetim zanor. Ju lutemi, që të nisni një të ri, përfundoni transmetimin tuaj zanor të tanishëm.", - "failed_already_recording_title": "S’niset dot një transmetim zanor i ri", - "failed_decrypt": "S’arrihet të shfshehtëzohet transmetim zanor", - "failed_generic": "S’arrihet të luhet ky transmetim zanor", - "failed_insufficient_permission_description": "S’keni lejet e domosdoshme që të nisni një transmetim zanor në këtë dhomë. Lidhuni me një përgjegjës dhome që të përmirësoni lejet tuaja.", - "failed_insufficient_permission_title": "S’niset dot një transmetim zanor i ri", - "failed_no_connection_description": "Mjerisht, s’qemë në gjendje të nisnim tani një regjistrim. Ju lutemi, riprovoni më vonë.", - "failed_no_connection_title": "Gabim lidhjeje", - "failed_others_already_recording_description": "Dikush tjetër është duke incizuar një transmetim zanor. Që të nisni një të ri, prisni të përfundojë incizimi zanor i tij.", - "failed_others_already_recording_title": "S’niset dot një transmetim zanor i ri", - "live": "Drejtpërdrejt", - "pause": "ndal transmetim zanor", - "play": "luaj transmetim zanor", - "resume": "vazhdo transmetim zanor" - }, - "voice_message": { - "cant_start_broadcast_description": "S’mund të niset mesazh zanor, ngaqë aktualisht po incizoni një transmetim të drejtpërdrejtë. Ju lutemi, përfundoni transmetimin e drejtpërdrejtë, që të mund të nisni incizimin e një mesazhi zanor.", - "cant_start_broadcast_title": "S’niset dot mesazh zanor" - }, "voip": { "already_in_call": "Tashmë në thirrje", "already_in_call_person": "Gjendeni tashmë në thirrje me këtë person.", @@ -3460,7 +3403,6 @@ "camera_disabled": "Kamera juaj është e fikur", "camera_enabled": "Kamera juaj është ende e aktivizuar", "cannot_call_yourself_description": "S’mund të bëni thirrje me vetveten.", - "change_input_device": "Ndryshoni pajisje dhëniesh", "connecting": "Po lidhet", "connection_lost": "Humbi lidhja me shërbyesin", "connection_lost_description": "S’mund të bëni thirrje pa një lidhje te shërbyesi.", @@ -3476,8 +3418,6 @@ "enable_camera": "Aktivizo kamerën", "enable_microphone": "Çheshto mikrofonin", "expand": "Kthehu te thirrja", - "failed_call_live_broadcast_description": "S’mund të nisni një thirrje, ngaqë aktualisht jeni duke regjistruar një transmetim të drejtpërdrejtë. Që të mund të nisni një thirrje, ju lutemi, përfundoni transmetimin tuaj të drejtpërdrejtë.", - "failed_call_live_broadcast_title": "S’fillohet dot thirrje", "hangup": "Mbylle Thirrjen", "hide_sidebar_button": "Fshihe anështyllën", "input_devices": "Pajisje input-i", diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json index bb4e489952a..e24878b5316 100644 --- a/src/i18n/strings/sv.json +++ b/src/i18n/strings/sv.json @@ -882,7 +882,6 @@ }, "udd": { "interactive_verification_button": "Verifiera interaktivt med emoji", - "manual_verification_button": "Verifiera manuellt med text", "other_ask_verify_text": "Be den här användaren att verifiera sin session, eller verifiera den manuellt nedan.", "other_new_session_text": "%(name)s (%(userId)s) loggade in i en ny session utan att verifiera den:", "own_ask_verify_text": "Verifiera din andra session med ett av alternativen nedan.", @@ -917,12 +916,6 @@ "incoming_sas_dialog_waiting": "Väntar på att andra parten ska bekräfta …", "incoming_sas_user_dialog_text_1": "Verifiera denna användare för att markera den som betrodd. Att lita på användare ger en extra sinnesfrid när man använder totalsträckskrypterade meddelanden.", "incoming_sas_user_dialog_text_2": "Att verifiera den här användaren kommer att markera dess session som betrodd, och markera din session som betrodd för denne.", - "manual_device_verification_device_id_label": "Sessions-ID", - "manual_device_verification_device_key_label": "Sessionsnyckel", - "manual_device_verification_device_name_label": "Sessionsnamn", - "manual_device_verification_footer": "Om de inte matchar så kan din kommunikations säkerhet vara äventyrad.", - "manual_device_verification_self_text": "Bekräfta genom att jämföra följande med användarinställningarna i din andra session:", - "manual_device_verification_user_text": "Bekräfta den här användarens session genom att jämföra följande med deras användarinställningar:", "no_key_or_device": "Det ser ut som att du inte har någon säkerhetsnyckel eller några andra enheter du kan verifiera mot. Den här enheten kommer inte kunna komma åt gamla krypterad meddelanden. För att verifiera din identitet på den här enheten så behöver du återställa dina verifieringsnycklar.", "no_support_qr_emoji": "Enheten du försöker verifiera stöder inte att skanna en QR-kod eller verifiera med emoji, vilket är var %(brand)s stöder. Pröva en annan klient.", "other_party_cancelled": "Den andra parten avbröt verifieringen.", @@ -1046,10 +1039,6 @@ }, "error_user_not_logged_in": "Användaren är inte inloggad", "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s avslutade en röstsändning", - "you": "Du avslutade en röstsändning" - }, "m.call.answer": { "dm": "Samtal pågår", "user": "%(senderName)s gick med i samtalet", @@ -1423,8 +1412,6 @@ "video_rooms_faq2_answer": "Ja, chattidslinjen visas tillsammans med videon.", "video_rooms_faq2_question": "Kan jag använda textchatt tillsammans med videosamtalet?", "video_rooms_feedbackSubheading": "Tack för att du prövar betan, vänligen ge så många detaljer du kan så att vi kan förbättra den.", - "voice_broadcast": "Röstsändning", - "voice_broadcast_force_small_chunks": "Tvinga dellängd på 15s för röstsändning", "wysiwyg_composer": "Riktextredigerare" }, "labs_mjolnir": { @@ -1568,7 +1555,6 @@ "mute_description": "Du får inga aviseringar" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s startade en röstsändning", "m.key.verification.request": "%(name)s begär verifiering" }, "onboarding": { @@ -2142,7 +2128,6 @@ "error_unbanning": "Misslyckades att avbanna", "events_default": "Skicka meddelanden", "invite": "Bjuda in användare", - "io.element.voice_broadcast_info": "Röstsändning", "kick": "Ta bort användare", "m.call": "Starta %(brand)s samtal", "m.call.member": "Gå med i %(brand)s samtal", @@ -2784,7 +2769,6 @@ "warning": "VARNING: " }, "share": { - "link_title": "Länk till rum", "permalink_message": "Länk till valt meddelande", "permalink_most_recent": "Länk till senaste meddelandet", "title_message": "Dela rumsmeddelande", @@ -2873,13 +2857,6 @@ "upgraderoom": "Uppgraderar ett rum till en ny version", "upgraderoom_permission_error": "Du har inte de behörigheter som krävs för att använda det här kommandot.", "usage": "Användande", - "verify": "Verifierar en användar-, sessions- och pubkey-tupel", - "verify_mismatch": "VARNING: NYCKELVERIFIERING MISSLYCKADES! Den signerade nyckeln för %(userId)s och sessionen %(deviceId)s är \"%(fprint)s\" vilket inte matchar den givna nyckeln \"%(fingerprint)s\". Detta kan betyda att kommunikationen är övervakad!", - "verify_nop": "Sessionen är redan verifierad!", - "verify_nop_warning_mismatch": "VARNING: sessionen är redan verifierad, men nycklarna MATCHAR INTE!", - "verify_success_description": "Signeringsnyckeln du gav matchar signeringsnyckeln du fick av %(userId)ss session %(deviceId)s. Sessionen markerades som verifierad.", - "verify_success_title": "Verifierade nyckeln", - "verify_unknown_pair": "Okänt (användare, session)-par: (%(userId)s, %(deviceId)s)", "view": "Visar rum med den angivna adressen", "whois": "Visar information om en användare" }, @@ -3097,10 +3074,6 @@ "error_rendering_message": "Kan inte ladda det här meddelandet", "historical_messages_unavailable": "Du kan inte se tidigare meddelanden", "in_room_name": " i %(room)s", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s avslutade en röstsändning", - "you": "Du avslutade en röstsändning" - }, "io.element.widgets.layout": "%(senderName)s har uppdaterat rummets arrangemang", "late_event_separator": "Ursprungligen skickad %(dateTime)s", "load_error": { @@ -3633,38 +3606,6 @@ "switch_theme_dark": "Byt till mörkt läge", "switch_theme_light": "Byt till ljust läge" }, - "voice_broadcast": { - "30s_backward": "30s bakåt", - "30s_forward": "30s framåt", - "action": "Röstsändning", - "buffering": "Buffrar…", - "confirm_listen_affirm": "Ja, avsluta min inspelning", - "confirm_listen_description": "Om du börjar lyssna på den här direktsändningen så kommer din nuvarande direktsändningsinspelning att avslutas.", - "confirm_listen_title": "Lyssna på direktsändning?", - "confirm_stop_affirm": "Ja, avsluta sändning", - "confirm_stop_description": "Är du säker på att du vill avsluta din direktsändning? Det här kommer att avsluta sändningen och den fulla inspelningen kommer att bli tillgänglig i rummet.", - "confirm_stop_title": "Avsluta livesändning?", - "connection_error": "Anslutningsfel - Inspelning pausad", - "failed_already_recording_description": "Du spelar redan in en röstsändning. Avsluta din nuvarande röstsändning för att påbörja en ny.", - "failed_already_recording_title": "Kan inte starta en ny röstsändning", - "failed_decrypt": "Kunde inte kryptera röstsändning", - "failed_generic": "Kan inte spela den här röstsändningen", - "failed_insufficient_permission_description": "Du är inte behörig att starta en röstsändning i det här rummet. Kontakta en rumsadministratör för att uppgradera dina behörigheter.", - "failed_insufficient_permission_title": "Kan inte starta en ny röstsändning", - "failed_no_connection_description": "Tyvärr kunde vi inte starta en inspelning just nu. Vänligen pröva igen senare.", - "failed_no_connection_title": "Anslutningsfel", - "failed_others_already_recording_description": "Någon annan spelar redan in en röstsändning. Vänta på att deras röstsändning tar slut för att starta en ny.", - "failed_others_already_recording_title": "Kan inte starta en ny röstsändning", - "go_live": "Börja sända", - "live": "Sänder", - "pause": "pausa röstsändning", - "play": "spela röstsändning", - "resume": "återuppta röstsändning" - }, - "voice_message": { - "cant_start_broadcast_description": "Du kan inte starta ett röstmeddelande eftersom du spelar in en direktsändning. Vänligen avsluta din direktsändning för att starta inspelning av ett röstmeddelande.", - "cant_start_broadcast_title": "Kan inte starta röstmeddelanden" - }, "voip": { "already_in_call": "Redan i samtal", "already_in_call_person": "Du är redan i ett samtal med den här personen.", @@ -3684,7 +3625,6 @@ "camera_disabled": "Din kamera är av", "camera_enabled": "Din kamera är fortfarande på", "cannot_call_yourself_description": "Du kan inte ringa till dig själv.", - "change_input_device": "Byt ingångsenhet", "close_lobby": "Stäng lobbyn", "connecting": "Ansluter", "connection_lost": "Anslutningen till servern har förlorats", @@ -3703,8 +3643,6 @@ "enable_camera": "Sätt på kamera", "enable_microphone": "Slå på mikrofonen", "expand": "Återgå till samtal", - "failed_call_live_broadcast_description": "Du kan inte starta ett samtal eftersom att du spelar in en direktsändning. Vänligen avsluta din direktsändning för att starta ett samtal.", - "failed_call_live_broadcast_title": "Kunde inte starta ett samtal", "hangup": "Lägg på", "hide_sidebar_button": "Göm sidopanel", "input_devices": "Ingångsenheter", diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json index 7d438ec2a41..8af8d0b2ea2 100644 --- a/src/i18n/strings/uk.json +++ b/src/i18n/strings/uk.json @@ -864,7 +864,6 @@ }, "udd": { "interactive_verification_button": "Звірити інтерактивно за допомогою емоджі", - "manual_verification_button": "Звірити вручну за допомогою тексту", "other_ask_verify_text": "Попросіть цього користувача звірити сеанс, або звірте його власноруч унизу.", "other_new_session_text": "%(name)s (%(userId)s) починає новий сеанс без його звірення:", "own_ask_verify_text": "Звірте інший сеанс за допомогою одного з варіантів знизу.", @@ -899,12 +898,6 @@ "incoming_sas_dialog_waiting": "Очікування підтвердження партнером…", "incoming_sas_user_dialog_text_1": "Звірте цього користувача щоб позначити його довіреним. Довіряння користувачам додає спокою якщо ви користуєтесь наскрізно зашифрованими повідомленнями.", "incoming_sas_user_dialog_text_2": "Звірка цього користувача позначить його сеанс довіреним вам, а ваш йому.", - "manual_device_verification_device_id_label": "ID сеансу", - "manual_device_verification_device_key_label": "Ключ сеансу", - "manual_device_verification_device_name_label": "Назва сеансу", - "manual_device_verification_footer": "Якщо вони не збігаються, безпека вашого спілкування ймовірно скомпрометована.", - "manual_device_verification_self_text": "Підтвердьте шляхом порівняння наступного рядка з рядком у користувацьких налаштуваннях вашого іншого сеансу:", - "manual_device_verification_user_text": "Підтвердьте сеанс цього користувача шляхом порівняння наступного рядка з рядком з їхніх користувацьких налаштувань:", "no_key_or_device": "Схоже, у вас немає ключа безпеки або будь-яких інших пристроїв, які ви можете підтвердити. Цей пристрій не зможе отримати доступ до старих зашифрованих повідомлень. Щоб підтвердити свою справжність на цьому пристрої, вам потрібно буде скинути ключі перевірки.", "no_support_qr_emoji": "Пристрій, який ви намагаєтесь звірити, не підтримує сканування QR-коду або звірення за допомогою емоджі, що є підтримувані %(brand)s. Спробуйте використати інший клієнт.", "other_party_cancelled": "Друга сторона скасувала звірення.", @@ -1021,10 +1014,6 @@ }, "error_user_not_logged_in": "Користувач не увійшов", "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s завершує голосову трансляцію", - "you": "Ви завершили голосову трансляцію" - }, "m.call.answer": { "dm": "Виклик триває", "user": "%(senderName)s приєднується до виклику", @@ -1376,8 +1365,6 @@ "video_rooms_faq1_question": "Як створити відеокімнату?", "video_rooms_faq2_answer": "Так, стрічка бесіди показана поряд із відео.", "video_rooms_faq2_question": "Чи можу я писати текстові повідомлення під час відеовиклику?", - "voice_broadcast": "Голосові трансляції", - "voice_broadcast_force_small_chunks": "Примусово обмежити тривалість голосових трансляцій до 15 с", "wysiwyg_composer": "Розширений текстовий редактор" }, "labs_mjolnir": { @@ -1516,7 +1503,6 @@ "mute_description": "Ви не отримуватимете жодних сповіщень" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s розпочинає голосову трансляцію", "m.key.verification.request": "%(name)s робить запит на звірення" }, "onboarding": { @@ -2072,7 +2058,6 @@ "error_unbanning": "Не вдалося розблокувати", "events_default": "Надіслати повідомлення", "invite": "Запросити користувачів", - "io.element.voice_broadcast_info": "Голосові трансляції", "kick": "Вилучити користувачів", "m.call": "Розпочати %(brand)s викликів", "m.call.member": "Приєднатися до %(brand)s викликів", @@ -2709,7 +2694,6 @@ "warning": "ПОПЕРЕДЖЕННЯ: " }, "share": { - "link_title": "Посилання на кімнату", "permalink_message": "Посилання на вибране повідомлення", "permalink_most_recent": "Посилання на останнє повідомлення", "title_message": "Поділитися повідомленням кімнати", @@ -2796,13 +2780,6 @@ "upgraderoom": "Поліпшує кімнату до нової версії", "upgraderoom_permission_error": "Вам бракує дозволу на використання цієї команди.", "usage": "Використання", - "verify": "Звіряє користувача, сеанс та супровід відкритого ключа", - "verify_mismatch": "УВАГА: НЕ ВДАЛОСЯ ЗВІРИТИ КЛЮЧ! Ключем для %(userId)s та сеансу %(deviceId)s є «%(fprint)s», що не збігається з наданим ключем «%(fingerprint)s». Це може означати, що ваші повідомлення перехоплюють!", - "verify_nop": "Сеанс вже звірено!", - "verify_nop_warning_mismatch": "ПОПЕРЕДЖЕННЯ: сеанс вже звірено, але ключі НЕ ЗБІГАЮТЬСЯ!", - "verify_success_description": "Наданий вами ключ підпису збігається з ключем підпису, що ви отримали від сеансу %(deviceId)s %(userId)s. Сеанс позначено звіреним.", - "verify_success_title": "Звірений ключ", - "verify_unknown_pair": "Невідома пара (користувач, сеанс): (%(userId)s, %(deviceId)s)", "view": "Перегляд кімнати з вказаною адресою", "whois": "Показує відомості про користувача" }, @@ -3014,10 +2991,6 @@ "error_rendering_message": "Не вдалося завантажити це повідомлення", "historical_messages_unavailable": "Ви не можете переглядати давніші повідомлення", "in_room_name": " в %(room)s", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s завершує голосову трансляцію", - "you": "Ви завершили голосову трансляцію" - }, "io.element.widgets.layout": "%(senderName)s оновлює макет кімнати", "load_error": { "no_permission": "У вас нема дозволу на перегляд повідомлення за вказаною позицією в стрічці цієї кімнати.", @@ -3545,38 +3518,6 @@ "switch_theme_dark": "Темна тема", "switch_theme_light": "Світла тема" }, - "voice_broadcast": { - "30s_backward": "Назад на 30 с", - "30s_forward": "Уперед на 30 с", - "action": "Голосові трансляції", - "buffering": "Буферизація…", - "confirm_listen_affirm": "Так, завершити мій запис", - "confirm_listen_description": "Якщо ви почнете слухати цю трансляцію наживо, ваш поточний запис трансляції наживо завершиться.", - "confirm_listen_title": "Слухати трансляцію наживо?", - "confirm_stop_affirm": "Так, припинити трансляцію", - "confirm_stop_description": "Ви впевнені, що хочете припинити пряму трансляцію? Це призведе до завершення трансляції, а повний запис буде доступний у кімнаті.", - "confirm_stop_title": "Припинити голосову трансляцію?", - "connection_error": "Помилка з'єднання - Запис призупинено", - "failed_already_recording_description": "Ви вже записуєте голосову трансляцію. Завершіть поточний запис, щоб розпочати новий.", - "failed_already_recording_title": "Не вдалося розпочати нову голосову трансляцію", - "failed_decrypt": "Невдалося розшифрувати голосову трансляцію", - "failed_generic": "Неможливо відтворити цю голосову трансляцію", - "failed_insufficient_permission_description": "Ви не маєте необхідних дозволів для початку голосової трансляції в цю кімнату. Зверніться до адміністратора кімнати, щоб розширити ваші дозволи.", - "failed_insufficient_permission_title": "Не вдалося розпочати нову голосову трансляцію", - "failed_no_connection_description": "На жаль, ми не можемо розпочати запис прямо зараз. Будь ласка, спробуйте пізніше.", - "failed_no_connection_title": "Помилка з'єднання", - "failed_others_already_recording_description": "Хтось інший вже записує голосову трансляцію. Зачекайте, поки запис завершиться, щоб розпочати новий.", - "failed_others_already_recording_title": "Не вдалося розпочати нову голосову трансляцію", - "go_live": "Слухати", - "live": "Наживо", - "pause": "призупинити голосову трансляцію", - "play": "відтворити голосову трансляцію", - "resume": "поновити голосову трансляцію" - }, - "voice_message": { - "cant_start_broadcast_description": "Ви не можете розпочати запис голосового повідомлення, оскільки зараз відбувається запис трансляції наживо. Завершіть трансляцію, щоб розпочати запис голосового повідомлення.", - "cant_start_broadcast_title": "Не можливо запустити запис голосового повідомлення" - }, "voip": { "already_in_call": "Вже у виклику", "already_in_call_person": "Ви вже спілкуєтесь із цією особою.", @@ -3596,7 +3537,6 @@ "camera_disabled": "Вашу камеру вимкнено", "camera_enabled": "Ваша камера досі увімкнена", "cannot_call_yourself_description": "Ви не можете подзвонити самим собі.", - "change_input_device": "Змінити пристрій вводу", "connecting": "З'єднання", "connection_lost": "Втрачено зʼєднання з сервером", "connection_lost_description": "Неможливо здійснювати виклики без з'єднання з сервером.", @@ -3613,8 +3553,6 @@ "enable_camera": "Увімкнути камеру", "enable_microphone": "Увімкнути мікрофон", "expand": "Повернутися до виклику", - "failed_call_live_broadcast_description": "Ви не можете розпочати виклик, оскільки зараз ведеться запис прямої трансляції. Будь ласка, заверште її, щоб розпочати виклик.", - "failed_call_live_broadcast_title": "Не вдалося розпочати виклик", "hangup": "Покласти слухавку", "hide_sidebar_button": "Сховати бічну панель", "input_devices": "Пристрої вводу", diff --git a/src/i18n/strings/vi.json b/src/i18n/strings/vi.json index 5ce36aed05f..3e1eaeaec14 100644 --- a/src/i18n/strings/vi.json +++ b/src/i18n/strings/vi.json @@ -792,7 +792,6 @@ }, "udd": { "interactive_verification_button": "Xác thực có tương tác bằng biểu tượng cảm xúc", - "manual_verification_button": "Xác thực thủ công bằng văn bản", "other_ask_verify_text": "Yêu cầu người dùng này xác thực phiên của họ hoặc xác minh theo cách thủ công bên dưới.", "other_new_session_text": "%(name)s (%(userId)s) đã đăng nhập vào một phiên mới mà không xác thực:", "own_ask_verify_text": "Xác minh phiên khác của bạn bằng một trong các tùy chọn bên dưới.", @@ -827,12 +826,6 @@ "incoming_sas_dialog_waiting": "Đang đợi bên kia xác nhận…", "incoming_sas_user_dialog_text_1": "Xác thực người dùng này để đánh dấu họ là đáng tin cậy. Người dùng đáng tin cậy giúp bạn yên tâm hơn khi sử dụng các tin nhắn được mã hóa end-to-end.", "incoming_sas_user_dialog_text_2": "Việc xác thực người dùng này sẽ đánh dấu phiên của họ là đáng tin cậy và cũng đánh dấu phiên của bạn là đáng tin cậy đối với họ.", - "manual_device_verification_device_id_label": "Định danh (ID) phiên", - "manual_device_verification_device_key_label": "Khóa phiên", - "manual_device_verification_device_name_label": "Tên phiên", - "manual_device_verification_footer": "Nếu chúng không khớp, sự bảo mật của việc giao tiếp của bạn có thể bị can thiệp.", - "manual_device_verification_self_text": "Xác nhận bằng cách so sánh những điều sau đây với Cài đặt người dùng trong phiên làm việc kia của bạn:", - "manual_device_verification_user_text": "Xác nhận phiên của người dùng này bằng cách so sánh phần sau với Cài đặt người dùng của họ:", "no_key_or_device": "Có vẻ như bạn không có Khóa Bảo mật hoặc bất kỳ thiết bị nào bạn có thể xác thực. Thiết bị này sẽ không thể truy cập vào các tin nhắn mã hóa cũ. Để xác minh danh tính của bạn trên thiết bị này, bạn sẽ cần đặt lại các khóa xác thực của mình.", "no_support_qr_emoji": "Thiết bị bạn đang cố xác thực không hỗ trợ quét mã QR hoặc xác minh biểu tượng cảm xúc, đó là những gì %(brand)s hỗ trợ. Hãy thử với một thiết bị đầu cuối khác.", "other_party_cancelled": "Người kia đã hủy xác thực.", @@ -949,10 +942,6 @@ }, "error_user_not_logged_in": "Người dùng đang không đăng nhập", "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s đã kết thúc một cuộc phát thanh", - "you": "Bạn đã kết thúc một cuộc phát thanh" - }, "m.call.answer": { "dm": "Cuộc gọi đang diễn ra", "user": "%(senderName)s đã tham gia cuộc gọi", @@ -1288,7 +1277,6 @@ "video_rooms_faq1_question": "Tôi có thể tạo một phòng video bằng cách nào?", "video_rooms_faq2_answer": "Vâng, dòng thời gian trò chuyện được hiển thị cùng với video.", "video_rooms_faq2_question": "Tôi có thể sử dụng nhắn tin cùng lúc với gọi video không?", - "voice_broadcast": "Phát thanh", "wysiwyg_composer": "Trình soạn thảo văn bản giàu tính chất" }, "labs_mjolnir": { @@ -1400,7 +1388,6 @@ "mute_description": "Bạn sẽ không nhận bất kỳ thông báo nào" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s đã bắt đầu phát thanh", "m.key.verification.request": "%(name)s đang yêu cầu xác thực" }, "onboarding": { @@ -1895,7 +1882,6 @@ "error_unbanning": "Không thể bỏ cấm", "events_default": "Gửi tin nhắn", "invite": "Mời người dùng", - "io.element.voice_broadcast_info": "Phát thanh", "kick": "Loại bỏ người dùng", "m.call": "Bắt đầu %(brand)s cuộc gọi", "m.call.member": "Tham gia %(brand)s cuộc gọi", @@ -2496,7 +2482,6 @@ "warn_quit": "Cảnh báo trước khi bỏ thuốc lá" }, "share": { - "link_title": "Liên kết đến phòng", "permalink_message": "Liên kết đến tin nhắn đã chọn", "permalink_most_recent": "Liên kết đến tin nhắn gần đây nhất", "title_message": "Chia sẻ tin nhắn trong phòng", @@ -2583,13 +2568,6 @@ "upgraderoom": "Nâng cấp phòng lên phiên bản mới", "upgraderoom_permission_error": "Bạn không có quyền để dùng lệnh này.", "usage": "Cách sử dụng", - "verify": "Xác thực người dùng, thiết bị và tuple pubkey", - "verify_mismatch": "CẢNH BÁO: XÁC THỰC KHÓA THẤT BẠI! Khóa đăng nhập cho %(userId)s và thiết bị %(deviceId)s là \"%(fprint)s\" không khớp với khóa được cung cấp \"%(fingerprint)s\". Điều này có nghĩa là các thông tin liên lạc của bạn đang bị chặn!", - "verify_nop": "Thiết bị đã được xác thực rồi!", - "verify_nop_warning_mismatch": "CẢNH BÁO: phiên đã được xác thực, nhưng các khóa KHÔNG KHỚP!", - "verify_success_description": "Khóa đăng nhập bạn cung cấp khớp với khóa đăng nhập bạn nhận từ thiết bị %(deviceId)s của %(userId)s. Thiết bị được đánh dấu là đã được xác minh.", - "verify_success_title": "Khóa được xác thực", - "verify_unknown_pair": "Cặp (người dùng, phiên) không xác định: (%(userId)s, %(deviceId)s)", "view": "Phòng truyền hình với địa chỉ đã cho", "whois": "Hiển thị thông tin về người dùng" }, @@ -2786,10 +2764,6 @@ "error_rendering_message": "Không thể tải tin nhắn này", "historical_messages_unavailable": "Bạn khồng thể thấy các tin nhắn trước", "in_room_name": " ở %(room)s", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s đã kết thúc một cuộc phát thanh", - "you": "Bạn đã kết thúc một cuộc phát thanh" - }, "io.element.widgets.layout": "%(senderName)s đã cập nhật bố trí của phòng", "load_error": { "no_permission": "Đã cố gắng tải một điểm cụ thể trong dòng thời gian của phòng này, nhưng bạn không có quyền xem tin nhắn được đề cập.", @@ -3280,34 +3254,6 @@ "switch_theme_dark": "Chuyển sang chế độ tối", "switch_theme_light": "Chuyển sang chế độ ánh sáng" }, - "voice_broadcast": { - "30s_backward": "30 giây trước", - "30s_forward": "30 giây kế tiếp", - "action": "Phát thanh", - "buffering": "Đang khởi tạo bộ đệm…", - "confirm_listen_affirm": "Vâng, ngừng ghi âm tôi", - "confirm_listen_description": "Nếu bạn bắt đầu nghe chương trình phát thanh trực tiếp này, quá trình ghi chương trình phát thanh trực tiếp hiện tại của bạn sẽ kết thúc.", - "confirm_listen_title": "Nghe phát thanh trực tiếp không?", - "confirm_stop_affirm": "Đúng rồi, dừng phát thanh", - "confirm_stop_description": "Bạn có chắc chắn muốn dừng phát sóng trực tiếp của mình không? Điều này sẽ kết thúc chương trình phát sóng và bản ghi đầy đủ sẽ có sẵn trong phòng.", - "confirm_stop_title": "Ngừng phát thanh trực tiếp?", - "connection_error": "Lỗi kết nối - Đã tạm dừng ghi âm", - "failed_already_recording_description": "Bạn hiện đang ghi một cuộc phát thanh. Kết thúc phát thanh để thực hiện một cái mới.", - "failed_already_recording_title": "Không thể bắt đầu cuộc phát thanh mới", - "failed_decrypt": "Không thể giải mã cuộc phát thanh", - "failed_generic": "Không thể nghe phát thanh", - "failed_insufficient_permission_description": "Bạn không có quyền để phát thanh trong phòng này. Hỏi một quản trị viên của phòng để nâng quyền của bạn.", - "failed_insufficient_permission_title": "Không thể bắt đầu cuộc phát thanh mới", - "failed_no_connection_description": "Thật không may là chúng tôi không thể bắt đầu ghi âm. Vui lòng thử lại.", - "failed_no_connection_title": "Lỗi kết nối", - "failed_others_already_recording_description": "Một người khác đang phát thanh. Hãy chờ cho đến khi họ ngừng rồi bạn mới bắt đầu phát thanh.", - "failed_others_already_recording_title": "Không thể bắt đầu cuộc phát thanh mới", - "go_live": "Phát trực tiếp", - "live": "Trực tiếp", - "pause": "Tạm dừng phát thanh", - "play": "nghe phát thanh", - "resume": "Tiếp tục phát thanh" - }, "voip": { "already_in_call": "Đang trong cuộc gọi", "already_in_call_person": "Bạn đang trong cuộc gọi với người này rồi.", @@ -3327,7 +3273,6 @@ "camera_disabled": "Camera của bạn đã tắt", "camera_enabled": "Camera của bạn vẫn đang được bật", "cannot_call_yourself_description": "Bạn không thể tự gọi chính mình.", - "change_input_device": "Đổi thiết bị đầu vào", "connecting": "Đang kết nối", "connection_lost": "Mất kết nối đến máy chủ", "connection_lost_description": "Bạn không thể gọi khi không có kết nối tới máy chủ.", @@ -3344,8 +3289,6 @@ "enable_camera": "Bật máy ghi hình", "enable_microphone": "Mở âm micrô", "expand": "Quay về cuộc gọi", - "failed_call_live_broadcast_description": "Bạn không thể bắt đầu gọi vì bạn đang ghi âm để cuộc phát thanh trực tiếp. Hãy ngừng phát thanh để bắt đầu gọi.", - "failed_call_live_broadcast_title": "Không thể bắt đầu cuộc gọi", "hangup": "Dập máy", "hide_sidebar_button": "Ẩn thanh bên", "input_devices": "Thiết bị đầu vào", diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json index 99d5586a5cc..1ddb47fcc44 100644 --- a/src/i18n/strings/zh_Hans.json +++ b/src/i18n/strings/zh_Hans.json @@ -809,7 +809,6 @@ }, "udd": { "interactive_verification_button": "用表情符号交互式验证", - "manual_verification_button": "用文本手动验证", "other_ask_verify_text": "要求此用户验证其会话,或在下面手动进行验证。", "other_new_session_text": "%(name)s(%(userId)s)登录到未验证的新会话:", "own_ask_verify_text": "使用以下选项之一验证你的其他会话。", @@ -841,12 +840,6 @@ "incoming_sas_dialog_title": "收到验证请求", "incoming_sas_user_dialog_text_1": "验证此用户并将其标记为已信任。在收发端到端加密消息时,信任用户可让你更加放心。", "incoming_sas_user_dialog_text_2": "验证此用户会将其会话标记为已信任,与此同时,你的会话也会被此用户标记为已信任。", - "manual_device_verification_device_id_label": "会话 ID", - "manual_device_verification_device_key_label": "会话密钥", - "manual_device_verification_device_name_label": "会话名称", - "manual_device_verification_footer": "如果它们不匹配,你通讯的安全性可能已受损。", - "manual_device_verification_self_text": "通过比较下方内容和你别的会话中的用户设置来确认:", - "manual_device_verification_user_text": "通过比较下方内容和对方用户设置来确认此用户会话:", "no_key_or_device": "看起来你没有安全密钥或者任何其他可以验证的设备。 此设备将无法访问旧的加密消息。为了在这个设备上验证你的身份,你需要重置你的验证密钥。", "no_support_qr_emoji": "你正在尝试验证的设备不支持扫码QR码或表情符号验证,这是%(brand)s所支持的。用不同的客户端试试。", "other_party_cancelled": "另一方取消了验证。", @@ -1296,7 +1289,6 @@ "video_rooms_faq1_question": "我如何创建视频房间?", "video_rooms_faq2_answer": "是的,聊天时间线显示在视频旁。", "video_rooms_faq2_question": "我能在视频通话的同时使用文字聊天吗?", - "voice_broadcast": "语音广播", "wysiwyg_composer": "富文本编辑器" }, "labs_mjolnir": { @@ -1426,7 +1418,6 @@ "mute_description": "你不会收到任何通知" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s开始了语音广播", "m.key.verification.request": "%(name)s 正在请求验证" }, "onboarding": { @@ -1917,7 +1908,6 @@ "error_unbanning": "解除封禁失败", "events_default": "发送消息", "invite": "邀请用户", - "io.element.voice_broadcast_info": "语音广播", "kick": "移除用户", "m.call": "开始%(brand)s呼叫", "m.call.member": "加入%(brand)s呼叫", @@ -2457,7 +2447,6 @@ "warning": "警告:" }, "share": { - "link_title": "房间链接", "permalink_message": "选中消息的链接", "permalink_most_recent": "最新消息的链接", "title_message": "分享房间消息", @@ -2544,13 +2533,6 @@ "upgraderoom": "将房间升级到新版本", "upgraderoom_permission_error": "你没有权限使用此命令。", "usage": "用法", - "verify": "验证用户、会话和公钥元组", - "verify_mismatch": "警告:密钥验证失败!%(userId)s 的会话 %(deviceId)s 的签名密钥为 %(fprint)s,与提供的密钥 %(fingerprint)s 不符。这可能表示你的通讯已被截获!", - "verify_nop": "会话已验证!", - "verify_nop_warning_mismatch": "警告:会话已验证,然而密钥不匹配!", - "verify_success_description": "你提供的签名密钥与你从 %(userId)s 的会话 %(deviceId)s 获取的一致。此会话被标为已验证。", - "verify_success_title": "已验证的密钥", - "verify_unknown_pair": "未知用户会话配对:(%(userId)s:%(deviceId)s)", "whois": "显示关于用户的信息" }, "space": { @@ -3256,27 +3238,6 @@ "switch_theme_dark": "切换到深色模式", "switch_theme_light": "切换到浅色模式" }, - "voice_broadcast": { - "30s_backward": "后退30秒", - "30s_forward": "前进30秒", - "action": "语音广播", - "buffering": "正在缓冲……", - "confirm_stop_affirm": "是的,停止广播", - "confirm_stop_title": "停止直播吗?", - "failed_already_recording_description": "你已经在录制一个语音广播。请结束你当前的语音广播以开始新的语音广播。", - "failed_already_recording_title": "无法开始新的语音广播", - "failed_insufficient_permission_description": "你没有必要的权限在这个房间开始语音广播。请联系房间管理员以提升你的权限。", - "failed_insufficient_permission_title": "无法开始新的语音广播", - "failed_no_connection_description": "很遗憾,我们现在无法开始录音。请稍后再试。", - "failed_no_connection_title": "连接错误", - "failed_others_already_recording_description": "别人已经在录制语音广播了。等到他们的语音广播结束后再开始新的广播。", - "failed_others_already_recording_title": "无法开始新的语音广播", - "go_live": "开始直播", - "live": "实时", - "pause": "暂停语音广播", - "play": "播放语音广播", - "resume": "恢复语音广播" - }, "voip": { "already_in_call": "正在通话中", "already_in_call_person": "你正在与其通话。", @@ -3296,7 +3257,6 @@ "camera_disabled": "你的摄像头已关闭", "camera_enabled": "你的摄像头仍然处于启用状态", "cannot_call_yourself_description": "你不能打给自己。", - "change_input_device": "变更输入设备", "connecting": "连接中", "connection_lost": "已丢失与服务器的连接", "connection_lost_description": "你不能在未连接到服务器时进行呼叫。", @@ -3313,7 +3273,6 @@ "enable_camera": "启动相机", "enable_microphone": "取消静音麦克风", "expand": "返回通话", - "failed_call_live_broadcast_title": "无法开始通话", "hangup": "挂断", "hide_sidebar_button": "隐藏侧边栏", "input_devices": "输入设备", diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 68b3694ee8c..584852748b1 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -865,7 +865,6 @@ }, "udd": { "interactive_verification_button": "透過表情符號互動來驗證", - "manual_verification_button": "透過文字手動驗證", "other_ask_verify_text": "要求此使用者驗證他們的工作階段,或在下方手動驗證。", "other_new_session_text": "%(name)s (%(userId)s)登入到未驗證的新工作階段:", "own_ask_verify_text": "使用下方的其中一個選項來驗證您其他工作階段。", @@ -900,12 +899,6 @@ "incoming_sas_dialog_waiting": "正在等待夥伴確認…", "incoming_sas_user_dialog_text_1": "驗證此工作階段,並標記為可受信任。由您將工作階段標記為可受信任後,可讓聊天夥伴傳送端到端加密訊息時能更加放心。", "incoming_sas_user_dialog_text_2": "驗證此使用者將會把他們的工作階段標記為受信任,並同時為他們標記您的工作階段為可信任。", - "manual_device_verification_device_id_label": "工作階段 ID", - "manual_device_verification_device_key_label": "工作階段金鑰", - "manual_device_verification_device_name_label": "工作階段名稱", - "manual_device_verification_footer": "如果它們不相符,則可能會威脅到您的通訊安全。", - "manual_device_verification_self_text": "透過將下列內容與您其他工作階段中的「使用者設定」所顯示的內容來確認:", - "manual_device_verification_user_text": "將以下內容與對方的「使用者設定」當中顯示的內容進行比對,來確認對方的工作階段:", "no_key_or_device": "您似乎沒有安全金鑰或其他可以驗證的裝置。此裝置將無法存取舊的加密訊息。為了在此裝置上驗證您的身分,您必須重設您的驗證金鑰。", "no_support_qr_emoji": "您正在嘗試驗證的裝置不支援掃描 QR Code 或表情符號驗證,這是 %(brand)s 所支援的。請嘗試使用其他客戶端。", "other_party_cancelled": "另一方取消了驗證。", @@ -1022,10 +1015,6 @@ }, "error_user_not_logged_in": "使用者未登入", "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s 已結束語音廣播", - "you": "您結束了語音廣播" - }, "m.call.answer": { "dm": "通話進行中", "user": "%(senderName)s 已加入通話", @@ -1379,8 +1368,6 @@ "video_rooms_faq1_question": "我要如何建立視訊聊天室?", "video_rooms_faq2_answer": "可以,會在視訊畫面旁顯示聊天時間軸。", "video_rooms_faq2_question": "我可以在視訊通話的同時使用文字聊天嗎?", - "voice_broadcast": "語音廣播", - "voice_broadcast_force_small_chunks": "強制 15 秒語音廣播區塊長度", "wysiwyg_composer": "格式化文字編輯器" }, "labs_mjolnir": { @@ -1519,7 +1506,6 @@ "mute_description": "不會收到任何通知" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s 開始了語音廣播", "m.key.verification.request": "%(name)s 正在要求驗證" }, "onboarding": { @@ -2074,7 +2060,6 @@ "error_unbanning": "無法解除封鎖", "events_default": "傳送訊息", "invite": "邀請使用者", - "io.element.voice_broadcast_info": "語音廣播", "kick": "移除使用者", "m.call": "開始 %(brand)s 通話", "m.call.member": "加入 %(brand)s 通話", @@ -2704,7 +2689,6 @@ "warning": "警告: " }, "share": { - "link_title": "連結到聊天室", "permalink_message": "連結到選定的訊息", "permalink_most_recent": "連結到最近的訊息", "title_message": "分享聊天室訊息", @@ -2791,13 +2775,6 @@ "upgraderoom": "升級聊天室到新版本", "upgraderoom_permission_error": "您沒有使用此指令的必要權限。", "usage": "使用方法", - "verify": "驗證使用者、工作階段與公開金鑰組合", - "verify_mismatch": "警告:無法驗證金鑰!%(userId)s 與工作階段 %(deviceId)s 簽署的金鑰是「%(fprint)s」,並不符合提供的金鑰「%(fingerprint)s」。這可能代表您的通訊已被攔截!", - "verify_nop": "工作階段已驗證!", - "verify_nop_warning_mismatch": "警告:工作階段已驗證,但金鑰不相符!", - "verify_success_description": "您提供的簽署金鑰符合您從 %(userId)s 的工作階段收到的簽署金鑰 %(deviceId)s。工作階段標記為已驗證。", - "verify_success_title": "已驗證的金鑰", - "verify_unknown_pair": "未知(使用者,工作階段)配對:(%(userId)s, %(deviceId)s)", "view": "檢視指定聊天室的地址", "whois": "顯示關於使用者的資訊" }, @@ -3005,10 +2982,6 @@ "error_rendering_message": "無法載入此訊息", "historical_messages_unavailable": "您看不到更早的訊息", "in_room_name": " 在 %(room)s", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s 結束了語音廣播", - "you": "您結束了語音廣播" - }, "io.element.widgets.layout": "%(senderName)s 已更新聊天室佈局", "load_error": { "no_permission": "嘗試載入此聊天室時間軸上的特定時間點,但您沒有權限檢視相關的訊息。", @@ -3536,38 +3509,6 @@ "switch_theme_dark": "切換至深色模式", "switch_theme_light": "切換至淺色模式" }, - "voice_broadcast": { - "30s_backward": "快退30秒", - "30s_forward": "快轉30秒", - "action": "語音廣播", - "buffering": "正在緩衝…", - "confirm_listen_affirm": "是的,結束我的錄製", - "confirm_listen_description": "若您開始收聽本次直播,您目前的直播錄製將會結束。", - "confirm_listen_title": "聆聽直播?", - "confirm_stop_affirm": "是的,停止廣播", - "confirm_stop_description": "您真的要停止即時廣播嗎?將會結束廣播,完整錄音存檔稍後將在聊天室中提供。", - "confirm_stop_title": "停止即時廣播?", - "connection_error": "連線錯誤 - 已暫停錄音", - "failed_already_recording_description": "您已經開始錄製語音廣播。請結束您目前的語音廣播以開始新的語音廣播。", - "failed_already_recording_title": "無法啟動新的語音廣播", - "failed_decrypt": "無法解密語音廣播", - "failed_generic": "無法播放此語音廣播", - "failed_insufficient_permission_description": "您沒有權限在此聊天室內開始語音廣播。請聯絡聊天室管理員升級您的權限。", - "failed_insufficient_permission_title": "無法啟動新的語音廣播", - "failed_no_connection_description": "很抱歉,現在無法錄音。請稍後再試。", - "failed_no_connection_title": "連線錯誤", - "failed_others_already_recording_description": "其他人已在錄製語音廣播。等待他們的語音廣播結束以開始新的。", - "failed_others_already_recording_title": "無法啟動新的語音廣播", - "go_live": "開始直播", - "live": "直播", - "pause": "暫停語音廣播", - "play": "播放語音廣播", - "resume": "恢復語音廣播" - }, - "voice_message": { - "cant_start_broadcast_description": "您無法開始語音訊息,因為您目前正在錄製直播。請結束您的直播以開始錄製語音訊息。", - "cant_start_broadcast_title": "無法開始語音訊息" - }, "voip": { "already_in_call": "已在通話中", "already_in_call_person": "您正在與此人通話。", @@ -3587,7 +3528,6 @@ "camera_disabled": "您的相機已關閉", "camera_enabled": "您的相機開啟中", "cannot_call_yourself_description": "您不能打電話給自己。", - "change_input_device": "變更輸入裝置", "connecting": "連線中", "connection_lost": "與伺服器的連線已遺失", "connection_lost_description": "您無法在未連線至伺服器的情況下通話。", @@ -3604,8 +3544,6 @@ "enable_camera": "開啟相機", "enable_microphone": "取消麥克風靜音", "expand": "回到通話", - "failed_call_live_broadcast_description": "您無法開始通話,因為您正在錄製直播。請結束您的直播以便開始通話。", - "failed_call_live_broadcast_title": "無法開始通話", "hangup": "掛斷", "hide_sidebar_button": "隱藏側邊欄", "input_devices": "輸入裝置", diff --git a/src/indexing/EventIndex.ts b/src/indexing/EventIndex.ts index fc1be4eba5d..da2eee995cd 100644 --- a/src/indexing/EventIndex.ts +++ b/src/indexing/EventIndex.ts @@ -819,7 +819,11 @@ export default class EventIndex extends EventEmitter { // Add the events to the timeline of the file panel. matrixEvents.forEach((e) => { if (!timelineSet.eventIdToTimeline(e.getId()!)) { - timelineSet.addEventToTimeline(e, timeline, direction == EventTimeline.BACKWARDS); + timelineSet.addEventToTimeline(e, timeline, { + toStartOfTimeline: direction == EventTimeline.BACKWARDS, + fromCache: false, + addToState: false, + }); } }); diff --git a/src/rageshake/rageshake.ts b/src/rageshake/rageshake.ts index 763df51d957..c68fa8503c7 100644 --- a/src/rageshake/rageshake.ts +++ b/src/rageshake/rageshake.ts @@ -97,6 +97,7 @@ export class ConsoleLogger { // run. // Example line: // 2017-01-18T11:23:53.214Z W Failed to set badge count + // eslint-disable-next-line @typescript-eslint/no-base-to-string let line = `${ts} ${level} ${args.join(" ")}\n`; // Do some cleanup line = line.replace(/token=[a-zA-Z0-9-]+/gm, "token=xxxxx"); diff --git a/src/rageshake/submit-rageshake.ts b/src/rageshake/submit-rageshake.ts index ec529336d1e..0c00cc777a2 100644 --- a/src/rageshake/submit-rageshake.ts +++ b/src/rageshake/submit-rageshake.ts @@ -9,7 +9,8 @@ Please see LICENSE files in the repository root for full details. */ import { logger } from "matrix-js-sdk/src/logger"; -import { Method, MatrixClient, Crypto } from "matrix-js-sdk/src/matrix"; +import { Method, MatrixClient } from "matrix-js-sdk/src/matrix"; +import { CryptoApi } from "matrix-js-sdk/src/crypto-api"; import type * as Pako from "pako"; import { MatrixClientPeg } from "../MatrixClientPeg"; @@ -169,7 +170,7 @@ async function collectSynapseSpecific(client: MatrixClient, body: FormData): Pro /** * Collects crypto related information. */ -async function collectCryptoInfo(cryptoApi: Crypto.CryptoApi, body: FormData): Promise { +async function collectCryptoInfo(cryptoApi: CryptoApi, body: FormData): Promise { body.append("crypto_version", cryptoApi.getVersion()); const ownDeviceKeys = await cryptoApi.getOwnDeviceKeys(); @@ -198,7 +199,7 @@ async function collectCryptoInfo(cryptoApi: Crypto.CryptoApi, body: FormData): P /** * Collects information about secret storage and backup. */ -async function collectRecoveryInfo(client: MatrixClient, cryptoApi: Crypto.CryptoApi, body: FormData): Promise { +async function collectRecoveryInfo(client: MatrixClient, cryptoApi: CryptoApi, body: FormData): Promise { const secretStorage = client.secretStorage; body.append("secret_storage_ready", String(await cryptoApi.isSecretStorageReady())); body.append("secret_storage_key_in_account", String(await secretStorage.hasKey())); diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 1c27f03e88a..6cd5b15a515 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -85,18 +85,9 @@ export enum LabGroup { } export enum Features { - VoiceBroadcast = "feature_voice_broadcast", - VoiceBroadcastForceSmallChunks = "feature_voice_broadcast_force_small_chunks", NotificationSettings2 = "feature_notification_settings2", OidcNativeFlow = "feature_oidc_native_flow", ReleaseAnnouncement = "feature_release_announcement", - - /** If true, use the Rust crypto implementation. - * - * This is no longer read, but we continue to populate it on all devices, to guard against people rolling back to - * old versions of EW that do not use rust crypto by default. - */ - RustCrypto = "feature_rust_crypto", } export const labGroupNames: Record = { @@ -447,19 +438,6 @@ export const SETTINGS: { [setting: string]: ISetting } = { shouldWarn: true, default: false, }, - [Features.VoiceBroadcast]: { - isFeature: true, - labsGroup: LabGroup.Messaging, - supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG_PRIORITISED, - supportedLevelsAreOrdered: true, - displayName: _td("labs|voice_broadcast"), - default: false, - }, - [Features.VoiceBroadcastForceSmallChunks]: { - supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, - displayName: _td("labs|voice_broadcast_force_small_chunks"), - default: false, - }, [Features.OidcNativeFlow]: { isFeature: true, labsGroup: LabGroup.Developer, @@ -469,10 +447,6 @@ export const SETTINGS: { [setting: string]: ISetting } = { description: _td("labs|oidc_native_flow_description"), default: false, }, - [Features.RustCrypto]: { - supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, - default: true, - }, /** * @deprecated in favor of {@link fontSizeDelta} */ diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx index 53e25736f07..66644c06a1f 100644 --- a/src/stores/RoomViewStore.tsx +++ b/src/stores/RoomViewStore.tsx @@ -42,15 +42,6 @@ import { UPDATE_EVENT } from "./AsyncStore"; import { SdkContextClass } from "../contexts/SDKContext"; import { CallStore } from "./CallStore"; import { ThreadPayload } from "../dispatcher/payloads/ThreadPayload"; -import { - doClearCurrentVoiceBroadcastPlaybackIfStopped, - doMaybeSetCurrentVoiceBroadcastPlayback, - VoiceBroadcastRecording, - VoiceBroadcastRecordingsStoreEvent, -} from "../voice-broadcast"; -import { IRoomStateEventsActionPayload } from "../actions/MatrixActionCreators"; -import { showCantStartACallDialog } from "../voice-broadcast/utils/showCantStartACallDialog"; -import { pauseNonLiveBroadcastFromOtherRoom } from "../voice-broadcast/utils/pauseNonLiveBroadcastFromOtherRoom"; import { ActionPayload } from "../dispatcher/payloads"; import { CancelAskToJoinPayload } from "../dispatcher/payloads/CancelAskToJoinPayload"; import { SubmitAskToJoinPayload } from "../dispatcher/payloads/SubmitAskToJoinPayload"; @@ -164,10 +155,6 @@ export class RoomViewStore extends EventEmitter { ) { super(); this.resetDispatcher(dis); - this.stores.voiceBroadcastRecordingsStore.addListener( - VoiceBroadcastRecordingsStoreEvent.CurrentChanged, - this.onCurrentBroadcastRecordingChanged, - ); } public addRoomListener(roomId: string, fn: Listener): void { @@ -182,16 +169,6 @@ export class RoomViewStore extends EventEmitter { this.emit(roomId, isActive); } - private onCurrentBroadcastRecordingChanged = (recording: VoiceBroadcastRecording | null): void => { - if (recording === null) { - const room = this.stores.client?.getRoom(this.state.roomId || undefined); - - if (room) { - this.doMaybeSetCurrentVoiceBroadcastPlayback(room); - } - } - }; - private setState(newState: Partial): void { // If values haven't changed, there's nothing to do. // This only tries a shallow comparison, so unchanged objects will slip @@ -207,16 +184,6 @@ export class RoomViewStore extends EventEmitter { return; } - if (newState.viewingCall) { - // Pause current broadcast, if any - this.stores.voiceBroadcastPlaybacksStore.getCurrent()?.pause(); - - if (this.stores.voiceBroadcastRecordingsStore.getCurrent()) { - showCantStartACallDialog(); - newState.viewingCall = false; - } - } - const lastRoomId = this.state.roomId; this.state = Object.assign(this.state, newState); if (lastRoomId !== this.state.roomId) { @@ -235,29 +202,6 @@ export class RoomViewStore extends EventEmitter { this.emit(UPDATE_EVENT); } - private doMaybeSetCurrentVoiceBroadcastPlayback(room: Room): void { - if (!this.stores.client) return; - doMaybeSetCurrentVoiceBroadcastPlayback( - room, - this.stores.client, - this.stores.voiceBroadcastPlaybacksStore, - this.stores.voiceBroadcastRecordingsStore, - ); - } - - private onRoomStateEvents(event: MatrixEvent): void { - const roomId = event.getRoomId?.(); - - // no room or not current room - if (!roomId || roomId !== this.state.roomId) return; - - const room = this.stores.client?.getRoom(roomId); - - if (room) { - this.doMaybeSetCurrentVoiceBroadcastPlayback(room); - } - } - private onDispatch(payload: ActionPayload): void { // eslint-disable-line @typescript-eslint/naming-convention switch (payload.action) { @@ -283,10 +227,6 @@ export class RoomViewStore extends EventEmitter { wasContextSwitch: false, viewingCall: false, }); - doClearCurrentVoiceBroadcastPlaybackIfStopped(this.stores.voiceBroadcastPlaybacksStore); - break; - case "MatrixActions.RoomState.events": - this.onRoomStateEvents((payload as IRoomStateEventsActionPayload).event); break; case Action.ViewRoomError: this.viewRoomError(payload as ViewRoomErrorPayload); @@ -489,9 +429,6 @@ export class RoomViewStore extends EventEmitter { } if (room) { - pauseNonLiveBroadcastFromOtherRoom(room, this.stores.voiceBroadcastPlaybacksStore); - this.doMaybeSetCurrentVoiceBroadcastPlayback(room); - await setMarkedUnreadState(room, MatrixClientPeg.safeGet(), false); } } else if (payload.room_alias) { diff --git a/src/stores/ThreepidInviteStore.ts b/src/stores/ThreepidInviteStore.ts index 23c6d450d60..bdb1c8faa26 100644 --- a/src/stores/ThreepidInviteStore.ts +++ b/src/stores/ThreepidInviteStore.ts @@ -99,7 +99,7 @@ export default class ThreepidInviteStore extends EventEmitter { private generateIdOf(persisted: IPersistedThreepidInvite): string { // Use a consistent "hash" to form an ID. - return base32.stringify(Buffer.from(JSON.stringify(persisted))); + return base32.stringify(new TextEncoder().encode(JSON.stringify(persisted))); } private translateInvite(persisted: IPersistedThreepidInvite): IThreepidInvite { diff --git a/src/stores/right-panel/RightPanelStore.ts b/src/stores/right-panel/RightPanelStore.ts index 9da06580dc5..99b2d7fe507 100644 --- a/src/stores/right-panel/RightPanelStore.ts +++ b/src/stores/right-panel/RightPanelStore.ts @@ -304,15 +304,13 @@ export default class RightPanelStore extends ReadyWatchingStore { logger.warn("removed card from right panel because of missing threadHeadEvent in card state"); } return !!card.state?.threadHeadEvent; - case RightPanelPhases.RoomMemberInfo: - case RightPanelPhases.SpaceMemberInfo: + case RightPanelPhases.MemberInfo: case RightPanelPhases.EncryptionPanel: if (!card.state?.member) { logger.warn("removed card from right panel because of missing member in card state"); } return !!card.state?.member; - case RightPanelPhases.Room3pidMemberInfo: - case RightPanelPhases.Space3pidMemberInfo: + case RightPanelPhases.ThreePidMemberInfo: if (!card.state?.memberInfoEvent) { logger.warn("removed card from right panel because of missing memberInfoEvent in card state"); } @@ -327,7 +325,7 @@ export default class RightPanelStore extends ReadyWatchingStore { } private getVerificationRedirect(card: IRightPanelCard): IRightPanelCard | null { - if (card.phase === RightPanelPhases.RoomMemberInfo && card.state) { + if (card.phase === RightPanelPhases.MemberInfo && card.state) { // RightPanelPhases.RoomMemberInfo -> needs to be changed to RightPanelPhases.EncryptionPanel if there is a pending verification request const { member } = card.state; const pendingRequest = member @@ -385,8 +383,7 @@ export default class RightPanelStore extends ReadyWatchingStore { if (panel?.history) { panel.history = panel.history.filter( (card: IRightPanelCard) => - card.phase != RightPanelPhases.RoomMemberInfo && - card.phase != RightPanelPhases.Room3pidMemberInfo, + card.phase != RightPanelPhases.MemberInfo && card.phase != RightPanelPhases.ThreePidMemberInfo, ); } } diff --git a/src/stores/right-panel/RightPanelStoreIPanelState.ts b/src/stores/right-panel/RightPanelStoreIPanelState.ts index afb74425636..0d205abd2fa 100644 --- a/src/stores/right-panel/RightPanelStoreIPanelState.ts +++ b/src/stores/right-panel/RightPanelStoreIPanelState.ts @@ -16,7 +16,6 @@ export interface IRightPanelCardState { verificationRequest?: VerificationRequest; verificationRequestPromise?: Promise; widgetId?: string; - spaceId?: string; // Room3pidMemberInfo, Space3pidMemberInfo, memberInfoEvent?: MatrixEvent; // threads @@ -32,7 +31,6 @@ export interface IRightPanelCardStateStored { memberId?: string; // we do not store the things associated with verification widgetId?: string; - spaceId?: string; // 3pidMemberInfo memberInfoEventId?: string; // threads @@ -80,7 +78,6 @@ export function convertCardToStore(panelState: IRightPanelCard): IRightPanelCard const state = panelState.state ?? {}; const stateStored: IRightPanelCardStateStored = { widgetId: state.widgetId, - spaceId: state.spaceId, isInitialEventHighlighted: state.isInitialEventHighlighted, initialEventScrollIntoView: state.initialEventScrollIntoView, threadHeadEventId: !!state?.threadHeadEvent?.getId() ? state.threadHeadEvent.getId() : undefined, @@ -97,7 +94,6 @@ function convertStoreToCard(panelStateStore: IRightPanelCardStored, room: Room): const stateStored = panelStateStore.state ?? {}; const state: IRightPanelCardState = { widgetId: stateStored.widgetId, - spaceId: stateStored.spaceId, isInitialEventHighlighted: stateStored.isInitialEventHighlighted, initialEventScrollIntoView: stateStored.initialEventScrollIntoView, threadHeadEvent: !!stateStored?.threadHeadEventId diff --git a/src/stores/right-panel/RightPanelStorePhases.ts b/src/stores/right-panel/RightPanelStorePhases.ts index 60b9e50bafa..9e7a5697bfa 100644 --- a/src/stores/right-panel/RightPanelStorePhases.ts +++ b/src/stores/right-panel/RightPanelStorePhases.ts @@ -10,11 +10,14 @@ import { _t } from "../../languageHandler"; // These are in their own file because of circular imports being a problem. export enum RightPanelPhases { + // Room & Space stuff + MemberList = "MemberList", + MemberInfo = "MemberInfo", + ThreePidMemberInfo = "ThreePidMemberInfo", + // Room stuff - RoomMemberList = "RoomMemberList", FilePanel = "FilePanel", NotificationPanel = "NotificationPanel", - RoomMemberInfo = "RoomMemberInfo", EncryptionPanel = "EncryptionPanel", RoomSummary = "RoomSummary", Widget = "Widget", @@ -22,13 +25,6 @@ export enum RightPanelPhases { Timeline = "Timeline", Extensions = "Extensions", - Room3pidMemberInfo = "Room3pidMemberInfo", - - // Space stuff - SpaceMemberList = "SpaceMemberList", - SpaceMemberInfo = "SpaceMemberInfo", - Space3pidMemberInfo = "Space3pidMemberInfo", - // Thread stuff ThreadView = "ThreadView", ThreadPanel = "ThreadPanel", @@ -42,7 +38,7 @@ export function backLabelForPhase(phase: RightPanelPhases | null): string | null return _t("chat_card_back_action_label"); case RightPanelPhases.RoomSummary: return _t("room_summary_card_back_action_label"); - case RightPanelPhases.RoomMemberList: + case RightPanelPhases.MemberList: return _t("member_list_back_action_label"); case RightPanelPhases.ThreadView: return _t("thread_view_back_action_label"); diff --git a/src/stores/right-panel/action-handlers/View3pidInvite.ts b/src/stores/right-panel/action-handlers/View3pidInvite.ts index e2aa191acb3..0f6661819f6 100644 --- a/src/stores/right-panel/action-handlers/View3pidInvite.ts +++ b/src/stores/right-panel/action-handlers/View3pidInvite.ts @@ -20,10 +20,10 @@ import { RightPanelPhases } from "../RightPanelStorePhases"; export const onView3pidInvite = (payload: ActionPayload, rightPanelStore: RightPanelStore): void => { if (payload.event) { rightPanelStore.pushCard({ - phase: RightPanelPhases.Room3pidMemberInfo, + phase: RightPanelPhases.ThreePidMemberInfo, state: { memberInfoEvent: payload.event }, }); } else { - rightPanelStore.showOrHidePhase(RightPanelPhases.RoomMemberList); + rightPanelStore.showOrHidePhase(RightPanelPhases.MemberList); } }; diff --git a/src/stores/room-list/MessagePreviewStore.ts b/src/stores/room-list/MessagePreviewStore.ts index e0e06ec980e..2577b2ba235 100644 --- a/src/stores/room-list/MessagePreviewStore.ts +++ b/src/stores/room-list/MessagePreviewStore.ts @@ -22,8 +22,6 @@ import { StickerEventPreview } from "./previews/StickerEventPreview"; import { ReactionEventPreview } from "./previews/ReactionEventPreview"; import { UPDATE_EVENT } from "../AsyncStore"; import { IPreview } from "./previews/IPreview"; -import { VoiceBroadcastInfoEventType } from "../../voice-broadcast"; -import { VoiceBroadcastPreview } from "./previews/VoiceBroadcastPreview"; import shouldHideEvent from "../../shouldHideEvent"; // Emitted event for when a room's preview has changed. First argument will the room for which @@ -69,10 +67,6 @@ const PREVIEWS: Record< isState: false, previewer: new PollStartEventPreview(), }, - [VoiceBroadcastInfoEventType]: { - isState: true, - previewer: new VoiceBroadcastPreview(), - }, }; // The maximum number of events we're willing to look back on to get a preview. diff --git a/src/stores/room-list/previews/MessageEventPreview.ts b/src/stores/room-list/previews/MessageEventPreview.ts index 2873320cf38..20631f1425b 100644 --- a/src/stores/room-list/previews/MessageEventPreview.ts +++ b/src/stores/room-list/previews/MessageEventPreview.ts @@ -14,15 +14,11 @@ import { _t, sanitizeForTranslation } from "../../../languageHandler"; import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils"; import { getHtmlText } from "../../../HtmlUtils"; import { stripHTMLReply, stripPlainReply } from "../../../utils/Reply"; -import { VoiceBroadcastChunkEventType } from "../../../voice-broadcast/types"; export class MessageEventPreview implements IPreview { public getTextFor(event: MatrixEvent, tagId?: TagID, isThread?: boolean): string | null { let eventContent = event.getContent(); - // no preview for broadcast chunks - if (eventContent[VoiceBroadcastChunkEventType]) return null; - if (event.isRelation(RelationType.Replace)) { // It's an edit, generate the preview on the new text eventContent = event.getContent()["m.new_content"]; diff --git a/src/stores/room-list/previews/PollStartEventPreview.ts b/src/stores/room-list/previews/PollStartEventPreview.ts index bb005f4a940..7548cf12f71 100644 --- a/src/stores/room-list/previews/PollStartEventPreview.ts +++ b/src/stores/room-list/previews/PollStartEventPreview.ts @@ -18,7 +18,7 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext"; export class PollStartEventPreview implements IPreview { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public getTextFor(event: MatrixEvent, tagId?: TagID, isThread?: boolean): string | null { let eventContent = event.getContent(); diff --git a/src/stores/room-list/previews/VoiceBroadcastPreview.ts b/src/stores/room-list/previews/VoiceBroadcastPreview.ts deleted file mode 100644 index 94116692a69..00000000000 --- a/src/stores/room-list/previews/VoiceBroadcastPreview.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixEvent } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastInfoState } from "../../../voice-broadcast/types"; -import { textForVoiceBroadcastStoppedEventWithoutLink } from "../../../voice-broadcast/utils/textForVoiceBroadcastStoppedEventWithoutLink"; -import { IPreview } from "./IPreview"; - -export class VoiceBroadcastPreview implements IPreview { - public getTextFor(event: MatrixEvent, tagId?: string, isThread?: boolean): string | null { - if (!event.isRedacted() && event.getContent()?.state === VoiceBroadcastInfoState.Stopped) { - return textForVoiceBroadcastStoppedEventWithoutLink(event); - } - - return null; - } -} diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index 8362f1048a0..0472b1664b1 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -284,10 +284,6 @@ export class StopGapWidget extends EventEmitter { }); this.messaging.on("capabilitiesNotified", () => this.emit("capabilitiesNotified")); this.messaging.on(`action:${WidgetApiFromWidgetAction.OpenModalWidget}`, this.onOpenModal); - this.messaging.on(`action:${ElementWidgetActions.JoinCall}`, () => { - // pause voice broadcast recording when any widget sends a "join" - SdkContextClass.instance.voiceBroadcastRecordingsStore.getCurrent()?.pause(); - }); // Always attach a handler for ViewRoom, but permission check it internally this.messaging.on(`action:${ElementWidgetActions.ViewRoom}`, (ev: CustomEvent) => { diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index 5bc2ac7fc01..de7a71fa800 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -194,6 +194,7 @@ export class StopGapWidgetDriver extends WidgetDriver { EventType.CallSDPStreamMetadataChanged, EventType.CallSDPStreamMetadataChangedPrefix, EventType.CallReplaces, + EventType.CallEncryptionKeysPrefix, ]; for (const eventType of sendRecvToDevice) { this.allowedCapabilities.add( diff --git a/src/theme.ts b/src/theme.ts index ebb2cf27d8b..836fceff996 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -7,6 +7,20 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ +import "@fontsource/inter/400.css"; +import "@fontsource/inter/400-italic.css"; +import "@fontsource/inter/500.css"; +import "@fontsource/inter/500-italic.css"; +import "@fontsource/inter/600.css"; +import "@fontsource/inter/600-italic.css"; +import "@fontsource/inter/700.css"; +import "@fontsource/inter/700-italic.css"; + +import "@fontsource/inconsolata/latin-ext-400.css"; +import "@fontsource/inconsolata/latin-400.css"; +import "@fontsource/inconsolata/latin-ext-700.css"; +import "@fontsource/inconsolata/latin-700.css"; + import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "./languageHandler"; diff --git a/src/toasts/IncomingCallToast.tsx b/src/toasts/IncomingCallToast.tsx index bdad2d45650..58e6980733e 100644 --- a/src/toasts/IncomingCallToast.tsx +++ b/src/toasts/IncomingCallToast.tsx @@ -33,7 +33,7 @@ import { useEventEmitter } from "../hooks/useEventEmitter"; import { CallStore, CallStoreEvent } from "../stores/CallStore"; export const getIncomingCallToastKey = (callId: string, roomId: string): string => `call_${callId}_${roomId}`; -const MAX_RING_TIME_MS = 10 * 1000; +const MAX_RING_TIME_MS = 90 * 1000; interface JoinCallButtonWithCallProps { onClick: (e: ButtonEvent) => void; diff --git a/src/utils/EventRenderingUtils.ts b/src/utils/EventRenderingUtils.ts index 099bf768d87..ed8d4af1012 100644 --- a/src/utils/EventRenderingUtils.ts +++ b/src/utils/EventRenderingUtils.ts @@ -21,7 +21,6 @@ import SettingsStore from "../settings/SettingsStore"; import { haveRendererForEvent, JitsiEventFactory, JSONEventFactory, pickFactory } from "../events/EventTileFactory"; import { getMessageModerationState, isLocationEvent, MessageModerationState } from "./EventUtils"; import { ElementCall } from "../models/Call"; -import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../voice-broadcast"; const calcIsInfoMessage = ( eventType: EventType | string, @@ -38,8 +37,7 @@ const calcIsInfoMessage = ( eventType !== EventType.RoomCreate && !M_POLL_START.matches(eventType) && !M_POLL_END.matches(eventType) && - !M_BEACON_INFO.matches(eventType) && - !(eventType === VoiceBroadcastInfoEventType && content?.state === VoiceBroadcastInfoState.Started) + !M_BEACON_INFO.matches(eventType) ); }; @@ -91,8 +89,7 @@ export function getEventDisplayInfo( (eventType === EventType.RoomMessage && msgtype === MsgType.Emote) || M_POLL_START.matches(eventType) || M_BEACON_INFO.matches(eventType) || - isLocationEvent(mxEvent) || - eventType === VoiceBroadcastInfoEventType; + isLocationEvent(mxEvent); // If we're showing hidden events in the timeline, we should use the // source tile when there's no regular tile for an event and also for diff --git a/src/utils/EventUtils.ts b/src/utils/EventUtils.ts index 7c5b80697bf..d57cefa1b53 100644 --- a/src/utils/EventUtils.ts +++ b/src/utils/EventUtils.ts @@ -30,7 +30,6 @@ import { TimelineRenderingType } from "../contexts/RoomContext"; import { launchPollEditor } from "../components/views/messages/MPollBody"; import { Action } from "../dispatcher/actions"; import { ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload"; -import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../voice-broadcast/types"; /** * Returns whether an event should allow actions like reply, reactions, edit, etc. @@ -56,9 +55,7 @@ export function isContentActionable(mxEvent: MatrixEvent): boolean { mxEvent.getType() === "m.sticker" || M_POLL_START.matches(mxEvent.getType()) || M_POLL_END.matches(mxEvent.getType()) || - M_BEACON_INFO.matches(mxEvent.getType()) || - (mxEvent.getType() === VoiceBroadcastInfoEventType && - mxEvent.getContent()?.state === VoiceBroadcastInfoState.Started) + M_BEACON_INFO.matches(mxEvent.getType()) ) { return true; } diff --git a/src/utils/FontManager.ts b/src/utils/FontManager.ts deleted file mode 100644 index 1ffa653a271..00000000000 --- a/src/utils/FontManager.ts +++ /dev/null @@ -1,124 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2019-2021 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -/* - * Based on... - * ChromaCheck 1.16 - * author Roel Nieskens, https://pixelambacht.nl - * MIT license - */ -import { logger } from "matrix-js-sdk/src/logger"; - -function safariVersionCheck(ua: string): boolean { - logger.log("Browser is Safari - checking version for COLR support"); - try { - const safariVersionMatch = ua.match(/Mac OS X ([\d|_]+).*Version\/([\d|.]+).*Safari/); - if (safariVersionMatch) { - const macOSVersionStr = safariVersionMatch[1]; - const safariVersionStr = safariVersionMatch[2]; - const macOSVersion = macOSVersionStr.split("_").map((n) => parseInt(n, 10)); - const safariVersion = safariVersionStr.split(".").map((n) => parseInt(n, 10)); - const colrFontSupported = - macOSVersion[0] >= 10 && macOSVersion[1] >= 14 && safariVersion[0] >= 12 && safariVersion[0] < 17; - // https://www.colorfonts.wtf/ states Safari supports COLR fonts from this version on but Safari 17 breaks it - logger.log( - `COLR support on Safari requires macOS 10.14 and Safari 12-16, ` + - `detected Safari ${safariVersionStr} on macOS ${macOSVersionStr}, ` + - `COLR supported: ${colrFontSupported}`, - ); - return colrFontSupported; - } - } catch (err) { - logger.error("Error in Safari COLR version check", err); - } - logger.warn("Couldn't determine Safari version to check COLR font support, assuming no."); - return false; -} - -async function isColrFontSupported(): Promise { - logger.log("Checking for COLR support"); - - const { userAgent } = navigator; - // Firefox has supported COLR fonts since version 26 - // but doesn't support the check below without - // "Extract canvas data" permissions - // when content blocking is enabled. - if (userAgent.includes("Firefox")) { - logger.log("Browser is Firefox - assuming COLR is supported"); - return true; - } - // Safari doesn't wait for the font to load (if it doesn't have it in cache) - // to emit the load event on the image, so there is no way to not make the check - // reliable. Instead sniff the version. - // Excluding "Chrome", as it's user agent unhelpfully also contains Safari... - if (!userAgent.includes("Chrome") && userAgent.includes("Safari")) { - return safariVersionCheck(userAgent); - } - - try { - const canvas = document.createElement("canvas"); - const context = canvas.getContext("2d")!; - const img = new Image(); - // eslint-disable-next-line - const fontCOLR = - "d09GRgABAAAAAAKAAAwAAAAAAowAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABDT0xSAAACVAAAABYAAAAYAAIAJUNQQUwAAAJsAAAAEgAAABLJAAAQT1MvMgAAAYAAAAA6AAAAYBfxJ0pjbWFwAAABxAAAACcAAAAsAAzpM2dseWYAAAH0AAAAGgAAABoNIh0kaGVhZAAAARwAAAAvAAAANgxLumdoaGVhAAABTAAAABUAAAAkCAEEAmhtdHgAAAG8AAAABgAAAAYEAAAAbG9jYQAAAewAAAAGAAAABgANAABtYXhwAAABZAAAABsAAAAgAg4AHW5hbWUAAAIQAAAAOAAAAD4C5wsecG9zdAAAAkgAAAAMAAAAIAADAAB4AWNgZGAAYQ5+qdB4fpuvDNIsDCBwaQGTAIi+VlscBaJZGMDiHAxMIAoAtjIF/QB4AWNgZGBgYQACOAkUQQWMAAGRABAAAAB4AWNgZGBgYGJgAdMMUJILJMQgAWICAAH3AC4AeAFjYGFhYJzAwMrAwDST6QwDA0M/hGZ8zWDMyMmAChgFkDgKQMBw4CXDSwYWEBdIYgAFBgYA/8sIdAAABAAAAAAAAAB4AWNgYGBkYAZiBgYeBhYGBSDNAoRA/kuG//8hpDgjWJ4BAFVMBiYAAAAAAAANAAAAAQAAAAAEAAQAAAMAABEhESEEAPwABAD8AAAAeAEtxgUNgAAAAMHHIQTShTlOAty9/4bf7AARCwlBNhBw4L/43qXjYGUmf19TMuLcj/BJL3XfBg54AWNgZsALAAB9AAR4AWNgYGAEYj4gFgGygGwICQACOwAoAAAAAAABAAEAAQAAAA4AAAAAyP8AAA=="; - const svg = ` - - - - - - `; - canvas.width = 20; - canvas.height = 100; - - img.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg); - - logger.log("Waiting for COLR SVG to load"); - await new Promise((resolve) => (img.onload = resolve)); - logger.log("Drawing canvas to detect COLR support"); - context.drawImage(img, 0, 0); - const colrFontSupported = context.getImageData(10, 10, 1, 1).data[0] === 200; - logger.log("Canvas check revealed COLR is supported? " + colrFontSupported); - return colrFontSupported; - } catch (e) { - logger.error("Couldn't load COLR font", e); - return false; - } -} - -let colrFontCheckStarted = false; -export async function fixupColorFonts(): Promise { - if (colrFontCheckStarted) { - return; - } - colrFontCheckStarted = true; - - if (await isColrFontSupported()) { - // eslint-disable-next-line @typescript-eslint/no-require-imports - const path = `url('${require("../../res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2")}')`; - document.fonts.add(new FontFace("Twemoji", path, {})); - // For at least Chrome on Windows 10, we have to explictly add extra - // weights for the emoji to appear in bold messages, etc. - document.fonts.add(new FontFace("Twemoji", path, { weight: "600" })); - document.fonts.add(new FontFace("Twemoji", path, { weight: "700" })); - } else { - // fall back to SBIX, generated via https://github.com/matrix-org/twemoji-colr/tree/matthew/sbix - // eslint-disable-next-line @typescript-eslint/no-require-imports - const path = `url('${require("../../res/fonts/Twemoji_Mozilla/TwemojiMozilla-sbix.woff2")}')`; - document.fonts.add(new FontFace("Twemoji", path, {})); - document.fonts.add(new FontFace("Twemoji", path, { weight: "600" })); - document.fonts.add(new FontFace("Twemoji", path, { weight: "700" })); - } - // ...and if SBIX is not supported, the browser will fall back to one of the native fonts specified. -} diff --git a/src/utils/WidgetUtils.ts b/src/utils/WidgetUtils.ts index de158efceff..ad2ed63ba16 100644 --- a/src/utils/WidgetUtils.ts +++ b/src/utils/WidgetUtils.ts @@ -445,7 +445,7 @@ export default class WidgetUtils { // For compatibility with Jitsi, use base32 without padding. // More details here: // https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification - confId = base32.stringify(Buffer.from(roomId), { pad: false }); + confId = base32.stringify(new TextEncoder().encode(roomId), { pad: false }); } else { // Create a random conference ID confId = `Jitsi${randomUppercaseString(1)}${randomLowercaseString(23)}`; diff --git a/src/utils/device/dehydration.ts b/src/utils/device/dehydration.ts index f18c4c5c7cd..b27b3c54c26 100644 --- a/src/utils/device/dehydration.ts +++ b/src/utils/device/dehydration.ts @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import { logger } from "matrix-js-sdk/src/logger"; -import { Crypto } from "matrix-js-sdk/src/matrix"; +import { CryptoApi } from "matrix-js-sdk/src/crypto-api"; import { MatrixClientPeg } from "../../MatrixClientPeg"; @@ -21,7 +21,7 @@ import { MatrixClientPeg } from "../../MatrixClientPeg"; * * Dehydration can currently only be enabled by setting a flag in the .well-known file. */ -async function deviceDehydrationEnabled(crypto: Crypto.CryptoApi | undefined): Promise { +async function deviceDehydrationEnabled(crypto: CryptoApi | undefined): Promise { if (!crypto) { return false; } diff --git a/src/utils/dm/createDmLocalRoom.ts b/src/utils/dm/createDmLocalRoom.ts index 6d6cf0712b3..0a3d3123686 100644 --- a/src/utils/dm/createDmLocalRoom.ts +++ b/src/utils/dm/createDmLocalRoom.ts @@ -109,7 +109,7 @@ export async function createDmLocalRoom(client: MatrixClient, targets: Member[]) localRoom.targets = targets; localRoom.updateMyMembership(KnownMembership.Join); - localRoom.addLiveEvents(events); + localRoom.addLiveEvents(events, { addToState: true }); localRoom.currentState.setStateEvents(events); localRoom.name = localRoom.getDefaultRoomName(client.getUserId()!); client.store.storeRoom(localRoom); diff --git a/src/utils/exportUtils/HtmlExport.tsx b/src/utils/exportUtils/HtmlExport.tsx index 9a6bb93bba8..e5b4667fc22 100644 --- a/src/utils/exportUtils/HtmlExport.tsx +++ b/src/utils/exportUtils/HtmlExport.tsx @@ -431,7 +431,7 @@ export default class HTMLExporter extends Exporter { !this.needsDateSeparator(event, prevEvent) && shouldFormContinuation(prevEvent, event, this.room.client, false); const body = await this.createMessageBody(event, shouldBeJoined); - this.totalSize += Buffer.byteLength(body); + this.totalSize += new TextEncoder().encode(body).byteLength; content += body; prevEvent = event; } diff --git a/src/utils/react.tsx b/src/utils/react.tsx index 164d704d913..b78f574fa97 100644 --- a/src/utils/react.tsx +++ b/src/utils/react.tsx @@ -15,23 +15,38 @@ import { createRoot, Root } from "react-dom/client"; export class ReactRootManager { private roots: Root[] = []; private rootElements: Element[] = []; + private revertElements: Array = []; public get elements(): Element[] { return this.rootElements; } - public render(children: ReactNode, element: Element): void { - const root = createRoot(element); + /** + * Render a React component into a new root based on the given root element + * @param children the React component to render + * @param rootElement the root element to render the component into + * @param revertElement the element to replace the root element with when unmounting + */ + public render(children: ReactNode, rootElement: Element, revertElement?: Element): void { + const root = createRoot(rootElement); this.roots.push(root); - this.rootElements.push(element); + this.rootElements.push(rootElement); + this.revertElements.push(revertElement ?? null); root.render(children); } + /** + * Unmount all roots and revert the elements they were rendered into + */ public unmount(): void { while (this.roots.length) { const root = this.roots.pop()!; - this.rootElements.pop(); + const rootElement = this.rootElements.pop(); + const revertElement = this.revertElements.pop(); root.unmount(); + if (revertElement) { + rootElement?.replaceWith(revertElement); + } } } } diff --git a/src/utils/tokens/pickling.ts b/src/utils/tokens/pickling.ts index a56915a4887..5fc82c16a5c 100644 --- a/src/utils/tokens/pickling.ts +++ b/src/utils/tokens/pickling.ts @@ -118,7 +118,7 @@ export async function buildAndEncodePickleKey( data.encrypted, ); if (pickleKeyBuf) { - return encodeUnpaddedBase64(pickleKeyBuf); + return encodeUnpaddedBase64(new Uint8Array(pickleKeyBuf)); } } catch { logger.error("Error decrypting pickle key"); diff --git a/src/utils/useId.ts b/src/utils/useId.ts deleted file mode 100644 index 6f7cf795980..00000000000 --- a/src/utils/useId.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2024 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; - -const getUniqueId = (() => { - return () => `:r${Math.random()}:`; -})(); - -// Replace this with React's own useId once we switch to React 18 -export const useId = (): string => React.useMemo(getUniqueId, []); diff --git a/src/vector/index.ts b/src/vector/index.ts index 60f0868eeb9..42b69af70e5 100644 --- a/src/vector/index.ts +++ b/src/vector/index.ts @@ -67,6 +67,10 @@ function checkBrowserFeatures(): boolean { // although this would start to make (more) assumptions about how rust-crypto loads its wasm. window.Modernizr.addTest("wasm", () => typeof WebAssembly === "object" && typeof WebAssembly.Module === "function"); + // Check that the session is in a secure context otherwise most Crypto & WebRTC APIs will be unavailable + // https://developer.mozilla.org/en-US/docs/Web/API/Window/isSecureContext + window.Modernizr.addTest("securecontext", () => window.isSecureContext); + const featureList = Object.keys(window.Modernizr) as Array; let featureComplete = true; diff --git a/src/verification.ts b/src/verification.ts index e446186f802..9f774964998 100644 --- a/src/verification.ts +++ b/src/verification.ts @@ -15,7 +15,6 @@ import { RightPanelPhases } from "./stores/right-panel/RightPanelStorePhases"; import { accessSecretStorage } from "./SecurityManager"; import UntrustedDeviceDialog from "./components/views/dialogs/UntrustedDeviceDialog"; import { IDevice } from "./components/views/right_panel/UserInfo"; -import { ManualDeviceKeyVerificationDialog } from "./components/views/dialogs/ManualDeviceKeyVerificationDialog"; import RightPanelStore from "./stores/right-panel/RightPanelStore"; import { IRightPanelCardState } from "./stores/right-panel/RightPanelStoreIPanelState"; import { findDMForUser } from "./utils/dm/findDMForUser"; @@ -53,11 +52,6 @@ export async function verifyDevice(matrixClient: MatrixClient, user: User, devic .getCrypto() ?.requestDeviceVerification(user.userId, device.deviceId); setRightPanel({ member: user, verificationRequestPromise }); - } else if (action === "legacy") { - Modal.createDialog(ManualDeviceKeyVerificationDialog, { - userId: user.userId, - device, - }); } }, }); @@ -81,7 +75,7 @@ function setRightPanel(state: IRightPanelCardState): void { } else { RightPanelStore.instance.setCards([ { phase: RightPanelPhases.RoomSummary }, - { phase: RightPanelPhases.RoomMemberInfo, state: { member: state.member } }, + { phase: RightPanelPhases.MemberInfo, state: { member: state.member } }, { phase: RightPanelPhases.EncryptionPanel, state }, ]); } diff --git a/src/voice-broadcast/audio/VoiceBroadcastRecorder.ts b/src/voice-broadcast/audio/VoiceBroadcastRecorder.ts deleted file mode 100644 index 8a6e17a1a5f..00000000000 --- a/src/voice-broadcast/audio/VoiceBroadcastRecorder.ts +++ /dev/null @@ -1,181 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { isEqual } from "lodash"; -import { Optional } from "matrix-events-sdk"; -import { logger } from "matrix-js-sdk/src/logger"; -import { TypedEventEmitter } from "matrix-js-sdk/src/matrix"; - -import { getChunkLength } from ".."; -import { IRecordingUpdate, VoiceRecording } from "../../audio/VoiceRecording"; -import { concat } from "../../utils/arrays"; -import { IDestroyable } from "../../utils/IDestroyable"; -import { Singleflight } from "../../utils/Singleflight"; - -export enum VoiceBroadcastRecorderEvent { - ChunkRecorded = "chunk_recorded", - CurrentChunkLengthUpdated = "current_chunk_length_updated", -} - -interface EventMap { - [VoiceBroadcastRecorderEvent.ChunkRecorded]: (chunk: ChunkRecordedPayload) => void; - [VoiceBroadcastRecorderEvent.CurrentChunkLengthUpdated]: (length: number) => void; -} - -export interface ChunkRecordedPayload { - buffer: Uint8Array; - length: number; -} - -// char sequence of "OpusHead" -const OpusHead = [79, 112, 117, 115, 72, 101, 97, 100]; - -// char sequence of "OpusTags" -const OpusTags = [79, 112, 117, 115, 84, 97, 103, 115]; - -/** - * This class provides the function to seamlessly record fixed length chunks. - * Subscribe with on(VoiceBroadcastRecordingEvents.ChunkRecorded, (payload: ChunkRecordedPayload) => {}) - * to retrieve chunks while recording. - */ -export class VoiceBroadcastRecorder - extends TypedEventEmitter - implements IDestroyable -{ - private opusHead?: Uint8Array; - private opusTags?: Uint8Array; - private chunkBuffer = new Uint8Array(0); - // position of the previous chunk in seconds - private previousChunkEndTimePosition = 0; - // current chunk length in seconds - private currentChunkLength = 0; - - public constructor( - private voiceRecording: VoiceRecording, - public readonly targetChunkLength: number, - ) { - super(); - this.voiceRecording.onDataAvailable = this.onDataAvailable; - } - - public async start(): Promise { - await this.voiceRecording.start(); - this.voiceRecording.liveData.onUpdate((data: IRecordingUpdate) => { - this.setCurrentChunkLength(data.timeSeconds - this.previousChunkEndTimePosition); - }); - } - - /** - * Stops the recording and returns the remaining chunk (if any). - */ - public async stop(): Promise> { - try { - await this.voiceRecording.stop(); - } catch { - // Ignore if the recording raises any error. - } - - // forget about that call, so that we can stop it again later - Singleflight.forgetAllFor(this.voiceRecording); - const chunk = this.extractChunk(); - this.currentChunkLength = 0; - this.previousChunkEndTimePosition = 0; - return chunk; - } - - public get contentType(): string { - return this.voiceRecording.contentType; - } - - private setCurrentChunkLength(currentChunkLength: number): void { - if (this.currentChunkLength === currentChunkLength) return; - - this.currentChunkLength = currentChunkLength; - this.emit(VoiceBroadcastRecorderEvent.CurrentChunkLengthUpdated, currentChunkLength); - } - - public getCurrentChunkLength(): number { - return this.currentChunkLength; - } - - private onDataAvailable = (data: ArrayBuffer): void => { - const dataArray = new Uint8Array(data); - - // extract the part, that contains the header type info - const headerType = Array.from(dataArray.slice(28, 36)); - - if (isEqual(OpusHead, headerType)) { - // data seems to be an "OpusHead" header - this.opusHead = dataArray; - return; - } - - if (isEqual(OpusTags, headerType)) { - // data seems to be an "OpusTags" header - this.opusTags = dataArray; - return; - } - - this.setCurrentChunkLength(this.voiceRecording.recorderSeconds! - this.previousChunkEndTimePosition); - this.handleData(dataArray); - }; - - private handleData(data: Uint8Array): void { - this.chunkBuffer = concat(this.chunkBuffer, data); - this.emitChunkIfTargetLengthReached(); - } - - private emitChunkIfTargetLengthReached(): void { - if (this.getCurrentChunkLength() >= this.targetChunkLength) { - this.emitAndResetChunk(); - } - } - - /** - * Extracts the current chunk and resets the buffer. - */ - private extractChunk(): Optional { - if (this.chunkBuffer.length === 0) { - return null; - } - - if (!this.opusHead || !this.opusTags) { - logger.warn("Broadcast chunk cannot be extracted. OpusHead or OpusTags is missing."); - return null; - } - - const currentRecorderTime = this.voiceRecording.recorderSeconds!; - const payload: ChunkRecordedPayload = { - buffer: concat(this.opusHead!, this.opusTags!, this.chunkBuffer), - length: this.getCurrentChunkLength(), - }; - this.chunkBuffer = new Uint8Array(0); - this.setCurrentChunkLength(0); - this.previousChunkEndTimePosition = currentRecorderTime; - return payload; - } - - private emitAndResetChunk(): void { - if (this.chunkBuffer.length === 0) { - return; - } - - this.emit(VoiceBroadcastRecorderEvent.ChunkRecorded, this.extractChunk()!); - } - - public destroy(): void { - this.removeAllListeners(); - this.voiceRecording.destroy(); - } -} - -export const createVoiceBroadcastRecorder = (): VoiceBroadcastRecorder => { - const voiceRecording = new VoiceRecording(); - voiceRecording.disableMaxLength(); - return new VoiceBroadcastRecorder(voiceRecording, getChunkLength()); -}; diff --git a/src/voice-broadcast/components/VoiceBroadcastBody.tsx b/src/voice-broadcast/components/VoiceBroadcastBody.tsx deleted file mode 100644 index 916ee9f9072..00000000000 --- a/src/voice-broadcast/components/VoiceBroadcastBody.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React, { useContext, useEffect, useState } from "react"; -import { MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix"; - -import { - VoiceBroadcastRecordingBody, - shouldDisplayAsVoiceBroadcastRecordingTile, - VoiceBroadcastInfoEventType, - VoiceBroadcastPlaybackBody, - VoiceBroadcastInfoState, -} from ".."; -import { IBodyProps } from "../../components/views/messages/IBodyProps"; -import { RelationsHelper, RelationsHelperEvent } from "../../events/RelationsHelper"; -import { SDKContext } from "../../contexts/SDKContext"; -import { useMatrixClientContext } from "../../contexts/MatrixClientContext"; - -export const VoiceBroadcastBody: React.FC = ({ mxEvent }) => { - const sdkContext = useContext(SDKContext); - const client = useMatrixClientContext(); - const [infoState, setInfoState] = useState(mxEvent.getContent()?.state || VoiceBroadcastInfoState.Stopped); - - useEffect(() => { - const onInfoEvent = (event: MatrixEvent): void => { - if (event.getContent()?.state === VoiceBroadcastInfoState.Stopped) { - // only a stopped event can change the tile state - setInfoState(VoiceBroadcastInfoState.Stopped); - } - }; - - const relationsHelper = new RelationsHelper( - mxEvent, - RelationType.Reference, - VoiceBroadcastInfoEventType, - client, - ); - relationsHelper.on(RelationsHelperEvent.Add, onInfoEvent); - relationsHelper.emitCurrent(); - - return () => { - relationsHelper.destroy(); - }; - }); - - if (shouldDisplayAsVoiceBroadcastRecordingTile(infoState, client, mxEvent)) { - const recording = sdkContext.voiceBroadcastRecordingsStore.getByInfoEvent(mxEvent, client); - return ; - } - - const playback = sdkContext.voiceBroadcastPlaybacksStore.getByInfoEvent(mxEvent, client); - return ; -}; diff --git a/src/voice-broadcast/components/atoms/LiveBadge.tsx b/src/voice-broadcast/components/atoms/LiveBadge.tsx deleted file mode 100644 index 2591fee4357..00000000000 --- a/src/voice-broadcast/components/atoms/LiveBadge.tsx +++ /dev/null @@ -1,30 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import classNames from "classnames"; -import React from "react"; - -import { Icon as LiveIcon } from "../../../../res/img/compound/live-16px.svg"; -import { _t } from "../../../languageHandler"; - -interface Props { - grey?: boolean; -} - -export const LiveBadge: React.FC = ({ grey = false }) => { - const liveBadgeClasses = classNames("mx_LiveBadge", { - "mx_LiveBadge--grey": grey, - }); - - return ( -
- - {_t("voice_broadcast|live")} -
- ); -}; diff --git a/src/voice-broadcast/components/atoms/SeekButton.tsx b/src/voice-broadcast/components/atoms/SeekButton.tsx deleted file mode 100644 index 5ee08264888..00000000000 --- a/src/voice-broadcast/components/atoms/SeekButton.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; - -import AccessibleButton from "../../../components/views/elements/AccessibleButton"; - -interface Props { - icon: React.FC>; - label: string; - onClick: () => void; -} - -export const SeekButton: React.FC = ({ onClick, icon: Icon, label }) => { - return ( - - - - ); -}; diff --git a/src/voice-broadcast/components/atoms/VoiceBroadcastControl.tsx b/src/voice-broadcast/components/atoms/VoiceBroadcastControl.tsx deleted file mode 100644 index 177b8fd732f..00000000000 --- a/src/voice-broadcast/components/atoms/VoiceBroadcastControl.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import classNames from "classnames"; -import React, { ReactElement } from "react"; - -import AccessibleButton from "../../../components/views/elements/AccessibleButton"; - -interface Props { - className?: string; - icon: ReactElement; - label: string; - onClick: () => void; -} - -export const VoiceBroadcastControl: React.FC = ({ className = "", icon, label, onClick }) => { - return ( - - {icon} - - ); -}; diff --git a/src/voice-broadcast/components/atoms/VoiceBroadcastError.tsx b/src/voice-broadcast/components/atoms/VoiceBroadcastError.tsx deleted file mode 100644 index d326853f4e7..00000000000 --- a/src/voice-broadcast/components/atoms/VoiceBroadcastError.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { WarningIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; - -interface Props { - message: string; -} - -export const VoiceBroadcastError: React.FC = ({ message }) => { - return ( -
- - {message} -
- ); -}; diff --git a/src/voice-broadcast/components/atoms/VoiceBroadcastHeader.tsx b/src/voice-broadcast/components/atoms/VoiceBroadcastHeader.tsx deleted file mode 100644 index 52c0251c5ea..00000000000 --- a/src/voice-broadcast/components/atoms/VoiceBroadcastHeader.tsx +++ /dev/null @@ -1,139 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { Room } from "matrix-js-sdk/src/matrix"; -import classNames from "classnames"; -import CloseIcon from "@vector-im/compound-design-tokens/assets/web/icons/close"; -import MicrophoneIcon from "@vector-im/compound-design-tokens/assets/web/icons/mic-on-solid"; - -import { LiveBadge, VoiceBroadcastLiveness } from "../.."; -import { Icon as LiveIcon } from "../../../../res/img/compound/live-16px.svg"; -import { Icon as TimerIcon } from "../../../../res/img/compound/timer-16px.svg"; -import { _t } from "../../../languageHandler"; -import RoomAvatar from "../../../components/views/avatars/RoomAvatar"; -import AccessibleButton, { ButtonEvent } from "../../../components/views/elements/AccessibleButton"; -import Clock from "../../../components/views/audio_messages/Clock"; -import { formatTimeLeft } from "../../../DateUtils"; -import Spinner from "../../../components/views/elements/Spinner"; -import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; -import { Action } from "../../../dispatcher/actions"; -import dis from "../../../dispatcher/dispatcher"; - -interface VoiceBroadcastHeaderProps { - linkToRoom?: boolean; - live?: VoiceBroadcastLiveness; - liveBadgePosition?: "middle" | "right"; - onCloseClick?: () => void; - onMicrophoneLineClick?: ((e: ButtonEvent) => void | Promise) | null; - room: Room; - microphoneLabel?: string; - showBroadcast?: boolean; - showBuffering?: boolean; - bufferingPosition?: "line" | "title"; - timeLeft?: number; - showClose?: boolean; -} - -export const VoiceBroadcastHeader: React.FC = ({ - linkToRoom = false, - live = "not-live", - liveBadgePosition = "right", - onCloseClick = (): void => {}, - onMicrophoneLineClick = null, - room, - microphoneLabel, - showBroadcast = false, - showBuffering = false, - bufferingPosition = "line", - showClose = false, - timeLeft, -}) => { - const broadcast = showBroadcast && ( -
- - {_t("voice_broadcast|action")} -
- ); - - const liveBadge = live !== "not-live" && ; - - const closeButton = showClose && ( - - - - ); - - const timeLeftLine = timeLeft && ( -
- - -
- ); - - const bufferingLine = showBuffering && bufferingPosition === "line" && ( -
- - {_t("voice_broadcast|buffering")} -
- ); - - const microphoneLineClasses = classNames({ - mx_VoiceBroadcastHeader_line: true, - ["mx_VoiceBroadcastHeader_mic--clickable"]: onMicrophoneLineClick, - }); - - const microphoneLine = microphoneLabel && ( - - - {microphoneLabel} - - ); - - const onRoomAvatarOrNameClick = (): void => { - dis.dispatch({ - action: Action.ViewRoom, - room_id: room.roomId, - metricsTrigger: undefined, // other - }); - }; - - let roomAvatar = ; - let roomName = ( -
-
{room.name}
- {showBuffering && bufferingPosition === "title" && } -
- ); - - if (linkToRoom) { - roomAvatar = {roomAvatar}; - - roomName = {roomName}; - } - - return ( -
- {roomAvatar} -
- {roomName} - {microphoneLine} - {timeLeftLine} - {broadcast} - {bufferingLine} - {liveBadgePosition === "middle" && liveBadge} -
- {liveBadgePosition === "right" && liveBadge} - {closeButton} -
- ); -}; diff --git a/src/voice-broadcast/components/atoms/VoiceBroadcastPlaybackControl.tsx b/src/voice-broadcast/components/atoms/VoiceBroadcastPlaybackControl.tsx deleted file mode 100644 index 08531b8afd9..00000000000 --- a/src/voice-broadcast/components/atoms/VoiceBroadcastPlaybackControl.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022, 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React, { ReactElement } from "react"; -import PauseIcon from "@vector-im/compound-design-tokens/assets/web/icons/pause-solid"; -import PlayIcon from "@vector-im/compound-design-tokens/assets/web/icons/play-solid"; - -import { _t } from "../../../languageHandler"; -import { VoiceBroadcastControl, VoiceBroadcastPlaybackState } from "../.."; - -interface Props { - onClick: () => void; - state: VoiceBroadcastPlaybackState; -} - -export const VoiceBroadcastPlaybackControl: React.FC = ({ onClick, state }) => { - let controlIcon: ReactElement | null = null; - let controlLabel: string | null = null; - let className = ""; - - switch (state) { - case VoiceBroadcastPlaybackState.Stopped: - controlIcon = ; - className = "mx_VoiceBroadcastControl-play"; - controlLabel = _t("voice_broadcast|play"); - break; - case VoiceBroadcastPlaybackState.Paused: - controlIcon = ; - className = "mx_VoiceBroadcastControl-play"; - controlLabel = _t("voice_broadcast|resume"); - break; - case VoiceBroadcastPlaybackState.Buffering: - case VoiceBroadcastPlaybackState.Playing: - controlIcon = ; - controlLabel = _t("voice_broadcast|pause"); - break; - } - - if (controlIcon && controlLabel) { - return ( - - ); - } - - return null; -}; diff --git a/src/voice-broadcast/components/atoms/VoiceBroadcastRecordingConnectionError.tsx b/src/voice-broadcast/components/atoms/VoiceBroadcastRecordingConnectionError.tsx deleted file mode 100644 index 250d71f2f30..00000000000 --- a/src/voice-broadcast/components/atoms/VoiceBroadcastRecordingConnectionError.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { WarningIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; - -import { _t } from "../../../languageHandler"; - -export const VoiceBroadcastRecordingConnectionError: React.FC = () => { - return ( -
- - {_t("voice_broadcast|connection_error")} -
- ); -}; diff --git a/src/voice-broadcast/components/atoms/VoiceBroadcastRoomSubtitle.tsx b/src/voice-broadcast/components/atoms/VoiceBroadcastRoomSubtitle.tsx deleted file mode 100644 index 20b73797893..00000000000 --- a/src/voice-broadcast/components/atoms/VoiceBroadcastRoomSubtitle.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; - -import { Icon as LiveIcon } from "../../../../res/img/compound/live-16px.svg"; -import { _t } from "../../../languageHandler"; - -export const VoiceBroadcastRoomSubtitle: React.FC = () => { - return ( -
- - {_t("voice_broadcast|live")} -
- ); -}; diff --git a/src/voice-broadcast/components/molecules/ConfirmListenBroadcastStopCurrent.tsx b/src/voice-broadcast/components/molecules/ConfirmListenBroadcastStopCurrent.tsx deleted file mode 100644 index 3dadfeba60d..00000000000 --- a/src/voice-broadcast/components/molecules/ConfirmListenBroadcastStopCurrent.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; - -import BaseDialog from "../../../components/views/dialogs/BaseDialog"; -import DialogButtons from "../../../components/views/elements/DialogButtons"; -import { _t } from "../../../languageHandler"; -import Modal from "../../../Modal"; - -interface Props { - onFinished: (confirmed?: boolean) => void; -} - -export const ConfirmListenBroadcastStopCurrentDialog: React.FC = ({ onFinished }) => { - return ( - -

{_t("voice_broadcast|confirm_listen_description")}

- onFinished(true)} - primaryButton={_t("voice_broadcast|confirm_listen_affirm")} - cancelButton={_t("action|no")} - onCancel={() => onFinished(false)} - /> -
- ); -}; - -export const showConfirmListenBroadcastStopCurrentDialog = async (): Promise => { - const { finished } = Modal.createDialog(ConfirmListenBroadcastStopCurrentDialog); - const [confirmed] = await finished; - return !!confirmed; -}; diff --git a/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx b/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx deleted file mode 100644 index 913b144960d..00000000000 --- a/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx +++ /dev/null @@ -1,102 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React, { ReactElement } from "react"; -import classNames from "classnames"; - -import { - VoiceBroadcastError, - VoiceBroadcastHeader, - VoiceBroadcastPlayback, - VoiceBroadcastPlaybackControl, - VoiceBroadcastPlaybackState, -} from "../.."; -import { useVoiceBroadcastPlayback } from "../../hooks/useVoiceBroadcastPlayback"; -import { Icon as Back30sIcon } from "../../../../res/img/compound/back-30s-24px.svg"; -import { Icon as Forward30sIcon } from "../../../../res/img/compound/forward-30s-24px.svg"; -import { _t } from "../../../languageHandler"; -import Clock from "../../../components/views/audio_messages/Clock"; -import SeekBar from "../../../components/views/audio_messages/SeekBar"; -import { SeekButton } from "../atoms/SeekButton"; - -const SEEK_TIME = 30; - -interface VoiceBroadcastPlaybackBodyProps { - pip?: boolean; - playback: VoiceBroadcastPlayback; -} - -export const VoiceBroadcastPlaybackBody: React.FC = ({ pip = false, playback }) => { - const { times, liveness, playbackState, room, sender, toggle } = useVoiceBroadcastPlayback(playback); - - let seekBackwardButton: ReactElement | null = null; - let seekForwardButton: ReactElement | null = null; - - if (playbackState !== VoiceBroadcastPlaybackState.Stopped) { - const onSeekBackwardButtonClick = (): void => { - playback.skipTo(Math.max(0, times.position - SEEK_TIME)); - }; - - seekBackwardButton = ( - - ); - - const onSeekForwardButtonClick = (): void => { - playback.skipTo(Math.min(times.duration, times.position + SEEK_TIME)); - }; - - seekForwardButton = ( - - ); - } - - const classes = classNames({ - mx_VoiceBroadcastBody: true, - ["mx_VoiceBroadcastBody--pip"]: pip, - }); - - const content = - playbackState === VoiceBroadcastPlaybackState.Error ? ( - - ) : ( - <> -
- {seekBackwardButton} - - {seekForwardButton} -
- -
- - -
- - ); - - return ( -
- - {content} -
- ); -}; diff --git a/src/voice-broadcast/components/molecules/VoiceBroadcastPreRecordingPip.tsx b/src/voice-broadcast/components/molecules/VoiceBroadcastPreRecordingPip.tsx deleted file mode 100644 index ac742e0fd81..00000000000 --- a/src/voice-broadcast/components/molecules/VoiceBroadcastPreRecordingPip.tsx +++ /dev/null @@ -1,82 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React, { useRef, useState } from "react"; - -import { VoiceBroadcastHeader } from "../.."; -import AccessibleButton from "../../../components/views/elements/AccessibleButton"; -import { VoiceBroadcastPreRecording } from "../../models/VoiceBroadcastPreRecording"; -import { Icon as LiveIcon } from "../../../../res/img/compound/live-16px.svg"; -import { _t } from "../../../languageHandler"; -import { useAudioDeviceSelection } from "../../../hooks/useAudioDeviceSelection"; -import { DevicesContextMenu } from "../../../components/views/audio_messages/DevicesContextMenu"; - -interface Props { - voiceBroadcastPreRecording: VoiceBroadcastPreRecording; -} - -interface State { - showDeviceSelect: boolean; - disableStartButton: boolean; -} - -export const VoiceBroadcastPreRecordingPip: React.FC = ({ voiceBroadcastPreRecording }) => { - const pipRef = useRef(null); - const { currentDevice, currentDeviceLabel, devices, setDevice } = useAudioDeviceSelection(); - const [state, setState] = useState({ - showDeviceSelect: false, - disableStartButton: false, - }); - - const onDeviceSelect = (device: MediaDeviceInfo): void => { - setState((state) => ({ - ...state, - showDeviceSelect: false, - })); - setDevice(device); - }; - - const onStartBroadcastClick = (): void => { - setState((state) => ({ - ...state, - disableStartButton: true, - })); - - voiceBroadcastPreRecording.start(); - }; - - return ( -
- setState({ ...state, showDeviceSelect: true })} - room={voiceBroadcastPreRecording.room} - microphoneLabel={currentDeviceLabel} - showClose={true} - /> - - - {_t("voice_broadcast|go_live")} - - {state.showDeviceSelect && ( - - )} -
- ); -}; diff --git a/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody.tsx b/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody.tsx deleted file mode 100644 index 15547792db8..00000000000 --- a/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; - -import { - useVoiceBroadcastRecording, - VoiceBroadcastHeader, - VoiceBroadcastRecording, - VoiceBroadcastRecordingConnectionError, -} from "../.."; - -interface VoiceBroadcastRecordingBodyProps { - recording: VoiceBroadcastRecording; -} - -export const VoiceBroadcastRecordingBody: React.FC = ({ recording }) => { - const { live, room, sender, recordingState } = useVoiceBroadcastRecording(recording); - - return ( -
- - {recordingState === "connection_error" && } -
- ); -}; diff --git a/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip.tsx b/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip.tsx deleted file mode 100644 index d04132b2200..00000000000 --- a/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip.tsx +++ /dev/null @@ -1,116 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React, { useRef, useState } from "react"; -import PauseIcon from "@vector-im/compound-design-tokens/assets/web/icons/pause-solid"; -import MicrophoneIcon from "@vector-im/compound-design-tokens/assets/web/icons/mic-on-solid"; - -import { - VoiceBroadcastControl, - VoiceBroadcastInfoState, - VoiceBroadcastRecording, - VoiceBroadcastRecordingConnectionError, - VoiceBroadcastRecordingState, -} from "../.."; -import { useVoiceBroadcastRecording } from "../../hooks/useVoiceBroadcastRecording"; -import { VoiceBroadcastHeader } from "../atoms/VoiceBroadcastHeader"; -import { Icon as StopIcon } from "../../../../res/img/compound/stop-16.svg"; -import { Icon as RecordIcon } from "../../../../res/img/compound/record-10px.svg"; -import { _t } from "../../../languageHandler"; -import { useAudioDeviceSelection } from "../../../hooks/useAudioDeviceSelection"; -import { DevicesContextMenu } from "../../../components/views/audio_messages/DevicesContextMenu"; -import AccessibleButton from "../../../components/views/elements/AccessibleButton"; - -interface VoiceBroadcastRecordingPipProps { - recording: VoiceBroadcastRecording; -} - -export const VoiceBroadcastRecordingPip: React.FC = ({ recording }) => { - const pipRef = useRef(null); - const { live, timeLeft, recordingState, room, stopRecording, toggleRecording } = - useVoiceBroadcastRecording(recording); - const { currentDevice, devices, setDevice } = useAudioDeviceSelection(); - - const onDeviceSelect = async (device: MediaDeviceInfo): Promise => { - setShowDeviceSelect(false); - - if (currentDevice?.deviceId === device.deviceId) { - // device unchanged - return; - } - - setDevice(device); - - if ( - ( - [VoiceBroadcastInfoState.Paused, VoiceBroadcastInfoState.Stopped] as VoiceBroadcastRecordingState[] - ).includes(recordingState) - ) { - // Nothing to do in these cases. Resume will use the selected device. - return; - } - - // pause and resume to switch the input device - await recording.pause(); - await recording.resume(); - }; - - const [showDeviceSelect, setShowDeviceSelect] = useState(false); - - const toggleControl = - recordingState === VoiceBroadcastInfoState.Paused ? ( - } - label={_t("voice_broadcast|resume")} - /> - ) : ( - } - label={_t("voice_broadcast|pause")} - /> - ); - - const controls = - recordingState === "connection_error" ? ( - - ) : ( -
- {toggleControl} - setShowDeviceSelect(true)} - title={_t("voip|change_input_device")} - > - - - } - label="Stop Recording" - onClick={stopRecording} - /> -
- ); - - return ( -
- -
- {controls} - {showDeviceSelect && ( - - )} -
- ); -}; diff --git a/src/voice-broadcast/components/molecules/VoiceBroadcastSmallPlaybackBody.tsx b/src/voice-broadcast/components/molecules/VoiceBroadcastSmallPlaybackBody.tsx deleted file mode 100644 index a791ac75d77..00000000000 --- a/src/voice-broadcast/components/molecules/VoiceBroadcastSmallPlaybackBody.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import CloseIcon from "@vector-im/compound-design-tokens/assets/web/icons/close"; - -import { - VoiceBroadcastHeader, - VoiceBroadcastPlayback, - VoiceBroadcastPlaybackControl, - VoiceBroadcastPlaybackState, -} from "../.."; -import AccessibleButton from "../../../components/views/elements/AccessibleButton"; -import { useVoiceBroadcastPlayback } from "../../hooks/useVoiceBroadcastPlayback"; - -interface VoiceBroadcastSmallPlaybackBodyProps { - playback: VoiceBroadcastPlayback; -} - -export const VoiceBroadcastSmallPlaybackBody: React.FC = ({ playback }) => { - const { liveness, playbackState, room, sender, toggle } = useVoiceBroadcastPlayback(playback); - return ( -
- - - playback.stop()}> - - -
- ); -}; diff --git a/src/voice-broadcast/hooks/useCurrentVoiceBroadcastPlayback.ts b/src/voice-broadcast/hooks/useCurrentVoiceBroadcastPlayback.ts deleted file mode 100644 index 3ff4081a9fd..00000000000 --- a/src/voice-broadcast/hooks/useCurrentVoiceBroadcastPlayback.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { useTypedEventEmitterState } from "../../hooks/useEventEmitter"; -import { VoiceBroadcastPlayback } from "../models/VoiceBroadcastPlayback"; -import { - VoiceBroadcastPlaybacksStore, - VoiceBroadcastPlaybacksStoreEvent, -} from "../stores/VoiceBroadcastPlaybacksStore"; - -export const useCurrentVoiceBroadcastPlayback = ( - voiceBroadcastPlaybackStore: VoiceBroadcastPlaybacksStore, -): { - currentVoiceBroadcastPlayback: VoiceBroadcastPlayback | null; -} => { - const currentVoiceBroadcastPlayback = useTypedEventEmitterState( - voiceBroadcastPlaybackStore, - VoiceBroadcastPlaybacksStoreEvent.CurrentChanged, - (playback?: VoiceBroadcastPlayback) => { - return playback ?? voiceBroadcastPlaybackStore.getCurrent(); - }, - ); - - return { - currentVoiceBroadcastPlayback, - }; -}; diff --git a/src/voice-broadcast/hooks/useCurrentVoiceBroadcastPreRecording.ts b/src/voice-broadcast/hooks/useCurrentVoiceBroadcastPreRecording.ts deleted file mode 100644 index bb14e386404..00000000000 --- a/src/voice-broadcast/hooks/useCurrentVoiceBroadcastPreRecording.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { useTypedEventEmitterState } from "../../hooks/useEventEmitter"; -import { VoiceBroadcastPreRecordingStore } from "../stores/VoiceBroadcastPreRecordingStore"; -import { VoiceBroadcastPreRecording } from "../models/VoiceBroadcastPreRecording"; - -export const useCurrentVoiceBroadcastPreRecording = ( - voiceBroadcastPreRecordingStore: VoiceBroadcastPreRecordingStore, -): { - currentVoiceBroadcastPreRecording: VoiceBroadcastPreRecording | null; -} => { - const currentVoiceBroadcastPreRecording = useTypedEventEmitterState( - voiceBroadcastPreRecordingStore, - "changed", - (preRecording?: VoiceBroadcastPreRecording) => { - return preRecording ?? voiceBroadcastPreRecordingStore.getCurrent(); - }, - ); - - return { - currentVoiceBroadcastPreRecording, - }; -}; diff --git a/src/voice-broadcast/hooks/useCurrentVoiceBroadcastRecording.ts b/src/voice-broadcast/hooks/useCurrentVoiceBroadcastRecording.ts deleted file mode 100644 index 1d4abe3f10e..00000000000 --- a/src/voice-broadcast/hooks/useCurrentVoiceBroadcastRecording.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { VoiceBroadcastRecording, VoiceBroadcastRecordingsStore, VoiceBroadcastRecordingsStoreEvent } from ".."; -import { useTypedEventEmitterState } from "../../hooks/useEventEmitter"; - -export const useCurrentVoiceBroadcastRecording = ( - voiceBroadcastRecordingsStore: VoiceBroadcastRecordingsStore, -): { - currentVoiceBroadcastRecording: VoiceBroadcastRecording | null; -} => { - const currentVoiceBroadcastRecording = useTypedEventEmitterState( - voiceBroadcastRecordingsStore, - VoiceBroadcastRecordingsStoreEvent.CurrentChanged, - (recording?: VoiceBroadcastRecording) => { - return recording ?? voiceBroadcastRecordingsStore.getCurrent(); - }, - ); - - return { - currentVoiceBroadcastRecording, - }; -}; diff --git a/src/voice-broadcast/hooks/useHasRoomLiveVoiceBroadcast.ts b/src/voice-broadcast/hooks/useHasRoomLiveVoiceBroadcast.ts deleted file mode 100644 index a298f4dc830..00000000000 --- a/src/voice-broadcast/hooks/useHasRoomLiveVoiceBroadcast.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { useContext, useEffect, useMemo, useState } from "react"; -import { Room, RoomStateEvent } from "matrix-js-sdk/src/matrix"; - -import { hasRoomLiveVoiceBroadcast } from "../utils/hasRoomLiveVoiceBroadcast"; -import { useTypedEventEmitter } from "../../hooks/useEventEmitter"; -import { SDKContext } from "../../contexts/SDKContext"; - -export const useHasRoomLiveVoiceBroadcast = (room: Room): boolean => { - const sdkContext = useContext(SDKContext); - const [hasLiveVoiceBroadcast, setHasLiveVoiceBroadcast] = useState(false); - - const update = useMemo(() => { - return sdkContext?.client - ? () => { - hasRoomLiveVoiceBroadcast(sdkContext.client!, room).then( - ({ hasBroadcast }) => { - setHasLiveVoiceBroadcast(hasBroadcast); - }, - () => {}, // no update on error - ); - } - : () => {}; // noop without client - }, [room, sdkContext, setHasLiveVoiceBroadcast]); - - useEffect(() => { - update(); - }, [update]); - - useTypedEventEmitter(room.currentState, RoomStateEvent.Update, () => update()); - return hasLiveVoiceBroadcast; -}; diff --git a/src/voice-broadcast/hooks/useVoiceBroadcastPlayback.ts b/src/voice-broadcast/hooks/useVoiceBroadcastPlayback.ts deleted file mode 100644 index eb50b0de08b..00000000000 --- a/src/voice-broadcast/hooks/useVoiceBroadcastPlayback.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022, 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { Room, RoomMember } from "matrix-js-sdk/src/matrix"; - -import { useTypedEventEmitterState } from "../../hooks/useEventEmitter"; -import { MatrixClientPeg } from "../../MatrixClientPeg"; -import { - VoiceBroadcastLiveness, - VoiceBroadcastPlayback, - VoiceBroadcastPlaybackEvent, - VoiceBroadcastPlaybackState, - VoiceBroadcastPlaybackTimes, -} from ".."; - -export const useVoiceBroadcastPlayback = ( - playback: VoiceBroadcastPlayback, -): { - times: { - duration: number; - position: number; - timeLeft: number; - }; - sender: RoomMember | null; - liveness: VoiceBroadcastLiveness; - playbackState: VoiceBroadcastPlaybackState; - toggle(): void; - room: Room; -} => { - const client = MatrixClientPeg.safeGet(); - const room = client.getRoom(playback.infoEvent.getRoomId()); - - if (!room) { - throw new Error(`Voice Broadcast room not found (event ${playback.infoEvent.getId()})`); - } - - const sender = playback.infoEvent.sender; - - if (!sender) { - throw new Error(`Voice Broadcast sender not found (event ${playback.infoEvent.getId()})`); - } - - const playbackToggle = (): void => { - playback.toggle(); - }; - - const playbackState = useTypedEventEmitterState( - playback, - VoiceBroadcastPlaybackEvent.StateChanged, - (state?: VoiceBroadcastPlaybackState) => { - return state ?? playback.getState(); - }, - ); - - const times = useTypedEventEmitterState( - playback, - VoiceBroadcastPlaybackEvent.TimesChanged, - (t?: VoiceBroadcastPlaybackTimes) => { - return ( - t ?? { - duration: playback.durationSeconds, - position: playback.timeSeconds, - timeLeft: playback.timeLeftSeconds, - } - ); - }, - ); - - const liveness = useTypedEventEmitterState( - playback, - VoiceBroadcastPlaybackEvent.LivenessChanged, - (l?: VoiceBroadcastLiveness) => { - return l ?? playback.getLiveness(); - }, - ); - - return { - times, - liveness: liveness, - playbackState, - room: room, - sender, - toggle: playbackToggle, - }; -}; diff --git a/src/voice-broadcast/hooks/useVoiceBroadcastRecording.tsx b/src/voice-broadcast/hooks/useVoiceBroadcastRecording.tsx deleted file mode 100644 index fa3c635bc92..00000000000 --- a/src/voice-broadcast/hooks/useVoiceBroadcastRecording.tsx +++ /dev/null @@ -1,96 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022, 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { Room, RoomMember } from "matrix-js-sdk/src/matrix"; -import React from "react"; - -import { - VoiceBroadcastInfoState, - VoiceBroadcastRecording, - VoiceBroadcastRecordingEvent, - VoiceBroadcastRecordingState, -} from ".."; -import QuestionDialog from "../../components/views/dialogs/QuestionDialog"; -import { useTypedEventEmitterState } from "../../hooks/useEventEmitter"; -import { _t } from "../../languageHandler"; -import { MatrixClientPeg } from "../../MatrixClientPeg"; -import Modal from "../../Modal"; - -const showStopBroadcastingDialog = async (): Promise => { - const { finished } = Modal.createDialog(QuestionDialog, { - title: _t("voice_broadcast|confirm_stop_title"), - description:

{_t("voice_broadcast|confirm_stop_description")}

, - button: _t("voice_broadcast|confirm_stop_affirm"), - }); - const [confirmed] = await finished; - return !!confirmed; -}; - -export const useVoiceBroadcastRecording = ( - recording: VoiceBroadcastRecording, -): { - live: boolean; - timeLeft: number; - recordingState: VoiceBroadcastRecordingState; - room: Room; - sender: RoomMember | null; - stopRecording(): void; - toggleRecording(): void; -} => { - const client = MatrixClientPeg.safeGet(); - const roomId = recording.infoEvent.getRoomId(); - const room = client.getRoom(roomId); - - if (!room) { - throw new Error("Unable to find voice broadcast room with Id: " + roomId); - } - - const sender = recording.infoEvent.sender; - - if (!sender) { - throw new Error(`Voice Broadcast sender not found (event ${recording.infoEvent.getId()})`); - } - - const stopRecording = async (): Promise => { - const confirmed = await showStopBroadcastingDialog(); - - if (confirmed) { - await recording.stop(); - } - }; - - const recordingState = useTypedEventEmitterState( - recording, - VoiceBroadcastRecordingEvent.StateChanged, - (state?: VoiceBroadcastRecordingState) => { - return state ?? recording.getState(); - }, - ); - - const timeLeft = useTypedEventEmitterState( - recording, - VoiceBroadcastRecordingEvent.TimeLeftChanged, - (t?: number) => { - return t ?? recording.getTimeLeft(); - }, - ); - - const live = ( - [VoiceBroadcastInfoState.Started, VoiceBroadcastInfoState.Resumed] as VoiceBroadcastRecordingState[] - ).includes(recordingState); - - return { - live, - timeLeft, - recordingState, - room, - sender, - stopRecording, - toggleRecording: recording.toggle, - }; -}; diff --git a/src/voice-broadcast/index.ts b/src/voice-broadcast/index.ts deleted file mode 100644 index 712c25fdc21..00000000000 --- a/src/voice-broadcast/index.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -/** - * Voice Broadcast module - * {@link https://github.com/vector-im/element-meta/discussions/632} - */ - -export * from "./types"; -export * from "./models/VoiceBroadcastPlayback"; -export * from "./models/VoiceBroadcastPreRecording"; -export * from "./models/VoiceBroadcastRecording"; -export * from "./audio/VoiceBroadcastRecorder"; -export * from "./components/VoiceBroadcastBody"; -export * from "./components/atoms/LiveBadge"; -export * from "./components/atoms/VoiceBroadcastControl"; -export * from "./components/atoms/VoiceBroadcastError"; -export * from "./components/atoms/VoiceBroadcastHeader"; -export * from "./components/atoms/VoiceBroadcastPlaybackControl"; -export * from "./components/atoms/VoiceBroadcastRecordingConnectionError"; -export * from "./components/atoms/VoiceBroadcastRoomSubtitle"; -export * from "./components/molecules/ConfirmListenBroadcastStopCurrent"; -export * from "./components/molecules/VoiceBroadcastPlaybackBody"; -export * from "./components/molecules/VoiceBroadcastSmallPlaybackBody"; -export * from "./components/molecules/VoiceBroadcastPreRecordingPip"; -export * from "./components/molecules/VoiceBroadcastRecordingBody"; -export * from "./components/molecules/VoiceBroadcastRecordingPip"; -export * from "./hooks/useCurrentVoiceBroadcastPreRecording"; -export * from "./hooks/useCurrentVoiceBroadcastRecording"; -export * from "./hooks/useHasRoomLiveVoiceBroadcast"; -export * from "./hooks/useVoiceBroadcastRecording"; -export * from "./stores/VoiceBroadcastPlaybacksStore"; -export * from "./stores/VoiceBroadcastPreRecordingStore"; -export * from "./stores/VoiceBroadcastRecordingsStore"; -export * from "./utils/checkVoiceBroadcastPreConditions"; -export * from "./utils/cleanUpBroadcasts"; -export * from "./utils/doClearCurrentVoiceBroadcastPlaybackIfStopped"; -export * from "./utils/doMaybeSetCurrentVoiceBroadcastPlayback"; -export * from "./utils/getChunkLength"; -export * from "./utils/getMaxBroadcastLength"; -export * from "./utils/hasRoomLiveVoiceBroadcast"; -export * from "./utils/isRelatedToVoiceBroadcast"; -export * from "./utils/isVoiceBroadcastStartedEvent"; -export * from "./utils/findRoomLiveVoiceBroadcastFromUserAndDevice"; -export * from "./utils/retrieveStartedInfoEvent"; -export * from "./utils/shouldDisplayAsVoiceBroadcastRecordingTile"; -export * from "./utils/shouldDisplayAsVoiceBroadcastTile"; -export * from "./utils/shouldDisplayAsVoiceBroadcastStoppedText"; -export * from "./utils/startNewVoiceBroadcastRecording"; -export * from "./utils/textForVoiceBroadcastStoppedEvent"; -export * from "./utils/textForVoiceBroadcastStoppedEventWithoutLink"; -export * from "./utils/VoiceBroadcastResumer"; diff --git a/src/voice-broadcast/models/VoiceBroadcastPlayback.ts b/src/voice-broadcast/models/VoiceBroadcastPlayback.ts deleted file mode 100644 index ce6215312fb..00000000000 --- a/src/voice-broadcast/models/VoiceBroadcastPlayback.ts +++ /dev/null @@ -1,651 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { - EventType, - MatrixClient, - MatrixEvent, - MatrixEventEvent, - MsgType, - RelationType, - TypedEventEmitter, -} from "matrix-js-sdk/src/matrix"; -import { SimpleObservable } from "matrix-widget-api"; -import { logger } from "matrix-js-sdk/src/logger"; -import { defer, IDeferred } from "matrix-js-sdk/src/utils"; - -import { Playback, PlaybackInterface, PlaybackState } from "../../audio/Playback"; -import { PlaybackManager } from "../../audio/PlaybackManager"; -import { UPDATE_EVENT } from "../../stores/AsyncStore"; -import { MediaEventHelper } from "../../utils/MediaEventHelper"; -import { IDestroyable } from "../../utils/IDestroyable"; -import { - VoiceBroadcastLiveness, - VoiceBroadcastInfoEventType, - VoiceBroadcastInfoState, - VoiceBroadcastInfoEventContent, - VoiceBroadcastRecordingsStore, - showConfirmListenBroadcastStopCurrentDialog, -} from ".."; -import { RelationsHelper, RelationsHelperEvent } from "../../events/RelationsHelper"; -import { VoiceBroadcastChunkEvents } from "../utils/VoiceBroadcastChunkEvents"; -import { determineVoiceBroadcastLiveness } from "../utils/determineVoiceBroadcastLiveness"; -import { _t } from "../../languageHandler"; - -export enum VoiceBroadcastPlaybackState { - Paused = "pause", - Playing = "playing", - Stopped = "stopped", - Buffering = "buffering", - Error = "error", -} - -export enum VoiceBroadcastPlaybackEvent { - TimesChanged = "times_changed", - LivenessChanged = "liveness_changed", - StateChanged = "state_changed", - InfoStateChanged = "info_state_changed", -} - -export type VoiceBroadcastPlaybackTimes = { - duration: number; - position: number; - timeLeft: number; -}; - -interface EventMap { - [VoiceBroadcastPlaybackEvent.TimesChanged]: (times: VoiceBroadcastPlaybackTimes) => void; - [VoiceBroadcastPlaybackEvent.LivenessChanged]: (liveness: VoiceBroadcastLiveness) => void; - [VoiceBroadcastPlaybackEvent.StateChanged]: ( - state: VoiceBroadcastPlaybackState, - playback: VoiceBroadcastPlayback, - ) => void; - [VoiceBroadcastPlaybackEvent.InfoStateChanged]: (state: VoiceBroadcastInfoState) => void; -} - -export class VoiceBroadcastPlayback - extends TypedEventEmitter - implements IDestroyable, PlaybackInterface -{ - private state = VoiceBroadcastPlaybackState.Stopped; - private chunkEvents = new VoiceBroadcastChunkEvents(); - /** @var Map: event Id → undecryptable event */ - private utdChunkEvents: Map = new Map(); - private playbacks = new Map(); - private currentlyPlaying: MatrixEvent | null = null; - /** @var total duration of all chunks in milliseconds */ - private duration = 0; - /** @var current playback position in milliseconds */ - private position = 0; - public readonly liveData = new SimpleObservable(); - private liveness: VoiceBroadcastLiveness = "not-live"; - - // set via addInfoEvent() in constructor - private infoState!: VoiceBroadcastInfoState; - private lastInfoEvent!: MatrixEvent; - - // set via setUpRelationsHelper() in constructor - private chunkRelationHelper!: RelationsHelper; - private infoRelationHelper!: RelationsHelper; - - private skipToNext?: number; - private skipToDeferred?: IDeferred; - - public constructor( - public readonly infoEvent: MatrixEvent, - private client: MatrixClient, - private recordings: VoiceBroadcastRecordingsStore, - ) { - super(); - this.addInfoEvent(this.infoEvent); - this.infoEvent.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction); - this.setUpRelationsHelper(); - } - - private async setUpRelationsHelper(): Promise { - this.infoRelationHelper = new RelationsHelper( - this.infoEvent, - RelationType.Reference, - VoiceBroadcastInfoEventType, - this.client, - ); - this.infoRelationHelper.getCurrent().forEach(this.addInfoEvent); - - if (this.infoState !== VoiceBroadcastInfoState.Stopped) { - // Only required if not stopped. Stopped is the final state. - this.infoRelationHelper.on(RelationsHelperEvent.Add, this.addInfoEvent); - - try { - await this.infoRelationHelper.emitFetchCurrent(); - } catch (err) { - logger.warn("error fetching server side relation for voice broadcast info", err); - // fall back to local events - this.infoRelationHelper.emitCurrent(); - } - } - - this.chunkRelationHelper = new RelationsHelper( - this.infoEvent, - RelationType.Reference, - EventType.RoomMessage, - this.client, - ); - this.chunkRelationHelper.on(RelationsHelperEvent.Add, this.addChunkEvent); - - try { - // TODO Michael W: only fetch events if needed, blocked by PSF-1708 - await this.chunkRelationHelper.emitFetchCurrent(); - } catch (err) { - logger.warn("error fetching server side relation for voice broadcast chunks", err); - // fall back to local events - this.chunkRelationHelper.emitCurrent(); - } - } - - private addChunkEvent = async (event: MatrixEvent): Promise => { - if (!event.getId() && !event.getTxnId()) { - // skip events without id and txn id - return false; - } - - if (event.isDecryptionFailure()) { - this.onChunkEventDecryptionFailure(event); - return false; - } - - if (event.getContent()?.msgtype !== MsgType.Audio) { - // skip non-audio event - return false; - } - - this.chunkEvents.addEvent(event); - this.setDuration(this.chunkEvents.getLength()); - - if (this.getState() === VoiceBroadcastPlaybackState.Buffering) { - await this.startOrPlayNext(); - } - - return true; - }; - - private onChunkEventDecryptionFailure = (event: MatrixEvent): void => { - const eventId = event.getId(); - - if (!eventId) { - // This should not happen, as the existence of the Id is checked before the call. - // Log anyway and return. - logger.warn("Broadcast chunk decryption failure for event without Id", { - broadcast: this.infoEvent.getId(), - }); - return; - } - - if (!this.utdChunkEvents.has(eventId)) { - event.once(MatrixEventEvent.Decrypted, this.onChunkEventDecrypted); - } - - this.utdChunkEvents.set(eventId, event); - this.setError(); - }; - - private onChunkEventDecrypted = async (event: MatrixEvent): Promise => { - const eventId = event.getId(); - - if (!eventId) { - // This should not happen, as the existence of the Id is checked before the call. - // Log anyway and return. - logger.warn("Broadcast chunk decrypted for event without Id", { broadcast: this.infoEvent.getId() }); - return; - } - - this.utdChunkEvents.delete(eventId); - await this.addChunkEvent(event); - - if (this.utdChunkEvents.size === 0) { - // no more UTD events, recover from error to paused - this.setState(VoiceBroadcastPlaybackState.Paused); - } - }; - - private startOrPlayNext = async (): Promise => { - if (this.currentlyPlaying) { - return this.playNext(); - } - - return await this.start(); - }; - - private addInfoEvent = (event: MatrixEvent): void => { - if (this.lastInfoEvent && this.lastInfoEvent.getTs() >= event.getTs()) { - // Only handle newer events - return; - } - - const state = event.getContent()?.state; - - if (!Object.values(VoiceBroadcastInfoState).includes(state)) { - // Do not handle unknown voice broadcast states - return; - } - - this.lastInfoEvent = event; - this.setInfoState(state); - }; - - private onBeforeRedaction = (): void => { - if (this.getState() !== VoiceBroadcastPlaybackState.Stopped) { - this.stop(); - // destroy cleans up everything - this.destroy(); - } - }; - - private async tryLoadPlayback(chunkEvent: MatrixEvent): Promise { - try { - return await this.loadPlayback(chunkEvent); - } catch (err: any) { - logger.warn("Unable to load broadcast playback", { - message: err.message, - broadcastId: this.infoEvent.getId(), - chunkId: chunkEvent.getId(), - }); - this.setError(); - } - } - - private async loadPlayback(chunkEvent: MatrixEvent): Promise { - const eventId = chunkEvent.getId(); - - if (!eventId) { - throw new Error("Broadcast chunk event without Id occurred"); - } - - const helper = new MediaEventHelper(chunkEvent); - const blob = await helper.sourceBlob.value; - const buffer = await blob.arrayBuffer(); - const playback = PlaybackManager.instance.createPlaybackInstance(buffer); - await playback.prepare(); - playback.clockInfo.populatePlaceholdersFrom(chunkEvent); - this.playbacks.set(eventId, playback); - playback.on(UPDATE_EVENT, (state) => this.onPlaybackStateChange(chunkEvent, state)); - playback.clockInfo.liveData.onUpdate(([position]) => { - this.onPlaybackPositionUpdate(chunkEvent, position); - }); - } - - private unloadPlayback(event: MatrixEvent): void { - const playback = this.playbacks.get(event.getId()!); - if (!playback) return; - - playback.destroy(); - this.playbacks.delete(event.getId()!); - } - - private onPlaybackPositionUpdate = (event: MatrixEvent, position: number): void => { - if (event !== this.currentlyPlaying) return; - - const newPosition = this.chunkEvents.getLengthTo(event) + position * 1000; // observable sends seconds - - // do not jump backwards - this can happen when transiting from one to another chunk - if (newPosition < this.position) return; - - this.setPosition(newPosition); - }; - - private setDuration(duration: number): void { - if (this.duration === duration) return; - - this.duration = duration; - this.emitTimesChanged(); - this.liveData.update([this.timeSeconds, this.durationSeconds]); - } - - private setPosition(position: number): void { - if (this.position === position) return; - - this.position = position; - this.emitTimesChanged(); - this.liveData.update([this.timeSeconds, this.durationSeconds]); - } - - private emitTimesChanged(): void { - this.emit(VoiceBroadcastPlaybackEvent.TimesChanged, { - duration: this.durationSeconds, - position: this.timeSeconds, - timeLeft: this.timeLeftSeconds, - }); - } - - private onPlaybackStateChange = async (event: MatrixEvent, newState: PlaybackState): Promise => { - if (event !== this.currentlyPlaying) return; - if (newState !== PlaybackState.Stopped) return; - - await this.playNext(); - this.unloadPlayback(event); - }; - - private async playNext(): Promise { - if (!this.currentlyPlaying) return; - - const next = this.chunkEvents.getNext(this.currentlyPlaying); - - if (next) { - return this.playEvent(next); - } - - if ( - this.getInfoState() === VoiceBroadcastInfoState.Stopped && - this.chunkEvents.getSequenceForEvent(this.currentlyPlaying) === this.lastChunkSequence - ) { - this.stop(); - } else { - // No more chunks available, although the broadcast is not finished → enter buffering state. - this.setState(VoiceBroadcastPlaybackState.Buffering); - } - } - - /** - * @returns {number} The last chunk sequence from the latest info event. - * Falls back to the length of received chunks if the info event does not provide the number. - */ - private get lastChunkSequence(): number { - return ( - this.lastInfoEvent.getContent()?.last_chunk_sequence || - this.chunkEvents.getNumberOfEvents() - ); - } - - private async playEvent(event: MatrixEvent): Promise { - this.setState(VoiceBroadcastPlaybackState.Playing); - this.currentlyPlaying = event; - const playback = await this.tryGetOrLoadPlaybackForEvent(event); - playback?.play(); - } - - private async tryGetOrLoadPlaybackForEvent(event: MatrixEvent): Promise { - try { - return await this.getOrLoadPlaybackForEvent(event); - } catch (err: any) { - logger.warn("Unable to load broadcast playback", { - message: err.message, - broadcastId: this.infoEvent.getId(), - chunkId: event.getId(), - }); - this.setError(); - } - } - - private async getOrLoadPlaybackForEvent(event: MatrixEvent): Promise { - const eventId = event.getId(); - - if (!eventId) { - throw new Error("Broadcast chunk event without Id occurred"); - } - - if (!this.playbacks.has(eventId)) { - // set to buffering while loading the chunk data - const currentState = this.getState(); - this.setState(VoiceBroadcastPlaybackState.Buffering); - await this.loadPlayback(event); - this.setState(currentState); - } - - const playback = this.playbacks.get(eventId); - - if (!playback) { - throw new Error(`Unable to find playback for event ${event.getId()}`); - } - - // try to load the playback for the next event for a smooth(er) playback - const nextEvent = this.chunkEvents.getNext(event); - if (nextEvent) this.tryLoadPlayback(nextEvent); - - return playback; - } - - private getCurrentPlayback(): Playback | undefined { - if (!this.currentlyPlaying) return; - return this.playbacks.get(this.currentlyPlaying.getId()!); - } - - public getLiveness(): VoiceBroadcastLiveness { - return this.liveness; - } - - private setLiveness(liveness: VoiceBroadcastLiveness): void { - if (this.liveness === liveness) return; - - this.liveness = liveness; - this.emit(VoiceBroadcastPlaybackEvent.LivenessChanged, liveness); - } - - public get currentState(): PlaybackState { - return PlaybackState.Playing; - } - - public get timeSeconds(): number { - return this.position / 1000; - } - - public get durationSeconds(): number { - return this.duration / 1000; - } - - public get timeLeftSeconds(): number { - // Sometimes the meta data and the audio files are a little bit out of sync. - // Be sure it never returns a negative value. - return Math.max(0, Math.round(this.durationSeconds) - this.timeSeconds); - } - - public async skipTo(timeSeconds: number): Promise { - this.skipToNext = timeSeconds; - - if (this.skipToDeferred) { - // Skip to position is already in progress. Return the promise for that. - return this.skipToDeferred.promise; - } - - this.skipToDeferred = defer(); - - while (this.skipToNext !== undefined) { - // Skip to position until skipToNext is undefined. - // skipToNext can be set if skipTo is called while already skipping. - const skipToNext = this.skipToNext; - this.skipToNext = undefined; - await this.doSkipTo(skipToNext); - } - - this.skipToDeferred.resolve(); - this.skipToDeferred = undefined; - } - - private async doSkipTo(timeSeconds: number): Promise { - const time = timeSeconds * 1000; - const event = this.chunkEvents.findByTime(time); - - if (!event) { - logger.warn("voice broadcast chunk event to skip to not found"); - return; - } - - const currentPlayback = this.getCurrentPlayback(); - const skipToPlayback = await this.tryGetOrLoadPlaybackForEvent(event); - const currentPlaybackEvent = this.currentlyPlaying; - - if (!skipToPlayback) { - logger.warn("voice broadcast chunk to skip to not found", event); - return; - } - - this.currentlyPlaying = event; - - if (currentPlayback && currentPlaybackEvent && currentPlayback !== skipToPlayback) { - // only stop and unload the playback here without triggering other effects, e.g. play next - currentPlayback.off(UPDATE_EVENT, this.onPlaybackStateChange); - await currentPlayback.stop(); - currentPlayback.on(UPDATE_EVENT, this.onPlaybackStateChange); - this.unloadPlayback(currentPlaybackEvent); - } - - const offsetInChunk = time - this.chunkEvents.getLengthTo(event); - await skipToPlayback.skipTo(offsetInChunk / 1000); - - if (this.state === VoiceBroadcastPlaybackState.Playing && !skipToPlayback.isPlaying) { - await skipToPlayback.play(); - } - - this.setPosition(time); - } - - public async start(): Promise { - if (this.state === VoiceBroadcastPlaybackState.Playing) return; - - const currentRecording = this.recordings.getCurrent(); - - if (currentRecording && currentRecording.getState() !== VoiceBroadcastInfoState.Stopped) { - const shouldStopRecording = await showConfirmListenBroadcastStopCurrentDialog(); - - if (!shouldStopRecording) { - // keep recording - return; - } - - await this.recordings.getCurrent()?.stop(); - } - - const chunkEvents = this.chunkEvents.getEvents(); - - const toPlay = - this.getInfoState() === VoiceBroadcastInfoState.Stopped - ? chunkEvents[0] // start at the beginning for an ended voice broadcast - : chunkEvents[chunkEvents.length - 1]; // start at the current chunk for an ongoing voice broadcast - - if (toPlay) { - return this.playEvent(toPlay); - } - - this.setState(VoiceBroadcastPlaybackState.Buffering); - } - - public stop(): void { - // error is a final state - if (this.getState() === VoiceBroadcastPlaybackState.Error) return; - - this.setState(VoiceBroadcastPlaybackState.Stopped); - this.getCurrentPlayback()?.stop(); - this.currentlyPlaying = null; - this.setPosition(0); - } - - public pause(): void { - // error is a final state - if (this.getState() === VoiceBroadcastPlaybackState.Error) return; - - // stopped voice broadcasts cannot be paused - if (this.getState() === VoiceBroadcastPlaybackState.Stopped) return; - - this.setState(VoiceBroadcastPlaybackState.Paused); - this.getCurrentPlayback()?.pause(); - } - - public resume(): void { - // error is a final state - if (this.getState() === VoiceBroadcastPlaybackState.Error) return; - - if (!this.currentlyPlaying) { - // no playback to resume, start from the beginning - this.start(); - return; - } - - this.setState(VoiceBroadcastPlaybackState.Playing); - this.getCurrentPlayback()?.play(); - } - - /** - * Toggles the playback: - * stopped → playing - * playing → paused - * paused → playing - */ - public async toggle(): Promise { - // error is a final state - if (this.getState() === VoiceBroadcastPlaybackState.Error) return; - - if (this.state === VoiceBroadcastPlaybackState.Stopped) { - await this.start(); - return; - } - - if (this.state === VoiceBroadcastPlaybackState.Paused) { - this.resume(); - return; - } - - this.pause(); - } - - public getState(): VoiceBroadcastPlaybackState { - return this.state; - } - - private setState(state: VoiceBroadcastPlaybackState): void { - if (this.state === state) { - return; - } - - this.state = state; - this.emit(VoiceBroadcastPlaybackEvent.StateChanged, state, this); - } - - /** - * Set error state. Stop current playback, if any. - */ - private setError(): void { - this.setState(VoiceBroadcastPlaybackState.Error); - this.getCurrentPlayback()?.stop(); - this.currentlyPlaying = null; - this.setPosition(0); - } - - public getInfoState(): VoiceBroadcastInfoState { - return this.infoState; - } - - private setInfoState(state: VoiceBroadcastInfoState): void { - if (this.infoState === state) { - return; - } - - this.infoState = state; - this.emit(VoiceBroadcastPlaybackEvent.InfoStateChanged, state); - this.setLiveness(determineVoiceBroadcastLiveness(this.infoState)); - } - - public get errorMessage(): string { - if (this.getState() !== VoiceBroadcastPlaybackState.Error) return ""; - if (this.utdChunkEvents.size) return _t("voice_broadcast|failed_decrypt"); - return _t("voice_broadcast|failed_generic"); - } - - public destroy(): void { - for (const [, utdEvent] of this.utdChunkEvents) { - utdEvent.off(MatrixEventEvent.Decrypted, this.onChunkEventDecrypted); - } - - this.utdChunkEvents.clear(); - - this.chunkRelationHelper.destroy(); - this.infoRelationHelper.destroy(); - this.removeAllListeners(); - - this.chunkEvents = new VoiceBroadcastChunkEvents(); - this.playbacks.forEach((p) => p.destroy()); - this.playbacks = new Map(); - } -} diff --git a/src/voice-broadcast/models/VoiceBroadcastPreRecording.ts b/src/voice-broadcast/models/VoiceBroadcastPreRecording.ts deleted file mode 100644 index 0cf47c6f214..00000000000 --- a/src/voice-broadcast/models/VoiceBroadcastPreRecording.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixClient, Room, RoomMember, TypedEventEmitter } from "matrix-js-sdk/src/matrix"; - -import { IDestroyable } from "../../utils/IDestroyable"; -import { VoiceBroadcastPlaybacksStore } from "../stores/VoiceBroadcastPlaybacksStore"; -import { VoiceBroadcastRecordingsStore } from "../stores/VoiceBroadcastRecordingsStore"; -import { startNewVoiceBroadcastRecording } from "../utils/startNewVoiceBroadcastRecording"; - -type VoiceBroadcastPreRecordingEvent = "dismiss"; - -interface EventMap { - dismiss: (voiceBroadcastPreRecording: VoiceBroadcastPreRecording) => void; -} - -export class VoiceBroadcastPreRecording - extends TypedEventEmitter - implements IDestroyable -{ - public constructor( - public room: Room, - public sender: RoomMember, - private client: MatrixClient, - private playbacksStore: VoiceBroadcastPlaybacksStore, - private recordingsStore: VoiceBroadcastRecordingsStore, - ) { - super(); - } - - public start = async (): Promise => { - await startNewVoiceBroadcastRecording(this.room, this.client, this.playbacksStore, this.recordingsStore); - this.emit("dismiss", this); - }; - - public cancel = (): void => { - this.emit("dismiss", this); - }; - - public destroy(): void { - this.removeAllListeners(); - } -} diff --git a/src/voice-broadcast/models/VoiceBroadcastRecording.ts b/src/voice-broadcast/models/VoiceBroadcastRecording.ts deleted file mode 100644 index ebf8ee697f8..00000000000 --- a/src/voice-broadcast/models/VoiceBroadcastRecording.ts +++ /dev/null @@ -1,441 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { logger } from "matrix-js-sdk/src/logger"; -import { - ClientEvent, - ClientEventHandlerMap, - EventType, - MatrixClient, - MatrixEvent, - MatrixEventEvent, - MsgType, - RelationType, - TypedEventEmitter, -} from "matrix-js-sdk/src/matrix"; -import { AudioContent, EncryptedFile } from "matrix-js-sdk/src/types"; - -import { - ChunkRecordedPayload, - createVoiceBroadcastRecorder, - getMaxBroadcastLength, - VoiceBroadcastInfoEventContent, - VoiceBroadcastInfoEventType, - VoiceBroadcastInfoState, - VoiceBroadcastRecorder, - VoiceBroadcastRecorderEvent, -} from ".."; -import { uploadFile } from "../../ContentMessages"; -import { createVoiceMessageContent } from "../../utils/createVoiceMessageContent"; -import { IDestroyable } from "../../utils/IDestroyable"; -import dis from "../../dispatcher/dispatcher"; -import { ActionPayload } from "../../dispatcher/payloads"; -import { VoiceBroadcastChunkEvents } from "../utils/VoiceBroadcastChunkEvents"; -import { RelationsHelper, RelationsHelperEvent } from "../../events/RelationsHelper"; -import { createReconnectedListener } from "../../utils/connection"; -import { localNotificationsAreSilenced } from "../../utils/notifications"; -import { BackgroundAudio } from "../../audio/BackgroundAudio"; - -export enum VoiceBroadcastRecordingEvent { - StateChanged = "liveness_changed", - TimeLeftChanged = "time_left_changed", -} - -export type VoiceBroadcastRecordingState = VoiceBroadcastInfoState | "connection_error"; - -interface EventMap { - [VoiceBroadcastRecordingEvent.StateChanged]: (state: VoiceBroadcastRecordingState) => void; - [VoiceBroadcastRecordingEvent.TimeLeftChanged]: (timeLeft: number) => void; -} - -export class VoiceBroadcastRecording - extends TypedEventEmitter - implements IDestroyable -{ - private state: VoiceBroadcastRecordingState; - private recorder: VoiceBroadcastRecorder | null = null; - private dispatcherRef: string; - private chunkEvents = new VoiceBroadcastChunkEvents(); - private chunkRelationHelper: RelationsHelper; - private maxLength: number; - private timeLeft: number; - private toRetry: Array<() => Promise> = []; - private reconnectedListener: ClientEventHandlerMap[ClientEvent.Sync]; - private roomId: string; - private infoEventId: string; - private backgroundAudio = new BackgroundAudio(); - - /** - * Broadcast chunks have a sequence number to bring them in the correct order and to know if a message is missing. - * This variable holds the last sequence number. - * Starts with 0 because there is no chunk at the beginning of a broadcast. - * Will be incremented when a chunk message is created. - */ - private sequence = 0; - - public constructor( - public readonly infoEvent: MatrixEvent, - private client: MatrixClient, - initialState?: VoiceBroadcastInfoState, - ) { - super(); - this.maxLength = getMaxBroadcastLength(); - this.timeLeft = this.maxLength; - this.infoEventId = this.determineEventIdFromInfoEvent(); - this.roomId = this.determineRoomIdFromInfoEvent(); - - if (initialState) { - this.state = initialState; - } else { - this.state = this.determineInitialStateFromInfoEvent(); - } - - // TODO Michael W: listen for state updates - - this.infoEvent.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction); - this.dispatcherRef = dis.register(this.onAction); - this.chunkRelationHelper = this.initialiseChunkEventRelation(); - this.reconnectedListener = createReconnectedListener(this.onReconnect); - this.client.on(ClientEvent.Sync, this.reconnectedListener); - } - - private initialiseChunkEventRelation(): RelationsHelper { - const relationsHelper = new RelationsHelper( - this.infoEvent, - RelationType.Reference, - EventType.RoomMessage, - this.client, - ); - relationsHelper.on(RelationsHelperEvent.Add, this.onChunkEvent); - - relationsHelper.emitFetchCurrent().catch((err) => { - logger.warn("error fetching server side relation for voice broadcast chunks", err); - // fall back to local events - relationsHelper.emitCurrent(); - }); - - return relationsHelper; - } - - private onChunkEvent = (event: MatrixEvent): void => { - if ( - (!event.getId() && !event.getTxnId()) || - event.getContent()?.msgtype !== MsgType.Audio // don't add non-audio event - ) { - return; - } - - this.chunkEvents.addEvent(event); - }; - - private determineEventIdFromInfoEvent(): string { - const infoEventId = this.infoEvent.getId(); - - if (!infoEventId) { - throw new Error("Cannot create broadcast for info event without Id."); - } - - return infoEventId; - } - - private determineRoomIdFromInfoEvent(): string { - const roomId = this.infoEvent.getRoomId(); - - if (!roomId) { - throw new Error(`Cannot create broadcast for unknown room (info event ${this.infoEventId})`); - } - - return roomId; - } - - /** - * Determines the initial broadcast state. - * Checks all related events. If one has the "stopped" state → stopped, else started. - */ - private determineInitialStateFromInfoEvent(): VoiceBroadcastRecordingState { - const room = this.client.getRoom(this.roomId); - const relations = room - ?.getUnfilteredTimelineSet() - ?.relations?.getChildEventsForEvent(this.infoEventId, RelationType.Reference, VoiceBroadcastInfoEventType); - const relatedEvents = relations?.getRelations(); - return !relatedEvents?.find((event: MatrixEvent) => { - return event.getContent()?.state === VoiceBroadcastInfoState.Stopped; - }) - ? VoiceBroadcastInfoState.Started - : VoiceBroadcastInfoState.Stopped; - } - - public getTimeLeft(): number { - return this.timeLeft; - } - - /** - * Retries failed actions on reconnect. - */ - private onReconnect = async (): Promise => { - // Do nothing if not in connection_error state. - if (this.state !== "connection_error") return; - - // Copy the array, so that it is possible to remove elements from it while iterating over the original. - const toRetryCopy = [...this.toRetry]; - - for (const retryFn of this.toRetry) { - try { - await retryFn(); - // Successfully retried. Remove from array copy. - toRetryCopy.splice(toRetryCopy.indexOf(retryFn), 1); - } catch { - // The current retry callback failed. Stop the loop. - break; - } - } - - this.toRetry = toRetryCopy; - - if (this.toRetry.length === 0) { - // Everything has been successfully retried. Recover from error state to paused. - await this.pause(); - } - }; - - private async setTimeLeft(timeLeft: number): Promise { - if (timeLeft <= 0) { - // time is up - stop the recording - return await this.stop(); - } - - // do never increase time left; no action if equals - if (timeLeft >= this.timeLeft) return; - - this.timeLeft = timeLeft; - this.emit(VoiceBroadcastRecordingEvent.TimeLeftChanged, timeLeft); - } - - public async start(): Promise { - return this.getRecorder().start(); - } - - public async stop(): Promise { - if (this.state === VoiceBroadcastInfoState.Stopped) return; - - this.setState(VoiceBroadcastInfoState.Stopped); - await this.stopRecorder(); - await this.sendInfoStateEvent(VoiceBroadcastInfoState.Stopped); - } - - public async pause(): Promise { - // stopped or already paused recordings cannot be paused - if ( - ( - [VoiceBroadcastInfoState.Stopped, VoiceBroadcastInfoState.Paused] as VoiceBroadcastRecordingState[] - ).includes(this.state) - ) - return; - - this.setState(VoiceBroadcastInfoState.Paused); - await this.stopRecorder(); - await this.sendInfoStateEvent(VoiceBroadcastInfoState.Paused); - } - - public async resume(): Promise { - if (this.state !== VoiceBroadcastInfoState.Paused) return; - - this.setState(VoiceBroadcastInfoState.Resumed); - await this.getRecorder().start(); - await this.sendInfoStateEvent(VoiceBroadcastInfoState.Resumed); - } - - public toggle = async (): Promise => { - if (this.getState() === VoiceBroadcastInfoState.Paused) return this.resume(); - - if ( - ( - [VoiceBroadcastInfoState.Started, VoiceBroadcastInfoState.Resumed] as VoiceBroadcastRecordingState[] - ).includes(this.getState()) - ) { - return this.pause(); - } - }; - - public getState(): VoiceBroadcastRecordingState { - return this.state; - } - - private getRecorder(): VoiceBroadcastRecorder { - if (!this.recorder) { - this.recorder = createVoiceBroadcastRecorder(); - this.recorder.on(VoiceBroadcastRecorderEvent.ChunkRecorded, this.onChunkRecorded); - this.recorder.on(VoiceBroadcastRecorderEvent.CurrentChunkLengthUpdated, this.onCurrentChunkLengthUpdated); - } - - return this.recorder; - } - - public async destroy(): Promise { - if (this.recorder) { - this.recorder.stop(); - this.recorder.destroy(); - } - - this.infoEvent.off(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction); - this.removeAllListeners(); - dis.unregister(this.dispatcherRef); - this.chunkEvents = new VoiceBroadcastChunkEvents(); - this.chunkRelationHelper.destroy(); - this.client.off(ClientEvent.Sync, this.reconnectedListener); - } - - private onBeforeRedaction = (): void => { - if (this.getState() !== VoiceBroadcastInfoState.Stopped) { - this.setState(VoiceBroadcastInfoState.Stopped); - // destroy cleans up everything - this.destroy(); - } - }; - - private onAction = (payload: ActionPayload): void => { - if (payload.action !== "call_state") return; - - // pause on any call action - this.pause(); - }; - - private setState(state: VoiceBroadcastRecordingState): void { - this.state = state; - this.emit(VoiceBroadcastRecordingEvent.StateChanged, this.state); - } - - private onCurrentChunkLengthUpdated = (currentChunkLength: number): void => { - this.setTimeLeft(this.maxLength - this.chunkEvents.getLengthSeconds() - currentChunkLength); - }; - - private onChunkRecorded = async (chunk: ChunkRecordedPayload): Promise => { - const uploadAndSendFn = async (): Promise => { - const { url, file } = await this.uploadFile(chunk); - await this.sendVoiceMessage(chunk, url, file); - }; - - await this.callWithRetry(uploadAndSendFn); - }; - - /** - * This function is called on connection errors. - * It sets the connection error state and stops the recorder. - */ - private async onConnectionError(): Promise { - this.playConnectionErrorAudioNotification().catch(() => { - // Error logged in playConnectionErrorAudioNotification(). - }); - await this.stopRecorder(false); - this.setState("connection_error"); - } - - private async playConnectionErrorAudioNotification(): Promise { - if (localNotificationsAreSilenced(this.client)) { - return; - } - - await this.backgroundAudio.pickFormatAndPlay("./media/error", ["mp3", "ogg"]); - } - - private async uploadFile(chunk: ChunkRecordedPayload): ReturnType { - return uploadFile( - this.client, - this.roomId, - new Blob([chunk.buffer], { - type: this.getRecorder().contentType, - }), - ); - } - - private async sendVoiceMessage(chunk: ChunkRecordedPayload, url?: string, file?: EncryptedFile): Promise { - /** - * Increment the last sequence number and use it for this message. - * Done outside of the sendMessageFn to get a scoped value. - * Also see {@link VoiceBroadcastRecording.sequence}. - */ - const sequence = ++this.sequence; - - const sendMessageFn = async (): Promise => { - const content = createVoiceMessageContent( - url, - this.getRecorder().contentType, - Math.round(chunk.length * 1000), - chunk.buffer.length, - file, - ); - content["m.relates_to"] = { - rel_type: RelationType.Reference, - event_id: this.infoEventId, - }; - (content)["io.element.voice_broadcast_chunk"] = { - sequence, - }; - - await this.client.sendMessage(this.roomId, content); - }; - - await this.callWithRetry(sendMessageFn); - } - - /** - * Sends an info state event with given state. - * On error stores a resend function and setState(state) in {@link toRetry} and - * sets the broadcast state to connection_error. - */ - private async sendInfoStateEvent(state: VoiceBroadcastInfoState): Promise { - const sendEventFn = async (): Promise => { - await this.client.sendStateEvent( - this.roomId, - VoiceBroadcastInfoEventType, - { - device_id: this.client.getDeviceId(), - state, - last_chunk_sequence: this.sequence, - ["m.relates_to"]: { - rel_type: RelationType.Reference, - event_id: this.infoEventId, - }, - } as VoiceBroadcastInfoEventContent, - this.client.getSafeUserId(), - ); - }; - - await this.callWithRetry(sendEventFn); - } - - /** - * Calls the function. - * On failure adds it to the retry list and triggers connection error. - * {@link toRetry} - * {@link onConnectionError} - */ - private async callWithRetry(retryAbleFn: () => Promise): Promise { - try { - await retryAbleFn(); - } catch { - this.toRetry.push(retryAbleFn); - this.onConnectionError(); - } - } - - private async stopRecorder(emit = true): Promise { - if (!this.recorder) { - return; - } - - try { - const lastChunk = await this.recorder.stop(); - if (lastChunk && emit) { - await this.onChunkRecorded(lastChunk); - } - } catch (err) { - logger.warn("error stopping voice broadcast recorder", err); - } - } -} diff --git a/src/voice-broadcast/stores/VoiceBroadcastPlaybacksStore.ts b/src/voice-broadcast/stores/VoiceBroadcastPlaybacksStore.ts deleted file mode 100644 index 69b8c21d903..00000000000 --- a/src/voice-broadcast/stores/VoiceBroadcastPlaybacksStore.ts +++ /dev/null @@ -1,113 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixClient, MatrixEvent, TypedEventEmitter } from "matrix-js-sdk/src/matrix"; - -import { - VoiceBroadcastPlayback, - VoiceBroadcastPlaybackEvent, - VoiceBroadcastPlaybackState, - VoiceBroadcastRecordingsStore, -} from ".."; -import { IDestroyable } from "../../utils/IDestroyable"; - -export enum VoiceBroadcastPlaybacksStoreEvent { - CurrentChanged = "current_changed", -} - -interface EventMap { - [VoiceBroadcastPlaybacksStoreEvent.CurrentChanged]: (recording: VoiceBroadcastPlayback | null) => void; -} - -/** - * This store manages VoiceBroadcastPlaybacks: - * - access the currently playing voice broadcast - * - ensures that only once broadcast is playing at a time - */ -export class VoiceBroadcastPlaybacksStore - extends TypedEventEmitter - implements IDestroyable -{ - private current: VoiceBroadcastPlayback | null = null; - - /** Playbacks indexed by their info event id. */ - private playbacks = new Map(); - - public constructor(private recordings: VoiceBroadcastRecordingsStore) { - super(); - } - - public setCurrent(current: VoiceBroadcastPlayback): void { - if (this.current === current) return; - - this.current = current; - this.addPlayback(current); - this.emit(VoiceBroadcastPlaybacksStoreEvent.CurrentChanged, current); - } - - public clearCurrent(): void { - if (this.current === null) return; - - this.current = null; - this.emit(VoiceBroadcastPlaybacksStoreEvent.CurrentChanged, null); - } - - public getCurrent(): VoiceBroadcastPlayback | null { - return this.current; - } - - public getByInfoEvent(infoEvent: MatrixEvent, client: MatrixClient): VoiceBroadcastPlayback { - const infoEventId = infoEvent.getId()!; - - if (!this.playbacks.has(infoEventId)) { - this.addPlayback(new VoiceBroadcastPlayback(infoEvent, client, this.recordings)); - } - - return this.playbacks.get(infoEventId)!; - } - - private addPlayback(playback: VoiceBroadcastPlayback): void { - const infoEventId = playback.infoEvent.getId()!; - - if (this.playbacks.has(infoEventId)) return; - - this.playbacks.set(infoEventId, playback); - playback.on(VoiceBroadcastPlaybackEvent.StateChanged, this.onPlaybackStateChanged); - } - - private onPlaybackStateChanged = (state: VoiceBroadcastPlaybackState, playback: VoiceBroadcastPlayback): void => { - switch (state) { - case VoiceBroadcastPlaybackState.Buffering: - case VoiceBroadcastPlaybackState.Playing: - this.pauseExcept(playback); - this.setCurrent(playback); - break; - case VoiceBroadcastPlaybackState.Stopped: - this.clearCurrent(); - break; - } - }; - - private pauseExcept(playbackNotToPause: VoiceBroadcastPlayback): void { - for (const playback of this.playbacks.values()) { - if (playback !== playbackNotToPause) { - playback.pause(); - } - } - } - - public destroy(): void { - this.removeAllListeners(); - - for (const playback of this.playbacks.values()) { - playback.off(VoiceBroadcastPlaybackEvent.StateChanged, this.onPlaybackStateChanged); - } - - this.playbacks = new Map(); - } -} diff --git a/src/voice-broadcast/stores/VoiceBroadcastPreRecordingStore.ts b/src/voice-broadcast/stores/VoiceBroadcastPreRecordingStore.ts deleted file mode 100644 index 3552930687b..00000000000 --- a/src/voice-broadcast/stores/VoiceBroadcastPreRecordingStore.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { TypedEventEmitter } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastPreRecording } from ".."; -import { IDestroyable } from "../../utils/IDestroyable"; - -export type VoiceBroadcastPreRecordingEvent = "changed"; - -interface EventMap { - changed: (preRecording: VoiceBroadcastPreRecording | null) => void; -} - -export class VoiceBroadcastPreRecordingStore - extends TypedEventEmitter - implements IDestroyable -{ - private current: VoiceBroadcastPreRecording | null = null; - - public setCurrent(current: VoiceBroadcastPreRecording): void { - if (this.current === current) return; - - if (this.current) { - this.current.off("dismiss", this.onCancel); - } - - this.current = current; - current.on("dismiss", this.onCancel); - this.emit("changed", current); - } - - public clearCurrent(): void { - if (this.current === null) return; - - this.current.off("dismiss", this.onCancel); - this.current = null; - this.emit("changed", null); - } - - public getCurrent(): VoiceBroadcastPreRecording | null { - return this.current; - } - - public destroy(): void { - this.removeAllListeners(); - - if (this.current) { - this.current.off("dismiss", this.onCancel); - } - } - - private onCancel = (voiceBroadcastPreRecording: VoiceBroadcastPreRecording): void => { - if (this.current === voiceBroadcastPreRecording) { - this.clearCurrent(); - } - }; -} diff --git a/src/voice-broadcast/stores/VoiceBroadcastRecordingsStore.ts b/src/voice-broadcast/stores/VoiceBroadcastRecordingsStore.ts deleted file mode 100644 index ff0f67b910e..00000000000 --- a/src/voice-broadcast/stores/VoiceBroadcastRecordingsStore.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022, 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixClient, MatrixEvent, TypedEventEmitter } from "matrix-js-sdk/src/matrix"; - -import { - VoiceBroadcastInfoState, - VoiceBroadcastRecording, - VoiceBroadcastRecordingEvent, - VoiceBroadcastRecordingState, -} from ".."; - -export enum VoiceBroadcastRecordingsStoreEvent { - CurrentChanged = "current_changed", -} - -interface EventMap { - [VoiceBroadcastRecordingsStoreEvent.CurrentChanged]: (recording: VoiceBroadcastRecording | null) => void; -} - -/** - * This store provides access to the current and specific Voice Broadcast recordings. - */ -export class VoiceBroadcastRecordingsStore extends TypedEventEmitter { - private current: VoiceBroadcastRecording | null = null; - private recordings = new Map(); - - public constructor() { - super(); - } - - public setCurrent(current: VoiceBroadcastRecording): void { - if (this.current === current) return; - - const infoEventId = current.infoEvent.getId(); - - if (!infoEventId) { - throw new Error("Got broadcast info event without Id"); - } - - if (this.current) { - this.current.off(VoiceBroadcastRecordingEvent.StateChanged, this.onCurrentStateChanged); - } - - this.current = current; - this.current.on(VoiceBroadcastRecordingEvent.StateChanged, this.onCurrentStateChanged); - this.recordings.set(infoEventId, current); - this.emit(VoiceBroadcastRecordingsStoreEvent.CurrentChanged, current); - } - - public getCurrent(): VoiceBroadcastRecording | null { - return this.current; - } - - public hasCurrent(): boolean { - return this.current !== null; - } - - public clearCurrent(): void { - if (!this.current) return; - - this.current.off(VoiceBroadcastRecordingEvent.StateChanged, this.onCurrentStateChanged); - this.current = null; - this.emit(VoiceBroadcastRecordingsStoreEvent.CurrentChanged, null); - } - - public getByInfoEvent(infoEvent: MatrixEvent, client: MatrixClient): VoiceBroadcastRecording { - const infoEventId = infoEvent.getId(); - - if (!infoEventId) { - throw new Error("Got broadcast info event without Id"); - } - - const recording = this.recordings.get(infoEventId) || new VoiceBroadcastRecording(infoEvent, client); - this.recordings.set(infoEventId, recording); - return recording; - } - - private onCurrentStateChanged = (state: VoiceBroadcastRecordingState): void => { - if (state === VoiceBroadcastInfoState.Stopped) { - this.clearCurrent(); - } - }; -} diff --git a/src/voice-broadcast/types.ts b/src/voice-broadcast/types.ts deleted file mode 100644 index 8191a0be162..00000000000 --- a/src/voice-broadcast/types.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { RelationType } from "matrix-js-sdk/src/matrix"; - -export const VoiceBroadcastInfoEventType = "io.element.voice_broadcast_info"; -export const VoiceBroadcastChunkEventType = "io.element.voice_broadcast_chunk"; - -export type VoiceBroadcastLiveness = "live" | "not-live" | "grey"; - -export enum VoiceBroadcastInfoState { - Started = "started", - Paused = "paused", - Resumed = "resumed", - Stopped = "stopped", -} - -export interface VoiceBroadcastInfoEventContent { - device_id: string; - state: VoiceBroadcastInfoState; - chunk_length?: number; - last_chunk_sequence?: number; - ["m.relates_to"]?: { - rel_type: RelationType; - event_id: string; - }; -} diff --git a/src/voice-broadcast/utils/VoiceBroadcastChunkEvents.ts b/src/voice-broadcast/utils/VoiceBroadcastChunkEvents.ts deleted file mode 100644 index 039749cf8d0..00000000000 --- a/src/voice-broadcast/utils/VoiceBroadcastChunkEvents.ts +++ /dev/null @@ -1,147 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixEvent } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastChunkEventType } from ".."; - -/** - * Voice broadcast chunk collection. - * Orders chunks by sequence (if available) or timestamp. - */ -export class VoiceBroadcastChunkEvents { - private events: MatrixEvent[] = []; - - public getEvents(): MatrixEvent[] { - return [...this.events]; - } - - public getNext(event: MatrixEvent): MatrixEvent | undefined { - return this.events[this.events.indexOf(event) + 1]; - } - - public addEvent(event: MatrixEvent): void { - if (this.addOrReplaceEvent(event)) { - this.sort(); - } - } - - public addEvents(events: MatrixEvent[]): void { - const atLeastOneNew = events.reduce((newSoFar: boolean, event: MatrixEvent): boolean => { - return this.addOrReplaceEvent(event) || newSoFar; - }, false); - - if (atLeastOneNew) { - this.sort(); - } - } - - public includes(event: MatrixEvent): boolean { - return !!this.events.find((e) => this.equalByTxnIdOrId(event, e)); - } - - /** - * @returns {number} Length in milliseconds - */ - public getLength(): number { - return this.events.reduce((length: number, event: MatrixEvent) => { - return length + this.calculateChunkLength(event); - }, 0); - } - - public getLengthSeconds(): number { - return this.getLength() / 1000; - } - - /** - * Returns the accumulated length to (excl.) a chunk event. - */ - public getLengthTo(event: MatrixEvent): number { - let length = 0; - - for (let i = 0; i < this.events.indexOf(event); i++) { - length += this.calculateChunkLength(this.events[i]); - } - - return length; - } - - public findByTime(time: number): MatrixEvent | null { - let lengthSoFar = 0; - - for (let i = 0; i < this.events.length; i++) { - lengthSoFar += this.calculateChunkLength(this.events[i]); - - if (lengthSoFar >= time) { - return this.events[i]; - } - } - - return null; - } - - public isLast(event: MatrixEvent): boolean { - return this.events.indexOf(event) >= this.events.length - 1; - } - - public getSequenceForEvent(event: MatrixEvent): number | null { - const sequence = parseInt(event.getContent()?.[VoiceBroadcastChunkEventType]?.sequence, 10); - if (!isNaN(sequence)) return sequence; - - if (this.events.includes(event)) return this.events.indexOf(event) + 1; - - return null; - } - - public getNumberOfEvents(): number { - return this.events.length; - } - - private calculateChunkLength(event: MatrixEvent): number { - return event.getContent()?.["org.matrix.msc1767.audio"]?.duration || event.getContent()?.info?.duration || 0; - } - - private addOrReplaceEvent = (event: MatrixEvent): boolean => { - this.events = this.events.filter((e) => !this.equalByTxnIdOrId(event, e)); - this.events.push(event); - return true; - }; - - private equalByTxnIdOrId(eventA: MatrixEvent, eventB: MatrixEvent): boolean { - return ( - (eventA.getTxnId() && eventB.getTxnId() && eventA.getTxnId() === eventB.getTxnId()) || - eventA.getId() === eventB.getId() - ); - } - - /** - * Sort by sequence, if available for all events. - * Else fall back to timestamp. - */ - private sort(): void { - const compareFn = this.allHaveSequence() ? this.compareBySequence : this.compareByTimestamp; - this.events.sort(compareFn); - } - - private compareBySequence = (a: MatrixEvent, b: MatrixEvent): number => { - const aSequence = a.getContent()?.[VoiceBroadcastChunkEventType]?.sequence || 0; - const bSequence = b.getContent()?.[VoiceBroadcastChunkEventType]?.sequence || 0; - return aSequence - bSequence; - }; - - private compareByTimestamp = (a: MatrixEvent, b: MatrixEvent): number => { - return a.getTs() - b.getTs(); - }; - - private allHaveSequence(): boolean { - return !this.events.some((event: MatrixEvent) => { - const sequence = event.getContent()?.[VoiceBroadcastChunkEventType]?.sequence; - return parseInt(sequence, 10) !== sequence; - }); - } -} diff --git a/src/voice-broadcast/utils/VoiceBroadcastResumer.ts b/src/voice-broadcast/utils/VoiceBroadcastResumer.ts deleted file mode 100644 index 963b6ef3a62..00000000000 --- a/src/voice-broadcast/utils/VoiceBroadcastResumer.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { ClientEvent, MatrixClient, MatrixEvent, RelationType, Room, SyncState } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastInfoEventContent, VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from ".."; -import { IDestroyable } from "../../utils/IDestroyable"; -import { findRoomLiveVoiceBroadcastFromUserAndDevice } from "./findRoomLiveVoiceBroadcastFromUserAndDevice"; - -/** - * Handles voice broadcasts on app resume (after logging in, reload, crash…). - */ -export class VoiceBroadcastResumer implements IDestroyable { - public constructor(private client: MatrixClient) { - if (client.isInitialSyncComplete()) { - this.resume(); - } else { - // wait for initial sync - client.on(ClientEvent.Sync, this.onClientSync); - } - } - - private onClientSync = (): void => { - if (this.client.getSyncState() === SyncState.Syncing) { - this.client.off(ClientEvent.Sync, this.onClientSync); - this.resume(); - } - }; - - private resume(): void { - const userId = this.client.getUserId(); - const deviceId = this.client.getDeviceId(); - - if (!userId || !deviceId) { - // Resuming a voice broadcast only makes sense if there is a user. - return; - } - - this.client.getRooms().forEach((room: Room) => { - const infoEvent = findRoomLiveVoiceBroadcastFromUserAndDevice(room, userId, deviceId); - - if (infoEvent) { - // Found a live broadcast event from current device; stop it. - // Stopping it is a temporary solution (see PSF-1669). - this.sendStopVoiceBroadcastStateEvent(infoEvent); - return false; - } - }); - } - - private sendStopVoiceBroadcastStateEvent(infoEvent: MatrixEvent): void { - const userId = this.client.getUserId(); - const deviceId = this.client.getDeviceId(); - const roomId = infoEvent.getRoomId(); - - if (!userId || !deviceId || !roomId) { - // We can only send a state event if we know all the IDs. - return; - } - - const content: VoiceBroadcastInfoEventContent = { - device_id: deviceId, - state: VoiceBroadcastInfoState.Stopped, - }; - - // all events should reference the started event - const referencedEventId = - infoEvent.getContent()?.state === VoiceBroadcastInfoState.Started - ? infoEvent.getId() - : infoEvent.getContent()?.["m.relates_to"]?.event_id; - - if (referencedEventId) { - content["m.relates_to"] = { - rel_type: RelationType.Reference, - event_id: referencedEventId, - }; - } - - this.client.sendStateEvent(roomId, VoiceBroadcastInfoEventType, content, userId); - } - - public destroy(): void { - this.client.off(ClientEvent.Sync, this.onClientSync); - } -} diff --git a/src/voice-broadcast/utils/checkVoiceBroadcastPreConditions.tsx b/src/voice-broadcast/utils/checkVoiceBroadcastPreConditions.tsx deleted file mode 100644 index ae96bc0b14e..00000000000 --- a/src/voice-broadcast/utils/checkVoiceBroadcastPreConditions.tsx +++ /dev/null @@ -1,86 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { MatrixClient, Room, SyncState } from "matrix-js-sdk/src/matrix"; - -import { hasRoomLiveVoiceBroadcast, VoiceBroadcastInfoEventType, VoiceBroadcastRecordingsStore } from ".."; -import InfoDialog from "../../components/views/dialogs/InfoDialog"; -import { _t } from "../../languageHandler"; -import Modal from "../../Modal"; - -const showAlreadyRecordingDialog = (): void => { - Modal.createDialog(InfoDialog, { - title: _t("voice_broadcast|failed_already_recording_title"), - description:

{_t("voice_broadcast|failed_already_recording_description")}

, - hasCloseButton: true, - }); -}; - -const showInsufficientPermissionsDialog = (): void => { - Modal.createDialog(InfoDialog, { - title: _t("voice_broadcast|failed_insufficient_permission_title"), - description:

{_t("voice_broadcast|failed_insufficient_permission_description")}

, - hasCloseButton: true, - }); -}; - -const showOthersAlreadyRecordingDialog = (): void => { - Modal.createDialog(InfoDialog, { - title: _t("voice_broadcast|failed_others_already_recording_title"), - description:

{_t("voice_broadcast|failed_others_already_recording_description")}

, - hasCloseButton: true, - }); -}; - -const showNoConnectionDialog = (): void => { - Modal.createDialog(InfoDialog, { - title: _t("voice_broadcast|failed_no_connection_title"), - description:

{_t("voice_broadcast|failed_no_connection_description")}

, - hasCloseButton: true, - }); -}; - -export const checkVoiceBroadcastPreConditions = async ( - room: Room, - client: MatrixClient, - recordingsStore: VoiceBroadcastRecordingsStore, -): Promise => { - if (recordingsStore.getCurrent()) { - showAlreadyRecordingDialog(); - return false; - } - - const currentUserId = client.getUserId(); - - if (!currentUserId) return false; - - if (!room.currentState.maySendStateEvent(VoiceBroadcastInfoEventType, currentUserId)) { - showInsufficientPermissionsDialog(); - return false; - } - - if (client.getSyncState() === SyncState.Error) { - showNoConnectionDialog(); - return false; - } - - const { hasBroadcast, startedByUser } = await hasRoomLiveVoiceBroadcast(client, room, currentUserId); - - if (hasBroadcast && startedByUser) { - showAlreadyRecordingDialog(); - return false; - } - - if (hasBroadcast) { - showOthersAlreadyRecordingDialog(); - return false; - } - - return true; -}; diff --git a/src/voice-broadcast/utils/cleanUpBroadcasts.ts b/src/voice-broadcast/utils/cleanUpBroadcasts.ts deleted file mode 100644 index 50133274b0d..00000000000 --- a/src/voice-broadcast/utils/cleanUpBroadcasts.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { SdkContextClass } from "../../contexts/SDKContext"; - -export const cleanUpBroadcasts = async (stores: SdkContextClass): Promise => { - stores.voiceBroadcastPlaybacksStore.getCurrent()?.stop(); - stores.voiceBroadcastPlaybacksStore.clearCurrent(); - - await stores.voiceBroadcastRecordingsStore.getCurrent()?.stop(); - stores.voiceBroadcastRecordingsStore.clearCurrent(); - - stores.voiceBroadcastPreRecordingStore.getCurrent()?.cancel(); - stores.voiceBroadcastPreRecordingStore.clearCurrent(); -}; diff --git a/src/voice-broadcast/utils/determineVoiceBroadcastLiveness.ts b/src/voice-broadcast/utils/determineVoiceBroadcastLiveness.ts deleted file mode 100644 index 8d9660c572c..00000000000 --- a/src/voice-broadcast/utils/determineVoiceBroadcastLiveness.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { VoiceBroadcastInfoState, VoiceBroadcastLiveness } from ".."; - -const stateLivenessMap: Map = new Map([ - ["started", "live"], - ["resumed", "live"], - ["paused", "grey"], - ["stopped", "not-live"], -] as Array<[VoiceBroadcastInfoState, VoiceBroadcastLiveness]>); - -export const determineVoiceBroadcastLiveness = (infoState: VoiceBroadcastInfoState): VoiceBroadcastLiveness => { - return stateLivenessMap.get(infoState) ?? "not-live"; -}; diff --git a/src/voice-broadcast/utils/doClearCurrentVoiceBroadcastPlaybackIfStopped.ts b/src/voice-broadcast/utils/doClearCurrentVoiceBroadcastPlaybackIfStopped.ts deleted file mode 100644 index ef0e1e7aed1..00000000000 --- a/src/voice-broadcast/utils/doClearCurrentVoiceBroadcastPlaybackIfStopped.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { VoiceBroadcastPlaybacksStore, VoiceBroadcastPlaybackState } from ".."; - -export const doClearCurrentVoiceBroadcastPlaybackIfStopped = ( - voiceBroadcastPlaybacksStore: VoiceBroadcastPlaybacksStore, -): void => { - if (voiceBroadcastPlaybacksStore.getCurrent()?.getState() === VoiceBroadcastPlaybackState.Stopped) { - // clear current if stopped - return; - } -}; diff --git a/src/voice-broadcast/utils/doMaybeSetCurrentVoiceBroadcastPlayback.ts b/src/voice-broadcast/utils/doMaybeSetCurrentVoiceBroadcastPlayback.ts deleted file mode 100644 index 2ec4ab185df..00000000000 --- a/src/voice-broadcast/utils/doMaybeSetCurrentVoiceBroadcastPlayback.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; - -import { - hasRoomLiveVoiceBroadcast, - VoiceBroadcastPlaybacksStore, - VoiceBroadcastPlaybackState, - VoiceBroadcastRecordingsStore, -} from ".."; - -/** - * When a live voice broadcast is in the room and - * another voice broadcast is not currently being listened to or recorded - * the live broadcast in the room is set as the current broadcast to listen to. - * When there is no live broadcast in the room: clear current broadcast. - * - * @param {Room} room The room to check for a live voice broadcast - * @param {MatrixClient} client - * @param {VoiceBroadcastPlaybacksStore} voiceBroadcastPlaybacksStore - * @param {VoiceBroadcastRecordingsStore} voiceBroadcastRecordingsStore - */ -export const doMaybeSetCurrentVoiceBroadcastPlayback = async ( - room: Room, - client: MatrixClient, - voiceBroadcastPlaybacksStore: VoiceBroadcastPlaybacksStore, - voiceBroadcastRecordingsStore: VoiceBroadcastRecordingsStore, -): Promise => { - // do not disturb the current recording - if (voiceBroadcastRecordingsStore.hasCurrent()) return; - - const currentPlayback = voiceBroadcastPlaybacksStore.getCurrent(); - - if (currentPlayback && currentPlayback.getState() !== VoiceBroadcastPlaybackState.Stopped) { - // do not disturb the current playback - return; - } - - const { infoEvent } = await hasRoomLiveVoiceBroadcast(client, room); - - if (infoEvent) { - // live broadcast in the room + no recording + not listening yet: set the current broadcast - const voiceBroadcastPlayback = voiceBroadcastPlaybacksStore.getByInfoEvent(infoEvent, client); - voiceBroadcastPlaybacksStore.setCurrent(voiceBroadcastPlayback); - return; - } - - // no broadcast; not listening: clear current - voiceBroadcastPlaybacksStore.clearCurrent(); -}; diff --git a/src/voice-broadcast/utils/findRoomLiveVoiceBroadcastFromUserAndDevice.ts b/src/voice-broadcast/utils/findRoomLiveVoiceBroadcastFromUserAndDevice.ts deleted file mode 100644 index fbd02d44fb7..00000000000 --- a/src/voice-broadcast/utils/findRoomLiveVoiceBroadcastFromUserAndDevice.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from ".."; - -export const findRoomLiveVoiceBroadcastFromUserAndDevice = ( - room: Room, - userId: string, - deviceId: string, -): MatrixEvent | null => { - const stateEvent = room.currentState.getStateEvents(VoiceBroadcastInfoEventType, userId); - - // no broadcast from that user - if (!stateEvent) return null; - - const content = stateEvent.getContent() || {}; - - // stopped broadcast - if (content.state === VoiceBroadcastInfoState.Stopped) return null; - - return content.device_id === deviceId ? stateEvent : null; -}; diff --git a/src/voice-broadcast/utils/getChunkLength.ts b/src/voice-broadcast/utils/getChunkLength.ts deleted file mode 100644 index b3fe2f557d4..00000000000 --- a/src/voice-broadcast/utils/getChunkLength.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import SdkConfig, { DEFAULTS } from "../../SdkConfig"; -import { Features } from "../../settings/Settings"; -import SettingsStore from "../../settings/SettingsStore"; - -/** - * Returns the target chunk length for voice broadcasts: - * - If {@see Features.VoiceBroadcastForceSmallChunks} is enabled uses 15s chunk length - * - Otherwise to get the value from the voice_broadcast.chunk_length config - * - If that fails from DEFAULTS - * - If that fails fall back to 120 (two minutes) - */ -export const getChunkLength = (): number => { - if (SettingsStore.getValue(Features.VoiceBroadcastForceSmallChunks)) return 15; - return SdkConfig.get("voice_broadcast")?.chunk_length || DEFAULTS.voice_broadcast?.chunk_length || 120; -}; diff --git a/src/voice-broadcast/utils/getMaxBroadcastLength.ts b/src/voice-broadcast/utils/getMaxBroadcastLength.ts deleted file mode 100644 index e5df83ef059..00000000000 --- a/src/voice-broadcast/utils/getMaxBroadcastLength.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import SdkConfig, { DEFAULTS } from "../../SdkConfig"; - -/** - * Returns the max length for voice broadcasts: - * - Tries to get the value from the voice_broadcast.max_length config - * - If that fails from DEFAULTS - * - If that fails fall back to four hours - */ -export const getMaxBroadcastLength = (): number => { - return SdkConfig.get("voice_broadcast")?.max_length || DEFAULTS.voice_broadcast?.max_length || 4 * 60 * 60; -}; diff --git a/src/voice-broadcast/utils/hasRoomLiveVoiceBroadcast.ts b/src/voice-broadcast/utils/hasRoomLiveVoiceBroadcast.ts deleted file mode 100644 index 939eb1a0c27..00000000000 --- a/src/voice-broadcast/utils/hasRoomLiveVoiceBroadcast.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; - -import { retrieveStartedInfoEvent, VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from ".."; -import { asyncEvery } from "../../utils/arrays"; - -interface Result { - // whether there is a live broadcast in the room - hasBroadcast: boolean; - // info event of any live broadcast in the room - infoEvent: MatrixEvent | null; - // whether the broadcast was started by the user - startedByUser: boolean; -} - -export const hasRoomLiveVoiceBroadcast = async (client: MatrixClient, room: Room, userId?: string): Promise => { - let hasBroadcast = false; - let startedByUser = false; - let infoEvent: MatrixEvent | null = null; - - const stateEvents = room.currentState.getStateEvents(VoiceBroadcastInfoEventType); - await asyncEvery(stateEvents, async (event: MatrixEvent) => { - const state = event.getContent()?.state; - - if (state && state !== VoiceBroadcastInfoState.Stopped) { - const startEvent = await retrieveStartedInfoEvent(event, client); - - // skip if started voice broadcast event is redacted - if (startEvent?.isRedacted()) return true; - - hasBroadcast = true; - infoEvent = startEvent; - - // state key = sender's MXID - if (event.getStateKey() === userId) { - startedByUser = true; - // break here, because more than true / true is not possible - return false; - } - } - - return true; - }); - - return { - hasBroadcast, - infoEvent, - startedByUser, - }; -}; diff --git a/src/voice-broadcast/utils/isRelatedToVoiceBroadcast.ts b/src/voice-broadcast/utils/isRelatedToVoiceBroadcast.ts deleted file mode 100644 index eca8f890e05..00000000000 --- a/src/voice-broadcast/utils/isRelatedToVoiceBroadcast.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixClient, MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastInfoEventType } from "../types"; - -export const isRelatedToVoiceBroadcast = (event: MatrixEvent, client: MatrixClient): boolean => { - const relation = event.getRelation(); - - return ( - relation?.rel_type === RelationType.Reference && - !!relation.event_id && - client.getRoom(event.getRoomId())?.findEventById(relation.event_id)?.getType() === VoiceBroadcastInfoEventType - ); -}; diff --git a/src/voice-broadcast/utils/isVoiceBroadcastStartedEvent.ts b/src/voice-broadcast/utils/isVoiceBroadcastStartedEvent.ts deleted file mode 100644 index fffe45850e9..00000000000 --- a/src/voice-broadcast/utils/isVoiceBroadcastStartedEvent.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixEvent } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../types"; - -export const isVoiceBroadcastStartedEvent = (event: MatrixEvent): boolean => { - return ( - event.getType() === VoiceBroadcastInfoEventType && event.getContent()?.state === VoiceBroadcastInfoState.Started - ); -}; diff --git a/src/voice-broadcast/utils/pauseNonLiveBroadcastFromOtherRoom.ts b/src/voice-broadcast/utils/pauseNonLiveBroadcastFromOtherRoom.ts deleted file mode 100644 index e854ba9bac9..00000000000 --- a/src/voice-broadcast/utils/pauseNonLiveBroadcastFromOtherRoom.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { Room } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastPlaybacksStore } from ".."; - -export const pauseNonLiveBroadcastFromOtherRoom = ( - room: Room, - voiceBroadcastPlaybacksStore: VoiceBroadcastPlaybacksStore, -): void => { - const playingBroadcast = voiceBroadcastPlaybacksStore.getCurrent(); - - if ( - !playingBroadcast || - playingBroadcast?.getLiveness() === "live" || - playingBroadcast?.infoEvent.getRoomId() === room.roomId - ) { - return; - } - - voiceBroadcastPlaybacksStore.clearCurrent(); - playingBroadcast.pause(); -}; diff --git a/src/voice-broadcast/utils/retrieveStartedInfoEvent.ts b/src/voice-broadcast/utils/retrieveStartedInfoEvent.ts deleted file mode 100644 index cc5be144c98..00000000000 --- a/src/voice-broadcast/utils/retrieveStartedInfoEvent.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastInfoState } from ".."; - -export const retrieveStartedInfoEvent = async ( - event: MatrixEvent, - client: MatrixClient, -): Promise => { - // started event passed as argument - if (event.getContent()?.state === VoiceBroadcastInfoState.Started) return event; - - const relatedEventId = event.getRelation()?.event_id; - - // no related event - if (!relatedEventId) return null; - - const roomId = event.getRoomId() || ""; - const relatedEventFromRoom = client.getRoom(roomId)?.findEventById(relatedEventId); - - // event found - if (relatedEventFromRoom) return relatedEventFromRoom; - - try { - const relatedEventData = await client.fetchRoomEvent(roomId, relatedEventId); - return new MatrixEvent(relatedEventData); - } catch {} - - return null; -}; diff --git a/src/voice-broadcast/utils/setUpVoiceBroadcastPreRecording.ts b/src/voice-broadcast/utils/setUpVoiceBroadcastPreRecording.ts deleted file mode 100644 index c50607c58fb..00000000000 --- a/src/voice-broadcast/utils/setUpVoiceBroadcastPreRecording.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; - -import { - checkVoiceBroadcastPreConditions, - VoiceBroadcastPlaybacksStore, - VoiceBroadcastPreRecording, - VoiceBroadcastPreRecordingStore, - VoiceBroadcastRecordingsStore, -} from ".."; - -export const setUpVoiceBroadcastPreRecording = async ( - room: Room, - client: MatrixClient, - playbacksStore: VoiceBroadcastPlaybacksStore, - recordingsStore: VoiceBroadcastRecordingsStore, - preRecordingStore: VoiceBroadcastPreRecordingStore, -): Promise => { - if (!(await checkVoiceBroadcastPreConditions(room, client, recordingsStore))) { - return null; - } - - const userId = client.getUserId(); - if (!userId) return null; - - const sender = room.getMember(userId); - if (!sender) return null; - - // pause and clear current playback (if any) - playbacksStore.getCurrent()?.pause(); - playbacksStore.clearCurrent(); - - const preRecording = new VoiceBroadcastPreRecording(room, sender, client, playbacksStore, recordingsStore); - preRecordingStore.setCurrent(preRecording); - return preRecording; -}; diff --git a/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastRecordingTile.ts b/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastRecordingTile.ts deleted file mode 100644 index d729d9e1ca1..00000000000 --- a/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastRecordingTile.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastInfoEventContent, VoiceBroadcastInfoState } from ".."; - -export const shouldDisplayAsVoiceBroadcastRecordingTile = ( - state: VoiceBroadcastInfoState, - client: MatrixClient, - event: MatrixEvent, -): boolean => { - const userId = client.getUserId(); - return ( - !!userId && - userId === event.getSender() && - client.getDeviceId() === event.getContent()?.device_id && - state !== VoiceBroadcastInfoState.Stopped - ); -}; diff --git a/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastStoppedText.ts b/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastStoppedText.ts deleted file mode 100644 index 2179aff3b76..00000000000 --- a/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastStoppedText.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixEvent } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from ".."; - -export const shouldDisplayAsVoiceBroadcastStoppedText = (event: MatrixEvent): boolean => - event.getType() === VoiceBroadcastInfoEventType && - event.getContent()?.state === VoiceBroadcastInfoState.Stopped && - !event.isRedacted(); diff --git a/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastTile.ts b/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastTile.ts deleted file mode 100644 index 9a51b33c9a6..00000000000 --- a/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastTile.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixEvent } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from ".."; - -export const shouldDisplayAsVoiceBroadcastTile = (event: MatrixEvent): boolean => - event.getType?.() === VoiceBroadcastInfoEventType && - (event.getContent?.()?.state === VoiceBroadcastInfoState.Started || event.isRedacted()); diff --git a/src/voice-broadcast/utils/showCantStartACallDialog.tsx b/src/voice-broadcast/utils/showCantStartACallDialog.tsx deleted file mode 100644 index eeeb86ee07b..00000000000 --- a/src/voice-broadcast/utils/showCantStartACallDialog.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; - -import InfoDialog from "../../components/views/dialogs/InfoDialog"; -import { _t } from "../../languageHandler"; -import Modal from "../../Modal"; - -export const showCantStartACallDialog = (): void => { - Modal.createDialog(InfoDialog, { - title: _t("voip|failed_call_live_broadcast_title"), - description:

{_t("voip|failed_call_live_broadcast_description")}

, - hasCloseButton: true, - }); -}; diff --git a/src/voice-broadcast/utils/startNewVoiceBroadcastRecording.ts b/src/voice-broadcast/utils/startNewVoiceBroadcastRecording.ts deleted file mode 100644 index f0c5a919329..00000000000 --- a/src/voice-broadcast/utils/startNewVoiceBroadcastRecording.ts +++ /dev/null @@ -1,92 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { ISendEventResponse, MatrixClient, Room, RoomStateEvent } from "matrix-js-sdk/src/matrix"; -import { defer } from "matrix-js-sdk/src/utils"; - -import { - VoiceBroadcastInfoEventContent, - VoiceBroadcastInfoEventType, - VoiceBroadcastInfoState, - VoiceBroadcastRecordingsStore, - VoiceBroadcastRecording, - getChunkLength, - VoiceBroadcastPlaybacksStore, -} from ".."; -import { checkVoiceBroadcastPreConditions } from "./checkVoiceBroadcastPreConditions"; - -const startBroadcast = async ( - room: Room, - client: MatrixClient, - recordingsStore: VoiceBroadcastRecordingsStore, -): Promise => { - const { promise, resolve, reject } = defer(); - - const userId = client.getUserId(); - - if (!userId) { - reject("unable to start voice broadcast if current user is unknown"); - return promise; - } - - let result: ISendEventResponse | null = null; - - const onRoomStateEvents = (): void => { - if (!result) return; - - const voiceBroadcastEvent = room.currentState.getStateEvents(VoiceBroadcastInfoEventType, userId); - - if (voiceBroadcastEvent?.getId() === result.event_id) { - room.off(RoomStateEvent.Events, onRoomStateEvents); - const recording = new VoiceBroadcastRecording(voiceBroadcastEvent, client); - recordingsStore.setCurrent(recording); - recording.start(); - resolve(recording); - } - }; - - room.on(RoomStateEvent.Events, onRoomStateEvents); - - // XXX Michael W: refactor to live event - result = await client.sendStateEvent( - room.roomId, - VoiceBroadcastInfoEventType, - { - device_id: client.getDeviceId(), - state: VoiceBroadcastInfoState.Started, - chunk_length: getChunkLength(), - } as VoiceBroadcastInfoEventContent, - userId, - ); - - return promise; -}; - -/** - * Starts a new Voice Broadcast Recording, if - * - the user has the permissions to do so in the room - * - the user is not already recording a voice broadcast - * - there is no other broadcast being recorded in the room, yet - * Sends a voice_broadcast_info state event and waits for the event to actually appear in the room state. - */ -export const startNewVoiceBroadcastRecording = async ( - room: Room, - client: MatrixClient, - playbacksStore: VoiceBroadcastPlaybacksStore, - recordingsStore: VoiceBroadcastRecordingsStore, -): Promise => { - if (!(await checkVoiceBroadcastPreConditions(room, client, recordingsStore))) { - return null; - } - - // pause and clear current playback (if any) - playbacksStore.getCurrent()?.pause(); - playbacksStore.clearCurrent(); - - return startBroadcast(room, client, recordingsStore); -}; diff --git a/src/voice-broadcast/utils/textForVoiceBroadcastStoppedEvent.tsx b/src/voice-broadcast/utils/textForVoiceBroadcastStoppedEvent.tsx deleted file mode 100644 index bc2aa412a5a..00000000000 --- a/src/voice-broadcast/utils/textForVoiceBroadcastStoppedEvent.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React, { ReactNode } from "react"; -import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; - -import { MatrixClientPeg } from "../../MatrixClientPeg"; -import AccessibleButton from "../../components/views/elements/AccessibleButton"; -import { highlightEvent } from "../../utils/EventUtils"; -import { _t } from "../../languageHandler"; -import { getSenderName } from "../../utils/event/getSenderName"; - -export const textForVoiceBroadcastStoppedEvent = (event: MatrixEvent, client: MatrixClient): (() => ReactNode) => { - return (): ReactNode => { - const ownUserId = MatrixClientPeg.get()?.getUserId(); - const startEventId = event.getRelation()?.event_id; - const roomId = event.getRoomId(); - - const templateTags = { - a: (text: string) => - startEventId && roomId ? ( - highlightEvent(roomId, startEventId)}> - {text} - - ) : ( - text - ), - }; - - if (ownUserId && ownUserId === event.getSender()) { - return _t("timeline|io.element.voice_broadcast_info|you", {}, templateTags); - } - - return _t("timeline|io.element.voice_broadcast_info|user", { senderName: getSenderName(event) }, templateTags); - }; -}; diff --git a/src/voice-broadcast/utils/textForVoiceBroadcastStoppedEventWithoutLink.ts b/src/voice-broadcast/utils/textForVoiceBroadcastStoppedEventWithoutLink.ts deleted file mode 100644 index 13d7f47c484..00000000000 --- a/src/voice-broadcast/utils/textForVoiceBroadcastStoppedEventWithoutLink.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixEvent } from "matrix-js-sdk/src/matrix"; - -import { _t } from "../../languageHandler"; -import { MatrixClientPeg } from "../../MatrixClientPeg"; -import { getSenderName } from "../../utils/event/getSenderName"; - -export const textForVoiceBroadcastStoppedEventWithoutLink = (event: MatrixEvent): string => { - const ownUserId = MatrixClientPeg.get()?.getUserId(); - - if (ownUserId && ownUserId === event.getSender()) { - return _t("event_preview|io.element.voice_broadcast_info|you", {}); - } - - return _t("event_preview|io.element.voice_broadcast_info|user", { senderName: getSenderName(event) }); -}; diff --git a/test/components/views/dialogs/security/CreateCrossSigningDialog-test.tsx b/test/components/views/dialogs/security/InitialCryptoSetupDialog-test.tsx similarity index 91% rename from test/components/views/dialogs/security/CreateCrossSigningDialog-test.tsx rename to test/components/views/dialogs/security/InitialCryptoSetupDialog-test.tsx index 3e5dc4eb94e..4d3d495a38d 100644 --- a/test/components/views/dialogs/security/CreateCrossSigningDialog-test.tsx +++ b/test/components/views/dialogs/security/InitialCryptoSetupDialog-test.tsx @@ -12,14 +12,14 @@ import { mocked } from "jest-mock"; import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { createCrossSigning } from "../../../../../src/CreateCrossSigning"; -import CreateCrossSigningDialog from "../../../../../src/components/views/dialogs/security/CreateCrossSigningDialog"; +import { InitialCryptoSetupDialog } from "../../../../../src/components/views/dialogs/security/InitialCryptoSetupDialog"; import { createTestClient } from "../../../../test-utils"; jest.mock("../../../../../src/CreateCrossSigning", () => ({ createCrossSigning: jest.fn(), })); -describe("CreateCrossSigningDialog", () => { +describe("InitialCryptoSetupDialog", () => { let client: MatrixClient; let createCrossSigningResolve: () => void; let createCrossSigningReject: (e: Error) => void; @@ -43,7 +43,7 @@ describe("CreateCrossSigningDialog", () => { const onFinished = jest.fn(); render( - { it("should display an error if createCrossSigning fails", async () => { render( - { const onFinished = jest.fn(); render( - { const onFinished = jest.fn(); render( - { it("should retry when the retry button is clicked", async () => { render( - ): IRoom canAskToJoin: false, promptAskToJoin: false, viewRoomOpts: { buttons: [] }, - + isRoomEncrypted: false, ...override, }; } diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index 9af2ffa340e..f9aee512a30 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -116,7 +116,7 @@ export function createTestClient(): MatrixClient { getCrypto: jest.fn().mockReturnValue({ getOwnDeviceKeys: jest.fn(), - getUserDeviceInfo: jest.fn(), + getUserDeviceInfo: jest.fn().mockResolvedValue(new Map()), getUserVerificationStatus: jest.fn(), getDeviceVerificationStatus: jest.fn(), resetKeyBackup: jest.fn(), @@ -135,6 +135,7 @@ export function createTestClient(): MatrixClient { loadSessionBackupPrivateKeyFromSecretStorage: jest.fn(), storeSessionBackupPrivateKey: jest.fn(), getKeyBackupInfo: jest.fn().mockResolvedValue(null), + getEncryptionInfoForEvent: jest.fn().mockResolvedValue(null), }), getPushActionsForEvent: jest.fn(), diff --git a/test/test-utils/threads.ts b/test/test-utils/threads.ts index d2459653e53..83313b1b8de 100644 --- a/test/test-utils/threads.ts +++ b/test/test-utils/threads.ts @@ -157,6 +157,6 @@ export const populateThread = async ({ // that it is already loaded, and send the events again to the room // so they are added to the thread timeline. ret.thread.initialEventsFetched = true; - await room.addLiveEvents(ret.events); + await room.addLiveEvents(ret.events, { addToState: false }); return ret; }; diff --git a/test/unit-tests/LegacyCallHandler-test.ts b/test/unit-tests/LegacyCallHandler-test.ts index c3e64dcf941..476d89a1f0f 100644 --- a/test/unit-tests/LegacyCallHandler-test.ts +++ b/test/unit-tests/LegacyCallHandler-test.ts @@ -39,10 +39,6 @@ import { Action } from "../../src/dispatcher/actions"; import { getFunctionalMembers } from "../../src/utils/room/getFunctionalMembers"; import SettingsStore from "../../src/settings/SettingsStore"; import { UIFeature } from "../../src/settings/UIFeature"; -import { VoiceBroadcastInfoState, VoiceBroadcastPlayback, VoiceBroadcastRecording } from "../../src/voice-broadcast"; -import { mkVoiceBroadcastInfoStateEvent } from "./voice-broadcast/utils/test-utils"; -import { SdkContextClass } from "../../src/contexts/SDKContext"; -import Modal from "../../src/Modal"; import { createAudioContext } from "../../src/audio/compat"; import * as ManagedHybrid from "../../src/widgets/ManagedHybrid"; @@ -403,53 +399,6 @@ describe("LegacyCallHandler", () => { await callHandler.placeCall(NATIVE_ROOM_ALICE, CallType.Voice); expect(spy).toHaveBeenCalledWith(MatrixClientPeg.safeGet().getRoom(NATIVE_ROOM_ALICE)); }); - - describe("when listening to a voice broadcast", () => { - let voiceBroadcastPlayback: VoiceBroadcastPlayback; - - beforeEach(() => { - voiceBroadcastPlayback = new VoiceBroadcastPlayback( - mkVoiceBroadcastInfoStateEvent( - "!room:example.com", - VoiceBroadcastInfoState.Started, - MatrixClientPeg.safeGet().getSafeUserId(), - "d42", - ), - MatrixClientPeg.safeGet(), - SdkContextClass.instance.voiceBroadcastRecordingsStore, - ); - SdkContextClass.instance.voiceBroadcastPlaybacksStore.setCurrent(voiceBroadcastPlayback); - jest.spyOn(voiceBroadcastPlayback, "pause").mockImplementation(); - }); - - it("and placing a call should pause the broadcast", async () => { - callHandler.placeCall(NATIVE_ROOM_ALICE, CallType.Voice); - await untilCallHandlerEvent(callHandler, LegacyCallHandlerEvent.CallState); - - expect(voiceBroadcastPlayback.pause).toHaveBeenCalled(); - }); - }); - - describe("when recording a voice broadcast", () => { - beforeEach(() => { - SdkContextClass.instance.voiceBroadcastRecordingsStore.setCurrent( - new VoiceBroadcastRecording( - mkVoiceBroadcastInfoStateEvent( - "!room:example.com", - VoiceBroadcastInfoState.Started, - MatrixClientPeg.safeGet().getSafeUserId(), - "d42", - ), - MatrixClientPeg.safeGet(), - ), - ); - }); - - it("and placing a call should show the info dialog", async () => { - callHandler.placeCall(NATIVE_ROOM_ALICE, CallType.Voice); - expect(Modal.createDialog).toMatchSnapshot(); - }); - }); }); describe("LegacyCallHandler without third party protocols", () => { @@ -528,9 +477,6 @@ describe("LegacyCallHandler without third party protocols", () => { audioElement.id = "remoteAudio"; document.body.appendChild(audioElement); - SdkContextClass.instance.voiceBroadcastPlaybacksStore.clearCurrent(); - SdkContextClass.instance.voiceBroadcastRecordingsStore.clearCurrent(); - fetchMock.get( "/media/ring.mp3", { body: new Blob(["1", "2", "3", "4"], { type: "audio/mpeg" }) }, diff --git a/test/unit-tests/Lifecycle-test.ts b/test/unit-tests/Lifecycle-test.ts index 67ac7c76373..04c3459bdf6 100644 --- a/test/unit-tests/Lifecycle-test.ts +++ b/test/unit-tests/Lifecycle-test.ts @@ -418,7 +418,7 @@ describe("Lifecycle", () => { undefined, ); - expect(MatrixClientPeg.start).toHaveBeenCalledWith({ rustCryptoStoreKey: expect.any(Buffer) }); + expect(MatrixClientPeg.start).toHaveBeenCalledWith({ rustCryptoStoreKey: expect.any(Uint8Array) }); }); describe("with a refresh token", () => { diff --git a/test/unit-tests/MatrixClientPeg-test.ts b/test/unit-tests/MatrixClientPeg-test.ts index 5a19b568c0b..46533405744 100644 --- a/test/unit-tests/MatrixClientPeg-test.ts +++ b/test/unit-tests/MatrixClientPeg-test.ts @@ -11,8 +11,6 @@ import fetchMockJest from "fetch-mock-jest"; import { advanceDateAndTime, stubClient } from "../test-utils"; import { IMatrixClientPeg, MatrixClientPeg as peg } from "../../src/MatrixClientPeg"; -import SettingsStore from "../../src/settings/SettingsStore"; -import { SettingLevel } from "../../src/settings/SettingLevel"; jest.useFakeTimers(); @@ -81,27 +79,18 @@ describe("MatrixClientPeg", () => { }); it("should initialise the rust crypto library by default", async () => { - const mockSetValue = jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined); - const mockInitRustCrypto = jest.spyOn(testPeg.safeGet(), "initRustCrypto").mockResolvedValue(undefined); const cryptoStoreKey = new Uint8Array([1, 2, 3, 4]); await testPeg.start({ rustCryptoStoreKey: cryptoStoreKey }); expect(mockInitRustCrypto).toHaveBeenCalledWith({ storageKey: cryptoStoreKey }); - - // we should have stashed the setting in the settings store - expect(mockSetValue).toHaveBeenCalledWith("feature_rust_crypto", null, SettingLevel.DEVICE, true); }); it("Should migrate existing login", async () => { - const mockSetValue = jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined); const mockInitRustCrypto = jest.spyOn(testPeg.safeGet(), "initRustCrypto").mockResolvedValue(undefined); await testPeg.start(); expect(mockInitRustCrypto).toHaveBeenCalledTimes(1); - - // we should have stashed the setting in the settings store - expect(mockSetValue).toHaveBeenCalledWith("feature_rust_crypto", null, SettingLevel.DEVICE, true); }); }); }); diff --git a/test/unit-tests/Notifier-test.ts b/test/unit-tests/Notifier-test.ts index 2fe7fdec0b0..f94f50724d0 100644 --- a/test/unit-tests/Notifier-test.ts +++ b/test/unit-tests/Notifier-test.ts @@ -43,8 +43,6 @@ import { mkThread } from "../test-utils/threads"; import dis from "../../src/dispatcher/dispatcher"; import { ThreadPayload } from "../../src/dispatcher/payloads/ThreadPayload"; import { Action } from "../../src/dispatcher/actions"; -import { VoiceBroadcastChunkEventType, VoiceBroadcastInfoState } from "../../src/voice-broadcast"; -import { mkVoiceBroadcastInfoStateEvent } from "./voice-broadcast/utils/test-utils"; import { addReplyToMessageContent } from "../../src/utils/Reply"; jest.mock("../../src/utils/notifications", () => ({ @@ -85,16 +83,13 @@ describe("Notifier", () => { }); }; - const mkAudioEvent = (broadcastChunkContent?: object): MatrixEvent => { - const chunkContent = broadcastChunkContent ? { [VoiceBroadcastChunkEventType]: broadcastChunkContent } : {}; - + const mkAudioEvent = (): MatrixEvent => { return mkEvent({ event: true, type: EventType.RoomMessage, user: "@user:example.com", room: "!room:example.com", content: { - ...chunkContent, msgtype: MsgType.Audio, body: "test audio message", }, @@ -320,24 +315,6 @@ describe("Notifier", () => { ); }); - it("should display the expected notification for a broadcast chunk with sequence = 1", () => { - const audioEvent = mkAudioEvent({ sequence: 1 }); - Notifier.displayPopupNotification(audioEvent, testRoom); - expect(MockPlatform.displayNotification).toHaveBeenCalledWith( - "@user:example.com (!room1:server)", - "@user:example.com started a voice broadcast", - "data:image/png;base64,00", - testRoom, - audioEvent, - ); - }); - - it("should display the expected notification for a broadcast chunk with sequence = 2", () => { - const audioEvent = mkAudioEvent({ sequence: 2 }); - Notifier.displayPopupNotification(audioEvent, testRoom); - expect(MockPlatform.displayNotification).not.toHaveBeenCalled(); - }); - it("should strip reply fallback", () => { const event = mkMessage({ msg: "Test", @@ -581,24 +558,6 @@ describe("Notifier", () => { Notifier.evaluateEvent(mkAudioEvent()); expect(Notifier.displayPopupNotification).toHaveBeenCalledTimes(1); }); - - it("should not show a notification for broadcast info events in any case", () => { - // Let client decide to show a notification - mockClient.getPushActionsForEvent.mockReturnValue({ - notify: true, - tweaks: {}, - }); - - const broadcastStartedEvent = mkVoiceBroadcastInfoStateEvent( - "!other:example.org", - VoiceBroadcastInfoState.Started, - "@user:example.com", - "ABC123", - ); - - Notifier.evaluateEvent(broadcastStartedEvent); - expect(Notifier.displayPopupNotification).not.toHaveBeenCalled(); - }); }); describe("setPromptHidden", () => { @@ -624,8 +583,7 @@ describe("Notifier", () => { content: { body: "this is a thread root" }, }), testRoom.threadsTimelineSets[0]!.getLiveTimeline(), - false, - false, + { toStartOfTimeline: false, fromCache: false, addToState: true }, ); expect(fn).not.toHaveBeenCalled(); diff --git a/test/unit-tests/RoomNotifs-test.ts b/test/unit-tests/RoomNotifs-test.ts index 51416ab7fdc..65089eba94c 100644 --- a/test/unit-tests/RoomNotifs-test.ts +++ b/test/unit-tests/RoomNotifs-test.ts @@ -147,7 +147,7 @@ describe("RoomNotifs test", () => { const itShouldCountPredecessorHighlightWhenThereIsAPredecessorInTheCreateEvent = (): void => { it("and there is a predecessor in the create event, it should count predecessor highlight", () => { - room.addLiveEvents([mkCreateEvent(OLD_ROOM_ID)]); + room.addLiveEvents([mkCreateEvent(OLD_ROOM_ID)], { addToState: true }); expect(getUnreadNotificationCount(room, NotificationCountType.Total, false)).toBe(8); expect(getUnreadNotificationCount(room, NotificationCountType.Highlight, false)).toBe(7); @@ -157,7 +157,7 @@ describe("RoomNotifs test", () => { const itShouldCountPredecessorHighlightWhenThereIsAPredecessorEvent = (): void => { it("and there is a predecessor event, it should count predecessor highlight", () => { client.getVisibleRooms(); - room.addLiveEvents([mkCreateEvent(OLD_ROOM_ID)]); + room.addLiveEvents([mkCreateEvent(OLD_ROOM_ID)], { addToState: true }); upsertRoomStateEvents(room, [mkPredecessorEvent(OLD_ROOM_ID)]); expect(getUnreadNotificationCount(room, NotificationCountType.Total, false)).toBe(8); @@ -185,7 +185,7 @@ describe("RoomNotifs test", () => { itShouldCountPredecessorHighlightWhenThereIsAPredecessorEvent(); it("and there is only a predecessor event, it should not count predecessor highlight", () => { - room.addLiveEvents([mkCreateEvent()]); + room.addLiveEvents([mkCreateEvent()], { addToState: true }); upsertRoomStateEvents(room, [mkPredecessorEvent(OLD_ROOM_ID)]); expect(getUnreadNotificationCount(room, NotificationCountType.Total, false)).toBe(2); @@ -204,7 +204,7 @@ describe("RoomNotifs test", () => { itShouldCountPredecessorHighlightWhenThereIsAPredecessorEvent(); it("and there is only a predecessor event, it should count predecessor highlight", () => { - room.addLiveEvents([mkCreateEvent()]); + room.addLiveEvents([mkCreateEvent()], { addToState: true }); upsertRoomStateEvents(room, [mkPredecessorEvent(OLD_ROOM_ID)]); expect(getUnreadNotificationCount(room, NotificationCountType.Total, false)).toBe(8); @@ -212,7 +212,7 @@ describe("RoomNotifs test", () => { }); it("and there is an unknown room in the predecessor event, it should not count predecessor highlight", () => { - room.addLiveEvents([mkCreateEvent()]); + room.addLiveEvents([mkCreateEvent()], { addToState: true }); upsertRoomStateEvents(room, [mkPredecessorEvent("!unknon:example.com")]); expect(getUnreadNotificationCount(room, NotificationCountType.Total, false)).toBe(2); diff --git a/test/unit-tests/SdkConfig-test.ts b/test/unit-tests/SdkConfig-test.ts index 4204a698fab..19d0eec9c30 100644 --- a/test/unit-tests/SdkConfig-test.ts +++ b/test/unit-tests/SdkConfig-test.ts @@ -18,10 +18,6 @@ describe("SdkConfig", () => { describe("with custom values", () => { beforeEach(() => { SdkConfig.put({ - voice_broadcast: { - chunk_length: 42, - max_length: 1337, - }, feedback: { existing_issues_url: "https://existing", } as any, @@ -30,8 +26,6 @@ describe("SdkConfig", () => { it("should return the custom config", () => { const customConfig = JSON.parse(JSON.stringify(DEFAULTS)); - customConfig.voice_broadcast.chunk_length = 42; - customConfig.voice_broadcast.max_length = 1337; customConfig.feedback.existing_issues_url = "https://existing"; expect(SdkConfig.get()).toEqual(customConfig); }); diff --git a/test/unit-tests/SupportedBrowser-test.ts b/test/unit-tests/SupportedBrowser-test.ts index ccf75e0dab7..a116ab1f9fd 100644 --- a/test/unit-tests/SupportedBrowser-test.ts +++ b/test/unit-tests/SupportedBrowser-test.ts @@ -66,10 +66,10 @@ describe("SupportedBrowser", () => { "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15", // Firefox 131 on macOS Sonoma "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:131.0) Gecko/20100101 Firefox/131.0", - // Edge 129 on Windows - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/129.0.2792.79", - // Edge 129 on macOS - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/129.0.2792.79", + // Edge 131 on Windows + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.2903.70", + // Edge 131 on macOS + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.2903.70", // Firefox 131 on Windows "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0", // Firefox 131 on Linux diff --git a/test/unit-tests/TestSdkContext.ts b/test/unit-tests/TestSdkContext.ts index d4bda4f8898..f60b083beff 100644 --- a/test/unit-tests/TestSdkContext.ts +++ b/test/unit-tests/TestSdkContext.ts @@ -16,29 +16,21 @@ import { SpaceStoreClass } from "../../src/stores/spaces/SpaceStore"; import { WidgetLayoutStore } from "../../src/stores/widgets/WidgetLayoutStore"; import { WidgetPermissionStore } from "../../src/stores/widgets/WidgetPermissionStore"; import WidgetStore from "../../src/stores/WidgetStore"; -import { - VoiceBroadcastPlaybacksStore, - VoiceBroadcastPreRecordingStore, - VoiceBroadcastRecordingsStore, -} from "../../src/voice-broadcast"; /** * A class which provides the same API as SdkContextClass but adds additional unsafe setters which can * replace individual stores. This is useful for tests which need to mock out stores. */ export class TestSdkContext extends SdkContextClass { - public declare _RightPanelStore?: RightPanelStore; - public declare _RoomNotificationStateStore?: RoomNotificationStateStore; - public declare _RoomViewStore?: RoomViewStore; - public declare _WidgetPermissionStore?: WidgetPermissionStore; - public declare _WidgetLayoutStore?: WidgetLayoutStore; - public declare _WidgetStore?: WidgetStore; - public declare _PosthogAnalytics?: PosthogAnalytics; - public declare _SlidingSyncManager?: SlidingSyncManager; - public declare _SpaceStore?: SpaceStoreClass; - public declare _VoiceBroadcastRecordingsStore?: VoiceBroadcastRecordingsStore; - public declare _VoiceBroadcastPreRecordingStore?: VoiceBroadcastPreRecordingStore; - public declare _VoiceBroadcastPlaybacksStore?: VoiceBroadcastPlaybacksStore; + declare public _RightPanelStore?: RightPanelStore; + declare public _RoomNotificationStateStore?: RoomNotificationStateStore; + declare public _RoomViewStore?: RoomViewStore; + declare public _WidgetPermissionStore?: WidgetPermissionStore; + declare public _WidgetLayoutStore?: WidgetLayoutStore; + declare public _WidgetStore?: WidgetStore; + declare public _PosthogAnalytics?: PosthogAnalytics; + declare public _SlidingSyncManager?: SlidingSyncManager; + declare public _SpaceStore?: SpaceStoreClass; constructor() { super(); diff --git a/test/unit-tests/Unread-test.ts b/test/unit-tests/Unread-test.ts index 8719da06ef7..15d3dab8f5e 100644 --- a/test/unit-tests/Unread-test.ts +++ b/test/unit-tests/Unread-test.ts @@ -138,7 +138,7 @@ describe("Unread", () => { room: roomId, content: {}, }); - room.addLiveEvents([event]); + room.addLiveEvents([event], { addToState: true }); // Don't care about the code path of hidden events. mocked(haveRendererForEvent).mockClear().mockReturnValue(true); @@ -157,7 +157,7 @@ describe("Unread", () => { content: {}, }); // Only for timeline events. - room.addLiveEvents([event]); + room.addLiveEvents([event], { addToState: true }); expect(doesRoomHaveUnreadMessages(room, false)).toBe(false); }); @@ -201,7 +201,7 @@ describe("Unread", () => { content: {}, }); // Only for timeline events. - room.addLiveEvents([event2]); + room.addLiveEvents([event2], { addToState: true }); expect(doesRoomHaveUnreadMessages(room, false)).toBe(true); }); @@ -403,7 +403,7 @@ describe("Unread", () => { redactedEvent.makeRedacted(redactedEvent, room); console.log("Event Id", redactedEvent.getId()); // Only for timeline events. - room.addLiveEvents([redactedEvent]); + room.addLiveEvents([redactedEvent], { addToState: true }); expect(doesRoomHaveUnreadMessages(room, true)).toBe(true); expect(logger.warn).toHaveBeenCalledWith( @@ -448,7 +448,7 @@ describe("Unread", () => { room: roomId, content: {}, }); - room.addLiveEvents([event]); + room.addLiveEvents([event], { addToState: true }); }); it("an unthreaded receipt for the event makes the room read", () => { @@ -502,7 +502,7 @@ describe("Unread", () => { ts: 100, currentUserId: myId, }); - room.addLiveEvents(events); + room.addLiveEvents(events, { addToState: true }); threadEvent = events[1]; }); @@ -555,7 +555,7 @@ describe("Unread", () => { room: roomId, content: {}, }); - room.addLiveEvents([event]); + room.addLiveEvents([event], { addToState: true }); // It still returns false expect(doesRoomHaveUnreadThreads(room)).toBe(false); diff --git a/test/unit-tests/__snapshots__/LegacyCallHandler-test.ts.snap b/test/unit-tests/__snapshots__/LegacyCallHandler-test.ts.snap deleted file mode 100644 index aaf4d787584..00000000000 --- a/test/unit-tests/__snapshots__/LegacyCallHandler-test.ts.snap +++ /dev/null @@ -1,24 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LegacyCallHandler when recording a voice broadcast and placing a call should show the info dialog 1`] = ` -[MockFunction] { - "calls": [ - [ - [Function], - { - "description":

- You can’t start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call. -

, - "hasCloseButton": true, - "title": "Can’t start a call", - }, - ], - ], - "results": [ - { - "type": "return", - "value": undefined, - }, - ], -} -`; diff --git a/test/unit-tests/components/structures/FilePanel-test.tsx b/test/unit-tests/components/structures/FilePanel-test.tsx index 1dce2206829..25bdd996768 100644 --- a/test/unit-tests/components/structures/FilePanel-test.tsx +++ b/test/unit-tests/components/structures/FilePanel-test.tsx @@ -7,13 +7,13 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; -import { EventTimelineSet, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix"; +import { EventTimelineSet, PendingEventOrdering, Room, RoomEvent } from "matrix-js-sdk/src/matrix"; import { screen, render, waitFor } from "jest-matrix-react"; import { mocked } from "jest-mock"; import FilePanel from "../../../../src/components/structures/FilePanel"; import ResizeNotifier from "../../../../src/utils/ResizeNotifier"; -import { stubClient } from "../../../test-utils"; +import { mkEvent, stubClient } from "../../../test-utils"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; jest.mock("matrix-js-sdk/src/matrix", () => ({ @@ -47,4 +47,43 @@ describe("FilePanel", () => { }); expect(asFragment()).toMatchSnapshot(); }); + + describe("addEncryptedLiveEvent", () => { + it("should add file msgtype event to filtered timelineSet", async () => { + const cli = MatrixClientPeg.safeGet(); + const room = new Room("!room:server", cli, cli.getSafeUserId(), { + pendingEventOrdering: PendingEventOrdering.Detached, + }); + cli.reEmitter.reEmit(room, [RoomEvent.Timeline]); + const timelineSet = new EventTimelineSet(room); + room.getOrCreateFilteredTimelineSet = jest.fn().mockReturnValue(timelineSet); + mocked(cli.getRoom).mockReturnValue(room); + + let filePanel: FilePanel | null; + render( + (filePanel = ref)} + />, + ); + await screen.findByText("No files visible in this room"); + + const event = mkEvent({ + type: "m.room.message", + user: cli.getSafeUserId(), + room: room.roomId, + content: { + body: "hello", + url: "mxc://matrix.org/1234", + msgtype: "m.file", + }, + event: true, + }); + filePanel!.addEncryptedLiveEvent(event); + + expect(timelineSet.getLiveTimeline().getEvents()).toContain(event); + }); + }); }); diff --git a/test/unit-tests/components/structures/MatrixChat-test.tsx b/test/unit-tests/components/structures/MatrixChat-test.tsx index 28bf99fa978..fd17ccf5838 100644 --- a/test/unit-tests/components/structures/MatrixChat-test.tsx +++ b/test/unit-tests/components/structures/MatrixChat-test.tsx @@ -44,7 +44,6 @@ import { } from "../../../test-utils"; import * as leaveRoomUtils from "../../../../src/utils/leave-behaviour"; import { OidcClientError } from "../../../../src/utils/oidc/error"; -import * as voiceBroadcastUtils from "../../../../src/voice-broadcast/utils/cleanUpBroadcasts"; import LegacyCallHandler from "../../../../src/LegacyCallHandler"; import { CallStore } from "../../../../src/stores/CallStore"; import { Call } from "../../../../src/models/Call"; @@ -811,7 +810,6 @@ describe("", () => { jest.spyOn(LegacyCallHandler.instance, "hangupAllCalls") .mockClear() .mockImplementation(() => {}); - jest.spyOn(voiceBroadcastUtils, "cleanUpBroadcasts").mockImplementation(async () => {}); jest.spyOn(PosthogAnalytics.instance, "logout").mockImplementation(() => {}); jest.spyOn(EventIndexPeg, "deleteEventIndex").mockImplementation(async () => {}); @@ -831,22 +829,12 @@ describe("", () => { jest.spyOn(logger, "warn").mockClear(); }); - afterAll(() => { - jest.spyOn(voiceBroadcastUtils, "cleanUpBroadcasts").mockRestore(); - }); - it("should hangup all legacy calls", async () => { await getComponentAndWaitForReady(); await dispatchLogoutAndWait(); expect(LegacyCallHandler.instance.hangupAllCalls).toHaveBeenCalled(); }); - it("should cleanup broadcasts", async () => { - await getComponentAndWaitForReady(); - await dispatchLogoutAndWait(); - expect(voiceBroadcastUtils.cleanUpBroadcasts).toHaveBeenCalled(); - }); - it("should disconnect all calls", async () => { await getComponentAndWaitForReady(); await dispatchLogoutAndWait(); diff --git a/test/unit-tests/components/structures/MessagePanel-test.tsx b/test/unit-tests/components/structures/MessagePanel-test.tsx index cf44716ba9e..dbb83da3124 100644 --- a/test/unit-tests/components/structures/MessagePanel-test.tsx +++ b/test/unit-tests/components/structures/MessagePanel-test.tsx @@ -30,6 +30,7 @@ import { import ResizeNotifier from "../../../../src/utils/ResizeNotifier"; import { IRoomState } from "../../../../src/components/structures/RoomView"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; +import { ScopedRoomContextProvider } from "../../../../src/contexts/ScopedRoomContext.tsx"; jest.mock("../../../../src/utils/beacon", () => ({ useBeacon: jest.fn(), @@ -91,9 +92,9 @@ describe("MessagePanel", function () { const getComponent = (props = {}, roomContext: Partial = {}) => ( - + - + ); diff --git a/test/unit-tests/components/structures/PipContainer-test.tsx b/test/unit-tests/components/structures/PipContainer-test.tsx index 446727c74e2..f573b0a0cde 100644 --- a/test/unit-tests/components/structures/PipContainer-test.tsx +++ b/test/unit-tests/components/structures/PipContainer-test.tsx @@ -10,7 +10,7 @@ import React from "react"; import { mocked, Mocked } from "jest-mock"; import { screen, render, act, cleanup } from "jest-matrix-react"; import userEvent from "@testing-library/user-event"; -import { MatrixClient, PendingEventOrdering, Room, MatrixEvent, RoomStateEvent } from "matrix-js-sdk/src/matrix"; +import { MatrixClient, PendingEventOrdering, Room, RoomStateEvent } from "matrix-js-sdk/src/matrix"; import { Widget, ClientWidgetApi } from "matrix-widget-api"; import { UserEvent } from "@testing-library/user-event/dist/types/setup/setup"; @@ -26,7 +26,6 @@ import { wrapInSdkContext, mkRoomCreateEvent, mockPlatformPeg, - flushPromises, useMockMediaDevices, } from "../../../test-utils"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; @@ -39,17 +38,7 @@ import defaultDispatcher from "../../../../src/dispatcher/dispatcher"; import { Action } from "../../../../src/dispatcher/actions"; import { ViewRoomPayload } from "../../../../src/dispatcher/payloads/ViewRoomPayload"; import { TestSdkContext } from "../../TestSdkContext"; -import { - VoiceBroadcastInfoState, - VoiceBroadcastPlaybacksStore, - VoiceBroadcastPreRecording, - VoiceBroadcastPreRecordingStore, - VoiceBroadcastRecording, - VoiceBroadcastRecordingsStore, -} from "../../../../src/voice-broadcast"; -import { mkVoiceBroadcastInfoStateEvent } from "../../voice-broadcast/utils/test-utils"; import { RoomViewStore } from "../../../../src/stores/RoomViewStore"; -import { IRoomStateEventsActionPayload } from "../../../../src/actions/MatrixActionCreators"; import { Container, WidgetLayoutStore } from "../../../../src/stores/widgets/WidgetLayoutStore"; import WidgetStore from "../../../../src/stores/WidgetStore"; import { WidgetType } from "../../../../src/widgets/WidgetType"; @@ -76,13 +65,6 @@ describe("PipContainer", () => { let room: Room; let room2: Room; let alice: RoomMember; - let voiceBroadcastRecordingsStore: VoiceBroadcastRecordingsStore; - let voiceBroadcastPreRecordingStore: VoiceBroadcastPreRecordingStore; - let voiceBroadcastPlaybacksStore: VoiceBroadcastPlaybacksStore; - - const actFlushPromises = async () => { - await flushPromises(); - }; beforeEach(async () => { useMockMediaDevices(); @@ -125,13 +107,7 @@ describe("PipContainer", () => { sdkContext = new TestSdkContext(); // @ts-ignore PipContainer uses SDKContext in the constructor SdkContextClass.instance = sdkContext; - voiceBroadcastRecordingsStore = new VoiceBroadcastRecordingsStore(); - voiceBroadcastPreRecordingStore = new VoiceBroadcastPreRecordingStore(); - voiceBroadcastPlaybacksStore = new VoiceBroadcastPlaybacksStore(voiceBroadcastRecordingsStore); sdkContext.client = client; - sdkContext._VoiceBroadcastRecordingsStore = voiceBroadcastRecordingsStore; - sdkContext._VoiceBroadcastPreRecordingStore = voiceBroadcastPreRecordingStore; - sdkContext._VoiceBroadcastPlaybacksStore = voiceBroadcastPlaybacksStore; }); afterEach(async () => { @@ -190,51 +166,10 @@ describe("PipContainer", () => { ActiveWidgetStore.instance.destroyPersistentWidget("1", room.roomId); }; - const makeVoiceBroadcastInfoStateEvent = (): MatrixEvent => { - return mkVoiceBroadcastInfoStateEvent( - room.roomId, - VoiceBroadcastInfoState.Started, - alice.userId, - client.getDeviceId() || "", - ); - }; - - const setUpVoiceBroadcastRecording = () => { - const infoEvent = makeVoiceBroadcastInfoStateEvent(); - const voiceBroadcastRecording = new VoiceBroadcastRecording(infoEvent, client); - voiceBroadcastRecordingsStore.setCurrent(voiceBroadcastRecording); - }; - - const setUpVoiceBroadcastPreRecording = () => { - const voiceBroadcastPreRecording = new VoiceBroadcastPreRecording( - room, - alice, - client, - voiceBroadcastPlaybacksStore, - voiceBroadcastRecordingsStore, - ); - voiceBroadcastPreRecordingStore.setCurrent(voiceBroadcastPreRecording); - }; - const setUpRoomViewStore = () => { sdkContext._RoomViewStore = new RoomViewStore(defaultDispatcher, sdkContext); }; - const mkVoiceBroadcast = (room: Room): MatrixEvent => { - const infoEvent = makeVoiceBroadcastInfoStateEvent(); - room.currentState.setStateEvents([infoEvent]); - defaultDispatcher.dispatch( - { - action: "MatrixActions.RoomState.events", - event: infoEvent, - state: room.currentState, - lastStateEvent: null, - }, - true, - ); - return infoEvent; - }; - it("hides if there's no content", () => { renderPip(); expect(screen.queryByRole("complementary")).toBeNull(); @@ -339,138 +274,4 @@ describe("PipContainer", () => { WidgetStore.instance.removeVirtualWidget("1", room.roomId); }); - - describe("when there is a voice broadcast recording and pre-recording", () => { - beforeEach(async () => { - setUpVoiceBroadcastPreRecording(); - setUpVoiceBroadcastRecording(); - renderPip(); - await actFlushPromises(); - }); - - it("should render the voice broadcast recording PiP", () => { - // check for the „Live“ badge to be present - expect(screen.queryByText("Live")).toBeInTheDocument(); - }); - - it("and a call it should show both, the call and the recording", async () => { - await withCall(async () => { - // Broadcast: Check for the „Live“ badge to be present - expect(screen.queryByText("Live")).toBeInTheDocument(); - // Call: Check for the „Leave“ button to be present - screen.getByRole("button", { name: "Leave" }); - }); - }); - }); - - describe("when there is a voice broadcast playback and pre-recording", () => { - beforeEach(async () => { - mkVoiceBroadcast(room); - setUpVoiceBroadcastPreRecording(); - renderPip(); - await actFlushPromises(); - }); - - it("should render the voice broadcast pre-recording PiP", () => { - // check for the „Go live“ button - expect(screen.queryByText("Go live")).toBeInTheDocument(); - }); - }); - - describe("when there is a voice broadcast pre-recording", () => { - beforeEach(async () => { - setUpVoiceBroadcastPreRecording(); - renderPip(); - await actFlushPromises(); - }); - - it("should render the voice broadcast pre-recording PiP", () => { - // check for the „Go live“ button - expect(screen.queryByText("Go live")).toBeInTheDocument(); - }); - }); - - describe("when listening to a voice broadcast in a room and then switching to another room", () => { - beforeEach(async () => { - setUpRoomViewStore(); - viewRoom(room.roomId); - mkVoiceBroadcast(room); - await actFlushPromises(); - - expect(voiceBroadcastPlaybacksStore.getCurrent()).toBeTruthy(); - - await voiceBroadcastPlaybacksStore.getCurrent()?.start(); - viewRoom(room2.roomId); - renderPip(); - }); - - it("should render the small voice broadcast playback PiP", () => { - // check for the „pause voice broadcast“ button - expect(screen.getByLabelText("pause voice broadcast")).toBeInTheDocument(); - // check for the absence of the „30s forward“ button - expect(screen.queryByLabelText("30s forward")).not.toBeInTheDocument(); - }); - }); - - describe("when viewing a room with a live voice broadcast", () => { - let startEvent!: MatrixEvent; - - beforeEach(async () => { - setUpRoomViewStore(); - viewRoom(room.roomId); - startEvent = mkVoiceBroadcast(room); - renderPip(); - await actFlushPromises(); - }); - - it("should render the voice broadcast playback pip", () => { - // check for the „resume voice broadcast“ button - expect(screen.queryByLabelText("play voice broadcast")).toBeInTheDocument(); - }); - - describe("and the broadcast stops", () => { - beforeEach(async () => { - const stopEvent = mkVoiceBroadcastInfoStateEvent( - room.roomId, - VoiceBroadcastInfoState.Stopped, - alice.userId, - client.getDeviceId() || "", - startEvent, - ); - - await act(async () => { - room.currentState.setStateEvents([stopEvent]); - defaultDispatcher.dispatch( - { - action: "MatrixActions.RoomState.events", - event: stopEvent, - state: room.currentState, - lastStateEvent: stopEvent, - }, - true, - ); - await flushPromises(); - }); - }); - - it("should not render the voice broadcast playback pip", () => { - // check for the „resume voice broadcast“ button - expect(screen.queryByLabelText("play voice broadcast")).not.toBeInTheDocument(); - }); - }); - - describe("and leaving the room", () => { - beforeEach(async () => { - await act(async () => { - viewRoom(room2.roomId); - await flushPromises(); - }); - }); - - it("should not render the voice broadcast playback pip", () => { - // check for the „resume voice broadcast“ button - expect(screen.queryByLabelText("play voice broadcast")).not.toBeInTheDocument(); - }); - }); - }); }); diff --git a/test/unit-tests/components/structures/RightPanel-test.tsx b/test/unit-tests/components/structures/RightPanel-test.tsx index e569369db54..ad29791ee9f 100644 --- a/test/unit-tests/components/structures/RightPanel-test.tsx +++ b/test/unit-tests/components/structures/RightPanel-test.tsx @@ -91,7 +91,7 @@ describe("RightPanel", () => { if (name !== "RightPanel.phases") return realGetValue(name, roomId); if (roomId === "r1") { return { - history: [{ phase: RightPanelPhases.RoomMemberList }], + history: [{ phase: RightPanelPhases.MemberList }], isOpen: true, }; } @@ -123,7 +123,7 @@ describe("RightPanel", () => { await rpsUpdated; await waitFor(() => expect(screen.queryByTestId("spinner")).not.toBeInTheDocument()); - // room one will be in the RoomMemberList phase - confirm this is rendered + // room one will be in the MemberList phase - confirm this is rendered expect(container.getElementsByClassName("mx_MemberList")).toHaveLength(1); // wait for RPS room 2 updates to fire, then rerender diff --git a/test/unit-tests/components/structures/RoomView-test.tsx b/test/unit-tests/components/structures/RoomView-test.tsx index b6fbd2e8504..385204c01b3 100644 --- a/test/unit-tests/components/structures/RoomView-test.tsx +++ b/test/unit-tests/components/structures/RoomView-test.tsx @@ -10,18 +10,19 @@ import React, { createRef, RefObject } from "react"; import { mocked, MockedObject } from "jest-mock"; import { ClientEvent, - MatrixClient, - Room, - RoomEvent, + EventTimeline, EventType, + IEvent, JoinRule, + MatrixClient, MatrixError, - RoomStateEvent, MatrixEvent, + Room, + RoomEvent, + RoomStateEvent, SearchResult, - IEvent, } from "matrix-js-sdk/src/matrix"; -import { CryptoApi, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api"; +import { CryptoApi, UserVerificationStatus, CryptoEvent } from "matrix-js-sdk/src/crypto-api"; import { KnownMembership } from "matrix-js-sdk/src/types"; import { fireEvent, @@ -34,6 +35,7 @@ import { cleanup, } from "jest-matrix-react"; import userEvent from "@testing-library/user-event"; +import { defer } from "matrix-js-sdk/src/utils"; import { stubClient, @@ -73,6 +75,7 @@ import { ViewRoomErrorPayload } from "../../../../src/dispatcher/payloads/ViewRo import { SearchScope } from "../../../../src/Searching"; import { MEGOLM_ENCRYPTION_ALGORITHM } from "../../../../src/utils/crypto"; import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; +import { ViewUserPayload } from "../../../../src/dispatcher/payloads/ViewUserPayload.ts"; describe("RoomView", () => { let cli: MockedObject; @@ -87,8 +90,7 @@ describe("RoomView", () => { beforeEach(() => { mockPlatformPeg({ reload: () => {} }); - stubClient(); - cli = mocked(MatrixClientPeg.safeGet()); + cli = mocked(stubClient()); room = new Room(`!${roomCount++}:example.org`, cli, "@alice:example.org"); jest.spyOn(room, "findPredecessor"); @@ -201,6 +203,21 @@ describe("RoomView", () => { return ref.current!; }; + it("should show member list right panel phase on Action.ViewUser without `payload.member`", async () => { + const spy = jest.spyOn(stores.rightPanelStore, "showOrHidePhase"); + await renderRoomView(false); + + defaultDispatcher.dispatch( + { + action: Action.ViewUser, + member: undefined, + }, + true, + ); + + expect(spy).toHaveBeenCalledWith(RightPanelPhases.MemberList); + }); + it("when there is no room predecessor, getHiddenHighlightCount should return 0", async () => { const instance = await getRoomViewInstance(); expect(instance.getHiddenHighlightCount()).toBe(0); @@ -247,8 +264,9 @@ describe("RoomView", () => { it("updates url preview visibility on encryption state change", async () => { room.getMyMembership = jest.fn().mockReturnValue(KnownMembership.Join); + jest.spyOn(cli, "getCrypto").mockReturnValue(crypto); // we should be starting unencrypted - expect(cli.isRoomEncrypted(room.roomId)).toEqual(false); + expect(await cli.getCrypto()?.isEncryptionEnabledInRoom(room.roomId)).toEqual(false); const roomViewInstance = await getRoomViewInstance(); @@ -263,23 +281,38 @@ describe("RoomView", () => { expect(roomViewInstance.state.showUrlPreview).toBe(true); // now enable encryption - cli.isRoomEncrypted.mockReturnValue(true); + jest.spyOn(cli.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true); // and fake an encryption event into the room to prompt it to re-check - await act(() => - room.addLiveEvents([ - new MatrixEvent({ - type: "m.room.encryption", - sender: cli.getUserId()!, - content: {}, - event_id: "someid", - room_id: room.roomId, - }), - ]), - ); + act(() => { + const encryptionEvent = new MatrixEvent({ + type: EventType.RoomEncryption, + sender: cli.getUserId()!, + content: {}, + event_id: "someid", + room_id: room.roomId, + }); + const roomState = room.getLiveTimeline().getState(EventTimeline.FORWARDS)!; + cli.emit(RoomStateEvent.Events, encryptionEvent, roomState, null); + }); // URL previews should now be disabled - expect(roomViewInstance.state.showUrlPreview).toBe(false); + await waitFor(() => expect(roomViewInstance.state.showUrlPreview).toBe(false)); + }); + + it("should not display the timeline when the room encryption is loading", async () => { + jest.spyOn(room, "getMyMembership").mockReturnValue(KnownMembership.Join); + jest.spyOn(cli, "getCrypto").mockReturnValue(crypto); + const deferred = defer(); + jest.spyOn(cli.getCrypto()!, "isEncryptionEnabledInRoom").mockImplementation(() => deferred.promise); + + const { asFragment, container } = await mountRoomView(); + expect(container.querySelector(".mx_RoomView_messagePanel")).toBeNull(); + expect(asFragment()).toMatchSnapshot(); + + deferred.resolve(true); + await waitFor(() => expect(container.querySelector(".mx_RoomView_messagePanel")).not.toBeNull()); + expect(asFragment()).toMatchSnapshot(); }); it("updates live timeline when a timeline reset happens", async () => { @@ -290,6 +323,32 @@ describe("RoomView", () => { expect(roomViewInstance.state.liveTimeline).not.toEqual(oldTimeline); }); + it("should update when the e2e status when the user verification changed", async () => { + room.currentState.setStateEvents([ + mkRoomMemberJoinEvent(cli.getSafeUserId(), room.roomId), + mkRoomMemberJoinEvent("user@example.com", room.roomId), + ]); + room.getMyMembership = jest.fn().mockReturnValue(KnownMembership.Join); + // Not all the calls to cli.isRoomEncrypted are migrated, so we need to mock both. + mocked(cli.isRoomEncrypted).mockReturnValue(true); + jest.spyOn(cli, "getCrypto").mockReturnValue(crypto); + jest.spyOn(cli.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true); + jest.spyOn(cli.getCrypto()!, "getUserVerificationStatus").mockResolvedValue( + new UserVerificationStatus(false, false, false), + ); + jest.spyOn(cli.getCrypto()!, "getUserDeviceInfo").mockResolvedValue( + new Map([["user@example.com", new Map()]]), + ); + + const { container } = await renderRoomView(); + await waitFor(() => expect(container.querySelector(".mx_E2EIcon_normal")).toBeInTheDocument()); + + const verificationStatus = new UserVerificationStatus(true, true, false); + jest.spyOn(cli.getCrypto()!, "getUserVerificationStatus").mockResolvedValue(verificationStatus); + cli.emit(CryptoEvent.UserTrustStatusChanged, cli.getSafeUserId(), verificationStatus); + await waitFor(() => expect(container.querySelector(".mx_E2EIcon_verified")).toBeInTheDocument()); + }); + describe("with virtual rooms", () => { it("checks for a virtual room on initial load", async () => { const { container } = await renderRoomView(); @@ -427,7 +486,8 @@ describe("RoomView", () => { ]); jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(cli.getSafeUserId()); jest.spyOn(DMRoomMap.shared(), "getRoomIds").mockReturnValue(new Set([room.roomId])); - mocked(cli).isRoomEncrypted.mockReturnValue(true); + jest.spyOn(cli, "getCrypto").mockReturnValue(crypto); + jest.spyOn(cli.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true); await renderRoomView(); }); @@ -653,7 +713,7 @@ describe("RoomView", () => { skey: id, ts, }); - room.addLiveEvents([widgetEvent]); + room.addLiveEvents([widgetEvent], { addToState: false }); room.currentState.setStateEvents([widgetEvent]); cli.emit(RoomStateEvent.Events, widgetEvent, room.currentState, null); await flushPromises(); diff --git a/test/unit-tests/components/structures/SpaceRoomView-test.tsx b/test/unit-tests/components/structures/SpaceRoomView-test.tsx new file mode 100644 index 00000000000..fb246032834 --- /dev/null +++ b/test/unit-tests/components/structures/SpaceRoomView-test.tsx @@ -0,0 +1,117 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import React from "react"; +import { mocked, MockedObject } from "jest-mock"; +import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; +import { render, cleanup, screen, fireEvent } from "jest-matrix-react"; + +import { stubClient, mockPlatformPeg, unmockPlatformPeg, withClientContextRenderOptions } from "../../../test-utils"; +import { RightPanelPhases } from "../../../../src/stores/right-panel/RightPanelStorePhases"; +import SpaceRoomView from "../../../../src/components/structures/SpaceRoomView.tsx"; +import ResizeNotifier from "../../../../src/utils/ResizeNotifier.ts"; +import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks.ts"; +import RightPanelStore from "../../../../src/stores/right-panel/RightPanelStore.ts"; +import DMRoomMap from "../../../../src/utils/DMRoomMap.ts"; + +describe("SpaceRoomView", () => { + let cli: MockedObject; + let space: Room; + + beforeEach(() => { + mockPlatformPeg({ reload: () => {} }); + cli = mocked(stubClient()); + + space = new Room(`!space:example.org`, cli, cli.getSafeUserId()); + space.currentState.setStateEvents([ + new MatrixEvent({ + type: "m.room.create", + room_id: space.roomId, + sender: cli.getSafeUserId(), + state_key: "", + content: { + creator: cli.getSafeUserId(), + type: "m.space", + }, + }), + new MatrixEvent({ + type: "m.room.member", + room_id: space.roomId, + sender: cli.getSafeUserId(), + state_key: cli.getSafeUserId(), + content: { + membership: "join", + }, + }), + new MatrixEvent({ + type: "m.room.member", + room_id: space.roomId, + sender: "@userA:server", + state_key: "@userA:server", + content: { + membership: "join", + }, + }), + new MatrixEvent({ + type: "m.room.member", + room_id: space.roomId, + sender: "@userB:server", + state_key: "@userB:server", + content: { + membership: "join", + }, + }), + new MatrixEvent({ + type: "m.room.member", + room_id: space.roomId, + sender: "@userC:server", + state_key: "@userC:server", + content: { + membership: "join", + }, + }), + ]); + space.updateMyMembership("join"); + + DMRoomMap.makeShared(cli); + }); + + afterEach(() => { + unmockPlatformPeg(); + jest.clearAllMocks(); + cleanup(); + }); + + const renderSpaceRoomView = async (): Promise> => { + const resizeNotifier = new ResizeNotifier(); + const permalinkCreator = new RoomPermalinkCreator(space); + + const spaceRoomView = render( + , + withClientContextRenderOptions(cli), + ); + return spaceRoomView; + }; + + describe("SpaceLanding", () => { + it("should show member list right panel phase on members click on landing", async () => { + const spy = jest.spyOn(RightPanelStore.instance, "setCard"); + const { container } = await renderSpaceRoomView(); + + await expect(screen.findByText("Welcome to")).resolves.toBeVisible(); + fireEvent.click(container.querySelector(".mx_FacePile")!); + + expect(spy).toHaveBeenCalledWith({ phase: RightPanelPhases.MemberList }); + }); + }); +}); diff --git a/test/unit-tests/components/structures/ThreadPanel-test.tsx b/test/unit-tests/components/structures/ThreadPanel-test.tsx index c19127de259..20fc7081032 100644 --- a/test/unit-tests/components/structures/ThreadPanel-test.tsx +++ b/test/unit-tests/components/structures/ThreadPanel-test.tsx @@ -20,7 +20,6 @@ import { import ThreadPanel, { ThreadFilterType, ThreadPanelHeader } from "../../../../src/components/structures/ThreadPanel"; import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; -import RoomContext from "../../../../src/contexts/RoomContext"; import { _t } from "../../../../src/languageHandler"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks"; @@ -28,6 +27,7 @@ import ResizeNotifier from "../../../../src/utils/ResizeNotifier"; import { createTestClient, getRoomContext, mkRoom, mockPlatformPeg, stubClient } from "../../../test-utils"; import { mkThread } from "../../../test-utils/threads"; import { IRoomState } from "../../../../src/components/structures/RoomView"; +import { ScopedRoomContextProvider } from "../../../../src/contexts/ScopedRoomContext.tsx"; jest.mock("../../../../src/utils/Feedback"); @@ -81,11 +81,11 @@ describe("ThreadPanel", () => { room: mockRoom, } as unknown as IRoomState; const { container } = render( - + undefined} /> - , + , ); fireEvent.click(getByRole(container, "button", { name: "Mark all as read" })); await waitFor(() => @@ -114,8 +114,8 @@ describe("ThreadPanel", () => { const TestThreadPanel = () => ( - @@ -125,7 +125,7 @@ describe("ThreadPanel", () => { resizeNotifier={new ResizeNotifier()} permalinkCreator={new RoomPermalinkCreator(room)} /> - + ); @@ -209,11 +209,11 @@ describe("ThreadPanel", () => { return event ? Promise.resolve(event) : Promise.reject(); }); const [allThreads, myThreads] = room.threadsTimelineSets; - allThreads!.addLiveEvent(otherThread.rootEvent); - allThreads!.addLiveEvent(mixedThread.rootEvent); - allThreads!.addLiveEvent(ownThread.rootEvent); - myThreads!.addLiveEvent(mixedThread.rootEvent); - myThreads!.addLiveEvent(ownThread.rootEvent); + allThreads!.addLiveEvent(otherThread.rootEvent, { addToState: true }); + allThreads!.addLiveEvent(mixedThread.rootEvent, { addToState: true }); + allThreads!.addLiveEvent(ownThread.rootEvent, { addToState: true }); + myThreads!.addLiveEvent(mixedThread.rootEvent, { addToState: true }); + myThreads!.addLiveEvent(ownThread.rootEvent, { addToState: true }); const renderResult = render(); await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy()); @@ -258,7 +258,7 @@ describe("ThreadPanel", () => { return event ? Promise.resolve(event) : Promise.reject(); }); const [allThreads] = room.threadsTimelineSets; - allThreads!.addLiveEvent(otherThread.rootEvent); + allThreads!.addLiveEvent(otherThread.rootEvent, { addToState: true }); const renderResult = render(); await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy()); diff --git a/test/unit-tests/components/structures/ThreadView-test.tsx b/test/unit-tests/components/structures/ThreadView-test.tsx index 697fd251810..ee4afff525b 100644 --- a/test/unit-tests/components/structures/ThreadView-test.tsx +++ b/test/unit-tests/components/structures/ThreadView-test.tsx @@ -23,7 +23,6 @@ import React, { useState } from "react"; import ThreadView from "../../../../src/components/structures/ThreadView"; import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; -import RoomContext from "../../../../src/contexts/RoomContext"; import { SdkContextClass } from "../../../../src/contexts/SDKContext"; import { Action } from "../../../../src/dispatcher/actions"; import dispatcher from "../../../../src/dispatcher/dispatcher"; @@ -34,6 +33,7 @@ import { mockPlatformPeg } from "../../../test-utils/platform"; import { getRoomContext } from "../../../test-utils/room"; import { mkMessage, stubClient } from "../../../test-utils/test-utils"; import { mkThread } from "../../../test-utils/threads"; +import { ScopedRoomContextProvider } from "../../../../src/contexts/ScopedRoomContext.tsx"; describe("ThreadView", () => { const ROOM_ID = "!roomId:example.org"; @@ -51,8 +51,8 @@ describe("ThreadView", () => { return ( - @@ -63,7 +63,7 @@ describe("ThreadView", () => { initialEvent={initialEvent} resizeNotifier={new ResizeNotifier()} /> - + , ); diff --git a/test/unit-tests/components/structures/TimelinePanel-test.tsx b/test/unit-tests/components/structures/TimelinePanel-test.tsx index 2f858430001..442ed1c1d28 100644 --- a/test/unit-tests/components/structures/TimelinePanel-test.tsx +++ b/test/unit-tests/components/structures/TimelinePanel-test.tsx @@ -66,7 +66,7 @@ const mkTimeline = (room: Room, events: MatrixEvent[]): [EventTimeline, EventTim getPendingEvents: () => [] as MatrixEvent[], } as unknown as EventTimelineSet; const timeline = new EventTimeline(timelineSet); - events.forEach((event) => timeline.addEvent(event, { toStartOfTimeline: false })); + events.forEach((event) => timeline.addEvent(event, { toStartOfTimeline: false, addToState: true })); return [timeline, timelineSet]; }; @@ -150,9 +150,11 @@ const setupPagination = ( mocked(client).paginateEventTimeline.mockImplementation(async (tl, { backwards }) => { if (tl === timeline) { if (backwards) { - forEachRight(previousPage ?? [], (event) => tl.addEvent(event, { toStartOfTimeline: true })); + forEachRight(previousPage ?? [], (event) => + tl.addEvent(event, { toStartOfTimeline: true, addToState: true }), + ); } else { - (nextPage ?? []).forEach((event) => tl.addEvent(event, { toStartOfTimeline: false })); + (nextPage ?? []).forEach((event) => tl.addEvent(event, { toStartOfTimeline: false, addToState: true })); } // Prevent any further pagination attempts in this direction tl.setPaginationToken(null, backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS); @@ -256,7 +258,7 @@ describe("TimelinePanel", () => { describe("and reading the timeline", () => { beforeEach(async () => { await renderTimelinePanel(); - timelineSet.addLiveEvent(ev1, {}); + timelineSet.addLiveEvent(ev1, { addToState: true }); await flushPromises(); // @ts-ignore await timelinePanel.sendReadReceipts(); @@ -284,11 +286,11 @@ describe("TimelinePanel", () => { }); it("and forgetting the read markers, should send the stored marker again", async () => { - timelineSet.addLiveEvent(ev2, {}); + timelineSet.addLiveEvent(ev2, { addToState: true }); // Add the event to the room as well as the timeline, so we can find it when we // call findEventById in getEventReadUpTo. This is odd because in our test // setup, timelineSet is not actually the timelineSet of the room. - await room.addLiveEvents([ev2], {}); + await room.addLiveEvents([ev2], { addToState: true }); room.addEphemeralEvents([newReceipt(ev2.getId()!, userId, 222, 200)]); await timelinePanel!.forgetReadMarker(); expect(client.setRoomReadMarkers).toHaveBeenCalledWith(roomId, ev2.getId()); @@ -314,7 +316,7 @@ describe("TimelinePanel", () => { it("should send a fully read marker and a private receipt", async () => { await renderTimelinePanel(); - act(() => timelineSet.addLiveEvent(ev1, {})); + act(() => timelineSet.addLiveEvent(ev1, { addToState: true })); await flushPromises(); // @ts-ignore @@ -361,7 +363,7 @@ describe("TimelinePanel", () => { it("should send receipts but no fully_read when reading the thread timeline", async () => { await renderTimelinePanel(); - act(() => timelineSet.addLiveEvent(threadEv1, {})); + act(() => timelineSet.addLiveEvent(threadEv1, { addToState: true })); await flushPromises(); // @ts-ignore @@ -871,7 +873,9 @@ describe("TimelinePanel", () => { // @ts-ignore thread.fetchEditsWhereNeeded = () => Promise.resolve(); await thread.addEvent(reply1, false, true); - await allThreads.getLiveTimeline().addEvent(thread.rootEvent!, { toStartOfTimeline: true }); + await allThreads + .getLiveTimeline() + .addEvent(thread.rootEvent!, { toStartOfTimeline: true, addToState: true }); const replyToEvent = jest.spyOn(thread, "replyToEvent", "get"); const dom = render( @@ -907,7 +911,9 @@ describe("TimelinePanel", () => { // @ts-ignore realThread.fetchEditsWhereNeeded = () => Promise.resolve(); await realThread.addEvent(reply1, true); - await allThreads.getLiveTimeline().addEvent(realThread.rootEvent!, { toStartOfTimeline: true }); + await allThreads + .getLiveTimeline() + .addEvent(realThread.rootEvent!, { toStartOfTimeline: true, addToState: true }); const replyToEvent = jest.spyOn(realThread, "replyToEvent", "get"); // @ts-ignore @@ -968,7 +974,9 @@ describe("TimelinePanel", () => { events.push(rootEvent); - events.forEach((event) => timelineSet.getLiveTimeline().addEvent(event, { toStartOfTimeline: true })); + events.forEach((event) => + timelineSet.getLiveTimeline().addEvent(event, { toStartOfTimeline: true, addToState: true }), + ); const roomMembership = mkMembership({ mship: KnownMembership.Join, @@ -988,7 +996,10 @@ describe("TimelinePanel", () => { jest.spyOn(roomState, "getMember").mockReturnValue(member); jest.spyOn(timelineSet.getLiveTimeline(), "getState").mockReturnValue(roomState); - timelineSet.addEventToTimeline(roomMembership, timelineSet.getLiveTimeline(), { toStartOfTimeline: false }); + timelineSet.addEventToTimeline(roomMembership, timelineSet.getLiveTimeline(), { + toStartOfTimeline: false, + addToState: true, + }); for (const event of events) { jest.spyOn(event, "isDecryptionFailure").mockReturnValue(true); diff --git a/test/unit-tests/components/structures/UserMenu-test.tsx b/test/unit-tests/components/structures/UserMenu-test.tsx index ac76aba2ade..907bf664b7f 100644 --- a/test/unit-tests/components/structures/UserMenu-test.tsx +++ b/test/unit-tests/components/structures/UserMenu-test.tsx @@ -7,20 +7,14 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; -import { act, render, RenderResult, screen, waitFor } from "jest-matrix-react"; -import { DEVICE_CODE_SCOPE, MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; +import { render, screen, waitFor } from "jest-matrix-react"; +import { DEVICE_CODE_SCOPE, MatrixClient, Room } from "matrix-js-sdk/src/matrix"; import { CryptoApi } from "matrix-js-sdk/src/crypto-api"; import { mocked } from "jest-mock"; import fetchMock from "fetch-mock-jest"; import UnwrappedUserMenu from "../../../../src/components/structures/UserMenu"; import { stubClient, wrapInSdkContext } from "../../../test-utils"; -import { - VoiceBroadcastInfoState, - VoiceBroadcastRecording, - VoiceBroadcastRecordingsStore, -} from "../../../../src/voice-broadcast"; -import { mkVoiceBroadcastInfoStateEvent } from "../../voice-broadcast/utils/test-utils"; import { TestSdkContext } from "../../TestSdkContext"; import defaultDispatcher from "../../../../src/dispatcher/dispatcher"; import LogoutDialog from "../../../../src/components/views/dialogs/LogoutDialog"; @@ -34,71 +28,12 @@ import { UserTab } from "../../../../src/components/views/dialogs/UserTab"; describe("", () => { let client: MatrixClient; - let renderResult: RenderResult; let sdkContext: TestSdkContext; beforeEach(() => { sdkContext = new TestSdkContext(); }); - describe(" when video broadcast", () => { - let voiceBroadcastInfoEvent: MatrixEvent; - let voiceBroadcastRecording: VoiceBroadcastRecording; - let voiceBroadcastRecordingsStore: VoiceBroadcastRecordingsStore; - - beforeAll(() => { - client = stubClient(); - voiceBroadcastInfoEvent = mkVoiceBroadcastInfoStateEvent( - "!room:example.com", - VoiceBroadcastInfoState.Started, - client.getUserId() || "", - client.getDeviceId() || "", - ); - }); - - beforeEach(() => { - voiceBroadcastRecordingsStore = new VoiceBroadcastRecordingsStore(); - sdkContext._VoiceBroadcastRecordingsStore = voiceBroadcastRecordingsStore; - - voiceBroadcastRecording = new VoiceBroadcastRecording(voiceBroadcastInfoEvent, client); - }); - - describe("when rendered", () => { - beforeEach(() => { - const UserMenu = wrapInSdkContext(UnwrappedUserMenu, sdkContext); - renderResult = render(); - }); - - it("should render as expected", () => { - expect(renderResult.container).toMatchSnapshot(); - }); - - describe("and a live voice broadcast starts", () => { - beforeEach(() => { - act(() => { - voiceBroadcastRecordingsStore.setCurrent(voiceBroadcastRecording); - }); - }); - - it("should render the live voice broadcast avatar addon", () => { - expect(renderResult.queryByTestId("user-menu-live-vb")).toBeInTheDocument(); - }); - - describe("and the broadcast ends", () => { - beforeEach(() => { - act(() => { - voiceBroadcastRecordingsStore.clearCurrent(); - }); - }); - - it("should not render the live voice broadcast avatar addon", () => { - expect(renderResult.queryByTestId("user-menu-live-vb")).not.toBeInTheDocument(); - }); - }); - }); - }); - }); - describe(" logout", () => { beforeEach(() => { client = stubClient(); @@ -106,7 +41,7 @@ describe("", () => { it("should logout directly if no crypto", async () => { const UserMenu = wrapInSdkContext(UnwrappedUserMenu, sdkContext); - renderResult = render(); + render(); mocked(client.getRooms).mockReturnValue([ { @@ -128,7 +63,7 @@ describe("", () => { it("should logout directly if no encrypted rooms", async () => { const UserMenu = wrapInSdkContext(UnwrappedUserMenu, sdkContext); - renderResult = render(); + render(); mocked(client.getRooms).mockReturnValue([ { @@ -152,7 +87,7 @@ describe("", () => { it("should show dialog if some encrypted rooms", async () => { const UserMenu = wrapInSdkContext(UnwrappedUserMenu, sdkContext); - renderResult = render(); + render(); mocked(client.getRooms).mockReturnValue([ { diff --git a/test/unit-tests/components/structures/__snapshots__/MatrixChat-test.tsx.snap b/test/unit-tests/components/structures/__snapshots__/MatrixChat-test.tsx.snap index 94c26783886..1bdbe016d41 100644 --- a/test/unit-tests/components/structures/__snapshots__/MatrixChat-test.tsx.snap +++ b/test/unit-tests/components/structures/__snapshots__/MatrixChat-test.tsx.snap @@ -314,7 +314,6 @@ exports[` with a soft-logged-out session should show the soft-logo class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary" role="button" tabindex="0" - type="submit" > Sign in
diff --git a/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap b/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap index 626f7f2b9b2..1e0ed2248b0 100644 --- a/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap +++ b/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap @@ -62,7 +62,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1 style="--cpd-icon-button-size: 100%;" >
`; +exports[`RoomView should not display the timeline when the room encryption is loading 1`] = ` + +
+