Compare commits

...

24 Commits

Author SHA1 Message Date
Robin
569d525c6e Wait for a close action before closing a call
This creates a distinction between the user hanging up and the widget being ready to close, which is useful for allowing Element Call to show error screens when disconnected from the call, for example.
2025-02-20 13:54:10 +07:00
Robin
7799cb2ec5 Avoid destroying calls until they are hidden from the UI
We often want calls to exist even when no more participants are left in the MatrixRTC session. So, we should avoid destroying calls as long as they're being presented in the UI; this means that the user has an intent to either join the call or continue looking at an error screen, and we shouldn't interrupt that interaction.

The RoomViewStore is now what takes care of creating and destroying calls, rather than the CallView. In general it seems kinda impossible to safely create and destroy model objects from React lifecycle hooks, so moving this responsibility to a store seemed appropriate and resolves existing issues with calls in React strict mode.
2025-02-20 13:54:10 +07:00
Michael Telatynski
e47d7aaaff Add Windows 64-bit arm link and remove 32-bit link on compatibility page (#29312)
* Add Windows 64-bit arm link and remove 32-bit link on compatibility page

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update snapshots

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-19 16:17:56 +00:00
Florian Duros
8857c07acb Move view_create_chat & view_create_room to actions.ts (#29319)
* refactor: replace `view_create_chat` by `Action.CreateChat`

* refactor: replace `view_create_room` by `Action.CreateRoom`
2025-02-19 15:11:43 +00:00
Florian Duros
28ed506fe1 refactor: rename RoomListHeader as LegacyRoomListHeader (#29308) 2025-02-19 14:06:01 +00:00
R Midhun Suresh
76b3be6263 Add some basic documentation for MVVM (#29316) 2025-02-19 13:13:28 +00:00
ElementRobot
6c768b8b32 [create-pull-request] automated change (#29313)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-19 10:39:36 +00:00
ElementRobot
809ada17a4 [create-pull-request] automated change (#29314)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-02-19 06:19:54 +00:00
renovate[bot]
c7762a80f1 Update dependency @sentry/browser to v9 (#29303)
* Update dependency @sentry/browser to v9

* Remove redundant option

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-18 21:11:36 +00:00
renovate[bot]
261923832d Update dependency caniuse-lite to v1.0.30001699 (#29297)
* Update dependency caniuse-lite to v1.0.30001699

* Update tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-18 20:29:47 +00:00
renovate[bot]
3daa1bf06a Update all non-major dependencies (#29298)
* Update all non-major dependencies

* Prettier

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-18 20:20:06 +00:00
renovate[bot]
e5c8d7dbf0 Update playwright to v1.50.1 (#29183)
* Update playwright to v1.50.1

* Update snapshots

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-18 18:43:40 +00:00
ElementRobot
441119ca3a [create-pull-request] automated change (#29286)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-02-18 17:49:15 +00:00
renovate[bot]
acb3e781a4 Update typescript-eslint monorepo to v8.24.0 (#29302)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 16:59:10 +00:00
renovate[bot]
3fb1f6ef4d Update testcontainers-node monorepo to v10.18.0 (#29301)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 16:55:48 +00:00
renovate[bot]
cbfbfad959 Update docker (#29291)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 15:27:32 +00:00
renovate[bot]
7546bbc1f0 Update dependency @sentry/browser to v8.55.0 (#29299)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 15:07:10 +00:00
renovate[bot]
6a10c86d7a Update dependency @stylistic/eslint-plugin to v3.1.0 (#29300)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 15:07:06 +00:00
renovate[bot]
6f9e3bfe3e Update dependency @types/node to v18.19.76 (#29296)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 15:06:06 +00:00
David Baker
61d55462df Honour the backup disable flag from Element X (#29290)
* Honour the backup disable flag from Element X

This unfortunately named and unspecced flag is set by Element X
to denote that the user has chosen to disable key storage and it
should not automatically try to enable it again. This changes Element
web to not prompt to enable recovery if this flag is set.

* Remove unnecessary conditional

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-18 15:06:00 +00:00
renovate[bot]
d391c69e53 Update dependency @formatjs/intl-segmenter to v11.7.9 (#29295)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 15:02:23 +00:00
renovate[bot]
a6afff9759 Update babel monorepo (#29294)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 15:02:22 +00:00
renovate[bot]
073d8e0b86 Update sigstore/cosign-installer digest to c56c2d3 (#29293)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 14:53:35 +00:00
renovate[bot]
ecf5d720b0 Update guibranco/github-status-action-v2 digest to 7ca807c (#29292)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 14:52:13 +00:00
116 changed files with 979 additions and 773 deletions

View File

@@ -22,13 +22,13 @@ jobs:
fetch-depth: 0 # needed for docker-package to be able to calculate the version
- name: Install Cosign
uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3
uses: sigstore/cosign-installer@c56c2d3e59e4281cc41dea2217323ba5694b171e # v3
- name: Set up QEMU
uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3
uses: docker/setup-qemu-action@4574d27a4764455b42196d70a065bc6853246a25 # v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3
uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca # v3
with:
install: true

View File

@@ -104,7 +104,7 @@ jobs:
- name: Skip SonarCloud in merge queue
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true'
uses: guibranco/github-status-action-v2@119b3320db3f04d89e91df840844b92d57ce3468
uses: guibranco/github-status-action-v2@7ca807c2ba3401be532d29a876b93262108099fb
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: success

67
docs/MVVM.md Normal file
View File

@@ -0,0 +1,67 @@
# MVVM
General description of the pattern can be found [here](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel). But the gist of it is that you divide your code into three sections:
1. Model: This is where the business logic and data resides.
2. View Model: This code exists to provide the logic necessary for the UI. It directly uses the Model code.
3. View: This is the UI code itself and depends on the view model.
If you do MVVM right, your view should be dumb i.e it gets data from the view model and merely displays it.
### Practical guidelines for MVVM in element-web
#### Model
This is anywhere your data or business logic comes from. If your view model is accessing something simple exposed from `matrix-js-sdk`, then the sdk is your model. If you're using something more high level in element-web to get your data/logic (eg: `MemberListStore`), then that becomes your model.
#### View Model
1. View model is always a custom react hook named like `useFooViewModel()`.
2. The return type of your view model (known as view state) must be defined as a typescript interface:
```ts
inteface FooViewState {
somethingUseful: string;
somethingElse: BarType;
update: () => Promise<void>
...
}
```
3. Any react state that your UI needs must be in the view model.
#### View
1. Views are simple react components (eg: `FooView`).
2. Views usually start by calling the view model hook, eg:
```tsx
const FooView: React.FC<IProps> = (props: IProps) => {
const vm = useFooViewModel();
....
return(
<div>
{vm.somethingUseful}
</div>
);
}
```
3. Views are also allowed to accept the view model as a prop, eg:
```tsx
const FooView: React.FC<IProps> = ({ vm }: IProps) => {
....
return(
<div>
{vm.somethingUseful}
</div>
);
}
```
4. Multiple views can share the same view model if necessary.
### Benefits
1. MVVM forces a separation of concern i.e we will no longer have large react components that have a lot of state and rendering code mixed together. This improves code readability and makes it easier to introduce changes.
2. Introduces the possibility of code reuse. You can reuse an old view model with a new view or vice versa.
3. Adding to the point above, in future you could import element-web view models to your project and supply your own views thus creating something similar to the [hydrogen sdk](https://github.com/element-hq/hydrogen-web/blob/master/doc/SDK.md).
### Example
We started experimenting with MVVM in the redesigned memberlist, you can see the code [here](https://github.com/vector-im/element-web/blob/develop/src/components/views/rooms/MemberList/MemberListView.tsx).

View File

@@ -163,14 +163,14 @@ These two options describe the various availability for the application. When th
such as trying to get the user to use an Android app or the desktop app for encrypted search, the config options will be looked
at to see if the link should be to somewhere else.
Starting with `desktop_builds`, the following subproperties are available:
Starting with `desktop_builds`, the following sub-properties are available:
1. `available`: Required. When `true`, the desktop app can be downloaded from somewhere.
2. `logo`: Required. A URL to a logo (SVG), intended to be shown at 24x24 pixels.
3. `url`: Required. The download URL for the app. This is used as a hyperlink.
4. `url_macos`: Optional. Direct link to download macOS desktop app.
5. `url_win32`: Optional. Direct link to download Windows 32-bit desktop app.
6. `url_win64`: Optional. Direct link to download Windows 64-bit desktop app.
5. `url_win64`: Optional. Direct link to download Windows x86 64-bit desktop app.
6. `url_win64arm`: Optional. Direct link to download Windows ARM 64-bit desktop app.
7. `url_linux`: Optional. Direct link to download Linux desktop app.
When `desktop_builds` is not specified at all, the app will assume desktop downloads are available from https://element.io

View File

@@ -74,7 +74,7 @@
"@types/react-dom": "18.3.5",
"oidc-client-ts": "3.1.0",
"jwt-decode": "4.0.0",
"caniuse-lite": "1.0.30001697",
"caniuse-lite": "1.0.30001699",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
"wrap-ansi": "npm:wrap-ansi@^7.0.0"
},
@@ -88,7 +88,7 @@
"@matrix-org/emojibase-bindings": "^1.3.4",
"@matrix-org/react-sdk-module-api": "^2.4.0",
"@matrix-org/spec": "^1.7.0",
"@sentry/browser": "^8.0.0",
"@sentry/browser": "^9.0.0",
"@types/png-chunks-extract": "^1.0.2",
"@types/react-virtualized": "^9.21.30",
"@vector-im/compound-design-tokens": "^3.0.0",
@@ -274,7 +274,7 @@
"postcss-preset-env": "^10.0.0",
"postcss-scss": "^4.0.4",
"postcss-simple-vars": "^7.0.1",
"prettier": "3.4.2",
"prettier": "3.5.1",
"process": "^0.11.10",
"raw-loader": "^4.0.2",
"rimraf": "^6.0.0",

View File

@@ -1,4 +1,4 @@
FROM mcr.microsoft.com/playwright:v1.49.1-noble
FROM mcr.microsoft.com/playwright:v1.50.1-noble
WORKDIR /work

View File

@@ -6,22 +6,21 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import { expect, test as base } from "../../element-web-test";
import { type CredentialsWithDisplayName, expect, test as base } from "../../element-web-test";
import { selectHomeserver } from "../utils";
import { emailHomeserver } from "../../plugins/homeserver/synapse/emailHomeserver.ts";
import { isDendrite } from "../../plugins/homeserver/dendrite";
import { type Credentials } from "../../plugins/homeserver";
const email = "user@nowhere.dummy";
const test = base.extend<{ credentials: Pick<Credentials, "username" | "password"> }>({
const test = base.extend({
// eslint-disable-next-line no-empty-pattern
credentials: async ({}, use, testInfo) => {
await use({
username: `user_${testInfo.testId}`,
// this has to be password-like enough to please zxcvbn. Needless to say it's just from pwgen.
password: "oETo7MPf0o",
});
} as CredentialsWithDisplayName);
},
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 983 KiB

After

Width:  |  Height:  |  Size: 985 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -25,7 +25,7 @@ import { type HomeserverContainer, type StartedHomeserverContainer } from "./Hom
import { type StartedMatrixAuthenticationServiceContainer } from "./mas.ts";
import { Api, ClientServerApi, type Verb } from "../plugins/utils/api.ts";
const TAG = "develop@sha256:32ee365ad97dde86033e8a33e143048167271299e4c727413f3cdff48c65f8d9";
const TAG = "develop@sha256:fa3090607a5e07a4ff245247aa3b598c6bbcff9231fd89a558de97c37adbd744";
const DEFAULT_CONFIG = {
server_name: "localhost",

View File

@@ -288,6 +288,7 @@
@import "./views/rooms/_IRCLayout.pcss";
@import "./views/rooms/_InvitedIconView.pcss";
@import "./views/rooms/_JumpToBottomButton.pcss";
@import "./views/rooms/_LegacyRoomListHeader.pcss";
@import "./views/rooms/_LinkPreviewGroup.pcss";
@import "./views/rooms/_LinkPreviewWidget.pcss";
@import "./views/rooms/_LiveContentSummary.pcss";
@@ -312,7 +313,6 @@
@import "./views/rooms/_RoomInfoLine.pcss";
@import "./views/rooms/_RoomKnocksBar.pcss";
@import "./views/rooms/_RoomList.pcss";
@import "./views/rooms/_RoomListHeader.pcss";
@import "./views/rooms/_RoomPreviewBar.pcss";
@import "./views/rooms/_RoomPreviewCard.pcss";
@import "./views/rooms/_RoomSearchAuxPanel.pcss";

View File

@@ -10,8 +10,9 @@ Please see LICENSE files in the repository root for full details.
--cpd-separator-inset: calc(50% - (var(--width) / 2));
--cpd-separator-spacing: var(--cpd-space-8x);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji",
"Segoe UI Emoji", "Segoe UI Symbol";
text-align: center;
color: var(--cpd-color-text-primary);
width: 100%;

View File

@@ -113,7 +113,7 @@ Please see LICENSE files in the repository root for full details.
display: flex;
align-items: center;
& + .mx_RoomListHeader {
& + .mx_LegacyRoomListHeader {
margin-top: 12px;
}
@@ -180,7 +180,7 @@ Please see LICENSE files in the repository root for full details.
}
}
.mx_RoomListHeader:first-child {
.mx_LegacyRoomListHeader:first-child {
margin-top: 12px;
}

View File

@@ -365,7 +365,8 @@ Please see LICENSE files in the repository root for full details.
Note the top fade is much smaller because the spaces start close to the top,
so otherwise a large gradient suddenly appears when you scroll down.
*/
mask-image: linear-gradient(to bottom, transparent, black 16px),
mask-image:
linear-gradient(to bottom, transparent, black 16px),
linear-gradient(
to top,
transparent,

View File

@@ -16,7 +16,8 @@ Please see LICENSE files in the repository root for full details.
position: absolute;
z-index: -1;
opacity: 0.6;
background-image: radial-gradient(
background-image:
radial-gradient(
53.85% 66.75% at 87.55% 0%,
hsla(250deg, 76%, 71%, 0.261) 0%,
hsla(250deg, 100%, 88%, 0) 100%

View File

@@ -6,12 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
.mx_RoomListHeader {
.mx_LegacyRoomListHeader {
display: flex;
align-items: center;
.mx_RoomListHeader_contextLessTitle,
.mx_RoomListHeader_contextMenuButton {
.mx_LegacyRoomListHeader_contextLessTitle,
.mx_LegacyRoomListHeader_contextMenuButton {
font: var(--cpd-font-heading-sm-semibold);
font-weight: var(--cpd-font-weight-semibold);
padding: 1px 24px 1px 4px;
@@ -24,7 +24,7 @@ Please see LICENSE files in the repository root for full details.
user-select: none;
}
.mx_RoomListHeader_contextMenuButton {
.mx_LegacyRoomListHeader_contextMenuButton {
border-radius: 6px;
&:hover {
@@ -54,7 +54,7 @@ Please see LICENSE files in the repository root for full details.
}
}
.mx_RoomListHeader_plusButton {
.mx_LegacyRoomListHeader_plusButton {
width: 32px;
height: 32px;
border-radius: 8px;
@@ -88,21 +88,21 @@ Please see LICENSE files in the repository root for full details.
}
}
.mx_RoomListHeader_iconInvite::before {
.mx_LegacyRoomListHeader_iconInvite::before {
mask-image: url("$(res)/img/element-icons/room/invite.svg");
}
.mx_RoomListHeader_iconStartChat::before {
.mx_LegacyRoomListHeader_iconStartChat::before {
mask-image: url("@vector-im/compound-design-tokens/icons/user-add-solid.svg");
}
.mx_RoomListHeader_iconNewRoom::before {
.mx_LegacyRoomListHeader_iconNewRoom::before {
mask-image: url("$(res)/img/element-icons/roomlist/hash-plus.svg");
}
.mx_RoomListHeader_iconNewVideoRoom::before {
.mx_LegacyRoomListHeader_iconNewVideoRoom::before {
mask-image: url("$(res)/img/element-icons/roomlist/hash-video.svg");
}
.mx_RoomListHeader_iconExplore::before {
.mx_LegacyRoomListHeader_iconExplore::before {
mask-image: url("$(res)/img/element-icons/roomlist/hash-search.svg");
}
.mx_RoomListHeader_iconPlus::before {
.mx_LegacyRoomListHeader_iconPlus::before {
mask-image: url("@vector-im/compound-design-tokens/icons/plus.svg");
}

View File

@@ -309,11 +309,8 @@ body {
/* Splash Page Gradient */
.mx_SplashPage::before {
background-image: radial-gradient(
53.85% 66.75% at 87.55% 0%,
hsla(0deg, 0%, 11%, 0.15) 0%,
hsla(250deg, 100%, 88%, 0) 100%
),
background-image:
radial-gradient(53.85% 66.75% at 87.55% 0%, hsla(0deg, 0%, 11%, 0.15) 0%, hsla(250deg, 100%, 88%, 0) 100%),
radial-gradient(41.93% 41.93% at 0% 0%, hsla(0deg, 0%, 38%, 0.28) 0%, hsla(250deg, 100%, 88%, 0) 100%),
radial-gradient(100% 100% at 0% 0%, hsla(250deg, 100%, 88%, 0.3) 0%, hsla(0deg, 100%, 86%, 0) 100%),
radial-gradient(106.35% 96.26% at 100% 0%, hsla(25deg, 100%, 88%, 0.4) 0%, hsla(167deg, 76%, 82%, 0) 100%) !important;

View File

@@ -10,11 +10,13 @@
/* Noto Color Emoji contains digits, in fixed-width, therefore causing
digits in flowed text to stand out.
TODO: Consider putting all emoji fonts to the end rather than the front. */
$font-family: "Nunito", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Arial", "Helvetica",
sans-serif, "Noto Color Emoji";
$font-family:
"Nunito", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Arial", "Helvetica", sans-serif,
"Noto Color Emoji";
$monospace-font-family: "Inconsolata", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Courier",
monospace, "Noto Color Emoji";
$monospace-font-family:
"Inconsolata", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Courier", monospace,
"Noto Color Emoji";
/* unified palette */
/* try to use these colors when possible */

View File

@@ -10,11 +10,13 @@
/* Noto Color Emoji contains digits, in fixed-width, therefore causing
digits in flowed text to stand out.
TODO: Consider putting all emoji fonts to the end rather than the front. */
$font-family: "Inter", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Arial", "Helvetica",
sans-serif, "Noto Color Emoji";
$font-family:
"Inter", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Arial", "Helvetica", sans-serif,
"Noto Color Emoji";
$monospace-font-family: "Inconsolata", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Courier",
monospace, "Noto Color Emoji";
$monospace-font-family:
"Inconsolata", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Courier", monospace,
"Noto Color Emoji";
/* Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=559%3A120 */
/* ******************** */

View File

@@ -48,6 +48,11 @@ import { asyncSomeParallel } from "./utils/arrays.ts";
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
// Unfortunately named account data key used by Element X to indicate that the user
// has chosen to disable server side key backups. We need to set and honour this
// to prevent Element X from automatically turning key backup back on.
const BACKUP_DISABLED_ACCOUNT_DATA_KEY = "m.org.matrix.custom.backup_disabled";
const logger = baseLogger.getChild("DeviceListener:");
export default class DeviceListener {
@@ -323,9 +328,11 @@ export default class DeviceListener {
logger.info("Some secrets not cached: showing KEY_STORAGE_OUT_OF_SYNC toast");
showSetupEncryptionToast(SetupKind.KEY_STORAGE_OUT_OF_SYNC);
} else if (defaultKeyId === null) {
// the user just hasn't set up 4S yet: prompt them to do so
logger.info("No default 4S key: showing SET_UP_RECOVERY toast");
showSetupEncryptionToast(SetupKind.SET_UP_RECOVERY);
// the user just hasn't set up 4S yet: prompt them to do so (unless they've explicitly said no to key storage)
const disabledEvent = cli.getAccountData(BACKUP_DISABLED_ACCOUNT_DATA_KEY);
if (!disabledEvent?.getContent().disabled) {
showSetupEncryptionToast(SetupKind.SET_UP_RECOVERY);
}
} else {
// some other condition... yikes! Show the 'set up encryption' toast: this is what we previously did
// in 'other' situations. Possibly we should consider prompting for a full reset in this case?

View File

@@ -71,7 +71,7 @@ export interface IConfigOptions {
url: string; // download url
url_macos?: string;
url_win64?: string;
url_win32?: string;
url_win64arm?: string;
url_linux?: string;
};
mobile_builds: {

View File

@@ -59,7 +59,7 @@ export const DEFAULTS: DeepReadonly<IConfigOptions> = {
url: "https://element.io/download",
url_macos: "https://packages.element.io/desktop/install/macos/Element.dmg",
url_win64: "https://packages.element.io/desktop/install/win32/x64/Element%20Setup.exe",
url_win32: "https://packages.element.io/desktop/install/win32/ia32/Element%20Setup.exe",
url_win64arm: "https://packages.element.io/desktop/install/win32/arm64/Element%20Setup.exe",
url_linux: "https://element.io/download#linux",
},
mobile_builds: {

View File

@@ -80,9 +80,9 @@ const MobileAppLinks: React.FC<{
const DesktopAppLinks: React.FC<{
macOsUrl?: string;
win64Url?: string;
win32Url?: string;
win64ArmUrl?: string;
linuxUrl?: string;
}> = ({ macOsUrl, win64Url, win32Url, linuxUrl }) => {
}> = ({ macOsUrl, win64Url, win64ArmUrl, linuxUrl }) => {
return (
<Flex gap="var(--cpd-space-4x)">
{macOsUrl && (
@@ -92,12 +92,12 @@ const DesktopAppLinks: React.FC<{
)}
{win64Url && (
<Button as="a" href={win64Url} kind="secondary" Icon={MicrosoftIcon}>
{_t("incompatible_browser|windows", { bits: "64" })}
{_t("incompatible_browser|windows_64bit")}
</Button>
)}
{win32Url && (
<Button as="a" href={win32Url} kind="secondary" Icon={MicrosoftIcon}>
{_t("incompatible_browser|windows", { bits: "32" })}
{win64ArmUrl && (
<Button as="a" href={win64ArmUrl} kind="secondary" Icon={MicrosoftIcon}>
{_t("incompatible_browser|windows_arm_64bit")}
</Button>
)}
{linuxUrl && (
@@ -127,7 +127,7 @@ export const UnsupportedBrowserView: React.FC<{
config.desktop_builds?.available &&
(config.desktop_builds?.url_macos ||
config.desktop_builds?.url_win64 ||
config.desktop_builds?.url_win32 ||
config.desktop_builds?.url_win64arm ||
config.desktop_builds?.url_linux);
const hasMobileBuilds = Boolean(
config.mobile_builds?.ios || config.mobile_builds?.android || config.mobile_builds?.fdroid,
@@ -157,7 +157,7 @@ export const UnsupportedBrowserView: React.FC<{
<DesktopAppLinks
macOsUrl={config.desktop_builds?.url_macos}
win64Url={config.desktop_builds?.url_win64}
win32Url={config.desktop_builds?.url_win32}
win64ArmUrl={config.desktop_builds?.url_win64arm}
linuxUrl={config.desktop_builds?.url_linux}
/>
</>

View File

@@ -27,7 +27,7 @@ import EmbeddedPage from "./EmbeddedPage";
const onClickSendDm = (ev: ButtonEvent): void => {
PosthogTrackers.trackInteraction("WebHomeCreateChatButton", ev);
dis.dispatch({ action: "view_create_chat" });
dis.dispatch({ action: Action.CreateChat });
};
const onClickExplore = (ev: ButtonEvent): void => {
@@ -37,7 +37,7 @@ const onClickExplore = (ev: ButtonEvent): void => {
const onClickNewRoom = (ev: ButtonEvent): void => {
PosthogTrackers.trackInteraction("WebHomeCreateRoomButton", ev);
dis.dispatch({ action: "view_create_room" });
dis.dispatch({ action: Action.CreateRoom });
};
interface IProps {

View File

@@ -23,7 +23,7 @@ import { MetaSpace, type SpaceKey, UPDATE_SELECTED_SPACE } from "../../stores/sp
import { getKeyBindingsManager } from "../../KeyBindingsManager";
import UIStore from "../../stores/UIStore";
import { type IState as IRovingTabIndexState } from "../../accessibility/RovingTabIndex";
import RoomListHeader from "../views/rooms/RoomListHeader";
import LegacyRoomListHeader from "../views/rooms/LegacyRoomListHeader";
import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore";
import { UPDATE_EVENT } from "../../stores/AsyncStore";
@@ -415,7 +415,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
<div className="mx_LeftPanel_roomListContainer">
{shouldShowComponent(UIComponent.FilterContainer) && this.renderSearchDialExplore()}
{this.renderBreadcrumbs()}
{!this.props.isMinimized && <RoomListHeader onVisibilityChange={this.refreshStickyHeaders} />}
{!this.props.isMinimized && <LegacyRoomListHeader onVisibilityChange={this.refreshStickyHeaders} />}
<nav className="mx_LeftPanel_roomListWrapper" aria-label={_t("common|rooms")}>
<div
className={roomListClasses}

View File

@@ -144,7 +144,7 @@ const AUTH_SCREENS = ["register", "mobile_register", "login", "forgot_password",
// Actions that are redirected through the onboarding process prior to being
// re-dispatched. NOTE: some actions are non-trivial and would require
// re-factoring to be included in this list in future.
const ONBOARDING_FLOW_STARTERS = [Action.ViewUserSettings, "view_create_chat", "view_create_room"];
const ONBOARDING_FLOW_STARTERS = [Action.ViewUserSettings, Action.CreateChat, Action.CreateRoom];
interface IScreen {
screen: string;
@@ -616,7 +616,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
// Start the onboarding process for certain actions
if (MatrixClientPeg.get()?.isGuest() && ONBOARDING_FLOW_STARTERS.includes(payload.action)) {
if (
MatrixClientPeg.get()?.isGuest() &&
ONBOARDING_FLOW_STARTERS.includes(payload.action as unknown as Action)
) {
// This will cause `payload` to be dispatched later, once a
// sync has reached the "prepared" state. Setting a matrix ID
// will cause a full login and sync and finally the deferred
@@ -785,7 +788,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.viewSomethingBehindModal();
break;
}
case "view_create_room":
case Action.CreateRoom:
this.createRoom(payload.public, payload.defaultName, payload.type);
// View the welcome or home page if we need something to look at
@@ -816,7 +819,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
case Action.ViewStartChatOrReuse:
this.chatCreateOrReuse(payload.user_id);
break;
case "view_create_chat":
case Action.CreateChat:
showStartChatInviteDialog(payload.initialText || "");
// View the welcome or home page if we need something to look at
@@ -1758,11 +1761,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
} else if (screen === "new") {
dis.dispatch({
action: "view_create_room",
action: Action.CreateRoom,
});
} else if (screen === "dm") {
dis.dispatch({
action: "view_create_chat",
action: Action.CreateChat,
});
} else if (screen === "settings") {
dis.fire(Action.ViewUserSettings);

View File

@@ -118,8 +118,6 @@ import { RoomStatusBarUnsentMessages } from "./RoomStatusBarUnsentMessages";
import { LargeLoader } from "./LargeLoader";
import { isVideoRoom } from "../../utils/video-rooms";
import { SDKContext } from "../../contexts/SDKContext";
import { CallStore, CallStoreEvent } from "../../stores/CallStore";
import { type Call } from "../../models/Call";
import { RoomSearchView } from "./RoomSearchView";
import eventSearch, { type SearchInfo, SearchScope } from "../../Searching";
import VoipUserMapper from "../../VoipUserMapper";
@@ -190,7 +188,6 @@ export interface IRoomState {
*/
search?: SearchInfo;
callState?: CallState;
activeCall: Call | null;
canPeek: boolean;
canSelfRedact: boolean;
showApps: boolean;
@@ -401,7 +398,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
membersLoaded: !llMembers,
numUnreadMessages: 0,
callState: undefined,
activeCall: null,
canPeek: false,
canSelfRedact: false,
showApps: false,
@@ -577,7 +573,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
mainSplitContentType: room ? this.getMainSplitContentType(room) : undefined,
initialEventId: undefined, // default to clearing this, will get set later in the method if needed
showRightPanel: roomId ? this.context.rightPanelStore.isOpenForRoom(roomId) : false,
activeCall: roomId ? CallStore.instance.getActiveCall(roomId) : null,
promptAskToJoin: this.context.roomViewStore.promptAskToJoin(),
viewRoomOpts: this.context.roomViewStore.getViewRoomOpts(),
};
@@ -727,23 +722,17 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
});
};
private onConnectedCalls = (): void => {
if (this.state.roomId === undefined) return;
const activeCall = CallStore.instance.getActiveCall(this.state.roomId);
if (activeCall === null) {
// We disconnected from the call, so stop viewing it
defaultDispatcher.dispatch<ViewRoomPayload>(
{
action: Action.ViewRoom,
room_id: this.state.roomId,
view_call: false,
metricsTrigger: undefined,
},
true,
); // Synchronous so that CallView disappears immediately
}
this.setState({ activeCall });
private onCallClose = (): void => {
// Stop viewing the call
defaultDispatcher.dispatch<ViewRoomPayload>(
{
action: Action.ViewRoom,
room_id: this.state.roomId,
view_call: false,
metricsTrigger: undefined,
},
true,
); // Synchronous so that CallView disappears immediately
};
private getRoomId = (): string | undefined => {
@@ -900,8 +889,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
WidgetEchoStore.on(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
this.context.widgetStore.on(UPDATE_EVENT, this.onWidgetStoreUpdate);
CallStore.instance.on(CallStoreEvent.ConnectedCalls, this.onConnectedCalls);
this.props.resizeNotifier.on("isResizing", this.onIsResizing);
this.settingWatchers = [
@@ -1027,7 +1014,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
);
}
CallStore.instance.off(CallStoreEvent.ConnectedCalls, this.onConnectedCalls);
this.context.legacyCallHandler.off(LegacyCallHandlerEvent.CallState, this.onCallState);
// cancel any pending calls to the throttled updated
@@ -2562,9 +2548,9 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
<CallView
room={this.state.room}
resizing={this.state.resizing}
waitForCall={isVideoRoom(this.state.room)}
skipLobby={this.context.roomViewStore.skipCallLobby() ?? false}
role="main"
onClose={this.onCallClose}
/>
{previewBar}
</>

View File

@@ -954,7 +954,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
className="mx_SpotlightDialog_createRoom"
onClick={() =>
defaultDispatcher.dispatch({
action: "view_create_room",
action: Action.CreateRoom,
public: true,
defaultName: capitalize(trimmedQuery),
})

View File

@@ -126,10 +126,6 @@ const ActiveLoadedCallEvent = forwardRef<any, ActiveLoadedCallEventProps>(({ mxE
return [_t("action|leave"), "danger", disconnect];
case ConnectionState.Disconnecting:
return [_t("action|leave"), "danger", null];
case ConnectionState.Connecting:
case ConnectionState.Lobby:
case ConnectionState.WidgetLoading:
return [_t("action|join"), "primary", null];
}
}, [connectionState, connect, disconnect]);

View File

@@ -147,7 +147,7 @@ const DmAuxButton: React.FC<IAuxButtonProps> = ({ tabIndex, dispatcher = default
e.preventDefault();
e.stopPropagation();
closeMenu();
defaultDispatcher.dispatch({ action: "view_create_chat" });
defaultDispatcher.dispatch({ action: Action.CreateChat });
PosthogTrackers.trackInteraction(
"WebRoomListRoomsSublistPlusMenuCreateChatItem",
e,
@@ -194,7 +194,7 @@ const DmAuxButton: React.FC<IAuxButtonProps> = ({ tabIndex, dispatcher = default
<AccessibleButton
tabIndex={tabIndex}
onClick={(e) => {
dispatcher.dispatch({ action: "view_create_chat" });
dispatcher.dispatch({ action: Action.CreateChat });
PosthogTrackers.trackInteraction("WebRoomListRoomsSublistPlusMenuCreateChatItem", e);
}}
className="mx_RoomSublist_auxButton"
@@ -305,7 +305,7 @@ const UntaggedAuxButton: React.FC<IAuxButtonProps> = ({ tabIndex }) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
defaultDispatcher.dispatch({ action: "view_create_room" });
defaultDispatcher.dispatch({ action: Action.CreateRoom });
PosthogTrackers.trackInteraction("WebRoomListRoomsSublistPlusMenuCreateRoomItem", e);
}}
/>
@@ -318,7 +318,7 @@ const UntaggedAuxButton: React.FC<IAuxButtonProps> = ({ tabIndex }) => {
e.stopPropagation();
closeMenu();
defaultDispatcher.dispatch({
action: "view_create_room",
action: Action.CreateRoom,
type: elementCallVideoRoomsEnabled
? RoomType.UnstableCall
: RoomType.ElementVideo,

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import { EventType, RoomType, type Room, RoomEvent, ClientEvent } from "matrix-js-sdk/src/matrix";
import { ClientEvent, EventType, type Room, RoomEvent, RoomType } from "matrix-js-sdk/src/matrix";
import React, { useContext, useEffect, useState } from "react";
import { Tooltip } from "@vector-im/compound-web";
@@ -38,10 +38,10 @@ import {
} from "../../../utils/space";
import {
ChevronFace,
ContextMenuTooltipButton,
useContextMenu,
type MenuProps,
ContextMenuButton,
ContextMenuTooltipButton,
type MenuProps,
useContextMenu,
} from "../../structures/ContextMenu";
import { BetaPill } from "../beta/BetaCard";
import IconizedContextMenu, {
@@ -108,7 +108,7 @@ interface IProps {
onVisibilityChange?(): void;
}
const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
const LegacyRoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
const cli = useContext(MatrixClientContext);
const [mainMenuDisplayed, mainMenuHandle, openMainMenu, closeMainMenu] = useContextMenu<HTMLDivElement>();
const [plusMenuDisplayed, plusMenuHandle, openPlusMenu, closePlusMenu] = useContextMenu<HTMLDivElement>();
@@ -178,7 +178,7 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
inviteOption = (
<IconizedContextMenuOption
label={_t("action|invite")}
iconClassName="mx_RoomListHeader_iconInvite"
iconClassName="mx_LegacyRoomListHeader_iconInvite"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
@@ -194,7 +194,7 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
newRoomOptions = (
<>
<IconizedContextMenuOption
iconClassName="mx_RoomListHeader_iconNewRoom"
iconClassName="mx_LegacyRoomListHeader_iconNewRoom"
label={_t("action|new_room")}
onClick={(e) => {
e.preventDefault();
@@ -206,7 +206,7 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
/>
{videoRoomsEnabled && (
<IconizedContextMenuOption
iconClassName="mx_RoomListHeader_iconNewVideoRoom"
iconClassName="mx_LegacyRoomListHeader_iconNewVideoRoom"
label={_t("action|new_video_room")}
onClick={(e) => {
e.preventDefault();
@@ -236,7 +236,7 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
{newRoomOptions}
<IconizedContextMenuOption
label={_t("action|explore_rooms")}
iconClassName="mx_RoomListHeader_iconExplore"
iconClassName="mx_LegacyRoomListHeader_iconExplore"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
@@ -251,7 +251,7 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
/>
<IconizedContextMenuOption
label={_t("action|add_existing_room")}
iconClassName="mx_RoomListHeader_iconPlus"
iconClassName="mx_LegacyRoomListHeader_iconPlus"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
@@ -264,7 +264,7 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
{canCreateSpaces && (
<IconizedContextMenuOption
label={_t("room_list|add_space_label")}
iconClassName="mx_RoomListHeader_iconPlus"
iconClassName="mx_LegacyRoomListHeader_iconPlus"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
@@ -289,22 +289,22 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
<>
<IconizedContextMenuOption
label={_t("action|start_new_chat")}
iconClassName="mx_RoomListHeader_iconStartChat"
iconClassName="mx_LegacyRoomListHeader_iconStartChat"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
defaultDispatcher.dispatch({ action: "view_create_chat" });
defaultDispatcher.dispatch({ action: Action.CreateChat });
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateChatItem", e);
closePlusMenu();
}}
/>
<IconizedContextMenuOption
label={_t("action|new_room")}
iconClassName="mx_RoomListHeader_iconNewRoom"
iconClassName="mx_LegacyRoomListHeader_iconNewRoom"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
defaultDispatcher.dispatch({ action: "view_create_room" });
defaultDispatcher.dispatch({ action: Action.CreateRoom });
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateRoomItem", e);
closePlusMenu();
}}
@@ -312,12 +312,12 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
{videoRoomsEnabled && (
<IconizedContextMenuOption
label={_t("action|new_video_room")}
iconClassName="mx_RoomListHeader_iconNewVideoRoom"
iconClassName="mx_LegacyRoomListHeader_iconNewVideoRoom"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
defaultDispatcher.dispatch({
action: "view_create_room",
action: Action.CreateRoom,
type: elementCallVideoRoomsEnabled ? RoomType.UnstableCall : RoomType.ElementVideo,
});
closePlusMenu();
@@ -333,7 +333,7 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
joinRoomOpt = (
<IconizedContextMenuOption
label={_t("room_list|join_public_room_label")}
iconClassName="mx_RoomListHeader_iconExplore"
iconClassName="mx_LegacyRoomListHeader_iconExplore"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
@@ -378,13 +378,13 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
})
.join("\n");
let contextMenuButton: JSX.Element = <div className="mx_RoomListHeader_contextLessTitle">{title}</div>;
let contextMenuButton: JSX.Element = <div className="mx_LegacyRoomListHeader_contextLessTitle">{title}</div>;
if (canShowMainMenu) {
const commonProps = {
ref: mainMenuHandle,
onClick: openMainMenu,
isExpanded: mainMenuDisplayed,
className: "mx_RoomListHeader_contextMenuButton",
className: "mx_LegacyRoomListHeader_contextMenuButton",
children: title,
};
@@ -401,7 +401,7 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
}
return (
<aside className="mx_RoomListHeader" aria-label={_t("room|context_menu|title")}>
<aside className="mx_LegacyRoomListHeader" aria-label={_t("room|context_menu|title")}>
{contextMenuButton}
{pendingActionSummary ? (
<Tooltip label={pendingActionSummary} isTriggerInteractive={false}>
@@ -413,7 +413,7 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
ref={plusMenuHandle}
onClick={openPlusMenu}
isExpanded={plusMenuDisplayed}
className="mx_RoomListHeader_plusButton"
className="mx_LegacyRoomListHeader_plusButton"
title={_t("action|add")}
/>
)}
@@ -423,4 +423,4 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
);
};
export default RoomListHeader;
export default LegacyRoomListHeader;

View File

@@ -27,18 +27,6 @@ export const RoomTileCallSummary: FC<Props> = ({ call }) => {
text = _t("common|video");
active = false;
break;
case ConnectionState.WidgetLoading:
text = _t("common|loading");
active = false;
break;
case ConnectionState.Lobby:
text = _t("common|lobby");
active = false;
break;
case ConnectionState.Connecting:
text = _t("room|joining");
active = true;
break;
case ConnectionState.Connected:
case ConnectionState.Disconnecting:
text = _t("common|joined");

View File

@@ -9,12 +9,13 @@ Please see LICENSE files in the repository root for full details.
import React, { type FC, useContext, useEffect, type AriaRole, useCallback } from "react";
import type { Room } from "matrix-js-sdk/src/matrix";
import { type Call, ConnectionState, ElementCall } from "../../../models/Call";
import { useCall } from "../../../hooks/useCall";
import { type Call, CallEvent } from "../../../models/Call";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import AppTile from "../elements/AppTile";
import { CallStore } from "../../../stores/CallStore";
import { SdkContextClass } from "../../../contexts/SDKContext";
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
import { useCall } from "../../../hooks/useCall";
interface JoinCallViewProps {
room: Room;
@@ -22,10 +23,12 @@ interface JoinCallViewProps {
call: Call;
skipLobby?: boolean;
role?: AriaRole;
onClose: () => void;
}
const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call, skipLobby, role }) => {
const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call, skipLobby, role, onClose }) => {
const cli = useContext(MatrixClientContext);
useTypedEventEmitter(call, CallEvent.Close, onClose);
useEffect(() => {
// We'll take this opportunity to tidy up our room state
@@ -38,17 +41,6 @@ const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call, skipLobby,
call.widget.data.skipLobby = skipLobby;
}, [call.widget, skipLobby]);
useEffect(() => {
if (call.connectionState === ConnectionState.Disconnected) {
// immediately start the call
// (this will start the lobby view in the widget and connect to all required widget events)
call.start();
}
return (): void => {
// If we are connected the widget is sticky and we do not want to destroy the call.
if (!call.connected) call.destroy();
};
}, [call]);
const disconnectAllOtherCalls: () => Promise<void> = useCallback(async () => {
// The stickyPromise has to resolve before the widget actually becomes sticky.
// We only let the widget become sticky after disconnecting all other active calls.
@@ -57,6 +49,7 @@ const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call, skipLobby,
);
await Promise.all(calls.map(async (call) => await call.disconnect()));
}, []);
return (
<div className="mx_CallView" role={role}>
<AppTile
@@ -76,26 +69,27 @@ const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call, skipLobby,
interface CallViewProps {
room: Room;
resizing: boolean;
/**
* If true, the view will be blank until a call appears. Otherwise, the join
* button will create a call if there isn't already one.
*/
waitForCall: boolean;
skipLobby?: boolean;
role?: AriaRole;
/**
* Callback for when the user closes the call.
*/
onClose: () => void;
}
export const CallView: FC<CallViewProps> = ({ room, resizing, waitForCall, skipLobby, role }) => {
export const CallView: FC<CallViewProps> = ({ room, resizing, skipLobby, role, onClose }) => {
const call = useCall(room.roomId);
useEffect(() => {
if (call === null && !waitForCall) {
ElementCall.create(room, skipLobby);
}
}, [call, room, skipLobby, waitForCall]);
if (call === null) {
return null;
} else {
return <JoinCallView room={room} resizing={resizing} call={call} skipLobby={skipLobby} role={role} />;
}
return (
call && (
<JoinCallView
room={room}
resizing={resizing}
call={call}
skipLobby={skipLobby}
role={role}
onClose={onClose}
/>
)
);
};

View File

@@ -70,7 +70,6 @@ const RoomContext = createContext<
threadId: undefined,
liveTimeline: undefined,
narrow: false,
activeCall: null,
msc3946ProcessDynamicPredecessor: false,
canAskToJoin: false,
promptAskToJoin: false,

View File

@@ -343,7 +343,7 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro
await client.setPowerLevel(roomId, client.getUserId()!, 100);
} else if (opts.roomType === RoomType.UnstableCall) {
// Set up this video room with an Element call
await ElementCall.create(await room);
ElementCall.create(await room);
// Reset our power level back to admin so that the call becomes immutable
await client.setPowerLevel(roomId, client.getUserId()!, 100);

View File

@@ -365,4 +365,14 @@ export enum Action {
* Opens right panel room summary and focuses the search input
*/
FocusMessageSearch = "focus_search",
/**
* Open the direct message dialog
*/
CreateChat = "view_create_chat",
/**
* Open the create room dialog
*/
CreateRoom = "view_create_room",
}

View File

@@ -75,9 +75,5 @@ export const useFull = (call: Call | null): boolean => {
export const useJoinCallButtonDisabledTooltip = (call: Call | null): string | null => {
const isFull = useFull(call);
const state = useConnectionState(call);
if (state === ConnectionState.Connecting) return _t("voip|join_button_tooltip_connecting");
if (isFull) return _t("voip|join_button_tooltip_call_full");
return null;
return isFull ? _t("voip|join_button_tooltip_call_full") : null;
};

View File

@@ -712,6 +712,44 @@
"category_room": "Stafell",
"caution_colon": "Rhybudd:",
"client_versions": "Fersiynau Cleient",
"crypto": {
"4s_public_key_in_account_data": "mewn data cyfrif",
"4s_public_key_not_in_account_data": "heb ei ganfod",
"4s_public_key_status": "Allwedd gyhoeddus storio gyfrinachol:",
"backup_key_cached": "wedi'i storio'n lleol",
"backup_key_cached_status": "Allwedd wrth gefn wedi'i storio:",
"backup_key_not_stored": "heb ei storio",
"backup_key_stored": "mewn storfa gyfrinachol",
"backup_key_stored_status": "Allwedd wrth gefn wedi'i storio:",
"backup_key_unexpected_type": "math annisgwyl",
"backup_key_well_formed": "wedi'i ffurfio'n dda",
"cross_signing": "Traws-arwyddo",
"cross_signing_cached": "wedi'i storio'n lleol",
"cross_signing_not_ready": "Nid yw traws-arwyddo wedi'i osod.",
"cross_signing_private_keys_in_storage": "mewn storfa gyfrinachol",
"cross_signing_private_keys_in_storage_status": "Traws-lofnodi allweddi preifat:",
"cross_signing_private_keys_not_in_storage": "heb ei ganfod yn y storfa",
"cross_signing_public_keys_on_device": "yn y cof",
"cross_signing_public_keys_on_device_status": "Traws-lofnodi allweddi cyhoeddus:",
"cross_signing_ready": "Mae croes-arwyddo yn barod i'w ddefnyddio.",
"cross_signing_status": "Statws traws-arwyddo:",
"cross_signing_untrusted": "Mae gan eich cyfrif hunaniaeth traws-lofnodi mewn storfa gyfrinachol, ond nid yw'r sesiwn hon yn ymddiried ynddo eto.",
"crypto_not_available": "Nid yw modiwl cryptograffig ar gael",
"key_backup_active_version": "Fersiwn wrth gefn gweithredol:",
"key_backup_active_version_none": "Dim",
"key_backup_inactive_warning": "Nid yw'ch allweddi'n cael eu gwneud wrth gefn o'r sesiwn hon.",
"key_backup_latest_version": "Fersiwn wrth gefn diweddaraf ar y gweinydd:",
"key_storage": "Storio Allwedd",
"master_private_key_cached_status": "Prif allwedd breifat:",
"not_found": "heb ei ganfod",
"not_found_locally": "heb ei ganfod yn lleol",
"secret_storage_not_ready": "ddim yn barod",
"secret_storage_ready": "barod",
"secret_storage_status": "Storio cyfrinachol:",
"self_signing_private_key_cached_status": "Allwedd breifat hunan-arwyddo:",
"title": "Amgryptio o ben i ben",
"user_signing_private_key_cached_status": "Defnyddiwr yn arwyddo allwedd breifat:"
},
"developer_mode": "Modd datblygwr",
"developer_tools": "Offer Datblygwr",
"edit_setting": "Golygu gosodiad",
@@ -989,8 +1027,11 @@
"waiting_other_user": "Aros i %(displayName)s wirio…"
},
"verification_requested_toast_title": "Gofynnwyd am ddilysiad",
"verified_identity_changed": "Mae hunaniaeth %(displayName)s ( <b>%(userId)s</b> ) a ddilyswyd wedi newid. <a>Dysgwch fwy</a>",
"verified_identity_changed_no_displayname": "Mae hunaniaeth ddilysedig <b>%(userId)s</b> wedi newid. <a>Dysgwch fwy</a>",
"verify_toast_description": "Efallai na fydd defnyddwyr eraill yn ymddiried ynddo",
"verify_toast_title": "Gwiriwch y sesiwn hon"
"verify_toast_title": "Gwiriwch y sesiwn hon",
"withdraw_verification_action": "Tynnu'r dilysiad yn ôl"
},
"error": {
"admin_contact": "<a>Cysylltwch â gweinyddwr eich gwasanaeth</a> i barhau i ddefnyddio'r gwasanaeth hwn.",
@@ -1399,6 +1440,7 @@
"location_share_live_description": "Gweithredu dros dro. Mae lleoliadau'n parhau yn hanes ystafelloedd.",
"mjolnir": "Ffyrdd newydd o anwybyddu pobl",
"msc3531_hide_messages_pending_moderation": "Gadael i safonwyr guddio negeseuon tra'n aros i'w safoni.",
"new_room_list": "Galluogi rhestr ystafelloedd newydd",
"notification_settings": "Gosodiadau Hysbysu Newydd",
"notification_settings_beta_caption": "Cyflwyno ffordd symlach o newid eich gosodiadau hysbysu. Addaswch eich %(brand)s, yn union fel y dymunwch.",
"notification_settings_beta_title": "Gosodiadau Hysbysiadau",
@@ -2279,6 +2321,25 @@
"enable_markdown": "Galluogi Markdown",
"enable_markdown_description": "Dechreuwch negeseuon gyda <code>/plain</code> i'w hanfon heb eu marcio i lawr.",
"encryption": {
"advanced": {
"breadcrumb_first_description": "Bydd manylion eich cyfrif, eich cysylltiadau, eich dewisiadau a'ch rhestr sgwrsio yn cael eu cadw",
"breadcrumb_page": "Ailosod amgryptio",
"breadcrumb_second_description": "Byddwch yn colli unrhyw hanes neges sydd wedi'i storio ar y gweinydd yn unig",
"breadcrumb_third_description": "Bydd angen i chi wirio'ch holl ddyfeisiau a chysylltiadau presennol eto",
"breadcrumb_title": "Ydych chi'n siŵr eich bod am ailosod eich hunaniaeth?",
"breadcrumb_title_forgot": "Wedi anghofio eich allwedd adfer? Bydd angen i chi ailosod eich hunaniaeth.",
"breadcrumb_warning": "Gwnewch hyn dim ond os ydych chi'n credu bod eich cyfrif wedi'i beryglu.",
"details_title": "Manylion amgryptio",
"export_keys": "Allforio allweddi",
"import_keys": "Mewnforio bysellau",
"other_people_device_description": "Yn ragosodedig mewn ystafelloedd wedi'u hamgryptio, peidiwch ag anfon negeseuon wedi'u hamgryptio at unrhyw un nes i chi eu gwirio",
"other_people_device_label": "Peidiwch byth ag anfon negeseuon wedi'u hamgryptio i ddyfeisiau heb eu gwirio",
"other_people_device_title": "Dyfeisiau pobl eraill",
"reset_identity": "Ailosod hunaniaeth cryptograffig",
"session_id": "ID y sesiwn:",
"session_key": "Allwedd sesiwn:",
"title": "Uwch"
},
"device_not_verified_button": "Dilyswch y ddyfais hon",
"device_not_verified_description": "Mae angen i chi wirio'r ddyfais hon er mwyn gweld eich gosodiadau amgryptio.",
"device_not_verified_title": "Dyfais heb ei gwirio",
@@ -2293,6 +2354,7 @@
"description": "Adfer eich hunaniaeth cryptograffig a hanes neges gydag allwedd adfer os ydych chi wedi colli'ch holl ddyfeisiau presennol.",
"enter_key_error": "Nid yw'r allwedd adfer a roesoch yn gywir.",
"enter_recovery_key": "Rhowch allwedd adfer",
"forgot_recovery_key": "Wedi anghofio allwedd adfer?",
"key_storage_warning": "Nid yw eich storfa allweddi wedi'i chysoni. Cliciwch ar y botwm isod i ddatrys y broblem.",
"save_key_description": "Peidiwch â rhannu hwn gyda neb!",
"save_key_title": "Allwedd adfer",
@@ -3198,6 +3260,7 @@
"left_reason": "Gadawodd %(targetName)s yr ystafell: %(reason)s",
"no_change": "Ni wnaeth %(senderName)s unrhyw newid",
"reject_invite": "Mae %(targetName)s wedi gwrthod y gwahoddiad",
"reject_invite_reason": "Mae %(targetName)s wedi gwrthod y gwahoddiad: %(reason)s",
"remove_avatar": "Mae %(senderName)s wedi tynnu eu llun proffil",
"remove_name": "Mae %(senderName)s wedi tynnu eu henw arddangos (%(oldDisplayName)s)",
"set_avatar": "Mae %(senderName)s wedi gosod llun proffil",
@@ -3233,6 +3296,10 @@
"sent": "Anfonodd %(senderName)s wahoddiad at %(targetDisplayName)s i ymuno â'r ystafell."
},
"m.room.tombstone": "Mae %(senderDisplayName)s wedi uwchraddio'r ystafell hon.",
"m.room.topic": {
"changed": "Newidiodd %(senderDisplayName)s y pwnc i \"%(topic)s\".",
"removed": "Mae %(senderDisplayName)s wedi dileu'r pwnc."
},
"m.sticker": "Anfonodd %(senderDisplayName)s sticer.",
"m.video": {
"error_decrypting": "Gwall wrth ddadgryptio fideo"
@@ -3417,6 +3484,7 @@
"unban_space_specific": "Gwahardd nhw o bethau penodol y gallaf",
"unban_space_warning": "Ni fyddant yn gallu cael mynediad at beth bynnag nad ydych yn weinyddwr iddo.",
"unignore_button": "Anwybyddu",
"verification_unavailable": "Nid yw dilysu defnyddiwr ar gael",
"verify_button": "Dilysu Defnyddiwr",
"verify_explainer": "Ar gyfer diogelwch ychwanegol, gwiriwch y defnyddiwr hwn trwy wirio cod un-amser ar eich dwy ddyfais."
},

View File

@@ -495,7 +495,6 @@
"legal": "Legal",
"light": "Light",
"loading": "Loading…",
"lobby": "Lobby",
"location": "Location",
"low_priority": "Low priority",
"matrix": "Matrix",
@@ -1286,7 +1285,8 @@
"use_desktop_heading": "Use %(brand)s Desktop instead",
"use_mobile_heading": "Use %(brand)s on mobile instead",
"use_mobile_heading_after_desktop": "Or use our mobile app",
"windows": "Windows (%(bits)s-bit)"
"windows_64bit": "Windows (64-bit)",
"windows_arm_64bit": "Windows (ARM 64-bit)"
},
"info_tooltip_title": "Information",
"integration_manager": {
@@ -3898,7 +3898,6 @@
"input_devices": "Input devices",
"jitsi_call": "Jitsi Conference",
"join_button_tooltip_call_full": "Sorry — this call is currently full",
"join_button_tooltip_connecting": "Connecting",
"legacy_call": "Legacy Call",
"maximise": "Fill screen",
"maximise_call": "Maximise call",

View File

@@ -77,13 +77,7 @@ const waitForEvent = async (
};
export enum ConnectionState {
// Widget related states that are equivalent to disconnected,
// but hold additional information about the state of the widget.
Lobby = "lobby",
WidgetLoading = "widget_loading",
Disconnected = "disconnected",
Connecting = "connecting",
Connected = "connected",
Disconnecting = "disconnecting",
}
@@ -100,6 +94,7 @@ export enum CallEvent {
ConnectionState = "connection_state",
Participants = "participants",
Layout = "layout",
Close = "close",
Destroy = "destroy",
}
@@ -110,6 +105,7 @@ interface CallEventHandlerMap {
prevParticipants: Map<RoomMember, Set<string>>,
) => void;
[CallEvent.Layout]: (layout: Layout) => void;
[CallEvent.Close]: () => void;
[CallEvent.Destroy]: () => void;
}
@@ -167,6 +163,17 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
this.emit(CallEvent.Participants, value, prevValue);
}
private _presented = false;
/**
* Whether the call widget is currently being presented in the user interface.
*/
public get presented(): boolean {
return this._presented;
}
public set presented(value: boolean) {
this._presented = value;
}
protected constructor(
/**
* The widget used to access this call.
@@ -177,6 +184,7 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
super();
this.widgetUid = WidgetUtils.getWidgetUid(this.widget);
this.room = this.client.getRoom(this.roomId)!;
WidgetMessagingStore.instance.on(WidgetMessagingStoreEvent.StopMessaging, this.onStopMessaging);
}
/**
@@ -221,8 +229,6 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
* Only call this if the call state is: ConnectionState.Disconnected.
*/
public async start(): Promise<void> {
this.connectionState = ConnectionState.WidgetLoading;
const { [MediaDeviceKindEnum.AudioInput]: audioInputs, [MediaDeviceKindEnum.VideoInput]: videoInputs } =
(await MediaDeviceHandler.getDevices())!;
@@ -257,16 +263,9 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
throw new Error(`Failed to bind call widget in room ${this.roomId}: ${e}`);
}
}
this.connectionState = ConnectionState.Connecting;
try {
await this.performConnection(audioInput, videoInput);
} catch (e) {
this.connectionState = ConnectionState.Disconnected;
throw e;
}
await this.performConnection(audioInput, videoInput);
this.room.on(RoomEvent.MyMembership, this.onMyMembership);
WidgetMessagingStore.instance.on(WidgetMessagingStoreEvent.StopMessaging, this.onStopMessaging);
window.addEventListener("beforeunload", this.beforeUnload);
this.connectionState = ConnectionState.Connected;
}
@@ -280,39 +279,54 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
this.connectionState = ConnectionState.Disconnecting;
await this.performDisconnection();
this.setDisconnected();
this.close();
}
/**
* Manually marks the call as disconnected and cleans up.
* Manually marks the call as disconnected.
*/
public setDisconnected(): void {
this.room.off(RoomEvent.MyMembership, this.onMyMembership);
WidgetMessagingStore.instance.off(WidgetMessagingStoreEvent.StopMessaging, this.onStopMessaging);
window.removeEventListener("beforeunload", this.beforeUnload);
this.messaging = null;
this.connectionState = ConnectionState.Disconnected;
}
/**
* Stops further communication with the widget and tells the UI to close.
*/
protected close(): void {
this.messaging = null;
this.emit(CallEvent.Close);
}
/**
* Stops all internal timers and tasks to prepare for garbage collection.
*/
public destroy(): void {
if (this.connected) this.setDisconnected();
if (this.connected) {
this.setDisconnected();
this.close();
}
WidgetMessagingStore.instance.off(WidgetMessagingStoreEvent.StopMessaging, this.onStopMessaging);
this.emit(CallEvent.Destroy);
}
private onMyMembership = async (_room: Room, membership: Membership): Promise<void> => {
private readonly onMyMembership = async (_room: Room, membership: Membership): Promise<void> => {
if (membership !== KnownMembership.Join) this.setDisconnected();
};
private onStopMessaging = (uid: string): void => {
if (uid === this.widgetUid) {
private readonly onStopMessaging = (uid: string): void => {
if (uid === this.widgetUid && this.connected) {
logger.log("The widget died; treating this as a user hangup");
this.setDisconnected();
this.close();
}
};
private beforeUnload = (): void => this.setDisconnected();
private beforeUnload = (): void => {
this.setDisconnected();
this.close();
};
}
export type { JitsiCallMemberContent };
@@ -466,7 +480,6 @@ export class JitsiCall extends Call {
audioInput: MediaDeviceInfo | null,
videoInput: MediaDeviceInfo | null,
): Promise<void> {
this.connectionState = ConnectionState.Lobby;
// Ensure that the messaging doesn't get stopped while we're waiting for responses
const dontStopMessaging = new Promise<void>((resolve, reject) => {
const messagingStore = WidgetMessagingStore.instance;
@@ -569,9 +582,9 @@ export class JitsiCall extends Call {
super.destroy();
}
private onRoomState = (): void => this.updateParticipants();
private readonly onRoomState = (): void => this.updateParticipants();
private onConnectionState = async (state: ConnectionState, prevState: ConnectionState): Promise<void> => {
private readonly onConnectionState = async (state: ConnectionState, prevState: ConnectionState): Promise<void> => {
if (state === ConnectionState.Connected && !isConnected(prevState)) {
this.updateParticipants(); // Local echo
@@ -597,18 +610,18 @@ export class JitsiCall extends Call {
}
};
private onDock = async (): Promise<void> => {
private readonly onDock = async (): Promise<void> => {
// The widget is no longer a PiP, so let's restore the default layout
await this.messaging!.transport.send(ElementWidgetActions.TileLayout, {});
};
private onUndock = async (): Promise<void> => {
private readonly onUndock = async (): Promise<void> => {
// The widget has become a PiP, so let's switch Jitsi to spotlight mode
// to only show the active speaker and economize on space
await this.messaging!.transport.send(ElementWidgetActions.SpotlightLayout, {});
};
private onHangup = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
private readonly onHangup = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
// If we're already in the middle of a client-initiated disconnection,
// ignore the event
if (this.connectionState === ConnectionState.Disconnecting) return;
@@ -617,14 +630,15 @@ export class JitsiCall extends Call {
// In case this hangup is caused by Jitsi Meet crashing at startup,
// wait for the connection event in order to avoid racing
if (this.connectionState === ConnectionState.Connecting) {
if (this.connectionState === ConnectionState.Disconnected) {
await waitForEvent(this, CallEvent.ConnectionState);
}
await this.messaging!.transport.reply(ev.detail, {}); // ack
this.messaging!.transport.reply(ev.detail, {}); // ack
this.setDisconnected();
this.close();
// In video rooms we immediately want to restart the call after hangup
// The lobby will be shown again and it connects to all signals from EC and Jitsi.
// The lobby will be shown again and it connects to all signals from Jitsi.
if (isVideoRoom(this.room)) {
this.start();
}
@@ -653,6 +667,14 @@ export class ElementCall extends Call {
this.emit(CallEvent.Layout, value);
}
public get presented(): boolean {
return super.presented;
}
public set presented(value: boolean) {
super.presented = value;
this.checkDestroy();
}
private static generateWidgetUrl(client: MatrixClient, roomId: string): URL {
const accountAnalyticsData = client.getAccountData(PosthogAnalytics.ANALYTICS_EVENT_TYPE);
// The analyticsID is passed directly to element call (EC) since this codepath is only for EC and no other widget.
@@ -740,7 +762,7 @@ export class ElementCall extends Call {
// To use Element Call without touching room state, we create a virtual
// widget (one that doesn't have a corresponding state event)
const url = ElementCall.generateWidgetUrl(client, roomId);
return WidgetStore.instance.addVirtualWidget(
const createdWidget = WidgetStore.instance.addVirtualWidget(
{
id: secureRandomString(24), // So that it's globally unique
creatorUserId: client.getUserId()!,
@@ -761,6 +783,8 @@ export class ElementCall extends Call {
},
roomId,
);
WidgetStore.instance.emit(UPDATE_EVENT, null);
return createdWidget;
}
private static getWidgetData(
@@ -794,7 +818,7 @@ export class ElementCall extends Call {
super(widget, client);
this.session.on(MatrixRTCSessionEvent.MembershipsChanged, this.onMembershipChanged);
this.client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, this.onRTCSessionEnded);
this.client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, this.checkDestroy);
SettingsStore.watchSetting(
"feature_disable_call_per_sender_encryption",
null,
@@ -827,9 +851,8 @@ export class ElementCall extends Call {
return null;
}
public static async create(room: Room, skipLobby = false): Promise<void> {
public static create(room: Room, skipLobby = false): void {
ElementCall.createOrGetCallWidget(room.roomId, room.client, skipLobby, false, isVideoRoom(room));
WidgetStore.instance.emit(UPDATE_EVENT, null);
}
protected async sendCallNotify(): Promise<void> {
@@ -875,17 +898,9 @@ export class ElementCall extends Call {
this.messaging!.on(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
this.messaging!.on(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
this.messaging!.on(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
this.messaging!.on(`action:${ElementWidgetActions.DeviceMute}`, async (ev) => {
ev.preventDefault();
await this.messaging!.transport.reply(ev.detail, {}); // ack
});
this.messaging!.once(`action:${ElementWidgetActions.Close}`, this.onClose);
this.messaging!.on(`action:${ElementWidgetActions.DeviceMute}`, this.onDeviceMute);
if (!this.widget.data?.skipLobby) {
// If we do not skip the lobby we need to wait until the widget has
// connected to matrixRTC. This is either observed through the session state
// or the MatrixRTCSessionManager session started event.
this.connectionState = ConnectionState.Lobby;
}
// TODO: if the widget informs us when the join button is clicked (widget action), so we can
// - set state to connecting
// - send call notify
@@ -927,15 +942,16 @@ export class ElementCall extends Call {
public setDisconnected(): void {
this.messaging!.off(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
this.messaging!.off(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
this.messaging!.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
this.messaging!.off(`action:${ElementWidgetActions.DeviceMute}`, this.onDeviceMute);
super.setDisconnected();
}
public destroy(): void {
ActiveWidgetStore.instance.destroyPersistentWidget(this.widget.id, this.widget.roomId);
WidgetStore.instance.removeVirtualWidget(this.widget.id, this.widget.roomId);
this.messaging?.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
this.session.off(MatrixRTCSessionEvent.MembershipsChanged, this.onMembershipChanged);
this.client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionEnded, this.onRTCSessionEnded);
this.client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionEnded, this.checkDestroy);
SettingsStore.unwatchSetting(this.settingsStoreCallEncryptionWatcher);
clearTimeout(this.terminationTimer);
@@ -944,11 +960,10 @@ export class ElementCall extends Call {
super.destroy();
}
private onRTCSessionEnded = (roomId: string, session: MatrixRTCSession): void => {
// Don't destroy the call on hangup for video call rooms.
if (roomId === this.roomId && !this.room.isCallRoom()) {
this.destroy();
}
private checkDestroy = (): void => {
// A call ceases to exist as soon as all participants leave and also the
// user isn't looking at it (for example, waiting in an empty lobby)
if (this.session.memberships.length === 0 && !this.presented && !this.room.isCallRoom()) this.destroy();
};
/**
@@ -960,7 +975,7 @@ export class ElementCall extends Call {
await this.messaging!.transport.send(action, {});
}
private onMembershipChanged = (): void => this.updateParticipants();
private readonly onMembershipChanged = (): void => this.updateParticipants();
private updateParticipants(): void {
const participants = new Map<RoomMember, Set<string>>();
@@ -980,27 +995,40 @@ export class ElementCall extends Call {
this.participants = participants;
}
private onHangup = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
private readonly onDeviceMute = (ev: CustomEvent<IWidgetApiRequest>): void => {
ev.preventDefault();
await this.messaging!.transport.reply(ev.detail, {}); // ack
this.messaging!.transport.reply(ev.detail, {}); // ack
};
private readonly onHangup = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
ev.preventDefault();
this.messaging!.transport.reply(ev.detail, {}); // ack
this.setDisconnected();
};
private readonly onClose = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
ev.preventDefault();
this.messaging!.transport.reply(ev.detail, {}); // ack
// In video rooms we immediately want to reconnect after hangup
// This starts the lobby again and connects to all signals from EC.
if (isVideoRoom(this.room)) {
this.start();
} else {
// User is done with the call; tell the UI to close it
this.close();
}
};
private onTileLayout = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
private readonly onTileLayout = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
ev.preventDefault();
this.layout = Layout.Tile;
await this.messaging!.transport.reply(ev.detail, {}); // ack
this.messaging!.transport.reply(ev.detail, {}); // ack
};
private onSpotlightLayout = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
private readonly onSpotlightLayout = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
ev.preventDefault();
this.layout = Layout.Spotlight;
await this.messaging!.transport.reply(ev.detail, {}); // ack
this.messaging!.transport.reply(ev.detail, {}); // ack
};
public clean(): Promise<void> {

View File

@@ -213,7 +213,6 @@ export async function initSentry(sentryConfig: IConfigOptions["sentry"]): Promis
release: process.env.VERSION,
environment: sentryConfig.environment,
defaultIntegrations: false,
autoSessionTracking: false,
integrations,
// Set to 1.0 which is reasonable if we're only submitting Rageshakes; will need to be set < 1.0
// if we collect more frequently.

View File

@@ -50,6 +50,8 @@ import { type CancelAskToJoinPayload } from "../dispatcher/payloads/CancelAskToJ
import { type SubmitAskToJoinPayload } from "../dispatcher/payloads/SubmitAskToJoinPayload";
import { ModuleRunner } from "../modules/ModuleRunner";
import { setMarkedUnreadState } from "../utils/notifications";
import { ConnectionState, ElementCall } from "../models/Call";
import { isVideoRoom } from "../utils/video-rooms";
const NUM_JOIN_RETRY = 5;
@@ -353,6 +355,23 @@ export class RoomViewStore extends EventEmitter {
});
}
if (room && (payload.view_call || isVideoRoom(room))) {
let call = CallStore.instance.getCall(payload.room_id);
// Start a call if not already there
if (call === null) {
ElementCall.create(room, false);
call = CallStore.instance.getCall(payload.room_id)!;
}
call.presented = true;
// Immediately start the call. This will connect to all required widget events
// and allow the widget to show the lobby.
if (call.connectionState === ConnectionState.Disconnected) call.start();
}
// If we switch to a different room from the call, we are no longer presenting it
const prevRoomCall = this.state.roomId ? CallStore.instance.getCall(this.state.roomId) : null;
if (prevRoomCall !== null && (!payload.view_call || payload.room_id !== this.state.roomId))
prevRoomCall.presented = false;
if (SettingsStore.getValue("feature_sliding_sync") && this.state.roomId !== payload.room_id) {
if (this.state.subscribingRoomId && this.state.subscribingRoomId !== payload.room_id) {
// unsubscribe from this room, but don't await it as we don't care when this gets done.

View File

@@ -12,6 +12,7 @@ export enum ElementWidgetActions {
// All of these actions are currently specific to Jitsi and Element Call
JoinCall = "io.element.join",
HangupCall = "im.vector.hangup",
Close = "io.element.close",
CallParticipants = "io.element.participants",
StartLiveStream = "im.vector.start_live_stream",

View File

@@ -72,8 +72,11 @@ export class WidgetMessagingStore extends AsyncStoreWithClient<EmptyObject> {
* @param {string} widgetUid The widget UID.
*/
public stopMessagingByUid(widgetUid: string): void {
this.widgetMap.remove(widgetUid)?.stop();
this.emit(WidgetMessagingStoreEvent.StopMessaging, widgetUid);
const messaging = this.widgetMap.remove(widgetUid);
if (messaging !== undefined) {
messaging.stop();
this.emit(WidgetMessagingStoreEvent.StopMessaging, widgetUid);
}
}
/**

View File

@@ -16,7 +16,8 @@
background: #f9fafb;
max-width: 680px;
margin: auto;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif,
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}

View File

@@ -16,7 +16,8 @@
background: #f9fafb;
max-width: 680px;
margin: auto;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif,
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}

View File

@@ -16,7 +16,8 @@
background: #f9fafb;
max-width: 680px;
margin: auto;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif,
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}

View File

@@ -109,5 +109,5 @@ export class MockedCall extends Call {
export const useMockedCalls = () => {
Call.get = (room) => MockedCall.get(room);
JitsiCall.create = async (room) => MockedCall.create(room, "1");
ElementCall.create = async (room) => MockedCall.create(room, "1");
ElementCall.create = (room) => MockedCall.create(room, "1");
};

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