Merge remote-tracking branch 'origin/develop' into hs/media-previews-server-config
2
.github/workflows/docker.yaml
vendored
@@ -132,7 +132,7 @@ jobs:
|
||||
cosign sign --yes ${images}
|
||||
|
||||
- name: Update repo description
|
||||
uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae # v4
|
||||
uses: peter-evans/dockerhub-description@0505d8b04853a30189aee66f5bb7fd1511bbac71 # v4
|
||||
if: github.event_name != 'pull_request'
|
||||
continue-on-error: true
|
||||
with:
|
||||
|
||||
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"
|
||||
|
||||
2
.github/workflows/tests.yml
vendored
@@ -104,7 +104,7 @@ jobs:
|
||||
|
||||
- name: Skip SonarCloud in merge queue
|
||||
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true'
|
||||
uses: guibranco/github-status-action-v2@fe98467f9071758c7fc214af9dbac7f301bd23d4
|
||||
uses: guibranco/github-status-action-v2@9b1d102b3c32583174557f58c53e3b09d43d1b1d
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: success
|
||||
|
||||
40
CHANGELOG.md
@@ -1,3 +1,43 @@
|
||||
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
|
||||
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
- [Skinning](skinning.md)
|
||||
- [Cider editor](ciderEditor.md)
|
||||
- [Iconography](icons.md)
|
||||
- [Jitsi](jitsi.md)
|
||||
- [Local echo](local-echo-dev.md)
|
||||
- [Media](media-handling.md)
|
||||
- [Room List Store](room-list-store.md)
|
||||
|
||||
@@ -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
|
||||
|
||||
25
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "element-web",
|
||||
"version": "1.11.95",
|
||||
"version": "1.11.96",
|
||||
"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",
|
||||
@@ -73,8 +74,8 @@
|
||||
"@types/react-dom": "18.3.5",
|
||||
"oidc-client-ts": "3.2.0",
|
||||
"jwt-decode": "4.0.0",
|
||||
"caniuse-lite": "1.0.30001704",
|
||||
"testcontainers": "10.21.0",
|
||||
"caniuse-lite": "1.0.30001707",
|
||||
"testcontainers": "10.23.0",
|
||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
|
||||
"wrap-ansi": "npm:wrap-ansi@^7.0.0"
|
||||
},
|
||||
@@ -92,8 +93,8 @@
|
||||
"@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/matrix-wysiwyg": "2.38.2",
|
||||
"@vector-im/compound-web": "^7.10.1",
|
||||
"@vector-im/matrix-wysiwyg": "2.38.3",
|
||||
"@zxcvbn-ts/core": "^3.0.4",
|
||||
"@zxcvbn-ts/language-common": "^3.0.4",
|
||||
"@zxcvbn-ts/language-en": "^3.0.2",
|
||||
@@ -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",
|
||||
@@ -144,13 +146,14 @@
|
||||
"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",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"sanitize-html": "2.14.0",
|
||||
"sanitize-html": "2.15.0",
|
||||
"tar-js": "^0.3.0",
|
||||
"temporal-polyfill": "^0.2.5",
|
||||
"temporal-polyfill": "^0.3.0",
|
||||
"ua-parser-js": "^1.0.2",
|
||||
"uuid": "^11.0.0",
|
||||
"what-input": "^5.2.10"
|
||||
@@ -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",
|
||||
@@ -211,7 +215,7 @@
|
||||
"@types/react-beautiful-dnd": "^13.0.0",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"@types/react-transition-group": "^4.4.0",
|
||||
"@types/sanitize-html": "2.13.0",
|
||||
"@types/sanitize-html": "2.15.0",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/tar-js": "^0.3.5",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,31 +22,82 @@ test.describe("Room list filters and sort", () => {
|
||||
return page.getByRole("listbox", { name: "Room list filters" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the room list
|
||||
* @param page
|
||||
*/
|
||||
function getRoomList(page: Page) {
|
||||
return page.getByTestId("room-list");
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page, app, bot, user }) => {
|
||||
// The notification toast is displayed above the search section
|
||||
await app.closeNotificationToast();
|
||||
});
|
||||
|
||||
test.describe("Scroll behaviour", () => {
|
||||
test("should scroll to the top of list when filter is applied and active room is not in filtered list", async ({
|
||||
page,
|
||||
app,
|
||||
}) => {
|
||||
const createFavouriteRoom = async (name: string) => {
|
||||
const id = await app.client.createRoom({
|
||||
name,
|
||||
});
|
||||
await app.client.evaluate(async (client, favouriteId) => {
|
||||
await client.setRoomTag(favouriteId, "m.favourite", { order: 0.5 });
|
||||
}, id);
|
||||
};
|
||||
|
||||
// Create 5 favourite rooms
|
||||
let i = 0;
|
||||
for (; i < 5; i++) {
|
||||
await createFavouriteRoom(`room${i}-fav`);
|
||||
}
|
||||
|
||||
// Create a non-favourite room
|
||||
await app.client.createRoom({ name: `room-non-fav` });
|
||||
|
||||
// Create rest of the favourite rooms
|
||||
for (; i < 20; i++) {
|
||||
await createFavouriteRoom(`room${i}-fav`);
|
||||
}
|
||||
|
||||
// Open the non-favourite room
|
||||
const roomListView = getRoomList(page);
|
||||
const tile = roomListView.getByRole("gridcell", { name: "Open room room-non-fav" });
|
||||
await tile.scrollIntoViewIfNeeded();
|
||||
await tile.click();
|
||||
|
||||
// Enable Favourite filter
|
||||
const primaryFilters = getPrimaryFilters(page);
|
||||
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
|
||||
await expect(tile).not.toBeVisible();
|
||||
|
||||
// Ensure the room list is not scrolled
|
||||
const isScrolledDown = await page
|
||||
.getByRole("grid", { name: "Room list" })
|
||||
.evaluate((e) => e.scrollTop !== 0);
|
||||
expect(isScrolledDown).toStrictEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Room list", () => {
|
||||
/**
|
||||
* Get the room list
|
||||
* @param page
|
||||
*/
|
||||
function getRoomList(page: Page) {
|
||||
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 +139,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,241 @@ 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 open the notification 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");
|
||||
let roomItemMenu = roomItem.getByRole("button", { name: "Notification options" });
|
||||
await roomItemMenu.click();
|
||||
|
||||
// Default settings should be selected
|
||||
await expect(page.getByRole("menuitem", { name: "Match default settings" })).toHaveAttribute(
|
||||
"aria-selected",
|
||||
"true",
|
||||
);
|
||||
await expect(page).toMatchScreenshot("room-list-item-open-notification-options.png");
|
||||
|
||||
// It should make the room muted
|
||||
await page.getByRole("menuitem", { name: "Mute room" }).click();
|
||||
|
||||
// Remove hover on the room list item
|
||||
await roomListView.hover();
|
||||
|
||||
// Scroll to the bottom of the list
|
||||
await page.getByRole("grid", { name: "Room list" }).evaluate((e) => {
|
||||
e.scrollTop = e.scrollHeight;
|
||||
});
|
||||
|
||||
// The room decoration should have the muted icon
|
||||
await expect(roomItem.getByTestId("notification-decoration")).toBeVisible();
|
||||
|
||||
await roomItem.hover();
|
||||
// On hover, the room should show the muted icon
|
||||
await expect(roomItem).toMatchScreenshot("room-list-item-hover-silent.png");
|
||||
|
||||
roomItemMenu = roomItem.getByRole("button", { name: "Notification options" });
|
||||
await roomItemMenu.click();
|
||||
// The Mute room option should be selected
|
||||
await expect(page.getByRole("menuitem", { name: "Mute room" })).toHaveAttribute("aria-selected", "true");
|
||||
await expect(page).toMatchScreenshot("room-list-item-open-notification-options-selection.png");
|
||||
});
|
||||
|
||||
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);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -136,13 +136,30 @@ test.describe("RightPanel", () => {
|
||||
});
|
||||
test.describe("room reporting", () => {
|
||||
test.skip(isDendrite, "Dendrite does not implement room reporting");
|
||||
test("should handle reporting a room", async ({ page, app }) => {
|
||||
test("should handle reporting a room", { tag: "@screenshot" }, 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 expect(dialog).toMatchScreenshot("room-report-dialog.png");
|
||||
await dialog.getByRole("button", { name: "Send report" }).click();
|
||||
await expect(page.getByText("Your report was sent.")).toBeVisible();
|
||||
|
||||
// Dialog should have gone
|
||||
await expect(page.locator(".mx_Dialog")).toHaveCount(0);
|
||||
});
|
||||
test("should handle reporting a room and leaving the 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.getByRole("switch", { name: "Leave room" }).click();
|
||||
await dialog.getByLabel("reason").fill("This room should be reported");
|
||||
await dialog.getByRole("button", { name: "Send report" }).click();
|
||||
await page.getByRole("dialog", { name: "Leave room" }).getByRole("button", { name: "Leave" }).click();
|
||||
|
||||
// Dialog should have gone
|
||||
await expect(page.locator(".mx_Dialog")).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -1341,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]");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 75 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 |
|
After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 28 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:d19854a3dbbb4d5d24d84767d17e1a623181ae5f2bdda3505819c05a8d3c8611";
|
||||
const TAG = "develop@sha256:66955f34a593cfc3b6e77b8d5510c60c6094f5bade8a17d2feaefbb8662ccf09";
|
||||
|
||||
/**
|
||||
* SynapseContainer which freezes the docker digest to stabilise tests,
|
||||
|
||||
@@ -13,4 +13,9 @@ Please see LICENSE files in the repository root for full details.
|
||||
border-radius: 0.5rem;
|
||||
padding: var(--cpd-space-3x) var(--cpd-space-4x);
|
||||
}
|
||||
|
||||
label {
|
||||
color: var(--cpd-color-text-primary);
|
||||
font-weight: var(--cpd-font-weight-semibold);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
@@ -35,7 +39,7 @@
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
|
||||
span {
|
||||
.mx_RoomListItemView_roomName {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -46,8 +50,28 @@
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomListItemView_bold .mx_RoomListItemView_roomName {
|
||||
font: var(--cpd-font-body-md-semibold);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,10 +46,8 @@ Please see LICENSE files in the repository root for full details.
|
||||
}
|
||||
|
||||
.mx_VerificationShowSas_emojiSas_label {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-size: $font-12px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.mx_VerificationShowSas_emojiSas_break {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -656,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, {
|
||||
@@ -671,7 +678,7 @@ async function handleLoadSessionFailure(e: unknown): Promise<boolean> {
|
||||
}
|
||||
|
||||
// try, try again
|
||||
return loadSession();
|
||||
return loadSession(loadSessionOpts);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1149,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, type HTMLAttributes } 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 {
|
||||
@@ -57,7 +57,8 @@ export default class NodeAnimator extends React.Component<IProps> {
|
||||
* @param {React.CSSProperties} styles a key/value pair of CSS properties
|
||||
* @returns {void}
|
||||
*/
|
||||
private applyStyles(node: HTMLElement, styles: React.CSSProperties): void {
|
||||
private applyStyles(node: HTMLElement, styles?: React.CSSProperties): void {
|
||||
if (!styles) return;
|
||||
Object.entries(styles).forEach(([property, value]) => {
|
||||
node.style[property as keyof Omit<CSSStyleDeclaration, "length" | "parentRule">] = value;
|
||||
});
|
||||
@@ -68,21 +69,22 @@ export default class NodeAnimator extends React.Component<IProps> {
|
||||
this.children = {};
|
||||
React.Children.toArray(newChildren).forEach((c) => {
|
||||
if (!isReactElement(c)) return;
|
||||
const props = c.props as HTMLAttributes<HTMLElement>;
|
||||
if (oldChildren[c.key!]) {
|
||||
const old = oldChildren[c.key!];
|
||||
const oldNode = this.nodes[old.key!];
|
||||
|
||||
if (oldNode && oldNode.style.left !== c.props.style.left) {
|
||||
this.applyStyles(oldNode, { left: c.props.style.left });
|
||||
if (oldNode && props.style && oldNode.style.left !== props.style.left) {
|
||||
this.applyStyles(oldNode, { left: props.style.left });
|
||||
}
|
||||
// clone the old element with the props (and children) of the new element
|
||||
// so prop updates are still received by the children.
|
||||
this.children[c.key!] = React.cloneElement(old, c.props, c.props.children);
|
||||
this.children[c.key!] = React.cloneElement(old, props, props.children);
|
||||
} else {
|
||||
// new element. If we have a startStyle, use that as the style and go through
|
||||
// the enter animations
|
||||
const newProps: Partial<IChildProps> = {};
|
||||
const restingStyle = c.props.style;
|
||||
const restingStyle = props.style;
|
||||
|
||||
const startStyles = this.props.startStyles;
|
||||
if (startStyles.length > 0) {
|
||||
@@ -97,7 +99,7 @@ export default class NodeAnimator extends React.Component<IProps> {
|
||||
});
|
||||
}
|
||||
|
||||
private collectNode(k: Key, domNode: HTMLElement | null, restingStyle: React.CSSProperties): void {
|
||||
private collectNode(k: Key, domNode: HTMLElement | null, restingStyle?: React.CSSProperties): void {
|
||||
const key = typeof k === "bigint" ? Number(k) : k;
|
||||
if (domNode && this.nodes[key] === undefined && this.props.startStyles.length > 0) {
|
||||
const startStyles = this.props.startStyles;
|
||||
|
||||
@@ -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>> = {
|
||||
/**
|
||||
|
||||
@@ -11,7 +11,7 @@ import { type Room, RoomEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
|
||||
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||
import { useUnreadNotifications } from "../../../hooks/useUnreadNotifications";
|
||||
import { hasAccessToOptionsMenu } from "./utils";
|
||||
import { hasAccessToNotificationMenu, hasAccessToOptionsMenu } from "./utils";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import { DefaultTagID } from "../../../stores/room-list/models";
|
||||
import { NotificationLevel } from "../../../stores/notifications/NotificationLevel";
|
||||
@@ -21,12 +21,18 @@ import dispatcher from "../../../dispatcher/dispatcher";
|
||||
import { clearRoomNotification, setMarkedUnreadState } from "../../../utils/notifications";
|
||||
import PosthogTrackers from "../../../PosthogTrackers";
|
||||
import { tagRoom } from "../../../utils/room/tagRoom";
|
||||
import { RoomNotifState } from "../../../RoomNotifs";
|
||||
import { useNotificationState } from "../../../hooks/useRoomNotificationState";
|
||||
|
||||
export interface RoomListItemMenuViewState {
|
||||
/**
|
||||
* Whether the more options menu should be shown.
|
||||
*/
|
||||
showMoreOptionsMenu: boolean;
|
||||
/**
|
||||
* Whether the notification menu should be shown.
|
||||
*/
|
||||
showNotificationMenu: boolean;
|
||||
/**
|
||||
* Whether the room is a favourite room.
|
||||
*/
|
||||
@@ -47,6 +53,22 @@ export interface RoomListItemMenuViewState {
|
||||
* Can mark the room as unread.
|
||||
*/
|
||||
canMarkAsUnread: boolean;
|
||||
/**
|
||||
* Whether the notification is set to all messages.
|
||||
*/
|
||||
isNotificationAllMessage: boolean;
|
||||
/**
|
||||
* Whether the notification is set to all messages loud.
|
||||
*/
|
||||
isNotificationAllMessageLoud: boolean;
|
||||
/**
|
||||
* Whether the notification is set to mentions and keywords only.
|
||||
*/
|
||||
isNotificationMentionOnly: boolean;
|
||||
/**
|
||||
* Whether the notification is muted.
|
||||
*/
|
||||
isNotificationMute: boolean;
|
||||
/**
|
||||
* Mark the room as read.
|
||||
* @param evt
|
||||
@@ -81,6 +103,11 @@ export interface RoomListItemMenuViewState {
|
||||
* @param evt
|
||||
*/
|
||||
leaveRoom: (evt: Event) => void;
|
||||
/**
|
||||
* Set the room notification state.
|
||||
* @param state
|
||||
*/
|
||||
setRoomNotifState: (state: RoomNotifState) => void;
|
||||
}
|
||||
|
||||
export function useRoomListItemMenuViewModel(room: Room): RoomListItemMenuViewState {
|
||||
@@ -88,12 +115,13 @@ export function useRoomListItemMenuViewModel(room: Room): RoomListItemMenuViewSt
|
||||
const roomTags = useEventEmitterState(room, RoomEvent.Tags, () => room.tags);
|
||||
const { level: notificationLevel } = useUnreadNotifications(room);
|
||||
|
||||
const showMoreOptionsMenu = hasAccessToOptionsMenu(room);
|
||||
|
||||
const isDm = Boolean(DMRoomMap.shared().getUserIdForRoomId(room.roomId));
|
||||
const isFavourite = Boolean(roomTags[DefaultTagID.Favourite]);
|
||||
const isArchived = Boolean(roomTags[DefaultTagID.Archived]);
|
||||
|
||||
const showMoreOptionsMenu = hasAccessToOptionsMenu(room);
|
||||
const showNotificationMenu = hasAccessToNotificationMenu(room, matrixClient.isGuest(), isArchived);
|
||||
|
||||
const canMarkAsRead = notificationLevel > NotificationLevel.None;
|
||||
const canMarkAsUnread = !canMarkAsRead && !isArchived;
|
||||
|
||||
@@ -101,6 +129,12 @@ export function useRoomListItemMenuViewModel(room: Room): RoomListItemMenuViewSt
|
||||
room.canInvite(matrixClient.getUserId()!) && !isDm && shouldShowComponent(UIComponent.InviteUsers);
|
||||
const canCopyRoomLink = !isDm;
|
||||
|
||||
const [roomNotifState, setRoomNotifState] = useNotificationState(room);
|
||||
const isNotificationAllMessage = roomNotifState === RoomNotifState.AllMessages;
|
||||
const isNotificationAllMessageLoud = roomNotifState === RoomNotifState.AllMessagesLoud;
|
||||
const isNotificationMentionOnly = roomNotifState === RoomNotifState.MentionsOnly;
|
||||
const isNotificationMute = roomNotifState === RoomNotifState.Mute;
|
||||
|
||||
// Actions
|
||||
|
||||
const markAsRead = useCallback(
|
||||
@@ -164,11 +198,16 @@ export function useRoomListItemMenuViewModel(room: Room): RoomListItemMenuViewSt
|
||||
|
||||
return {
|
||||
showMoreOptionsMenu,
|
||||
showNotificationMenu,
|
||||
isFavourite,
|
||||
canInvite,
|
||||
canCopyRoomLink,
|
||||
canMarkAsRead,
|
||||
canMarkAsUnread,
|
||||
isNotificationAllMessage,
|
||||
isNotificationAllMessageLoud,
|
||||
isNotificationMentionOnly,
|
||||
isNotificationMute,
|
||||
markAsRead,
|
||||
markAsUnread,
|
||||
toggleFavorite,
|
||||
@@ -176,5 +215,6 @@ export function useRoomListItemMenuViewModel(room: Room): RoomListItemMenuViewSt
|
||||
invite,
|
||||
copyRoomLink,
|
||||
leaveRoom,
|
||||
setRoomNotifState,
|
||||
};
|
||||
}
|
||||
|
||||