From 6961c256035bed0b7640a6e5907652c806968478 Mon Sep 17 00:00:00 2001
From: Callum Brown
Date: Tue, 24 Jan 2023 12:02:07 +0000
Subject: [PATCH 01/97] Support registration tokens (#7275)
* Support token authenticated registration
https://spec.matrix.org/v1.2/client-server-api/#token-authenticated-registration
Signed-off-by: Callum Brown
* Backwards compatibility with unstable auth type
Some server installs are not updated to use the stable version
of the registration token authentication type, so also handle
the unstable version defined in MSC3231.
Signed-off-by: Callum Brown
* Make LOGIN_TYPE public and readonly
Co-authored-by: Travis Ralston
* Remove line related to skinning
Signed-off-by: Callum Brown
* Change empty string to null
Signed-off-by: Callum Brown
* Use "public"s for new code style
Signed-off-by: Callum Brown
* Change input to AccessibleButton
Signed-off-by: Callum Brown
* Add more detail regarding source of token
Signed-off-by: Callum Brown
* Fix lint error
The text and button type will be the same every time
for registration tokens, unlike (possibly) for SSO.
Signed-off-by: Callum Brown
* Change null back to ""
Due to the following warning when attempting to test:
> Warning: `value` prop on `input` should not be null.
> Consider using an empty string to clear the component or
> `undefined` for uncontrolled components.
Signed-off-by: Callum Brown
* Disable submit button when no token entered
Signed-off-by: Callum Brown
* Add test for registration tokens
Adapted from test/components/views/dialogs/InteractiveAuthDialog-test.tsx
Signed-off-by: Callum Brown
* Fix linting errors
Signed-off-by: Callum Brown
* Fix test for registration tokens
Signed-off-by: Callum Brown
Signed-off-by: Callum Brown
Co-authored-by: Travis Ralston
Co-authored-by: Andy Balaam
---
.../auth/InteractiveAuthEntryComponents.tsx | 88 ++++++++++++++-
src/i18n/strings/en_EN.json | 2 +
.../views/auth/RegistrationToken-test.tsx | 106 ++++++++++++++++++
3 files changed, 195 insertions(+), 1 deletion(-)
create mode 100644 test/components/views/auth/RegistrationToken-test.tsx
diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx
index 78fcdf7c2ee..4a995e4d06b 100644
--- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx
+++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx
@@ -692,6 +692,89 @@ export class MsisdnAuthEntry extends React.Component {
+ public static readonly LOGIN_TYPE = AuthType.RegistrationToken;
+
+ public constructor(props: IAuthEntryProps) {
+ super(props);
+
+ this.state = {
+ registrationToken: "",
+ };
+ }
+
+ public componentDidMount(): void {
+ this.props.onPhaseChange(DEFAULT_PHASE);
+ }
+
+ private onSubmit = (e: FormEvent): void => {
+ e.preventDefault();
+ if (this.props.busy) return;
+
+ this.props.submitAuthDict({
+ // Could be AuthType.RegistrationToken or AuthType.UnstableRegistrationToken
+ type: this.props.loginType,
+ token: this.state.registrationToken,
+ });
+ };
+
+ private onRegistrationTokenFieldChange = (ev: ChangeEvent): void => {
+ // enable the submit button if the registration token is non-empty
+ this.setState({
+ registrationToken: ev.target.value,
+ });
+ };
+
+ public render(): JSX.Element {
+ const registrationTokenBoxClass = classNames({
+ error: this.props.errorText,
+ });
+
+ let submitButtonOrSpinner;
+ if (this.props.busy) {
+ submitButtonOrSpinner = ;
+ } else {
+ submitButtonOrSpinner = (
+
+ {_t("Continue")}
+
+ );
+ }
+
+ let errorSection;
+ if (this.props.errorText) {
+ errorSection = (
+
+ {this.props.errorText}
+
+ );
+ }
+
+ return (
+
+
{_t("Enter a registration token provided by the homeserver administrator.")}
+
+
+ );
+ }
+}
+
interface ISSOAuthEntryProps extends IAuthEntryProps {
continueText?: string;
continueKind?: string;
@@ -713,7 +796,7 @@ export class SSOAuthEntry extends React.Component mount();
+
+ beforeEach(function () {
+ jest.clearAllMocks();
+ });
+
+ afterAll(() => {
+ unmockClientPeg();
+ });
+
+ const getSubmitButton = (wrapper: ReactWrapper) => wrapper.find('AccessibleButton[kind="primary"]').at(0);
+ const getRegistrationTokenInput = (wrapper: ReactWrapper) =>
+ wrapper.find('input[name="registrationTokenField"]').at(0);
+
+ it("Should successfully complete a registration token flow", async () => {
+ const onAuthFinished = jest.fn();
+ const makeRequest = jest.fn().mockResolvedValue({ a: 1 });
+
+ const authData = {
+ session: "sess",
+ flows: [{ stages: ["m.login.registration_token"] }],
+ };
+
+ const wrapper = getComponent({ makeRequest, onAuthFinished, authData });
+
+ const registrationTokenNode = getRegistrationTokenInput(wrapper);
+ const submitNode = getSubmitButton(wrapper);
+ const formNode = wrapper.find("form").at(0);
+
+ expect(registrationTokenNode).toBeTruthy();
+ expect(submitNode).toBeTruthy();
+ expect(formNode).toBeTruthy();
+
+ // submit should be disabled
+ expect(submitNode.props().disabled).toBe(true);
+
+ // put something in the registration token box
+ act(() => {
+ registrationTokenNode.simulate("change", { target: { value: "s3kr3t" } });
+ wrapper.setProps({});
+ });
+
+ expect(getRegistrationTokenInput(wrapper).props().value).toEqual("s3kr3t");
+ expect(getSubmitButton(wrapper).props().disabled).toBe(false);
+
+ // hit enter; that should trigger a request
+ act(() => {
+ formNode.simulate("submit");
+ });
+
+ // wait for auth request to resolve
+ await flushPromises();
+
+ expect(makeRequest).toHaveBeenCalledTimes(1);
+ expect(makeRequest).toBeCalledWith(
+ expect.objectContaining({
+ session: "sess",
+ type: "m.login.registration_token",
+ token: "s3kr3t",
+ }),
+ );
+
+ expect(onAuthFinished).toBeCalledTimes(1);
+ expect(onAuthFinished).toBeCalledWith(
+ true,
+ { a: 1 },
+ { clientSecret: "t35tcl1Ent5ECr3T", emailSid: undefined },
+ );
+ });
+});
From 3f766b3f0f195a2ffaa837acd4e1a2df0eb61ccb Mon Sep 17 00:00:00 2001
From: Mustafa Kapadia <60058032+mustafa-kapadia1483@users.noreply.github.com>
Date: Wed, 25 Jan 2023 13:21:07 +0530
Subject: [PATCH 02/97] Issue Fix: Misaligned reply preview in thread composer
(#9977)
---
res/css/views/rooms/_ReplyPreview.pcss | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/res/css/views/rooms/_ReplyPreview.pcss b/res/css/views/rooms/_ReplyPreview.pcss
index 96b0321f298..95a061c4042 100644
--- a/res/css/views/rooms/_ReplyPreview.pcss
+++ b/res/css/views/rooms/_ReplyPreview.pcss
@@ -26,7 +26,7 @@ limitations under the License.
display: flex;
flex-flow: column;
row-gap: $spacing-8;
- padding: $spacing-8 $spacing-8 0 $spacing-8;
+ padding: $spacing-8 $spacing-8 0 0;
.mx_ReplyPreview_header {
display: flex;
From 441ad40e5549470d390ef20fc347e8cb2af4f4f5 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 25 Jan 2023 12:33:44 +0000
Subject: [PATCH 03/97] Update dependency ua-parser-js to v1.0.33 [SECURITY]
(#9982)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
yarn.lock | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/yarn.lock b/yarn.lock
index bef6f4eb3ad..d9d770a0330 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8511,9 +8511,9 @@ ua-parser-js@^0.7.30:
integrity sha512-f9BESNVhzlhEFf2CHMSj40NWOjYPl1YKYbrvIr/hFTDEmLq7SRbWvm7FcdcpCYT95zrOhC7gZSxjdnnTpBcwVw==
ua-parser-js@^1.0.2:
- version "1.0.32"
- resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.32.tgz#786bf17df97de159d5b1c9d5e8e9e89806f8a030"
- integrity sha512-dXVsz3M4j+5tTiovFVyVqssXBu5HM47//YSOeZ9fQkdDKkfzv2v3PP1jmH6FUyPW+yCSn7aBVK1fGGKNhowdDA==
+ version "1.0.33"
+ resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.33.tgz#f21f01233e90e7ed0f059ceab46eb190ff17f8f4"
+ integrity sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ==
unbox-primitive@^1.0.2:
version "1.0.2"
From 222f8a919dd90867bec77a471f90a55a22fb85a5 Mon Sep 17 00:00:00 2001
From: Clark Fischer <439978+clarkf@users.noreply.github.com>
Date: Wed, 25 Jan 2023 18:14:15 +0000
Subject: [PATCH 04/97] Fix failing userAgentParser tests (#9988)
---
test/utils/device/parseUserAgent-test.ts | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/test/utils/device/parseUserAgent-test.ts b/test/utils/device/parseUserAgent-test.ts
index e6a1a228fa5..2aabdc479db 100644
--- a/test/utils/device/parseUserAgent-test.ts
+++ b/test/utils/device/parseUserAgent-test.ts
@@ -70,7 +70,7 @@ const DESKTOP_UA = [
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) ElementNightly/2022091301 Chrome/104.0.5112.102 Electron/20.1.1 Safari/537.36",
];
const DESKTOP_EXPECTED_RESULT = [
- makeDeviceExtendedInfo(DeviceType.Desktop, undefined, "Mac OS", "Electron", "20.1.1"),
+ makeDeviceExtendedInfo(DeviceType.Desktop, "Apple Macintosh", "Mac OS", "Electron", "20.1.1"),
makeDeviceExtendedInfo(DeviceType.Desktop, undefined, "Windows", "Electron", "20.1.1"),
];
@@ -88,10 +88,10 @@ const WEB_UA = [
];
const WEB_EXPECTED_RESULT = [
- makeDeviceExtendedInfo(DeviceType.Web, undefined, "Mac OS", "Chrome", "104.0.5112.102"),
+ makeDeviceExtendedInfo(DeviceType.Web, "Apple Macintosh", "Mac OS", "Chrome", "104.0.5112.102"),
makeDeviceExtendedInfo(DeviceType.Web, undefined, "Windows", "Chrome", "104.0.5112.102"),
- makeDeviceExtendedInfo(DeviceType.Web, undefined, "Mac OS", "Firefox", "39.0"),
- makeDeviceExtendedInfo(DeviceType.Web, undefined, "Mac OS", "Safari", "8.0.3"),
+ makeDeviceExtendedInfo(DeviceType.Web, "Apple Macintosh", "Mac OS", "Firefox", "39.0"),
+ makeDeviceExtendedInfo(DeviceType.Web, "Apple Macintosh", "Mac OS", "Safari", "8.0.3"),
makeDeviceExtendedInfo(DeviceType.Web, undefined, "Windows", "Firefox", "40.0"),
makeDeviceExtendedInfo(DeviceType.Web, undefined, "Windows", "Edge", "12.246"),
// using mobile browser
From 406edfc27d5a38a9f59214560d132467b00cd281 Mon Sep 17 00:00:00 2001
From: Florian Duros
Date: Thu, 26 Jan 2023 11:08:23 +0100
Subject: [PATCH 05/97] Fix link creation with backward selection (#9986)
Fix link creation with backward selection
---
cypress/e2e/composer/composer.spec.ts | 22 ++++++++++++++-
.../rooms/wysiwyg_composer/ComposerContext.ts | 2 +-
.../hooks/useComposerFunctions.ts | 1 +
.../wysiwyg_composer/hooks/useSelection.ts | 4 +++
.../views/rooms/wysiwyg_composer/types.ts | 4 ++-
.../rooms/wysiwyg_composer/utils/selection.ts | 9 +++++--
.../SendWysiwygComposer-test.tsx | 27 +++++++++++++++++++
.../components/LinkModal-test.tsx | 1 +
8 files changed, 65 insertions(+), 5 deletions(-)
diff --git a/cypress/e2e/composer/composer.spec.ts b/cypress/e2e/composer/composer.spec.ts
index 289d865ba64..48467ac6564 100644
--- a/cypress/e2e/composer/composer.spec.ts
+++ b/cypress/e2e/composer/composer.spec.ts
@@ -98,7 +98,7 @@ describe("Composer", () => {
});
});
- describe("WYSIWYG", () => {
+ describe("Rich text editor", () => {
beforeEach(() => {
cy.enableLabsFeature("feature_wysiwyg_composer");
cy.initTestUser(homeserver, "Janet").then(() => {
@@ -165,5 +165,25 @@ describe("Composer", () => {
cy.contains(".mx_EventTile_body", "my message 3");
});
});
+
+ describe("links", () => {
+ it("create link with a forward selection", () => {
+ // Type a message
+ cy.get("div[contenteditable=true]").type("my message 0{selectAll}");
+
+ // Open link modal
+ cy.get('button[aria-label="Link"]').click();
+ // Fill the link field
+ cy.get('input[label="Link"]').type("https://matrix.org/");
+ // Click on save
+ cy.get('button[type="submit"]').click();
+ // Send the message
+ cy.get('div[aria-label="Send message"]').click();
+
+ // It was sent
+ cy.contains(".mx_EventTile_body a", "my message 0");
+ cy.get(".mx_EventTile_body a").should("have.attr", "href").and("include", "https://matrix.org/");
+ });
+ });
});
});
diff --git a/src/components/views/rooms/wysiwyg_composer/ComposerContext.ts b/src/components/views/rooms/wysiwyg_composer/ComposerContext.ts
index c8a369ead29..582c883dfee 100644
--- a/src/components/views/rooms/wysiwyg_composer/ComposerContext.ts
+++ b/src/components/views/rooms/wysiwyg_composer/ComposerContext.ts
@@ -20,7 +20,7 @@ import { SubSelection } from "./types";
export function getDefaultContextValue(): { selection: SubSelection } {
return {
- selection: { anchorNode: null, anchorOffset: 0, focusNode: null, focusOffset: 0 },
+ selection: { anchorNode: null, anchorOffset: 0, focusNode: null, focusOffset: 0, isForward: true },
};
}
diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useComposerFunctions.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useComposerFunctions.ts
index cad2e130745..7578fb7389d 100644
--- a/src/components/views/rooms/wysiwyg_composer/hooks/useComposerFunctions.ts
+++ b/src/components/views/rooms/wysiwyg_composer/hooks/useComposerFunctions.ts
@@ -44,6 +44,7 @@ export function useComposerFunctions(
anchorOffset: anchorOffset + text.length,
focusNode: ref.current.firstChild,
focusOffset: focusOffset + text.length,
+ isForward: true,
});
setContent(ref.current.innerHTML);
}
diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useSelection.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useSelection.ts
index 7f828cee391..92823773c35 100644
--- a/src/components/views/rooms/wysiwyg_composer/hooks/useSelection.ts
+++ b/src/components/views/rooms/wysiwyg_composer/hooks/useSelection.ts
@@ -23,11 +23,15 @@ function setSelectionContext(composerContext: ComposerContextState): void {
const selection = document.getSelection();
if (selection) {
+ const range = selection.getRangeAt(0);
+ const isForward = range.startContainer === selection.anchorNode && range.startOffset === selection.anchorOffset;
+
composerContext.selection = {
anchorNode: selection.anchorNode,
anchorOffset: selection.anchorOffset,
focusNode: selection.focusNode,
focusOffset: selection.focusOffset,
+ isForward,
};
}
}
diff --git a/src/components/views/rooms/wysiwyg_composer/types.ts b/src/components/views/rooms/wysiwyg_composer/types.ts
index 6505825b286..97d3fd91a5e 100644
--- a/src/components/views/rooms/wysiwyg_composer/types.ts
+++ b/src/components/views/rooms/wysiwyg_composer/types.ts
@@ -19,4 +19,6 @@ export type ComposerFunctions = {
insertText: (text: string) => void;
};
-export type SubSelection = Pick;
+export type SubSelection = Pick & {
+ isForward: boolean;
+};
diff --git a/src/components/views/rooms/wysiwyg_composer/utils/selection.ts b/src/components/views/rooms/wysiwyg_composer/utils/selection.ts
index e6a594451bb..ca515d06eef 100644
--- a/src/components/views/rooms/wysiwyg_composer/utils/selection.ts
+++ b/src/components/views/rooms/wysiwyg_composer/utils/selection.ts
@@ -19,9 +19,14 @@ import { SubSelection } from "../types";
export function setSelection(selection: SubSelection): Promise {
if (selection.anchorNode && selection.focusNode) {
const range = new Range();
- range.setStart(selection.anchorNode, selection.anchorOffset);
- range.setEnd(selection.focusNode, selection.focusOffset);
+ if (selection.isForward) {
+ range.setStart(selection.anchorNode, selection.anchorOffset);
+ range.setEnd(selection.focusNode, selection.focusOffset);
+ } else {
+ range.setStart(selection.focusNode, selection.focusOffset);
+ range.setEnd(selection.anchorNode, selection.anchorOffset);
+ }
document.getSelection()?.removeAllRanges();
document.getSelection()?.addRange(range);
}
diff --git a/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx
index 7f04bf21ffb..aa39de7a573 100644
--- a/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx
+++ b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx
@@ -299,6 +299,7 @@ describe("SendWysiwygComposer", () => {
anchorOffset: 2,
focusNode: textNode,
focusOffset: 2,
+ isForward: true,
});
// the event is not automatically fired by jest
document.dispatchEvent(new CustomEvent("selectionchange"));
@@ -308,6 +309,32 @@ describe("SendWysiwygComposer", () => {
// Then
await waitFor(() => expect(screen.getByRole("textbox")).toHaveTextContent(/wo🦫rd/));
});
+
+ it("Should add an emoji when a word is selected", async () => {
+ // When
+ screen.getByRole("textbox").focus();
+ screen.getByRole("textbox").innerHTML = "word";
+ fireEvent.input(screen.getByRole("textbox"), {
+ data: "word",
+ inputType: "insertText",
+ });
+
+ const textNode = screen.getByRole("textbox").firstChild;
+ await setSelection({
+ anchorNode: textNode,
+ anchorOffset: 3,
+ focusNode: textNode,
+ focusOffset: 2,
+ isForward: false,
+ });
+ // the event is not automatically fired by jest
+ document.dispatchEvent(new CustomEvent("selectionchange"));
+
+ emojiButton.click();
+
+ // Then
+ await waitFor(() => expect(screen.getByRole("textbox")).toHaveTextContent(/wo🦫d/));
+ });
},
);
});
diff --git a/test/components/views/rooms/wysiwyg_composer/components/LinkModal-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/LinkModal-test.tsx
index 527f7e7b3d6..852354f0ba5 100644
--- a/test/components/views/rooms/wysiwyg_composer/components/LinkModal-test.tsx
+++ b/test/components/views/rooms/wysiwyg_composer/components/LinkModal-test.tsx
@@ -35,6 +35,7 @@ describe("LinkModal", () => {
anchorNode: null,
focusOffset: 3,
anchorOffset: 4,
+ isForward: true,
};
const customRender = (isTextEnabled: boolean, onClose: () => void, isEditing = false) => {
From 5807c649907ddf38ac2e8ced522da343a71f68a9 Mon Sep 17 00:00:00 2001
From: Marco Bartelt
Date: Fri, 27 Jan 2023 08:57:24 +0100
Subject: [PATCH 06/97] Improve avatar settings accessibility (#9985)
Co-authored-by: Germain
---
.../views/settings/AvatarSetting.tsx | 27 ++++++---
.../views/settings/AvatarSetting-test.tsx | 55 +++++++++++++++++++
2 files changed, 73 insertions(+), 9 deletions(-)
create mode 100644 test/components/views/settings/AvatarSetting-test.tsx
diff --git a/src/components/views/settings/AvatarSetting.tsx b/src/components/views/settings/AvatarSetting.tsx
index 5c311195f40..33efe8f9d8c 100644
--- a/src/components/views/settings/AvatarSetting.tsx
+++ b/src/components/views/settings/AvatarSetting.tsx
@@ -14,17 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { useState } from "react";
+import React, { useRef, useState } from "react";
import classNames from "classnames";
import { _t } from "../../../languageHandler";
-import AccessibleButton from "../elements/AccessibleButton";
+import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
interface IProps {
avatarUrl?: string;
avatarName: string; // name of user/room the avatar belongs to
- uploadAvatar?: (e: React.MouseEvent) => void;
- removeAvatar?: (e: React.MouseEvent) => void;
+ uploadAvatar?: (e: ButtonEvent) => void;
+ removeAvatar?: (e: ButtonEvent) => void;
avatarAltText: string;
}
@@ -34,12 +34,16 @@ const AvatarSetting: React.FC = ({ avatarUrl, avatarAltText, avatarName,
onMouseEnter: () => setIsHovering(true),
onMouseLeave: () => setIsHovering(false),
};
+ // TODO: Use useId() as soon as we're using React 18.
+ // Prevents ID collisions when this component is used more than once on the same page.
+ const a11yId = useRef(`hover-text-${Math.random()}`);
let avatarElement = (
);
@@ -50,7 +54,7 @@ const AvatarSetting: React.FC = ({ avatarUrl, avatarAltText, avatarName,
src={avatarUrl}
alt={avatarAltText}
aria-label={avatarAltText}
- onClick={uploadAvatar}
+ onClick={uploadAvatar ?? null}
{...hoveringProps}
/>
);
@@ -60,7 +64,12 @@ const AvatarSetting: React.FC = ({ avatarUrl, avatarAltText, avatarName,
if (uploadAvatar) {
// insert an empty div to be the host for a css mask containing the upload.svg
uploadAvatarBtn = (
-
+
);
}
@@ -80,9 +89,9 @@ const AvatarSetting: React.FC = ({ avatarUrl, avatarAltText, avatarName,
return (
{avatarElement}
-
+
-
{_t("Upload")}
+
{_t("Upload")}
{uploadAvatarBtn}
{removeAvatarBtn}
diff --git a/test/components/views/settings/AvatarSetting-test.tsx b/test/components/views/settings/AvatarSetting-test.tsx
new file mode 100644
index 00000000000..d0efc0d06c6
--- /dev/null
+++ b/test/components/views/settings/AvatarSetting-test.tsx
@@ -0,0 +1,55 @@
+/*
+Copyright 2023 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+import React from "react";
+import { render } from "@testing-library/react";
+
+import AvatarSetting from "../../../../src/components/views/settings/AvatarSetting";
+
+describe("
", () => {
+ it("renders avatar with specified alt text", async () => {
+ const { queryByAltText } = render(
+
,
+ );
+
+ const imgElement = queryByAltText("Avatar of Peter Fox");
+ expect(imgElement).toBeInTheDocument();
+ });
+
+ it("renders avatar with remove button", async () => {
+ const { queryByText } = render(
+
,
+ );
+
+ const removeButton = queryByText("Remove");
+ expect(removeButton).toBeInTheDocument();
+ });
+
+ it("renders avatar without remove button", async () => {
+ const { queryByText } = render(
);
+
+ const removeButton = queryByText("Remove");
+ expect(removeButton).toBeNull();
+ });
+});
From b3586cf1941d906d13698f62a3560fd7cb0eb211 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Fri, 27 Jan 2023 10:57:46 +0000
Subject: [PATCH 07/97] Update sentry-javascript monorepo to v7.32.1 (#9896)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
yarn.lock | 65 ++++++++++++++++++++++++++++++++++---------------------
1 file changed, 40 insertions(+), 25 deletions(-)
diff --git a/yarn.lock b/yarn.lock
index d9d770a0330..6946cd23759 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1903,45 +1903,60 @@
integrity sha512-/nPyK4NCjFGYNVQ7vOivfuEYveOJhA4gWzB7w2PjCkw/Y3kCtu+axRpUiDPEybTz2H6RTvr+I526DbtUYguqVw==
"@sentry/browser@^7.0.0":
- version "7.23.0"
- resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.23.0.tgz#ca2a01ce2b00727036906158efaa1c7af1395cc0"
- integrity sha512-2/dLGOSaM5AvlRdMgYxDyxPxkUUqYyxF7QZ0NicdIXkKXa0fM38IdibeXrE8XzC7rF2B7DQZ6U7uDb1Yry60ig==
- dependencies:
- "@sentry/core" "7.23.0"
- "@sentry/types" "7.23.0"
- "@sentry/utils" "7.23.0"
+ version "7.34.0"
+ resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.34.0.tgz#6a521c5d95d535e6e89cf4eae85153f90c37d17a"
+ integrity sha512-5Jmjj0DLxx+31o12T+VH4U+gO7wz3L+ftjuTxcQaC8GeFVe5qCyXZoDmWKNV9NEyREiZ3azV62bJc5wojZrIIg==
+ dependencies:
+ "@sentry/core" "7.34.0"
+ "@sentry/replay" "7.34.0"
+ "@sentry/types" "7.34.0"
+ "@sentry/utils" "7.34.0"
tslib "^1.9.3"
-"@sentry/core@7.23.0":
- version "7.23.0"
- resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.23.0.tgz#d320b2b6e5620b41f345bc01d69b547cdf28f78d"
- integrity sha512-oNLGsscSdMs1urCbpwe868NsoJWyeTOQXOm5w2e78yE7G6zm2Ra473NQio3lweaEvjQgSGpFyEfAn/3ubZbtPw==
+"@sentry/core@7.34.0":
+ version "7.34.0"
+ resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.34.0.tgz#bfda8d386cf7343200aa9fb7a7a26e99b839fc0c"
+ integrity sha512-J1oxsYZX1N0tkEcaHt/uuDqk6zOnaivyampp+EvBsUMCdemjg7rwKvawlRB0ZtBEQu3HAhi8zecm03mlpWfCDw==
dependencies:
- "@sentry/types" "7.23.0"
- "@sentry/utils" "7.23.0"
+ "@sentry/types" "7.34.0"
+ "@sentry/utils" "7.34.0"
tslib "^1.9.3"
+"@sentry/replay@7.34.0":
+ version "7.34.0"
+ resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.34.0.tgz#66d63b1e04d7e8068cac0c623a607f470d000751"
+ integrity sha512-4L4YZfWt8mcVNcI99RxHORPb308URI1R9xsFj97fagk0ATjexLKr5QCA2ApnKaSn8Q0q1Zdzd4XmFtW9anU45Q==
+ dependencies:
+ "@sentry/core" "7.34.0"
+ "@sentry/types" "7.34.0"
+ "@sentry/utils" "7.34.0"
+
"@sentry/tracing@^7.0.0":
- version "7.23.0"
- resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.23.0.tgz#9b6c5d3761d7664b6e40c476912281589d7cbe43"
- integrity sha512-sbwvf6gjLgUTkBwZQOV7RkZPah7KnnpeVcwnNl+vigq6FNgNtejz53FFCo6t4mNGZSerfWbEy/c3C1LMX9AaXw==
+ version "7.34.0"
+ resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.34.0.tgz#bc084389cad4f1e8520311ac195b070eced72b40"
+ integrity sha512-JtfSWBfcWslfIujcpGEPF5oOiAOCd5shMoWYrdTvCfruHhYjp4w5kv/ndkvq2EpFkcQYhdmtQEytXEO8IJIqRw==
dependencies:
- "@sentry/core" "7.23.0"
- "@sentry/types" "7.23.0"
- "@sentry/utils" "7.23.0"
+ "@sentry/core" "7.34.0"
+ "@sentry/types" "7.34.0"
+ "@sentry/utils" "7.34.0"
tslib "^1.9.3"
-"@sentry/types@7.23.0", "@sentry/types@^7.2.0":
+"@sentry/types@7.34.0":
+ version "7.34.0"
+ resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.34.0.tgz#e0dc6a927dd13e4cacbca7bfee67a088885e8309"
+ integrity sha512-K+OeHIrl35PSYn6Zwqe4b8WWyAJQoI5NeWxHVkM7oQTGJ1YLG4BvLsR+UiUXnKdR5krE4EDtEA5jLsDlBEyPvw==
+
+"@sentry/types@^7.2.0":
version "7.23.0"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.23.0.tgz#5d2ce94d81d7c1fad702645306f3c0932708cad5"
integrity sha512-fZ5XfVRswVZhKoCutQ27UpIHP16tvyc6ws+xq+njHv8Jg8gFBCoOxlJxuFhegD2xxylAn1aiSHNAErFWdajbpA==
-"@sentry/utils@7.23.0":
- version "7.23.0"
- resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.23.0.tgz#5f38640fe49f5abac88f048b92d3e83375d7ddf7"
- integrity sha512-ad/XXH03MfgDH/7N7FjKEOVaKrfQWdMaE0nCxZCr2RrvlitlmGQmPpms95epr1CpzSU3BDRImlILx6+TlrXOgg==
+"@sentry/utils@7.34.0":
+ version "7.34.0"
+ resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.34.0.tgz#32fb6db8b352477d219ddff8200372959c68b445"
+ integrity sha512-VIHHXEBw0htzqxnU8A7WkXKvmsG2pZVqHlAn0H9W/yyFQtXMuP1j1i0NsjADB/3JXUKK83kTNWGzScXvp0o+Jg==
dependencies:
- "@sentry/types" "7.23.0"
+ "@sentry/types" "7.34.0"
tslib "^1.9.3"
"@sinclair/typebox@^0.24.1":
From 32bd350b7e531576067356a7d3e3622fca477bea Mon Sep 17 00:00:00 2001
From: Andy Balaam
Date: Fri, 27 Jan 2023 11:00:06 +0000
Subject: [PATCH 08/97] Tests for AdvancedRoomSettingsTab (#9994)
* Sort the methods in mkStubRoom
* Tests for AdvancedRoomSettingsTab
---
.../room/AdvancedRoomSettingsTab-test.tsx | 104 +++++++++++++++++
.../AdvancedRoomSettingsTab-test.tsx.snap | 55 +++++++++
test/test-utils/test-utils.ts | 105 +++++++++---------
3 files changed, 213 insertions(+), 51 deletions(-)
create mode 100644 test/components/views/settings/tabs/room/AdvancedRoomSettingsTab-test.tsx
create mode 100644 test/components/views/settings/tabs/room/__snapshots__/AdvancedRoomSettingsTab-test.tsx.snap
diff --git a/test/components/views/settings/tabs/room/AdvancedRoomSettingsTab-test.tsx b/test/components/views/settings/tabs/room/AdvancedRoomSettingsTab-test.tsx
new file mode 100644
index 00000000000..dafd555966e
--- /dev/null
+++ b/test/components/views/settings/tabs/room/AdvancedRoomSettingsTab-test.tsx
@@ -0,0 +1,104 @@
+/*
+Copyright 2023 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from "react";
+import { fireEvent, render, RenderResult } from "@testing-library/react";
+import { MatrixClient } from "matrix-js-sdk/src/client";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { mocked } from "jest-mock";
+import { EventType, MatrixEvent } from "matrix-js-sdk/src/matrix";
+
+import AdvancedRoomSettingsTab from "../../../../../../src/components/views/settings/tabs/room/AdvancedRoomSettingsTab";
+import { mkEvent, mkStubRoom, stubClient } from "../../../../../test-utils";
+import dis from "../../../../../../src/dispatcher/dispatcher";
+import { Action } from "../../../../../../src/dispatcher/actions";
+import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
+
+jest.mock("../../../../../../src/dispatcher/dispatcher");
+
+describe("AdvancedRoomSettingsTab", () => {
+ const roomId = "!room:example.com";
+ let cli: MatrixClient;
+ let room: Room;
+
+ const renderTab = (): RenderResult => {
+ return render();
+ };
+
+ beforeEach(() => {
+ stubClient();
+ cli = MatrixClientPeg.get();
+ room = mkStubRoom(roomId, "test room", cli);
+ mocked(cli.getRoom).mockReturnValue(room);
+ });
+
+ it("should render as expected", () => {
+ const tab = renderTab();
+ expect(tab.asFragment()).toMatchSnapshot();
+ });
+
+ it("should display room ID", () => {
+ const tab = renderTab();
+ tab.getByText(roomId);
+ });
+
+ it("should display room version", () => {
+ mocked(room.getVersion).mockReturnValue("custom_room_version_1");
+
+ const tab = renderTab();
+ tab.getByText("custom_room_version_1");
+ });
+
+ function mockStateEvents(room: Room) {
+ const createEvent = mkEvent({
+ event: true,
+ user: "@a:b.com",
+ type: EventType.RoomCreate,
+ content: { predecessor: { room_id: "old_room_id", event_id: "tombstone_event_id" } },
+ room: room.roomId,
+ });
+
+ type GetStateEvents2Args = (eventType: EventType | string, stateKey: string) => MatrixEvent | null;
+
+ const getStateEvents = jest.spyOn(
+ room.currentState,
+ "getStateEvents",
+ ) as unknown as jest.MockedFunction;
+
+ getStateEvents.mockImplementation((eventType: string | null, _key: string) => {
+ switch (eventType) {
+ case EventType.RoomCreate:
+ return createEvent;
+ default:
+ return null;
+ }
+ });
+ }
+
+ it("should link to predecessor room", async () => {
+ mockStateEvents(room);
+ const tab = renderTab();
+ const link = await tab.findByText("View older messages in test room.");
+ fireEvent.click(link);
+ expect(dis.dispatch).toHaveBeenCalledWith({
+ action: Action.ViewRoom,
+ event_id: "tombstone_event_id",
+ room_id: "old_room_id",
+ metricsTrigger: "WebPredecessorSettings",
+ metricsViaKeyboard: false,
+ });
+ });
+});
diff --git a/test/components/views/settings/tabs/room/__snapshots__/AdvancedRoomSettingsTab-test.tsx.snap b/test/components/views/settings/tabs/room/__snapshots__/AdvancedRoomSettingsTab-test.tsx.snap
new file mode 100644
index 00000000000..fa0aa0338e9
--- /dev/null
+++ b/test/components/views/settings/tabs/room/__snapshots__/AdvancedRoomSettingsTab-test.tsx.snap
@@ -0,0 +1,55 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`AdvancedRoomSettingsTab should render as expected 1`] = `
+
+
+
+ Advanced
+
+
+
+ Room information
+
+
+
+ Internal room ID
+
+
+
+
+
+
+ Room version
+
+
+
+ Room version:
+
+ 1
+
+
+
+
+`;
diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts
index 25a581ba6f7..ef7bc1dcef3 100644
--- a/test/test-utils/test-utils.ts
+++ b/test/test-utils/test-utils.ts
@@ -40,6 +40,7 @@ import { MediaHandler } from "matrix-js-sdk/src/webrtc/mediaHandler";
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
import { CryptoBackend } from "matrix-js-sdk/src/common-crypto/CryptoBackend";
import { IEventDecryptionResult } from "matrix-js-sdk/src/@types/crypto";
+import { MapperOpts } from "matrix-js-sdk/src/event-mapper";
import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
import { MatrixClientPeg as peg } from "../../src/MatrixClientPeg";
@@ -145,7 +146,7 @@ export function createTestClient(): MatrixClient {
content: {},
});
}),
- mxcUrlToHttp: (mxc) => `http://this.is.a.url/${mxc.substring(6)}`,
+ mxcUrlToHttp: (mxc: string) => `http://this.is.a.url/${mxc.substring(6)}`,
setAccountData: jest.fn(),
setRoomAccountData: jest.fn(),
setRoomTopic: jest.fn(),
@@ -200,7 +201,7 @@ export function createTestClient(): MatrixClient {
stopAllStreams: jest.fn(),
} as unknown as MediaHandler),
uploadContent: jest.fn(),
- getEventMapper: () => (opts) => new MatrixEvent(opts),
+ getEventMapper: (_options?: MapperOpts) => (event: Partial) => new MatrixEvent(event),
leaveRoomChain: jest.fn((roomId) => ({ [roomId]: null })),
doesServerSupportLogoutDevices: jest.fn().mockReturnValue(true),
requestPasswordEmailToken: jest.fn().mockRejectedValue({}),
@@ -476,34 +477,11 @@ export function mkMessage({
}
export function mkStubRoom(roomId: string = null, name: string, client: MatrixClient): Room {
- const stubTimeline = { getEvents: () => [] } as unknown as EventTimeline;
+ const stubTimeline = { getEvents: () => [] as MatrixEvent[] } as unknown as EventTimeline;
return {
- roomId,
- getReceiptsForEvent: jest.fn().mockReturnValue([]),
- getMember: jest.fn().mockReturnValue({
- userId: "@member:domain.bla",
- name: "Member",
- rawDisplayName: "Member",
- roomId: roomId,
- getAvatarUrl: () => "mxc://avatar.url/image.png",
- getMxcAvatarUrl: () => "mxc://avatar.url/image.png",
- }),
- getMembersWithMembership: jest.fn().mockReturnValue([]),
- getJoinedMembers: jest.fn().mockReturnValue([]),
- getJoinedMemberCount: jest.fn().mockReturnValue(1),
- getInvitedAndJoinedMemberCount: jest.fn().mockReturnValue(1),
- setUnreadNotificationCount: jest.fn(),
- getMembers: jest.fn().mockReturnValue([]),
- getPendingEvents: () => [],
- getLiveTimeline: jest.fn().mockReturnValue(stubTimeline),
- getUnfilteredTimelineSet: jest.fn(),
- findEventById: () => null,
- getAccountData: () => null,
- hasMembershipState: () => null,
- getVersion: () => "1",
- shouldUpgradeToVersion: () => null,
- getMyMembership: jest.fn().mockReturnValue("join"),
- maySendMessage: jest.fn().mockReturnValue(true),
+ canInvite: jest.fn(),
+ client,
+ createThreadsTimelineSets: jest.fn().mockReturnValue(new Promise(() => {})),
currentState: {
getStateEvents: jest.fn((_type, key) => (key === undefined ? [] : null)),
getMember: jest.fn(),
@@ -516,37 +494,62 @@ export function mkStubRoom(roomId: string = null, name: string, client: MatrixCl
on: jest.fn(),
off: jest.fn(),
} as unknown as RoomState,
- tags: {},
- setBlacklistUnverifiedDevices: jest.fn(),
- on: jest.fn(),
- off: jest.fn(),
- removeListener: jest.fn(),
- getDMInviter: jest.fn(),
- name,
- normalizedName: normalize(name || ""),
+ eventShouldLiveIn: jest.fn().mockReturnValue({}),
+ fetchRoomThreads: jest.fn().mockReturnValue(Promise.resolve()),
+ findEventById: (_: string) => undefined as MatrixEvent | undefined,
+ findPredecessor: jest.fn().mockReturnValue({ roomId: "", eventId: null }),
+ getAccountData: (_: EventType | string) => undefined as MatrixEvent | undefined,
+ getAltAliases: jest.fn().mockReturnValue([]),
getAvatarUrl: () => "mxc://avatar.url/room.png",
+ getCanonicalAlias: jest.fn(),
+ getDMInviter: jest.fn(),
+ getEventReadUpTo: jest.fn(() => null),
+ getInvitedAndJoinedMemberCount: jest.fn().mockReturnValue(1),
+ getJoinRule: jest.fn().mockReturnValue("invite"),
+ getJoinedMemberCount: jest.fn().mockReturnValue(1),
+ getJoinedMembers: jest.fn().mockReturnValue([]),
+ getLiveTimeline: jest.fn().mockReturnValue(stubTimeline),
+ getMember: jest.fn().mockReturnValue({
+ userId: "@member:domain.bla",
+ name: "Member",
+ rawDisplayName: "Member",
+ roomId: roomId,
+ getAvatarUrl: () => "mxc://avatar.url/image.png",
+ getMxcAvatarUrl: () => "mxc://avatar.url/image.png",
+ }),
+ getMembers: jest.fn().mockReturnValue([]),
+ getMembersWithMembership: jest.fn().mockReturnValue([]),
getMxcAvatarUrl: () => "mxc://avatar.url/room.png",
- isSpaceRoom: jest.fn().mockReturnValue(false),
+ getMyMembership: jest.fn().mockReturnValue("join"),
+ getPendingEvents: () => [] as MatrixEvent[],
+ getReceiptsForEvent: jest.fn().mockReturnValue([]),
+ getRecommendedVersion: jest.fn().mockReturnValue(Promise.resolve("")),
+ getThreads: jest.fn().mockReturnValue([]),
getType: jest.fn().mockReturnValue(undefined),
- isElementVideoRoom: jest.fn().mockReturnValue(false),
+ getUnfilteredTimelineSet: jest.fn(),
getUnreadNotificationCount: jest.fn(() => 0),
- getEventReadUpTo: jest.fn(() => null),
- getCanonicalAlias: jest.fn(),
- getAltAliases: jest.fn().mockReturnValue([]),
- timeline: [],
- getJoinRule: jest.fn().mockReturnValue("invite"),
+ getVersion: jest.fn().mockReturnValue("1"),
+ hasMembershipState: () => false,
+ isElementVideoRoom: jest.fn().mockReturnValue(false),
+ isSpaceRoom: jest.fn().mockReturnValue(false),
loadMembersIfNeeded: jest.fn(),
- client,
+ maySendMessage: jest.fn().mockReturnValue(true),
myUserId: client?.getUserId(),
- canInvite: jest.fn(),
- getThreads: jest.fn().mockReturnValue([]),
- eventShouldLiveIn: jest.fn().mockReturnValue({}),
- createThreadsTimelineSets: jest.fn().mockReturnValue(new Promise(() => {})),
- fetchRoomThreads: jest.fn().mockReturnValue(new Promise(() => {})),
+ name,
+ normalizedName: normalize(name || ""),
+ off: jest.fn(),
+ on: jest.fn(),
+ removeListener: jest.fn(),
+ roomId,
+ setBlacklistUnverifiedDevices: jest.fn(),
+ setUnreadNotificationCount: jest.fn(),
+ shouldUpgradeToVersion: (() => null) as () => string | null,
+ tags: {},
+ timeline: [],
} as unknown as Room;
}
-export function mkServerConfig(hsUrl, isUrl) {
+export function mkServerConfig(hsUrl: string, isUrl: string) {
return makeType(ValidatedServerConfig, {
hsUrl,
hsName: "TEST_ENVIRONMENT",
From d698193196c91e16385f898dab0ba6420a0bc567 Mon Sep 17 00:00:00 2001
From: Hugh Nimmo-Smith
Date: Fri, 27 Jan 2023 11:06:10 +0000
Subject: [PATCH 09/97] Implementation of MSC3824 to make the client OIDC-aware
(#8681)
---
src/BasePlatform.ts | 11 ++-
src/Lifecycle.ts | 3 +-
src/Login.ts | 12 ++-
src/components/structures/auth/Login.tsx | 4 +-
.../structures/auth/Registration.tsx | 3 +-
src/components/structures/auth/SoftLogout.tsx | 3 +-
src/components/views/elements/SSOButtons.tsx | 57 ++++++++++----
.../tabs/user/GeneralUserSettingsTab.tsx | 36 ++++++++-
src/i18n/strings/en_EN.json | 2 +
.../components/structures/auth/Login-test.tsx | 61 +++++++++++++++
.../tabs/user/GeneralUserSettingsTab-test.tsx | 77 +++++++++++++++++++
11 files changed, 240 insertions(+), 29 deletions(-)
create mode 100644 test/components/views/settings/tabs/user/GeneralUserSettingsTab-test.tsx
diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts
index 22d274ffb1e..ab8bca4b5fa 100644
--- a/src/BasePlatform.ts
+++ b/src/BasePlatform.ts
@@ -22,6 +22,7 @@ import { encodeUnpaddedBase64 } from "matrix-js-sdk/src/crypto/olmlib";
import { logger } from "matrix-js-sdk/src/logger";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room";
+import { SSOAction } from "matrix-js-sdk/src/@types/auth";
import dis from "./dispatcher/dispatcher";
import BaseEventIndexManager from "./indexing/BaseEventIndexManager";
@@ -308,9 +309,9 @@ export default abstract class BasePlatform {
return null;
}
- protected getSSOCallbackUrl(fragmentAfterLogin: string): URL {
+ protected getSSOCallbackUrl(fragmentAfterLogin = ""): URL {
const url = new URL(window.location.href);
- url.hash = fragmentAfterLogin || "";
+ url.hash = fragmentAfterLogin;
return url;
}
@@ -319,13 +320,15 @@ export default abstract class BasePlatform {
* @param {MatrixClient} mxClient the matrix client using which we should start the flow
* @param {"sso"|"cas"} loginType the type of SSO it is, CAS/SSO.
* @param {string} fragmentAfterLogin the hash to pass to the app during sso callback.
+ * @param {SSOAction} action the SSO flow to indicate to the IdP, optional.
* @param {string} idpId The ID of the Identity Provider being targeted, optional.
*/
public startSingleSignOn(
mxClient: MatrixClient,
loginType: "sso" | "cas",
- fragmentAfterLogin: string,
+ fragmentAfterLogin?: string,
idpId?: string,
+ action?: SSOAction,
): void {
// persist hs url and is url for when the user is returned to the app with the login token
localStorage.setItem(SSO_HOMESERVER_URL_KEY, mxClient.getHomeserverUrl());
@@ -336,7 +339,7 @@ export default abstract class BasePlatform {
localStorage.setItem(SSO_IDP_ID_KEY, idpId);
}
const callbackUrl = this.getSSOCallbackUrl(fragmentAfterLogin);
- window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType, idpId); // redirect to SSO
+ window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType, idpId, action); // redirect to SSO
}
/**
diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts
index 30aab429fb7..b28a7f80380 100644
--- a/src/Lifecycle.ts
+++ b/src/Lifecycle.ts
@@ -23,6 +23,7 @@ import { MatrixClient } from "matrix-js-sdk/src/client";
import { decryptAES, encryptAES, IEncryptedPayload } from "matrix-js-sdk/src/crypto/aes";
import { QueryDict } from "matrix-js-sdk/src/utils";
import { logger } from "matrix-js-sdk/src/logger";
+import { SSOAction } from "matrix-js-sdk/src/@types/auth";
import { IMatrixClientCreds, MatrixClientPeg } from "./MatrixClientPeg";
import SecurityCustomisations from "./customisations/Security";
@@ -248,7 +249,7 @@ export function attemptTokenLogin(
idBaseUrl: identityServer,
});
const idpId = localStorage.getItem(SSO_IDP_ID_KEY) || undefined;
- PlatformPeg.get().startSingleSignOn(cli, "sso", fragmentAfterLogin, idpId);
+ PlatformPeg.get()?.startSingleSignOn(cli, "sso", fragmentAfterLogin, idpId, SSOAction.LOGIN);
}
},
});
diff --git a/src/Login.ts b/src/Login.ts
index 90f8f5d0eb6..6475a9f5c93 100644
--- a/src/Login.ts
+++ b/src/Login.ts
@@ -19,7 +19,7 @@ limitations under the License.
import { createClient } from "matrix-js-sdk/src/matrix";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { logger } from "matrix-js-sdk/src/logger";
-import { ILoginParams, LoginFlow } from "matrix-js-sdk/src/@types/auth";
+import { DELEGATED_OIDC_COMPATIBILITY, ILoginParams, LoginFlow } from "matrix-js-sdk/src/@types/auth";
import { IMatrixClientCreds } from "./MatrixClientPeg";
import SecurityCustomisations from "./customisations/Security";
@@ -32,7 +32,6 @@ export default class Login {
private hsUrl: string;
private isUrl: string;
private fallbackHsUrl: string;
- // TODO: Flows need a type in JS SDK
private flows: Array;
private defaultDeviceDisplayName: string;
private tempClient: MatrixClient;
@@ -81,8 +80,13 @@ export default class Login {
public async getFlows(): Promise> {
const client = this.createTemporaryClient();
- const { flows } = await client.loginFlows();
- this.flows = flows;
+ const { flows }: { flows: LoginFlow[] } = await client.loginFlows();
+ // If an m.login.sso flow is present which is also flagged as being for MSC3824 OIDC compatibility then we only
+ // return that flow as (per MSC3824) it is the only one that the user should be offered to give the best experience
+ const oidcCompatibilityFlow = flows.find(
+ (f) => f.type === "m.login.sso" && DELEGATED_OIDC_COMPATIBILITY.findIn(f),
+ );
+ this.flows = oidcCompatibilityFlow ? [oidcCompatibilityFlow] : flows;
return this.flows;
}
diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx
index c857b96fe50..4cbe0f5bc60 100644
--- a/src/components/structures/auth/Login.tsx
+++ b/src/components/structures/auth/Login.tsx
@@ -18,7 +18,7 @@ import React, { ReactNode } from "react";
import { ConnectionError, MatrixError } from "matrix-js-sdk/src/http-api";
import classNames from "classnames";
import { logger } from "matrix-js-sdk/src/logger";
-import { ISSOFlow, LoginFlow } from "matrix-js-sdk/src/@types/auth";
+import { ISSOFlow, LoginFlow, SSOAction } from "matrix-js-sdk/src/@types/auth";
import { _t, _td } from "../../../languageHandler";
import Login from "../../../Login";
@@ -345,6 +345,7 @@ export default class LoginComponent extends React.PureComponent
this.loginLogic.createTemporaryClient(),
ssoKind,
this.props.fragmentAfterLogin,
+ SSOAction.REGISTER,
);
} else {
// Don't intercept - just go through to the register page
@@ -549,6 +550,7 @@ export default class LoginComponent extends React.PureComponent
loginType={loginType}
fragmentAfterLogin={this.props.fragmentAfterLogin}
primary={!this.state.flows.find((flow) => flow.type === "m.login.password")}
+ action={SSOAction.LOGIN}
/>
);
};
diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx
index f2c2314b02d..aac39334a0d 100644
--- a/src/components/structures/auth/Registration.tsx
+++ b/src/components/structures/auth/Registration.tsx
@@ -19,7 +19,7 @@ import React, { Fragment, ReactNode } from "react";
import { IRequestTokenResponse, MatrixClient } from "matrix-js-sdk/src/client";
import classNames from "classnames";
import { logger } from "matrix-js-sdk/src/logger";
-import { ISSOFlow } from "matrix-js-sdk/src/@types/auth";
+import { ISSOFlow, SSOAction } from "matrix-js-sdk/src/@types/auth";
import { _t, _td } from "../../../languageHandler";
import { messageForResourceLimitError } from "../../../utils/ErrorUtils";
@@ -539,6 +539,7 @@ export default class Registration extends React.Component {
flow={this.state.ssoFlow}
loginType={this.state.ssoFlow.type === "m.login.sso" ? "sso" : "cas"}
fragmentAfterLogin={this.props.fragmentAfterLogin}
+ action={SSOAction.REGISTER}
/>
{_t("%(ssoButtons)s Or %(usernamePassword)s", {
diff --git a/src/components/structures/auth/SoftLogout.tsx b/src/components/structures/auth/SoftLogout.tsx
index 5eabfa0956c..d6ad4bfb165 100644
--- a/src/components/structures/auth/SoftLogout.tsx
+++ b/src/components/structures/auth/SoftLogout.tsx
@@ -17,7 +17,7 @@ limitations under the License.
import React from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { Optional } from "matrix-events-sdk";
-import { ISSOFlow, LoginFlow } from "matrix-js-sdk/src/@types/auth";
+import { ISSOFlow, LoginFlow, SSOAction } from "matrix-js-sdk/src/@types/auth";
import { _t } from "../../../languageHandler";
import dis from "../../../dispatcher/dispatcher";
@@ -256,6 +256,7 @@ export default class SoftLogout extends React.Component {
loginType={loginType}
fragmentAfterLogin={this.props.fragmentAfterLogin}
primary={!this.state.flows.find((flow) => flow.type === "m.login.password")}
+ action={SSOAction.LOGIN}
/>
);
diff --git a/src/components/views/elements/SSOButtons.tsx b/src/components/views/elements/SSOButtons.tsx
index ab9ebc29562..0dffacb7ce9 100644
--- a/src/components/views/elements/SSOButtons.tsx
+++ b/src/components/views/elements/SSOButtons.tsx
@@ -19,7 +19,13 @@ import { chunk } from "lodash";
import classNames from "classnames";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { Signup } from "@matrix-org/analytics-events/types/typescript/Signup";
-import { IdentityProviderBrand, IIdentityProvider, ISSOFlow } from "matrix-js-sdk/src/@types/auth";
+import {
+ IdentityProviderBrand,
+ IIdentityProvider,
+ ISSOFlow,
+ DELEGATED_OIDC_COMPATIBILITY,
+ SSOAction,
+} from "matrix-js-sdk/src/@types/auth";
import PlatformPeg from "../../../PlatformPeg";
import AccessibleButton from "./AccessibleButton";
@@ -28,9 +34,10 @@ import AccessibleTooltipButton from "./AccessibleTooltipButton";
import { mediaFromMxc } from "../../../customisations/Media";
import { PosthogAnalytics } from "../../../PosthogAnalytics";
-interface ISSOButtonProps extends Omit
{
+interface ISSOButtonProps extends IProps {
idp?: IIdentityProvider;
mini?: boolean;
+ action?: SSOAction;
}
const getIcon = (brand: IdentityProviderBrand | string): string | null => {
@@ -79,20 +86,29 @@ const SSOButton: React.FC = ({
idp,
primary,
mini,
+ action,
+ flow,
...props
}) => {
- const label = idp ? _t("Continue with %(provider)s", { provider: idp.name }) : _t("Sign in with single sign-on");
+ let label: string;
+ if (idp) {
+ label = _t("Continue with %(provider)s", { provider: idp.name });
+ } else if (DELEGATED_OIDC_COMPATIBILITY.findIn(flow)) {
+ label = _t("Continue");
+ } else {
+ label = _t("Sign in with single sign-on");
+ }
const onClick = (): void => {
const authenticationType = getAuthenticationType(idp?.brand ?? "");
PosthogAnalytics.instance.setAuthenticationType(authenticationType);
- PlatformPeg.get().startSingleSignOn(matrixClient, loginType, fragmentAfterLogin, idp?.id);
+ PlatformPeg.get()?.startSingleSignOn(matrixClient, loginType, fragmentAfterLogin, idp?.id, action);
};
- let icon;
- let brandClass;
- const brandIcon = idp ? getIcon(idp.brand) : null;
- if (brandIcon) {
+ let icon: JSX.Element | undefined;
+ let brandClass: string | undefined;
+ const brandIcon = idp?.brand ? getIcon(idp.brand) : null;
+ if (idp?.brand && brandIcon) {
const brandName = idp.brand.split(".").pop();
brandClass = `mx_SSOButton_brand_${brandName}`;
icon = ;
@@ -101,12 +117,16 @@ const SSOButton: React.FC = ({
icon = ;
}
- const classes = classNames("mx_SSOButton", {
- [brandClass]: brandClass,
- mx_SSOButton_mini: mini,
- mx_SSOButton_default: !idp,
- mx_SSOButton_primary: primary,
- });
+ const brandPart = brandClass ? { [brandClass]: brandClass } : undefined;
+ const classes = classNames(
+ "mx_SSOButton",
+ {
+ mx_SSOButton_mini: mini,
+ mx_SSOButton_default: !idp,
+ mx_SSOButton_primary: primary,
+ },
+ brandPart,
+ );
if (mini) {
// TODO fallback icon
@@ -128,14 +148,15 @@ const SSOButton: React.FC = ({
interface IProps {
matrixClient: MatrixClient;
flow: ISSOFlow;
- loginType?: "sso" | "cas";
+ loginType: "sso" | "cas";
fragmentAfterLogin?: string;
primary?: boolean;
+ action?: SSOAction;
}
const MAX_PER_ROW = 6;
-const SSOButtons: React.FC = ({ matrixClient, flow, loginType, fragmentAfterLogin, primary }) => {
+const SSOButtons: React.FC = ({ matrixClient, flow, loginType, fragmentAfterLogin, primary, action }) => {
const providers = flow.identity_providers || [];
if (providers.length < 2) {
return (
@@ -146,6 +167,8 @@ const SSOButtons: React.FC = ({ matrixClient, flow, loginType, fragmentA
fragmentAfterLogin={fragmentAfterLogin}
idp={providers[0]}
primary={primary}
+ action={action}
+ flow={flow}
/>
);
@@ -167,6 +190,8 @@ const SSOButtons: React.FC = ({ matrixClient, flow, loginType, fragmentA
idp={idp}
mini={true}
primary={primary}
+ action={action}
+ flow={flow}
/>
))}
diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx
index 89d070210bd..b1744e80cca 100644
--- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx
+++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx
@@ -20,6 +20,7 @@ import React from "react";
import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types";
import { IThreepid } from "matrix-js-sdk/src/@types/threepids";
import { logger } from "matrix-js-sdk/src/logger";
+import { IDelegatedAuthConfig, M_AUTHENTICATION } from "matrix-js-sdk/src/matrix";
import { _t } from "../../../../../languageHandler";
import ProfileSettings from "../../ProfileSettings";
@@ -79,6 +80,7 @@ interface IState {
loading3pids: boolean; // whether or not the emails and msisdns have been loaded
canChangePassword: boolean;
idServerName: string;
+ externalAccountManagementUrl?: string;
}
export default class GeneralUserSettingsTab extends React.Component {
@@ -106,6 +108,7 @@ export default class GeneralUserSettingsTab extends React.Component(cli.getClientWellKnown());
+ const externalAccountManagementUrl = delegatedAuthConfig?.account;
+
+ this.setState({ serverSupportsSeparateAddAndBind, canChangePassword, externalAccountManagementUrl });
}
private async getThreepidState(): Promise {
@@ -348,9 +354,37 @@ export default class GeneralUserSettingsTab extends React.Component
+
+ {_t(
+ "Your account details are managed separately at %(hostname)s
.",
+ { hostname },
+ { code: (sub) => {sub}
},
+ )}
+
+
+ {_t("Manage account")}
+
+ >
+ );
+ }
return (
{_t("Account")}
+ {externalAccountManagement}
{passwordChangeText}
{passwordChangeForm}
{threepidSection}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index d2ca5a48b00..45db7cb23f2 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1532,6 +1532,8 @@
"Email addresses": "Email addresses",
"Phone numbers": "Phone numbers",
"Set a new account password...": "Set a new account password...",
+ "Your account details are managed separately at
%(hostname)s
.": "Your account details are managed separately at
%(hostname)s
.",
+ "Manage account": "Manage account",
"Account": "Account",
"Language and region": "Language and region",
"Spell check": "Spell check",
diff --git a/test/components/structures/auth/Login-test.tsx b/test/components/structures/auth/Login-test.tsx
index 48df7f410fc..25f3263749f 100644
--- a/test/components/structures/auth/Login-test.tsx
+++ b/test/components/structures/auth/Login-test.tsx
@@ -19,6 +19,7 @@ import { fireEvent, render, screen, waitForElementToBeRemoved } from "@testing-l
import { mocked, MockedObject } from "jest-mock";
import { createClient, MatrixClient } from "matrix-js-sdk/src/matrix";
import fetchMock from "fetch-mock-jest";
+import { DELEGATED_OIDC_COMPATIBILITY, IdentityProviderBrand } from "matrix-js-sdk/src/@types/auth";
import SdkConfig from "../../../../src/SdkConfig";
import { mkServerConfig, mockPlatformPeg, unmockPlatformPeg } from "../../../test-utils";
@@ -192,4 +193,64 @@ describe("Login", function () {
fireEvent.click(container.querySelector(".mx_SSOButton"));
expect(platform.startSingleSignOn.mock.calls[1][0].baseUrl).toBe("https://server2");
});
+
+ it("should show single Continue button if OIDC MSC3824 compatibility is given by server", async () => {
+ mockClient.loginFlows.mockResolvedValue({
+ flows: [
+ {
+ type: "m.login.sso",
+ [DELEGATED_OIDC_COMPATIBILITY.name]: true,
+ },
+ {
+ type: "m.login.password",
+ },
+ ],
+ });
+
+ const { container } = getComponent();
+ await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading..."));
+
+ const ssoButtons = container.querySelectorAll(".mx_SSOButton");
+
+ expect(ssoButtons.length).toBe(1);
+ expect(ssoButtons[0].textContent).toBe("Continue");
+
+ // no password form visible
+ expect(container.querySelector("form")).toBeFalsy();
+ });
+
+ it("should show branded SSO buttons", async () => {
+ const idpsWithIcons = Object.values(IdentityProviderBrand).map((brand) => ({
+ id: brand,
+ brand,
+ name: `Provider ${brand}`,
+ }));
+
+ mockClient.loginFlows.mockResolvedValue({
+ flows: [
+ {
+ type: "m.login.sso",
+ identity_providers: [
+ ...idpsWithIcons,
+ {
+ id: "foo",
+ name: "Provider foo",
+ },
+ ],
+ },
+ ],
+ });
+
+ const { container } = getComponent();
+ await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading..."));
+
+ for (const idp of idpsWithIcons) {
+ const ssoButton = container.querySelector(`.mx_SSOButton.mx_SSOButton_brand_${idp.brand}`);
+ expect(ssoButton).toBeTruthy();
+ expect(ssoButton?.querySelector(`img[alt="${idp.brand}"]`)).toBeTruthy();
+ }
+
+ const ssoButtons = container.querySelectorAll(".mx_SSOButton");
+ expect(ssoButtons.length).toBe(idpsWithIcons.length + 1);
+ });
});
diff --git a/test/components/views/settings/tabs/user/GeneralUserSettingsTab-test.tsx b/test/components/views/settings/tabs/user/GeneralUserSettingsTab-test.tsx
new file mode 100644
index 00000000000..863f1fbe557
--- /dev/null
+++ b/test/components/views/settings/tabs/user/GeneralUserSettingsTab-test.tsx
@@ -0,0 +1,77 @@
+/*
+Copyright 2023 The Matrix.org Foundation C.I.C.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+import { render } from "@testing-library/react";
+import React from "react";
+import { M_AUTHENTICATION } from "matrix-js-sdk/src/matrix";
+
+import GeneralUserSettingsTab from "../../../../../../src/components/views/settings/tabs/user/GeneralUserSettingsTab";
+import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
+import SettingsStore from "../../../../../../src/settings/SettingsStore";
+import {
+ getMockClientWithEventEmitter,
+ mockClientMethodsServer,
+ mockClientMethodsUser,
+ mockPlatformPeg,
+ flushPromises,
+} from "../../../../../test-utils";
+
+describe("
", () => {
+ const defaultProps = {
+ closeSettingsFn: jest.fn(),
+ };
+
+ const userId = "@alice:server.org";
+ const mockClient = getMockClientWithEventEmitter({
+ ...mockClientMethodsUser(userId),
+ ...mockClientMethodsServer(),
+ });
+
+ const getComponent = () => (
+
+
+
+ );
+
+ jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
+ const clientWellKnownSpy = jest.spyOn(mockClient, "getClientWellKnown");
+
+ beforeEach(() => {
+ mockPlatformPeg();
+ jest.clearAllMocks();
+ clientWellKnownSpy.mockReturnValue({});
+ });
+
+ it("does not show account management link when not available", () => {
+ const { queryByTestId } = render(getComponent());
+
+ expect(queryByTestId("external-account-management-outer")).toBeFalsy();
+ expect(queryByTestId("external-account-management-link")).toBeFalsy();
+ });
+
+ it("show account management link in expected format", async () => {
+ const accountManagementLink = "https://id.server.org/my-account";
+ clientWellKnownSpy.mockReturnValue({
+ [M_AUTHENTICATION.name]: {
+ issuer: "https://id.server.org",
+ account: accountManagementLink,
+ },
+ });
+ const { getByTestId } = render(getComponent());
+
+ // wait for well-known call to settle
+ await flushPromises();
+
+ expect(getByTestId("external-account-management-outer").textContent).toMatch(/.*id\.server\.org/);
+ expect(getByTestId("external-account-management-link").getAttribute("href")).toMatch(accountManagementLink);
+ });
+});
From a69b805156126708c78e2a0ed7c5fab3919a7f82 Mon Sep 17 00:00:00 2001
From: Andy Balaam
Date: Fri, 27 Jan 2023 12:17:31 +0000
Subject: [PATCH 10/97] Revert "Update sentry-javascript monorepo to v7.32.1
(#9896)"
This update caused "Cannot redeclare block-scoped variable '__DEBUG_BUILD__' " error
This reverts commit b3586cf1941d906d13698f62a3560fd7cb0eb211.
---
yarn.lock | 65 +++++++++++++++++++++----------------------------------
1 file changed, 25 insertions(+), 40 deletions(-)
diff --git a/yarn.lock b/yarn.lock
index 6946cd23759..d9d770a0330 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1903,60 +1903,45 @@
integrity sha512-/nPyK4NCjFGYNVQ7vOivfuEYveOJhA4gWzB7w2PjCkw/Y3kCtu+axRpUiDPEybTz2H6RTvr+I526DbtUYguqVw==
"@sentry/browser@^7.0.0":
- version "7.34.0"
- resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.34.0.tgz#6a521c5d95d535e6e89cf4eae85153f90c37d17a"
- integrity sha512-5Jmjj0DLxx+31o12T+VH4U+gO7wz3L+ftjuTxcQaC8GeFVe5qCyXZoDmWKNV9NEyREiZ3azV62bJc5wojZrIIg==
- dependencies:
- "@sentry/core" "7.34.0"
- "@sentry/replay" "7.34.0"
- "@sentry/types" "7.34.0"
- "@sentry/utils" "7.34.0"
- tslib "^1.9.3"
-
-"@sentry/core@7.34.0":
- version "7.34.0"
- resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.34.0.tgz#bfda8d386cf7343200aa9fb7a7a26e99b839fc0c"
- integrity sha512-J1oxsYZX1N0tkEcaHt/uuDqk6zOnaivyampp+EvBsUMCdemjg7rwKvawlRB0ZtBEQu3HAhi8zecm03mlpWfCDw==
+ version "7.23.0"
+ resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.23.0.tgz#ca2a01ce2b00727036906158efaa1c7af1395cc0"
+ integrity sha512-2/dLGOSaM5AvlRdMgYxDyxPxkUUqYyxF7QZ0NicdIXkKXa0fM38IdibeXrE8XzC7rF2B7DQZ6U7uDb1Yry60ig==
dependencies:
- "@sentry/types" "7.34.0"
- "@sentry/utils" "7.34.0"
+ "@sentry/core" "7.23.0"
+ "@sentry/types" "7.23.0"
+ "@sentry/utils" "7.23.0"
tslib "^1.9.3"
-"@sentry/replay@7.34.0":
- version "7.34.0"
- resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.34.0.tgz#66d63b1e04d7e8068cac0c623a607f470d000751"
- integrity sha512-4L4YZfWt8mcVNcI99RxHORPb308URI1R9xsFj97fagk0ATjexLKr5QCA2ApnKaSn8Q0q1Zdzd4XmFtW9anU45Q==
+"@sentry/core@7.23.0":
+ version "7.23.0"
+ resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.23.0.tgz#d320b2b6e5620b41f345bc01d69b547cdf28f78d"
+ integrity sha512-oNLGsscSdMs1urCbpwe868NsoJWyeTOQXOm5w2e78yE7G6zm2Ra473NQio3lweaEvjQgSGpFyEfAn/3ubZbtPw==
dependencies:
- "@sentry/core" "7.34.0"
- "@sentry/types" "7.34.0"
- "@sentry/utils" "7.34.0"
+ "@sentry/types" "7.23.0"
+ "@sentry/utils" "7.23.0"
+ tslib "^1.9.3"
"@sentry/tracing@^7.0.0":
- version "7.34.0"
- resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.34.0.tgz#bc084389cad4f1e8520311ac195b070eced72b40"
- integrity sha512-JtfSWBfcWslfIujcpGEPF5oOiAOCd5shMoWYrdTvCfruHhYjp4w5kv/ndkvq2EpFkcQYhdmtQEytXEO8IJIqRw==
+ version "7.23.0"
+ resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.23.0.tgz#9b6c5d3761d7664b6e40c476912281589d7cbe43"
+ integrity sha512-sbwvf6gjLgUTkBwZQOV7RkZPah7KnnpeVcwnNl+vigq6FNgNtejz53FFCo6t4mNGZSerfWbEy/c3C1LMX9AaXw==
dependencies:
- "@sentry/core" "7.34.0"
- "@sentry/types" "7.34.0"
- "@sentry/utils" "7.34.0"
+ "@sentry/core" "7.23.0"
+ "@sentry/types" "7.23.0"
+ "@sentry/utils" "7.23.0"
tslib "^1.9.3"
-"@sentry/types@7.34.0":
- version "7.34.0"
- resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.34.0.tgz#e0dc6a927dd13e4cacbca7bfee67a088885e8309"
- integrity sha512-K+OeHIrl35PSYn6Zwqe4b8WWyAJQoI5NeWxHVkM7oQTGJ1YLG4BvLsR+UiUXnKdR5krE4EDtEA5jLsDlBEyPvw==
-
-"@sentry/types@^7.2.0":
+"@sentry/types@7.23.0", "@sentry/types@^7.2.0":
version "7.23.0"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.23.0.tgz#5d2ce94d81d7c1fad702645306f3c0932708cad5"
integrity sha512-fZ5XfVRswVZhKoCutQ27UpIHP16tvyc6ws+xq+njHv8Jg8gFBCoOxlJxuFhegD2xxylAn1aiSHNAErFWdajbpA==
-"@sentry/utils@7.34.0":
- version "7.34.0"
- resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.34.0.tgz#32fb6db8b352477d219ddff8200372959c68b445"
- integrity sha512-VIHHXEBw0htzqxnU8A7WkXKvmsG2pZVqHlAn0H9W/yyFQtXMuP1j1i0NsjADB/3JXUKK83kTNWGzScXvp0o+Jg==
+"@sentry/utils@7.23.0":
+ version "7.23.0"
+ resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.23.0.tgz#5f38640fe49f5abac88f048b92d3e83375d7ddf7"
+ integrity sha512-ad/XXH03MfgDH/7N7FjKEOVaKrfQWdMaE0nCxZCr2RrvlitlmGQmPpms95epr1CpzSU3BDRImlILx6+TlrXOgg==
dependencies:
- "@sentry/types" "7.34.0"
+ "@sentry/types" "7.23.0"
tslib "^1.9.3"
"@sinclair/typebox@^0.24.1":
From e0e149be3de8364127f3dce6844bc3bc3686dbeb Mon Sep 17 00:00:00 2001
From: Johannes Marbach
Date: Fri, 27 Jan 2023 14:58:38 +0100
Subject: [PATCH 11/97] Ensure room is actually in space hierarchy when
resolving its latest version
Relates to: vector-im/element-web#24329
---
src/components/structures/SpaceHierarchy.tsx | 15 ++++++++++++---
1 file changed, 12 insertions(+), 3 deletions(-)
diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx
index af6f2983826..f99cf73f5f5 100644
--- a/src/components/structures/SpaceHierarchy.tsx
+++ b/src/components/structures/SpaceHierarchy.tsx
@@ -413,9 +413,18 @@ interface IHierarchyLevelProps {
onToggleClick?(parentId: string, childId: string): void;
}
-const toLocalRoom = (cli: MatrixClient, room: IHierarchyRoom): IHierarchyRoom => {
+const toLocalRoom = (cli: MatrixClient, room: IHierarchyRoom, hierarchy: RoomHierarchy): IHierarchyRoom => {
const history = cli.getRoomUpgradeHistory(room.room_id, true);
- const cliRoom = history[history.length - 1];
+
+ // Pick latest room that is actually part of the hierarchy
+ let cliRoom = null;
+ for (let idx = history.length - 1; idx >= 0; --idx) {
+ if (hierarchy.roomMap[history[idx].roomId]) {
+ cliRoom = history[idx];
+ break;
+ }
+ }
+
if (cliRoom) {
return {
...room,
@@ -461,7 +470,7 @@ export const HierarchyLevel: React.FC = ({
(result, ev: IHierarchyRelation) => {
const room = hierarchy.roomMap.get(ev.state_key);
if (room && roomSet.has(room)) {
- result[room.room_type === RoomType.Space ? 0 : 1].push(toLocalRoom(cli, room));
+ result[room.room_type === RoomType.Space ? 0 : 1].push(toLocalRoom(cli, room, hierarchy));
}
return result;
},
From d84509d8d335d96cb865773f45a29924c923fccf Mon Sep 17 00:00:00 2001
From: Andy Balaam
Date: Fri, 27 Jan 2023 14:07:05 +0000
Subject: [PATCH 12/97] Implement MSC3946 for AdvancedRoomSettingsTab (#9995)
---
.../tabs/room/AdvancedRoomSettingsTab.tsx | 20 +++++----
src/i18n/strings/en_EN.json | 2 +
src/settings/Settings.tsx | 9 ++++
.../room/AdvancedRoomSettingsTab-test.tsx | 42 +++++++++++++++++++
4 files changed, 64 insertions(+), 9 deletions(-)
diff --git a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx
index 809da069baf..d9acba8524a 100644
--- a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx
+++ b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx
@@ -19,13 +19,14 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
import { _t } from "../../../../../languageHandler";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
-import AccessibleButton from "../../../elements/AccessibleButton";
+import AccessibleButton, { ButtonEvent } from "../../../elements/AccessibleButton";
import RoomUpgradeDialog from "../../../dialogs/RoomUpgradeDialog";
import Modal from "../../../../../Modal";
import dis from "../../../../../dispatcher/dispatcher";
import { Action } from "../../../../../dispatcher/actions";
import CopyableText from "../../../elements/CopyableText";
import { ViewRoomPayload } from "../../../../../dispatcher/payloads/ViewRoomPayload";
+import SettingsStore from "../../../../../settings/SettingsStore";
interface IProps {
roomId: string;
@@ -46,9 +47,11 @@ interface IState {
}
export default class AdvancedRoomSettingsTab extends React.Component {
- public constructor(props, context) {
+ public constructor(props: IProps, context: any) {
super(props, context);
+ const msc3946ProcessDynamicPredecessor = SettingsStore.getValue("feature_dynamic_room_predecessors");
+
this.state = {
// This is eventually set to the value of room.getRecommendedVersion()
upgradeRecommendation: null,
@@ -60,11 +63,10 @@ export default class AdvancedRoomSettingsTab extends React.Component = {};
- const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, "");
- const predecessor = createEvent ? createEvent.getContent().predecessor : null;
- if (predecessor && predecessor.room_id) {
- additionalStateChanges.oldRoomId = predecessor.room_id;
- additionalStateChanges.oldEventId = predecessor.event_id;
+ const predecessor = room.findPredecessor(msc3946ProcessDynamicPredecessor);
+ if (predecessor) {
+ additionalStateChanges.oldRoomId = predecessor.roomId;
+ additionalStateChanges.oldEventId = predecessor.eventId;
}
this.setState({
@@ -75,12 +77,12 @@ export default class AdvancedRoomSettingsTab extends React.Component {
+ private upgradeRoom = (): void => {
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
Modal.createDialog(RoomUpgradeDialog, { room });
};
- private onOldRoomClicked = (e): void => {
+ private onOldRoomClicked = (e: ButtonEvent): void => {
e.preventDefault();
e.stopPropagation();
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 45db7cb23f2..6d315bf38d2 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -955,6 +955,8 @@
"New group call experience": "New group call experience",
"Live Location Sharing": "Live Location Sharing",
"Temporary implementation. Locations persist in room history.": "Temporary implementation. Locations persist in room history.",
+ "Dynamic room predecessors": "Dynamic room predecessors",
+ "Enable MSC3946 (to support late-arriving room archives)": "Enable MSC3946 (to support late-arriving room archives)",
"Favourite Messages": "Favourite Messages",
"Under active development.": "Under active development.",
"Force 15s voice broadcast chunk length": "Force 15s voice broadcast chunk length",
diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx
index bfd40e96ce1..fdd3a857c12 100644
--- a/src/settings/Settings.tsx
+++ b/src/settings/Settings.tsx
@@ -439,6 +439,15 @@ export const SETTINGS: { [setting: string]: ISetting } = {
shouldWarn: true,
default: false,
},
+ "feature_dynamic_room_predecessors": {
+ isFeature: true,
+ labsGroup: LabGroup.Rooms,
+ supportedLevels: LEVELS_FEATURE,
+ displayName: _td("Dynamic room predecessors"),
+ description: _td("Enable MSC3946 (to support late-arriving room archives)"),
+ shouldWarn: true,
+ default: false,
+ },
"feature_favourite_messages": {
isFeature: true,
labsGroup: LabGroup.Messaging,
diff --git a/test/components/views/settings/tabs/room/AdvancedRoomSettingsTab-test.tsx b/test/components/views/settings/tabs/room/AdvancedRoomSettingsTab-test.tsx
index dafd555966e..cb4b373e4fb 100644
--- a/test/components/views/settings/tabs/room/AdvancedRoomSettingsTab-test.tsx
+++ b/test/components/views/settings/tabs/room/AdvancedRoomSettingsTab-test.tsx
@@ -26,6 +26,7 @@ import { mkEvent, mkStubRoom, stubClient } from "../../../../../test-utils";
import dis from "../../../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../../../src/dispatcher/actions";
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
+import SettingsStore from "../../../../../../src/settings/SettingsStore";
jest.mock("../../../../../../src/dispatcher/dispatcher");
@@ -43,6 +44,12 @@ describe("AdvancedRoomSettingsTab", () => {
cli = MatrixClientPeg.get();
room = mkStubRoom(roomId, "test room", cli);
mocked(cli.getRoom).mockReturnValue(room);
+ mocked(dis.dispatch).mockReset();
+ mocked(room.findPredecessor).mockImplementation((msc3946: boolean) =>
+ msc3946
+ ? { roomId: "old_room_id_via_predecessor", eventId: null }
+ : { roomId: "old_room_id", eventId: "tombstone_event_id" },
+ );
});
it("should render as expected", () => {
@@ -71,6 +78,17 @@ describe("AdvancedRoomSettingsTab", () => {
room: room.roomId,
});
+ // Because we're mocking Room.findPredecessor, it may not be necessary
+ // to provide the actual event here, but we do need the create event,
+ // and in future this may be needed, so included for symmetry.
+ const predecessorEvent = mkEvent({
+ event: true,
+ user: "@a:b.com",
+ type: EventType.RoomPredecessor,
+ content: { predecessor_room_id: "old_room_id_via_predecessor" },
+ room: room.roomId,
+ });
+
type GetStateEvents2Args = (eventType: EventType | string, stateKey: string) => MatrixEvent | null;
const getStateEvents = jest.spyOn(
@@ -82,6 +100,8 @@ describe("AdvancedRoomSettingsTab", () => {
switch (eventType) {
case EventType.RoomCreate:
return createEvent;
+ case EventType.RoomPredecessor:
+ return predecessorEvent;
default:
return null;
}
@@ -101,4 +121,26 @@ describe("AdvancedRoomSettingsTab", () => {
metricsViaKeyboard: false,
});
});
+
+ describe("When MSC3946 support is enabled", () => {
+ beforeEach(() => {
+ jest.spyOn(SettingsStore, "getValue")
+ .mockReset()
+ .mockImplementation((settingName) => settingName === "feature_dynamic_room_predecessors");
+ });
+
+ it("should link to predecessor room via MSC3946 if enabled", async () => {
+ mockStateEvents(room);
+ const tab = renderTab();
+ const link = await tab.findByText("View older messages in test room.");
+ fireEvent.click(link);
+ expect(dis.dispatch).toHaveBeenCalledWith({
+ action: Action.ViewRoom,
+ event_id: null,
+ room_id: "old_room_id_via_predecessor",
+ metricsTrigger: "WebPredecessorSettings",
+ metricsViaKeyboard: false,
+ });
+ });
+ });
});
From c557162592a38e99bf907a48f89d87eda4ca7d93 Mon Sep 17 00:00:00 2001
From: Johannes Marbach
Date: Fri, 27 Jan 2023 15:14:20 +0100
Subject: [PATCH 13/97] Try to appease the linter
---
src/components/structures/SpaceHierarchy.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx
index f99cf73f5f5..84ec987337f 100644
--- a/src/components/structures/SpaceHierarchy.tsx
+++ b/src/components/structures/SpaceHierarchy.tsx
@@ -419,7 +419,7 @@ const toLocalRoom = (cli: MatrixClient, room: IHierarchyRoom, hierarchy: RoomHie
// Pick latest room that is actually part of the hierarchy
let cliRoom = null;
for (let idx = history.length - 1; idx >= 0; --idx) {
- if (hierarchy.roomMap[history[idx].roomId]) {
+ if (hierarchy.roomMap.get(history[idx].roomId)) {
cliRoom = history[idx];
break;
}
@@ -432,7 +432,7 @@ const toLocalRoom = (cli: MatrixClient, room: IHierarchyRoom, hierarchy: RoomHie
room_type: cliRoom.getType(),
name: cliRoom.name,
topic: cliRoom.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent().topic,
- avatar_url: cliRoom.getMxcAvatarUrl(),
+ avatar_url: cliRoom.getMxcAvatarUrl() ?? undefined,
canonical_alias: cliRoom.getCanonicalAlias(),
aliases: cliRoom.getAltAliases(),
world_readable:
From 0d0d7a3e2cfe5793c60bb3f3b4e110ea66eec758 Mon Sep 17 00:00:00 2001
From: Johannes Marbach
Date: Fri, 27 Jan 2023 15:25:10 +0100
Subject: [PATCH 14/97] Try to appease the linter
---
src/components/structures/SpaceHierarchy.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx
index 84ec987337f..f358e378897 100644
--- a/src/components/structures/SpaceHierarchy.tsx
+++ b/src/components/structures/SpaceHierarchy.tsx
@@ -432,8 +432,8 @@ const toLocalRoom = (cli: MatrixClient, room: IHierarchyRoom, hierarchy: RoomHie
room_type: cliRoom.getType(),
name: cliRoom.name,
topic: cliRoom.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent().topic,
- avatar_url: cliRoom.getMxcAvatarUrl() ?? undefined,
- canonical_alias: cliRoom.getCanonicalAlias(),
+ avatar_url: cliRoom.getMxcAvatarUrl(),
+ canonical_alias: cliRoom.getCanonicalAlias() ?? undefined,
aliases: cliRoom.getAltAliases(),
world_readable:
cliRoom.currentState.getStateEvents(EventType.RoomHistoryVisibility, "")?.getContent()
From 364c453907afa169c54ec9c8e3bfc42d07f71406 Mon Sep 17 00:00:00 2001
From: Andy Balaam
Date: Fri, 27 Jan 2023 15:23:23 +0000
Subject: [PATCH 15/97] Tests for RoomCreate (#9997)
* Tests for RoomCreate tile
* Prefer screen instead of holding the return from render
* use userEvent instead of fireEvent
---
src/components/views/messages/RoomCreate.tsx | 6 +-
.../views/messages/RoomCreate-test.tsx | 87 +++++++++++++++++++
.../__snapshots__/RoomCreate-test.tsx.snap | 24 +++++
3 files changed, 116 insertions(+), 1 deletion(-)
create mode 100644 test/components/views/messages/RoomCreate-test.tsx
create mode 100644 test/components/views/messages/__snapshots__/RoomCreate-test.tsx.snap
diff --git a/src/components/views/messages/RoomCreate.tsx b/src/components/views/messages/RoomCreate.tsx
index a9035ca03cb..8bff5dfdcc5 100644
--- a/src/components/views/messages/RoomCreate.tsx
+++ b/src/components/views/messages/RoomCreate.tsx
@@ -27,11 +27,15 @@ import EventTileBubble from "./EventTileBubble";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
interface IProps {
- /* the MatrixEvent to show */
+ /** The m.room.create MatrixEvent that this tile represents */
mxEvent: MatrixEvent;
timestamp?: JSX.Element;
}
+/**
+ * A message tile showing that this room was created as an upgrade of a previous
+ * room.
+ */
export default class RoomCreate extends React.Component {
private onLinkClicked = (e: React.MouseEvent): void => {
e.preventDefault();
diff --git a/test/components/views/messages/RoomCreate-test.tsx b/test/components/views/messages/RoomCreate-test.tsx
new file mode 100644
index 00000000000..31763f9ae89
--- /dev/null
+++ b/test/components/views/messages/RoomCreate-test.tsx
@@ -0,0 +1,87 @@
+/*
+Copyright 2023 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from "react";
+import { act, render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { mocked } from "jest-mock";
+import { EventType, MatrixEvent } from "matrix-js-sdk/src/matrix";
+
+import dis from "../../../../src/dispatcher/dispatcher";
+import SettingsStore from "../../../../src/settings/SettingsStore";
+import RoomCreate from "../../../../src/components/views/messages/RoomCreate";
+import { stubClient } from "../../../test-utils/test-utils";
+import { Action } from "../../../../src/dispatcher/actions";
+
+jest.mock("../../../../src/dispatcher/dispatcher");
+
+describe("", () => {
+ const userId = "@alice:server.org";
+ const roomId = "!room:server.org";
+ const createEvent = new MatrixEvent({
+ type: EventType.RoomCreate,
+ sender: userId,
+ room_id: roomId,
+ content: {
+ predecessor: { room_id: "old_room_id", event_id: "tombstone_event_id" },
+ },
+ event_id: "$create",
+ });
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mocked(dis.dispatch).mockReset();
+ jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
+ jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined);
+ stubClient();
+ });
+
+ afterAll(() => {
+ jest.spyOn(SettingsStore, "getValue").mockRestore();
+ jest.spyOn(SettingsStore, "setValue").mockRestore();
+ });
+
+ it("Renders as expected", () => {
+ const roomCreate = render();
+ expect(roomCreate.asFragment()).toMatchSnapshot();
+ });
+
+ it("Links to the old version of the room", () => {
+ render();
+ expect(screen.getByText("Click here to see older messages.")).toHaveAttribute(
+ "href",
+ "https://matrix.to/#/old_room_id/tombstone_event_id",
+ );
+ });
+
+ it("Opens the old room on click", async () => {
+ render();
+ const link = screen.getByText("Click here to see older messages.");
+
+ await act(() => userEvent.click(link));
+
+ await waitFor(() =>
+ expect(dis.dispatch).toHaveBeenCalledWith({
+ action: Action.ViewRoom,
+ event_id: "tombstone_event_id",
+ highlighted: true,
+ room_id: "old_room_id",
+ metricsTrigger: "Predecessor",
+ metricsViaKeyboard: false,
+ }),
+ );
+ });
+});
diff --git a/test/components/views/messages/__snapshots__/RoomCreate-test.tsx.snap b/test/components/views/messages/__snapshots__/RoomCreate-test.tsx.snap
new file mode 100644
index 00000000000..97c1cee66f6
--- /dev/null
+++ b/test/components/views/messages/__snapshots__/RoomCreate-test.tsx.snap
@@ -0,0 +1,24 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` Renders as expected 1`] = `
+
+
+
+ This room is a continuation of another conversation.
+
+
+
+
+`;
From 7788d50b02456d34d12d5f94aed4db7b18cf225a Mon Sep 17 00:00:00 2001
From: Johannes Marbach
Date: Fri, 27 Jan 2023 20:20:01 +0100
Subject: [PATCH 16/97] Add tests
---
src/components/structures/SpaceHierarchy.tsx | 2 +-
.../structures/SpaceHierarchy-test.tsx | 48 ++++++++++++++++++-
2 files changed, 47 insertions(+), 3 deletions(-)
diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx
index f358e378897..7d6888a1979 100644
--- a/src/components/structures/SpaceHierarchy.tsx
+++ b/src/components/structures/SpaceHierarchy.tsx
@@ -413,7 +413,7 @@ interface IHierarchyLevelProps {
onToggleClick?(parentId: string, childId: string): void;
}
-const toLocalRoom = (cli: MatrixClient, room: IHierarchyRoom, hierarchy: RoomHierarchy): IHierarchyRoom => {
+export const toLocalRoom = (cli: MatrixClient, room: IHierarchyRoom, hierarchy: RoomHierarchy): IHierarchyRoom => {
const history = cli.getRoomUpgradeHistory(room.room_id, true);
// Pick latest room that is actually part of the hierarchy
diff --git a/test/components/structures/SpaceHierarchy-test.tsx b/test/components/structures/SpaceHierarchy-test.tsx
index 91818028142..a2a63ae0b82 100644
--- a/test/components/structures/SpaceHierarchy-test.tsx
+++ b/test/components/structures/SpaceHierarchy-test.tsx
@@ -17,11 +17,12 @@ limitations under the License.
import { MatrixClient } from "matrix-js-sdk/src/client";
import { Room } from "matrix-js-sdk/src/models/room";
import { RoomHierarchy } from "matrix-js-sdk/src/room-hierarchy";
+import { IHierarchyRoom } from "matrix-js-sdk/src/@types/spaces";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
-import { stubClient } from "../../test-utils";
+import { mkStubRoom, stubClient } from "../../test-utils";
import dispatcher from "../../../src/dispatcher/dispatcher";
-import { showRoom } from "../../../src/components/structures/SpaceHierarchy";
+import { showRoom, toLocalRoom } from "../../../src/components/structures/SpaceHierarchy";
import { Action } from "../../../src/dispatcher/actions";
describe("SpaceHierarchy", () => {
@@ -67,4 +68,47 @@ describe("SpaceHierarchy", () => {
});
});
});
+
+ describe("toLocalRoom", () => {
+ let client: MatrixClient;
+ let roomV1: Room;
+ let roomV2: Room;
+ let roomV3: Room;
+
+ beforeEach(() => {
+ stubClient();
+ client = MatrixClientPeg.get();
+ roomV1 = mkStubRoom("room-id-1", "Room V1", client);
+ roomV2 = mkStubRoom("room-id-2", "Room V2", client);
+ roomV3 = mkStubRoom("room-id-3", "Room V3", client);
+ jest.spyOn(client, "getRoomUpgradeHistory").mockReturnValue([roomV1, roomV2, roomV3]);
+ });
+
+ it("grabs last room that is in hierarchy when latest version is in hierarchy", () => {
+ const hierarchy = { roomMap: new Map([
+ [roomV1.roomId, { room_id: roomV1.roomId } as IHierarchyRoom],
+ [roomV2.roomId, { room_id: roomV2.roomId } as IHierarchyRoom],
+ [roomV3.roomId, { room_id: roomV3.roomId } as IHierarchyRoom],
+ ]) } as RoomHierarchy;
+ const localRoomV1 = toLocalRoom(client, { room_id: roomV1.roomId } as IHierarchyRoom, hierarchy);
+ expect(localRoomV1.room_id).toEqual(roomV3.roomId);
+ const localRoomV2 = toLocalRoom(client, { room_id: roomV2.roomId } as IHierarchyRoom, hierarchy);
+ expect(localRoomV2.room_id).toEqual(roomV3.roomId);
+ const localRoomV3 = toLocalRoom(client, { room_id: roomV3.roomId } as IHierarchyRoom, hierarchy);
+ expect(localRoomV3.room_id).toEqual(roomV3.roomId);
+ });
+
+ it("grabs last room that is in hierarchy when latest version is *not* in hierarchy", () => {
+ const hierarchy = { roomMap: new Map([
+ [roomV1.roomId, { room_id: roomV1.roomId } as IHierarchyRoom],
+ [roomV2.roomId, { room_id: roomV2.roomId } as IHierarchyRoom]
+ ]) } as RoomHierarchy;
+ const localRoomV1 = toLocalRoom(client, { room_id: roomV1.roomId } as IHierarchyRoom, hierarchy);
+ expect(localRoomV1.room_id).toEqual(roomV2.roomId);
+ const localRoomV2 = toLocalRoom(client, { room_id: roomV2.roomId } as IHierarchyRoom, hierarchy);
+ expect(localRoomV2.room_id).toEqual(roomV2.roomId);
+ const localRoomV3 = toLocalRoom(client, { room_id: roomV3.roomId } as IHierarchyRoom, hierarchy);
+ expect(localRoomV3.room_id).toEqual(roomV2.roomId);
+ });
+ });
});
From 6f4581943b003c533303e41d746b9f4112cbc40c Mon Sep 17 00:00:00 2001
From: Johannes Marbach
Date: Fri, 27 Jan 2023 20:29:50 +0100
Subject: [PATCH 17/97] Prettify it
---
.../structures/SpaceHierarchy-test.tsx | 22 +++++++++++--------
1 file changed, 13 insertions(+), 9 deletions(-)
diff --git a/test/components/structures/SpaceHierarchy-test.tsx b/test/components/structures/SpaceHierarchy-test.tsx
index a2a63ae0b82..796e8848d48 100644
--- a/test/components/structures/SpaceHierarchy-test.tsx
+++ b/test/components/structures/SpaceHierarchy-test.tsx
@@ -85,11 +85,13 @@ describe("SpaceHierarchy", () => {
});
it("grabs last room that is in hierarchy when latest version is in hierarchy", () => {
- const hierarchy = { roomMap: new Map([
- [roomV1.roomId, { room_id: roomV1.roomId } as IHierarchyRoom],
- [roomV2.roomId, { room_id: roomV2.roomId } as IHierarchyRoom],
- [roomV3.roomId, { room_id: roomV3.roomId } as IHierarchyRoom],
- ]) } as RoomHierarchy;
+ const hierarchy = {
+ roomMap: new Map([
+ [roomV1.roomId, { room_id: roomV1.roomId } as IHierarchyRoom],
+ [roomV2.roomId, { room_id: roomV2.roomId } as IHierarchyRoom],
+ [roomV3.roomId, { room_id: roomV3.roomId } as IHierarchyRoom],
+ ]),
+ } as RoomHierarchy;
const localRoomV1 = toLocalRoom(client, { room_id: roomV1.roomId } as IHierarchyRoom, hierarchy);
expect(localRoomV1.room_id).toEqual(roomV3.roomId);
const localRoomV2 = toLocalRoom(client, { room_id: roomV2.roomId } as IHierarchyRoom, hierarchy);
@@ -99,10 +101,12 @@ describe("SpaceHierarchy", () => {
});
it("grabs last room that is in hierarchy when latest version is *not* in hierarchy", () => {
- const hierarchy = { roomMap: new Map([
- [roomV1.roomId, { room_id: roomV1.roomId } as IHierarchyRoom],
- [roomV2.roomId, { room_id: roomV2.roomId } as IHierarchyRoom]
- ]) } as RoomHierarchy;
+ const hierarchy = {
+ roomMap: new Map([
+ [roomV1.roomId, { room_id: roomV1.roomId } as IHierarchyRoom],
+ [roomV2.roomId, { room_id: roomV2.roomId } as IHierarchyRoom],
+ ]),
+ } as RoomHierarchy;
const localRoomV1 = toLocalRoom(client, { room_id: roomV1.roomId } as IHierarchyRoom, hierarchy);
expect(localRoomV1.room_id).toEqual(roomV2.roomId);
const localRoomV2 = toLocalRoom(client, { room_id: roomV2.roomId } as IHierarchyRoom, hierarchy);
From 64cec319812e9a72c386c1643d20db17d1bbe480 Mon Sep 17 00:00:00 2001
From: Nawaraj Shah
Date: Sun, 29 Jan 2023 15:48:38 +0545
Subject: [PATCH 18/97] changing the color of message time stamp (#10016)
---
res/css/views/right_panel/_ThreadPanel.pcss | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/res/css/views/right_panel/_ThreadPanel.pcss b/res/css/views/right_panel/_ThreadPanel.pcss
index aff186fca5d..c1f1daaca1d 100644
--- a/res/css/views/right_panel/_ThreadPanel.pcss
+++ b/res/css/views/right_panel/_ThreadPanel.pcss
@@ -130,7 +130,7 @@ limitations under the License.
}
.mx_MessageTimestamp {
- color: $secondary-content;
+ color: $event-timestamp-color;
}
.mx_BaseCard_footer {
From d0de2a6851ff21d383ed18f054f8a32f37b06619 Mon Sep 17 00:00:00 2001
From: Johannes Marbach
Date: Mon, 30 Jan 2023 09:53:41 +0100
Subject: [PATCH 19/97] Add test case for no-version-in-hierarchy situation
---
test/components/structures/SpaceHierarchy-test.tsx | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/test/components/structures/SpaceHierarchy-test.tsx b/test/components/structures/SpaceHierarchy-test.tsx
index 796e8848d48..27e55e95c10 100644
--- a/test/components/structures/SpaceHierarchy-test.tsx
+++ b/test/components/structures/SpaceHierarchy-test.tsx
@@ -114,5 +114,15 @@ describe("SpaceHierarchy", () => {
const localRoomV3 = toLocalRoom(client, { room_id: roomV3.roomId } as IHierarchyRoom, hierarchy);
expect(localRoomV3.room_id).toEqual(roomV2.roomId);
});
+
+ it("returns specified room when none of the versions is in hierarchy", () => {
+ const hierarchy = { roomMap: new Map([]) } as RoomHierarchy;
+ const localRoomV1 = toLocalRoom(client, { room_id: roomV1.roomId } as IHierarchyRoom, hierarchy);
+ expect(localRoomV1.room_id).toEqual(roomV1.roomId);
+ const localRoomV2 = toLocalRoom(client, { room_id: roomV2.roomId } as IHierarchyRoom, hierarchy);
+ expect(localRoomV2.room_id).toEqual(roomV2.roomId);
+ const localRoomV3 = toLocalRoom(client, { room_id: roomV3.roomId } as IHierarchyRoom, hierarchy);
+ expect(localRoomV3.room_id).toEqual(roomV3.roomId);
+ });
});
});
From a8aa4de4b4c14a6a995071aab3f2f871b3ff2388 Mon Sep 17 00:00:00 2001
From: Clark Fischer <439978+clarkf@users.noreply.github.com>
Date: Mon, 30 Jan 2023 09:50:08 +0000
Subject: [PATCH 20/97] Member avatars without canvas (#9990)
* Strict typechecking fixes for Base/Member/Avatar
Update the core avatar files to pass `--strict --noImplicitAny` typechecks.
Signed-off-by: Clark Fischer
* Add tests for Base/Member/Avatar
More thoroughly test the core avatar files. Not necessarily the most thorough,
but an improvement.
Signed-off-by: Clark Fischer
* Extract TextAvatar from BaseAvatar
Extracted the fallback/textual avatar into its own component.
Signed-off-by: Clark Fischer
* Use standard HTML for non-image avatars
Firefox users with `resistFingerprinting` enabled were seeing random noise
for rooms and users without avatars. There's no real reason to use data
URLs to present flat colors.
This converts non-image avatars to inline blocks with background colors.
See https://github.com/vector-im/element-web/issues/23936
Signed-off-by: Clark Fischer
* Have pills use solid backgrounds rather than colored images
Similar to room and member avatars, pills now use colored pseudo-elements
rather than background images.
Signed-off-by: Clark Fischer
---------
Signed-off-by: Clark Fischer
Co-authored-by: Andy Balaam
---
.../views/rooms/_BasicMessageComposer.pcss | 2 +-
src/Avatar.ts | 64 ++++--
src/components/views/avatars/BaseAvatar.tsx | 128 ++++++-----
src/components/views/avatars/MemberAvatar.tsx | 22 +-
src/components/views/avatars/RoomAvatar.tsx | 3 +-
src/dispatcher/payloads/ViewUserPayload.ts | 7 +
src/editor/parts.ts | 40 ++--
test/Avatar-test.ts | 121 ++++++++---
.../__snapshots__/RoomView-test.tsx.snap | 98 +++------
.../__snapshots__/UserMenu-test.tsx.snap | 14 +-
.../views/avatars/BaseAvatar-test.tsx | 201 ++++++++++++++++++
.../views/avatars/MemberAvatar-test.tsx | 94 ++++++--
.../views/avatars/RoomAvatar-test.tsx | 10 +-
.../__snapshots__/BaseAvatar-test.tsx.snap | 72 +++++++
.../__snapshots__/MemberAvatar-test.tsx.snap | 14 ++
.../__snapshots__/RoomAvatar-test.tsx.snap | 42 ++--
.../__snapshots__/BeaconMarker-test.tsx.snap | 16 +-
.../views/rooms/RoomHeader-test.tsx | 10 +-
.../RoomPreviewBar-test.tsx.snap | 28 +--
.../__snapshots__/RoomTile-test.tsx.snap | 14 +-
test/editor/__snapshots__/parts-test.ts.snap | 45 ++++
test/editor/parts-test.ts | 72 ++++++-
22 files changed, 806 insertions(+), 311 deletions(-)
create mode 100644 test/components/views/avatars/BaseAvatar-test.tsx
create mode 100644 test/components/views/avatars/__snapshots__/BaseAvatar-test.tsx.snap
create mode 100644 test/components/views/avatars/__snapshots__/MemberAvatar-test.tsx.snap
create mode 100644 test/editor/__snapshots__/parts-test.ts.snap
diff --git a/res/css/views/rooms/_BasicMessageComposer.pcss b/res/css/views/rooms/_BasicMessageComposer.pcss
index 7b88a058153..32e7c5288f6 100644
--- a/res/css/views/rooms/_BasicMessageComposer.pcss
+++ b/res/css/views/rooms/_BasicMessageComposer.pcss
@@ -78,7 +78,7 @@ limitations under the License.
min-width: $font-16px; /* ensure the avatar is not compressed */
height: $font-16px;
margin-inline-end: 0.24rem;
- background: var(--avatar-background), $background;
+ background: var(--avatar-background);
color: $avatar-initial-color;
background-repeat: no-repeat;
background-size: $font-16px;
diff --git a/src/Avatar.ts b/src/Avatar.ts
index 8a3f10a22ca..3e6b18dbc79 100644
--- a/src/Avatar.ts
+++ b/src/Avatar.ts
@@ -1,5 +1,5 @@
/*
-Copyright 2015, 2016 OpenMarket Ltd
+Copyright 2015, 2016, 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -24,16 +24,19 @@ import DMRoomMap from "./utils/DMRoomMap";
import { mediaFromMxc } from "./customisations/Media";
import { isLocalRoom } from "./utils/localRoom/isLocalRoom";
+const DEFAULT_COLORS: Readonly = ["#0DBD8B", "#368bd6", "#ac3ba8"];
+
// Not to be used for BaseAvatar urls as that has similar default avatar fallback already
export function avatarUrlForMember(
- member: RoomMember,
+ member: RoomMember | null | undefined,
width: number,
height: number,
resizeMethod: ResizeMethod,
): string {
- let url: string;
- if (member?.getMxcAvatarUrl()) {
- url = mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod);
+ let url: string | undefined;
+ const mxcUrl = member?.getMxcAvatarUrl();
+ if (mxcUrl) {
+ url = mediaFromMxc(mxcUrl).getThumbnailOfSourceHttp(width, height, resizeMethod);
}
if (!url) {
// member can be null here currently since on invites, the JS SDK
@@ -44,6 +47,17 @@ export function avatarUrlForMember(
return url;
}
+export function getMemberAvatar(
+ member: RoomMember | null | undefined,
+ width: number,
+ height: number,
+ resizeMethod: ResizeMethod,
+): string | undefined {
+ const mxcUrl = member?.getMxcAvatarUrl();
+ if (!mxcUrl) return undefined;
+ return mediaFromMxc(mxcUrl).getThumbnailOfSourceHttp(width, height, resizeMethod);
+}
+
export function avatarUrlForUser(
user: Pick,
width: number,
@@ -86,18 +100,10 @@ function urlForColor(color: string): string {
// hard to install a listener here, even if there were a clear event to listen to
const colorToDataURLCache = new Map();
-export function defaultAvatarUrlForString(s: string): string {
+export function defaultAvatarUrlForString(s: string | undefined): string {
if (!s) return ""; // XXX: should never happen but empirically does by evidence of a rageshake
- const defaultColors = ["#0DBD8B", "#368bd6", "#ac3ba8"];
- let total = 0;
- for (let i = 0; i < s.length; ++i) {
- total += s.charCodeAt(i);
- }
- const colorIndex = total % defaultColors.length;
- // overwritten color value in custom themes
- const cssVariable = `--avatar-background-colors_${colorIndex}`;
- const cssValue = document.body.style.getPropertyValue(cssVariable);
- const color = cssValue || defaultColors[colorIndex];
+
+ const color = getColorForString(s);
let dataUrl = colorToDataURLCache.get(color);
if (!dataUrl) {
// validate color as this can come from account_data
@@ -112,13 +118,23 @@ export function defaultAvatarUrlForString(s: string): string {
return dataUrl;
}
+export function getColorForString(input: string): string {
+ const charSum = [...input].reduce((s, c) => s + c.charCodeAt(0), 0);
+ const index = charSum % DEFAULT_COLORS.length;
+
+ // overwritten color value in custom themes
+ const cssVariable = `--avatar-background-colors_${index}`;
+ const cssValue = document.body.style.getPropertyValue(cssVariable);
+ return cssValue || DEFAULT_COLORS[index]!;
+}
+
/**
* returns the first (non-sigil) character of 'name',
* converted to uppercase
* @param {string} name
* @return {string} the first letter
*/
-export function getInitialLetter(name: string): string {
+export function getInitialLetter(name: string): string | undefined {
if (!name) {
// XXX: We should find out what causes the name to sometimes be falsy.
console.trace("`name` argument to `getInitialLetter` not supplied");
@@ -134,19 +150,20 @@ export function getInitialLetter(name: string): string {
}
// rely on the grapheme cluster splitter in lodash so that we don't break apart compound emojis
- return split(name, "", 1)[0].toUpperCase();
+ return split(name, "", 1)[0]!.toUpperCase();
}
export function avatarUrlForRoom(
- room: Room,
+ room: Room | undefined,
width: number,
height: number,
resizeMethod?: ResizeMethod,
): string | null {
if (!room) return null; // null-guard
- if (room.getMxcAvatarUrl()) {
- return mediaFromMxc(room.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod);
+ const mxcUrl = room.getMxcAvatarUrl();
+ if (mxcUrl) {
+ return mediaFromMxc(mxcUrl).getThumbnailOfSourceHttp(width, height, resizeMethod);
}
// space rooms cannot be DMs so skip the rest
@@ -159,8 +176,9 @@ export function avatarUrlForRoom(
// If there are only two members in the DM use the avatar of the other member
const otherMember = room.getAvatarFallbackMember();
- if (otherMember?.getMxcAvatarUrl()) {
- return mediaFromMxc(otherMember.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod);
+ const otherMemberMxc = otherMember?.getMxcAvatarUrl();
+ if (otherMemberMxc) {
+ return mediaFromMxc(otherMemberMxc).getThumbnailOfSourceHttp(width, height, resizeMethod);
}
return null;
}
diff --git a/src/components/views/avatars/BaseAvatar.tsx b/src/components/views/avatars/BaseAvatar.tsx
index 025cb9d2711..d1dbe7743de 100644
--- a/src/components/views/avatars/BaseAvatar.tsx
+++ b/src/components/views/avatars/BaseAvatar.tsx
@@ -1,8 +1,6 @@
/*
-Copyright 2015, 2016 OpenMarket Ltd
-Copyright 2018 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
-Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
+Copyright 2015, 2016, 2018, 2019, 2020, 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -21,34 +19,41 @@ import React, { useCallback, useContext, useEffect, useState } from "react";
import classNames from "classnames";
import { ResizeMethod } from "matrix-js-sdk/src/@types/partials";
import { ClientEvent } from "matrix-js-sdk/src/client";
+import { SyncState } from "matrix-js-sdk/src/sync";
import * as AvatarLogic from "../../../Avatar";
-import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from "../elements/AccessibleButton";
import RoomContext from "../../../contexts/RoomContext";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
import { toPx } from "../../../utils/units";
import { _t } from "../../../languageHandler";
+import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
interface IProps {
- name: string; // The name (first initial used as default)
- idName?: string; // ID for generating hash colours
- title?: string; // onHover title text
- url?: string; // highest priority of them all, shortcut to set in urls[0]
- urls?: string[]; // [highest_priority, ... , lowest_priority]
+ /** The name (first initial used as default) */
+ name: string;
+ /** ID for generating hash colours */
+ idName?: string;
+ /** onHover title text */
+ title?: string;
+ /** highest priority of them all, shortcut to set in urls[0] */
+ url?: string;
+ /** [highest_priority, ... , lowest_priority] */
+ urls?: string[];
width?: number;
height?: number;
- // XXX: resizeMethod not actually used.
+ /** @deprecated not actually used */
resizeMethod?: ResizeMethod;
- defaultToInitialLetter?: boolean; // true to add default url
- onClick?: React.MouseEventHandler;
+ /** true to add default url */
+ defaultToInitialLetter?: boolean;
+ onClick?: React.ComponentPropsWithoutRef["onClick"];
inputRef?: React.RefObject;
className?: string;
tabIndex?: number;
}
-const calculateUrls = (url: string, urls: string[], lowBandwidth: boolean): string[] => {
+const calculateUrls = (url: string | undefined, urls: string[] | undefined, lowBandwidth: boolean): string[] => {
// work out the full set of urls to try to load. This is formed like so:
// imageUrls: [ props.url, ...props.urls ]
@@ -66,11 +71,26 @@ const calculateUrls = (url: string, urls: string[], lowBandwidth: boolean): stri
return Array.from(new Set(_urls));
};
-const useImageUrl = ({ url, urls }): [string, () => void] => {
+/**
+ * Hook for cycling through a changing set of images.
+ *
+ * The set of images is updated whenever `url` or `urls` change, the user's
+ * `lowBandwidth` preference changes, or the client reconnects.
+ *
+ * Returns `[imageUrl, onError]`. When `onError` is called, the next image in
+ * the set will be displayed.
+ */
+const useImageUrl = ({
+ url,
+ urls,
+}: {
+ url: string | undefined;
+ urls: string[] | undefined;
+}): [string | undefined, () => void] => {
// Since this is a hot code path and the settings store can be slow, we
// use the cached lowBandwidth value from the room context if it exists
const roomContext = useContext(RoomContext);
- const lowBandwidth = roomContext ? roomContext.lowBandwidth : SettingsStore.getValue("lowBandwidth");
+ const lowBandwidth = roomContext.lowBandwidth;
const [imageUrls, setUrls] = useState(calculateUrls(url, urls, lowBandwidth));
const [urlsIndex, setIndex] = useState(0);
@@ -85,10 +105,10 @@ const useImageUrl = ({ url, urls }): [string, () => void] => {
}, [url, JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps
const cli = useContext(MatrixClientContext);
- const onClientSync = useCallback((syncState, prevState) => {
+ const onClientSync = useCallback((syncState: SyncState, prevState: SyncState | null) => {
// Consider the client reconnected if there is no error with syncing.
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
- const reconnected = syncState !== "ERROR" && prevState !== syncState;
+ const reconnected = syncState !== SyncState.Error && prevState !== syncState;
if (reconnected) {
setIndex(0);
}
@@ -108,46 +128,18 @@ const BaseAvatar: React.FC = (props) => {
urls,
width = 40,
height = 40,
- resizeMethod = "crop", // eslint-disable-line @typescript-eslint/no-unused-vars
defaultToInitialLetter = true,
onClick,
inputRef,
className,
+ resizeMethod: _unused, // to keep it from being in `otherProps`
...otherProps
} = props;
const [imageUrl, onError] = useImageUrl({ url, urls });
if (!imageUrl && defaultToInitialLetter && name) {
- const initialLetter = AvatarLogic.getInitialLetter(name);
- const textNode = (
-
- {initialLetter}
-
- );
- const imgNode = (
-
- );
+ const avatar = ;
if (onClick) {
return (
@@ -159,9 +151,12 @@ const BaseAvatar: React.FC = (props) => {
className={classNames("mx_BaseAvatar", className)}
onClick={onClick}
inputRef={inputRef}
+ style={{
+ width: toPx(width),
+ height: toPx(height),
+ }}
>
- {textNode}
- {imgNode}
+ {avatar}
);
} else {
@@ -170,10 +165,13 @@ const BaseAvatar: React.FC = (props) => {
className={classNames("mx_BaseAvatar", className)}
ref={inputRef}
{...otherProps}
+ style={{
+ width: toPx(width),
+ height: toPx(height),
+ }}
role="presentation"
>
- {textNode}
- {imgNode}
+ {avatar}
);
}
@@ -220,3 +218,31 @@ const BaseAvatar: React.FC = (props) => {
export default BaseAvatar;
export type BaseAvatarType = React.FC;
+
+const TextAvatar: React.FC<{
+ name: string;
+ idName?: string;
+ width: number;
+ height: number;
+ title?: string;
+}> = ({ name, idName, width, height, title }) => {
+ const initialLetter = AvatarLogic.getInitialLetter(name);
+
+ return (
+
+ {initialLetter}
+
+ );
+};
diff --git a/src/components/views/avatars/MemberAvatar.tsx b/src/components/views/avatars/MemberAvatar.tsx
index 48138714559..f493c58f8cf 100644
--- a/src/components/views/avatars/MemberAvatar.tsx
+++ b/src/components/views/avatars/MemberAvatar.tsx
@@ -1,6 +1,5 @@
/*
-Copyright 2015, 2016 OpenMarket Ltd
-Copyright 2019 - 2022 The Matrix.org Foundation C.I.C.
+Copyright 2015, 2016, 2019 - 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -26,6 +25,7 @@ import { mediaFromMxc } from "../../../customisations/Media";
import { CardContext } from "../right_panel/context";
import UserIdentifierCustomisations from "../../../customisations/UserIdentifier";
import { useRoomMemberProfile } from "../../../hooks/room/useRoomMemberProfile";
+import { ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload";
interface IProps extends Omit, "name" | "idName" | "url"> {
member: RoomMember | null;
@@ -33,14 +33,13 @@ interface IProps extends Omit, "name" |
width: number;
height: number;
resizeMethod?: ResizeMethod;
- // The onClick to give the avatar
- onClick?: React.MouseEventHandler;
- // Whether the onClick of the avatar should be overridden to dispatch `Action.ViewUser`
+ /** Whether the onClick of the avatar should be overridden to dispatch `Action.ViewUser` */
viewUserOnClick?: boolean;
pushUserOnClick?: boolean;
title?: string;
- style?: any;
- forceHistorical?: boolean; // true to deny `useOnlyCurrentProfiles` usage. Default false.
+ style?: React.CSSProperties;
+ /** true to deny `useOnlyCurrentProfiles` usage. Default false. */
+ forceHistorical?: boolean;
hideTitle?: boolean;
}
@@ -77,8 +76,8 @@ export default function MemberAvatar({
if (!title) {
title =
- UserIdentifierCustomisations.getDisplayUserIdentifier(member?.userId ?? "", {
- roomId: member?.roomId ?? "",
+ UserIdentifierCustomisations.getDisplayUserIdentifier!(member.userId, {
+ roomId: member.roomId,
}) ?? fallbackUserId;
}
}
@@ -88,7 +87,6 @@ export default function MemberAvatar({
{...props}
width={width}
height={height}
- resizeMethod={resizeMethod}
name={name ?? ""}
title={hideTitle ? undefined : title}
idName={member?.userId ?? fallbackUserId}
@@ -96,9 +94,9 @@ export default function MemberAvatar({
onClick={
viewUserOnClick
? () => {
- dis.dispatch({
+ dis.dispatch({
action: Action.ViewUser,
- member: propsMember,
+ member: propsMember || undefined,
push: card.isCard,
});
}
diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx
index 50389c77491..4abfdbbf67e 100644
--- a/src/components/views/avatars/RoomAvatar.tsx
+++ b/src/components/views/avatars/RoomAvatar.tsx
@@ -109,7 +109,8 @@ export default class RoomAvatar extends React.Component {
}
private onRoomAvatarClick = (): void => {
- const avatarUrl = Avatar.avatarUrlForRoom(this.props.room, null, null, null);
+ const avatarMxc = this.props.room?.getMxcAvatarUrl();
+ const avatarUrl = avatarMxc ? mediaFromMxc(avatarMxc).srcHttp : null;
const params = {
src: avatarUrl,
name: this.props.room.name,
diff --git a/src/dispatcher/payloads/ViewUserPayload.ts b/src/dispatcher/payloads/ViewUserPayload.ts
index 20df21beb49..a09804babee 100644
--- a/src/dispatcher/payloads/ViewUserPayload.ts
+++ b/src/dispatcher/payloads/ViewUserPayload.ts
@@ -28,4 +28,11 @@ export interface ViewUserPayload extends ActionPayload {
* should be shown (hide whichever relevant components).
*/
member?: RoomMember | User;
+
+ /**
+ * Should this event be pushed as a card into the right panel?
+ *
+ * @see RightPanelStore#pushCard
+ */
+ push?: boolean;
}
diff --git a/src/editor/parts.ts b/src/editor/parts.ts
index 306d86dbc94..0157cd738a0 100644
--- a/src/editor/parts.ts
+++ b/src/editor/parts.ts
@@ -1,6 +1,5 @@
/*
-Copyright 2019 New Vector Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2019, 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -295,9 +294,9 @@ export abstract class PillPart extends BasePart implements IPillPart {
}
// helper method for subclasses
- protected setAvatarVars(node: HTMLElement, avatarUrl: string, initialLetter: string): void {
- const avatarBackground = `url('${avatarUrl}')`;
- const avatarLetter = `'${initialLetter}'`;
+ protected setAvatarVars(node: HTMLElement, avatarBackground: string, initialLetter: string | undefined): void {
+ // const avatarBackground = `url('${avatarUrl}')`;
+ const avatarLetter = `'${initialLetter || ""}'`;
// check if the value is changing,
// otherwise the avatars flicker on every keystroke while updating.
if (node.style.getPropertyValue("--avatar-background") !== avatarBackground) {
@@ -413,13 +412,15 @@ class RoomPillPart extends PillPart {
}
protected setAvatar(node: HTMLElement): void {
- let initialLetter = "";
- let avatarUrl = Avatar.avatarUrlForRoom(this.room, 16, 16, "crop");
- if (!avatarUrl) {
- initialLetter = Avatar.getInitialLetter(this.room?.name || this.resourceId);
- avatarUrl = Avatar.defaultAvatarUrlForString(this.room?.roomId ?? this.resourceId);
+ const avatarUrl = Avatar.avatarUrlForRoom(this.room, 16, 16, "crop");
+ if (avatarUrl) {
+ this.setAvatarVars(node, `url('${avatarUrl}')`, "");
+ return;
}
- this.setAvatarVars(node, avatarUrl, initialLetter);
+
+ const initialLetter = Avatar.getInitialLetter(this.room?.name || this.resourceId);
+ const color = Avatar.getColorForString(this.room?.roomId ?? this.resourceId);
+ this.setAvatarVars(node, color, initialLetter);
}
public get type(): IPillPart["type"] {
@@ -465,14 +466,17 @@ class UserPillPart extends PillPart {
if (!this.member) {
return;
}
- const name = this.member.name || this.member.userId;
- const defaultAvatarUrl = Avatar.defaultAvatarUrlForString(this.member.userId);
- const avatarUrl = Avatar.avatarUrlForMember(this.member, 16, 16, "crop");
- let initialLetter = "";
- if (avatarUrl === defaultAvatarUrl) {
- initialLetter = Avatar.getInitialLetter(name);
+
+ const avatar = Avatar.getMemberAvatar(this.member, 16, 16, "crop");
+ if (avatar) {
+ this.setAvatarVars(node, `url('${avatar}')`, "");
+ return;
}
- this.setAvatarVars(node, avatarUrl, initialLetter);
+
+ const name = this.member.name || this.member.userId;
+ const initialLetter = Avatar.getInitialLetter(name);
+ const color = Avatar.getColorForString(this.member.userId);
+ this.setAvatarVars(node, color, initialLetter);
}
protected onClick = (): void => {
diff --git a/test/Avatar-test.ts b/test/Avatar-test.ts
index 0ff064ed57d..8b4ee03b7fc 100644
--- a/test/Avatar-test.ts
+++ b/test/Avatar-test.ts
@@ -1,5 +1,5 @@
/*
-Copyright 2022 The Matrix.org Foundation C.I.C.
+Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,33 +15,106 @@ limitations under the License.
*/
import { mocked } from "jest-mock";
-import { Room, RoomMember, RoomType } from "matrix-js-sdk/src/matrix";
-
-import { avatarUrlForRoom } from "../src/Avatar";
-import { Media, mediaFromMxc } from "../src/customisations/Media";
+import { Room, RoomMember, RoomType, User } from "matrix-js-sdk/src/matrix";
+
+import {
+ avatarUrlForMember,
+ avatarUrlForRoom,
+ avatarUrlForUser,
+ defaultAvatarUrlForString,
+ getColorForString,
+ getInitialLetter,
+} from "../src/Avatar";
+import { mediaFromMxc } from "../src/customisations/Media";
import DMRoomMap from "../src/utils/DMRoomMap";
-
-jest.mock("../src/customisations/Media", () => ({
- mediaFromMxc: jest.fn(),
-}));
+import { filterConsole, stubClient } from "./test-utils";
const roomId = "!room:example.com";
const avatarUrl1 = "https://example.com/avatar1";
const avatarUrl2 = "https://example.com/avatar2";
+describe("avatarUrlForMember", () => {
+ let member: RoomMember;
+
+ beforeEach(() => {
+ stubClient();
+ member = new RoomMember(roomId, "@user:example.com");
+ });
+
+ it("returns the member's url", () => {
+ const mxc = "mxc://example.com/a/b/c/d/avatar.gif";
+ jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue(mxc);
+
+ expect(avatarUrlForMember(member, 32, 32, "crop")).toBe(
+ mediaFromMxc(mxc).getThumbnailOfSourceHttp(32, 32, "crop"),
+ );
+ });
+
+ it("returns a default if the member has no avatar", () => {
+ jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue(undefined);
+
+ expect(avatarUrlForMember(member, 32, 32, "crop")).toMatch(/^data:/);
+ });
+});
+
+describe("avatarUrlForUser", () => {
+ let user: User;
+
+ beforeEach(() => {
+ stubClient();
+ user = new User("@user:example.com");
+ });
+
+ it("should return the user's avatar", () => {
+ const mxc = "mxc://example.com/a/b/c/d/avatar.gif";
+ user.avatarUrl = mxc;
+
+ expect(avatarUrlForUser(user, 64, 64, "scale")).toBe(
+ mediaFromMxc(mxc).getThumbnailOfSourceHttp(64, 64, "scale"),
+ );
+ });
+
+ it("should not provide a fallback", () => {
+ expect(avatarUrlForUser(user, 64, 64, "scale")).toBeNull();
+ });
+});
+
+describe("defaultAvatarUrlForString", () => {
+ it.each(["a", "abc", "abcde", "@".repeat(150)])("should return a value for %s", (s) => {
+ expect(defaultAvatarUrlForString(s)).not.toBe("");
+ });
+});
+
+describe("getColorForString", () => {
+ it.each(["a", "abc", "abcde", "@".repeat(150)])("should return a value for %s", (s) => {
+ expect(getColorForString(s)).toMatch(/^#\w+$/);
+ });
+
+ it("should return different values for different strings", () => {
+ expect(getColorForString("a")).not.toBe(getColorForString("b"));
+ });
+});
+
+describe("getInitialLetter", () => {
+ filterConsole("argument to `getInitialLetter` not supplied");
+
+ it.each(["a", "abc", "abcde", "@".repeat(150)])("should return a value for %s", (s) => {
+ expect(getInitialLetter(s)).not.toBe("");
+ });
+
+ it("should return undefined for empty strings", () => {
+ expect(getInitialLetter("")).toBeUndefined();
+ });
+});
+
describe("avatarUrlForRoom", () => {
- let getThumbnailOfSourceHttp: jest.Mock;
let room: Room;
let roomMember: RoomMember;
let dmRoomMap: DMRoomMap;
beforeEach(() => {
- getThumbnailOfSourceHttp = jest.fn();
- mocked(mediaFromMxc).mockImplementation((): Media => {
- return {
- getThumbnailOfSourceHttp,
- } as unknown as Media;
- });
+ stubClient();
+
room = {
roomId,
getMxcAvatarUrl: jest.fn(),
@@ -59,14 +132,14 @@ describe("avatarUrlForRoom", () => {
});
it("should return null for a null room", () => {
- expect(avatarUrlForRoom(null, 128, 128)).toBeNull();
+ expect(avatarUrlForRoom(undefined, 128, 128)).toBeNull();
});
it("should return the HTTP source if the room provides a MXC url", () => {
mocked(room.getMxcAvatarUrl).mockReturnValue(avatarUrl1);
- getThumbnailOfSourceHttp.mockReturnValue(avatarUrl2);
- expect(avatarUrlForRoom(room, 128, 256, "crop")).toEqual(avatarUrl2);
- expect(getThumbnailOfSourceHttp).toHaveBeenCalledWith(128, 256, "crop");
+ expect(avatarUrlForRoom(room, 128, 256, "crop")).toBe(
+ mediaFromMxc(avatarUrl1).getThumbnailOfSourceHttp(128, 256, "crop"),
+ );
});
it("should return null for a space room", () => {
@@ -83,7 +156,7 @@ describe("avatarUrlForRoom", () => {
it("should return null if there is no other member in the room", () => {
mocked(dmRoomMap).getUserIdForRoomId.mockReturnValue("@user:example.com");
- mocked(room.getAvatarFallbackMember).mockReturnValue(null);
+ mocked(room.getAvatarFallbackMember).mockReturnValue(undefined);
expect(avatarUrlForRoom(room, 128, 128)).toBeNull();
});
@@ -97,8 +170,8 @@ describe("avatarUrlForRoom", () => {
mocked(dmRoomMap).getUserIdForRoomId.mockReturnValue("@user:example.com");
mocked(room.getAvatarFallbackMember).mockReturnValue(roomMember);
mocked(roomMember.getMxcAvatarUrl).mockReturnValue(avatarUrl2);
- getThumbnailOfSourceHttp.mockReturnValue(avatarUrl2);
- expect(avatarUrlForRoom(room, 128, 256, "crop")).toEqual(avatarUrl2);
- expect(getThumbnailOfSourceHttp).toHaveBeenCalledWith(128, 256, "crop");
+ expect(avatarUrlForRoom(room, 128, 256, "crop")).toEqual(
+ mediaFromMxc(avatarUrl2).getThumbnailOfSourceHttp(128, 256, "crop"),
+ );
});
});
diff --git a/test/components/structures/__snapshots__/RoomView-test.tsx.snap b/test/components/structures/__snapshots__/RoomView-test.tsx.snap
index 47318525d56..c81e180c421 100644
--- a/test/components/structures/__snapshots__/RoomView-test.tsx.snap
+++ b/test/components/structures/__snapshots__/RoomView-test.tsx.snap
@@ -20,22 +20,16 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
U
-
@@ -119,22 +113,16 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
U
-
@@ -215,23 +203,17 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
aria-live="off"
class="mx_AccessibleButton mx_BaseAvatar"
role="button"
+ style="width: 52px; height: 52px;"
tabindex="0"
>
U
-
@user:example.com
@@ -314,22 +296,16 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
U
-
@@ -410,23 +386,17 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
aria-live="off"
class="mx_AccessibleButton mx_BaseAvatar"
role="button"
+ style="width: 52px; height: 52px;"
tabindex="0"
>
U
-
@user:example.com
@@ -581,22 +551,16 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
U
-
@@ -672,23 +636,17 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
aria-live="off"
class="mx_AccessibleButton mx_BaseAvatar"
role="button"
+ style="width: 52px; height: 52px;"
tabindex="0"
>
U
-
@user:example.com
diff --git a/test/components/structures/__snapshots__/UserMenu-test.tsx.snap b/test/components/structures/__snapshots__/UserMenu-test.tsx.snap
index 769711434a8..0546900abb3 100644
--- a/test/components/structures/__snapshots__/UserMenu-test.tsx.snap
+++ b/test/components/structures/__snapshots__/UserMenu-test.tsx.snap
@@ -20,22 +20,16 @@ exports[` when rendered should render as expected 1`] = `
diff --git a/test/components/views/avatars/BaseAvatar-test.tsx b/test/components/views/avatars/BaseAvatar-test.tsx
new file mode 100644
index 00000000000..294a64c4362
--- /dev/null
+++ b/test/components/views/avatars/BaseAvatar-test.tsx
@@ -0,0 +1,201 @@
+/*
+Copyright 2023 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { fireEvent, render } from "@testing-library/react";
+import { ClientEvent, PendingEventOrdering } from "matrix-js-sdk/src/client";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { RoomMember } from "matrix-js-sdk/src/models/room-member";
+import React from "react";
+import { act } from "react-dom/test-utils";
+import { SyncState } from "matrix-js-sdk/src/sync";
+
+import type { MatrixClient } from "matrix-js-sdk/src/client";
+import RoomContext from "../../../../src/contexts/RoomContext";
+import { getRoomContext } from "../../../test-utils/room";
+import { stubClient } from "../../../test-utils/test-utils";
+import BaseAvatar from "../../../../src/components/views/avatars/BaseAvatar";
+import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
+
+type Props = React.ComponentPropsWithoutRef;
+
+describe("", () => {
+ let client: MatrixClient;
+ let room: Room;
+ let member: RoomMember;
+
+ function getComponent(props: Partial) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ function failLoadingImg(container: HTMLElement): void {
+ const img = container.querySelector("img")!;
+ expect(img).not.toBeNull();
+ act(() => {
+ fireEvent.error(img);
+ });
+ }
+
+ function emitReconnect(): void {
+ act(() => {
+ client.emit(ClientEvent.Sync, SyncState.Prepared, SyncState.Reconnecting);
+ });
+ }
+
+ beforeEach(() => {
+ client = stubClient();
+
+ room = new Room("!room:example.com", client, client.getUserId() ?? "", {
+ pendingEventOrdering: PendingEventOrdering.Detached,
+ });
+
+ member = new RoomMember(room.roomId, "@bob:example.org");
+ jest.spyOn(room, "getMember").mockReturnValue(member);
+ });
+
+ it("renders with minimal properties", () => {
+ const { container } = render(getComponent({}));
+
+ expect(container.querySelector(".mx_BaseAvatar")).not.toBeNull();
+ });
+
+ it("matches snapshot (avatar)", () => {
+ const { container } = render(
+ getComponent({
+ name: "CoolUser22",
+ title: "Hover title",
+ url: "https://example.com/images/avatar.gif",
+ className: "mx_SomethingArbitrary",
+ }),
+ );
+
+ expect(container).toMatchSnapshot();
+ });
+
+ it("matches snapshot (avatar + click)", () => {
+ const { container } = render(
+ getComponent({
+ name: "CoolUser22",
+ title: "Hover title",
+ url: "https://example.com/images/avatar.gif",
+ className: "mx_SomethingArbitrary",
+ onClick: () => {},
+ }),
+ );
+
+ expect(container).toMatchSnapshot();
+ });
+
+ it("matches snapshot (no avatar)", () => {
+ const { container } = render(
+ getComponent({
+ name: "xX_Element_User_Xx",
+ title: ":kiss:",
+ defaultToInitialLetter: true,
+ className: "big-and-bold",
+ }),
+ );
+
+ expect(container).toMatchSnapshot();
+ });
+
+ it("matches snapshot (no avatar + click)", () => {
+ const { container } = render(
+ getComponent({
+ name: "xX_Element_User_Xx",
+ title: ":kiss:",
+ defaultToInitialLetter: true,
+ className: "big-and-bold",
+ onClick: () => {},
+ }),
+ );
+
+ expect(container).toMatchSnapshot();
+ });
+
+ it("uses fallback images", () => {
+ const images = [...Array(10)].map((_, i) => `https://example.com/images/${i}.webp`);
+
+ const { container } = render(
+ getComponent({
+ url: images[0],
+ urls: images.slice(1),
+ }),
+ );
+
+ for (const image of images) {
+ expect(container.querySelector("img")!.src).toBe(image);
+ failLoadingImg(container);
+ }
+ });
+
+ it("re-renders on reconnect", () => {
+ const primary = "https://example.com/image.jpeg";
+ const fallback = "https://example.com/fallback.png";
+ const { container } = render(
+ getComponent({
+ url: primary,
+ urls: [fallback],
+ }),
+ );
+
+ failLoadingImg(container);
+ expect(container.querySelector("img")!.src).toBe(fallback);
+
+ emitReconnect();
+ expect(container.querySelector("img")!.src).toBe(primary);
+ });
+
+ it("renders with an image", () => {
+ const url = "https://example.com/images/small/avatar.gif?size=realBig";
+ const { container } = render(getComponent({ url }));
+
+ const img = container.querySelector("img");
+ expect(img!.src).toBe(url);
+ });
+
+ it("renders the initial letter", () => {
+ const { container } = render(getComponent({ name: "Yellow", defaultToInitialLetter: true }));
+
+ const avatar = container.querySelector(".mx_BaseAvatar_initial")!;
+ expect(avatar.innerHTML).toBe("Y");
+ });
+
+ it.each([{}, { name: "CoolUser22" }, { name: "XxElement_FanxX", defaultToInitialLetter: true }])(
+ "includes a click handler",
+ (props: Partial) => {
+ const onClick = jest.fn();
+
+ const { container } = render(
+ getComponent({
+ ...props,
+ onClick,
+ }),
+ );
+
+ act(() => {
+ fireEvent.click(container.querySelector(".mx_BaseAvatar")!);
+ });
+
+ expect(onClick).toHaveBeenCalled();
+ },
+ );
+});
diff --git a/test/components/views/avatars/MemberAvatar-test.tsx b/test/components/views/avatars/MemberAvatar-test.tsx
index 4895b70f217..3dc793bd929 100644
--- a/test/components/views/avatars/MemberAvatar-test.tsx
+++ b/test/components/views/avatars/MemberAvatar-test.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2022 The Matrix.org Foundation C.I.C.
+Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,19 +14,25 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import { getByTestId, render, waitFor } from "@testing-library/react";
-import { mocked } from "jest-mock";
+import { fireEvent, getByTestId, render } from "@testing-library/react";
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
import { Room } from "matrix-js-sdk/src/models/room";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import React from "react";
+import { act } from "react-dom/test-utils";
import MemberAvatar from "../../../../src/components/views/avatars/MemberAvatar";
import RoomContext from "../../../../src/contexts/RoomContext";
-import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
+import { mediaFromMxc } from "../../../../src/customisations/Media";
+import { ViewUserPayload } from "../../../../src/dispatcher/payloads/ViewUserPayload";
+import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
+import { SettingLevel } from "../../../../src/settings/SettingLevel";
import SettingsStore from "../../../../src/settings/SettingsStore";
import { getRoomContext } from "../../../test-utils/room";
import { stubClient } from "../../../test-utils/test-utils";
+import { Action } from "../../../../src/dispatcher/actions";
+
+type Props = React.ComponentPropsWithoutRef;
describe("MemberAvatar", () => {
const ROOM_ID = "roomId";
@@ -35,7 +41,7 @@ describe("MemberAvatar", () => {
let room: Room;
let member: RoomMember;
- function getComponent(props) {
+ function getComponent(props: Partial) {
return (
@@ -44,10 +50,7 @@ describe("MemberAvatar", () => {
}
beforeEach(() => {
- jest.clearAllMocks();
-
- stubClient();
- mockClient = mocked(MatrixClientPeg.get());
+ mockClient = stubClient();
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", {
pendingEventOrdering: PendingEventOrdering.Detached,
@@ -55,22 +58,77 @@ describe("MemberAvatar", () => {
member = new RoomMember(ROOM_ID, "@bob:example.org");
jest.spyOn(room, "getMember").mockReturnValue(member);
+ });
+
+ it("supports 'null' members", () => {
+ const { container } = render(getComponent({ member: null }));
+
+ expect(container.querySelector("img")).not.toBeNull();
+ });
+
+ it("matches the snapshot", () => {
jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue("http://placekitten.com/400/400");
+ const { container } = render(
+ getComponent({
+ member,
+ fallbackUserId: "Fallback User ID",
+ title: "Hover title",
+ style: {
+ color: "pink",
+ },
+ }),
+ );
+
+ expect(container).toMatchSnapshot();
});
- it("shows an avatar for useOnlyCurrentProfiles", async () => {
- jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) => {
- return settingName === "useOnlyCurrentProfiles";
- });
+ it("shows an avatar for useOnlyCurrentProfiles", () => {
+ jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue("http://placekitten.com/400/400");
+
+ SettingsStore.setValue("useOnlyCurrentProfiles", null, SettingLevel.DEVICE, true);
const { container } = render(getComponent({}));
- let avatar: HTMLElement;
- await waitFor(() => {
- avatar = getByTestId(container, "avatar-img");
- expect(avatar).toBeInTheDocument();
+ const avatar = getByTestId(container, "avatar-img");
+ expect(avatar).toBeInTheDocument();
+ expect(avatar.getAttribute("src")).not.toBe("");
+ });
+
+ it("uses the member's configured avatar", () => {
+ const mxcUrl = "mxc://example.com/avatars/user.tiff";
+ jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue(mxcUrl);
+
+ const { container } = render(getComponent({ member }));
+
+ const img = container.querySelector("img");
+ expect(img).not.toBeNull();
+ expect(img!.src).toBe(mediaFromMxc(mxcUrl).srcHttp);
+ });
+
+ it("uses a fallback when the member has no avatar", () => {
+ jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue(undefined);
+
+ const { container } = render(getComponent({ member }));
+
+ const img = container.querySelector(".mx_BaseAvatar_image");
+ expect(img).not.toBeNull();
+ });
+
+ it("dispatches on click", () => {
+ const { container } = render(getComponent({ member, viewUserOnClick: true }));
+
+ const spy = jest.spyOn(defaultDispatcher, "dispatch");
+
+ act(() => {
+ fireEvent.click(container.querySelector(".mx_BaseAvatar")!);
});
- expect(avatar!.getAttribute("src")).not.toBe("");
+ expect(spy).toHaveBeenCalled();
+ const [payload] = spy.mock.lastCall!;
+ expect(payload).toStrictEqual({
+ action: Action.ViewUser,
+ member,
+ push: false,
+ });
});
});
diff --git a/test/components/views/avatars/RoomAvatar-test.tsx b/test/components/views/avatars/RoomAvatar-test.tsx
index e23cd96f02d..7be7dd65e90 100644
--- a/test/components/views/avatars/RoomAvatar-test.tsx
+++ b/test/components/views/avatars/RoomAvatar-test.tsx
@@ -39,7 +39,7 @@ describe("RoomAvatar", () => {
const dmRoomMap = new DMRoomMap(client);
jest.spyOn(dmRoomMap, "getUserIdForRoomId");
jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap);
- jest.spyOn(AvatarModule, "defaultAvatarUrlForString");
+ jest.spyOn(AvatarModule, "getColorForString");
});
afterAll(() => {
@@ -48,14 +48,14 @@ describe("RoomAvatar", () => {
afterEach(() => {
mocked(DMRoomMap.shared().getUserIdForRoomId).mockReset();
- mocked(AvatarModule.defaultAvatarUrlForString).mockClear();
+ mocked(AvatarModule.getColorForString).mockClear();
});
it("should render as expected for a Room", () => {
const room = new Room("!room:example.com", client, client.getSafeUserId());
room.name = "test room";
expect(render().container).toMatchSnapshot();
- expect(AvatarModule.defaultAvatarUrlForString).toHaveBeenCalledWith(room.roomId);
+ expect(AvatarModule.getColorForString).toHaveBeenCalledWith(room.roomId);
});
it("should render as expected for a DM room", () => {
@@ -64,7 +64,7 @@ describe("RoomAvatar", () => {
room.name = "DM room";
mocked(DMRoomMap.shared().getUserIdForRoomId).mockReturnValue(userId);
expect(render().container).toMatchSnapshot();
- expect(AvatarModule.defaultAvatarUrlForString).toHaveBeenCalledWith(userId);
+ expect(AvatarModule.getColorForString).toHaveBeenCalledWith(userId);
});
it("should render as expected for a LocalRoom", () => {
@@ -73,6 +73,6 @@ describe("RoomAvatar", () => {
localRoom.name = "local test room";
localRoom.targets.push(new DirectoryMember({ user_id: userId }));
expect(render().container).toMatchSnapshot();
- expect(AvatarModule.defaultAvatarUrlForString).toHaveBeenCalledWith(userId);
+ expect(AvatarModule.getColorForString).toHaveBeenCalledWith(userId);
});
});
diff --git a/test/components/views/avatars/__snapshots__/BaseAvatar-test.tsx.snap b/test/components/views/avatars/__snapshots__/BaseAvatar-test.tsx.snap
new file mode 100644
index 00000000000..da62540b90e
--- /dev/null
+++ b/test/components/views/avatars/__snapshots__/BaseAvatar-test.tsx.snap
@@ -0,0 +1,72 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` matches snapshot (avatar + click) 1`] = `
+
+
+
+`;
+
+exports[` matches snapshot (avatar) 1`] = `
+
+
+
+`;
+
+exports[` matches snapshot (no avatar + click) 1`] = `
+
+
+
+ X
+
+
+
+`;
+
+exports[` matches snapshot (no avatar) 1`] = `
+
+
+
+ X
+
+
+
+`;
diff --git a/test/components/views/avatars/__snapshots__/MemberAvatar-test.tsx.snap b/test/components/views/avatars/__snapshots__/MemberAvatar-test.tsx.snap
new file mode 100644
index 00000000000..feda79035cd
--- /dev/null
+++ b/test/components/views/avatars/__snapshots__/MemberAvatar-test.tsx.snap
@@ -0,0 +1,14 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`MemberAvatar matches the snapshot 1`] = `
+
+
+
+`;
diff --git a/test/components/views/avatars/__snapshots__/RoomAvatar-test.tsx.snap b/test/components/views/avatars/__snapshots__/RoomAvatar-test.tsx.snap
index 6bffa157b63..699113689e4 100644
--- a/test/components/views/avatars/__snapshots__/RoomAvatar-test.tsx.snap
+++ b/test/components/views/avatars/__snapshots__/RoomAvatar-test.tsx.snap
@@ -5,22 +5,16 @@ exports[`RoomAvatar should render as expected for a DM room 1`] = `
D
-
`;
@@ -30,22 +24,16 @@ exports[`RoomAvatar should render as expected for a LocalRoom 1`] = `
L
-
`;
@@ -55,22 +43,16 @@ exports[`RoomAvatar should render as expected for a Room 1`] = `
T
-
`;
diff --git a/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap b/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap
index b42ccb83ee3..b965f50b2f9 100644
--- a/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap
+++ b/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap
@@ -13,23 +13,17 @@ exports[` renders marker when beacon has location 1`] = `
A
-
diff --git a/test/components/views/rooms/RoomHeader-test.tsx b/test/components/views/rooms/RoomHeader-test.tsx
index 5857e282957..c27d4c0c20f 100644
--- a/test/components/views/rooms/RoomHeader-test.tsx
+++ b/test/components/views/rooms/RoomHeader-test.tsx
@@ -72,7 +72,7 @@ describe("RoomHeader (Enzyme)", () => {
// And there is no image avatar (because it's not set on this room)
const image = findImg(rendered, ".mx_BaseAvatar_image");
- expect(image.prop("src")).toEqual("data:image/png;base64,00");
+ expect(image).toBeTruthy();
});
it("shows the room avatar in a room with 2 people", () => {
@@ -86,7 +86,7 @@ describe("RoomHeader (Enzyme)", () => {
// And there is no image avatar (because it's not set on this room)
const image = findImg(rendered, ".mx_BaseAvatar_image");
- expect(image.prop("src")).toEqual("data:image/png;base64,00");
+ expect(image).toBeTruthy();
});
it("shows the room avatar in a room with >2 people", () => {
@@ -100,7 +100,7 @@ describe("RoomHeader (Enzyme)", () => {
// And there is no image avatar (because it's not set on this room)
const image = findImg(rendered, ".mx_BaseAvatar_image");
- expect(image.prop("src")).toEqual("data:image/png;base64,00");
+ expect(image).toBeTruthy();
});
it("shows the room avatar in a DM with only ourselves", () => {
@@ -114,7 +114,7 @@ describe("RoomHeader (Enzyme)", () => {
// And there is no image avatar (because it's not set on this room)
const image = findImg(rendered, ".mx_BaseAvatar_image");
- expect(image.prop("src")).toEqual("data:image/png;base64,00");
+ expect(image).toBeTruthy();
});
it("shows the user avatar in a DM with 2 people", () => {
@@ -148,7 +148,7 @@ describe("RoomHeader (Enzyme)", () => {
// And there is no image avatar (because it's not set on this room)
const image = findImg(rendered, ".mx_BaseAvatar_image");
- expect(image.prop("src")).toEqual("data:image/png;base64,00");
+ expect(image).toBeTruthy();
});
it("renders call buttons normally", () => {
diff --git a/test/components/views/rooms/__snapshots__/RoomPreviewBar-test.tsx.snap b/test/components/views/rooms/__snapshots__/RoomPreviewBar-test.tsx.snap
index f35467e1efd..a78a452e890 100644
--- a/test/components/views/rooms/__snapshots__/RoomPreviewBar-test.tsx.snap
+++ b/test/components/views/rooms/__snapshots__/RoomPreviewBar-test.tsx.snap
@@ -161,22 +161,16 @@ exports[` with an invite without an invited email for a dm roo
R
-
@@ -236,22 +230,16 @@ exports[` with an invite without an invited email for a non-dm
R
-
diff --git a/test/components/views/rooms/__snapshots__/RoomTile-test.tsx.snap b/test/components/views/rooms/__snapshots__/RoomTile-test.tsx.snap
index bcbb7932c69..557d97c243e 100644
--- a/test/components/views/rooms/__snapshots__/RoomTile-test.tsx.snap
+++ b/test/components/views/rooms/__snapshots__/RoomTile-test.tsx.snap
@@ -15,22 +15,16 @@ exports[`RoomTile should render the room 1`] = `
!
-
+ !room:example.com
+
+`;
+
+exports[`RoomPillPart matches snapshot (no avatar) 1`] = `
+
+ !room:example.com
+
+`;
+
+exports[`UserPillPart matches snapshot (avatar) 1`] = `
+
+ DisplayName
+
+`;
+
+exports[`UserPillPart matches snapshot (no avatar) 1`] = `
+
+ DisplayName
+
+`;
diff --git a/test/editor/parts-test.ts b/test/editor/parts-test.ts
index 534221ece3a..31c620c94ad 100644
--- a/test/editor/parts-test.ts
+++ b/test/editor/parts-test.ts
@@ -1,5 +1,5 @@
/*
-Copyright 2022 The Matrix.org Foundation C.I.C.
+Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,7 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import { EmojiPart, PlainPart } from "../../src/editor/parts";
+import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix";
+
+import { EmojiPart, PartCreator, PlainPart } from "../../src/editor/parts";
+import DMRoomMap from "../../src/utils/DMRoomMap";
+import { stubClient } from "../test-utils";
import { createPartCreator } from "./mock";
describe("editor/parts", () => {
@@ -40,3 +44,67 @@ describe("editor/parts", () => {
expect(() => part.toDOMNode()).not.toThrow();
});
});
+
+describe("UserPillPart", () => {
+ const roomId = "!room:example.com";
+ let client: MatrixClient;
+ let room: Room;
+ let creator: PartCreator;
+
+ beforeEach(() => {
+ client = stubClient();
+ room = new Room(roomId, client, "@me:example.com");
+ creator = new PartCreator(room, client);
+ });
+
+ it("matches snapshot (no avatar)", () => {
+ jest.spyOn(room, "getMember").mockReturnValue(new RoomMember(room.roomId, "@user:example.com"));
+ const pill = creator.userPill("DisplayName", "@user:example.com");
+ const el = pill.toDOMNode();
+
+ expect(el).toMatchSnapshot();
+ });
+
+ it("matches snapshot (avatar)", () => {
+ const member = new RoomMember(room.roomId, "@user:example.com");
+ jest.spyOn(room, "getMember").mockReturnValue(member);
+ jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue("mxc://www.example.com/avatar.png");
+
+ const pill = creator.userPill("DisplayName", "@user:example.com");
+ const el = pill.toDOMNode();
+
+ expect(el).toMatchSnapshot();
+ });
+});
+
+describe("RoomPillPart", () => {
+ const roomId = "!room:example.com";
+ let client: jest.Mocked
;
+ let room: Room;
+ let creator: PartCreator;
+
+ beforeEach(() => {
+ client = stubClient() as jest.Mocked;
+ DMRoomMap.makeShared();
+
+ room = new Room(roomId, client, "@me:example.com");
+ client.getRoom.mockReturnValue(room);
+ creator = new PartCreator(room, client);
+ });
+
+ it("matches snapshot (no avatar)", () => {
+ jest.spyOn(room, "getMxcAvatarUrl").mockReturnValue(null);
+ const pill = creator.roomPill("super-secret clubhouse");
+ const el = pill.toDOMNode();
+
+ expect(el).toMatchSnapshot();
+ });
+
+ it("matches snapshot (avatar)", () => {
+ jest.spyOn(room, "getMxcAvatarUrl").mockReturnValue("mxc://www.example.com/avatars/room1.jpeg");
+ const pill = creator.roomPill("cool chat club");
+ const el = pill.toDOMNode();
+
+ expect(el).toMatchSnapshot();
+ });
+});
From c7b01af49e648482707792d4292a6e7f211feb2c Mon Sep 17 00:00:00 2001
From: AHMAD KADRI <52747422+ahmadkadri@users.noreply.github.com>
Date: Mon, 30 Jan 2023 10:54:05 +0100
Subject: [PATCH 21/97] Should open new 1:1 chat room after leaving the old one
(#9880)
* should open new 1:1 chat room after leaving the old one
Signed-off-by: Ahmad Kadri
* change the copyright
* update the test
Signed-off-by: AHMAD KADRI <52747422+ahmadkadri@users.noreply.github.com>
---------
Signed-off-by: Ahmad Kadri
Signed-off-by: AHMAD KADRI <52747422+ahmadkadri@users.noreply.github.com>
Co-authored-by: Oliver Sand
Co-authored-by: Dominik Henneke
---
.../one-to-one-chat/one-to-one-chat.spec.ts | 61 +++++++++++++++++++
src/components/structures/MatrixChat.tsx | 8 +--
2 files changed, 65 insertions(+), 4 deletions(-)
create mode 100644 cypress/e2e/one-to-one-chat/one-to-one-chat.spec.ts
diff --git a/cypress/e2e/one-to-one-chat/one-to-one-chat.spec.ts b/cypress/e2e/one-to-one-chat/one-to-one-chat.spec.ts
new file mode 100644
index 00000000000..897b916105e
--- /dev/null
+++ b/cypress/e2e/one-to-one-chat/one-to-one-chat.spec.ts
@@ -0,0 +1,61 @@
+/*
+Copyright 2023 Ahmad Kadri
+Copyright 2023 Nordeck IT + Consulting GmbH.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+///
+
+import { HomeserverInstance } from "../../plugins/utils/homeserver";
+import { Credentials } from "../../support/homeserver";
+
+describe("1:1 chat room", () => {
+ let homeserver: HomeserverInstance;
+ let user2: Credentials;
+
+ const username = "user1234";
+ const password = "p4s5W0rD";
+
+ beforeEach(() => {
+ cy.startHomeserver("default").then((data) => {
+ homeserver = data;
+
+ cy.initTestUser(homeserver, "Jeff");
+ cy.registerUser(homeserver, username, password).then((credential) => {
+ user2 = credential;
+ cy.visit(`/#/user/${user2.userId}?action=chat`);
+ });
+ });
+ });
+
+ afterEach(() => {
+ cy.stopHomeserver(homeserver);
+ });
+
+ it("should open new 1:1 chat room after leaving the old one", () => {
+ // leave 1:1 chat room
+ cy.contains(".mx_RoomHeader_nametext", username).click();
+ cy.contains('[role="menuitem"]', "Leave").click();
+ cy.get('[data-testid="dialog-primary-button"]').click();
+
+ // wait till the room was left
+ cy.get('[role="group"][aria-label="Historical"]').within(() => {
+ cy.contains(".mx_RoomTile", username);
+ });
+
+ // open new 1:1 chat room
+ cy.visit(`/#/user/${user2.userId}?action=chat`);
+ cy.contains(".mx_RoomHeader_nametext", username);
+ });
+});
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index 429adb6f504..e517aaaf83d 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -138,6 +138,7 @@ import { cleanUpBroadcasts, VoiceBroadcastResumer } from "../../voice-broadcast"
import GenericToast from "../views/toasts/GenericToast";
import { Linkify } from "../views/elements/Linkify";
import RovingSpotlightDialog, { Filter } from "../views/dialogs/spotlight/SpotlightDialog";
+import { findDMForUser } from "../../utils/dm/findDMForUser";
// legacy export
export { default as Views } from "../../Views";
@@ -1101,13 +1102,12 @@ export default class MatrixChat extends React.PureComponent {
// TODO: Immutable DMs replaces this
const client = MatrixClientPeg.get();
- const dmRoomMap = new DMRoomMap(client);
- const dmRooms = dmRoomMap.getDMRoomsForUserId(userId);
+ const dmRoom = findDMForUser(client, userId);
- if (dmRooms.length > 0) {
+ if (dmRoom) {
dis.dispatch({
action: Action.ViewRoom,
- room_id: dmRooms[0],
+ room_id: dmRoom.roomId,
metricsTrigger: "MessageUser",
});
} else {
From a21929dba00491e026926d2cdea51a4457b18313 Mon Sep 17 00:00:00 2001
From: Andy Balaam
Date: Mon, 30 Jan 2023 10:02:32 +0000
Subject: [PATCH 22/97] Convert RoomCreate to a functional component (#9999)
---
src/components/views/messages/RoomCreate.tsx | 78 +++++++++----------
src/events/EventTileFactory.tsx | 4 +-
.../views/messages/RoomCreate-test.tsx | 2 +-
3 files changed, 42 insertions(+), 42 deletions(-)
diff --git a/src/components/views/messages/RoomCreate.tsx b/src/components/views/messages/RoomCreate.tsx
index 8bff5dfdcc5..25cfb90a488 100644
--- a/src/components/views/messages/RoomCreate.tsx
+++ b/src/components/views/messages/RoomCreate.tsx
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from "react";
+import React, { useCallback } from "react";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import dis from "../../../dispatcher/dispatcher";
@@ -36,44 +36,44 @@ interface IProps {
* A message tile showing that this room was created as an upgrade of a previous
* room.
*/
-export default class RoomCreate extends React.Component {
- private onLinkClicked = (e: React.MouseEvent): void => {
- e.preventDefault();
+export const RoomCreate: React.FC = ({ mxEvent, timestamp }) => {
+ const onLinkClicked = useCallback(
+ (e: React.MouseEvent): void => {
+ e.preventDefault();
- const predecessor = this.props.mxEvent.getContent()["predecessor"];
+ const predecessor = mxEvent.getContent()["predecessor"];
- dis.dispatch({
- action: Action.ViewRoom,
- event_id: predecessor["event_id"],
- highlighted: true,
- room_id: predecessor["room_id"],
- metricsTrigger: "Predecessor",
- metricsViaKeyboard: e.type !== "click",
- });
- };
-
- public render(): JSX.Element {
- const predecessor = this.props.mxEvent.getContent()["predecessor"];
- if (predecessor === undefined) {
- return ; // We should never have been instantiated in this case
- }
- const prevRoom = MatrixClientPeg.get().getRoom(predecessor["room_id"]);
- const permalinkCreator = new RoomPermalinkCreator(prevRoom, predecessor["room_id"]);
- permalinkCreator.load();
- const predecessorPermalink = permalinkCreator.forEvent(predecessor["event_id"]);
- const link = (
-
- {_t("Click here to see older messages.")}
-
- );
-
- return (
-
- );
+ dis.dispatch({
+ action: Action.ViewRoom,
+ event_id: predecessor["event_id"],
+ highlighted: true,
+ room_id: predecessor["room_id"],
+ metricsTrigger: "Predecessor",
+ metricsViaKeyboard: e.type !== "click",
+ });
+ },
+ [mxEvent],
+ );
+ const predecessor = mxEvent.getContent()["predecessor"];
+ if (predecessor === undefined) {
+ return ; // We should never have been instantiated in this case
}
-}
+ const prevRoom = MatrixClientPeg.get().getRoom(predecessor["room_id"]);
+ const permalinkCreator = new RoomPermalinkCreator(prevRoom, predecessor["room_id"]);
+ permalinkCreator.load();
+ const predecessorPermalink = permalinkCreator.forEvent(predecessor["event_id"]);
+ const link = (
+
+ {_t("Click here to see older messages.")}
+
+ );
+
+ return (
+
+ );
+};
diff --git a/src/events/EventTileFactory.tsx b/src/events/EventTileFactory.tsx
index 1d30416f0f9..d6b5e18ad53 100644
--- a/src/events/EventTileFactory.tsx
+++ b/src/events/EventTileFactory.tsx
@@ -33,7 +33,7 @@ import LegacyCallEvent from "../components/views/messages/LegacyCallEvent";
import { CallEvent } from "../components/views/messages/CallEvent";
import TextualEvent from "../components/views/messages/TextualEvent";
import EncryptionEvent from "../components/views/messages/EncryptionEvent";
-import RoomCreate from "../components/views/messages/RoomCreate";
+import { RoomCreate } from "../components/views/messages/RoomCreate";
import RoomAvatarEvent from "../components/views/messages/RoomAvatarEvent";
import { WIDGET_LAYOUT_EVENT_TYPE } from "../stores/widgets/WidgetLayoutStore";
import { ALL_RULE_TYPES } from "../mjolnir/BanList";
@@ -101,7 +101,7 @@ const EVENT_TILE_TYPES = new Map([
const STATE_EVENT_TILE_TYPES = new Map([
[EventType.RoomEncryption, (ref, props) => ],
[EventType.RoomCanonicalAlias, TextualEventFactory],
- [EventType.RoomCreate, (ref, props) => ],
+ [EventType.RoomCreate, (_ref, props) => ],
[EventType.RoomMember, TextualEventFactory],
[EventType.RoomName, TextualEventFactory],
[EventType.RoomAvatar, (ref, props) => ],
diff --git a/test/components/views/messages/RoomCreate-test.tsx b/test/components/views/messages/RoomCreate-test.tsx
index 31763f9ae89..09f17e2ae4f 100644
--- a/test/components/views/messages/RoomCreate-test.tsx
+++ b/test/components/views/messages/RoomCreate-test.tsx
@@ -22,7 +22,7 @@ import { EventType, MatrixEvent } from "matrix-js-sdk/src/matrix";
import dis from "../../../../src/dispatcher/dispatcher";
import SettingsStore from "../../../../src/settings/SettingsStore";
-import RoomCreate from "../../../../src/components/views/messages/RoomCreate";
+import { RoomCreate } from "../../../../src/components/views/messages/RoomCreate";
import { stubClient } from "../../../test-utils/test-utils";
import { Action } from "../../../../src/dispatcher/actions";
From 3e2bf5640e17ff1c096a746089c87f8cf93e174a Mon Sep 17 00:00:00 2001
From: Germain
Date: Mon, 30 Jan 2023 12:20:11 +0000
Subject: [PATCH 23/97] Update to supportsThreads (#9907)
---
src/MatrixClientPeg.ts | 2 +-
src/utils/EventUtils.ts | 6 +-----
test/Notifier-test.ts | 2 +-
test/Unread-test.ts | 2 +-
test/components/structures/RoomSearchView-test.tsx | 2 +-
test/components/structures/ThreadPanel-test.tsx | 2 +-
test/components/structures/ThreadView-test.tsx | 2 +-
test/components/structures/TimelinePanel-test.tsx | 4 ++--
.../components/views/right_panel/RoomHeaderButtons-test.tsx | 2 +-
test/components/views/rooms/EventTile-test.tsx | 2 +-
.../NotificationBadge/UnreadNotificationBadge-test.tsx | 2 +-
test/components/views/settings/Notifications-test.tsx | 2 +-
test/stores/RoomViewStore-test.ts | 2 +-
test/test-utils/test-utils.ts | 2 +-
test/utils/EventUtils-test.ts | 2 +-
15 files changed, 16 insertions(+), 20 deletions(-)
diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts
index f674892bf7c..19a9eb5fde8 100644
--- a/src/MatrixClientPeg.ts
+++ b/src/MatrixClientPeg.ts
@@ -218,7 +218,7 @@ class MatrixClientPegClass implements IMatrixClientPeg {
opts.pendingEventOrdering = PendingEventOrdering.Detached;
opts.lazyLoadMembers = true;
opts.clientWellKnownPollPeriod = 2 * 60 * 60; // 2 hours
- opts.experimentalThreadSupport = SettingsStore.getValue("feature_threadenabled");
+ opts.threadSupport = SettingsStore.getValue("feature_threadenabled");
if (SettingsStore.getValue("feature_sliding_sync")) {
const proxyUrl = SettingsStore.getValue("feature_sliding_sync_proxy_url");
diff --git a/src/utils/EventUtils.ts b/src/utils/EventUtils.ts
index e80ffb83e20..5e650018a0e 100644
--- a/src/utils/EventUtils.ts
+++ b/src/utils/EventUtils.ts
@@ -229,11 +229,7 @@ export async function fetchInitialEvent(
initialEvent = null;
}
- if (
- client.supportsExperimentalThreads() &&
- initialEvent?.isRelation(THREAD_RELATION_TYPE.name) &&
- !initialEvent.getThread()
- ) {
+ if (client.supportsThreads() && initialEvent?.isRelation(THREAD_RELATION_TYPE.name) && !initialEvent.getThread()) {
const threadId = initialEvent.threadRootId;
const room = client.getRoom(roomId);
const mapper = client.getEventMapper();
diff --git a/test/Notifier-test.ts b/test/Notifier-test.ts
index 20b2c2e3610..e2b867ee7f5 100644
--- a/test/Notifier-test.ts
+++ b/test/Notifier-test.ts
@@ -109,7 +109,7 @@ describe("Notifier", () => {
decryptEventIfNeeded: jest.fn(),
getRoom: jest.fn(),
getPushActionsForEvent: jest.fn(),
- supportsExperimentalThreads: jest.fn().mockReturnValue(false),
+ supportsThreads: jest.fn().mockReturnValue(false),
});
mockClient.pushRules = {
diff --git a/test/Unread-test.ts b/test/Unread-test.ts
index 8ff759b142b..4cfb7f265a7 100644
--- a/test/Unread-test.ts
+++ b/test/Unread-test.ts
@@ -124,7 +124,7 @@ describe("Unread", () => {
const myId = client.getUserId()!;
beforeAll(() => {
- client.supportsExperimentalThreads = () => true;
+ client.supportsThreads = () => true;
});
beforeEach(() => {
diff --git a/test/components/structures/RoomSearchView-test.tsx b/test/components/structures/RoomSearchView-test.tsx
index 26786956b5e..35e297b7a1a 100644
--- a/test/components/structures/RoomSearchView-test.tsx
+++ b/test/components/structures/RoomSearchView-test.tsx
@@ -48,7 +48,7 @@ describe("", () => {
beforeEach(async () => {
stubClient();
client = MatrixClientPeg.get();
- client.supportsExperimentalThreads = jest.fn().mockReturnValue(true);
+ client.supportsThreads = jest.fn().mockReturnValue(true);
room = new Room("!room:server", client, client.getUserId());
mocked(client.getRoom).mockReturnValue(room);
permalinkCreator = new RoomPermalinkCreator(room, room.roomId);
diff --git a/test/components/structures/ThreadPanel-test.tsx b/test/components/structures/ThreadPanel-test.tsx
index b868549da4e..20c6cf03973 100644
--- a/test/components/structures/ThreadPanel-test.tsx
+++ b/test/components/structures/ThreadPanel-test.tsx
@@ -161,7 +161,7 @@ describe("ThreadPanel", () => {
Thread.setServerSideSupport(FeatureSupport.Stable);
Thread.setServerSideListSupport(FeatureSupport.Stable);
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
- jest.spyOn(mockClient, "supportsExperimentalThreads").mockReturnValue(true);
+ jest.spyOn(mockClient, "supportsThreads").mockReturnValue(true);
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", {
pendingEventOrdering: PendingEventOrdering.Detached,
diff --git a/test/components/structures/ThreadView-test.tsx b/test/components/structures/ThreadView-test.tsx
index c578121e461..ef6b195a2dc 100644
--- a/test/components/structures/ThreadView-test.tsx
+++ b/test/components/structures/ThreadView-test.tsx
@@ -117,7 +117,7 @@ describe("ThreadView", () => {
stubClient();
mockPlatformPeg();
mockClient = mocked(MatrixClientPeg.get());
- jest.spyOn(mockClient, "supportsExperimentalThreads").mockReturnValue(true);
+ jest.spyOn(mockClient, "supportsThreads").mockReturnValue(true);
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", {
pendingEventOrdering: PendingEventOrdering.Detached,
diff --git a/test/components/structures/TimelinePanel-test.tsx b/test/components/structures/TimelinePanel-test.tsx
index e66a015a5c5..eaeb19d8ec0 100644
--- a/test/components/structures/TimelinePanel-test.tsx
+++ b/test/components/structures/TimelinePanel-test.tsx
@@ -362,7 +362,7 @@ describe("TimelinePanel", () => {
client = MatrixClientPeg.get();
Thread.hasServerSideSupport = FeatureSupport.Stable;
- client.supportsExperimentalThreads = () => true;
+ client.supportsThreads = () => true;
const getValueCopy = SettingsStore.getValue;
SettingsStore.getValue = jest.fn().mockImplementation((name: string) => {
if (name === "feature_threadenabled") return true;
@@ -524,7 +524,7 @@ describe("TimelinePanel", () => {
const client = MatrixClientPeg.get();
client.isRoomEncrypted = () => true;
- client.supportsExperimentalThreads = () => true;
+ client.supportsThreads = () => true;
client.decryptEventIfNeeded = () => Promise.resolve();
const authorId = client.getUserId()!;
const room = new Room("roomId", client, authorId, {
diff --git a/test/components/views/right_panel/RoomHeaderButtons-test.tsx b/test/components/views/right_panel/RoomHeaderButtons-test.tsx
index 06192ccc232..8df11a76028 100644
--- a/test/components/views/right_panel/RoomHeaderButtons-test.tsx
+++ b/test/components/views/right_panel/RoomHeaderButtons-test.tsx
@@ -38,7 +38,7 @@ describe("RoomHeaderButtons-test.tsx", function () {
stubClient();
client = MatrixClientPeg.get();
- client.supportsExperimentalThreads = () => true;
+ client.supportsThreads = () => true;
room = new Room(ROOM_ID, client, client.getUserId() ?? "", {
pendingEventOrdering: PendingEventOrdering.Detached,
});
diff --git a/test/components/views/rooms/EventTile-test.tsx b/test/components/views/rooms/EventTile-test.tsx
index c3c0b55ab5b..e3f28d851ae 100644
--- a/test/components/views/rooms/EventTile-test.tsx
+++ b/test/components/views/rooms/EventTile-test.tsx
@@ -92,7 +92,7 @@ describe("EventTile", () => {
describe("EventTile thread summary", () => {
beforeEach(() => {
- jest.spyOn(client, "supportsExperimentalThreads").mockReturnValue(true);
+ jest.spyOn(client, "supportsThreads").mockReturnValue(true);
});
it("removes the thread summary when thread is deleted", async () => {
diff --git a/test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx b/test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx
index cfa44165765..9c60d26e265 100644
--- a/test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx
+++ b/test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx
@@ -48,7 +48,7 @@ describe("UnreadNotificationBadge", () => {
}
beforeAll(() => {
- client.supportsExperimentalThreads = () => true;
+ client.supportsThreads = () => true;
});
beforeEach(() => {
diff --git a/test/components/views/settings/Notifications-test.tsx b/test/components/views/settings/Notifications-test.tsx
index b33f72838ad..aedb96fb130 100644
--- a/test/components/views/settings/Notifications-test.tsx
+++ b/test/components/views/settings/Notifications-test.tsx
@@ -225,7 +225,7 @@ describe("", () => {
}),
setAccountData: jest.fn(),
sendReadReceipt: jest.fn(),
- supportsExperimentalThreads: jest.fn().mockReturnValue(true),
+ supportsThreads: jest.fn().mockReturnValue(true),
});
mockClient.getPushRules.mockResolvedValue(pushRules);
diff --git a/test/stores/RoomViewStore-test.ts b/test/stores/RoomViewStore-test.ts
index 7c2b1ec81b3..370fb95bcd8 100644
--- a/test/stores/RoomViewStore-test.ts
+++ b/test/stores/RoomViewStore-test.ts
@@ -93,7 +93,7 @@ describe("RoomViewStore", function () {
getSafeUserId: jest.fn().mockReturnValue(userId),
getDeviceId: jest.fn().mockReturnValue("ABC123"),
sendStateEvent: jest.fn().mockResolvedValue({}),
- supportsExperimentalThreads: jest.fn(),
+ supportsThreads: jest.fn(),
});
const room = new Room(roomId, mockClient, userId);
const room2 = new Room(roomId2, mockClient, userId);
diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts
index ef7bc1dcef3..5938f89cfe3 100644
--- a/test/test-utils/test-utils.ts
+++ b/test/test-utils/test-utils.ts
@@ -166,7 +166,7 @@ export function createTestClient(): MatrixClient {
decryptEventIfNeeded: () => Promise.resolve(),
isUserIgnored: jest.fn().mockReturnValue(false),
getCapabilities: jest.fn().mockResolvedValue({}),
- supportsExperimentalThreads: () => false,
+ supportsThreads: () => false,
getRoomUpgradeHistory: jest.fn().mockReturnValue([]),
getOpenIdToken: jest.fn().mockResolvedValue(undefined),
registerWithIdentityServer: jest.fn().mockResolvedValue({}),
diff --git a/test/utils/EventUtils-test.ts b/test/utils/EventUtils-test.ts
index decf42931a8..9c4012a4c4c 100644
--- a/test/utils/EventUtils-test.ts
+++ b/test/utils/EventUtils-test.ts
@@ -436,7 +436,7 @@ describe("EventUtils", () => {
pendingEventOrdering: PendingEventOrdering.Detached,
});
- jest.spyOn(client, "supportsExperimentalThreads").mockReturnValue(true);
+ jest.spyOn(client, "supportsThreads").mockReturnValue(true);
jest.spyOn(client, "getRoom").mockReturnValue(room);
jest.spyOn(client, "fetchRoomEvent").mockImplementation(async (roomId, eventId) => {
return events[eventId] ?? Promise.reject();
From 4c1e4f5127bd7fc8a3ab852d630242a704ffbda2 Mon Sep 17 00:00:00 2001
From: Clark Fischer <439978+clarkf@users.noreply.github.com>
Date: Mon, 30 Jan 2023 14:31:32 +0000
Subject: [PATCH 24/97] Fix "[object Promise]" appearing in HTML exports
(#9975)
Fixes https://github.com/vector-im/element-web/issues/24272
---
src/DateUtils.ts | 2 +-
src/components/structures/MessagePanel.tsx | 4 +-
.../dialogs/MessageEditHistoryDialog.tsx | 2 +-
.../views/rooms/SearchResultTile.tsx | 7 +-
src/utils/exportUtils/HtmlExport.tsx | 43 ++-
src/utils/exportUtils/exportCSS.ts | 4 +-
.../dialogs/MessageEditHistoryDialog-test.tsx | 83 +++++
.../MessageEditHistoryDialog-test.tsx.snap | 322 ++++++++++++++++++
.../views/rooms/SearchResultTile-test.tsx | 106 +++---
test/test-utils/test-utils.ts | 4 +
test/utils/exportUtils/HTMLExport-test.ts | 286 +++++++++++++++-
.../__snapshots__/HTMLExport-test.ts.snap | 86 +++++
test/utils/exportUtils/exportCSS-test.ts | 26 ++
13 files changed, 893 insertions(+), 82 deletions(-)
create mode 100644 test/components/views/dialogs/MessageEditHistoryDialog-test.tsx
create mode 100644 test/components/views/dialogs/__snapshots__/MessageEditHistoryDialog-test.tsx.snap
create mode 100644 test/utils/exportUtils/exportCSS-test.ts
diff --git a/src/DateUtils.ts b/src/DateUtils.ts
index 5973a7c5f2e..c1aa69aacd6 100644
--- a/src/DateUtils.ts
+++ b/src/DateUtils.ts
@@ -175,7 +175,7 @@ function withinCurrentYear(prevDate: Date, nextDate: Date): boolean {
return prevDate.getFullYear() === nextDate.getFullYear();
}
-export function wantsDateSeparator(prevEventDate: Date, nextEventDate: Date): boolean {
+export function wantsDateSeparator(prevEventDate: Date | undefined, nextEventDate: Date | undefined): boolean {
if (!nextEventDate || !prevEventDate) {
return false;
}
diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx
index 98e8f79ec72..2dd432cb928 100644
--- a/src/components/structures/MessagePanel.tsx
+++ b/src/components/structures/MessagePanel.tsx
@@ -72,7 +72,7 @@ const groupedStateEvents = [
// check if there is a previous event and it has the same sender as this event
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
export function shouldFormContinuation(
- prevEvent: MatrixEvent,
+ prevEvent: MatrixEvent | null,
mxEvent: MatrixEvent,
showHiddenEvents: boolean,
threadsEnabled: boolean,
@@ -821,7 +821,7 @@ export default class MessagePanel extends React.Component {
// here.
return !this.props.canBackPaginate;
}
- return wantsDateSeparator(prevEvent.getDate(), nextEventDate);
+ return wantsDateSeparator(prevEvent.getDate() || undefined, nextEventDate);
}
// Get a list of read receipts that should be shown next to this event
diff --git a/src/components/views/dialogs/MessageEditHistoryDialog.tsx b/src/components/views/dialogs/MessageEditHistoryDialog.tsx
index 943e7f58d2d..8775b4eb5c3 100644
--- a/src/components/views/dialogs/MessageEditHistoryDialog.tsx
+++ b/src/components/views/dialogs/MessageEditHistoryDialog.tsx
@@ -130,7 +130,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
- if (!lastEvent || wantsDateSeparator(lastEvent.getDate(), e.getDate())) {
+ if (!lastEvent || wantsDateSeparator(lastEvent.getDate() || undefined, e.getDate() || undefined)) {
nodes.push(
diff --git a/src/components/views/rooms/SearchResultTile.tsx b/src/components/views/rooms/SearchResultTile.tsx
index 067cbaee383..3ec68b989f4 100644
--- a/src/components/views/rooms/SearchResultTile.tsx
+++ b/src/components/views/rooms/SearchResultTile.tsx
@@ -84,7 +84,7 @@ export default class SearchResultTile extends React.Component {
// is this a continuation of the previous message?
const continuation =
prevEv &&
- !wantsDateSeparator(prevEv.getDate(), mxEv.getDate()) &&
+ !wantsDateSeparator(prevEv.getDate() || undefined, mxEv.getDate() || undefined) &&
shouldFormContinuation(
prevEv,
mxEv,
@@ -96,7 +96,10 @@ export default class SearchResultTile extends React.Component {
let lastInSection = true;
const nextEv = timeline[j + 1];
if (nextEv) {
- const willWantDateSeparator = wantsDateSeparator(mxEv.getDate(), nextEv.getDate());
+ const willWantDateSeparator = wantsDateSeparator(
+ mxEv.getDate() || undefined,
+ nextEv.getDate() || undefined,
+ );
lastInSection =
willWantDateSeparator ||
mxEv.getSender() !== nextEv.getSender() ||
diff --git a/src/utils/exportUtils/HtmlExport.tsx b/src/utils/exportUtils/HtmlExport.tsx
index 6b4240375c6..e915d180250 100644
--- a/src/utils/exportUtils/HtmlExport.tsx
+++ b/src/utils/exportUtils/HtmlExport.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2021 The Matrix.org Foundation C.I.C.
+Copyright 2021, 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -66,7 +66,7 @@ export default class HTMLExporter extends Exporter {
}
protected async getRoomAvatar(): Promise {
- let blob: Blob;
+ let blob: Blob | undefined = undefined;
const avatarUrl = Avatar.avatarUrlForRoom(this.room, 32, 32, "crop");
const avatarPath = "room.png";
if (avatarUrl) {
@@ -85,7 +85,7 @@ export default class HTMLExporter extends Exporter {
height={32}
name={this.room.name}
title={this.room.name}
- url={blob ? avatarPath : null}
+ url={blob ? avatarPath : ""}
resizeMethod="crop"
/>
);
@@ -96,9 +96,9 @@ export default class HTMLExporter extends Exporter {
const roomAvatar = await this.getRoomAvatar();
const exportDate = formatFullDateNoDayNoTime(new Date());
const creator = this.room.currentState.getStateEvents(EventType.RoomCreate, "")?.getSender();
- const creatorName = this.room?.getMember(creator)?.rawDisplayName || creator;
- const exporter = this.client.getUserId();
- const exporterName = this.room?.getMember(exporter)?.rawDisplayName;
+ const creatorName = (creator ? this.room.getMember(creator)?.rawDisplayName : creator) || creator;
+ const exporter = this.client.getUserId()!;
+ const exporterName = this.room.getMember(exporter)?.rawDisplayName;
const topic = this.room.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic || "";
const createdText = _t("%(creatorName)s created this room.", {
creatorName,
@@ -217,20 +217,19 @@ export default class HTMLExporter extends Exporter {