Compare commits
51 Commits
t3chguy/oi
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1fdd313ae9 | ||
|
|
18cd641cf6 | ||
|
|
286231aa37 | ||
|
|
3f20df5e08 | ||
|
|
d5e070b300 | ||
|
|
d8ecb6362a | ||
|
|
bcc4ecf0cb | ||
|
|
24d9a174d7 | ||
|
|
7970b968c2 | ||
|
|
59e591c462 | ||
|
|
804cb62698 | ||
|
|
8bb4d44532 | ||
|
|
209ab59978 | ||
|
|
6ae11dab52 | ||
|
|
fac982811c | ||
|
|
d7730f417b | ||
|
|
829b588dbf | ||
|
|
9a7cc7eb34 | ||
|
|
e537da4251 | ||
|
|
094a7071e2 | ||
|
|
a5673f603f | ||
|
|
0c210b9b3a | ||
|
|
05df321f34 | ||
|
|
8116dc5f60 | ||
|
|
d090499329 | ||
|
|
6784d071a6 | ||
|
|
3f47487472 | ||
|
|
89e22e00fb | ||
|
|
bbd798ef36 | ||
|
|
f3f05874fa | ||
|
|
d9091bcba9 | ||
|
|
68692c5af5 | ||
|
|
03dc093e89 | ||
|
|
c68157ec46 | ||
|
|
4fc8b8915b | ||
|
|
690d623dcf | ||
|
|
102a1ddb9e | ||
|
|
99ea51c6f2 | ||
|
|
3f1e56b715 | ||
|
|
f3653abe92 | ||
|
|
a6e8d512d0 | ||
|
|
13c4ab2cf4 | ||
|
|
74da64db63 | ||
|
|
e5d37a324d | ||
|
|
d0c1610bd2 | ||
|
|
64e2a843c3 | ||
|
|
fba59381a0 | ||
|
|
e1970df704 | ||
|
|
b54122884c | ||
|
|
b5f8f2b9f5 | ||
|
|
425adb147a |
1
.github/workflows/static_analysis.yaml
vendored
@@ -51,6 +51,7 @@ jobs:
|
||||
error|invalid_json
|
||||
error|misconfigured
|
||||
welcome_to_element
|
||||
devtools|settings|elementCallUrl
|
||||
|
||||
rethemendex_lint:
|
||||
name: "Rethemendex Check"
|
||||
|
||||
78
CHANGELOG.md
@@ -1,3 +1,81 @@
|
||||
Changes in [1.11.97](https://github.com/element-hq/element-web/releases/tag/v1.11.97) (2025-04-08)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* New room list: reduce padding between avatar and room list border ([#29634](https://github.com/element-hq/element-web/pull/29634)). Contributed by @florianduros.
|
||||
* Bundle Element Call with Element Web packages ([#29309](https://github.com/element-hq/element-web/pull/29309)). Contributed by @t3chguy.
|
||||
* Hide an event notification if it is redacted ([#29605](https://github.com/element-hq/element-web/pull/29605)). Contributed by @Half-Shot.
|
||||
* Docker: Use nginx-unprivileged as base image ([#29353](https://github.com/element-hq/element-web/pull/29353)). Contributed by @AndrewFerr.
|
||||
* Switch away from nesting React trees and mangling the DOM ([#29586](https://github.com/element-hq/element-web/pull/29586)). Contributed by @t3chguy.
|
||||
* New room list: add notification decoration ([#29552](https://github.com/element-hq/element-web/pull/29552)). Contributed by @florianduros.
|
||||
* RoomListStore: Unread filter should match rooms that were marked as unread ([#29580](https://github.com/element-hq/element-web/pull/29580)). Contributed by @MidhunSureshR.
|
||||
* Add support for hiding videos ([#29496](https://github.com/element-hq/element-web/pull/29496)). Contributed by @Half-Shot.
|
||||
* Use an outline icon for the report room button ([#29573](https://github.com/element-hq/element-web/pull/29573)). Contributed by @robintown.
|
||||
* Generate/load pickle key on SSO ([#29568](https://github.com/element-hq/element-web/pull/29568)). Contributed by @Jujure.
|
||||
* Add report room dialog button/dialog. ([#29513](https://github.com/element-hq/element-web/pull/29513)). Contributed by @Half-Shot.
|
||||
* RoomListViewModel: Make the active room sticky in the list ([#29551](https://github.com/element-hq/element-web/pull/29551)). Contributed by @MidhunSureshR.
|
||||
* Replace checkboxes with Compound checkboxes, and appropriately label each checkbox. ([#29363](https://github.com/element-hq/element-web/pull/29363)). Contributed by @Half-Shot.
|
||||
* New room list: add selection decoration ([#29531](https://github.com/element-hq/element-web/pull/29531)). Contributed by @florianduros.
|
||||
* Simplified Sliding Sync ([#28515](https://github.com/element-hq/element-web/pull/28515)). Contributed by @dbkr.
|
||||
* Add ability to hide images after clicking "show image" ([#29467](https://github.com/element-hq/element-web/pull/29467)). Contributed by @Half-Shot.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix scroll issues in memberlist ([#29392](https://github.com/element-hq/element-web/pull/29392)). Contributed by @MidhunSureshR.
|
||||
* Ensure clicks on spoilers do not get handled by the hidden content ([#29618](https://github.com/element-hq/element-web/pull/29618)). Contributed by @t3chguy.
|
||||
* New room list: add cursor pointer on room list item ([#29627](https://github.com/element-hq/element-web/pull/29627)). Contributed by @florianduros.
|
||||
* Fix missing ambiguous url tooltips on Element Desktop ([#29619](https://github.com/element-hq/element-web/pull/29619)). Contributed by @t3chguy.
|
||||
* New room list: fix spacing and padding ([#29607](https://github.com/element-hq/element-web/pull/29607)). Contributed by @florianduros.
|
||||
* Make fetchdep check out matching branch name ([#29601](https://github.com/element-hq/element-web/pull/29601)). Contributed by @dbkr.
|
||||
* Fix MFileBody fileName not considering `filename` ([#29589](https://github.com/element-hq/element-web/pull/29589)). Contributed by @t3chguy.
|
||||
* Fix token expiry racing with login causing wrong error to be shown ([#29566](https://github.com/element-hq/element-web/pull/29566)). Contributed by @t3chguy.
|
||||
* Fix bug which caused startup to hang if the clock was wound back since a previous session ([#29558](https://github.com/element-hq/element-web/pull/29558)). Contributed by @richvdh.
|
||||
* RoomListViewModel: Reset any primary filter on secondary filter change ([#29562](https://github.com/element-hq/element-web/pull/29562)). Contributed by @MidhunSureshR.
|
||||
* RoomListStore: Unread filter should only filter rooms having unread counts ([#29555](https://github.com/element-hq/element-web/pull/29555)). Contributed by @MidhunSureshR.
|
||||
* In force-verify mode, prevent bypassing by cancelling device verification ([#29487](https://github.com/element-hq/element-web/pull/29487)). Contributed by @andybalaam.
|
||||
* Add title attribute to user identifier ([#29547](https://github.com/element-hq/element-web/pull/29547)). Contributed by @arpitbatra123.
|
||||
|
||||
|
||||
Changes in [1.11.96](https://github.com/element-hq/element-web/releases/tag/v1.11.96) (2025-03-25)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* RoomListViewModel: Track the index of the active room in the list ([#29519](https://github.com/element-hq/element-web/pull/29519)). Contributed by @MidhunSureshR.
|
||||
* New room list: add empty state ([#29512](https://github.com/element-hq/element-web/pull/29512)). Contributed by @florianduros.
|
||||
* Implement `MessagePreviewViewModel` ([#29514](https://github.com/element-hq/element-web/pull/29514)). Contributed by @MidhunSureshR.
|
||||
* RoomListViewModel: Add functionality to toggle message preview setting ([#29511](https://github.com/element-hq/element-web/pull/29511)). Contributed by @MidhunSureshR.
|
||||
* New room list: add more options menu on room list item ([#29445](https://github.com/element-hq/element-web/pull/29445)). Contributed by @florianduros.
|
||||
* RoomListViewModel: Provide a way to resort the room list and track the active sort method ([#29499](https://github.com/element-hq/element-web/pull/29499)). Contributed by @MidhunSureshR.
|
||||
* Change \*All rooms\* meta space name to \*All Chats\* ([#29498](https://github.com/element-hq/element-web/pull/29498)). Contributed by @florianduros.
|
||||
* Add setting to hide avatars of rooms you have been invited to. ([#29497](https://github.com/element-hq/element-web/pull/29497)). Contributed by @Half-Shot.
|
||||
* Room List Store: Save preferred sorting algorithm and use that on app launch ([#29493](https://github.com/element-hq/element-web/pull/29493)). Contributed by @MidhunSureshR.
|
||||
* Add key storage toggle to Encryption settings ([#29310](https://github.com/element-hq/element-web/pull/29310)). Contributed by @dbkr.
|
||||
* New room list: add primary filters ([#29481](https://github.com/element-hq/element-web/pull/29481)). Contributed by @florianduros.
|
||||
* Implement MSC4142: Remove unintentional intentional mentions in replies ([#28209](https://github.com/element-hq/element-web/pull/28209)). Contributed by @tulir.
|
||||
* White background for 'They do not match' button ([#29470](https://github.com/element-hq/element-web/pull/29470)). Contributed by @andybalaam.
|
||||
* RoomListViewModel: Support secondary filters in the view model ([#29465](https://github.com/element-hq/element-web/pull/29465)). Contributed by @MidhunSureshR.
|
||||
* RoomListViewModel: Support primary filters in the view model ([#29454](https://github.com/element-hq/element-web/pull/29454)). Contributed by @MidhunSureshR.
|
||||
* Room List Store: Implement secondary filters ([#29458](https://github.com/element-hq/element-web/pull/29458)). Contributed by @MidhunSureshR.
|
||||
* Room List Store: Implement rest of the primary filters ([#29444](https://github.com/element-hq/element-web/pull/29444)). Contributed by @MidhunSureshR.
|
||||
* Room List Store: Support filters by implementing just the favourite filter ([#29433](https://github.com/element-hq/element-web/pull/29433)). Contributed by @MidhunSureshR.
|
||||
* Move toggle switch for integration manager for a11y ([#29436](https://github.com/element-hq/element-web/pull/29436)). Contributed by @Half-Shot.
|
||||
* New room list: basic flat list ([#29368](https://github.com/element-hq/element-web/pull/29368)). Contributed by @florianduros.
|
||||
* Improve rageshake upload experience by providing useful error information ([#29378](https://github.com/element-hq/element-web/pull/29378)). Contributed by @Half-Shot.
|
||||
* Add more functionality to the room list vm ([#29402](https://github.com/element-hq/element-web/pull/29402)). Contributed by @MidhunSureshR.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* New room list: fix compose menu action in space ([#29500](https://github.com/element-hq/element-web/pull/29500)). Contributed by @florianduros.
|
||||
* Change ToggleHiddenEventVisibility \& GoToHome KeyBindingActions ([#29374](https://github.com/element-hq/element-web/pull/29374)). Contributed by @gy-mate.
|
||||
* Fix Docker Healthcheck ([#29471](https://github.com/element-hq/element-web/pull/29471)). Contributed by @benbz.
|
||||
* Room List Store: Fetch rooms after space store is ready + attach store to window ([#29453](https://github.com/element-hq/element-web/pull/29453)). Contributed by @MidhunSureshR.
|
||||
* Room List Store: Fix bug where left rooms appear in room list ([#29452](https://github.com/element-hq/element-web/pull/29452)). Contributed by @MidhunSureshR.
|
||||
* Add space to the bottom of the room summary actions below leave room ([#29270](https://github.com/element-hq/element-web/pull/29270)). Contributed by @langleyd.
|
||||
* Show error screens in group calls ([#29254](https://github.com/element-hq/element-web/pull/29254)). Contributed by @robintown.
|
||||
* Prevent user from accidentally triggering multiple identity resets ([#29388](https://github.com/element-hq/element-web/pull/29388)). Contributed by @uhoreg.
|
||||
* Remove buggy tooltip on room intro \& homepage ([#29406](https://github.com/element-hq/element-web/pull/29406)). Contributed by @t3chguy.
|
||||
|
||||
|
||||
Changes in [1.11.95](https://github.com/element-hq/element-web/releases/tag/v1.11.95) (2025-03-11)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
12
Dockerfile
@@ -19,7 +19,10 @@ RUN /src/scripts/docker-package.sh
|
||||
RUN cp /src/config.sample.json /src/webapp/config.json
|
||||
|
||||
# App
|
||||
FROM nginx:alpine-slim
|
||||
FROM nginxinc/nginx-unprivileged:alpine-slim
|
||||
|
||||
# Need root user to install packages & manipulate the usr directory
|
||||
USER root
|
||||
|
||||
# Install jq and moreutils for sponge, both used by our entrypoints
|
||||
RUN apk add jq moreutils
|
||||
@@ -31,13 +34,6 @@ COPY --from=builder /src/webapp /app
|
||||
COPY /docker/nginx-templates/* /etc/nginx/templates/
|
||||
COPY /docker/docker-entrypoint.d/* /docker-entrypoint.d/
|
||||
|
||||
# Tell nginx to put its pidfile elsewhere, so it can run as non-root
|
||||
RUN sed -i -e 's,/var/run/nginx.pid,/tmp/nginx.pid,' /etc/nginx/nginx.conf
|
||||
|
||||
# nginx user must own the cache and etc directory to write cache and tweak the nginx config
|
||||
RUN chown -R nginx:0 /var/cache/nginx /etc/nginx
|
||||
RUN chmod -R g+w /var/cache/nginx /etc/nginx
|
||||
|
||||
RUN rm -rf /usr/share/nginx/html \
|
||||
&& ln -s /app /usr/share/nginx/html
|
||||
|
||||
|
||||
@@ -384,8 +384,6 @@ The VoIP and Jitsi options are:
|
||||
5. `audio_stream_url`: Optional URL to pass to Jitsi to enable live streaming. This option is considered experimental and may be removed
|
||||
at any time without notice.
|
||||
6. `element_call`: Optional configuration for native group calls using Element Call, with the following subkeys:
|
||||
- `url`: The URL of the Element Call instance to use for native group calls. This option is considered experimental
|
||||
and may be removed at any time without notice. Defaults to `https://call.element.io`.
|
||||
- `use_exclusively`: A boolean specifying whether Element Call should be used exclusively as the only VoIP stack in
|
||||
the app, removing the ability to start legacy 1:1 calls or Jitsi calls. Defaults to `false`.
|
||||
- `participant_limit`: The maximum number of users who can join a call; if
|
||||
|
||||
2
knip.ts
@@ -40,6 +40,8 @@ export default {
|
||||
// Used by webpack
|
||||
"process",
|
||||
"util",
|
||||
// Embedded into webapp
|
||||
"@element-hq/element-call-embedded",
|
||||
],
|
||||
ignoreBinaries: [
|
||||
// Used in scripts & workflows
|
||||
|
||||
15
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "element-web",
|
||||
"version": "1.11.95",
|
||||
"version": "1.11.97",
|
||||
"description": "Element: the future of secure communication",
|
||||
"author": "New Vector Ltd.",
|
||||
"repository": {
|
||||
@@ -65,7 +65,8 @@
|
||||
"test:playwright:screenshots": "playwright-screenshots --project=Chrome",
|
||||
"coverage": "yarn test --coverage",
|
||||
"analyse:webpack-bundles": "webpack-bundle-analyzer webpack-stats.json webapp",
|
||||
"update:jitsi": "curl -s https://meet.element.io/libs/external_api.min.js > ./res/jitsi_external_api.min.js"
|
||||
"update:jitsi": "curl -s https://meet.element.io/libs/external_api.min.js > ./res/jitsi_external_api.min.js",
|
||||
"postinstall": "patch-package"
|
||||
},
|
||||
"resolutions": {
|
||||
"@playwright/test": "1.51.1",
|
||||
@@ -92,7 +93,7 @@
|
||||
"@types/png-chunks-extract": "^1.0.2",
|
||||
"@types/react-virtualized": "^9.21.30",
|
||||
"@vector-im/compound-design-tokens": "^4.0.0",
|
||||
"@vector-im/compound-web": "^7.7.2",
|
||||
"@vector-im/compound-web": "^7.9.0",
|
||||
"@vector-im/matrix-wysiwyg": "2.38.2",
|
||||
"@zxcvbn-ts/core": "^3.0.4",
|
||||
"@zxcvbn-ts/language-common": "^3.0.4",
|
||||
@@ -107,6 +108,7 @@
|
||||
"css-tree": "^3.0.0",
|
||||
"diff-dom": "^5.0.0",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"domutils": "^3.2.2",
|
||||
"emojibase-regex": "15.3.2",
|
||||
"escape-html": "^1.0.3",
|
||||
"file-saver": "^2.0.5",
|
||||
@@ -115,12 +117,12 @@
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
"highlight.js": "^11.3.1",
|
||||
"html-entities": "^2.0.0",
|
||||
"html-react-parser": "^5.2.2",
|
||||
"is-ip": "^3.1.0",
|
||||
"js-xxhash": "^4.0.0",
|
||||
"jsrsasign": "^11.0.0",
|
||||
"jszip": "^3.7.0",
|
||||
"katex": "^0.16.0",
|
||||
"linkify-element": "4.2.0",
|
||||
"linkify-react": "4.2.0",
|
||||
"linkify-string": "4.2.0",
|
||||
"linkifyjs": "4.2.0",
|
||||
@@ -128,7 +130,7 @@
|
||||
"maplibre-gl": "^5.0.0",
|
||||
"matrix-encrypt-attachment": "^1.0.3",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||
"matrix-js-sdk": "37.3.0",
|
||||
"matrix-widget-api": "^1.10.0",
|
||||
"memoize-one": "^6.0.0",
|
||||
"mime": "^4.0.4",
|
||||
@@ -144,6 +146,7 @@
|
||||
"react-blurhash": "^0.3.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-focus-lock": "^2.5.1",
|
||||
"react-string-replace": "^1.1.1",
|
||||
"react-transition-group": "^4.4.1",
|
||||
"react-virtualized": "^9.22.5",
|
||||
"rfc4648": "^1.4.0",
|
||||
@@ -177,6 +180,7 @@
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@casualbot/jest-sonar-reporter": "2.2.7",
|
||||
"@element-hq/element-call-embedded": "0.9.0",
|
||||
"@element-hq/element-web-playwright-common": "^1.1.5",
|
||||
"@peculiar/webcrypto": "^1.4.3",
|
||||
"@playwright/test": "^1.50.1",
|
||||
@@ -263,6 +267,7 @@
|
||||
"minimist": "^1.2.6",
|
||||
"modernizr": "^3.12.0",
|
||||
"node-fetch": "^2.6.7",
|
||||
"patch-package": "^8.0.0",
|
||||
"playwright-core": "^1.51.0",
|
||||
"postcss": "8.4.46",
|
||||
"postcss-easings": "^4.0.0",
|
||||
|
||||
13
patches/@matrix-org+react-sdk-module-api+2.5.0.patch
Normal file
@@ -0,0 +1,13 @@
|
||||
diff --git a/node_modules/@matrix-org/react-sdk-module-api/lib/ModuleApi.d.ts b/node_modules/@matrix-org/react-sdk-module-api/lib/ModuleApi.d.ts
|
||||
index 917a7fc..a2710c6 100644
|
||||
--- a/node_modules/@matrix-org/react-sdk-module-api/lib/ModuleApi.d.ts
|
||||
+++ b/node_modules/@matrix-org/react-sdk-module-api/lib/ModuleApi.d.ts
|
||||
@@ -37,7 +37,7 @@ export interface ModuleApi {
|
||||
* @returns Whether the user submitted the dialog or closed it, and the model returned by the
|
||||
* dialog component if submitted.
|
||||
*/
|
||||
- openDialog<M extends object, P extends DialogProps = DialogProps, C extends DialogContent<P> = DialogContent<P>>(initialTitleOrOptions: string | ModuleUiDialogOptions, body: (props: P, ref: React.RefObject<C>) => React.ReactNode, props?: Omit<P, keyof DialogProps>): Promise<{
|
||||
+ openDialog<M extends object, P extends DialogProps = DialogProps, C extends DialogContent<P> = DialogContent<P>>(initialTitleOrOptions: string | ModuleUiDialogOptions, body: (props: P, ref: React.RefObject<C | null>) => React.ReactNode, props?: Omit<P, keyof DialogProps>): Promise<{
|
||||
didOkOrSubmit: boolean;
|
||||
model: M;
|
||||
}>;
|
||||
76
patches/@types+react+18.3.18.patch
Normal file
@@ -0,0 +1,76 @@
|
||||
diff --git a/node_modules/@types/react/index.d.ts b/node_modules/@types/react/index.d.ts
|
||||
index 6ea73ef..cb51757 100644
|
||||
--- a/node_modules/@types/react/index.d.ts
|
||||
+++ b/node_modules/@types/react/index.d.ts
|
||||
@@ -151,7 +151,7 @@ declare namespace React {
|
||||
/**
|
||||
* The current value of the ref.
|
||||
*/
|
||||
- readonly current: T | null;
|
||||
+ current: T;
|
||||
}
|
||||
|
||||
interface DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES {
|
||||
@@ -186,7 +186,7 @@ declare namespace React {
|
||||
* @see {@link RefObject}
|
||||
*/
|
||||
|
||||
- type Ref<T> = RefCallback<T> | RefObject<T> | null;
|
||||
+ type Ref<T> = RefCallback<T> | RefObject<T | null> | null;
|
||||
/**
|
||||
* A legacy implementation of refs where you can pass a string to a ref prop.
|
||||
*
|
||||
@@ -300,7 +300,7 @@ declare namespace React {
|
||||
*
|
||||
* @see {@link https://react.dev/learn/referencing-values-with-refs#refs-and-the-dom React Docs}
|
||||
*/
|
||||
- ref?: LegacyRef<T> | undefined;
|
||||
+ ref?: LegacyRef<T | null> | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1234,7 +1234,7 @@ declare namespace React {
|
||||
*
|
||||
* @see {@link ForwardRefRenderFunction}
|
||||
*/
|
||||
- type ForwardedRef<T> = ((instance: T | null) => void) | MutableRefObject<T | null> | null;
|
||||
+ type ForwardedRef<T> = ((instance: T | null) => void) | RefObject<T | null> | null;
|
||||
|
||||
/**
|
||||
* The type of the function passed to {@link forwardRef}. This is considered different
|
||||
@@ -1565,7 +1565,7 @@ declare namespace React {
|
||||
[propertyName: string]: any;
|
||||
}
|
||||
|
||||
- function createRef<T>(): RefObject<T>;
|
||||
+ function createRef<T>(): RefObject<T | null>;
|
||||
|
||||
/**
|
||||
* The type of the component returned from {@link forwardRef}.
|
||||
@@ -1989,7 +1989,7 @@ declare namespace React {
|
||||
* @version 16.8.0
|
||||
* @see {@link https://react.dev/reference/react/useRef}
|
||||
*/
|
||||
- function useRef<T>(initialValue: T): MutableRefObject<T>;
|
||||
+ function useRef<T>(initialValue: T): RefObject<T>;
|
||||
// convenience overload for refs given as a ref prop as they typically start with a null value
|
||||
/**
|
||||
* `useRef` returns a mutable ref object whose `.current` property is initialized to the passed argument
|
||||
@@ -2004,7 +2004,7 @@ declare namespace React {
|
||||
* @version 16.8.0
|
||||
* @see {@link https://react.dev/reference/react/useRef}
|
||||
*/
|
||||
- function useRef<T>(initialValue: T | null): RefObject<T>;
|
||||
+ function useRef<T>(initialValue: T | null): RefObject<T | null>;
|
||||
// convenience overload for potentially undefined initialValue / call with 0 arguments
|
||||
// has a default to stop it from defaulting to {} instead
|
||||
/**
|
||||
@@ -2017,7 +2017,7 @@ declare namespace React {
|
||||
* @version 16.8.0
|
||||
* @see {@link https://react.dev/reference/react/useRef}
|
||||
*/
|
||||
- function useRef<T = undefined>(initialValue?: undefined): MutableRefObject<T | undefined>;
|
||||
+ function useRef<T>(initialValue: T | undefined): RefObject<T | undefined>;
|
||||
/**
|
||||
* The signature is identical to `useEffect`, but it fires synchronously after all DOM mutations.
|
||||
* Use this to read layout from the DOM and synchronously re-render. Updates scheduled inside
|
||||
@@ -162,6 +162,7 @@ test.describe("Cryptography", function () {
|
||||
}
|
||||
|
||||
test("Can reset cross-signing keys", async ({ page, app, user: aliceCredentials }) => {
|
||||
await app.client.bootstrapCrossSigning(aliceCredentials);
|
||||
const secretStorageKey = await enableKeyBackup(app);
|
||||
|
||||
// Fetch the current cross-signing keys
|
||||
|
||||
@@ -27,16 +27,22 @@ test.use({
|
||||
test.describe("Dehydration", () => {
|
||||
test.skip(isDendrite, "does not yet support dehydration v2");
|
||||
|
||||
test("'Set up secure backup' creates dehydrated device", async ({ page, user, app }, workerInfo) => {
|
||||
// Create a backup (which will create SSSS, and dehydrated device)
|
||||
test("Verify device and reset creates dehydrated device", async ({ page, user, credentials, app }, workerInfo) => {
|
||||
// Verify the device by resetting the key (which will create SSSS, and dehydrated device)
|
||||
|
||||
const securityTab = await app.settings.openUserSettings("Security & Privacy");
|
||||
|
||||
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
|
||||
await expect(securityTab.getByText("Offline device enabled")).not.toBeVisible();
|
||||
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
|
||||
|
||||
await completeCreateSecretStorageDialog(page);
|
||||
await app.closeDialog();
|
||||
|
||||
// Verify the device by resetting the key
|
||||
const settings = await app.settings.openUserSettings("Encryption");
|
||||
await settings.getByRole("button", { name: "Verify this device" }).click();
|
||||
await page.getByRole("button", { name: "Proceed with reset" }).click();
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
await page.getByRole("button", { name: "Copy" }).click();
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
await page.getByRole("button", { name: "Done" }).click();
|
||||
|
||||
await expectDehydratedDeviceEnabled(app);
|
||||
|
||||
|
||||
@@ -324,7 +324,7 @@ test.describe("Cryptography", function () {
|
||||
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
||||
await lastE2eIcon.focus();
|
||||
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
|
||||
"Sender's verified identity has changed",
|
||||
"Sender's verified identity was reset",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -52,6 +52,6 @@ test.describe("Invisible cryptography", () => {
|
||||
/* should show an error for a message from a previously verified device */
|
||||
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from user that was previously verified");
|
||||
const lastTile = page.locator(".mx_EventTile_last");
|
||||
await expect(lastTile).toContainText("Sender's verified identity has changed");
|
||||
await expect(lastTile).toContainText("Sender's verified identity was reset");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -292,17 +292,28 @@ export async function doTwoWaySasVerification(page: Page, verifier: JSHandle<Ver
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the security settings and enable secure key backup.
|
||||
*
|
||||
* Assumes that the current device has been cross-signed (which means that we skip a step where we set it up).
|
||||
* Open the encryption settings and enable key storage and recovery
|
||||
* Assumes that the current device has been verified
|
||||
*
|
||||
* Returns the recovery key
|
||||
*/
|
||||
export async function enableKeyBackup(app: ElementAppPage): Promise<string> {
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
await app.page.getByRole("button", { name: "Set up Secure Backup" }).click();
|
||||
const encryptionTab = await app.settings.openUserSettings("Encryption");
|
||||
|
||||
return await completeCreateSecretStorageDialog(app.page);
|
||||
const keyStorageToggle = encryptionTab.getByRole("checkbox", { name: "Allow key storage" });
|
||||
if (!(await keyStorageToggle.isChecked())) {
|
||||
await encryptionTab.getByRole("checkbox", { name: "Allow key storage" }).click();
|
||||
}
|
||||
|
||||
await encryptionTab.getByRole("button", { name: "Set up recovery" }).click();
|
||||
await encryptionTab.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
const recoveryKey = await encryptionTab.getByTestId("recoveryKey").innerText();
|
||||
await encryptionTab.getByRole("button", { name: "Continue" }).click();
|
||||
await encryptionTab.getByRole("textbox").fill(recoveryKey);
|
||||
await encryptionTab.getByRole("button", { name: "Finish set up" }).click();
|
||||
await app.settings.closeDialog();
|
||||
return recoveryKey;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -36,17 +36,21 @@ test.describe("Room list filters and sort", () => {
|
||||
return page.getByTestId("room-list");
|
||||
}
|
||||
|
||||
let unReadDmId: string | undefined;
|
||||
let unReadRoomId: string | undefined;
|
||||
|
||||
test.beforeEach(async ({ page, app, bot, user }) => {
|
||||
await app.client.createRoom({ name: "empty room" });
|
||||
|
||||
const unReadDmId = await bot.createRoom({
|
||||
unReadDmId = await bot.createRoom({
|
||||
name: "unread dm",
|
||||
invite: [user.userId],
|
||||
is_direct: true,
|
||||
});
|
||||
await app.client.joinRoom(unReadDmId);
|
||||
await bot.sendMessage(unReadDmId, "I am a robot. Beep.");
|
||||
|
||||
const unReadRoomId = await app.client.createRoom({ name: "unread room" });
|
||||
unReadRoomId = await app.client.createRoom({ name: "unread room" });
|
||||
await app.client.inviteUser(unReadRoomId, bot.credentials.userId);
|
||||
await bot.joinRoom(unReadRoomId);
|
||||
await bot.sendMessage(unReadRoomId, "I am a robot. Beep.");
|
||||
@@ -88,6 +92,30 @@ test.describe("Room list filters and sort", () => {
|
||||
await expect(roomList.getByRole("gridcell", { name: "empty room" })).toBeVisible();
|
||||
expect(await roomList.locator("role=gridcell").count()).toBe(3);
|
||||
});
|
||||
|
||||
test("unread filter should only match unread rooms that have a count", async ({ page, app, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
// Let's configure unread dm room so that we only get notification for mentions and keywords
|
||||
await app.viewRoomById(unReadDmId);
|
||||
await app.settings.openRoomSettings("Notifications");
|
||||
await page.getByText("@mentions & keywords").click();
|
||||
await app.settings.closeDialog();
|
||||
|
||||
// Let's open a room other than unread room or unread dm
|
||||
await roomListView.getByRole("gridcell", { name: "Open room favourite room" }).click();
|
||||
|
||||
// Let's make the bot send a new message in both rooms
|
||||
await bot.sendMessage(unReadDmId, "Hello!");
|
||||
await bot.sendMessage(unReadRoomId, "Hello!");
|
||||
|
||||
// Let's activate the unread filter now
|
||||
await page.getByRole("option", { name: "Unread" }).click();
|
||||
|
||||
// Unread filter should only show unread room and not unread dm!
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room unread room" })).toBeVisible();
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room unread dm" })).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Empty room list", () => {
|
||||
|
||||
@@ -13,6 +13,9 @@ test.describe("Room list", () => {
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
labsFlags: ["feature_new_room_list"],
|
||||
botCreateOpts: {
|
||||
displayName: "BotBob",
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -26,108 +29,199 @@ test.describe("Room list", () => {
|
||||
test.beforeEach(async ({ page, app, user }) => {
|
||||
// The notification toast is displayed above the search section
|
||||
await app.closeNotificationToast();
|
||||
for (let i = 0; i < 30; i++) {
|
||||
await app.client.createRoom({ name: `room${i}` });
|
||||
}
|
||||
});
|
||||
|
||||
test("should render the room list", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room29" })).toBeVisible();
|
||||
await expect(roomListView).toMatchScreenshot("room-list.png");
|
||||
|
||||
await roomListView.hover();
|
||||
// Scroll to the end of the room list
|
||||
await page.mouse.wheel(0, 1000);
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
|
||||
await expect(roomListView).toMatchScreenshot("room-list-scrolled.png");
|
||||
});
|
||||
|
||||
test("should open the room when it is clicked", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||
await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible();
|
||||
});
|
||||
|
||||
test("should open the more options menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
const roomItem = roomListView.getByRole("gridcell", { name: "Open room room29" });
|
||||
await roomItem.hover();
|
||||
|
||||
await expect(roomItem).toMatchScreenshot("room-list-item-hover.png");
|
||||
const roomItemMenu = roomItem.getByRole("button", { name: "More Options" });
|
||||
await roomItemMenu.click();
|
||||
await expect(page).toMatchScreenshot("room-list-item-open-more-options.png");
|
||||
|
||||
// It should make the room favourited
|
||||
await page.getByRole("menuitemcheckbox", { name: "Favourited" }).click();
|
||||
|
||||
// Check that the room is favourited
|
||||
await roomItem.hover();
|
||||
await roomItemMenu.click();
|
||||
await expect(page.getByRole("menuitemcheckbox", { name: "Favourited" })).toBeChecked();
|
||||
// It should show the invite dialog
|
||||
await page.getByRole("menuitem", { name: "invite" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Invite to room29" })).toBeVisible();
|
||||
await app.closeDialog();
|
||||
|
||||
// It should leave the room
|
||||
await roomItem.hover();
|
||||
await roomItemMenu.click();
|
||||
await page.getByRole("menuitem", { name: "leave room" }).click();
|
||||
await expect(roomItem).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("should scroll to the current room", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await roomListView.hover();
|
||||
// Scroll to the end of the room list
|
||||
await page.mouse.wheel(0, 1000);
|
||||
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room0" }).click();
|
||||
|
||||
const filters = page.getByRole("listbox", { name: "Room list filters" });
|
||||
await filters.getByRole("option", { name: "People" }).click();
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).not.toBeVisible();
|
||||
|
||||
await filters.getByRole("option", { name: "People" }).click();
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("unread filter should only match unread rooms that have a count", async ({ page, app, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
// Let's create a new room and invite the bot
|
||||
const room1Id = await app.client.createRoom({
|
||||
name: "Unread Room 1",
|
||||
invite: [bot.credentials?.userId],
|
||||
test.describe("Room list", () => {
|
||||
test.beforeEach(async ({ page, app, user }) => {
|
||||
for (let i = 0; i < 30; i++) {
|
||||
await app.client.createRoom({ name: `room${i}` });
|
||||
}
|
||||
});
|
||||
await bot.awaitRoomMembership(room1Id);
|
||||
|
||||
// Let's create another room as well
|
||||
const room2Id = await app.client.createRoom({
|
||||
name: "Unread Room 2",
|
||||
invite: [bot.credentials?.userId],
|
||||
test("should render the room list", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room29" })).toBeVisible();
|
||||
await expect(roomListView).toMatchScreenshot("room-list.png");
|
||||
|
||||
await roomListView.hover();
|
||||
// Scroll to the end of the room list
|
||||
await page.mouse.wheel(0, 1000);
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
|
||||
await expect(roomListView).toMatchScreenshot("room-list-scrolled.png");
|
||||
});
|
||||
await bot.awaitRoomMembership(room2Id);
|
||||
|
||||
// Let's configure unread room 1 so that we only get notification for mentions and keywords
|
||||
await app.viewRoomById(room1Id);
|
||||
await app.settings.openRoomSettings("Notifications");
|
||||
await page.getByText("@mentions & keywords").click();
|
||||
await app.settings.closeDialog();
|
||||
test("should open the room when it is clicked", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||
await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible();
|
||||
});
|
||||
|
||||
// Let's open a room other than room 1 or room 2
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||
test("should open the more options menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
const roomItem = roomListView.getByRole("gridcell", { name: "Open room room29" });
|
||||
await roomItem.hover();
|
||||
|
||||
// Let's make the bot send a new message in both room 1 and room 2
|
||||
await bot.sendMessage(room1Id, "Hello!");
|
||||
await bot.sendMessage(room2Id, "Hello!");
|
||||
await expect(roomItem).toMatchScreenshot("room-list-item-hover.png");
|
||||
const roomItemMenu = roomItem.getByRole("button", { name: "More Options" });
|
||||
await roomItemMenu.click();
|
||||
await expect(page).toMatchScreenshot("room-list-item-open-more-options.png");
|
||||
|
||||
// Let's activate the unread filter now
|
||||
await page.getByRole("option", { name: "Unread" }).click();
|
||||
// It should make the room favourited
|
||||
await page.getByRole("menuitemcheckbox", { name: "Favourited" }).click();
|
||||
|
||||
// Unread filter should only show room 2!!
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room Unread Room 2" })).toBeVisible();
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room Unread Room 1" })).not.toBeVisible();
|
||||
// Check that the room is favourited
|
||||
await roomItem.hover();
|
||||
await roomItemMenu.click();
|
||||
await expect(page.getByRole("menuitemcheckbox", { name: "Favourited" })).toBeChecked();
|
||||
// It should show the invite dialog
|
||||
await page.getByRole("menuitem", { name: "invite" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Invite to room29" })).toBeVisible();
|
||||
await app.closeDialog();
|
||||
|
||||
// It should leave the room
|
||||
await roomItem.hover();
|
||||
await roomItemMenu.click();
|
||||
await page.getByRole("menuitem", { name: "leave room" }).click();
|
||||
await expect(roomItem).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("should scroll to the current room", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await roomListView.hover();
|
||||
// Scroll to the end of the room list
|
||||
await page.mouse.wheel(0, 1000);
|
||||
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room0" }).click();
|
||||
|
||||
const filters = page.getByRole("listbox", { name: "Room list filters" });
|
||||
await filters.getByRole("option", { name: "People" }).click();
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).not.toBeVisible();
|
||||
|
||||
await filters.getByRole("option", { name: "People" }).click();
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Notification decoration", () => {
|
||||
test("should render the invitation decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
await bot.createRoom({
|
||||
name: "invited room",
|
||||
invite: [user.userId],
|
||||
is_direct: true,
|
||||
});
|
||||
const invitedRoom = roomListView.getByRole("gridcell", { name: "invited room" });
|
||||
await expect(invitedRoom).toBeVisible();
|
||||
await expect(invitedRoom).toMatchScreenshot("room-list-item-invited.png");
|
||||
});
|
||||
|
||||
test("should render the regular decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
const roomId = await app.client.createRoom({ name: "2 notifications" });
|
||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||
await bot.joinRoom(roomId);
|
||||
|
||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||
|
||||
const room = roomListView.getByRole("gridcell", { name: "2 notifications" });
|
||||
await expect(room).toBeVisible();
|
||||
await expect(room.getByTestId("notification-decoration")).toHaveText("2");
|
||||
await expect(room).toMatchScreenshot("room-list-item-notification.png");
|
||||
});
|
||||
|
||||
test("should render the mention decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
const roomId = await app.client.createRoom({ name: "mention" });
|
||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||
await bot.joinRoom(roomId);
|
||||
|
||||
const clientBot = await bot.prepareClient();
|
||||
await clientBot.evaluate(
|
||||
async (client, { roomId, userId }) => {
|
||||
await client.sendMessage(roomId, {
|
||||
// @ts-ignore ignore usage of MsgType.text
|
||||
"msgtype": "m.text",
|
||||
"body": "User",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": `<a href="https://matrix.to/#/${userId}">User</a>`,
|
||||
"m.mentions": {
|
||||
user_ids: [userId],
|
||||
},
|
||||
});
|
||||
},
|
||||
{ roomId, userId: user.userId },
|
||||
);
|
||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||
|
||||
const room = roomListView.getByRole("gridcell", { name: "mention" });
|
||||
await expect(room).toBeVisible();
|
||||
await expect(room).toMatchScreenshot("room-list-item-mention.png");
|
||||
});
|
||||
|
||||
test("should render an activity decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
const otherRoomId = await app.client.createRoom({ name: "other room" });
|
||||
|
||||
const roomId = await app.client.createRoom({ name: "activity" });
|
||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||
await bot.joinRoom(roomId);
|
||||
|
||||
await app.viewRoomById(roomId);
|
||||
await app.settings.openRoomSettings("Notifications");
|
||||
await page.getByText("@mentions & keywords").click();
|
||||
await app.settings.closeDialog();
|
||||
|
||||
await app.settings.openUserSettings("Notifications");
|
||||
await page.getByText("Show all activity in the room list (dots or number of unread messages)").click();
|
||||
await app.settings.closeDialog();
|
||||
|
||||
// Switch to the other room to avoid the notification to be cleared
|
||||
await app.viewRoomById(otherRoomId);
|
||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||
|
||||
const room = roomListView.getByRole("gridcell", { name: "activity" });
|
||||
await expect(room.getByTestId("notification-decoration")).toBeVisible();
|
||||
await expect(room).toMatchScreenshot("room-list-item-activity.png");
|
||||
});
|
||||
|
||||
test("should render a mark as unread decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
const roomId = await app.client.createRoom({ name: "mark as unread" });
|
||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||
await bot.joinRoom(roomId);
|
||||
|
||||
const room = roomListView.getByRole("gridcell", { name: "mark as unread" });
|
||||
await room.hover();
|
||||
await room.getByRole("button", { name: "More Options" }).click();
|
||||
await page.getByRole("menuitem", { name: "mark as unread" }).click();
|
||||
|
||||
// Remove hover on the room list item
|
||||
await roomListView.hover();
|
||||
|
||||
await expect(room).toMatchScreenshot("room-list-item-mark-as-unread.png");
|
||||
});
|
||||
|
||||
test("should render silent decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
const roomId = await app.client.createRoom({ name: "silent" });
|
||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||
await bot.joinRoom(roomId);
|
||||
|
||||
await app.viewRoomById(roomId);
|
||||
await app.settings.openRoomSettings("Notifications");
|
||||
await page.getByText("Off").click();
|
||||
await app.settings.closeDialog();
|
||||
|
||||
const room = roomListView.getByRole("gridcell", { name: "silent" });
|
||||
await expect(room.getByTestId("notification-decoration")).toBeVisible();
|
||||
await expect(room).toMatchScreenshot("room-list-item-silent.png");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -73,4 +73,33 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
await revokeAccessTokenPromise;
|
||||
await revokeRefreshTokenPromise;
|
||||
});
|
||||
|
||||
test(
|
||||
"it should log out the user & wipe data when logging out via MAS",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ mas, page, mailpitClient }, testInfo) => {
|
||||
// We use this over the `user` fixture to ensure we get an OIDC session rather than a compatibility one
|
||||
await page.goto("/#/login");
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
const userId = `alice_${testInfo.testId}`;
|
||||
await registerAccountMas(page, mailpitClient, userId, "alice@email.com", "Pa$sW0rD!");
|
||||
|
||||
await expect(page.getByText("Welcome")).toBeVisible();
|
||||
await page.goto("about:blank");
|
||||
|
||||
// @ts-expect-error
|
||||
const result = await mas.manage("kill-sessions", userId);
|
||||
expect(result.output).toContain("Ended 1 active OAuth 2.0 session");
|
||||
|
||||
await page.goto("http://localhost:8080");
|
||||
await expect(
|
||||
page.getByText("For security, this session has been signed out. Please sign in again."),
|
||||
).toBeVisible();
|
||||
await expect(page).toMatchScreenshot("token-expired.png", { includeDialogBackground: true });
|
||||
|
||||
const localStorageKeys = await page.evaluate(() => Object.keys(localStorage));
|
||||
expect(localStorageKeys).toHaveLength(0);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024, 2025 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
@@ -10,6 +10,7 @@ import { type Locator, type Page } from "@playwright/test";
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { checkRoomSummaryCard, viewRoomSummaryByName } from "./utils";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
const ROOM_NAME = "Test room";
|
||||
const ROOM_NAME_LONG =
|
||||
@@ -133,6 +134,17 @@ test.describe("RightPanel", () => {
|
||||
await page.getByLabel("Room info").nth(1).click();
|
||||
await checkRoomSummaryCard(page, ROOM_NAME);
|
||||
});
|
||||
test.describe("room reporting", () => {
|
||||
test.skip(isDendrite, "Dendrite does not implement room reporting");
|
||||
test("should handle reporting a room", async ({ page, app }) => {
|
||||
await viewRoomSummaryByName(page, app, ROOM_NAME);
|
||||
await page.getByRole("menuitem", { name: "Report room" }).click();
|
||||
const dialog = await page.getByRole("dialog", { name: "Report Room" });
|
||||
await dialog.getByLabel("reason").fill("This room should be reported");
|
||||
await dialog.getByRole("button", { name: "Send report" }).click();
|
||||
await expect(page.getByText("Your report was sent.")).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("in spaces", () => {
|
||||
|
||||
@@ -28,7 +28,10 @@ test.describe("Preferences user settings tab", () => {
|
||||
const tab = await app.settings.openUserSettings("Preferences");
|
||||
// Assert that the top heading is rendered
|
||||
await expect(tab.getByRole("heading", { name: "Preferences" })).toBeVisible();
|
||||
await expect(tab).toMatchScreenshot("Preferences-user-settings-tab-should-be-rendered-properly-1.png");
|
||||
await expect(tab).toMatchScreenshot("Preferences-user-settings-tab-should-be-rendered-properly-1.png", {
|
||||
// masked due to daylight saving time
|
||||
mask: [tab.locator("#mx_dropdownUserTimezone_value")],
|
||||
});
|
||||
});
|
||||
|
||||
test("should be able to change the app language", { tag: ["@no-firefox", "@no-webkit"] }, async ({ uut, user }) => {
|
||||
|
||||
@@ -28,6 +28,8 @@ const NEW_AVATAR = fs.readFileSync("playwright/sample-files/element.png");
|
||||
const OLD_NAME = "Alan";
|
||||
const NEW_NAME = "Alan (away)";
|
||||
|
||||
const VIDEO_FILE = fs.readFileSync("playwright/sample-files/5secvid.webm");
|
||||
|
||||
const getEventTilesWithBodies = (page: Page): Locator => {
|
||||
return page.locator(".mx_EventTile").filter({ has: page.locator(".mx_EventTile_body") });
|
||||
};
|
||||
@@ -916,7 +918,27 @@ test.describe("Timeline", () => {
|
||||
await page.getByRole("button", { name: "Hide" }).click();
|
||||
|
||||
// Check that the image is now hidden.
|
||||
await expect(page.getByRole("link", { name: "Show image" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Show image" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("should be able to hide a video", async ({ page, app, room, context }) => {
|
||||
await app.viewRoomById(room.roomId);
|
||||
const upload = await app.client.uploadContent(VIDEO_FILE, { name: "bbb.webm", type: "video/webm" });
|
||||
await app.client.sendEvent(room.roomId, null, "m.room.message" as EventType, {
|
||||
msgtype: "m.video" as MsgType,
|
||||
body: "bbb.webm",
|
||||
url: upload.content_uri,
|
||||
});
|
||||
|
||||
await app.timeline.scrollToBottom();
|
||||
const imgTile = page.locator(".mx_MVideoBody").first();
|
||||
await expect(imgTile).toBeVisible();
|
||||
await imgTile.hover();
|
||||
await page.getByRole("button", { name: "Hide" }).click();
|
||||
|
||||
// Check that the video is now hidden.
|
||||
await expect(page.getByRole("button", { name: "Show video" })).toBeVisible();
|
||||
await expect(page.locator("video")).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1319,4 +1341,44 @@ test.describe("Timeline", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("spoilers", { tag: "@screenshot" }, () => {
|
||||
test("clicking a spoiler containing the pill de-spoilers on 1st click, then follows link on 2nd", async ({
|
||||
page,
|
||||
user,
|
||||
app,
|
||||
room,
|
||||
}) => {
|
||||
// View room
|
||||
await page.goto(`/#/room/${room.roomId}`);
|
||||
|
||||
// Send a spoilered pill
|
||||
await app.client.sendMessage(room.roomId, {
|
||||
msgtype: "m.text",
|
||||
body: user.userId,
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: `<span data-mx-spoiler>https://matrix.to/#/${user.userId}</span>`,
|
||||
});
|
||||
|
||||
const screenshotOptions = {
|
||||
css: `
|
||||
.mx_MessageTimestamp {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
const eventTile = page.locator(".mx_RoomView_body .mx_EventTile_last");
|
||||
await expect(eventTile).toMatchScreenshot("spoiler.png", screenshotOptions);
|
||||
|
||||
const rightPanelButton = page.getByText("Share profile");
|
||||
const pill = page.locator(".mx_UserPill");
|
||||
await pill.click({ force: true }); // force to click the spoiler wrapper instead
|
||||
await expect(eventTile).toMatchScreenshot("spoiler-uncovered.png", screenshotOptions);
|
||||
await expect(rightPanelButton).not.toBeVisible(); // assert the right panel is not yet open
|
||||
|
||||
await pill.click();
|
||||
await expect(rightPanelButton).toBeVisible(); // assert the right panel is open
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -114,7 +114,7 @@ export class ElementAppPage {
|
||||
* @param isRightPanel whether to select the right panel composer, otherwise the main timeline composer
|
||||
*/
|
||||
public getComposerField(isRightPanel?: boolean): Locator {
|
||||
return this.getComposer(isRightPanel).locator("[contenteditable]");
|
||||
return this.getComposer(isRightPanel).locator("div[contenteditable]");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
BIN
playwright/sample-files/5secvid.webm
Normal file
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 957 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 249 KiB After Width: | Height: | Size: 247 KiB |
BIN
playwright/snapshots/timeline/timeline.spec.ts/spoiler-linux.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import { SynapseContainer as BaseSynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers";
|
||||
|
||||
const TAG = "develop@sha256:4285f51332a658ba6d4871b04d33f49261e6118e751d70fd2894aca97bd587c3";
|
||||
const TAG = "develop@sha256:66955f34a593cfc3b6e77b8d5510c60c6094f5bade8a17d2feaefbb8662ccf09";
|
||||
|
||||
/**
|
||||
* SynapseContainer which freezes the docker digest to stabilise tests,
|
||||
|
||||
@@ -152,6 +152,7 @@
|
||||
@import "./views/dialogs/_ModalWidgetDialog.pcss";
|
||||
@import "./views/dialogs/_PollCreateDialog.pcss";
|
||||
@import "./views/dialogs/_RegistrationEmailPromptDialog.pcss";
|
||||
@import "./views/dialogs/_ReportRoomDialog.pcss";
|
||||
@import "./views/dialogs/_RoomSettingsDialog.pcss";
|
||||
@import "./views/dialogs/_RoomSettingsDialogBridges.pcss";
|
||||
@import "./views/dialogs/_RoomUpgradeDialog.pcss";
|
||||
@@ -226,6 +227,7 @@
|
||||
@import "./views/messages/_DisambiguatedProfile.pcss";
|
||||
@import "./views/messages/_EventTileBubble.pcss";
|
||||
@import "./views/messages/_HiddenBody.pcss";
|
||||
@import "./views/messages/_HiddenMediaPlaceholder.pcss";
|
||||
@import "./views/messages/_JumpToDatePicker.pcss";
|
||||
@import "./views/messages/_LegacyCallEvent.pcss";
|
||||
@import "./views/messages/_MEmoteBody.pcss";
|
||||
|
||||
16
res/css/views/dialogs/_ReportRoomDialog.pcss
Normal file
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.mx_ReportRoomDialog {
|
||||
textarea {
|
||||
font: var(--cpd-font-body-md-regular);
|
||||
border: 1px solid var(--cpd-color-border-interactive-primary);
|
||||
background: var(--cpd-color-bg-canvas-default);
|
||||
border-radius: 0.5rem;
|
||||
padding: var(--cpd-space-3x) var(--cpd-space-4x);
|
||||
}
|
||||
}
|
||||
29
res/css/views/messages/_HiddenMediaPlaceholder.pcss
Normal file
@@ -0,0 +1,29 @@
|
||||
.mx_HiddenMediaPlaceholder {
|
||||
border: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
inset: 0;
|
||||
|
||||
/* To center the text in the middle of the frame */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
|
||||
cursor: pointer;
|
||||
background-color: $header-panel-bg-color;
|
||||
|
||||
> div {
|
||||
color: $accent;
|
||||
/* Icon alignment */
|
||||
display: flex;
|
||||
> svg {
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EventTile:hover .mx_HiddenMediaPlaceholder {
|
||||
background-color: $background;
|
||||
}
|
||||
@@ -79,39 +79,3 @@ Please see LICENSE files in the repository root for full details.
|
||||
color: $imagebody-giflabel-color;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.mx_HiddenImagePlaceholder {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
||||
/* To center the text in the middle of the frame */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
|
||||
cursor: pointer;
|
||||
background-color: $header-panel-bg-color;
|
||||
|
||||
.mx_HiddenImagePlaceholder_button {
|
||||
color: $accent;
|
||||
|
||||
span.mx_HiddenImagePlaceholder_eye {
|
||||
margin-right: 8px;
|
||||
|
||||
background-color: $accent;
|
||||
mask-image: url("$(res)/img/element-icons/eye.svg");
|
||||
display: inline-block;
|
||||
width: 18px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
span:not(.mx_HiddenImagePlaceholder_eye) {
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EventTile:hover .mx_HiddenImagePlaceholder {
|
||||
background-color: $background;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024, 2025 New Vector Ltd.
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
@@ -101,6 +101,6 @@ Please see LICENSE files in the repository root for full details.
|
||||
margin: $spacing-12 0 $spacing-4;
|
||||
}
|
||||
|
||||
.mx_RoomSummaryCard_leave {
|
||||
.mx_RoomSummaryCard_bottomOptions {
|
||||
margin: 0 0 var(--cpd-space-8x);
|
||||
}
|
||||
|
||||
@@ -16,18 +16,22 @@
|
||||
*/
|
||||
.mx_RoomListItemView {
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--cpd-color-bg-action-secondary-hovered);
|
||||
|
||||
.mx_RoomListItemView_content {
|
||||
padding-right: var(--cpd-space-1-5x);
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomListItemView_container {
|
||||
padding-left: var(--cpd-space-3x);
|
||||
padding-left: var(--cpd-space-2x);
|
||||
font: var(--cpd-font-body-md-regular);
|
||||
height: 100%;
|
||||
|
||||
.mx_RoomListItemView_content {
|
||||
padding-right: var(--cpd-space-3x);
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
/* The border is only under the room name and the future hover menu */
|
||||
@@ -46,8 +50,24 @@
|
||||
|
||||
.mx_RoomListItemView_menu_open {
|
||||
background-color: var(--cpd-color-bg-action-secondary-hovered);
|
||||
|
||||
.mx_RoomListItemView_content {
|
||||
padding-right: var(--cpd-space-1-5x);
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomListItemView_selected {
|
||||
background-color: var(--cpd-color-bg-action-secondary-pressed);
|
||||
}
|
||||
|
||||
.mx_RoomListItemView_notification_decoration {
|
||||
.mx_RoomListItemView_content {
|
||||
padding-right: var(--cpd-space-2x);
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomListItemView_empty {
|
||||
.mx_RoomListItemView_content {
|
||||
padding-right: var(--cpd-space-3x);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -816,11 +816,13 @@ $left-gutter: 64px;
|
||||
.mx_EventTile_spoiler_content {
|
||||
filter: blur(5px) saturate(0.1) sepia(1);
|
||||
transition-duration: 0.5s;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.visible > .mx_EventTile_spoiler_content {
|
||||
filter: none;
|
||||
user-select: auto;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,10 +45,7 @@ getPRInfo() {
|
||||
|
||||
# Some CIs don't give us enough info, so we just get the PR number and ask the
|
||||
# GH API for more info - "fork:branch". Some give us this directly.
|
||||
if [ -n "$BUILDKITE_BRANCH" ]; then
|
||||
# BuildKite
|
||||
head=$BUILDKITE_BRANCH
|
||||
elif [ -n "$PR_NUMBER" ]; then
|
||||
if [ -n "$PR_NUMBER" ]; then
|
||||
# GitHub
|
||||
getPRInfo $PR_NUMBER
|
||||
elif [ -n "$REVIEW_ID" ]; then
|
||||
@@ -79,11 +76,14 @@ if [[ "$GITHUB_EVENT_NAME" == "merge_group" ]]; then
|
||||
clone $deforg $defrepo ${withoutPrefix%%/pr-*}
|
||||
fi
|
||||
|
||||
# Try the target branch of the push or PR.
|
||||
if [ -n "$GITHUB_BASE_REF" ]; then
|
||||
clone $deforg $defrepo $GITHUB_BASE_REF
|
||||
elif [ -n "$BUILDKITE_PULL_REQUEST_BASE_BRANCH" ]; then
|
||||
clone $deforg $defrepo $BUILDKITE_PULL_REQUEST_BASE_BRANCH
|
||||
# Try the target branch of the push or PR, or the branch that was pushed to
|
||||
# (ie. the 'master' branch should use matching 'master' dependencies)
|
||||
base_or_branch=$GITHUB_BASE_REF
|
||||
if [[ "$GITHUB_EVENT_NAME" == "push" ]]; then
|
||||
base_or_branch=${GITHUB_REF}
|
||||
fi
|
||||
if [ -n "$base_or_branch" ]; then
|
||||
clone $deforg $defrepo $base_or_branch
|
||||
fi
|
||||
|
||||
# Try HEAD which is the branch name in Netlify (not BRANCH which is pull/xxxx/head for PR builds)
|
||||
|
||||
@@ -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 { type JSXElementConstructor } from "react";
|
||||
import { type JSX, type JSXElementConstructor } from "react";
|
||||
|
||||
export type { NonEmptyArray, XOR, Writeable } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
type SSOAction,
|
||||
encodeUnpaddedBase64,
|
||||
type OidcRegistrationClientMetadata,
|
||||
MatrixEventEvent,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
@@ -228,6 +229,16 @@ export default abstract class BasePlatform {
|
||||
window.focus();
|
||||
};
|
||||
|
||||
const closeHandler = (): void => notification.close();
|
||||
|
||||
// Clear a notification from a redacted event.
|
||||
if (ev) {
|
||||
ev.once(MatrixEventEvent.BeforeRedaction, closeHandler);
|
||||
notification.onclose = () => {
|
||||
ev.off(MatrixEventEvent.BeforeRedaction, closeHandler);
|
||||
};
|
||||
}
|
||||
|
||||
return notification;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,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 React, { type LegacyRef, type ReactNode } from "react";
|
||||
import React, { type JSX, type LegacyRef, type ReactNode } from "react";
|
||||
import sanitizeHtml, { type IOptions } from "sanitize-html";
|
||||
import classNames from "classnames";
|
||||
import katex from "katex";
|
||||
@@ -25,7 +25,7 @@ import { PERMITTED_URL_SCHEMES } from "./utils/UrlUtils";
|
||||
import { sanitizeHtmlParams, transformTags } from "./Linkify";
|
||||
import { graphemeSegmenter } from "./utils/strings";
|
||||
|
||||
export { Linkify, linkifyElement, linkifyAndSanitizeHtml } from "./Linkify";
|
||||
export { Linkify, linkifyAndSanitizeHtml } from "./Linkify";
|
||||
|
||||
// Anything outside the basic multilingual plane will be a surrogate pair
|
||||
const SURROGATE_PAIR_PATTERN = /([\ud800-\udbff])([\udc00-\udfff])/;
|
||||
@@ -365,53 +365,6 @@ function analyseEvent(content: IContent, highlights: Optional<string[]>, opts: E
|
||||
}
|
||||
}
|
||||
|
||||
export function bodyToDiv(
|
||||
content: IContent,
|
||||
highlights: Optional<string[]>,
|
||||
opts: EventRenderOpts = {},
|
||||
ref?: React.Ref<HTMLDivElement>,
|
||||
): ReactNode {
|
||||
const { strippedBody, formattedBody, emojiBodyElements, className } = bodyToNode(content, highlights, opts);
|
||||
|
||||
return formattedBody ? (
|
||||
<div
|
||||
key="body"
|
||||
ref={ref}
|
||||
className={className}
|
||||
dangerouslySetInnerHTML={{ __html: formattedBody }}
|
||||
dir="auto"
|
||||
/>
|
||||
) : (
|
||||
<div key="body" ref={ref} className={className} dir="auto">
|
||||
{emojiBodyElements || strippedBody}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function bodyToSpan(
|
||||
content: IContent,
|
||||
highlights: Optional<string[]>,
|
||||
opts: EventRenderOpts = {},
|
||||
ref?: React.Ref<HTMLSpanElement>,
|
||||
includeDir = true,
|
||||
): ReactNode {
|
||||
const { strippedBody, formattedBody, emojiBodyElements, className } = bodyToNode(content, highlights, opts);
|
||||
|
||||
return formattedBody ? (
|
||||
<span
|
||||
key="body"
|
||||
ref={ref}
|
||||
className={className}
|
||||
dangerouslySetInnerHTML={{ __html: formattedBody }}
|
||||
dir={includeDir ? "auto" : undefined}
|
||||
/>
|
||||
) : (
|
||||
<span key="body" ref={ref} className={className} dir={includeDir ? "auto" : undefined}>
|
||||
{emojiBodyElements || strippedBody}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
interface BodyToNodeReturn {
|
||||
strippedBody: string;
|
||||
formattedBody?: string;
|
||||
@@ -419,7 +372,11 @@ interface BodyToNodeReturn {
|
||||
className: string;
|
||||
}
|
||||
|
||||
function bodyToNode(content: IContent, highlights: Optional<string[]>, opts: EventRenderOpts = {}): BodyToNodeReturn {
|
||||
export function bodyToNode(
|
||||
content: IContent,
|
||||
highlights: Optional<string[]>,
|
||||
opts: EventRenderOpts = {},
|
||||
): BodyToNodeReturn {
|
||||
const eventInfo = analyseEvent(content, highlights, opts);
|
||||
|
||||
let emojiBody = false;
|
||||
|
||||
@@ -117,7 +117,6 @@ export interface IConfigOptions {
|
||||
obey_asserted_identity?: boolean; // MSC3086
|
||||
};
|
||||
element_call: {
|
||||
url?: string;
|
||||
guest_spa_url?: string;
|
||||
use_exclusively?: boolean;
|
||||
participant_limit?: number;
|
||||
|
||||
@@ -149,6 +149,7 @@ interface ILoadSessionOpts {
|
||||
ignoreGuest?: boolean;
|
||||
defaultDeviceDisplayName?: string;
|
||||
fragmentQueryParams?: QueryDict;
|
||||
abortSignal?: AbortSignal;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -196,7 +197,7 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
|
||||
|
||||
if (enableGuest && guestHsUrl && fragmentQueryParams.guest_user_id && fragmentQueryParams.guest_access_token) {
|
||||
logger.log("Using guest access credentials");
|
||||
return doSetLoggedIn(
|
||||
await doSetLoggedIn(
|
||||
{
|
||||
userId: fragmentQueryParams.guest_user_id as string,
|
||||
accessToken: fragmentQueryParams.guest_access_token as string,
|
||||
@@ -206,7 +207,8 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
|
||||
},
|
||||
true,
|
||||
false,
|
||||
).then(() => true);
|
||||
);
|
||||
return true;
|
||||
}
|
||||
const success = await restoreSessionFromStorage({
|
||||
ignoreGuest: Boolean(opts.ignoreGuest),
|
||||
@@ -225,6 +227,11 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
|
||||
// fall back to welcome screen
|
||||
return false;
|
||||
} catch (e) {
|
||||
// We may be aborted e.g. because our token expired, so don't show an error here
|
||||
if (opts.abortSignal?.aborted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (e instanceof AbortLoginAndRebuildStorage) {
|
||||
// If we're aborting login because of a storage inconsistency, we don't
|
||||
// need to show the general failure dialog. Instead, just go back to welcome.
|
||||
@@ -236,7 +243,7 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
|
||||
return false;
|
||||
}
|
||||
|
||||
return handleLoadSessionFailure(e);
|
||||
return handleLoadSessionFailure(e, opts);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,6 +413,39 @@ export function attemptTokenLogin(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the pickle key inside the credentials or create it if it does not exist for this device.
|
||||
*
|
||||
* @param credentials Holds the device to load/store the pickle key
|
||||
*
|
||||
* @returns {Promise} promise which resolves to the loaded or generated pickle key or undefined if
|
||||
* none was loaded nor generated
|
||||
*/
|
||||
async function loadOrCreatePickleKey(credentials: IMatrixClientCreds): Promise<string | undefined> {
|
||||
// Try to load the pickle key
|
||||
const userId = credentials.userId;
|
||||
const deviceId = credentials.deviceId;
|
||||
let pickleKey = (await PlatformPeg.get()?.getPickleKey(userId, deviceId ?? "")) ?? undefined;
|
||||
if (!pickleKey) {
|
||||
// Create it if it did not exist
|
||||
pickleKey =
|
||||
userId && deviceId
|
||||
? ((await PlatformPeg.get()?.createPickleKey(userId, deviceId)) ?? undefined)
|
||||
: undefined;
|
||||
if (pickleKey) {
|
||||
logger.log(`Created pickle key for ${credentials.userId}|${credentials.deviceId}`);
|
||||
} else {
|
||||
logger.log("Pickle key not created");
|
||||
}
|
||||
} else {
|
||||
logger.log(
|
||||
`Pickle key already exists for ${credentials.userId}|${credentials.deviceId} do not create a new one`,
|
||||
);
|
||||
}
|
||||
|
||||
return pickleKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after a successful token login or OIDC authorization.
|
||||
* Clear storage then save new credentials in storage
|
||||
@@ -413,6 +453,8 @@ export function attemptTokenLogin(
|
||||
*/
|
||||
async function onSuccessfulDelegatedAuthLogin(credentials: IMatrixClientCreds): Promise<void> {
|
||||
await clearStorage();
|
||||
// SSO does not go through setLoggedIn so we need to load/create the pickle key here too
|
||||
credentials.pickleKey = await loadOrCreatePickleKey(credentials);
|
||||
await persistCredentials(credentials);
|
||||
|
||||
// remember that we just logged in
|
||||
@@ -621,7 +663,7 @@ export async function restoreSessionFromStorage(opts?: { ignoreGuest?: boolean }
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLoadSessionFailure(e: unknown): Promise<boolean> {
|
||||
async function handleLoadSessionFailure(e: unknown, loadSessionOpts?: ILoadSessionOpts): Promise<boolean> {
|
||||
logger.error("Unable to load session", e);
|
||||
|
||||
const modal = Modal.createDialog(SessionRestoreErrorDialog, {
|
||||
@@ -636,7 +678,7 @@ async function handleLoadSessionFailure(e: unknown): Promise<boolean> {
|
||||
}
|
||||
|
||||
// try, try again
|
||||
return loadSession();
|
||||
return loadSession(loadSessionOpts);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -655,18 +697,8 @@ async function handleLoadSessionFailure(e: unknown): Promise<boolean> {
|
||||
export async function setLoggedIn(credentials: IMatrixClientCreds): Promise<MatrixClient> {
|
||||
credentials.freshLogin = true;
|
||||
stopMatrixClient();
|
||||
const pickleKey =
|
||||
credentials.userId && credentials.deviceId
|
||||
? await PlatformPeg.get()?.createPickleKey(credentials.userId, credentials.deviceId)
|
||||
: null;
|
||||
|
||||
if (pickleKey) {
|
||||
logger.log(`Created pickle key for ${credentials.userId}|${credentials.deviceId}`);
|
||||
} else {
|
||||
logger.log("Pickle key not created");
|
||||
}
|
||||
|
||||
return doSetLoggedIn({ ...credentials, pickleKey: pickleKey ?? undefined }, true, true);
|
||||
credentials.pickleKey = await loadOrCreatePickleKey(credentials);
|
||||
return doSetLoggedIn(credentials, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1124,12 +1156,13 @@ window.mxLoginWithAccessToken = async (hsUrl: string, accessToken: string): Prom
|
||||
baseUrl: hsUrl,
|
||||
accessToken,
|
||||
});
|
||||
const { user_id: userId } = await tempClient.whoami();
|
||||
const { user_id: userId, device_id: deviceId } = await tempClient.whoami();
|
||||
await doSetLoggedIn(
|
||||
{
|
||||
homeserverUrl: hsUrl,
|
||||
accessToken,
|
||||
userId,
|
||||
deviceId,
|
||||
},
|
||||
true,
|
||||
false,
|
||||
|
||||
@@ -11,12 +11,7 @@ import sanitizeHtml, { type IOptions } from "sanitize-html";
|
||||
import { merge } from "lodash";
|
||||
import _Linkify from "linkify-react";
|
||||
|
||||
import {
|
||||
_linkifyElement,
|
||||
_linkifyString,
|
||||
ELEMENT_URL_PATTERN,
|
||||
options as linkifyMatrixOptions,
|
||||
} from "./linkify-matrix";
|
||||
import { _linkifyString, ELEMENT_URL_PATTERN, options as linkifyMatrixOptions } from "./linkify-matrix";
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks";
|
||||
import { mediaFromMxc } from "./customisations/Media";
|
||||
@@ -223,17 +218,6 @@ export function linkifyString(str: string, options = linkifyMatrixOptions): stri
|
||||
return _linkifyString(str, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Linkifies the given DOM element. This is a wrapper around 'linkifyjs/element'.
|
||||
*
|
||||
* @param {object} element DOM element to linkify
|
||||
* @param {object} [options] Options for linkifyElement. Default: linkifyMatrixOptions
|
||||
* @returns {object}
|
||||
*/
|
||||
export function linkifyElement(element: HTMLElement, options = linkifyMatrixOptions): HTMLElement {
|
||||
return _linkifyElement(element, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Linkify the given string and sanitize the HTML afterwards.
|
||||
*
|
||||
|
||||
@@ -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 React, { type Key, type MutableRefObject, type ReactElement, type RefCallback } from "react";
|
||||
import React, { type Key, type RefObject, type ReactElement, type RefCallback } from "react";
|
||||
|
||||
interface IChildProps {
|
||||
style: React.CSSProperties;
|
||||
@@ -20,7 +20,7 @@ interface IProps {
|
||||
// a list of state objects to apply to each child node in turn
|
||||
startStyles: React.CSSProperties[];
|
||||
|
||||
innerRef?: MutableRefObject<any>;
|
||||
innerRef?: RefObject<any>;
|
||||
}
|
||||
|
||||
function isReactElement(c: ReturnType<(typeof React.Children)["toArray"]>[number]): c is ReactElement {
|
||||
|
||||
@@ -238,25 +238,25 @@ export function determineUnreadState(
|
||||
room?: Room,
|
||||
threadId?: string,
|
||||
includeThreads?: boolean,
|
||||
): { level: NotificationLevel; symbol: string | null; count: number } {
|
||||
): { level: NotificationLevel; symbol: string | null; count: number; invited: boolean } {
|
||||
if (!room) {
|
||||
return { symbol: null, count: 0, level: NotificationLevel.None };
|
||||
return { symbol: null, count: 0, level: NotificationLevel.None, invited: false };
|
||||
}
|
||||
|
||||
if (getUnsentMessages(room, threadId).length > 0) {
|
||||
return { symbol: "!", count: 1, level: NotificationLevel.Unsent };
|
||||
return { symbol: "!", count: 1, level: NotificationLevel.Unsent, invited: false };
|
||||
}
|
||||
|
||||
if (getEffectiveMembership(room.getMyMembership()) === EffectiveMembership.Invite) {
|
||||
return { symbol: "!", count: 1, level: NotificationLevel.Highlight };
|
||||
return { symbol: "!", count: 1, level: NotificationLevel.Highlight, invited: true };
|
||||
}
|
||||
|
||||
if (SettingsStore.getValue("feature_ask_to_join") && isKnockDenied(room)) {
|
||||
return { symbol: "!", count: 1, level: NotificationLevel.Highlight };
|
||||
return { symbol: "!", count: 1, level: NotificationLevel.Highlight, invited: false };
|
||||
}
|
||||
|
||||
if (getRoomNotifsState(room.client, room.roomId) === RoomNotifState.Mute) {
|
||||
return { symbol: null, count: 0, level: NotificationLevel.None };
|
||||
return { symbol: null, count: 0, level: NotificationLevel.None, invited: false };
|
||||
}
|
||||
|
||||
const redNotifs = getUnreadNotificationCount(
|
||||
@@ -269,12 +269,12 @@ export function determineUnreadState(
|
||||
|
||||
const trueCount = greyNotifs || redNotifs;
|
||||
if (redNotifs > 0) {
|
||||
return { symbol: null, count: trueCount, level: NotificationLevel.Highlight };
|
||||
return { symbol: null, count: trueCount, level: NotificationLevel.Highlight, invited: false };
|
||||
}
|
||||
|
||||
const markedUnreadState = getMarkedUnreadState(room);
|
||||
if (greyNotifs > 0 || markedUnreadState) {
|
||||
return { symbol: null, count: trueCount, level: NotificationLevel.Notification };
|
||||
return { symbol: null, count: trueCount, level: NotificationLevel.Notification, invited: false };
|
||||
}
|
||||
|
||||
// We don't have any notified messages, but we might have unread messages. Let's find out.
|
||||
@@ -293,5 +293,6 @@ export function determineUnreadState(
|
||||
symbol: null,
|
||||
count: trueCount,
|
||||
level: hasUnread ? NotificationLevel.Activity : NotificationLevel.None,
|
||||
invited: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ export const DEFAULTS: DeepReadonly<IConfigOptions> = {
|
||||
preferred_domain: "meet.element.io",
|
||||
},
|
||||
element_call: {
|
||||
url: "https://call.element.io",
|
||||
use_exclusively: false,
|
||||
participant_limit: 8,
|
||||
brand: "Element Call",
|
||||
|
||||
@@ -354,7 +354,7 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({
|
||||
* nodeRef = inputRef when inputRef argument is provided.
|
||||
*/
|
||||
export const useRovingTabIndex = <T extends HTMLElement>(
|
||||
inputRef?: RefObject<T>,
|
||||
inputRef?: RefObject<T | null>,
|
||||
): [FocusHandler, boolean, RefCallback<T>, RefObject<T | null>] => {
|
||||
const context = useContext(RovingTabIndexContext);
|
||||
|
||||
|
||||
@@ -6,13 +6,13 @@ 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 React, { type RefObject } from "react";
|
||||
import React, { type JSX, type RefObject } from "react";
|
||||
|
||||
import AccessibleButton, { type ButtonProps } from "../../components/views/elements/AccessibleButton";
|
||||
import { useRovingTabIndex } from "../RovingTabIndex";
|
||||
|
||||
type Props<T extends keyof HTMLElementTagNameMap> = Omit<ButtonProps<T>, "tabIndex"> & {
|
||||
inputRef?: RefObject<HTMLElementTagNameMap[T]>;
|
||||
inputRef?: RefObject<HTMLElementTagNameMap[T] | null>;
|
||||
focusOnMouseOver?: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -6,14 +6,14 @@ 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 { type ReactElement, type RefCallback } from "react";
|
||||
import { type ReactElement, type RefCallback, type RefObject } from "react";
|
||||
|
||||
import type React from "react";
|
||||
import { useRovingTabIndex } from "../RovingTabIndex";
|
||||
import { type FocusHandler, type Ref } from "./types";
|
||||
import { type FocusHandler } from "./types";
|
||||
|
||||
interface IProps {
|
||||
inputRef?: Ref;
|
||||
inputRef?: RefObject<HTMLElement | null>;
|
||||
children(renderProps: {
|
||||
onFocus: FocusHandler;
|
||||
isActive: boolean;
|
||||
|
||||
@@ -6,8 +6,4 @@ 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 { type RefObject } from "react";
|
||||
|
||||
export type Ref = RefObject<HTMLElement>;
|
||||
|
||||
export type FocusHandler = () => void;
|
||||
|
||||
@@ -5,7 +5,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 React, { type ReactNode } from "react";
|
||||
import React, { type JSX, type ReactNode } from "react";
|
||||
import { Text, Heading, Button, Separator } from "@vector-im/compound-web";
|
||||
import PopOutIcon from "@vector-im/compound-design-tokens/assets/web/icons/pop-out";
|
||||
|
||||
|
||||
@@ -7,7 +7,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 React from "react";
|
||||
import React, { type JSX } from "react";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||
|
||||
@@ -7,7 +7,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 React, { createRef } from "react";
|
||||
import React, { type JSX, createRef } from "react";
|
||||
import FileSaver from "file-saver";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { type AuthDict } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
@@ -178,7 +178,9 @@ export default class ExportE2eKeysDialog extends React.Component<IProps, IState>
|
||||
type="password"
|
||||
disabled={disableForm}
|
||||
autoComplete="new-password"
|
||||
fieldRef={(field) => (this.fieldPassword = field)}
|
||||
fieldRef={(field) => {
|
||||
this.fieldPassword = field;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_E2eKeysDialog_inputRow">
|
||||
@@ -195,7 +197,9 @@ export default class ExportE2eKeysDialog extends React.Component<IProps, IState>
|
||||
type="password"
|
||||
disabled={disableForm}
|
||||
autoComplete="new-password"
|
||||
fieldRef={(field) => (this.fieldPasswordConfirm = field)}
|
||||
fieldRef={(field) => {
|
||||
this.fieldPasswordConfirm = field;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import classNames from "classnames";
|
||||
import React, { type HTMLAttributes, type ReactHTML, type ReactNode, type WheelEvent } from "react";
|
||||
import React, { type HTMLAttributes, type JSX, type ReactNode, type WheelEvent } from "react";
|
||||
|
||||
type DynamicHtmlElementProps<T extends keyof JSX.IntrinsicElements> =
|
||||
JSX.IntrinsicElements[T] extends HTMLAttributes<object> ? DynamicElementProps<T> : DynamicElementProps<"div">;
|
||||
@@ -27,10 +27,10 @@ export type IProps<T extends keyof JSX.IntrinsicElements> = Omit<DynamicHtmlElem
|
||||
|
||||
export default class AutoHideScrollbar<T extends keyof JSX.IntrinsicElements> extends React.Component<IProps<T>> {
|
||||
public static defaultProps = {
|
||||
element: "div" as keyof ReactHTML,
|
||||
element: "div" as keyof HTMLElementTagNameMap,
|
||||
};
|
||||
|
||||
public readonly containerRef: React.RefObject<HTMLDivElement> = React.createRef();
|
||||
public readonly containerRef = React.createRef<HTMLDivElement>();
|
||||
|
||||
public componentDidMount(): void {
|
||||
if (this.containerRef.current && this.props.onScroll) {
|
||||
|
||||
@@ -8,7 +8,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 React, { type CSSProperties, type RefObject, type SyntheticEvent, useRef, useState } from "react";
|
||||
import React, { type JSX, type CSSProperties, type RefObject, type SyntheticEvent, useRef, useState } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import classNames from "classnames";
|
||||
import FocusLock from "react-focus-lock";
|
||||
@@ -440,7 +440,7 @@ export default class ContextMenu extends React.PureComponent<React.PropsWithChil
|
||||
);
|
||||
}
|
||||
|
||||
public render(): React.ReactChild {
|
||||
public render(): JSX.Element {
|
||||
if (this.props.mountAsChild) {
|
||||
// Render as a child of the current parent
|
||||
return this.renderMenu();
|
||||
@@ -582,13 +582,15 @@ export const alwaysAboveRightOf = (
|
||||
|
||||
type ContextMenuTuple<T> = [
|
||||
boolean,
|
||||
RefObject<T>,
|
||||
RefObject<T | null>,
|
||||
(ev?: SyntheticEvent) => void,
|
||||
(ev?: SyntheticEvent) => void,
|
||||
(val: boolean) => void,
|
||||
];
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint
|
||||
export const useContextMenu = <T extends any = HTMLElement>(inputRef?: RefObject<T>): ContextMenuTuple<T> => {
|
||||
export const useContextMenu = <T extends HTMLElement = HTMLElement>(
|
||||
inputRef?: RefObject<T | null>,
|
||||
): ContextMenuTuple<T> => {
|
||||
let button = useRef<T>(null);
|
||||
if (inputRef) {
|
||||
// if we are given a ref, use it instead of ours
|
||||
|
||||
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import classNames from "classnames";
|
||||
import React, { type FunctionComponent, type Key, type PropsWithChildren, type ReactNode } from "react";
|
||||
import React, { type JSX, type FunctionComponent, type Key, type PropsWithChildren, type ReactNode } from "react";
|
||||
|
||||
import { MenuItemRadio } from "../../accessibility/context_menu/MenuItemRadio";
|
||||
import { type ButtonEvent } from "../views/elements/AccessibleButton";
|
||||
|
||||
@@ -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 * as React from "react";
|
||||
import React, { type JSX } from "react";
|
||||
import { useContext, useState } from "react";
|
||||
|
||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||
|
||||
@@ -5,7 +5,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 React, { createRef } from "react";
|
||||
import React, { createRef, type JSX } from "react";
|
||||
|
||||
import AutoHideScrollbar, { type IProps as AutoHideScrollbarProps } from "./AutoHideScrollbar";
|
||||
import UIStore, { UI_EVENTS } from "../../stores/UIStore";
|
||||
|
||||
@@ -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 * as React from "react";
|
||||
import React, { type JSX } from "react";
|
||||
import { createRef } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
|
||||
@@ -124,9 +124,9 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||
public static displayName = "LoggedInView";
|
||||
|
||||
protected readonly _matrixClient: MatrixClient;
|
||||
protected readonly _roomView: React.RefObject<RoomView>;
|
||||
protected readonly _resizeContainer: React.RefObject<HTMLDivElement>;
|
||||
protected readonly resizeHandler: React.RefObject<HTMLDivElement>;
|
||||
protected readonly _roomView: React.RefObject<RoomView | null>;
|
||||
protected readonly _resizeContainer: React.RefObject<HTMLDivElement | null>;
|
||||
protected readonly resizeHandler: React.RefObject<HTMLDivElement | null>;
|
||||
protected layoutWatcherRef?: string;
|
||||
protected compactLayoutWatcherRef?: string;
|
||||
protected backgroundImageWatcherRef?: string;
|
||||
|
||||
@@ -7,7 +7,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 React, { type ReactNode } from "react";
|
||||
import React, { type JSX, type ReactNode } from "react";
|
||||
import { type NumberSize, Resizable } from "re-resizable";
|
||||
import { type Direction } from "re-resizable/lib/resizer";
|
||||
import { type WebPanelResize } from "@matrix-org/analytics-events/types/typescript/WebPanelResize";
|
||||
|
||||
@@ -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 React, { createRef, lazy } from "react";
|
||||
import React, { type JSX, createRef, lazy } from "react";
|
||||
import {
|
||||
ClientEvent,
|
||||
createClient,
|
||||
@@ -235,6 +235,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
private themeWatcher?: ThemeWatcher;
|
||||
private fontWatcher?: FontWatcher;
|
||||
private readonly stores: SdkContextClass;
|
||||
private loadSessionAbortController = new AbortController();
|
||||
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
@@ -327,7 +328,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
// When the session loads it'll be detected as soft logged out and a dispatch
|
||||
// will be sent out to say that, triggering this MatrixChat to show the soft
|
||||
// logout page.
|
||||
Lifecycle.loadSession();
|
||||
Lifecycle.loadSession({ abortSignal: this.loadSessionAbortController.signal });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -552,6 +553,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
guestHsUrl: this.getServerProperties().serverConfig.hsUrl,
|
||||
guestIsUrl: this.getServerProperties().serverConfig.isUrl,
|
||||
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
|
||||
abortSignal: this.loadSessionAbortController.signal,
|
||||
});
|
||||
})
|
||||
.then((loadedSession) => {
|
||||
@@ -1565,26 +1567,33 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
});
|
||||
|
||||
cli.on(HttpApiEvent.SessionLoggedOut, function (errObj) {
|
||||
cli.on(HttpApiEvent.SessionLoggedOut, (errObj) => {
|
||||
this.loadSessionAbortController.abort(errObj);
|
||||
this.loadSessionAbortController = new AbortController();
|
||||
|
||||
if (Lifecycle.isLoggingOut()) return;
|
||||
|
||||
// A modal might have been open when we were logged out by the server
|
||||
Modal.forceCloseAllModals();
|
||||
|
||||
if (errObj.httpStatus === 401 && errObj.data && errObj.data["soft_logout"]) {
|
||||
if (errObj.httpStatus === 401 && errObj.data?.["soft_logout"]) {
|
||||
logger.warn("Soft logout issued by server - avoiding data deletion");
|
||||
Lifecycle.softLogout();
|
||||
return;
|
||||
}
|
||||
|
||||
dis.dispatch(
|
||||
{
|
||||
action: "logout",
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
// The above dispatch closes all modals, so open the modal after calling it synchronously
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: _t("auth|session_logged_out_title"),
|
||||
description: _t("auth|session_logged_out_description"),
|
||||
});
|
||||
|
||||
dis.dispatch({
|
||||
action: "logout",
|
||||
});
|
||||
});
|
||||
cli.on(HttpApiEvent.NoConsent, function (message, consentUri) {
|
||||
Modal.createDialog(
|
||||
|
||||
@@ -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 React, { createRef, type ReactNode, type TransitionEvent } from "react";
|
||||
import React, { type JSX, createRef, type ReactNode, type TransitionEvent } from "react";
|
||||
import classNames from "classnames";
|
||||
import {
|
||||
type Room,
|
||||
@@ -292,6 +292,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||
this.props.room?.currentState.off(RoomStateEvent.Update, this.calculateRoomMembersCount);
|
||||
SettingsStore.unwatchSetting(this.showTypingNotificationsWatcherRef);
|
||||
this.readReceiptMap = {};
|
||||
this.resizeObserver.disconnect();
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: IProps, prevState: IState): void {
|
||||
@@ -800,7 +801,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||
isRedacted={mxEv.isRedacted()}
|
||||
replacingEventId={mxEv.replacingEventId()}
|
||||
editState={isEditing ? this.props.editState : undefined}
|
||||
onHeightChanged={this.onHeightChanged}
|
||||
resizeObserver={this.resizeObserver}
|
||||
readReceipts={readReceipts}
|
||||
readReceiptMap={this.readReceiptMap}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
@@ -953,15 +954,13 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||
this.eventTiles[eventId] = node;
|
||||
};
|
||||
|
||||
// once dynamic content in the events load, make the scrollPanel check the
|
||||
// scroll offsets.
|
||||
// Once dynamic content in the events load, make the scrollPanel check the scroll offsets.
|
||||
public onHeightChanged = (): void => {
|
||||
const scrollPanel = this.scrollPanel.current;
|
||||
if (scrollPanel) {
|
||||
scrollPanel.checkScroll();
|
||||
}
|
||||
this.scrollPanel.current?.checkScroll();
|
||||
};
|
||||
|
||||
private resizeObserver = new ResizeObserver(this.onHeightChanged);
|
||||
|
||||
private onTypingShown = (): void => {
|
||||
const scrollPanel = this.scrollPanel.current;
|
||||
// this will make the timeline grow, so checkScroll
|
||||
|
||||
@@ -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 React from "react";
|
||||
import React, { type JSX } from "react";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import NotificationsIcon from "@vector-im/compound-design-tokens/assets/web/icons/notifications";
|
||||
|
||||
|
||||
@@ -5,7 +5,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 React, { createRef } from "react";
|
||||
import React, { type JSX, createRef } from "react";
|
||||
|
||||
import UIStore, { UI_EVENTS } from "../../stores/UIStore";
|
||||
import { lerp } from "../../utils/AnimationUtils";
|
||||
|
||||
@@ -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 React, { type MutableRefObject, type ReactNode, useRef } from "react";
|
||||
import React, { type RefObject, type ReactNode, useRef } from "react";
|
||||
import { CallEvent, CallState, type MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { type Optional } from "matrix-events-sdk";
|
||||
@@ -34,7 +34,7 @@ const SHOW_CALL_IN_STATES = [
|
||||
];
|
||||
|
||||
interface IProps {
|
||||
movePersistedElement: MutableRefObject<(() => void) | undefined>;
|
||||
movePersistedElement: RefObject<(() => void) | null>;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
@@ -280,7 +280,7 @@ class PipContainerInner extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
export const PipContainer: React.FC = () => {
|
||||
const movePersistedElement = useRef<() => void>();
|
||||
const movePersistedElement = useRef<() => void>(null);
|
||||
|
||||
return <PipContainerInner movePersistedElement={movePersistedElement} />;
|
||||
};
|
||||
|
||||
@@ -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 React, { forwardRef, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import React, { type JSX, forwardRef, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
type ISearchResults,
|
||||
type IThreadBundledRelationship,
|
||||
@@ -59,7 +59,7 @@ export const RoomSearchView = forwardRef<ScrollPanel, Props>(
|
||||
const aborted = useRef(false);
|
||||
// A map from room ID to permalink creator
|
||||
const permalinkCreators = useMemo(() => new Map<string, RoomPermalinkCreator>(), []);
|
||||
const innerRef = useRef<ScrollPanel | null>();
|
||||
const innerRef = useRef<ScrollPanel>(null);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -198,12 +198,6 @@ export const RoomSearchView = forwardRef<ScrollPanel, Props>(
|
||||
}
|
||||
}
|
||||
|
||||
// once dynamic content in the search results load, make the scrollPanel check
|
||||
// the scroll offsets.
|
||||
const onHeightChanged = (): void => {
|
||||
innerRef.current?.checkScroll();
|
||||
};
|
||||
|
||||
const onRef = (e: ScrollPanel | null): void => {
|
||||
if (typeof ref === "function") {
|
||||
ref(e);
|
||||
@@ -302,7 +296,6 @@ export const RoomSearchView = forwardRef<ScrollPanel, Props>(
|
||||
searchHighlights={highlights ?? []}
|
||||
resultLink={resultLink}
|
||||
permalinkCreator={permalinkCreator}
|
||||
onHeightChanged={onHeightChanged}
|
||||
/>,
|
||||
);
|
||||
|
||||
|
||||
@@ -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 React, { type ReactNode } from "react";
|
||||
import React, { type JSX, type ReactNode } from "react";
|
||||
import {
|
||||
ClientEvent,
|
||||
EventStatus,
|
||||
|
||||
@@ -256,7 +256,7 @@ interface LocalRoomViewProps {
|
||||
localRoom: LocalRoom;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
permalinkCreator: RoomPermalinkCreator;
|
||||
roomView: RefObject<HTMLElement>;
|
||||
roomView: RefObject<HTMLElement | null>;
|
||||
onFileDrop: (dataTransfer: DataTransfer) => Promise<void>;
|
||||
mainSplitContentType: MainSplitContentType;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
type JSX,
|
||||
type Dispatch,
|
||||
type KeyboardEvent,
|
||||
type KeyboardEventHandler,
|
||||
@@ -636,7 +637,7 @@ const useIntersectionObserver = (callback: () => void): ((element: HTMLDivElemen
|
||||
}
|
||||
};
|
||||
|
||||
const observerRef = useRef<IntersectionObserver>();
|
||||
const observerRef = useRef<IntersectionObserver>(undefined);
|
||||
return (element: HTMLDivElement) => {
|
||||
if (observerRef.current) {
|
||||
observerRef.current.disconnect();
|
||||
|
||||
@@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
import { EventType, RoomType, JoinRule, Preset, type Room, RoomEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import React, { useCallback, useContext, useRef, useState } from "react";
|
||||
import React, { type JSX, useCallback, useContext, useRef, useState } from "react";
|
||||
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import createRoom, { type IOpts } from "../../createRoom";
|
||||
|
||||
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import classNames from "classnames";
|
||||
import React, { type DetailedHTMLProps, type HTMLAttributes, type ReactNode } from "react";
|
||||
import React, { type JSX, type DetailedHTMLProps, type HTMLAttributes, type ReactNode } from "react";
|
||||
|
||||
interface Props extends DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement> {
|
||||
className?: string;
|
||||
|
||||
@@ -8,7 +8,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 * as React from "react";
|
||||
import React, { type JSX } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { _t, type TranslationKey } from "../../languageHandler";
|
||||
|
||||
@@ -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 React, { createRef, type KeyboardEvent } from "react";
|
||||
import React, { type JSX, createRef, type KeyboardEvent } from "react";
|
||||
import {
|
||||
type Thread,
|
||||
THREAD_RELATION_TYPE,
|
||||
|
||||
@@ -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 React, { createRef, type ReactNode } from "react";
|
||||
import React, { type JSX, createRef, type ReactNode } from "react";
|
||||
import { type Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
@@ -81,7 +81,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||
private dispatcherRef?: string;
|
||||
private themeWatcherRef?: string;
|
||||
private readonly dndWatcherRef?: string;
|
||||
private buttonRef: React.RefObject<HTMLButtonElement> = createRef();
|
||||
private buttonRef = createRef<HTMLButtonElement>();
|
||||
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
@@ -7,7 +7,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 React from "react";
|
||||
import React, { type JSX } from "react";
|
||||
import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import SyntaxHighlight from "../views/elements/SyntaxHighlight";
|
||||
|
||||
@@ -21,7 +21,7 @@ import SdkConfig from "../../SdkConfig";
|
||||
import { useScopedRoomContext } from "../../contexts/ScopedRoomContext.tsx";
|
||||
|
||||
interface Props {
|
||||
roomView: RefObject<HTMLElement>;
|
||||
roomView: RefObject<HTMLElement | null>;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
inviteEvent: MatrixEvent;
|
||||
}
|
||||
|
||||
@@ -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 React from "react";
|
||||
import React, { type JSX } from "react";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
|
||||
@@ -8,7 +8,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 React, { type ReactNode } from "react";
|
||||
import React, { type JSX, type ReactNode } from "react";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
import { LockSolidIcon, CheckIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
@@ -388,7 +388,9 @@ export default class ForgotPassword extends React.Component<Props, State> {
|
||||
label={_td("auth|change_password_new_label")}
|
||||
value={this.state.password}
|
||||
minScore={PASSWORD_MIN_SCORE}
|
||||
fieldRef={(field) => (this.fieldPassword = field)}
|
||||
fieldRef={(field) => {
|
||||
this.fieldPassword = field;
|
||||
}}
|
||||
onChange={this.onInputChanged.bind(this, "password")}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
@@ -399,7 +401,9 @@ export default class ForgotPassword extends React.Component<Props, State> {
|
||||
labelInvalid={_td("auth|reset_password|passwords_mismatch")}
|
||||
value={this.state.password2}
|
||||
password={this.state.password}
|
||||
fieldRef={(field) => (this.fieldPasswordConfirm = field)}
|
||||
fieldRef={(field) => {
|
||||
this.fieldPasswordConfirm = field;
|
||||
}}
|
||||
onChange={this.onInputChanged.bind(this, "password2")}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
|
||||
@@ -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 React, { type ReactNode } from "react";
|
||||
import React, { type JSX, type ReactNode } from "react";
|
||||
import classNames from "classnames";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { type SSOFlow, SSOAction } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
SSOAction,
|
||||
type RegisterResponse,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import React, { Fragment, type ReactNode } from "react";
|
||||
import React, { type JSX, Fragment, type ReactNode } from "react";
|
||||
import classNames from "classnames";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
|
||||
@@ -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 React from "react";
|
||||
import React, { type JSX } from "react";
|
||||
|
||||
import SplashPage from "../SplashPage";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
@@ -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 React from "react";
|
||||
import React, { type JSX } from "react";
|
||||
import { type KeyBackupInfo, type VerificationRequest } from "matrix-js-sdk/src/crypto-api";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { type SecretStorageKeyDescription } from "matrix-js-sdk/src/secret-storage";
|
||||
|
||||
@@ -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 React, { type ChangeEvent, type SyntheticEvent } from "react";
|
||||
import React, { type JSX, type ChangeEvent, type SyntheticEvent } from "react";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { type Optional } from "matrix-events-sdk";
|
||||
import { type LoginFlow, MatrixError, SSOAction, type SSOFlow } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
@@ -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 React, { Fragment, type PropsWithChildren, type ReactNode, useContext } from "react";
|
||||
import React, { type JSX, Fragment, type PropsWithChildren, type ReactNode, useContext } from "react";
|
||||
|
||||
import { AuthHeaderContext } from "./AuthHeaderContext";
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { isEqual } from "lodash";
|
||||
import React, { type ComponentProps, type PropsWithChildren, type Reducer, useReducer } from "react";
|
||||
import React, { type JSX, type ComponentProps, type PropsWithChildren, type Reducer, useReducer } from "react";
|
||||
|
||||
import { AuthHeaderContext } from "./AuthHeaderContext";
|
||||
import { type AuthHeaderModifier } from "./AuthHeaderModifier";
|
||||
|
||||
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import classNames from "classnames";
|
||||
import React, { useMemo } from "react";
|
||||
import React, { type JSX, useMemo } from "react";
|
||||
|
||||
type FlexProps = {
|
||||
/**
|
||||
|
||||
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import classNames from "classnames";
|
||||
import React, { type ComponentProps, type JSXElementConstructor, useMemo } from "react";
|
||||
import React, { type JSX, type ComponentProps, type JSXElementConstructor, useMemo } from "react";
|
||||
|
||||
type FlexProps<T extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>> = {
|
||||
/**
|
||||
|
||||
@@ -5,13 +5,16 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { type Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import dispatcher from "../../../dispatcher/dispatcher";
|
||||
import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { hasAccessToOptionsMenu } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { type RoomNotificationState } from "../../../stores/notifications/RoomNotificationState";
|
||||
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
||||
|
||||
export interface RoomListItemViewState {
|
||||
/**
|
||||
@@ -22,6 +25,14 @@ export interface RoomListItemViewState {
|
||||
* Open the room having given roomId.
|
||||
*/
|
||||
openRoom: () => void;
|
||||
/**
|
||||
* The a11y label for the room list item.
|
||||
*/
|
||||
a11yLabel: string;
|
||||
/**
|
||||
* The notification state of the room.
|
||||
*/
|
||||
notificationState: RoomNotificationState;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,6 +42,8 @@ export interface RoomListItemViewState {
|
||||
export function useRoomListItemViewModel(room: Room): RoomListItemViewState {
|
||||
// incoming: Check notification menu rights
|
||||
const showHoverMenu = hasAccessToOptionsMenu(room);
|
||||
const notificationState = useMemo(() => RoomNotificationStateStore.instance.getRoomState(room), [room]);
|
||||
const a11yLabel = getA11yLabel(room, notificationState);
|
||||
|
||||
// Actions
|
||||
|
||||
@@ -43,7 +56,38 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState {
|
||||
}, [room]);
|
||||
|
||||
return {
|
||||
notificationState,
|
||||
showHoverMenu,
|
||||
openRoom,
|
||||
a11yLabel,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the a11y label for the room list item
|
||||
* @param room
|
||||
* @param notificationState
|
||||
*/
|
||||
function getA11yLabel(room: Room, notificationState: RoomNotificationState): string {
|
||||
if (notificationState.isUnsetMessage) {
|
||||
return _t("a11y|room_messsage_not_sent", {
|
||||
roomName: room.name,
|
||||
});
|
||||
} else if (notificationState.invited) {
|
||||
return _t("a11y|room_n_unread_invite", {
|
||||
roomName: room.name,
|
||||
});
|
||||
} else if (notificationState.isMention) {
|
||||
return _t("a11y|room_n_unread_messages_mentions", {
|
||||
roomName: room.name,
|
||||
count: notificationState.count,
|
||||
});
|
||||
} else if (notificationState.hasUnreadCount) {
|
||||
return _t("a11y|room_n_unread_messages", {
|
||||
roomName: room.name,
|
||||
count: notificationState.count,
|
||||
});
|
||||
} else {
|
||||
return _t("room_list|room|open_room", { roomName: room.name });
|
||||
}
|
||||
}
|
||||
|
||||