Merge branch 'develop' of https://github.com/vector-im/element-web into t3chguy/fix/25969

This commit is contained in:
Michael Telatynski 2024-12-02 16:56:22 +00:00
commit 0f3a057175
No known key found for this signature in database
GPG key ID: A2B008A5F49F5D0D
360 changed files with 1982 additions and 15489 deletions

View file

@ -2,7 +2,7 @@
## Checklist ## Checklist
- [ ] Tests written for new code (and old code if feasible). - [ ] Tests written for new code (and old code if feasible).
- [ ] New or updated `public`/`exported` symbols have accurate [TSDoc](https://tsdoc.org/) documentation. - [ ] New or updated `public`/`exported` symbols have accurate [TSDoc](https://tsdoc.org/) documentation.
- [ ] Linter and other CI checks pass. - [ ] Linter and other CI checks pass.
- [ ] I have licensed the changes to Element by completing the [Contributor License Agreement (CLA)](https://cla-assistant.io/element-hq/element-web) - [ ] I have licensed the changes to Element by completing the [Contributor License Agreement (CLA)](https://cla-assistant.io/element-hq/element-web)

View file

@ -19,8 +19,23 @@ on:
default: true default: true
permissions: {} # Uses ELEMENT_BOT_TOKEN instead permissions: {} # Uses ELEMENT_BOT_TOKEN instead
jobs: jobs:
checks:
name: Sanity checks
strategy:
matrix:
repo:
- matrix-org/matrix-js-sdk
- element-hq/element-web
- element-hq/element-desktop
uses: matrix-org/matrix-js-sdk/.github/workflows/release-checks.yml@develop
secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
with:
repository: ${{ matrix.repo }}
prepare: prepare:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: checks
env: env:
# The order is specified bottom-up to avoid any races for allchange # The order is specified bottom-up to avoid any races for allchange
REPOS: matrix-js-sdk element-web element-desktop REPOS: matrix-js-sdk element-web element-desktop

View file

@ -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: As for your PR description, it should include these things:
- References to any bugs fixed by the change (in GitHub's `Fixes` notation) - 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 - 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 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 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. 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 - Why didn't it work before? Why does it work now? What use cases does it
unlock? unlock?
- If you find yourself adding information on how the code works or why you - 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 chose to do it the way you did, make sure this information is instead
written as comments in the code itself. written as comments in the code itself.
- Sometimes a PR can change considerably as it is developed. In this case, - 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 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 the PR. (It can be helpful to retain the old content under a suitable
heading, for additional context.) heading, for additional context.)
- Include both **before** and **after** screenshots to easily compare and discuss - Include both **before** and **after** screenshots to easily compare and discuss
what's changing. what's changing.
- Include a step-by-step testing strategy so that a reviewer can check out the - 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. 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 - 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. why the change is necessary or how they might better understand and review it.
### Changelogs ### 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: This example is for Element Web. You can specify:
- element-web - element-web
- element-desktop - element-desktop
If your PR introduces a breaking change, use the `Notes` section in the same 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 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. 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. - `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-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-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. - `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 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. to add them: ask in the PR description or comments.

View file

@ -16,28 +16,28 @@ JS SDK](https://github.com/matrix-org/matrix-js-sdk).
Element has several tiers of support for different environments: Element has several tiers of support for different environments:
- Supported - Supported
- Definition: - Definition:
- Issues **actively triaged**, regressions **block** the release - Issues **actively triaged**, regressions **block** the release
- Last 2 major versions of Chrome, Firefox, and Edge on desktop OSes - Last 2 major versions of Chrome, Firefox, and Edge on desktop OSes
- Last 2 versions of Safari - Last 2 versions of Safari
- Latest release of official Element Desktop app on desktop OSes - Latest release of official Element Desktop app on desktop OSes
- Desktop OSes means macOS, Windows, and Linux versions for desktop devices - Desktop OSes means macOS, Windows, and Linux versions for desktop devices
that are actively supported by the OS vendor and receive security updates that are actively supported by the OS vendor and receive security updates
- Best effort - Best effort
- Definition: - Definition:
- Issues **accepted**, regressions **do not block** the release - Issues **accepted**, regressions **do not block** the release
- The wider Element Products(including Element Call and the Enterprise Server Suite) do still not officially support these browsers. - The wider Element Products(including Element Call and the Enterprise Server Suite) do still not officially support these browsers.
- The element web project and its contributors should keep the client functioning and gracefully degrade where other sibling features (E.g. Element Call) may not function. - The element web project and its contributors should keep the client functioning and gracefully degrade where other sibling features (E.g. Element Call) may not function.
- Last major release of Firefox ESR and Chrome/Edge Extended Stable - Last major release of Firefox ESR and Chrome/Edge Extended Stable
- Community Supported - Community Supported
- Definition: - Definition:
- Issues **accepted**, regressions **do not block** the release - Issues **accepted**, regressions **do not block** the release
- Community contributions are welcome to support these issues - Community contributions are welcome to support these issues
- Mobile web for current stable version of Chrome, Firefox, and Safari on Android, iOS, and iPadOS - Mobile web for current stable version of Chrome, Firefox, and Safari on Android, iOS, and iPadOS
- Not supported - Not supported
- Definition: Issues only affecting unsupported environments are **closed** - Definition: Issues only affecting unsupported environments are **closed**
- Everything else - Everything else
The period of support for these tiers should last until the releases specified above, plus 1 app release cycle(2 weeks). In the case of Firefox ESR this is extended further to allow it land in Debian Stable. The period of support for these tiers should last until the releases specified above, plus 1 app release cycle(2 weeks). In the case of Firefox ESR this is extended further to allow it land in Debian Stable.
@ -74,16 +74,16 @@ situation, but it's still not good practice to do it in the first place. See
Unless you have special requirements, you will want to add the following to Unless you have special requirements, you will want to add the following to
your web server configuration when hosting Element Web: your web server configuration when hosting Element Web:
- The `X-Frame-Options: SAMEORIGIN` header, to prevent Element Web from being - The `X-Frame-Options: SAMEORIGIN` header, to prevent Element Web from being
framed and protect from [clickjacking][owasp-clickjacking]. framed and protect from [clickjacking][owasp-clickjacking].
- The `frame-ancestors 'self'` directive to your `Content-Security-Policy` - The `frame-ancestors 'self'` directive to your `Content-Security-Policy`
header, as the modern replacement for `X-Frame-Options` (though both should be header, as the modern replacement for `X-Frame-Options` (though both should be
included since not all browsers support it yet, see included since not all browsers support it yet, see
[this][owasp-clickjacking-csp]). [this][owasp-clickjacking-csp]).
- The `X-Content-Type-Options: nosniff` header, to [disable MIME - The `X-Content-Type-Options: nosniff` header, to [disable MIME
sniffing][mime-sniffing]. sniffing][mime-sniffing].
- The `X-XSS-Protection: 1; mode=block;` header, for basic XSS protection in - The `X-XSS-Protection: 1; mode=block;` header, for basic XSS protection in
legacy browsers. legacy browsers.
[mime-sniffing]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#mime_sniffing [mime-sniffing]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#mime_sniffing
[owasp-clickjacking-csp]: https://cheatsheetseries.owasp.org/cheatsheets/Clickjacking_Defense_Cheat_Sheet.html#content-security-policy-frame-ancestors-examples [owasp-clickjacking-csp]: https://cheatsheetseries.owasp.org/cheatsheets/Clickjacking_Defense_Cheat_Sheet.html#content-security-policy-frame-ancestors-examples

View file

@ -3,9 +3,9 @@
This code style applies to projects which the element-web team directly maintains or is reasonably This code style applies to projects which the element-web team directly maintains or is reasonably
adjacent to. As of writing, these are: adjacent to. As of writing, these are:
- element-desktop - element-desktop
- element-web - element-web
- matrix-js-sdk - matrix-js-sdk
Other projects might extend this code style for increased strictness. For example, matrix-events-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 has stricter code organization to reduce the maintenance burden. These projects will declare their code

View file

@ -1,55 +1,55 @@
# Summary # Summary
- [Introduction](../README.md) - [Introduction](../README.md)
# Usage # Usage
- [Betas](betas.md) - [Betas](betas.md)
- [Labs](labs.md) - [Labs](labs.md)
# Setup # Setup
- [Install](install.md) - [Install](install.md)
- [Config](config.md) - [Config](config.md)
- [Custom home page](custom-home.md) - [Custom home page](custom-home.md)
- [Kubernetes](kubernetes.md) - [Kubernetes](kubernetes.md)
- [Jitsi](jitsi.md) - [Jitsi](jitsi.md)
- [Encryption](e2ee.md) - [Encryption](e2ee.md)
# Build # Build
- [Customisations](customisations.md) - [Customisations](customisations.md)
- [Modules](modules.md) - [Modules](modules.md)
- [Native Node modules](native-node-modules.md) - [Native Node modules](native-node-modules.md)
# Contribution # Contribution
- [Choosing an issue](choosing-an-issue.md) - [Choosing an issue](choosing-an-issue.md)
- [Translation](translating.md) - [Translation](translating.md)
- [Netlify builds](pr-previews.md) - [Netlify builds](pr-previews.md)
- [Code review](review.md) - [Code review](review.md)
# Development # Development
- [App load order](app-load.md) - [App load order](app-load.md)
- [Translation](translating-dev.md) - [Translation](translating-dev.md)
- [Theming](theming.md) - [Theming](theming.md)
- [Playwright end to end tests](playwright.md) - [Playwright end to end tests](playwright.md)
- [Memory profiling](memory-profiles-and-leaks.md) - [Memory profiling](memory-profiles-and-leaks.md)
- [Jitsi](jitsi-dev.md) - [Jitsi](jitsi-dev.md)
- [Feature flags](feature-flags.md) - [Feature flags](feature-flags.md)
- [OIDC and delegated authentication](oidc.md) - [OIDC and delegated authentication](oidc.md)
- [Release Process](release.md) - [Release Process](release.md)
# Deep dive # Deep dive
- [Skinning](skinning.md) - [Skinning](skinning.md)
- [Cider editor](ciderEditor.md) - [Cider editor](ciderEditor.md)
- [Iconography](icons.md) - [Iconography](icons.md)
- [Jitsi](jitsi.md) - [Jitsi](jitsi.md)
- [Local echo](local-echo-dev.md) - [Local echo](local-echo-dev.md)
- [Media](media-handling.md) - [Media](media-handling.md)
- [Room List Store](room-list-store.md) - [Room List Store](room-list-store.md)
- [Scrolling](scrolling.md) - [Scrolling](scrolling.md)
- [Usercontent](usercontent.md) - [Usercontent](usercontent.md)
- [Widget layouts](widget-layouts.md) - [Widget layouts](widget-layouts.md)

View file

@ -61,18 +61,18 @@ flowchart TD
Key: Key:
- Parallelogram: async/await task - Parallelogram: async/await task
- Box: sync task - Box: sync task
- Diamond: conditional branch - Diamond: conditional branch
- Circle: user interaction - Circle: user interaction
- Blue arrow: async task is allowed to settle but allowed to fail - Blue arrow: async task is allowed to settle but allowed to fail
- Red arrow: async task success is asserted - Red arrow: async task success is asserted
Notes: Notes:
- A task begins when all its dependencies (arrows going into it) are fulfilled. - 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). - 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. - 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: Underlying dependencies:

View file

@ -32,19 +32,19 @@ someone to add something.
When you're looking through the list, here are some things that might make an When you're looking through the list, here are some things that might make an
issue a **GOOD** choice: issue a **GOOD** choice:
- It is a problem or feature you care about. - It is a problem or feature you care about.
- It concerns a type of code you know a little about. - It concerns a type of code you know a little about.
- You think you can understand what's needed. - You think you can understand what's needed.
- It already has approval from Element Web's designers (look for comments from - It already has approval from Element Web's designers (look for comments from
members of the members of the
[Product](https://github.com/orgs/element-hq/teams/product/members) or [Product](https://github.com/orgs/element-hq/teams/product/members) or
[Design](https://github.com/orgs/element-hq/teams/design/members) teams). [Design](https://github.com/orgs/element-hq/teams/design/members) teams).
Here are some things that might make it a **BAD** choice: Here are some things that might make it a **BAD** choice:
- You don't understand it (maybe add a comment asking a clarifying question). - 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 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`.** - **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 **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 changes that require approval from one of those teams, you will probably have

View file

@ -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: 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. 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: Currently, the following UI feature flags are supported:
- `UIFeature.urlPreviews` - Whether URL previews are enabled across the entire application. - `UIFeature.urlPreviews` - Whether URL previews are enabled across the entire application.
- `UIFeature.feedback` - Whether prompts to supply feedback are shown. - `UIFeature.feedback` - Whether prompts to supply feedback are shown.
- `UIFeature.voip` - Whether or not VoIP is shown readily to the user. When disabled, - `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. Jitsi widgets will still work though they cannot easily be added.
- `UIFeature.widgets` - Whether or not widgets will be shown. - `UIFeature.widgets` - Whether or not widgets will be shown.
- `UIFeature.advancedSettings` - Whether or not sections titled "advanced" in room and - `UIFeature.advancedSettings` - Whether or not sections titled "advanced" in room and
user settings are shown to the user. user settings are shown to the user.
- `UIFeature.shareQrCode` - Whether or not the QR code on the share room/event dialog - `UIFeature.shareQrCode` - Whether or not the QR code on the share room/event dialog
is shown. is shown.
- `UIFeature.shareSocial` - Whether or not the social icons on the share room/event dialog - `UIFeature.shareSocial` - Whether or not the social icons on the share room/event dialog
are shown. are shown.
- `UIFeature.identityServer` - Whether or not functionality requiring an identity server - `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 is shown. When disabled, the user will not be able to interact with the identity
server (sharing email addresses, 3PID invites, etc). server (sharing email addresses, 3PID invites, etc).
- `UIFeature.thirdPartyId` - Whether or not UI relating to third party identifiers (3PIDs) - `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 is shown. Typically this is considered "contact information" on the homeserver, and is
not directly related to the identity server. not directly related to the identity server.
- `UIFeature.registration` - Whether or not the registration page is accessible. Typically - `UIFeature.registration` - Whether or not the registration page is accessible. Typically
useful if accounts are managed externally. useful if accounts are managed externally.
- `UIFeature.passwordReset` - Whether or not the password reset page is accessible. Typically - `UIFeature.passwordReset` - Whether or not the password reset page is accessible. Typically
useful if accounts are managed externally. useful if accounts are managed externally.
- `UIFeature.deactivate` - Whether or not the deactivate account button is accessible. Typically - `UIFeature.deactivate` - Whether or not the deactivate account button is accessible. Typically
useful if accounts are managed externally. useful if accounts are managed externally.
- `UIFeature.advancedEncryption` - Whether or not advanced encryption options are shown to the - `UIFeature.advancedEncryption` - Whether or not advanced encryption options are shown to the
user. user.
- `UIFeature.roomHistorySettings` - Whether or not the room history settings 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. 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 - `UIFeature.TimelineEnableRelativeDates` - Display relative date separators (eg: 'Today', 'Yesterday') in the
timeline for recent messages. When false day dates will be used. timeline for recent messages. When false day dates will be used.
- `UIFeature.BulkUnverifiedSessionsReminder` - Display popup reminders to verify or remove unverified sessions. Defaults - `UIFeature.BulkUnverifiedSessionsReminder` - Display popup reminders to verify or remove unverified sessions. Defaults
to true. to true.
- `UIFeature.locationSharing` - Whether or not location sharing menus will be shown. - `UIFeature.locationSharing` - Whether or not location sharing menus will be shown.
## Undocumented / developer options ## Undocumented / developer options
@ -592,4 +592,3 @@ The following are undocumented or intended for developer use only.
2. `sync_timeline_limit` 2. `sync_timeline_limit`
3. `dangerously_allow_unsafe_and_insecure_passwords` 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. 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.

View file

@ -50,9 +50,9 @@ that properties/state machines won't change.
UI for some actions can be hidden via the ComponentVisibility customisation: UI for some actions can be hidden via the ComponentVisibility customisation:
- inviting users to rooms and spaces, - inviting users to rooms and spaces,
- creating rooms, - creating rooms,
- creating spaces, - 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. 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.

View file

@ -31,9 +31,9 @@ Set the following on your homeserver's
When `force_disable` is true: When `force_disable` is true:
- all rooms will be created with encryption disabled, and it will not be possible to enable - all rooms will be created with encryption disabled, and it will not be possible to enable
encryption from room settings. encryption from room settings.
- any `io.element.e2ee.default` value will be disregarded. - any `io.element.e2ee.default` value will be disregarded.
Note: If the server is configured to forcibly enable encryption for some or all rooms, Note: If the server is configured to forcibly enable encryption for some or all rooms,
this behaviour will be overridden. this behaviour will be overridden.

View file

@ -5,10 +5,10 @@ flexibility and control over when and where those features are enabled.
For example, flags make the following things possible: For example, flags make the following things possible:
- Extended testing of a feature via labs on develop - Extended testing of a feature via labs on develop
- Enabling features when ready instead of the first moment the code is released - 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 - Testing a feature with a specific set of users (by enabling only on a specific
Element instance) Element instance)
The size of the feature controlled by a feature flag may vary widely: it could 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. be a large project like reactions or a smaller change to an existing algorithm.

View file

@ -2,37 +2,37 @@
## Auto Complete ## Auto Complete
- Hitting tab tries to auto-complete the word before the caret as a room member - 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 - If no matching name is found, a visual bell is shown
- @ + a letter opens auto complete for members starting with the given letter - @ + 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 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 - 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 - # + a letter opens auto complete for rooms starting with the given letter
- : open auto complete for emoji - : open auto complete for emoji
- Pressing arrow-up/arrow-down while the autocomplete is open navigates between auto complete options - 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, - 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. wrapping around at the end after reverting to the typed text first.
## Formatting ## Formatting
- When selecting text, a formatting bar appears above the selection. - When selecting text, a formatting bar appears above the selection.
- The formatting bar allows to format the selected test as: - 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). bold, italic, strikethrough, a block quote, and a code block (inline if no linebreak is selected).
- Formatting is applied as markdown syntax. - Formatting is applied as markdown syntax.
- Hitting ctrl/cmd+B also marks the selected text as bold - 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+I also marks the selected text as italic
- Hitting ctrl/cmd+> also marks the selected text as a blockquote - Hitting ctrl/cmd+> also marks the selected text as a blockquote
## Misc ## Misc
- When hitting the arrow-up button while having the caret at the start 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. 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 - 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 - Emoticons (like :-), >:-), :-/, ...) are replaced by emojis while typing if the relevant setting is enabled
- Typing in the composer sends typing notifications in the room - Typing in the composer sends typing notifications in the room
- Pressing ctrl/mod+z and ctrl/mod+y undoes/redoes modifications - Pressing ctrl/mod+z and ctrl/mod+y undoes/redoes modifications
- Pressing shift+enter inserts a line break - Pressing shift+enter inserts a line break
- Pressing enter sends the message. - 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 "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. - 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. - Pressing alt+arrow up/arrow down navigates in previously sent messages, putting them in the composer.

View file

@ -8,9 +8,9 @@ Icons have `role="presentation"` and `aria-hidden` automatically applied. These
SVG file recommendations: SVG file recommendations:
- Colours should not be defined absolutely. Use `currentColor` instead. - 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. - 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. This means that there should be icons for each size, e.g. warning-16px and warning-32px.
Example usage: Example usage:

View file

@ -81,27 +81,27 @@ which takes several parameters:
_Query string_: _Query string_:
- `widgetId`: The ID of the widget. This is needed for communication back to the - `widgetId`: The ID of the widget. This is needed for communication back to the
react-sdk. react-sdk.
- `parentUrl`: The URL of the parent window. This is also needed for - `parentUrl`: The URL of the parent window. This is also needed for
communication back to the react-sdk. communication back to the react-sdk.
_Hash/fragment (formatted as a query string)_: _Hash/fragment (formatted as a query string)_:
- `conferenceDomain`: The domain to connect Jitsi Meet to. - `conferenceDomain`: The domain to connect Jitsi Meet to.
- `conferenceId`: The room or conference ID 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 - `isAudioOnly`: Boolean for whether this is a voice-only conference. May not
be present, should default to `false`. be present, should default to `false`.
- `startWithAudioMuted`: Boolean for whether the calls start with audio - `startWithAudioMuted`: Boolean for whether the calls start with audio
muted. May not be present. muted. May not be present.
- `startWithVideoMuted`: Boolean for whether the calls start with video - `startWithVideoMuted`: Boolean for whether the calls start with video
muted. May not be present. muted. May not be present.
- `displayName`: The display name of the user viewing the widget. May not - `displayName`: The display name of the user viewing the widget. May not
be present or could be null. be present or could be null.
- `avatarUrl`: The HTTP(S) URL for the avatar of the user viewing the widget. May - `avatarUrl`: The HTTP(S) URL for the avatar of the user viewing the widget. May
not be present or could be null. not be present or could be null.
- `userId`: The MXID of the user viewing the widget. May not be present or could - `userId`: The MXID of the user viewing the widget. May not be present or could
be null. be null.
The react-sdk will assume that `jitsi.html` is at the path of wherever it is currently 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`. being served. For example, `https://develop.element.io/jitsi.html` or `vector://webapp/jitsi.html`.

View file

@ -2,10 +2,10 @@
## Contents ## Contents
- How to run the tests - How to run the tests
- How the tests work - How the tests work
- How to write great Playwright tests - How to write great Playwright tests
- Visual testing - Visual testing
## Running the Tests ## 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 templates in `playwright/plugins/homeserver/synapse/templates`. There are a couple of special files
in these templates: in these templates:
- `homeserver.yaml`: - `homeserver.yaml`:
Template substitution happens in this file. Template variables are: Template substitution happens in this file. Template variables are:
- `REGISTRATION_SECRET`: The secret used to register users via the REST API. - `REGISTRATION_SECRET`: The secret used to register users via the REST API.
- `MACAROON_SECRET_KEY`: Generated each time for security - `MACAROON_SECRET_KEY`: Generated each time for security
- `FORM_SECRET`: Generated each time for security - `FORM_SECRET`: Generated each time for security
- `PUBLIC_BASEURL`: The localhost url + port combination the synapse is accessible at - `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. - `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 Config templates should not contain a signing key and instead assume that one will exist
in this file. in this file.
All other files in the template are copied recursively to `/data/`, so the file `foo.html` 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`. in a template can be referenced in the config as `/data/foo.html`.

View file

@ -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: This release process revolves around our main repositories:
- [Element Desktop](https://github.com/element-hq/element-desktop/) - [Element Desktop](https://github.com/element-hq/element-desktop/)
- [Element Web](https://github.com/element-hq/element-web/) - [Element Web](https://github.com/element-hq/element-web/)
- [Matrix JS SDK](https://github.com/matrix-org/matrix-js-sdk/) - [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: 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-web-i18n/
- https://github.com/matrix-org/matrix-react-sdk-module-api - https://github.com/matrix-org/matrix-react-sdk-module-api
</blockquote></details> </blockquote></details>
<details><summary><h1>Prerequisites</h1></summary><blockquote> <details><summary><h1>Prerequisites</h1></summary><blockquote>
- You must be part of the 2 Releasers GitHub groups: - You must be part of the 2 Releasers GitHub groups:
- <https://github.com/orgs/element-hq/teams/element-web-releasers> - <https://github.com/orgs/element-hq/teams/element-web-releasers>
- <https://github.com/orgs/matrix-org/teams/element-web-releasers> - <https://github.com/orgs/matrix-org/teams/element-web-releasers>
- 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 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: - 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). - 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) - SSH access to `horme` (staging.element.io and app.element.io)
- Permission to sudo on horme as the user `element` - 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 need "**jumphost**" configuration in your local `~/.ssh/config`. This should have been set up as part of your onboarding.
</blockquote></details> </blockquote></details>
@ -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` - 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. 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 # Releasing
@ -192,21 +192,21 @@ switched back to the version of the dependency from the master branch to not lea
### Matrix JS SDK ### 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) - [ ] 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** - [ ] 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. - [ ] 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 ### 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) - [ ] 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** - [ ] 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. - [ ] 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 ### 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) - [ ] 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** - [ ] 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. - [ ] 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 # 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 Web to dockerhub, `*.element.io`, and packages.element.io.
We ship Element Desktop to packages.element.io. We ship Element Desktop to packages.element.io.
- [ ] Check that element-web has shipped to dockerhub - [ ] 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) - [ ] Deploy staging.element.io. [See docs.](https://handbook.element.io/books/element-web-team/page/deploying-appstagingelementio)
- [ ] Test staging.element.io - [ ] Test staging.element.io
For final releases additionally do these steps: For final releases additionally do these steps:
- [ ] Deploy app.element.io. [See docs.](https://handbook.element.io/books/element-web-team/page/deploying-appstagingelementio) - [ ] Deploy app.element.io. [See docs.](https://handbook.element.io/books/element-web-team/page/deploying-appstagingelementio)
- [ ] Test app.element.io - [ ] Test app.element.io
- [ ] Ensure Element Web package has shipped to packages.element.io - [ ] Ensure Element Web package has shipped to packages.element.io
- [ ] Ensure Element Desktop packages have shipped to packages.element.io - [ ] Ensure Element Desktop packages have shipped to packages.element.io
# Housekeeping # Housekeeping
We have some manual housekeeping to do in order to prepare for the next release. 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. - [ ] 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) - [ ] Announce the release in [#element-web-announcements:matrix.org](https://matrix.to/#/#element-web-announcements:matrix.org)
<details><summary>(show)</summary> <details><summary>(show)</summary>
@ -246,15 +246,15 @@ With wording like:
For the first RC of a given release cycle do these steps: 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: 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_ - [ ] 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. - [ ] 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.

View file

@ -10,53 +10,53 @@ When reviewing code, here are some things we look for and also things we avoid:
### We review for ### We review for
- Correctness - Correctness
- Performance - Performance
- Accessibility - Accessibility
- Security - Security
- Quality via automated and manual testing - Quality via automated and manual testing
- Comments and documentation where needed - Comments and documentation where needed
- Sharing knowledge of different areas among the team - Sharing knowledge of different areas among the team
- Ensuring it's something we're comfortable maintaining for the long term - Ensuring it's something we're comfortable maintaining for the long term
- Progress indicators and local echo where appropriate with network activity - Progress indicators and local echo where appropriate with network activity
### We should avoid ### We should avoid
- Style nits that are already handled by the linter - Style nits that are already handled by the linter
- Dramatically increasing scope - Dramatically increasing scope
### Good practices ### Good practices
- Use empathetic language - Use empathetic language
- See also [Mindful Communication in Code - See also [Mindful Communication in Code
Reviews](https://kickstarter.engineering/a-guide-to-mindful-communication-in-code-reviews-48aab5282e5e) 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/) 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 - Authors should prefer smaller commits for easier reviewing and bisection
- Reviewers should be explicit about required versus optional changes - Reviewers should be explicit about required versus optional changes
- Reviews are conversations and the PR author should feel comfortable - Reviews are conversations and the PR author should feel comfortable
discussing and pushing back on changes before making them discussing and pushing back on changes before making them
- Reviewers are encouraged to ask for tests where they believe it is reasonable - Reviewers are encouraged to ask for tests where they believe it is reasonable
- Core team should lead by example through their tone and language - Core team should lead by example through their tone and language
- Take the time to thank and point out good code changes - 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 - 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 towards making others feel like colleagues working towards a common goal
### Workflow ### Workflow
- Authors should request review from the element-web team by default (if someone on - 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 the team is clearly the expert in an area, a direct review request to them may
be more appropriate) be more appropriate)
- Reviewers should remove the team review request and request review from - Reviewers should remove the team review request and request review from
themselves when starting a review to avoid double review themselves when starting a review to avoid double review
- If there are multiple related PRs authors should reference each of the PRs in - If there are multiple related PRs authors should reference each of the PRs in
the others before requesting review. Reviewers might start reviewing from the others before requesting review. Reviewers might start reviewing from
different places and could miss other required PRs. different places and could miss other required PRs.
- Avoid force pushing to a PR after the first round of review - Avoid force pushing to a PR after the first round of review
- Use the GitHub default of merge commits when landing (avoid alternate options - Use the GitHub default of merge commits when landing (avoid alternate options
like squash or rebase) like squash or rebase)
- PR author merges after review (assuming they have write access) - PR author merges after review (assuming they have write access)
- Assign issues only when in progress to indicate to others what can be picked - Assign issues only when in progress to indicate to others what can be picked
up up
## Code Quality ## 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 Element and the SDKs, but it hasn't been a consistent focus. Going forward, we'd
like to change that. like to change that.
- For new features, code reviewers will expect some form of automated testing to - For new features, code reviewers will expect some form of automated testing to
be included by default be included by default
- For bug fixes, regression tests are of course great to have, but we don't want - 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 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 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 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 In more detail, our usual process for changes that affect the UI or alter user
functionality is: functionality is:
- For changes that will go live when merged, always flag Design and Product - For changes that will go live when merged, always flag Design and Product
teams as appropriate teams as appropriate
- For changes guarded by a feature flag, Design and Product review is not - For changes guarded by a feature flag, Design and Product review is not
required (though may still be useful) since we can continue tweaking 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 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 files in a PR, a [preview site](./pr-previews.md) that includes your changes

View file

@ -6,11 +6,11 @@ It's so complicated it needs its own README.
Legend: Legend:
- Orange = External event. - Orange = External event.
- Purple = Deterministic flow. - Purple = Deterministic flow.
- Green = Algorithm definition. - Green = Algorithm definition.
- Red = Exit condition/point. - Red = Exit condition/point.
- Blue = Process definition. - Blue = Process definition.
## Algorithms involved ## 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 algorithm. There are 4 categories: Red, Grey, Bold, and Idle. Each has their own definition based off
relative (perceived) importance to the user: relative (perceived) importance to the user:
- **Red**: The room has unread mentions waiting for 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 - **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 messages which cause a push notification or badge count. Typically, this is the default as rooms get
set to 'All Messages'. set to 'All Messages'.
- **Bold**: The room has unread messages waiting for the user. Essentially this is a grey room without - **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'). 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 - **Idle**: No useful (see definition of useful above) activity has occurred in the room since the user
last read it. last read it.
Conveniently, each tag gets ordered by those categories as presented: red rooms appear above grey, grey Conveniently, each tag gets ordered by those categories as presented: red rooms appear above grey, grey
above bold, etc. above bold, etc.

View file

@ -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 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: order of priority, are:
- `device` - The current user's device - `device` - The current user's device
- `room-device` - The current user's device, but only when in a specific room - `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 - `room-account` - The current user's account, but only when in a specific room
- `account` - The current user's account - `account` - The current user's account
- `room` - A specific room (setting for all members of the room) - `room` - A specific room (setting for all members of the room)
- `config` - Values are defined by the `setting_defaults` key (usually) in `config.json` - `config` - Values are defined by the `setting_defaults` key (usually) in `config.json`
- `default` - The hardcoded default for the settings - `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 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. that room administrators cannot force account-only settings upon participants.

View file

@ -2,9 +2,9 @@
## Requirements ## Requirements
- A working [Development Setup](../README.md#setting-up-a-dev-environment) - A working [Development Setup](../README.md#setting-up-a-dev-environment)
- Latest LTS version of Node.js installed - Latest LTS version of Node.js installed
- Be able to understand English - Be able to understand English
## Translating strings vs. marking strings for translation ## 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 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. 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). - 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('<a>Click here!</a>', {}, { 'a': (sub) => <a>{sub}</a> })`. If you don't do the tag substitution you will end up showing literally '<a>' rather than making a hyperlink. - If you want to translate text that includes e.g. hyperlinks or other HTML you have to also use tag substitution, e.g. `_t('<a>Click here!</a>', {}, { 'a': (sub) => <a>{sub}</a> })`. If you don't do the tag substitution you will end up showing literally '<a>' 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: <i>{userEmailAddress}</i> })`. - 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: <i>{userEmailAddress}</i> })`.
## Things to know/Style Guides ## 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. - 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 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. - 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. - 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. - 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. - 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. - 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) - Don't forget curly braces when you assign an expression to JSX attributes in the render method)

View file

@ -2,9 +2,9 @@
## Requirements ## Requirements
- Web Browser - Web Browser
- Be able to understand English - Be able to understand English
- Be able to understand the language you want to translate Element into - Be able to understand the language you want to translate Element into
## Join #element-translations:matrix.org ## Join #element-translations:matrix.org

View file

@ -270,11 +270,12 @@
"postcss-preset-env": "^10.0.0", "postcss-preset-env": "^10.0.0",
"postcss-scss": "^4.0.4", "postcss-scss": "^4.0.4",
"postcss-simple-vars": "^7.0.1", "postcss-simple-vars": "^7.0.1",
"prettier": "3.3.3", "prettier": "3.4.1",
"process": "^0.11.10", "process": "^0.11.10",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
"rimraf": "^6.0.0", "rimraf": "^6.0.0",
"semver": "^7.5.2", "semver": "^7.5.2",
"source-map-loader": "^5.0.0",
"stylelint": "^16.1.0", "stylelint": "^16.1.0",
"stylelint-config-standard": "^36.0.0", "stylelint-config-standard": "^36.0.0",
"stylelint-scss": "^6.0.0", "stylelint-scss": "^6.0.0",

View file

@ -2,19 +2,19 @@
Tips for writing these tests: Tips for writing these tests:
- Break up your tests into the smallest test case possible. The purpose of - 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. 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 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 that will frustrate our goals here. (We will need to find a different way to
reduce CI time.) reduce CI time.)
- Try to assert something after every action, to make sure it has completed. - Try to assert something after every action, to make sure it has completed.
E.g.: E.g.:
markAsRead(room2); markAsRead(room2);
assertRead(room2); assertRead(room2);
You should especially follow this rule if you are jumping to a different You should especially follow this rule if you are jumping to a different
room or similar straight afterward. room or similar straight afterward.
- Use assertStillRead() if you are asserting something is read when it was - 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 also read before. This waits a little while to make sure you're not getting a
false positive. false positive.

View file

@ -20,7 +20,7 @@ import { randB64Bytes } from "../../utils/rand";
// Docker tag to use for synapse docker image. // 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. // 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. // This digest is updated by the playwright-image-updates.yaml workflow periodically.
const DOCKER_TAG = "develop@sha256:e163b15bf4905e4067dece856cca00e6ac8d1d655f4f1307978eee256b3ea775"; const DOCKER_TAG = "develop@sha256:489fe921e03440af87e001106c41c70ffc55a1e8078d1a7f45e16fbaddc5088a";
async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise<Omit<HomeserverConfig, "dockerUrl">> { async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise<Omit<HomeserverConfig, "dockerUrl">> {
const templateDir = path.join(__dirname, "templates", opts.template); const templateDir = path.join(__dirname, "templates", opts.template);

View file

@ -4,16 +4,16 @@ A very simple OAuth identity provider server.
The following endpoints are exposed: The following endpoints are exposed:
- `/oauth/auth.html`: An OAuth2 [authorization endpoint](https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint). - `/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 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 auth code that can be changed if we want the next step to fail). It redirects back to the calling application
with a "code". with a "code".
- `/oauth/token`: An OAuth2 [token endpoint](https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint). - `/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. 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). - `/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. Returns details about the owner of the offered access token.
To start the server, do: To start the server, do:

View file

@ -393,9 +393,3 @@
@import "./views/voip/_LegacyCallViewHeader.pcss"; @import "./views/voip/_LegacyCallViewHeader.pcss";
@import "./views/voip/_LegacyCallViewSidebar.pcss"; @import "./views/voip/_LegacyCallViewSidebar.pcss";
@import "./views/voip/_VideoFeed.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";

View file

@ -22,20 +22,6 @@ Please see LICENSE files in the repository root for full details.
pointer-events: none; /* makes the avatar non-draggable */ 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 { .mx_UserMenu_contextMenuButton {

View file

@ -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"); 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 { .mx_MessageComposer_plain_text::before {
mask-image: url("$(res)/img/element-icons/room/composer/plain_text.svg"); mask-image: url("$(res)/img/element-icons/room/composer/plain_text.svg");
} }

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -240,11 +240,6 @@ $location-live-secondary-color: #deddfd;
} }
/* ******************** */ /* ******************** */
/* Voice Broadcast */
/* ******************** */
$live-badge-color: #ffffff;
/* ******************** */
/* One-off colors */ /* One-off colors */
/* ******************** */ /* ******************** */
$progressbar-bg-color: var(--cpd-color-gray-200); $progressbar-bg-color: var(--cpd-color-gray-200);

View file

@ -226,11 +226,6 @@ $location-live-color: #5c56f5;
$location-live-secondary-color: #deddfd; $location-live-secondary-color: #deddfd;
/* ******************** */ /* ******************** */
/* Voice Broadcast */
/* ******************** */
$live-badge-color: #ffffff;
/* ******************** */
body { body {
color-scheme: dark; color-scheme: dark;
} }

View file

@ -325,11 +325,6 @@ $location-live-color: #5c56f5;
$location-live-secondary-color: #deddfd; $location-live-secondary-color: #deddfd;
/* ******************** */ /* ******************** */
/* Voice Broadcast */
/* ******************** */
$live-badge-color: #ffffff;
/* ******************** */
body { body {
color-scheme: light; color-scheme: light;
} }

View file

@ -10,8 +10,8 @@
/* Noto Color Emoji contains digits, in fixed-width, therefore causing /* Noto Color Emoji contains digits, in fixed-width, therefore causing
digits in flowed text to stand out. digits in flowed text to stand out.
TODO: Consider putting all emoji fonts to the end rather than the front. */ 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, $font-family: "Inter", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Arial", "Helvetica",
"Noto Color Emoji"; sans-serif, "Noto Color Emoji";
$monospace-font-family: "Inconsolata", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Courier", $monospace-font-family: "Inconsolata", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Courier",
monospace, "Noto Color Emoji"; 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); $location-live-secondary-color: var(--cpd-color-purple-600);
/* ******************** */ /* ******************** */
/* Voice Broadcast */
/* ******************** */
$live-badge-color: var(--cpd-color-icon-on-solid-primary);
/* ******************** */
body { body {
color-scheme: light; color-scheme: light;
} }

View file

@ -1,47 +0,0 @@
#!/usr/bin/env python
import json
import sys
import os
if len(sys.argv) < 3:
print "Usage: %s <source> <dest>" % (sys.argv[0],)
print "eg. %s pt_BR.json pt.json" % (sys.argv[0],)
print
print "Adds any translations to <dest> that exist in <source> but not <dest>"
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)

View file

@ -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
##############################

View file

@ -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

View file

@ -10,7 +10,6 @@ import type { IWidget } from "matrix-widget-api";
import type { BLURHASH_FIELD } from "../utils/image-media"; import type { BLURHASH_FIELD } from "../utils/image-media";
import type { JitsiCallMemberEventType, JitsiCallMemberContent } from "../call-types"; import type { JitsiCallMemberEventType, JitsiCallMemberContent } from "../call-types";
import type { ILayoutStateEvent, WIDGET_LAYOUT_EVENT_TYPE } from "../stores/widgets/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"; 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 // 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 | {}; "im.vector.modular.widgets": IWidget | {};
[WIDGET_LAYOUT_EVENT_TYPE]: ILayoutStateEvent; [WIDGET_LAYOUT_EVENT_TYPE]: ILayoutStateEvent;
// Unstable voice broadcast state events
[VoiceBroadcastInfoEventType]: VoiceBroadcastInfoEventContent;
// Element custom state events // Element custom state events
"im.vector.web.settings": Record<string, any>; "im.vector.web.settings": Record<string, any>;
"org.matrix.room.preview_urls": { disable: boolean }; "org.matrix.room.preview_urls": { disable: boolean };
@ -78,7 +74,5 @@ declare module "matrix-js-sdk/src/types" {
waveform?: number[]; waveform?: number[];
}; };
"org.matrix.msc3245.voice"?: {}; "org.matrix.msc3245.voice"?: {};
"io.element.voice_broadcast_chunk"?: { sequence: number };
} }
} }

View file

@ -175,13 +175,6 @@ export interface IConfigOptions {
sync_timeline_limit?: number; sync_timeline_limit?: number;
dangerously_allow_unsafe_and_insecure_passwords?: boolean; // developer option 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?: { user_notice?: {
title: string; title: string;
description: string; description: string;

View file

@ -55,8 +55,6 @@ import { OpenInviteDialogPayload } from "./dispatcher/payloads/OpenInviteDialogP
import { findDMForUser } from "./utils/dm/findDMForUser"; import { findDMForUser } from "./utils/dm/findDMForUser";
import { getJoinedNonFunctionalMembers } from "./utils/room/getJoinedNonFunctionalMembers"; import { getJoinedNonFunctionalMembers } from "./utils/room/getJoinedNonFunctionalMembers";
import { localNotificationsAreSilenced } from "./utils/notifications"; import { localNotificationsAreSilenced } from "./utils/notifications";
import { SdkContextClass } from "./contexts/SDKContext";
import { showCantStartACallDialog } from "./voice-broadcast/utils/showCantStartACallDialog";
import { isNotNull } from "./Typeguards"; import { isNotNull } from "./Typeguards";
import { BackgroundAudio } from "./audio/BackgroundAudio"; import { BackgroundAudio } from "./audio/BackgroundAudio";
import { Jitsi } from "./widgets/Jitsi.ts"; import { Jitsi } from "./widgets/Jitsi.ts";
@ -859,15 +857,6 @@ export default class LegacyCallHandler extends EventEmitter {
return; 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 // We might be using managed hybrid widgets
if (isManagedHybridWidgetEnabled(room)) { if (isManagedHybridWidgetEnabled(room)) {
await addManagedHybridWidget(room); await addManagedHybridWidget(room);

View file

@ -35,13 +35,11 @@ import IdentityAuthClient from "./IdentityAuthClient";
import { crossSigningCallbacks } from "./SecurityManager"; import { crossSigningCallbacks } from "./SecurityManager";
import { SlidingSyncManager } from "./SlidingSyncManager"; import { SlidingSyncManager } from "./SlidingSyncManager";
import { _t, UserFriendlyError } from "./languageHandler"; import { _t, UserFriendlyError } from "./languageHandler";
import { SettingLevel } from "./settings/SettingLevel";
import MatrixClientBackedController from "./settings/controllers/MatrixClientBackedController"; import MatrixClientBackedController from "./settings/controllers/MatrixClientBackedController";
import ErrorDialog from "./components/views/dialogs/ErrorDialog"; import ErrorDialog from "./components/views/dialogs/ErrorDialog";
import PlatformPeg from "./PlatformPeg"; import PlatformPeg from "./PlatformPeg";
import { formatList } from "./utils/FormattingUtils"; import { formatList } from "./utils/FormattingUtils";
import SdkConfig from "./SdkConfig"; import SdkConfig from "./SdkConfig";
import { Features } from "./settings/Settings";
import { setDeviceIsolationMode } from "./settings/controllers/DeviceIsolationModeController.ts"; import { setDeviceIsolationMode } from "./settings/controllers/DeviceIsolationModeController.ts";
export interface IMatrixClientCreds { export interface IMatrixClientCreds {
@ -333,11 +331,6 @@ class MatrixClientPegClass implements IMatrixClientPeg {
logger.error("Warning! Not using an encryption key for rust crypto store."); 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({ await this.matrixClient.initRustCrypto({
storageKey: rustCryptoStoreKey, storageKey: rustCryptoStoreKey,
storagePassword: rustCryptoStorePassword, storagePassword: rustCryptoStorePassword,

View file

@ -49,8 +49,6 @@ import { SdkContextClass } from "./contexts/SDKContext";
import { localNotificationsAreSilenced, createLocalNotificationSettingsIfNeeded } from "./utils/notifications"; import { localNotificationsAreSilenced, createLocalNotificationSettingsIfNeeded } from "./utils/notifications";
import { getIncomingCallToastKey, IncomingCallToast } from "./toasts/IncomingCallToast"; import { getIncomingCallToastKey, IncomingCallToast } from "./toasts/IncomingCallToast";
import ToastStore from "./stores/ToastStore"; import ToastStore from "./stores/ToastStore";
import { VoiceBroadcastChunkEventType, VoiceBroadcastInfoEventType } from "./voice-broadcast";
import { getSenderName } from "./utils/event/getSenderName";
import { stripPlainReply } from "./utils/Reply"; import { stripPlainReply } from "./utils/Reply";
import { BackgroundAudio } from "./audio/BackgroundAudio"; import { BackgroundAudio } from "./audio/BackgroundAudio";
@ -81,17 +79,6 @@ const msgTypeHandlers: Record<string, (event: MatrixEvent) => string | null> = {
return TextForEvent.textForLocationEvent(event)(); return TextForEvent.textForLocationEvent(event)();
}, },
[MsgType.Audio]: (event: MatrixEvent): string | null => { [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()); return TextForEvent.textForEvent(event, MatrixClientPeg.safeGet());
}, },
}; };
@ -460,8 +447,6 @@ class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents
// XXX: exported for tests // XXX: exported for tests
public evaluateEvent(ev: MatrixEvent): void { public evaluateEvent(ev: MatrixEvent): void {
// Mute notifications for broadcast info events
if (ev.getType() === VoiceBroadcastInfoEventType) return;
let roomId = ev.getRoomId()!; let roomId = ev.getRoomId()!;
if (LegacyCallHandler.instance.getSupportsVirtualRooms()) { if (LegacyCallHandler.instance.getSupportsVirtualRooms()) {
// Attempt to translate a virtual room to a native one // Attempt to translate a virtual room to a native one

View file

@ -46,10 +46,6 @@ export const DEFAULTS: DeepReadonly<IConfigOptions> = {
logo: require("../res/img/element-desktop-logo.svg").default, logo: require("../res/img/element-desktop-logo.svg").default,
url: "https://element.io/get-started", url: "https://element.io/get-started",
}, },
voice_broadcast: {
chunk_length: 2 * 60, // two minutes
max_length: 4 * 60 * 60, // four hours
},
feedback: { feedback: {
existing_issues_url: existing_issues_url:

View file

@ -36,7 +36,6 @@ import AccessibleButton from "./components/views/elements/AccessibleButton";
import RightPanelStore from "./stores/right-panel/RightPanelStore"; import RightPanelStore from "./stores/right-panel/RightPanelStore";
import { highlightEvent, isLocationEvent } from "./utils/EventUtils"; import { highlightEvent, isLocationEvent } from "./utils/EventUtils";
import { ElementCall } from "./models/Call"; import { ElementCall } from "./models/Call";
import { textForVoiceBroadcastStoppedEvent, VoiceBroadcastInfoEventType } from "./voice-broadcast";
import { getSenderName } from "./utils/event/getSenderName"; import { getSenderName } from "./utils/event/getSenderName";
import PosthogTrackers from "./PosthogTrackers.ts"; 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) // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
"im.vector.modular.widgets": textForWidgetEvent, "im.vector.modular.widgets": textForWidgetEvent,
[WIDGET_LAYOUT_EVENT_TYPE]: textForWidgetLayoutEvent, [WIDGET_LAYOUT_EVENT_TYPE]: textForWidgetLayoutEvent,
[VoiceBroadcastInfoEventType]: textForVoiceBroadcastStoppedEvent,
}; };
// Add all the Mjolnir stuff to the renderer // Add all the Mjolnir stuff to the renderer

View file

@ -36,7 +36,7 @@ interface IState {
export default class EmbeddedPage extends React.PureComponent<IProps, IState> { export default class EmbeddedPage extends React.PureComponent<IProps, IState> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>; declare public context: React.ContextType<typeof MatrixClientContext>;
private unmounted = false; private unmounted = false;
private dispatcherRef?: string; private dispatcherRef?: string;

View file

@ -34,6 +34,7 @@ import { Layout } from "../../settings/enums/Layout";
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
import Measured from "../views/elements/Measured"; import Measured from "../views/elements/Measured";
import EmptyState from "../views/right_panel/EmptyState"; import EmptyState from "../views/right_panel/EmptyState";
import { ScopedRoomContextProvider } from "../../contexts/ScopedRoomContext.tsx";
interface IProps { interface IProps {
roomId: string; roomId: string;
@ -51,7 +52,7 @@ interface IState {
*/ */
class FilePanel extends React.Component<IProps, IState> { class FilePanel extends React.Component<IProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
// This is used to track if a decrypted event was a live event and should be // This is used to track if a decrypted event was a live event and should be
// added to the timeline. // added to the timeline.
@ -104,7 +105,11 @@ class FilePanel extends React.Component<IProps, IState> {
} }
if (!this.state.timelineSet.eventIdToTimeline(ev.getId()!)) { 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<IProps, IState> {
if (this.state.timelineSet) { if (this.state.timelineSet) {
return ( return (
<RoomContext.Provider <ScopedRoomContextProvider
value={{ {...this.context}
...this.context, timelineRenderingType={TimelineRenderingType.File}
timelineRenderingType: TimelineRenderingType.File, narrow={this.state.narrow}
narrow: this.state.narrow,
}}
> >
<BaseCard <BaseCard
className="mx_FilePanel" className="mx_FilePanel"
@ -298,16 +301,11 @@ class FilePanel extends React.Component<IProps, IState> {
layout={Layout.Group} layout={Layout.Group}
/> />
</BaseCard> </BaseCard>
</RoomContext.Provider> </ScopedRoomContextProvider>
); );
} else { } else {
return ( return (
<RoomContext.Provider <ScopedRoomContextProvider {...this.context} timelineRenderingType={TimelineRenderingType.File}>
value={{
...this.context,
timelineRenderingType: TimelineRenderingType.File,
}}
>
<BaseCard <BaseCard
className="mx_FilePanel" className="mx_FilePanel"
onClose={this.props.onClose} onClose={this.props.onClose}
@ -315,7 +313,7 @@ class FilePanel extends React.Component<IProps, IState> {
> >
<Spinner /> <Spinner />
</BaseCard> </BaseCard>
</RoomContext.Provider> </ScopedRoomContextProvider>
); );
} }
} }

View file

@ -119,7 +119,6 @@ import { ValidatedServerConfig } from "../../utils/ValidatedServerConfig";
import { isLocalRoom } from "../../utils/localRoom/isLocalRoom"; import { isLocalRoom } from "../../utils/localRoom/isLocalRoom";
import { SDKContext, SdkContextClass } from "../../contexts/SDKContext"; import { SDKContext, SdkContextClass } from "../../contexts/SDKContext";
import { viewUserDeviceSettings } from "../../actions/handlers/viewUserDeviceSettings"; import { viewUserDeviceSettings } from "../../actions/handlers/viewUserDeviceSettings";
import { cleanUpBroadcasts, VoiceBroadcastResumer } from "../../voice-broadcast";
import GenericToast from "../views/toasts/GenericToast"; import GenericToast from "../views/toasts/GenericToast";
import RovingSpotlightDialog from "../views/dialogs/spotlight/SpotlightDialog"; import RovingSpotlightDialog from "../views/dialogs/spotlight/SpotlightDialog";
import { findDMForUser } from "../../utils/dm/findDMForUser"; import { findDMForUser } from "../../utils/dm/findDMForUser";
@ -227,7 +226,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private focusNext: FocusNextType; private focusNext: FocusNextType;
private subTitleStatus: string; private subTitleStatus: string;
private prevWindowWidth: number; private prevWindowWidth: number;
private voiceBroadcastResumer?: VoiceBroadcastResumer;
private readonly loggedInView = createRef<LoggedInViewType>(); private readonly loggedInView = createRef<LoggedInViewType>();
private dispatcherRef?: string; private dispatcherRef?: string;
@ -501,7 +499,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
window.removeEventListener("resize", this.onWindowResized); window.removeEventListener("resize", this.onWindowResized);
this.stores.accountPasswordStore.clearPassword(); this.stores.accountPasswordStore.clearPassword();
this.voiceBroadcastResumer?.destroy();
} }
private onWindowResized = (): void => { private onWindowResized = (): void => {
@ -651,10 +648,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
break; break;
case "logout": case "logout":
LegacyCallHandler.instance.hangupAllCalls(); LegacyCallHandler.instance.hangupAllCalls();
Promise.all([ Promise.all([...[...CallStore.instance.connectedCalls].map((call) => call.disconnect())]).finally(() =>
...[...CallStore.instance.connectedCalls].map((call) => call.disconnect()), Lifecycle.logout(this.stores.oidcClientStore),
cleanUpBroadcasts(this.stores), );
]).finally(() => Lifecycle.logout(this.stores.oidcClientStore));
break; break;
case "require_registration": case "require_registration":
startAnyRegistrationFlow(payload as any); startAnyRegistrationFlow(payload as any);
@ -1679,8 +1675,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}); });
} }
}); });
this.voiceBroadcastResumer = new VoiceBroadcastResumer(cli);
} }
/** /**

View file

@ -196,7 +196,7 @@ interface IReadReceiptForUser {
*/ */
export default class MessagePanel extends React.Component<IProps, IState> { export default class MessagePanel extends React.Component<IProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
public static defaultProps = { public static defaultProps = {
disableGrouping: false, disableGrouping: false,

View file

@ -19,6 +19,7 @@ import { Layout } from "../../settings/enums/Layout";
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
import Measured from "../views/elements/Measured"; import Measured from "../views/elements/Measured";
import EmptyState from "../views/right_panel/EmptyState"; import EmptyState from "../views/right_panel/EmptyState";
import { ScopedRoomContextProvider } from "../../contexts/ScopedRoomContext.tsx";
interface IProps { interface IProps {
onClose(): void; onClose(): void;
@ -33,7 +34,7 @@ interface IState {
*/ */
export default class NotificationPanel extends React.PureComponent<IProps, IState> { export default class NotificationPanel extends React.PureComponent<IProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
private card = React.createRef<HTMLDivElement>(); private card = React.createRef<HTMLDivElement>();
@ -79,12 +80,10 @@ export default class NotificationPanel extends React.PureComponent<IProps, IStat
} }
return ( return (
<RoomContext.Provider <ScopedRoomContextProvider
value={{ {...this.context}
...this.context, timelineRenderingType={TimelineRenderingType.Notification}
timelineRenderingType: TimelineRenderingType.Notification, narrow={this.state.narrow}
narrow: this.state.narrow,
}}
> >
<BaseCard <BaseCard
header={_t("notifications|enable_prompt_toast_title")} header={_t("notifications|enable_prompt_toast_title")}
@ -99,7 +98,7 @@ export default class NotificationPanel extends React.PureComponent<IProps, IStat
{this.card.current && <Measured sensor={this.card.current} onMeasurement={this.onMeasurement} />} {this.card.current && <Measured sensor={this.card.current} onMeasurement={this.onMeasurement} />}
{content} {content}
</BaseCard> </BaseCard>
</RoomContext.Provider> </ScopedRoomContextProvider>
); );
} }
} }

View file

@ -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. 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 { CallEvent, CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { Optional } from "matrix-events-sdk"; import { Optional } from "matrix-events-sdk";
@ -21,19 +21,7 @@ import { WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../stores/ActiveWidgetStore"; import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../stores/ActiveWidgetStore";
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
import { UPDATE_EVENT } from "../../stores/AsyncStore"; import { UPDATE_EVENT } from "../../stores/AsyncStore";
import { SDKContext, SdkContextClass } from "../../contexts/SDKContext"; import { 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 { WidgetPip } from "../views/pips/WidgetPip"; import { WidgetPip } from "../views/pips/WidgetPip";
const SHOW_CALL_IN_STATES = [ const SHOW_CALL_IN_STATES = [
@ -46,9 +34,6 @@ const SHOW_CALL_IN_STATES = [
]; ];
interface IProps { interface IProps {
voiceBroadcastRecording: Optional<VoiceBroadcastRecording>;
voiceBroadcastPreRecording: Optional<VoiceBroadcastPreRecording>;
voiceBroadcastPlayback: Optional<VoiceBroadcastPlayback>;
movePersistedElement: MutableRefObject<(() => void) | undefined>; movePersistedElement: MutableRefObject<(() => void) | undefined>;
} }
@ -245,52 +230,9 @@ class PipContainerInner extends React.Component<IProps, IState> {
this.setState({ showWidgetInPip, persistentWidgetId, persistentRoomId }); this.setState({ showWidgetInPip, persistentWidgetId, persistentRoomId });
} }
private createVoiceBroadcastPlaybackPipContent(voiceBroadcastPlayback: VoiceBroadcastPlayback): CreatePipChildren {
const content =
this.state.viewedRoomId === voiceBroadcastPlayback.infoEvent.getRoomId() ? (
<VoiceBroadcastPlaybackBody playback={voiceBroadcastPlayback} pip={true} />
) : (
<VoiceBroadcastSmallPlaybackBody playback={voiceBroadcastPlayback} />
);
return ({ onStartMoving }) => (
<div key={`vb-playback-${voiceBroadcastPlayback.infoEvent.getId()}`} onMouseDown={onStartMoving}>
{content}
</div>
);
}
private createVoiceBroadcastPreRecordingPipContent(
voiceBroadcastPreRecording: VoiceBroadcastPreRecording,
): CreatePipChildren {
return ({ onStartMoving }) => (
<div key="vb-pre-recording" onMouseDown={onStartMoving}>
<VoiceBroadcastPreRecordingPip voiceBroadcastPreRecording={voiceBroadcastPreRecording} />
</div>
);
}
private createVoiceBroadcastRecordingPipContent(
voiceBroadcastRecording: VoiceBroadcastRecording,
): CreatePipChildren {
return ({ onStartMoving }) => (
<div key={`vb-recording-${voiceBroadcastRecording.infoEvent.getId()}`} onMouseDown={onStartMoving}>
<VoiceBroadcastRecordingPip recording={voiceBroadcastRecording} />
</div>
);
}
public render(): ReactNode { public render(): ReactNode {
const pipMode = true; const pipMode = true;
let pipContent: Array<CreatePipChildren> = []; const pipContent: Array<CreatePipChildren> = [];
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)];
}
if (this.state.primaryCall) { if (this.state.primaryCall) {
// get a ref to call inside the current scope // get a ref to call inside the current scope
@ -338,24 +280,7 @@ class PipContainerInner extends React.Component<IProps, IState> {
} }
export const PipContainer: React.FC = () => { 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>(); const movePersistedElement = useRef<() => void>();
return ( return <PipContainerInner movePersistedElement={movePersistedElement} />;
<PipContainerInner
voiceBroadcastPlayback={currentVoiceBroadcastPlayback}
voiceBroadcastPreRecording={currentVoiceBroadcastPreRecording}
voiceBroadcastRecording={currentVoiceBroadcastRecording}
movePersistedElement={movePersistedElement}
/>
);
}; };

View file

@ -63,7 +63,7 @@ interface IState {
export default class RightPanel extends React.Component<Props, IState> { export default class RightPanel extends React.Component<Props, IState> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>; declare public context: React.ContextType<typeof MatrixClientContext>;
public constructor(props: Props, context: React.ContextType<typeof MatrixClientContext>) { public constructor(props: Props, context: React.ContextType<typeof MatrixClientContext>) {
super(props, context); super(props, context);
@ -109,10 +109,10 @@ export default class RightPanel extends React.Component<Props, IState> {
} }
// redraw the badge on the membership list // redraw the badge on the membership list
if (this.state.phase === RightPanelPhases.RoomMemberList) { if (this.state.phase === RightPanelPhases.MemberList) {
this.delayedUpdate(); this.delayedUpdate();
} else if ( } else if (
this.state.phase === RightPanelPhases.RoomMemberInfo && this.state.phase === RightPanelPhases.MemberInfo &&
member.userId === this.state.cardState?.member?.userId member.userId === this.state.cardState?.member?.userId
) { ) {
// refresh the member info (e.g. new power level) // refresh the member info (e.g. new power level)
@ -157,7 +157,7 @@ export default class RightPanel extends React.Component<Props, IState> {
const phase = this.props.overwriteCard?.phase ?? this.state.phase; const phase = this.props.overwriteCard?.phase ?? this.state.phase;
const cardState = this.props.overwriteCard?.state ?? this.state.cardState; const cardState = this.props.overwriteCard?.state ?? this.state.cardState;
switch (phase) { switch (phase) {
case RightPanelPhases.RoomMemberList: case RightPanelPhases.MemberList:
if (!!roomId) { if (!!roomId) {
card = ( card = (
<MemberList <MemberList
@ -170,22 +170,8 @@ export default class RightPanel extends React.Component<Props, IState> {
); );
} }
break; break;
case RightPanelPhases.SpaceMemberList:
if (!!cardState?.spaceId || !!roomId) {
card = (
<MemberList
roomId={cardState?.spaceId ?? roomId!}
key={cardState?.spaceId ?? roomId!}
onClose={this.onClose}
searchQuery={this.state.searchQuery}
onSearchQueryChanged={this.onSearchQueryChanged}
/>
);
}
break;
case RightPanelPhases.RoomMemberInfo: case RightPanelPhases.MemberInfo:
case RightPanelPhases.SpaceMemberInfo:
case RightPanelPhases.EncryptionPanel: { case RightPanelPhases.EncryptionPanel: {
if (!!cardState?.member) { if (!!cardState?.member) {
const roomMember = cardState.member instanceof RoomMember ? cardState.member : undefined; const roomMember = cardState.member instanceof RoomMember ? cardState.member : undefined;
@ -203,8 +189,7 @@ export default class RightPanel extends React.Component<Props, IState> {
} }
break; break;
} }
case RightPanelPhases.Room3pidMemberInfo: case RightPanelPhases.ThreePidMemberInfo:
case RightPanelPhases.Space3pidMemberInfo:
if (!!cardState?.memberInfoEvent) { if (!!cardState?.memberInfoEvent) {
card = ( card = (
<ThirdPartyMemberInfo event={cardState.memberInfoEvent} key={roomId} onClose={this.onClose} /> <ThirdPartyMemberInfo event={cardState.memberInfoEvent} key={roomId} onClose={this.onClose} />

View file

@ -26,7 +26,7 @@ import ErrorDialog from "../views/dialogs/ErrorDialog";
import ResizeNotifier from "../../utils/ResizeNotifier"; import ResizeNotifier from "../../utils/ResizeNotifier";
import MatrixClientContext from "../../contexts/MatrixClientContext"; import MatrixClientContext from "../../contexts/MatrixClientContext";
import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
import RoomContext from "../../contexts/RoomContext"; import { useScopedRoomContext } from "../../contexts/ScopedRoomContext.tsx";
const DEBUG = false; const DEBUG = false;
let debuglog = function (msg: string): void {}; let debuglog = function (msg: string): void {};
@ -53,7 +53,7 @@ interface Props {
export const RoomSearchView = forwardRef<ScrollPanel, Props>( export const RoomSearchView = forwardRef<ScrollPanel, Props>(
({ term, scope, promise, abortController, resizeNotifier, className, onUpdate, inProgress }: Props, ref) => { ({ term, scope, promise, abortController, resizeNotifier, className, onUpdate, inProgress }: Props, ref) => {
const client = useContext(MatrixClientContext); const client = useContext(MatrixClientContext);
const roomContext = useContext(RoomContext); const roomContext = useScopedRoomContext("showHiddenEvents");
const [highlights, setHighlights] = useState<string[] | null>(null); const [highlights, setHighlights] = useState<string[] | null>(null);
const [results, setResults] = useState<ISearchResults | null>(null); const [results, setResults] = useState<ISearchResults | null>(null);
const aborted = useRef(false); const aborted = useRef(false);

View file

@ -89,7 +89,7 @@ interface IState {
export default class RoomStatusBar extends React.PureComponent<IProps, IState> { export default class RoomStatusBar extends React.PureComponent<IProps, IState> {
private unmounted = false; private unmounted = false;
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>; declare public context: React.ContextType<typeof MatrixClientContext>;
public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) { public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
super(props, context); super(props, context);

View file

@ -9,16 +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. Please see LICENSE files in the repository root for full details.
*/ */
import React, { import React, { ChangeEvent, ComponentProps, createRef, ReactElement, ReactNode, RefObject, JSX } from "react";
ChangeEvent,
ComponentProps,
createRef,
ReactElement,
ReactNode,
RefObject,
useContext,
JSX,
} from "react";
import classNames from "classnames"; import classNames from "classnames";
import { import {
IRecommendedVersion, IRecommendedVersion,
@ -64,7 +55,7 @@ import WidgetEchoStore from "../../stores/WidgetEchoStore";
import SettingsStore from "../../settings/SettingsStore"; import SettingsStore from "../../settings/SettingsStore";
import { Layout } from "../../settings/enums/Layout"; import { Layout } from "../../settings/enums/Layout";
import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton"; 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 { E2EStatus, shieldStatusForRoom } from "../../utils/ShieldUtils";
import { Action } from "../../dispatcher/actions"; import { Action } from "../../dispatcher/actions";
import { IMatrixClientCreds } from "../../MatrixClientPeg"; import { IMatrixClientCreds } from "../../MatrixClientPeg";
@ -136,6 +127,7 @@ import RightPanelStore from "../../stores/right-panel/RightPanelStore";
import { onView3pidInvite } from "../../stores/right-panel/action-handlers"; import { onView3pidInvite } from "../../stores/right-panel/action-handlers";
import RoomSearchAuxPanel from "../views/rooms/RoomSearchAuxPanel"; import RoomSearchAuxPanel from "../views/rooms/RoomSearchAuxPanel";
import { PinnedMessageBanner } from "../views/rooms/PinnedMessageBanner"; import { PinnedMessageBanner } from "../views/rooms/PinnedMessageBanner";
import { ScopedRoomContextProvider, useScopedRoomContext } from "../../contexts/ScopedRoomContext";
const DEBUG = false; const DEBUG = false;
const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000; const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000;
@ -261,6 +253,7 @@ interface LocalRoomViewProps {
permalinkCreator: RoomPermalinkCreator; permalinkCreator: RoomPermalinkCreator;
roomView: RefObject<HTMLElement>; roomView: RefObject<HTMLElement>;
onFileDrop: (dataTransfer: DataTransfer) => Promise<void>; onFileDrop: (dataTransfer: DataTransfer) => Promise<void>;
mainSplitContentType: MainSplitContentType;
} }
/** /**
@ -270,7 +263,7 @@ interface LocalRoomViewProps {
* @returns {ReactElement} * @returns {ReactElement}
*/ */
function LocalRoomView(props: LocalRoomViewProps): ReactElement { function LocalRoomView(props: LocalRoomViewProps): ReactElement {
const context = useContext(RoomContext); const context = useScopedRoomContext("room");
const room = context.room as LocalRoom; const room = context.room as LocalRoom;
const encryptionEvent = props.localRoom.currentState.getStateEvents(EventType.RoomEncryption)[0]; const encryptionEvent = props.localRoom.currentState.getStateEvents(EventType.RoomEncryption)[0];
let encryptionTile: ReactNode; let encryptionTile: ReactNode;
@ -338,6 +331,7 @@ interface ILocalRoomCreateLoaderProps {
localRoom: LocalRoom; localRoom: LocalRoom;
names: string; names: string;
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
mainSplitContentType: MainSplitContentType;
} }
/** /**
@ -378,7 +372,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
private roomViewBody = createRef<HTMLDivElement>(); private roomViewBody = createRef<HTMLDivElement>();
public static contextType = SDKContext; public static contextType = SDKContext;
public declare context: React.ContextType<typeof SDKContext>; declare public context: React.ContextType<typeof SDKContext>;
public constructor(props: IRoomProps, context: React.ContextType<typeof SDKContext>) { public constructor(props: IRoomProps, context: React.ContextType<typeof SDKContext>) {
super(props, context); super(props, context);
@ -671,6 +665,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
// the RoomView instance // the RoomView instance
if (initial) { if (initial) {
newState.room = this.context.client!.getRoom(newState.roomId) || undefined; newState.room = this.context.client!.getRoom(newState.roomId) || undefined;
newState.isRoomEncrypted = null;
if (newState.room) { if (newState.room) {
newState.showApps = this.shouldShowApps(newState.room); newState.showApps = this.shouldShowApps(newState.room);
this.onRoomLoaded(newState.room); this.onRoomLoaded(newState.room);
@ -713,6 +708,14 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
if (initial) { if (initial) {
this.setupRoom(newState.room, newState.roomId, !!newState.joining, !!newState.shouldPeek); 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 => { private onConnectedCalls = (): void => {
@ -863,7 +866,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
return isManuallyShown && widgets.length > 0; return isManuallyShown && widgets.length > 0;
} }
public async componentDidMount(): Promise<void> { public componentDidMount(): void {
this.unmounted = false; this.unmounted = false;
this.dispatcherRef = defaultDispatcher.register(this.onAction); this.dispatcherRef = defaultDispatcher.register(this.onAction);
@ -1230,18 +1233,18 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
if (payload.member) { if (payload.member) {
if (payload.push) { if (payload.push) {
RightPanelStore.instance.pushCard({ RightPanelStore.instance.pushCard({
phase: RightPanelPhases.RoomMemberInfo, phase: RightPanelPhases.MemberInfo,
state: { member: payload.member }, state: { member: payload.member },
}); });
} else { } else {
RightPanelStore.instance.setCards([ RightPanelStore.instance.setCards([
{ phase: RightPanelPhases.RoomSummary }, { phase: RightPanelPhases.RoomSummary },
{ phase: RightPanelPhases.RoomMemberList }, { phase: RightPanelPhases.MemberList },
{ phase: RightPanelPhases.RoomMemberInfo, state: { member: payload.member } }, { phase: RightPanelPhases.MemberInfo, state: { member: payload.member } },
]); ]);
} }
} else { } else {
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomMemberList); RightPanelStore.instance.showOrHidePhase(RightPanelPhases.MemberList);
} }
break; break;
case Action.View3pidInvite: case Action.View3pidInvite:
@ -1482,24 +1485,17 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
private async updateE2EStatus(room: Room): Promise<void> { private async updateE2EStatus(room: Room): Promise<void> {
if (!this.context.client || !this.state.isRoomEncrypted) return; if (!this.context.client || !this.state.isRoomEncrypted) return;
const e2eStatus = await this.cacheAndGetE2EStatus(room, this.context.client);
// If crypto is not currently enabled, we aren't tracking devices at all, if (this.unmounted) return;
// 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;
// set the state immediately then update, so we don't scare the user into thinking the room is unencrypted
this.setState({ e2eStatus }); this.setState({ e2eStatus });
if (this.context.client.getCrypto()) {
/* At this point, the user has encryption on and cross-signing on */
e2eStatus = await this.cacheAndGetE2EStatus(room, this.context.client);
if (this.unmounted) return;
this.setState({ e2eStatus });
}
} }
private async cacheAndGetE2EStatus(room: Room, client: MatrixClient): Promise<E2EStatus> { private async cacheAndGetE2EStatus(room: Room, client: MatrixClient): Promise<E2EStatus> {
const e2eStatus = await shieldStatusForRoom(client, room); 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
if (e2eStatus) this.setState({ e2eStatus });
e2eStatus = await shieldStatusForRoom(client, room);
RoomView.e2eStatusCache.set(room.roomId, e2eStatus); RoomView.e2eStatusCache.set(room.roomId, e2eStatus);
return e2eStatus; return e2eStatus;
} }
@ -2005,35 +2001,41 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
if (!this.state.room || !this.context?.client) return null; if (!this.state.room || !this.context?.client) return null;
const names = this.state.room.getDefaultRoomName(this.context.client.getSafeUserId()); const names = this.state.room.getDefaultRoomName(this.context.client.getSafeUserId());
return ( return (
<RoomContext.Provider value={this.state}> <ScopedRoomContextProvider {...this.state}>
<LocalRoomCreateLoader localRoom={localRoom} names={names} resizeNotifier={this.props.resizeNotifier} /> <LocalRoomCreateLoader
</RoomContext.Provider> localRoom={localRoom}
names={names}
resizeNotifier={this.props.resizeNotifier}
mainSplitContentType={this.state.mainSplitContentType}
/>
</ScopedRoomContextProvider>
); );
} }
private renderLocalRoomView(localRoom: LocalRoom): ReactNode { private renderLocalRoomView(localRoom: LocalRoom): ReactNode {
return ( return (
<RoomContext.Provider value={this.state}> <ScopedRoomContextProvider {...this.state}>
<LocalRoomView <LocalRoomView
localRoom={localRoom} localRoom={localRoom}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
permalinkCreator={this.permalinkCreator} permalinkCreator={this.permalinkCreator}
roomView={this.roomView} roomView={this.roomView}
onFileDrop={this.onFileDrop} onFileDrop={this.onFileDrop}
mainSplitContentType={this.state.mainSplitContentType}
/> />
</RoomContext.Provider> </ScopedRoomContextProvider>
); );
} }
private renderWaitingForThirdPartyRoomView(inviteEvent: MatrixEvent): ReactNode { private renderWaitingForThirdPartyRoomView(inviteEvent: MatrixEvent): ReactNode {
return ( return (
<RoomContext.Provider value={this.state}> <ScopedRoomContextProvider {...this.state}>
<WaitingForThirdPartyRoomView <WaitingForThirdPartyRoomView
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
roomView={this.roomView} roomView={this.roomView}
inviteEvent={inviteEvent} inviteEvent={inviteEvent}
/> />
</RoomContext.Provider> </ScopedRoomContextProvider>
); );
} }
@ -2571,7 +2573,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
} }
return ( return (
<RoomContext.Provider value={this.state}> <ScopedRoomContextProvider {...this.state}>
<div className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}> <div className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}>
{showChatEffects && this.roomView.current && ( {showChatEffects && this.roomView.current && (
<EffectsOverlay roomWidth={this.roomView.current.offsetWidth} /> <EffectsOverlay roomWidth={this.roomView.current.offsetWidth} />
@ -2598,7 +2600,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
</MainSplit> </MainSplit>
</ErrorBoundary> </ErrorBoundary>
</div> </div>
</RoomContext.Provider> </ScopedRoomContextProvider>
); );
} }
} }

View file

@ -208,7 +208,7 @@ const SpaceLanding: React.FC<{ space: Room }> = ({ space }) => {
const storeIsShowingSpaceMembers = useCallback( const storeIsShowingSpaceMembers = useCallback(
() => () =>
RightPanelStore.instance.isOpenForRoom(space.roomId) && RightPanelStore.instance.isOpenForRoom(space.roomId) &&
RightPanelStore.instance.currentCardForRoom(space.roomId)?.phase === RightPanelPhases.SpaceMemberList, RightPanelStore.instance.currentCardForRoom(space.roomId)?.phase === RightPanelPhases.MemberList,
[space.roomId], [space.roomId],
); );
const isShowingMembers = useEventEmitterState(RightPanelStore.instance, UPDATE_EVENT, storeIsShowingSpaceMembers); const isShowingMembers = useEventEmitterState(RightPanelStore.instance, UPDATE_EVENT, storeIsShowingSpaceMembers);
@ -251,7 +251,7 @@ const SpaceLanding: React.FC<{ space: Room }> = ({ space }) => {
} }
const onMembersClick = (): void => { const onMembersClick = (): void => {
RightPanelStore.instance.setCard({ phase: RightPanelPhases.SpaceMemberList }); RightPanelStore.instance.setCard({ phase: RightPanelPhases.MemberList });
}; };
return ( return (
@ -597,7 +597,7 @@ const SpaceSetupPrivateInvite: React.FC<{
export default class SpaceRoomView extends React.PureComponent<IProps, IState> { export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>; declare public context: React.ContextType<typeof MatrixClientContext>;
private dispatcherRef?: string; private dispatcherRef?: string;

View file

@ -20,7 +20,7 @@ import MatrixClientContext, { useMatrixClientContext } from "../../contexts/Matr
import { _t } from "../../languageHandler"; import { _t } from "../../languageHandler";
import { ContextMenuButton } from "../../accessibility/context_menu/ContextMenuButton"; import { ContextMenuButton } from "../../accessibility/context_menu/ContextMenuButton";
import ContextMenu, { ChevronFace, MenuItemRadio, useContextMenu } from "./ContextMenu"; import ContextMenu, { ChevronFace, MenuItemRadio, useContextMenu } from "./ContextMenu";
import RoomContext, { TimelineRenderingType, useRoomContext } from "../../contexts/RoomContext"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
import TimelinePanel from "./TimelinePanel"; import TimelinePanel from "./TimelinePanel";
import { Layout } from "../../settings/enums/Layout"; import { Layout } from "../../settings/enums/Layout";
import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
@ -30,6 +30,7 @@ import { ButtonEvent } from "../views/elements/AccessibleButton";
import Spinner from "../views/elements/Spinner"; import Spinner from "../views/elements/Spinner";
import { clearRoomNotification } from "../../utils/notifications"; import { clearRoomNotification } from "../../utils/notifications";
import EmptyState from "../views/right_panel/EmptyState"; import EmptyState from "../views/right_panel/EmptyState";
import { ScopedRoomContextProvider, useScopedRoomContext } from "../../contexts/ScopedRoomContext.tsx";
interface IProps { interface IProps {
roomId: string; roomId: string;
@ -68,7 +69,7 @@ export const ThreadPanelHeader: React.FC<{
setFilterOption: (filterOption: ThreadFilterType) => void; setFilterOption: (filterOption: ThreadFilterType) => void;
}> = ({ filterOption, setFilterOption }) => { }> = ({ filterOption, setFilterOption }) => {
const mxClient = useMatrixClientContext(); const mxClient = useMatrixClientContext();
const roomContext = useRoomContext(); const roomContext = useScopedRoomContext("room");
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu<HTMLElement>(); const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu<HTMLElement>();
const options: readonly ThreadPanelHeaderOption[] = [ const options: readonly ThreadPanelHeaderOption[] = [
{ {
@ -184,13 +185,11 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
}, [timelineSet, timelinePanel]); }, [timelineSet, timelinePanel]);
return ( return (
<RoomContext.Provider <ScopedRoomContextProvider
value={{ {...roomContext}
...roomContext, timelineRenderingType={TimelineRenderingType.ThreadsList}
timelineRenderingType: TimelineRenderingType.ThreadsList, showHiddenEvents={true}
showHiddenEvents: true, narrow={narrow}
narrow,
}}
> >
<BaseCard <BaseCard
header={ header={
@ -241,7 +240,7 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
</div> </div>
)} )}
</BaseCard> </BaseCard>
</RoomContext.Provider> </ScopedRoomContextProvider>
); );
}; };
export default ThreadPanel; export default ThreadPanel;

View file

@ -51,6 +51,7 @@ import { ComposerInsertPayload, ComposerType } from "../../dispatcher/payloads/C
import Heading from "../views/typography/Heading"; import Heading from "../views/typography/Heading";
import { SdkContextClass } from "../../contexts/SDKContext"; import { SdkContextClass } from "../../contexts/SDKContext";
import { ThreadPayload } from "../../dispatcher/payloads/ThreadPayload"; import { ThreadPayload } from "../../dispatcher/payloads/ThreadPayload";
import { ScopedRoomContextProvider } from "../../contexts/ScopedRoomContext.tsx";
interface IProps { interface IProps {
room: Room; room: Room;
@ -75,7 +76,7 @@ interface IState {
export default class ThreadView extends React.Component<IProps, IState> { export default class ThreadView extends React.Component<IProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
private dispatcherRef?: string; private dispatcherRef?: string;
private layoutWatcherRef?: string; private layoutWatcherRef?: string;
@ -422,14 +423,12 @@ export default class ThreadView extends React.Component<IProps, IState> {
} }
return ( return (
<RoomContext.Provider <ScopedRoomContextProvider
value={{ {...this.context}
...this.context, timelineRenderingType={TimelineRenderingType.Thread}
timelineRenderingType: TimelineRenderingType.Thread, threadId={this.state.thread?.id}
threadId: this.state.thread?.id, liveTimeline={this.state?.thread?.timelineSet?.getLiveTimeline()}
liveTimeline: this.state?.thread?.timelineSet?.getLiveTimeline(), narrow={this.state.narrow}
narrow: this.state.narrow,
}}
> >
<BaseCard <BaseCard
className={classNames("mx_ThreadView mx_ThreadPanel", { className={classNames("mx_ThreadView mx_ThreadPanel", {
@ -463,7 +462,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
/> />
)} )}
</BaseCard> </BaseCard>
</RoomContext.Provider> </ScopedRoomContextProvider>
); );
} }
} }

View file

@ -229,7 +229,7 @@ interface IEventIndexOpts {
*/ */
class TimelinePanel extends React.Component<IProps, IState> { class TimelinePanel extends React.Component<IProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
// a map from room id to read marker event timestamp // a map from room id to read marker event timestamp
public static roomReadMarkerTsMap: Record<string, number> = {}; public static roomReadMarkerTsMap: Record<string, number> = {};

View file

@ -40,8 +40,6 @@ import { UPDATE_SELECTED_SPACE } from "../../stores/spaces";
import UserIdentifierCustomisations from "../../customisations/UserIdentifier"; import UserIdentifierCustomisations from "../../customisations/UserIdentifier";
import PosthogTrackers from "../../PosthogTrackers"; import PosthogTrackers from "../../PosthogTrackers";
import { ViewHomePagePayload } from "../../dispatcher/payloads/ViewHomePagePayload"; import { ViewHomePagePayload } from "../../dispatcher/payloads/ViewHomePagePayload";
import { Icon as LiveIcon } from "../../../res/img/compound/live-8px.svg";
import { VoiceBroadcastRecording, VoiceBroadcastRecordingsStoreEvent } from "../../voice-broadcast";
import { SDKContext } from "../../contexts/SDKContext"; import { SDKContext } from "../../contexts/SDKContext";
import { shouldShowFeedback } from "../../utils/Feedback"; import { shouldShowFeedback } from "../../utils/Feedback";
import DarkLightModeSvg from "../../../res/img/element-icons/roomlist/dark-light-mode.svg"; import DarkLightModeSvg from "../../../res/img/element-icons/roomlist/dark-light-mode.svg";
@ -58,7 +56,6 @@ interface IState {
isDarkTheme: boolean; isDarkTheme: boolean;
isHighContrast: boolean; isHighContrast: boolean;
selectedSpace?: Room | null; selectedSpace?: Room | null;
showLiveAvatarAddon: boolean;
} }
const toRightOf = (rect: PartialDOMRect): MenuProps => { const toRightOf = (rect: PartialDOMRect): MenuProps => {
@ -79,7 +76,7 @@ const below = (rect: PartialDOMRect): MenuProps => {
export default class UserMenu extends React.Component<IProps, IState> { export default class UserMenu extends React.Component<IProps, IState> {
public static contextType = SDKContext; public static contextType = SDKContext;
public declare context: React.ContextType<typeof SDKContext>; declare public context: React.ContextType<typeof SDKContext>;
private dispatcherRef?: string; private dispatcherRef?: string;
private themeWatcherRef?: string; private themeWatcherRef?: string;
@ -94,7 +91,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
isDarkTheme: this.isUserOnDarkTheme(), isDarkTheme: this.isUserOnDarkTheme(),
isHighContrast: this.isUserOnHighContrastTheme(), isHighContrast: this.isUserOnHighContrastTheme(),
selectedSpace: SpaceStore.instance.activeSpaceRoom, selectedSpace: SpaceStore.instance.activeSpaceRoom,
showLiveAvatarAddon: this.context.voiceBroadcastRecordingsStore.hasCurrent(),
}; };
} }
@ -102,19 +98,9 @@ export default class UserMenu extends React.Component<IProps, IState> {
return !!getHomePageUrl(SdkConfig.get(), this.context.client!); return !!getHomePageUrl(SdkConfig.get(), this.context.client!);
} }
private onCurrentVoiceBroadcastRecordingChanged = (recording: VoiceBroadcastRecording | null): void => {
this.setState({
showLiveAvatarAddon: recording !== null,
});
};
public componentDidMount(): void { public componentDidMount(): void {
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate); OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate); SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
this.context.voiceBroadcastRecordingsStore.on(
VoiceBroadcastRecordingsStoreEvent.CurrentChanged,
this.onCurrentVoiceBroadcastRecordingChanged,
);
this.dispatcherRef = defaultDispatcher.register(this.onAction); this.dispatcherRef = defaultDispatcher.register(this.onAction);
this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged); this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged);
} }
@ -125,10 +111,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
defaultDispatcher.unregister(this.dispatcherRef); defaultDispatcher.unregister(this.dispatcherRef);
OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate); OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate);
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate); SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
this.context.voiceBroadcastRecordingsStore.off(
VoiceBroadcastRecordingsStoreEvent.CurrentChanged,
this.onCurrentVoiceBroadcastRecordingChanged,
);
} }
private isUserOnDarkTheme(): boolean { private isUserOnDarkTheme(): boolean {
@ -435,12 +417,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
name = <div className="mx_UserMenu_name">{displayName}</div>; name = <div className="mx_UserMenu_name">{displayName}</div>;
} }
const liveAvatarAddon = this.state.showLiveAvatarAddon ? (
<div className="mx_UserMenu_userAvatarLive" data-testid="user-menu-live-vb">
<LiveIcon className="mx_Icon_8" />
</div>
) : null;
return ( return (
<div className="mx_UserMenu"> <div className="mx_UserMenu">
<ContextMenuButton <ContextMenuButton
@ -459,7 +435,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
size={avatarSize + "px"} size={avatarSize + "px"}
className="mx_UserMenu_userAvatar_BaseAvatar" className="mx_UserMenu_userAvatar_BaseAvatar"
/> />
{liveAvatarAddon}
</div> </div>
{name} {name}
{this.renderContextMenu()} {this.renderContextMenu()}

View file

@ -32,7 +32,7 @@ interface IState {
export default class UserView extends React.Component<IProps, IState> { export default class UserView extends React.Component<IProps, IState> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>; declare public context: React.ContextType<typeof MatrixClientContext>;
public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) { public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
super(props, context); super(props, context);
@ -82,7 +82,7 @@ export default class UserView extends React.Component<IProps, IState> {
} else if (this.state.member) { } else if (this.state.member) {
const panel = ( const panel = (
<RightPanel <RightPanel
overwriteCard={{ phase: RightPanelPhases.RoomMemberInfo, state: { member: this.state.member } }} overwriteCard={{ phase: RightPanelPhases.MemberInfo, state: { member: this.state.member } }}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
/> />
); );

View file

@ -9,7 +9,6 @@ Please see LICENSE files in the repository root for full details.
import React, { RefObject } from "react"; import React, { RefObject } from "react";
import { MatrixEvent } from "matrix-js-sdk/src/matrix"; import { MatrixEvent } from "matrix-js-sdk/src/matrix";
import { useRoomContext } from "../../contexts/RoomContext";
import ResizeNotifier from "../../utils/ResizeNotifier"; import ResizeNotifier from "../../utils/ResizeNotifier";
import ErrorBoundary from "../views/elements/ErrorBoundary"; import ErrorBoundary from "../views/elements/ErrorBoundary";
import RoomHeader from "../views/rooms/RoomHeader"; import RoomHeader from "../views/rooms/RoomHeader";
@ -19,6 +18,7 @@ import NewRoomIntro from "../views/rooms/NewRoomIntro";
import { UnwrappedEventTile } from "../views/rooms/EventTile"; import { UnwrappedEventTile } from "../views/rooms/EventTile";
import { _t } from "../../languageHandler"; import { _t } from "../../languageHandler";
import SdkConfig from "../../SdkConfig"; import SdkConfig from "../../SdkConfig";
import { useScopedRoomContext } from "../../contexts/ScopedRoomContext.tsx";
interface Props { interface Props {
roomView: RefObject<HTMLElement>; roomView: RefObject<HTMLElement>;
@ -32,7 +32,7 @@ interface Props {
* To avoid UTDs, users are shown a waiting room until the others have joined. * To avoid UTDs, users are shown a waiting room until the others have joined.
*/ */
export const WaitingForThirdPartyRoomView: React.FC<Props> = ({ roomView, resizeNotifier, inviteEvent }) => { export const WaitingForThirdPartyRoomView: React.FC<Props> = ({ roomView, resizeNotifier, inviteEvent }) => {
const context = useRoomContext(); const context = useScopedRoomContext("room");
const brand = SdkConfig.get().brand; const brand = SdkConfig.get().brand;
return ( return (

View file

@ -64,7 +64,7 @@ interface IState {
export default class SoftLogout extends React.Component<IProps, IState> { export default class SoftLogout extends React.Component<IProps, IState> {
public static contextType = SDKContext; public static contextType = SDKContext;
public declare context: React.ContextType<typeof SDKContext>; declare public context: React.ContextType<typeof SDKContext>;
public constructor(props: IProps, context: React.ContextType<typeof SDKContext>) { public constructor(props: IProps, context: React.ContextType<typeof SDKContext>) {
super(props, context); super(props, context);

View file

@ -12,7 +12,6 @@ import { KnownMembership } from "matrix-js-sdk/src/types";
import { BaseGrouper } from "./BaseGrouper"; import { BaseGrouper } from "./BaseGrouper";
import MessagePanel, { WrappedEvent } from "../MessagePanel"; import MessagePanel, { WrappedEvent } from "../MessagePanel";
import { VoiceBroadcastInfoEventType } from "../../../voice-broadcast";
import DMRoomMap from "../../../utils/DMRoomMap"; import DMRoomMap from "../../../utils/DMRoomMap";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import DateSeparator from "../../views/messages/DateSeparator"; import DateSeparator from "../../views/messages/DateSeparator";
@ -53,11 +52,6 @@ export class CreationGrouper extends BaseGrouper {
return false; return false;
} }
if (VoiceBroadcastInfoEventType === eventType) {
// always show voice broadcast info events in timeline
return false;
}
if (event.isState() && event.getSender() === createEvent.getSender()) { if (event.isState() && event.getSender() === createEvent.getSender()) {
return true; return true;
} }

View file

@ -1,45 +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, { MutableRefObject } from "react";
import { toLeftOrRightOf } from "../../structures/ContextMenu";
import IconizedContextMenu, {
IconizedContextMenuOptionList,
IconizedContextMenuRadio,
} from "../context_menus/IconizedContextMenu";
interface Props {
containerRef: MutableRefObject<HTMLElement | null>;
currentDevice: MediaDeviceInfo | null;
devices: MediaDeviceInfo[];
onDeviceSelect: (device: MediaDeviceInfo) => void;
}
export const DevicesContextMenu: React.FC<Props> = ({ containerRef, currentDevice, devices, onDeviceSelect }) => {
const deviceOptions = devices.map((d: MediaDeviceInfo) => {
return (
<IconizedContextMenuRadio
key={d.deviceId}
active={d.deviceId === currentDevice?.deviceId}
onClick={() => onDeviceSelect(d)}
label={d.label}
/>
);
});
return (
<IconizedContextMenu
mountAsChild={false}
onFinished={() => {}}
{...(containerRef.current ? toLeftOrRightOf(containerRef.current.getBoundingClientRect(), 0) : {})}
>
<IconizedContextMenuOptionList>{deviceOptions}</IconizedContextMenuOptionList>
</IconizedContextMenu>
);
};

View file

@ -108,12 +108,9 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
private generateAndShowCode = async (): Promise<void> => { private generateAndShowCode = async (): Promise<void> => {
let rendezvous: MSC4108SignInWithQR; let rendezvous: MSC4108SignInWithQR;
try { try {
const fallbackRzServer = this.props.client?.getClientWellKnown()?.["io.element.rendezvous"]?.server;
const transport = new MSC4108RendezvousSession({ const transport = new MSC4108RendezvousSession({
onFailure: this.onFailure, onFailure: this.onFailure,
client: this.props.client, client: this.props.client,
fallbackRzServer,
}); });
await transport.send(""); await transport.send("");
const channel = new MSC4108SecureChannel(transport, undefined, this.onFailure); const channel = new MSC4108SecureChannel(transport, undefined, this.onFailure);

View file

@ -16,10 +16,10 @@ import { Avatar } from "@vector-im/compound-web";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { ButtonEvent } from "../elements/AccessibleButton"; import { ButtonEvent } from "../elements/AccessibleButton";
import RoomContext from "../../../contexts/RoomContext";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter"; import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx";
interface IProps { interface IProps {
name?: React.ComponentProps<typeof Avatar>["name"]; // The name (first initial used as default) name?: React.ComponentProps<typeof Avatar>["name"]; // The name (first initial used as default)
@ -57,8 +57,8 @@ const calculateUrls = (url?: string | null, urls?: string[], lowBandwidth = fals
const useImageUrl = ({ url, urls }: { url?: string | null; urls?: string[] }): [string, () => void] => { const useImageUrl = ({ url, urls }: { url?: string | null; urls?: string[] }): [string, () => void] => {
// Since this is a hot code path and the settings store can be slow, we // Since this is a hot code path and the settings store can be slow, we
// use the cached lowBandwidth value from the room context if it exists // use the cached lowBandwidth value from the room context if it exists
const roomContext = useContext(RoomContext); const roomContext = useScopedRoomContext("lowBandwidth");
const lowBandwidth = roomContext ? roomContext.lowBandwidth : SettingsStore.getValue("lowBandwidth"); const lowBandwidth = roomContext?.lowBandwidth ?? SettingsStore.getValue("lowBandwidth");
const [imageUrls, setUrls] = useState<string[]>(calculateUrls(url, urls, lowBandwidth)); const [imageUrls, setUrls] = useState<string[]>(calculateUrls(url, urls, lowBandwidth));
const [urlsIndex, setIndex] = useState<number>(0); const [urlsIndex, setIndex] = useState<number>(0);

View file

@ -126,7 +126,7 @@ interface IState {
export default class MessageContextMenu extends React.Component<IProps, IState> { export default class MessageContextMenu extends React.Component<IProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
private reactButtonRef = createRef<any>(); // XXX Ref to a functional component private reactButtonRef = createRef<any>(); // XXX Ref to a functional component

View file

@ -18,7 +18,6 @@ import { _t } from "../../../languageHandler";
import { isAppWidget } from "../../../stores/WidgetStore"; import { isAppWidget } from "../../../stores/WidgetStore";
import WidgetUtils from "../../../utils/WidgetUtils"; import WidgetUtils from "../../../utils/WidgetUtils";
import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore"; import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
import RoomContext from "../../../contexts/RoomContext";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
@ -30,6 +29,7 @@ import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayo
import { getConfigLivestreamUrl, startJitsiAudioLivestream } from "../../../Livestream"; import { getConfigLivestreamUrl, startJitsiAudioLivestream } from "../../../Livestream";
import { ModuleRunner } from "../../../modules/ModuleRunner"; import { ModuleRunner } from "../../../modules/ModuleRunner";
import { ElementWidget } from "../../../stores/widgets/StopGapWidget"; import { ElementWidget } from "../../../stores/widgets/StopGapWidget";
import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx";
interface IProps extends Omit<ComponentProps<typeof IconizedContextMenu>, "children"> { interface IProps extends Omit<ComponentProps<typeof IconizedContextMenu>, "children"> {
app: IWidget; app: IWidget;
@ -114,7 +114,7 @@ export const WidgetContextMenu: React.FC<IProps> = ({
...props ...props
}) => { }) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const { room, roomId } = useContext(RoomContext); const { room, roomId } = useScopedRoomContext("room", "roomId");
const widgetMessaging = WidgetMessagingStore.instance.getMessagingForUid(WidgetUtils.getWidgetUid(app)); const widgetMessaging = WidgetMessagingStore.instance.getMessagingForUid(WidgetUtils.getWidgetUid(app));
const canModify = userWidget || WidgetUtils.canUserModifyWidgets(cli, roomId); const canModify = userWidget || WidgetUtils.canUserModifyWidgets(cli, roomId);

View file

@ -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 { _t } from "../../../languageHandler";
import Modal from "../../../Modal";
import InfoDialog from "./InfoDialog";
export const createCantStartVoiceMessageBroadcastDialog = (): void => {
Modal.createDialog(InfoDialog, {
title: _t("voice_message|cant_start_broadcast_title"),
description: <p>{_t("voice_message|cant_start_broadcast_description")}</p>,
hasCloseButton: true,
});
};

View file

@ -6,14 +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. Please see LICENSE files in the repository root for full details.
*/ */
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature"; import { IRedactOpts, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { IRedactOpts, MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix";
import React from "react"; import React from "react";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import { isVoiceBroadcastStartedEvent } from "../../../voice-broadcast/utils/isVoiceBroadcastStartedEvent";
import ErrorDialog from "./ErrorDialog"; import ErrorDialog from "./ErrorDialog";
import TextInputDialog from "./TextInputDialog"; import TextInputDialog from "./TextInputDialog";
@ -70,18 +68,6 @@ export function createRedactEventDialog({
const cli = MatrixClientPeg.safeGet(); const cli = MatrixClientPeg.safeGet();
const withRelTypes: Pick<IRedactOpts, "with_rel_types"> = {}; const withRelTypes: Pick<IRedactOpts, "with_rel_types"> = {};
// redact related events if this is a voice broadcast started event and
// server has support for relation based redactions
if (isVoiceBroadcastStartedEvent(mxEvent)) {
const relationBasedRedactionsSupport = cli.canSupport.get(Feature.RelationBasedRedactions);
if (
relationBasedRedactionsSupport &&
relationBasedRedactionsSupport !== ServerSupport.Unsupported
) {
withRelTypes.with_rel_types = [RelationType.Reference];
}
}
try { try {
onCloseDialog?.(); onCloseDialog?.();
await cli.redactEvent(roomId, eventId, undefined, { await cli.redactEvent(roomId, eventId, undefined, {

View file

@ -22,7 +22,6 @@ import { AccountDataExplorer, RoomAccountDataExplorer } from "./devtools/Account
import SettingsFlag from "../elements/SettingsFlag"; import SettingsFlag from "../elements/SettingsFlag";
import { SettingLevel } from "../../../settings/SettingLevel"; import { SettingLevel } from "../../../settings/SettingLevel";
import ServerInfo from "./devtools/ServerInfo"; import ServerInfo from "./devtools/ServerInfo";
import { Features } from "../../../settings/Settings";
import CopyableText from "../elements/CopyableText"; import CopyableText from "../elements/CopyableText";
import RoomNotifications from "./devtools/RoomNotifications"; import RoomNotifications from "./devtools/RoomNotifications";
@ -100,7 +99,6 @@ const DevtoolsDialog: React.FC<IProps> = ({ roomId, threadRootId, onFinished })
<SettingsFlag name="developerMode" level={SettingLevel.ACCOUNT} /> <SettingsFlag name="developerMode" level={SettingLevel.ACCOUNT} />
<SettingsFlag name="showHiddenEventsInTimeline" level={SettingLevel.DEVICE} /> <SettingsFlag name="showHiddenEventsInTimeline" level={SettingLevel.DEVICE} />
<SettingsFlag name="enableWidgetScreenshots" level={SettingLevel.ACCOUNT} /> <SettingsFlag name="enableWidgetScreenshots" level={SettingLevel.ACCOUNT} />
<SettingsFlag name={Features.VoiceBroadcastForceSmallChunks} level={SettingLevel.DEVICE} />
</div> </div>
</BaseTool> </BaseTool>
); );

View file

@ -116,7 +116,7 @@ interface IState {
export default class AppTile extends React.Component<IProps, IState> { export default class AppTile extends React.Component<IProps, IState> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: ContextType<typeof MatrixClientContext>; declare public context: ContextType<typeof MatrixClientContext>;
public static defaultProps: Partial<IProps> = { public static defaultProps: Partial<IProps> = {
waitForIframeLoad: true, waitForIframeLoad: true,

View file

@ -73,7 +73,7 @@ export default class EventListSummary extends React.Component<
IProps & Required<Pick<IProps, "summaryLength" | "threshold" | "avatarsMaxLength" | "layout">> IProps & Required<Pick<IProps, "summaryLength" | "threshold" | "avatarsMaxLength" | "layout">>
> { > {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
public static defaultProps = { public static defaultProps = {
summaryLength: 1, summaryLength: 1,

View file

@ -12,12 +12,12 @@ import React, { useContext, useRef, useState, MouseEvent, ReactNode } from "reac
import { Tooltip } from "@vector-im/compound-web"; import { Tooltip } from "@vector-im/compound-web";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import RoomContext from "../../../contexts/RoomContext";
import { useTimeout } from "../../../hooks/useTimeout"; import { useTimeout } from "../../../hooks/useTimeout";
import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds";
import AccessibleButton from "./AccessibleButton"; import AccessibleButton from "./AccessibleButton";
import Spinner from "./Spinner"; import Spinner from "./Spinner";
import { getFileChanged } from "../settings/AvatarSetting.tsx"; import { getFileChanged } from "../settings/AvatarSetting.tsx";
import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx";
export const AVATAR_SIZE = "52px"; export const AVATAR_SIZE = "52px";
@ -56,7 +56,7 @@ const MiniAvatarUploader: React.FC<IProps> = ({
const label = hasAvatar || busy ? hasAvatarLabel : noAvatarLabel; const label = hasAvatar || busy ? hasAvatarLabel : noAvatarLabel;
const { room } = useContext(RoomContext); const { room } = useScopedRoomContext("room");
const canSetAvatar = const canSetAvatar =
isUserAvatar || room?.currentState?.maySendStateEvent(EventType.RoomAvatar, cli.getSafeUserId()); isUserAvatar || room?.currentState?.maySendStateEvent(EventType.RoomAvatar, cli.getSafeUserId());
if (!canSetAvatar) return <React.Fragment>{children}</React.Fragment>; if (!canSetAvatar) return <React.Fragment>{children}</React.Fragment>;

View file

@ -25,7 +25,7 @@ interface IProps {
export default class PersistentApp extends React.Component<IProps> { export default class PersistentApp extends React.Component<IProps> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: ContextType<typeof MatrixClientContext>; declare public context: ContextType<typeof MatrixClientContext>;
private room: Room; private room: Room;
public constructor(props: IProps, context: ContextType<typeof MatrixClientContext>) { public constructor(props: IProps, context: ContextType<typeof MatrixClientContext>) {

View file

@ -65,7 +65,7 @@ interface IState {
// be low as each event being loaded (after the first) is triggered by an explicit user action. // be low as each event being loaded (after the first) is triggered by an explicit user action.
export default class ReplyChain extends React.Component<IProps, IState> { export default class ReplyChain extends React.Component<IProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
private unmounted = false; private unmounted = false;
private room: Room; private room: Room;

View file

@ -33,7 +33,7 @@ interface IState {
// Controlled form component wrapping Field for inputting a room alias scoped to a given domain // Controlled form component wrapping Field for inputting a room alias scoped to a given domain
export default class RoomAliasField extends React.PureComponent<IProps, IState> { export default class RoomAliasField extends React.PureComponent<IProps, IState> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>; declare public context: React.ContextType<typeof MatrixClientContext>;
private fieldRef = createRef<Field>(); private fieldRef = createRef<Field>();

View file

@ -29,7 +29,7 @@ interface IState {
class ReactionPicker extends React.Component<IProps, IState> { class ReactionPicker extends React.Component<IProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
public constructor(props: IProps, context: React.ContextType<typeof RoomContext>) { public constructor(props: IProps, context: React.ContextType<typeof RoomContext>) {
super(props, context); super(props, context);

View file

@ -23,7 +23,7 @@ interface IProps {
class Search extends React.PureComponent<IProps> { class Search extends React.PureComponent<IProps> {
public static contextType = RovingTabIndexContext; public static contextType = RovingTabIndexContext;
public declare context: React.ContextType<typeof RovingTabIndexContext>; declare public context: React.ContextType<typeof RovingTabIndexContext>;
private inputRef = React.createRef<HTMLInputElement>(); private inputRef = React.createRef<HTMLInputElement>();

View file

@ -42,7 +42,7 @@ const isSharingOwnLocation = (shareType: LocationShareType): boolean =>
class LocationPicker extends React.Component<ILocationPickerProps, IState> { class LocationPicker extends React.Component<ILocationPickerProps, IState> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>; declare public context: React.ContextType<typeof MatrixClientContext>;
private map?: maplibregl.Map; private map?: maplibregl.Map;
private geolocate?: maplibregl.GeolocateControl; private geolocate?: maplibregl.GeolocateControl;
private marker?: maplibregl.Marker; private marker?: maplibregl.Marker;

View file

@ -45,7 +45,7 @@ interface IState {
export default class EditHistoryMessage extends React.PureComponent<IProps, IState> { export default class EditHistoryMessage extends React.PureComponent<IProps, IState> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>; declare public context: React.ContextType<typeof MatrixClientContext>;
private content = createRef<HTMLDivElement>(); private content = createRef<HTMLDivElement>();
private pills = new ReactRootManager(); private pills = new ReactRootManager();

View file

@ -30,7 +30,7 @@ interface IState {
export default class MAudioBody extends React.PureComponent<IBodyProps, IState> { export default class MAudioBody extends React.PureComponent<IBodyProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
public state: IState = {}; public state: IState = {};

View file

@ -102,7 +102,7 @@ interface IState {
export default class MFileBody extends React.Component<IProps, IState> { export default class MFileBody extends React.Component<IProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
public state: IState = {}; public state: IState = {};

View file

@ -51,13 +51,14 @@ interface IState {
naturalHeight: number; naturalHeight: number;
}; };
hover: boolean; hover: boolean;
focus: boolean;
showImage: boolean; showImage: boolean;
placeholder: Placeholder; placeholder: Placeholder;
} }
export default class MImageBody extends React.Component<IBodyProps, IState> { export default class MImageBody extends React.Component<IBodyProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
private unmounted = false; private unmounted = false;
private image = createRef<HTMLImageElement>(); private image = createRef<HTMLImageElement>();
@ -71,6 +72,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
imgError: false, imgError: false,
imgLoaded: false, imgLoaded: false,
hover: false, hover: false,
focus: false,
showImage: SettingsStore.getValue("showImages"), showImage: SettingsStore.getValue("showImages"),
placeholder: Placeholder.NoImage, placeholder: Placeholder.NoImage,
}; };
@ -120,30 +122,29 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
} }
}; };
protected onImageEnter = (e: React.MouseEvent<HTMLImageElement>): void => { private get shouldAutoplay(): boolean {
this.setState({ hover: true }); return !(
if (
!this.state.contentUrl || !this.state.contentUrl ||
!this.state.showImage || !this.state.showImage ||
!this.state.isAnimated || !this.state.isAnimated ||
SettingsStore.getValue("autoplayGifs") SettingsStore.getValue("autoplayGifs")
) { );
return; }
}
const imgElement = e.currentTarget; protected onImageEnter = (): void => {
imgElement.src = this.state.contentUrl; this.setState({ hover: true });
}; };
protected onImageLeave = (e: React.MouseEvent<HTMLImageElement>): void => { protected onImageLeave = (): void => {
this.setState({ hover: false }); this.setState({ hover: false });
};
const url = this.state.thumbUrl ?? this.state.contentUrl; private onFocus = (): void => {
if (!url || !this.state.showImage || !this.state.isAnimated || SettingsStore.getValue("autoplayGifs")) { this.setState({ focus: true });
return; };
}
const imgElement = e.currentTarget; private onBlur = (): void => {
imgElement.src = url; this.setState({ focus: false });
}; };
private reconnectedListener = createReconnectedListener((): void => { private reconnectedListener = createReconnectedListener((): void => {
@ -470,14 +471,20 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
let showPlaceholder = Boolean(placeholder); let showPlaceholder = Boolean(placeholder);
const hoverOrFocus = this.state.hover || this.state.focus;
if (thumbUrl && !this.state.imgError) { 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 // Restrict the width of the thumbnail here, otherwise it will fill the container
// which has the same width as the timeline // which has the same width as the timeline
// mx_MImageBody_thumbnail resizes img to exactly container size // mx_MImageBody_thumbnail resizes img to exactly container size
img = ( img = (
<img <img
className="mx_MImageBody_thumbnail" className="mx_MImageBody_thumbnail"
src={thumbUrl} src={url}
ref={this.image} ref={this.image}
alt={content.body} alt={content.body}
onError={this.onImageError} onError={this.onImageError}
@ -493,13 +500,13 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
showPlaceholder = false; // because we're hiding the image, so don't show the placeholder. 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 // XXX: Arguably we may want a different label when the animated image is WEBP and not GIF
gifLabel = <p className="mx_MImageBody_gifLabel">GIF</p>; gifLabel = <p className="mx_MImageBody_gifLabel">GIF</p>;
} }
let banner: ReactNode | undefined; let banner: ReactNode | undefined;
if (this.state.showImage && this.state.hover) { if (this.state.showImage && hoverOrFocus) {
banner = this.getBanner(content); banner = this.getBanner(content);
} }
@ -568,7 +575,13 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
protected wrapImage(contentUrl: string | null | undefined, children: JSX.Element): ReactNode { protected wrapImage(contentUrl: string | null | undefined, children: JSX.Element): ReactNode {
if (contentUrl) { if (contentUrl) {
return ( return (
<a href={contentUrl} target={this.props.forExport ? "_blank" : undefined} onClick={this.onClick}> <a
href={contentUrl}
target={this.props.forExport ? "_blank" : undefined}
onClick={this.onClick}
onFocus={this.onFocus}
onBlur={this.onBlur}
>
{children} {children}
</a> </a>
); );
@ -657,17 +670,14 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
} }
interface PlaceholderIProps { interface PlaceholderIProps {
hover?: boolean;
maxWidth?: number; maxWidth?: number;
} }
export class HiddenImagePlaceholder extends React.PureComponent<PlaceholderIProps> { export class HiddenImagePlaceholder extends React.PureComponent<PlaceholderIProps> {
public render(): React.ReactNode { public render(): React.ReactNode {
const maxWidth = this.props.maxWidth ? this.props.maxWidth + "px" : null; const maxWidth = this.props.maxWidth ? this.props.maxWidth + "px" : null;
let className = "mx_HiddenImagePlaceholder";
if (this.props.hover) className += " mx_HiddenImagePlaceholder_hover";
return ( return (
<div className={className} style={{ maxWidth: `min(100%, ${maxWidth}px)` }}> <div className="mx_HiddenImagePlaceholder" style={{ maxWidth: `min(100%, ${maxWidth}px)` }}>
<div className="mx_HiddenImagePlaceholder_button"> <div className="mx_HiddenImagePlaceholder_button">
<span className="mx_HiddenImagePlaceholder_eye" /> <span className="mx_HiddenImagePlaceholder_eye" />
<span>{_t("timeline|m.image|show_image")}</span> <span>{_t("timeline|m.image|show_image")}</span>

View file

@ -30,7 +30,7 @@ interface IState {
export default class MLocationBody extends React.Component<IBodyProps, IState> { export default class MLocationBody extends React.Component<IBodyProps, IState> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>; declare public context: React.ContextType<typeof MatrixClientContext>;
private unmounted = false; private unmounted = false;
private mapId: string; private mapId: string;

View file

@ -139,7 +139,7 @@ export function launchPollEditor(mxEvent: MatrixEvent, getRelationsForEvent?: Ge
export default class MPollBody extends React.Component<IBodyProps, IState> { export default class MPollBody extends React.Component<IBodyProps, IState> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>; declare public context: React.ContextType<typeof MatrixClientContext>;
private seenEventIds: string[] = []; // Events we have already seen private seenEventIds: string[] = []; // Events we have already seen
public constructor(props: IBodyProps, context: React.ContextType<typeof MatrixClientContext>) { public constructor(props: IBodyProps, context: React.ContextType<typeof MatrixClientContext>) {

View file

@ -34,7 +34,7 @@ interface IState {
export default class MVideoBody extends React.PureComponent<IBodyProps, IState> { export default class MVideoBody extends React.PureComponent<IBodyProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
private videoRef = React.createRef<HTMLVideoElement>(); private videoRef = React.createRef<HTMLVideoElement>();
private sizeWatcher?: string; private sizeWatcher?: string;

View file

@ -58,7 +58,6 @@ import { ALTERNATE_KEY_NAME } from "../../../accessibility/KeyboardShortcuts";
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload"; import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload";
import { GetRelationsForEvent, IEventTileType } from "../rooms/EventTile"; import { GetRelationsForEvent, IEventTileType } from "../rooms/EventTile";
import { VoiceBroadcastInfoEventType } from "../../../voice-broadcast/types";
import { ButtonEvent } from "../elements/AccessibleButton"; import { ButtonEvent } from "../elements/AccessibleButton";
import PinningUtils from "../../../utils/PinningUtils"; import PinningUtils from "../../../utils/PinningUtils";
import PosthogTrackers from "../../../PosthogTrackers.ts"; import PosthogTrackers from "../../../PosthogTrackers.ts";
@ -262,7 +261,7 @@ interface IMessageActionBarProps {
export default class MessageActionBar extends React.PureComponent<IMessageActionBarProps> { export default class MessageActionBar extends React.PureComponent<IMessageActionBarProps> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
public componentDidMount(): void { public componentDidMount(): void {
if (this.props.mxEvent.status && this.props.mxEvent.status !== EventStatus.SENT) { if (this.props.mxEvent.status && this.props.mxEvent.status !== EventStatus.SENT) {
@ -354,8 +353,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
* until cross-platform support * until cross-platform support
* (PSF-1041) * (PSF-1041)
*/ */
!M_BEACON_INFO.matches(this.props.mxEvent.getType()) && !M_BEACON_INFO.matches(this.props.mxEvent.getType());
!(this.props.mxEvent.getType() === VoiceBroadcastInfoEventType);
return inNotThreadTimeline && isAllowedMessageType; return inNotThreadTimeline && isAllowedMessageType;
} }

View file

@ -41,7 +41,6 @@ import MjolnirBody from "./MjolnirBody";
import MBeaconBody from "./MBeaconBody"; import MBeaconBody from "./MBeaconBody";
import { DecryptionFailureBody } from "./DecryptionFailureBody"; import { DecryptionFailureBody } from "./DecryptionFailureBody";
import { GetRelationsForEvent, IEventTileOps } from "../rooms/EventTile"; import { GetRelationsForEvent, IEventTileOps } from "../rooms/EventTile";
import { VoiceBroadcastBody, VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../../../voice-broadcast";
// onMessageAllowed is handled internally // onMessageAllowed is handled internally
interface IProps extends Omit<IBodyProps, "onMessageAllowed" | "mediaEventHelper"> { interface IProps extends Omit<IBodyProps, "onMessageAllowed" | "mediaEventHelper"> {
@ -85,7 +84,7 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
private evTypes = new Map<string, React.ComponentType<IBodyProps>>(baseEvTypes.entries()); private evTypes = new Map<string, React.ComponentType<IBodyProps>>(baseEvTypes.entries());
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>; declare public context: React.ContextType<typeof MatrixClientContext>;
public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) { public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
super(props, context); super(props, context);
@ -276,10 +275,6 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
if (M_LOCATION.matches(type) || (type === EventType.RoomMessage && msgtype === MsgType.Location)) { if (M_LOCATION.matches(type) || (type === EventType.RoomMessage && msgtype === MsgType.Location)) {
BodyType = MLocationBody; BodyType = MLocationBody;
} }
if (type === VoiceBroadcastInfoEventType && content?.state === VoiceBroadcastInfoState.Started) {
BodyType = VoiceBroadcastBody;
}
} }
if (SettingsStore.getValue("feature_mjolnir")) { if (SettingsStore.getValue("feature_mjolnir")) {

View file

@ -75,7 +75,7 @@ interface IState {
export default class ReactionsRow extends React.PureComponent<IProps, IState> { export default class ReactionsRow extends React.PureComponent<IProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
public constructor(props: IProps, context: React.ContextType<typeof RoomContext>) { public constructor(props: IProps, context: React.ContextType<typeof RoomContext>) {
super(props, context); super(props, context);

View file

@ -38,7 +38,7 @@ export interface IProps {
export default class ReactionsRowButton extends React.PureComponent<IProps> { export default class ReactionsRowButton extends React.PureComponent<IProps> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>; declare public context: React.ContextType<typeof MatrixClientContext>;
public onClick = (): void => { public onClick = (): void => {
const { mxEvent, myReactionEvent, content } = this.props; const { mxEvent, myReactionEvent, content } = this.props;

View file

@ -28,7 +28,7 @@ interface IProps {
export default class ReactionsRowButtonTooltip extends React.PureComponent<PropsWithChildren<IProps>> { export default class ReactionsRowButtonTooltip extends React.PureComponent<PropsWithChildren<IProps>> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>; declare public context: React.ContextType<typeof MatrixClientContext>;
public render(): React.ReactNode { public render(): React.ReactNode {
const { content, reactionEvents, mxEvent, children } = this.props; const { content, reactionEvents, mxEvent, children } = this.props;

View file

@ -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. 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 { logger } from "matrix-js-sdk/src/logger";
import { MatrixEvent, Room, RoomState } from "matrix-js-sdk/src/matrix"; import { MatrixEvent, Room, RoomState } from "matrix-js-sdk/src/matrix";
@ -18,10 +18,10 @@ import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import EventTileBubble from "./EventTileBubble"; import EventTileBubble from "./EventTileBubble";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import RoomContext from "../../../contexts/RoomContext";
import { useRoomState } from "../../../hooks/useRoomState"; import { useRoomState } from "../../../hooks/useRoomState";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import MatrixToPermalinkConstructor from "../../../utils/permalinks/MatrixToPermalinkConstructor"; import MatrixToPermalinkConstructor from "../../../utils/permalinks/MatrixToPermalinkConstructor";
import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx";
interface IProps { interface IProps {
/** The m.room.create MatrixEvent that this tile represents */ /** The m.room.create MatrixEvent that this tile represents */
@ -40,7 +40,7 @@ export const RoomPredecessorTile: React.FC<IProps> = ({ mxEvent, timestamp }) =>
// the information inside mxEvent. This allows us the flexibility later to // the information inside mxEvent. This allows us the flexibility later to
// use a different predecessor (e.g. through MSC3946) and still display it // use a different predecessor (e.g. through MSC3946) and still display it
// in the timeline location of the create event. // in the timeline location of the create event.
const roomContext = useContext(RoomContext); const roomContext = useScopedRoomContext("room");
const predecessor = useRoomState( const predecessor = useRoomState(
roomContext.room, roomContext.room,
useCallback( useCallback(

View file

@ -52,10 +52,8 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
private tooltips = new ReactRootManager(); private tooltips = new ReactRootManager();
private reactRoots = new ReactRootManager(); private reactRoots = new ReactRootManager();
private ref = createRef<HTMLDivElement>();
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
public state = { public state = {
links: [], links: [],
@ -86,7 +84,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") { if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") {
// Handle expansion and add buttons // Handle expansion and add buttons
const pres = this.ref.current?.getElementsByTagName("pre"); const pres = [...content.getElementsByTagName("pre")];
if (pres && pres.length > 0) { if (pres && pres.length > 0) {
for (let i = 0; i < pres.length; i++) { for (let i = 0; i < pres.length; i++) {
// If there already is a div wrapping the codeblock we want to skip this. // 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<IBodyProps, IState> {
root.className = "mx_EventTile_pre_container"; root.className = "mx_EventTile_pre_container";
// Insert containing div in place of <pre> block // Insert containing div in place of <pre> block
pre.parentNode?.replaceChild(root, pre); pre.replaceWith(root);
this.reactRoots.render( this.reactRoots.render(
<StrictMode> <StrictMode>
<CodeBlock onHeightChanged={this.props.onHeightChanged}>{pre}</CodeBlock> <CodeBlock onHeightChanged={this.props.onHeightChanged}>{pre}</CodeBlock>
</StrictMode>, </StrictMode>,
root, root,
pre,
); );
} }
@ -196,10 +195,9 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
</StrictMode> </StrictMode>
); );
this.reactRoots.render(spoiler, spoilerContainer); this.reactRoots.render(spoiler, spoilerContainer, node);
node.parentNode?.replaceChild(spoilerContainer, node);
node.replaceWith(spoilerContainer);
node = spoilerContainer; node = spoilerContainer;
} }
@ -479,12 +477,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
if (isEmote) { if (isEmote) {
return ( return (
<div <div className="mx_MEmoteBody mx_EventTile_content" onClick={this.onBodyLinkClick} dir="auto">
className="mx_MEmoteBody mx_EventTile_content"
onClick={this.onBodyLinkClick}
dir="auto"
ref={this.ref}
>
*&nbsp; *&nbsp;
<span className="mx_MEmoteBody_sender" onClick={this.onEmoteSenderClick}> <span className="mx_MEmoteBody_sender" onClick={this.onEmoteSenderClick}>
{mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()} {mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()}
@ -497,7 +490,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
} }
if (isNotice) { if (isNotice) {
return ( return (
<div className="mx_MNoticeBody mx_EventTile_content" onClick={this.onBodyLinkClick} ref={this.ref}> <div className="mx_MNoticeBody mx_EventTile_content" onClick={this.onBodyLinkClick}>
{body} {body}
{widgets} {widgets}
</div> </div>
@ -505,14 +498,14 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
} }
if (isCaption) { if (isCaption) {
return ( return (
<div className="mx_MTextBody mx_EventTile_caption" onClick={this.onBodyLinkClick} ref={this.ref}> <div className="mx_MTextBody mx_EventTile_caption" onClick={this.onBodyLinkClick}>
{body} {body}
{widgets} {widgets}
</div> </div>
); );
} }
return ( return (
<div className="mx_MTextBody mx_EventTile_content" onClick={this.onBodyLinkClick} ref={this.ref}> <div className="mx_MTextBody mx_EventTile_content" onClick={this.onBodyLinkClick}>
{body} {body}
{widgets} {widgets}
</div> </div>

View file

@ -19,7 +19,7 @@ interface IProps {
export default class TextualEvent extends React.Component<IProps> { export default class TextualEvent extends React.Component<IProps> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
public render(): React.ReactNode { public render(): React.ReactNode {
const text = TextForEvent.textForEvent( const text = TextForEvent.textForEvent(

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