Compare commits

..

8 Commits

Author SHA1 Message Date
Half-Shot
bcf9854a4c Fix so that we still use notificationsMuted / unread even if a room isn't in view. 2025-01-17 12:14:46 +00:00
Half-Shot
b589757c34 Replace with module API. 2025-01-17 11:48:53 +00:00
Half-Shot
0d576b217b lint 2025-01-17 11:48:53 +00:00
Half-Shot
80cc0a928f dollars 2025-01-17 11:48:53 +00:00
Half-Shot
9cccbeb799 clear current room in two more contexts. 2025-01-17 11:48:53 +00:00
Half-Shot
a8c170f8be Add tests 2025-01-17 11:48:53 +00:00
Half-Shot
f5402b4ec4 Add ability to customize the title template in branding. 2025-01-17 11:48:53 +00:00
Half-Shot
131b28ede8 New config options. 2025-01-17 11:48:53 +00:00
59 changed files with 1252 additions and 1348 deletions

View File

@@ -96,4 +96,3 @@ jobs:
projectName: ${{ env.SITE == 'staging.element.io' && 'element-web-staging' || 'element-web' }}
directory: _deploy
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
branch: main

View File

@@ -51,7 +51,7 @@ jobs:
- name: Build and push
id: build-and-push
uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6
uses: docker/build-push-action@b32b51a8eda65d6793cd0494a773d4f6bcef32dc # v6
with:
context: .
push: true

View File

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

View File

@@ -8,13 +8,11 @@
#### develop
The develop branch holds the very latest and greatest code we have to offer, as such it may be less stable.
It is auto-deployed on every commit to element-web or matrix-js-sdk to develop.element.io via GitHub Actions `build_develop.yml`.
The develop branch holds the very latest and greatest code we have to offer, as such it may be less stable. It corresponds to the develop.element.io CD platform.
#### staging
The staging branch corresponds to the very latest release regardless of whether it is an RC or not. Deployed to staging.element.io manually.
It is auto-deployed on every release of element-web to staging.element.io via GitHub Actions `deploy.yml`.
#### master
@@ -217,7 +215,7 @@ We ship Element Web to dockerhub, `*.element.io`, and packages.element.io.
We ship Element Desktop to packages.element.io.
- [ ] Check that element-web has shipped to dockerhub
- [ ] Check that the staging [deployment](https://github.com/element-hq/element-web/actions/workflows/deploy.yml) has completed successfully
- [ ] Deploy staging.element.io. [See docs.](https://handbook.element.io/books/element-web-team/page/deploying-appstagingelementio)
- [ ] Test staging.element.io
For final releases additionally do these steps:
@@ -227,9 +225,6 @@ For final releases additionally do these steps:
- [ ] Ensure Element Web package has shipped to packages.element.io
- [ ] Ensure Element Desktop packages have shipped to packages.element.io
If you need to roll back a deployment to staging.element.io,
you can run the `deploy.yml` automation choosing an older tag which you wish to deploy.
# Housekeeping
We have some manual housekeeping to do in order to prepare for the next release.

View File

@@ -74,7 +74,7 @@
"@types/react-dom": "18.3.5",
"oidc-client-ts": "3.1.0",
"jwt-decode": "4.0.0",
"caniuse-lite": "1.0.30001692",
"caniuse-lite": "1.0.30001690",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
"wrap-ansi": "npm:wrap-ansi@^7.0.0"
},
@@ -128,7 +128,7 @@
"matrix-encrypt-attachment": "^1.0.3",
"matrix-events-sdk": "0.0.1",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
"matrix-widget-api": "1.11.0",
"matrix-widget-api": "^1.10.0",
"memoize-one": "^6.0.0",
"mime": "^4.0.4",
"oidc-client-ts": "^3.0.1",
@@ -178,7 +178,7 @@
"@peculiar/webcrypto": "^1.4.3",
"@playwright/test": "^1.40.1",
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
"@sentry/webpack-plugin": "^3.0.0",
"@sentry/webpack-plugin": "^2.7.1",
"@stylistic/eslint-plugin": "^2.9.0",
"@svgr/webpack": "^8.0.0",
"@testcontainers/postgresql": "^10.16.0",
@@ -230,7 +230,7 @@
"dotenv": "^16.0.2",
"eslint": "8.57.1",
"eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^10.0.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-deprecate": "0.8.5",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jest": "^28.0.0",
@@ -287,7 +287,7 @@
"terser-webpack-plugin": "^5.3.9",
"testcontainers": "^10.16.0",
"ts-node": "^10.9.1",
"typescript": "5.7.3",
"typescript": "5.7.2",
"util": "^0.12.5",
"web-streams-polyfill": "^4.0.0",
"webpack": "^5.89.0",

View File

@@ -0,0 +1,43 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { expect, test } from "../../element-web-test";
/*
* Tests for branding configuration
**/
test.describe("Test without branding config", () => {
test("Shows standard branding when showing the home page", async ({ pageWithCredentials: page }) => {
await page.goto("/");
await page.waitForSelector(".mx_MatrixChat", { timeout: 30000 });
expect(page.title()).toEqual("Element *");
});
test("Shows standard branding when showing a room", async ({ app, pageWithCredentials: page }) => {
await app.client.createRoom({ name: "Test Room" });
await app.viewRoomByName("Test Room");
expect(page.title()).toEqual("Element * | Test Room");
});
});
test.describe("Test with custom branding", () => {
test.use({
config: {
brand: "TestBrand",
},
});
test("Shows custom branding when showing the home page", async ({ pageWithCredentials: page }) => {
await page.goto("/");
await page.waitForSelector(".mx_MatrixChat", { timeout: 30000 });
expect(page.title()).toEqual("TestingApp TestBrand * $ignoredParameter");
});
test("Shows custom branding when showing a room", async ({ app, pageWithCredentials: page }) => {
await app.client.createRoom({ name: "Test Room" });
await app.viewRoomByName("Test Room");
expect(page.title()).toEqual("TestingApp TestBrand * Test Room $ignoredParameter");
});
});

View File

@@ -42,7 +42,7 @@ test.describe("Memberlist", () => {
await app.viewRoomByName(ROOM_NAME);
const memberlist = await app.toggleMemberlistPanel();
await expect(memberlist.locator(".mx_MemberTileView")).toHaveCount(4);
await expect(memberlist.getByText("Invited")).toHaveCount(1);
await expect(memberlist.getByText("(Invited)")).toHaveCount(1);
await expect(page.locator(".mx_MemberListView")).toMatchScreenshot("with-four-members.png");
});
});

View File

@@ -69,6 +69,11 @@ const test = base.extend<{
});
test.describe("Sliding Sync", () => {
test.skip(
({ homeserverType }) => homeserverType === "pinecone",
"due to a bug in Pinecone https://github.com/element-hq/dendrite/issues/3490",
);
const checkOrder = async (wantOrder: string[], page: Page) => {
await expect(page.getByRole("group", { name: "Rooms" }).locator(".mx_RoomTile_title")).toHaveText(wantOrder);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -19,7 +19,7 @@ import { HomeserverContainer, StartedHomeserverContainer } from "./HomeserverCon
import { StartedMatrixAuthenticationServiceContainer } from "./mas.ts";
import { Api, ClientServerApi, Verb } from "../plugins/utils/api.ts";
const TAG = "develop@sha256:3594fba0d21ad44f407225baed4be0542da8abcb6e1a7e2e16d3be35c278a7cb";
const TAG = "develop@sha256:e48308d68dec00af6ce43a05785d475de21a37bc2afaabb440d3a575bcc3d57d";
const DEFAULT_CONFIG = {
server_name: "localhost",

View File

@@ -283,7 +283,6 @@
@import "./views/rooms/_EventTile.pcss";
@import "./views/rooms/_HistoryTile.pcss";
@import "./views/rooms/_IRCLayout.pcss";
@import "./views/rooms/_InvitedIconView.pcss";
@import "./views/rooms/_JumpToBottomButton.pcss";
@import "./views/rooms/_LinkPreviewGroup.pcss";
@import "./views/rooms/_LinkPreviewWidget.pcss";

View File

@@ -35,8 +35,6 @@ Please see LICENSE files in the repository root for full details.
.mx_DisambiguatedProfile_mxid {
margin-inline-start: 0;
font: var(--cpd-font-body-sm-regular);
text-overflow: ellipsis;
overflow: hidden;
}
span:not(.mx_DisambiguatedProfile_mxid) {

View File

@@ -1,10 +0,0 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
.mx_InvitedIconView {
color: var(--cpd-color-icon-tertiary);
}

View File

@@ -14,10 +14,4 @@ Please see LICENSE files in the repository root for full details.
.mx_MemberListView_container {
height: 100%;
}
.mx_MemberListView_separator {
margin: 0;
border: none;
border-top: 2px solid var(--cpd-color-bg-subtle-primary);
}
}

View File

@@ -31,11 +31,9 @@ Please see LICENSE files in the repository root for full details.
min-width: 0;
}
.mx_MemberTileView_userLabel {
.mx_MemberTileView_user_label {
font: var(--cpd-font-body-sm-regular);
font-size: 13px;
color: var(--cpd-color-text-secondary);
margin-left: var(--cpd-space-4x);
}
.mx_MemberTileView_avatar {
@@ -43,4 +41,18 @@ Please see LICENSE files in the repository root for full details.
height: 32px;
width: 32px;
}
.mx_E2EIconView {
display: flex;
justify-content: center;
align-items: center;
}
.mx_E2EIconView_warning {
color: var(--cpd-color-icon-critical-primary);
}
.mx_E2EIconView_verified {
color: var(--cpd-color-icon-success-primary);
}
}

View File

@@ -131,6 +131,7 @@ import { ConfirmSessionLockTheftView } from "./auth/ConfirmSessionLockTheftView"
import { LoginSplashView } from "./auth/LoginSplashView";
import { cleanUpDraftsIfRequired } from "../../DraftCleaner";
import { InitialCryptoSetupStore } from "../../stores/InitialCryptoSetupStore";
import { AppTitleContext } from "@matrix-org/react-sdk-module-api/lib/lifecycles/BrandingExtensions";
// legacy export
export { default as Views } from "../../Views";
@@ -223,7 +224,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private tokenLogin?: boolean;
// What to focus on next component update, if anything
private focusNext: FocusNextType;
private subTitleStatus: string;
private prevWindowWidth: number;
private readonly loggedInView = createRef<LoggedInViewType>();
@@ -232,6 +232,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private fontWatcher?: FontWatcher;
private readonly stores: SdkContextClass;
private subtitleContext?: {unreadNotificationCount: number, userNotificationLevel: NotificationLevel, syncState: SyncState};
public constructor(props: IProps) {
super(props);
this.stores = SdkContextClass.instance;
@@ -275,10 +277,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
this.prevWindowWidth = UIStore.instance.windowWidth || 1000;
// object field used for tracking the status info appended to the title tag.
// we don't do it as react state as i'm scared about triggering needless react refreshes.
this.subTitleStatus = "";
}
/**
@@ -1474,7 +1472,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
collapseLhs: false,
currentRoomId: null,
});
this.subTitleStatus = "";
this.subtitleContext = undefined;
this.setPageSubtitle();
this.stores.onLoggedOut();
}
@@ -1490,7 +1488,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
collapseLhs: false,
currentRoomId: null,
});
this.subTitleStatus = "";
this.subtitleContext = undefined;
this.setPageSubtitle();
}
@@ -1941,15 +1939,51 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
});
}
private setPageSubtitle(subtitle = ""): void {
private setPageSubtitle(): void {
const extraContext = this.subtitleContext;
let context: AppTitleContext = {
brand: SdkConfig.get().brand,
syncError: extraContext?.syncState === SyncState.Error,
notificationsMuted: extraContext && extraContext.userNotificationLevel < NotificationLevel.Activity,
unreadNotificationCount: extraContext?.unreadNotificationCount,
};
if (this.state.currentRoomId) {
const client = MatrixClientPeg.get();
const room = client?.getRoom(this.state.currentRoomId);
if (room) {
subtitle = `${this.subTitleStatus} | ${room.name} ${subtitle}`;
context = {
...context,
roomId: this.state.currentRoomId,
roomName: room?.name,
};
}
const moduleTitle = ModuleRunner.instance.extensions.branding?.getAppTitle(context);
if (moduleTitle) {
if (document.title !== moduleTitle) {
document.title = moduleTitle;
}
return;
}
// Use application default.
let subtitle = "";
if (context?.syncError) {
subtitle += `[${_t("common|offline")}] `;
}
if (context.unreadNotificationCount !== undefined && context.unreadNotificationCount > 0) {
subtitle += `[${context.unreadNotificationCount}]`;
} else if (context.notificationsMuted !== undefined && !context.notificationsMuted) {
subtitle += `*`;
}
if ('roomId' in context && context.roomId) {
if (context.roomName) {
subtitle = `${subtitle} | ${context.roomName}`;
}
} else {
subtitle = `${this.subTitleStatus} ${subtitle}`;
subtitle = subtitle;
}
const title = `${SdkConfig.get().brand} ${subtitle}`;
@@ -1966,17 +2000,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
PlatformPeg.get()!.setErrorStatus(state === SyncState.Error);
PlatformPeg.get()!.setNotificationCount(numUnreadRooms);
}
this.subTitleStatus = "";
if (state === SyncState.Error) {
this.subTitleStatus += `[${_t("common|offline")}] `;
}
if (numUnreadRooms > 0) {
this.subTitleStatus += `[${numUnreadRooms}]`;
} else if (notificationState.level >= NotificationLevel.Activity) {
this.subTitleStatus += `*`;
}
this.subtitleContext = {
syncState: state,
userNotificationLevel: notificationState.level,
unreadNotificationCount: numUnreadRooms,
};
this.setPageSubtitle();
};

View File

@@ -99,12 +99,8 @@ export function sdkRoomMemberToRoomMember(member: SdkRoomMember): Member {
};
}
export const SEPARATOR = "SEPARATOR";
export type MemberWithSeparator = Member | typeof SEPARATOR;
export interface MemberListViewState {
members: MemberWithSeparator[];
memberCount: number;
members: Member[];
search: (searchQuery: string) => void;
isPresenceEnabled: boolean;
shouldShowInvite: boolean;
@@ -122,16 +118,10 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
}
const sdkContext = useContext(SDKContext);
const [memberMap, setMemberMap] = useState<Map<string, MemberWithSeparator>>(new Map());
const [memberMap, setMemberMap] = useState<Map<string, Member>>(new Map());
const [isLoading, setIsLoading] = useState<boolean>(true);
// This is the last known total number of members in this room.
const [totalMemberCount, setTotalMemberCount] = useState(0);
/**
* This is the current number of members in the list.
* This number will be less than the total number of members
* in the room when the search functionality is used.
*/
const [memberCount, setMemberCount] = useState(0);
const loadMembers = useMemo(
() =>
@@ -141,34 +131,24 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
roomId,
searchQuery,
);
const threePidInvited = getPending3PidInvites(room, searchQuery);
const newMemberMap = new Map<string, MemberWithSeparator>();
// First add the joined room members
for (const member of joinedSdk) {
const roomMember = sdkRoomMemberToRoomMember(member);
newMemberMap.set(member.userId, roomMember);
}
// Then a separator if needed
if (joinedSdk.length > 0 && (invitedSdk.length > 0 || threePidInvited.length > 0))
newMemberMap.set(SEPARATOR, SEPARATOR);
// Then add the invited room members
const newMemberMap = new Map<string, Member>();
// First add the invited room members
for (const member of invitedSdk) {
const roomMember = sdkRoomMemberToRoomMember(member);
newMemberMap.set(member.userId, roomMember);
}
// Finally add the third party invites
// Then add the third party invites
const threePidInvited = getPending3PidInvites(room, searchQuery);
for (const invited of threePidInvited) {
const key = invited.threePidInvite!.event.getContent().display_name;
newMemberMap.set(key, invited);
}
// Finally add the joined room members
for (const member of joinedSdk) {
const roomMember = sdkRoomMemberToRoomMember(member);
newMemberMap.set(member.userId, roomMember);
}
setMemberMap(newMemberMap);
setMemberCount(joinedSdk.length + invitedSdk.length + threePidInvited.length);
if (!searchQuery) {
/**
* Since searching for members only gives you the relevant
@@ -261,7 +241,6 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
return {
members: Array.from(memberMap.values()),
memberCount,
search: loadMembers,
shouldShowInvite,
isPresenceEnabled,

View File

@@ -145,7 +145,7 @@ export function useMemberTileViewModel(props: MemberTileViewModelProps): MemberT
userLabel = _t(PowerLabel[powerStatus]);
}
if (props.member.isInvite) {
userLabel = _t("member_list|invited_label");
userLabel = `(${_t("member_list|invited_label")})`;
}
return {

View File

@@ -8,7 +8,6 @@ Please see LICENSE files in the repository root for full details.
import dis from "../../../../dispatcher/dispatcher";
import { Action } from "../../../../dispatcher/actions";
import { ThreePIDInvite } from "../../../../models/rooms/ThreePIDInvite";
import { _t } from "../../../../languageHandler";
interface ThreePidTileViewModelProps {
threePidInvite: ThreePIDInvite;
@@ -17,7 +16,6 @@ interface ThreePidTileViewModelProps {
export interface ThreePidTileViewState {
name: string;
onClick: () => void;
userLabel?: string;
}
export function useThreePidTileViewModel(props: ThreePidTileViewModelProps): ThreePidTileViewState {
@@ -30,11 +28,8 @@ export function useThreePidTileViewModel(props: ThreePidTileViewModelProps): Thr
});
};
const userLabel = _t("member_list|invited_label");
return {
name,
onClick,
userLabel,
};
}

View File

@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
import React from "react";
import classNames from "classnames";
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
import { randomString } from "matrix-js-sdk/src/randomstring";
import ToggleSwitch from "./ToggleSwitch";
import { Caption } from "../typography/Caption";
@@ -36,7 +36,7 @@ interface IProps {
}
export default class LabelledToggleSwitch extends React.PureComponent<IProps> {
private readonly id = `mx_LabelledToggleSwitch_${secureRandomString(12)}`;
private readonly id = `mx_LabelledToggleSwitch_${randomString(12)}`;
public render(): React.ReactNode {
// This is a minimal version of a SettingsFlag

View File

@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
import { randomString } from "matrix-js-sdk/src/randomstring";
import SettingsStore from "../../../settings/SettingsStore";
import { _t } from "../../../languageHandler";
@@ -35,7 +35,7 @@ interface IState {
}
export default class SettingsFlag extends React.Component<IProps, IState> {
private readonly id = `mx_SettingsFlag_${secureRandomString(12)}`;
private readonly id = `mx_SettingsFlag_${randomString(12)}`;
public constructor(props: IProps) {
super(props);

View File

@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import React, { Ref } from "react";
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
import { randomString } from "matrix-js-sdk/src/randomstring";
import classnames from "classnames";
export enum CheckboxStyle {
@@ -33,7 +33,7 @@ export default class StyledCheckbox extends React.PureComponent<IProps, IState>
public constructor(props: IProps) {
super(props);
// 56^10 so unlikely chance of collision.
this.id = this.props.id || "checkbox_" + secureRandomString(10);
this.id = this.props.id || "checkbox_" + randomString(10);
}
public render(): React.ReactNode {

View File

@@ -18,7 +18,7 @@ import {
ContentHelpers,
M_BEACON,
} from "matrix-js-sdk/src/matrix";
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
import { randomString } from "matrix-js-sdk/src/randomstring";
import classNames from "classnames";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
@@ -81,10 +81,10 @@ const useBeaconState = (
// eg thread and main timeline, reply
// maplibregl needs a unique id to attach the map instance to
const useUniqueId = (eventId: string): string => {
const [id, setId] = useState(`${eventId}_${secureRandomString(8)}`);
const [id, setId] = useState(`${eventId}_${randomString(8)}`);
useEffect(() => {
setId(`${eventId}_${secureRandomString(8)}`);
setId(`${eventId}_${randomString(8)}`);
}, [eventId]);
return id;

View File

@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
import React from "react";
import { MatrixEvent, ClientEvent, ClientEventHandlerMap } from "matrix-js-sdk/src/matrix";
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
import { randomString } from "matrix-js-sdk/src/randomstring";
import { Tooltip } from "@vector-im/compound-web";
import { _t } from "../../../languageHandler";
@@ -41,7 +41,7 @@ export default class MLocationBody extends React.Component<IBodyProps, IState> {
// multiple instances of same map might be in document
// eg thread and main timeline, reply
const idSuffix = `${props.mxEvent.getId()}_${secureRandomString(8)}`;
const idSuffix = `${props.mxEvent.getId()}_${randomString(8)}`;
this.mapId = `mx_MLocationBody_${idSuffix}`;
this.reconnectedListener = createReconnectedListener(this.clearError);

View File

@@ -88,10 +88,12 @@ function getHeaderLabelJSX(vm: MemberListViewState): React.ReactNode {
</Flex>
);
}
if (vm.memberCount === 0) {
const filteredMemberCount = vm.members.length;
if (filteredMemberCount === 0) {
return _t("member_list|no_matches");
}
return _t("member_list|count", { count: vm.memberCount });
return _t("member_list|count", { count: filteredMemberCount });
}
export const MemberListHeaderView: React.FC<Props> = (props: Props) => {

View File

@@ -11,11 +11,7 @@ import { List, ListRowProps } from "react-virtualized/dist/commonjs/List";
import { AutoSizer } from "react-virtualized";
import { Flex } from "../../../utils/Flex";
import {
MemberWithSeparator,
SEPARATOR,
useMemberListViewModel,
} from "../../../viewmodels/memberlist/MemberListViewModel";
import { useMemberListViewModel } from "../../../viewmodels/memberlist/MemberListViewModel";
import { RoomMemberTileView } from "./tiles/RoomMemberTileView";
import { ThreePidInviteTileView } from "./tiles/ThreePidInviteTileView";
import { MemberListHeaderView } from "./MemberListHeaderView";
@@ -30,41 +26,10 @@ interface IProps {
const MemberListView: React.FC<IProps> = (props: IProps) => {
const vm = useMemberListViewModel(props.roomId);
const totalRows = vm.members.length;
const getRowComponent = (item: MemberWithSeparator): React.JSX.Element => {
if (item === SEPARATOR) {
return <hr className="mx_MemberListView_separator" />;
} else if (item.member) {
return <RoomMemberTileView member={item.member} showPresence={vm.isPresenceEnabled} />;
} else {
return <ThreePidInviteTileView threePidInvite={item.threePidInvite} />;
}
};
const getRowHeight = ({ index }: { index: number }): number => {
if (vm.members[index] === SEPARATOR) {
/**
* This is a separator of 2px height rendered between
* joined and invited members.
*/
return 2;
} else if (totalRows && index === totalRows) {
/**
* The empty spacer div rendered at the bottom should
* have a height of 32px.
*/
return 32;
} else {
/**
* The actual member tiles have a height of 56px.
*/
return 56;
}
};
const memberCount = vm.members.length;
const rowRenderer = ({ key, index, style }: ListRowProps): React.JSX.Element => {
if (index === totalRows) {
if (index === memberCount) {
// We've rendered all the members,
// now we render an empty div to add some space to the end of the list.
return <div key={key} style={style} />;
@@ -72,7 +37,11 @@ const MemberListView: React.FC<IProps> = (props: IProps) => {
const item = vm.members[index];
return (
<div key={key} style={style}>
{getRowComponent(item)}
{item.member ? (
<RoomMemberTileView member={item.member} showPresence={vm.isPresenceEnabled} />
) : (
<ThreePidInviteTileView threePidInvite={item.threePidInvite} />
)}
</div>
);
};
@@ -94,9 +63,11 @@ const MemberListView: React.FC<IProps> = (props: IProps) => {
{({ height, width }) => (
<List
rowRenderer={rowRenderer}
rowHeight={getRowHeight}
// All the member tiles will have a height of 56px.
// The additional empty div at the end of the list should have a height of 32px.
rowHeight={({ index }) => (index === memberCount ? 32 : 56)}
// The +1 refers to the additional empty div that we render at the end of the list.
rowCount={totalRows + 1}
rowCount={memberCount + 1}
// Subtract the height of MemberlistHeaderView so that the parent div does not overflow.
height={height - 113}
width={width}

View File

@@ -14,8 +14,7 @@ import { E2EIconView } from "./common/E2EIconView";
import AvatarPresenceIconView from "./common/PresenceIconView";
import BaseAvatar from "../../../avatars/BaseAvatar";
import { _t } from "../../../../../languageHandler";
import { MemberTileView } from "./common/MemberTileView";
import { InvitedIconView } from "./common/InvitedIconView";
import { MemberTileLayout } from "./common/MemberTileLayout";
interface IProps {
member: RoomMember;
@@ -44,23 +43,25 @@ export function RoomMemberTileView(props: IProps): JSX.Element {
presenceJSX = <AvatarPresenceIconView presenceState={presenceState} />;
}
let iconJsx;
if (vm.e2eStatus) {
iconJsx = <E2EIconView status={vm.e2eStatus} />;
let userLabelJSX;
if (vm.userLabel) {
userLabelJSX = <div className="mx_MemberTileView_user_label">{vm.userLabel}</div>;
}
if (member.isInvite) {
iconJsx = <InvitedIconView isThreePid={false} />;
let e2eIcon;
if (vm.e2eStatus) {
e2eIcon = <E2EIconView status={vm.e2eStatus} />;
}
return (
<MemberTileView
<MemberTileLayout
title={vm.title}
onClick={vm.onClick}
avatarJsx={av}
presenceJsx={presenceJSX}
nameJsx={nameJSX}
userLabel={vm.userLabel}
iconJsx={iconJsx}
userLabelJsx={userLabelJSX}
e2eIconJsx={e2eIcon}
/>
);
}

View File

@@ -10,8 +10,7 @@ import React from "react";
import { useThreePidTileViewModel } from "../../../../viewmodels/memberlist/tiles/ThreePidTileViewModel";
import { ThreePIDInvite } from "../../../../../models/rooms/ThreePIDInvite";
import BaseAvatar from "../../../avatars/BaseAvatar";
import { MemberTileView } from "./common/MemberTileView";
import { InvitedIconView } from "./common/InvitedIconView";
import { MemberTileLayout } from "./common/MemberTileLayout";
interface Props {
threePidInvite: ThreePIDInvite;
@@ -20,15 +19,5 @@ interface Props {
export function ThreePidInviteTileView(props: Props): JSX.Element {
const vm = useThreePidTileViewModel(props);
const av = <BaseAvatar name={vm.name} size="32px" aria-hidden="true" />;
const iconJsx = <InvitedIconView isThreePid={true} />;
return (
<MemberTileView
nameJsx={vm.name}
avatarJsx={av}
onClick={vm.onClick}
userLabel={vm.userLabel}
iconJsx={iconJsx}
/>
);
return <MemberTileLayout nameJsx={vm.name} avatarJsx={av} onClick={vm.onClick} />;
}

View File

@@ -1,25 +0,0 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import EmailIcon from "@vector-im/compound-design-tokens/assets/web/icons/email-solid";
import UserAddIcon from "@vector-im/compound-design-tokens/assets/web/icons/user-add-solid";
import { Flex } from "../../../../../utils/Flex";
interface Props {
isThreePid: boolean;
}
export function InvitedIconView({ isThreePid }: Props): JSX.Element {
const Icon = isThreePid ? EmailIcon : UserAddIcon;
return (
<Flex align="center" className="mx_InvitedIconView">
<Icon height="16px" width="16px" />
</Flex>
);
}

View File

@@ -15,16 +15,11 @@ interface Props {
onClick: () => void;
title?: string;
presenceJsx?: JSX.Element;
userLabel?: React.ReactNode;
iconJsx?: JSX.Element;
userLabelJsx?: JSX.Element;
e2eIconJsx?: JSX.Element;
}
export function MemberTileView(props: Props): JSX.Element {
let userLabelJsx: React.ReactNode;
if (props.userLabel) {
userLabelJsx = <div className="mx_MemberTileView_userLabel">{props.userLabel}</div>;
}
export function MemberTileLayout(props: Props): JSX.Element {
return (
// The wrapping div is required to make the magic mouse listener work, for some reason.
<div>
@@ -36,8 +31,8 @@ export function MemberTileView(props: Props): JSX.Element {
<div className="mx_MemberTileView_name">{props.nameJsx}</div>
</div>
<div className="mx_MemberTileView_right">
{userLabelJsx}
{props.iconJsx}
{props.userLabelJsx}
{props.e2eIconJsx}
</div>
</AccessibleButton>
</div>

View File

@@ -31,7 +31,8 @@ export function shouldShowQr(
): boolean {
const msc4108Supported = !!versions?.unstable_features?.["org.matrix.msc4108"];
const deviceAuthorizationGrantSupported = oidcClientConfig?.grant_types_supported.includes(DEVICE_CODE_SCOPE);
const deviceAuthorizationGrantSupported =
oidcClientConfig?.metadata?.grant_types_supported.includes(DEVICE_CODE_SCOPE);
return (
!!deviceAuthorizationGrantSupported &&

View File

@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import React, { lazy, Suspense, useCallback, useContext, useEffect, useRef, useState } from "react";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { discoverAndValidateOIDCIssuerWellKnown, MatrixClient } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import { defer } from "matrix-js-sdk/src/utils";
@@ -163,7 +163,10 @@ const SessionManagerTab: React.FC<{
const clientVersions = useAsyncMemo(() => matrixClient.getVersions(), [matrixClient]);
const oidcClientConfig = useAsyncMemo(async () => {
try {
return await matrixClient?.getAuthMetadata();
const authIssuer = await matrixClient?.getAuthIssuer();
if (authIssuer) {
return discoverAndValidateOIDCIssuerWellKnown(authIssuer.issuer);
}
} catch (e) {
logger.error("Failed to discover OIDC metadata", e);
}

View File

@@ -18,7 +18,7 @@ import {
} from "matrix-js-sdk/src/matrix";
import { KnownMembership, Membership } from "matrix-js-sdk/src/types";
import { logger } from "matrix-js-sdk/src/logger";
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
import { randomString } from "matrix-js-sdk/src/randomstring";
import { CallType } from "matrix-js-sdk/src/webrtc/call";
import { NamespacedValue } from "matrix-js-sdk/src/NamespacedValue";
import { IWidgetApiRequest } from "matrix-widget-api";
@@ -743,7 +743,7 @@ export class ElementCall extends Call {
const url = ElementCall.generateWidgetUrl(client, roomId);
return WidgetStore.instance.addVirtualWidget(
{
id: secureRandomString(24), // So that it's globally unique
id: randomString(24), // So that it's globally unique
creatorUserId: client.getUserId()!,
name: "Element Call",
type: WidgetType.CALL.preferred,

View File

@@ -17,6 +17,10 @@ import {
DefaultExperimentalExtensions,
ProvideExperimentalExtensions,
} from "@matrix-org/react-sdk-module-api/lib/lifecycles/ExperimentalExtensions";
import {
ProvideBrandingExtensions,
} from "@matrix-org/react-sdk-module-api/lib/lifecycles/BrandingExtensions";
import { AppModule } from "./AppModule";
import { ModuleFactory } from "./ModuleFactory";
@@ -30,6 +34,7 @@ class ExtensionsManager {
// Private backing fields for extensions
private cryptoSetupExtension: ProvideCryptoSetupExtensions;
private experimentalExtension: ProvideExperimentalExtensions;
private brandingExtension?: ProvideBrandingExtensions;
/** `true` if `cryptoSetupExtension` is the default implementation; `false` if it is implemented by a module. */
private hasDefaultCryptoSetupExtension = true;
@@ -67,6 +72,15 @@ class ExtensionsManager {
return this.experimentalExtension;
}
/**
* Provides branding extension.
*
* @returns The registered extension. If no module provides this extension, undefined is returned..
*/
public get branding(): ProvideBrandingExtensions|undefined {
return this.brandingExtension;
}
/**
* Add any extensions provided by the module.
*
@@ -100,6 +114,16 @@ class ExtensionsManager {
);
}
}
if (runtimeModule.extensions?.branding) {
if (!this.brandingExtension) {
this.brandingExtension = runtimeModule.extensions?.branding;
} else {
throw new Error(
`adding experimental branding implementation from module ${runtimeModule.moduleName} but an implementation was already provided.`,
);
}
}
}
}

View File

@@ -31,7 +31,7 @@ Please see LICENSE files in the repository root for full details.
// the frequency with which we flush to indexeddb
import { logger } from "matrix-js-sdk/src/logger";
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
import { randomString } from "matrix-js-sdk/src/randomstring";
import { getCircularReplacer } from "../utils/JSON";
@@ -135,7 +135,7 @@ export class IndexedDBLogStore {
private indexedDB: IDBFactory,
private logger: ConsoleLogger,
) {
this.id = "instance-" + secureRandomString(16);
this.id = "instance-" + randomString(16);
}
/**

View File

@@ -50,8 +50,11 @@ export class OidcClientStore {
} else {
// We are not in OIDC Native mode, as we have no locally stored issuer. Check if the server delegates auth to OIDC.
try {
const authMetadata = await this.matrixClient.getAuthMetadata();
this.setAccountManagementEndpoint(authMetadata.account_management_uri, authMetadata.issuer);
const authIssuer = await this.matrixClient.getAuthIssuer();
const { accountManagementEndpoint, metadata } = await discoverAndValidateOIDCIssuerWellKnown(
authIssuer.issuer,
);
this.setAccountManagementEndpoint(accountManagementEndpoint, metadata.issuer);
} catch (e) {
console.log("Auth issuer not found", e);
}
@@ -150,11 +153,14 @@ export class OidcClientStore {
try {
const clientId = getStoredOidcClientId();
const authMetadata = await discoverAndValidateOIDCIssuerWellKnown(this.authenticatedIssuer);
this.setAccountManagementEndpoint(authMetadata.account_management_uri, authMetadata.issuer);
const { accountManagementEndpoint, metadata, signingKeys } = await discoverAndValidateOIDCIssuerWellKnown(
this.authenticatedIssuer,
);
this.setAccountManagementEndpoint(accountManagementEndpoint, metadata.issuer);
this.oidcClient = new OidcClient({
authority: authMetadata.issuer,
signingKeys: authMetadata.signingKeys ?? undefined,
...metadata,
authority: metadata.issuer,
signingKeys,
redirect_uri: PlatformPeg.get()!.getOidcCallbackUrl().href,
client_id: clientId,
});

View File

@@ -11,6 +11,7 @@ import {
AutoDiscovery,
AutoDiscoveryError,
ClientConfig,
discoverAndValidateOIDCIssuerWellKnown,
IClientWellKnown,
MatrixClient,
MatrixError,
@@ -292,7 +293,8 @@ export default class AutoDiscoveryUtils {
let delegatedAuthenticationError: Error | undefined;
try {
const tempClient = new MatrixClient({ baseUrl: preferredHomeserverUrl });
delegatedAuthentication = await tempClient.getAuthMetadata();
const { issuer } = await tempClient.getAuthIssuer();
delegatedAuthentication = await discoverAndValidateOIDCIssuerWellKnown(issuer);
} catch (e) {
if (e instanceof MatrixError && e.httpStatus === 404 && e.errcode === "M_UNRECOGNIZED") {
// 404 M_UNRECOGNIZED means the server does not support OIDC

View File

@@ -9,13 +9,12 @@ Please see LICENSE files in the repository root for full details.
import { useCallback, useEffect, useState } from "react";
import { base32 } from "rfc4648";
import { capitalize } from "lodash";
import { IWidget, IWidgetData } from "matrix-widget-api";
import { Room, ClientEvent, MatrixClient, RoomStateEvent, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { logger } from "matrix-js-sdk/src/logger";
import { CallType } from "matrix-js-sdk/src/webrtc/call";
import { LOWERCASE, secureRandomString, secureRandomStringFrom } from "matrix-js-sdk/src/randomstring";
import { randomString, randomLowercaseString, randomUppercaseString } from "matrix-js-sdk/src/randomstring";
import PlatformPeg from "../PlatformPeg";
import SdkConfig from "../SdkConfig";
@@ -428,10 +427,7 @@ export default class WidgetUtils {
): Promise<void> {
const domain = Jitsi.getInstance().preferredDomain;
const auth = (await Jitsi.getInstance().getJitsiAuth()) ?? undefined;
// Must be globally unique, although predicatablity is not important, the js-sdk has functions to generate
// secure ranom strings, and speed is not important here.
const widgetId = secureRandomString(24);
const widgetId = randomString(24); // Must be globally unique
let confId: string;
if (auth === "openidtoken-jwt") {
@@ -441,8 +437,8 @@ export default class WidgetUtils {
// https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification
confId = base32.stringify(new TextEncoder().encode(roomId), { pad: false });
} else {
// Create a random conference ID (capitalised so the name looks sensible in Jitsi)
confId = `Jitsi${capitalize(secureRandomStringFrom(24, LOWERCASE))}`;
// Create a random conference ID
confId = `Jitsi${randomUppercaseString(1)}${randomLowercaseString(23)}`;
}
// TODO: Remove URL hacks when the mobile clients eventually support v2 widgets

View File

@@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details.
import { completeAuthorizationCodeGrant, generateOidcAuthorizationUrl } from "matrix-js-sdk/src/oidc/authorize";
import { QueryDict } from "matrix-js-sdk/src/utils";
import { OidcClientConfig } from "matrix-js-sdk/src/matrix";
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
import { randomString } from "matrix-js-sdk/src/randomstring";
import { IdTokenClaims } from "oidc-client-ts";
import { OidcClientError } from "./error";
@@ -34,12 +34,12 @@ export const startOidcLogin = async (
): Promise<void> => {
const redirectUri = PlatformPeg.get()!.getOidcCallbackUrl().href;
const nonce = secureRandomString(10);
const nonce = randomString(10);
const prompt = isRegistration ? "create" : undefined;
const authorizationUrl = await generateOidcAuthorizationUrl({
metadata: delegatedAuthConfig,
metadata: delegatedAuthConfig.metadata,
redirectUri,
clientId,
homeserverUrl,

View File

@@ -15,6 +15,8 @@ import { OidcClientConfig } from "matrix-js-sdk/src/matrix";
* @returns whether user registration is supported
*/
export const isUserRegistrationSupported = (delegatedAuthConfig: OidcClientConfig): boolean => {
const supportedPrompts = delegatedAuthConfig.prompt_values_supported;
// The OidcMetadata type from oidc-client-ts does not include `prompt_values_supported`
// even though it is part of the OIDC spec, so cheat TS here to access it
const supportedPrompts = (delegatedAuthConfig.metadata as Record<string, unknown>)["prompt_values_supported"];
return Array.isArray(supportedPrompts) && supportedPrompts?.includes("create");
};

View File

@@ -40,9 +40,9 @@ export const getOidcClientId = async (
delegatedAuthConfig: OidcClientConfig,
staticOidcClients?: IConfigOptions["oidc_static_clients"],
): Promise<string> => {
const staticClientId = getStaticOidcClientId(delegatedAuthConfig.issuer, staticOidcClients);
const staticClientId = getStaticOidcClientId(delegatedAuthConfig.metadata.issuer, staticOidcClients);
if (staticClientId) {
logger.debug(`Using static clientId for issuer ${delegatedAuthConfig.issuer}`);
logger.debug(`Using static clientId for issuer ${delegatedAuthConfig.metadata.issuer}`);
return staticClientId;
}
return await registerOidcClient(delegatedAuthConfig, await PlatformPeg.get()!.getOidcClientMetadata());

View File

@@ -12,7 +12,7 @@ Please see LICENSE files in the repository root for full details.
import { MatrixClient, Room, MatrixEvent, OidcRegistrationClientMetadata } from "matrix-js-sdk/src/matrix";
import React from "react";
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
import { randomString } from "matrix-js-sdk/src/randomstring";
import { logger } from "matrix-js-sdk/src/logger";
import BasePlatform, { UpdateCheckStatus, UpdateStatus } from "../../BasePlatform";
@@ -93,7 +93,7 @@ export default class ElectronPlatform extends BasePlatform {
private readonly ipc = new IPCManager("ipcCall", "ipcReply");
private readonly eventIndexManager: BaseEventIndexManager = new SeshatIndexManager();
// this is the opaque token we pass to the HS which when we get it in our callback we can resolve to a profile
private readonly ssoID: string = secureRandomString(32);
private readonly ssoID: string = randomString(32);
public constructor() {
super();

View File

@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
import "@testing-library/jest-dom";
import "blob-polyfill";
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
import { randomString } from "matrix-js-sdk/src/randomstring";
import { mocked } from "jest-mock";
import { PredictableRandom } from "./test-utils/predictableRandom"; // https://github.com/jsdom/jsdom/issues/2555
@@ -25,8 +25,7 @@ jest.mock("matrix-js-sdk/src/randomstring");
beforeEach(() => {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const mockRandom = new PredictableRandom();
// needless to say, the mock is not cryptographically secure
mocked(secureRandomString).mockImplementation((len) => {
mocked(randomString).mockImplementation((len) => {
let ret = "";
for (let i = 0; i < len; ++i) {
const v = mockRandom.get() * chars.length;

View File

@@ -6,4 +6,41 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
export { makeDelegatedAuthConfig, mockOpenIdConfiguration } from "matrix-js-sdk/src/testing";
import { OidcClientConfig } from "matrix-js-sdk/src/matrix";
import { ValidatedIssuerMetadata } from "matrix-js-sdk/src/oidc/validate";
/**
* Makes a valid OidcClientConfig with minimum valid values
* @param issuer used as the base for all other urls
* @returns OidcClientConfig
*/
export const makeDelegatedAuthConfig = (issuer = "https://auth.org/"): OidcClientConfig => {
const metadata = mockOpenIdConfiguration(issuer);
return {
accountManagementEndpoint: issuer + "account",
registrationEndpoint: metadata.registration_endpoint,
authorizationEndpoint: metadata.authorization_endpoint,
tokenEndpoint: metadata.token_endpoint,
metadata,
};
};
/**
* Useful for mocking <issuer>/.well-known/openid-configuration
* @param issuer used as the base for all other urls
* @returns ValidatedIssuerMetadata
*/
export const mockOpenIdConfiguration = (issuer = "https://auth.org/"): ValidatedIssuerMetadata => ({
issuer,
revocation_endpoint: issuer + "revoke",
token_endpoint: issuer + "token",
authorization_endpoint: issuer + "auth",
registration_endpoint: issuer + "registration",
device_authorization_endpoint: issuer + "device",
jwks_uri: issuer + "jwks",
response_types_supported: ["code"],
grant_types_supported: ["authorization_code", "refresh_token"],
code_challenge_methods_supported: ["S256"],
account_management_uri: issuer + "account",
});

View File

@@ -18,7 +18,7 @@ import {
M_POLL_RESPONSE,
M_TEXT,
} from "matrix-js-sdk/src/matrix";
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
import { randomString } from "matrix-js-sdk/src/randomstring";
import { flushPromises } from "./utilities";
@@ -67,7 +67,7 @@ export const makePollEndEvent = (
id?: string,
): MatrixEvent => {
return new MatrixEvent({
event_id: id || secureRandomString(16),
event_id: id || randomString(16),
room_id: roomId,
origin_server_ts: ts,
type: M_POLL_END.name,
@@ -91,7 +91,7 @@ export const makePollResponseEvent = (
ts = 0,
): MatrixEvent =>
new MatrixEvent({
event_id: secureRandomString(16),
event_id: randomString(16),
room_id: roomId,
origin_server_ts: ts,
type: M_POLL_RESPONSE.name,

View File

@@ -749,8 +749,11 @@ describe("Lifecycle", () => {
"eyJhbGciOiJSUzI1NiIsImtpZCI6Imh4ZEhXb0Y5bW4ifQ.eyJzdWIiOiIwMUhQUDJGU0JZREU5UDlFTU04REQ3V1pIUiIsImlzcyI6Imh0dHBzOi8vYXV0aC1vaWRjLmxhYi5lbGVtZW50LmRldi8iLCJpYXQiOjE3MTUwNzE5ODUsImF1dGhfdGltZSI6MTcwNzk5MDMxMiwiY19oYXNoIjoidGt5R1RhUjU5aTk3YXoyTU4yMGdidyIsImV4cCI6MTcxNTA3NTU4NSwibm9uY2UiOiJxaXhwM0hFMmVaIiwiYXVkIjoiMDFIWDk0Mlg3QTg3REgxRUs2UDRaNjI4WEciLCJhdF9oYXNoIjoiNFlFUjdPRlVKTmRTeEVHV2hJUDlnZyJ9.HxODneXvSTfWB5Vc4cf7b8GiN2gdwUuTiyVqZuupWske2HkZiJZUt5Lsxg9BW3gz28POkE0Ln17snlkmy02B_AD3DQxKOOxQCzIIARHdfFvZxgGWsMdFcVQZDW7rtXcqgj-SpVaUQ_8acsgxSrz_DF2o0O4tto0PT6wVUiw8KlBmgWTscWPeAWe-39T-8EiQ8Wi16h6oSPcz2NzOQ7eOM_S9fDkOorgcBkRGLl1nrahrPSdWJSGAeruk5mX4YxN714YThFDyEA2t9YmKpjaiSQ2tT-Xkd7tgsZqeirNs2ni9mIiFX3bRX6t2AhUNzA7MaX9ZyizKGa6go3BESO_oDg";
beforeAll(() => {
fetchMock.get(`${delegatedAuthConfig.issuer}.well-known/openid-configuration`, delegatedAuthConfig);
fetchMock.get(`${delegatedAuthConfig.issuer}jwks`, {
fetchMock.get(
`${delegatedAuthConfig.metadata.issuer}.well-known/openid-configuration`,
delegatedAuthConfig.metadata,
);
fetchMock.get(`${delegatedAuthConfig.metadata.issuer}jwks`, {
status: 200,
headers: {
"Content-Type": "application/json",
@@ -769,7 +772,9 @@ describe("Lifecycle", () => {
await setLoggedIn(credentials);
// didn't try to initialise token refresher
expect(fetchMock).not.toHaveFetched(`${delegatedAuthConfig.issuer}.well-known/openid-configuration`);
expect(fetchMock).not.toHaveFetched(
`${delegatedAuthConfig.metadata.issuer}.well-known/openid-configuration`,
);
});
it("should not try to create a token refresher without a deviceId", async () => {
@@ -780,7 +785,9 @@ describe("Lifecycle", () => {
});
// didn't try to initialise token refresher
expect(fetchMock).not.toHaveFetched(`${delegatedAuthConfig.issuer}.well-known/openid-configuration`);
expect(fetchMock).not.toHaveFetched(
`${delegatedAuthConfig.metadata.issuer}.well-known/openid-configuration`,
);
});
it("should not try to create a token refresher without an issuer in session storage", async () => {
@@ -796,7 +803,9 @@ describe("Lifecycle", () => {
});
// didn't try to initialise token refresher
expect(fetchMock).not.toHaveFetched(`${delegatedAuthConfig.issuer}.well-known/openid-configuration`);
expect(fetchMock).not.toHaveFetched(
`${delegatedAuthConfig.metadata.issuer}.well-known/openid-configuration`,
);
});
it("should create a client with a tokenRefreshFunction", async () => {

View File

@@ -384,7 +384,7 @@ describe("Login", function () {
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
// didn't try to register
expect(fetchMock).not.toHaveBeenCalledWith(delegatedAuth.registration_endpoint);
expect(fetchMock).not.toHaveBeenCalledWith(delegatedAuth.registrationEndpoint);
// continued with normal setup
expect(mockClient.loginFlows).toHaveBeenCalled();
// normal password login rendered
@@ -394,25 +394,25 @@ describe("Login", function () {
it("should attempt to register oidc client", async () => {
// dont mock, spy so we can check config values were correctly passed
jest.spyOn(registerClientUtils, "getOidcClientId");
fetchMock.post(delegatedAuth.registration_endpoint!, { status: 500 });
fetchMock.post(delegatedAuth.registrationEndpoint!, { status: 500 });
getComponent(hsUrl, isUrl, delegatedAuth);
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
// tried to register
expect(fetchMock).toHaveBeenCalledWith(delegatedAuth.registration_endpoint, expect.any(Object));
expect(fetchMock).toHaveBeenCalledWith(delegatedAuth.registrationEndpoint, expect.any(Object));
// called with values from config
expect(registerClientUtils.getOidcClientId).toHaveBeenCalledWith(delegatedAuth, oidcStaticClientsConfig);
});
it("should fallback to normal login when client registration fails", async () => {
fetchMock.post(delegatedAuth.registration_endpoint!, { status: 500 });
fetchMock.post(delegatedAuth.registrationEndpoint!, { status: 500 });
getComponent(hsUrl, isUrl, delegatedAuth);
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
// tried to register
expect(fetchMock).toHaveBeenCalledWith(delegatedAuth.registration_endpoint, expect.any(Object));
expect(fetchMock).toHaveBeenCalledWith(delegatedAuth.registrationEndpoint, expect.any(Object));
expect(logger.error).toHaveBeenCalledWith(new Error(OidcError.DynamicRegistrationFailed));
// continued with normal setup
@@ -423,7 +423,7 @@ describe("Login", function () {
// short term during active development, UI will be added in next PRs
it("should show continue button when oidc native flow is correctly configured", async () => {
fetchMock.post(delegatedAuth.registration_endpoint!, { client_id: "abc123" });
fetchMock.post(delegatedAuth.registrationEndpoint!, { client_id: "abc123" });
getComponent(hsUrl, isUrl, delegatedAuth);
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
@@ -455,7 +455,7 @@ describe("Login", function () {
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
// didn't try to register
expect(fetchMock).not.toHaveBeenCalledWith(delegatedAuth.registration_endpoint);
expect(fetchMock).not.toHaveBeenCalledWith(delegatedAuth.registrationEndpoint);
// continued with normal setup
expect(mockClient.loginFlows).toHaveBeenCalled();
// oidc-aware 'continue' button displayed

View File

@@ -158,26 +158,24 @@ describe("Registration", function () {
describe("when delegated authentication is configured and enabled", () => {
const authConfig = makeDelegatedAuthConfig();
const clientId = "test-client-id";
authConfig.prompt_values_supported = ["create"];
// @ts-ignore
authConfig.metadata["prompt_values_supported"] = ["create"];
beforeEach(() => {
// mock a statically registered client to avoid dynamic registration
SdkConfig.put({
oidc_static_clients: {
[authConfig.issuer]: {
[authConfig.metadata.issuer]: {
client_id: clientId,
},
},
});
fetchMock.get(`${defaultHsUrl}/_matrix/client/unstable/org.matrix.msc2965/auth_issuer`, {
issuer: authConfig.issuer,
issuer: authConfig.metadata.issuer,
});
fetchMock.get("https://auth.org/.well-known/openid-configuration", {
...authConfig,
signingKeys: undefined,
});
fetchMock.get(authConfig.jwks_uri!, { keys: [] });
fetchMock.get("https://auth.org/.well-known/openid-configuration", authConfig.metadata);
fetchMock.get(authConfig.metadata.jwks_uri!, { keys: [] });
});
it("should display oidc-native continue button", async () => {

View File

@@ -224,29 +224,7 @@ exports[`MemberTileView ThreePidInviteTileView renders ThreePidInvite correctly
</div>
<div
class="mx_MemberTileView_right"
>
<div
class="mx_MemberTileView_userLabel"
>
Invited
</div>
<div
class="mx_Flex mx_InvitedIconView"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
>
<svg
fill="currentColor"
height="16px"
viewBox="0 0 24 24"
width="16px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Zm0 5.111a1 1 0 0 0 .514.874l7 3.89a1 1 0 0 0 .972 0l7-3.89a1 1 0 1 0-.972-1.748L12 11.856 5.486 8.237A1 1 0 0 0 4 9.111Z"
/>
</svg>
</div>
</div>
/>
</div>
</div>
</div>

View File

@@ -23,7 +23,7 @@ import {
IThreepid,
ThreepidMedium,
} from "matrix-js-sdk/src/matrix";
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
import { randomString } from "matrix-js-sdk/src/randomstring";
import {
act,
fireEvent,
@@ -287,7 +287,7 @@ describe("<Notifications />", () => {
beforeEach(async () => {
let i = 0;
mocked(secureRandomString).mockImplementation(() => {
mocked(randomString).mockImplementation(() => {
return "testid_" + i++;
});

View File

@@ -57,7 +57,7 @@ import SettingsStore from "../../../../../../../src/settings/SettingsStore";
import { getClientInformationEventType } from "../../../../../../../src/utils/device/clientInformation";
import { SDKContext, SdkContextClass } from "../../../../../../../src/contexts/SDKContext";
import { OidcClientStore } from "../../../../../../../src/stores/oidc/OidcClientStore";
import { makeDelegatedAuthConfig } from "../../../../../../test-utils/oidc";
import { mockOpenIdConfiguration } from "../../../../../../test-utils/oidc";
import MatrixClientContext from "../../../../../../../src/contexts/MatrixClientContext";
mockPlatformPeg();
@@ -215,7 +215,7 @@ describe("<SessionManagerTab />", () => {
getPushers: jest.fn(),
setPusher: jest.fn(),
setLocalNotificationSettings: jest.fn(),
getAuthMetadata: jest.fn().mockRejectedValue(new MatrixError({ errcode: "M_UNRECOGNIZED" }, 404)),
getAuthIssuer: jest.fn().mockReturnValue(new Promise(() => {})),
});
jest.clearAllMocks();
jest.spyOn(logger, "error").mockRestore();
@@ -1615,6 +1615,7 @@ describe("<SessionManagerTab />", () => {
describe("MSC4108 QR code login", () => {
const settingsValueSpy = jest.spyOn(SettingsStore, "getValue");
const issuer = "https://issuer.org";
const openIdConfiguration = mockOpenIdConfiguration(issuer);
beforeEach(() => {
settingsValueSpy.mockClear().mockReturnValue(true);
@@ -1630,16 +1631,16 @@ describe("<SessionManagerTab />", () => {
enabled: true,
},
});
const delegatedAuthConfig = makeDelegatedAuthConfig(issuer);
mockClient.getAuthMetadata.mockResolvedValue({
...delegatedAuthConfig,
mockClient.getAuthIssuer.mockResolvedValue({ issuer });
mockCrypto.exportSecretsBundle = jest.fn();
fetchMock.mock(`${issuer}/.well-known/openid-configuration`, {
...openIdConfiguration,
grant_types_supported: [
...delegatedAuthConfig.grant_types_supported,
...openIdConfiguration.grant_types_supported,
"urn:ietf:params:oauth:grant-type:device_code",
],
});
mockCrypto.exportSecretsBundle = jest.fn();
fetchMock.mock(delegatedAuthConfig.jwks_uri!, {
fetchMock.mock(openIdConfiguration.jwks_uri!, {
status: 200,
headers: {
"Content-Type": "application/json",

View File

@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
import React from "react";
import { mocked } from "jest-mock";
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
import { randomString } from "matrix-js-sdk/src/randomstring";
import { act, fireEvent, render, RenderResult } from "jest-matrix-react";
import { EventType, MatrixClient, Room, GuestAccess, HistoryVisibility, JoinRule } from "matrix-js-sdk/src/matrix";
@@ -92,7 +92,7 @@ describe("<SpaceSettingsVisibilityTab />", () => {
beforeEach(() => {
let i = 0;
mocked(secureRandomString).mockImplementation(() => {
mocked(randomString).mockImplementation(() => {
return "testid_" + i++;
});

View File

@@ -15,7 +15,7 @@ import { OidcError } from "matrix-js-sdk/src/oidc/error";
import { OidcClientStore } from "../../../../src/stores/oidc/OidcClientStore";
import { flushPromises, getMockClientWithEventEmitter, mockPlatformPeg } from "../../../test-utils";
import { makeDelegatedAuthConfig } from "../../../test-utils/oidc";
import { mockOpenIdConfiguration } from "../../../test-utils/oidc";
jest.mock("matrix-js-sdk/src/matrix", () => ({
...jest.requireActual("matrix-js-sdk/src/matrix"),
@@ -24,30 +24,28 @@ jest.mock("matrix-js-sdk/src/matrix", () => ({
describe("OidcClientStore", () => {
const clientId = "test-client-id";
const authConfig = makeDelegatedAuthConfig();
const account = authConfig.issuer + "account";
const metadata = mockOpenIdConfiguration();
const account = metadata.issuer + "account";
const mockClient = getMockClientWithEventEmitter({
getAuthMetadata: jest.fn(),
getAuthIssuer: jest.fn(),
});
beforeEach(() => {
localStorage.clear();
localStorage.setItem("mx_oidc_client_id", clientId);
localStorage.setItem("mx_oidc_token_issuer", authConfig.issuer);
localStorage.setItem("mx_oidc_token_issuer", metadata.issuer);
mocked(discoverAndValidateOIDCIssuerWellKnown)
.mockClear()
.mockResolvedValue({
...authConfig,
account_management_uri: account,
authorization_endpoint: "authorization-endpoint",
token_endpoint: "token-endpoint",
});
mocked(discoverAndValidateOIDCIssuerWellKnown).mockClear().mockResolvedValue({
metadata,
accountManagementEndpoint: account,
authorizationEndpoint: "authorization-endpoint",
tokenEndpoint: "token-endpoint",
});
jest.spyOn(logger, "error").mockClear();
fetchMock.get(`${authConfig.issuer}.well-known/openid-configuration`, authConfig);
fetchMock.get(`${authConfig.issuer}jwks`, { keys: [] });
fetchMock.get(`${metadata.issuer}.well-known/openid-configuration`, metadata);
fetchMock.get(`${metadata.issuer}jwks`, { keys: [] });
mockPlatformPeg();
});
@@ -118,7 +116,7 @@ describe("OidcClientStore", () => {
const client = await store.getOidcClient();
expect(client?.settings.client_id).toEqual(clientId);
expect(client?.settings.authority).toEqual(authConfig.issuer);
expect(client?.settings.authority).toEqual(metadata.issuer);
});
it("should set account management endpoint when configured", async () => {
@@ -131,19 +129,17 @@ describe("OidcClientStore", () => {
});
it("should set account management endpoint to issuer when not configured", async () => {
mocked(discoverAndValidateOIDCIssuerWellKnown)
.mockClear()
.mockResolvedValue({
...authConfig,
account_management_uri: undefined,
authorization_endpoint: "authorization-endpoint",
token_endpoint: "token-endpoint",
});
mocked(discoverAndValidateOIDCIssuerWellKnown).mockClear().mockResolvedValue({
metadata,
accountManagementEndpoint: undefined,
authorizationEndpoint: "authorization-endpoint",
tokenEndpoint: "token-endpoint",
});
const store = new OidcClientStore(mockClient);
await store.readyPromise;
expect(store.accountManagementEndpoint).toEqual(authConfig.issuer);
expect(store.accountManagementEndpoint).toEqual(metadata.issuer);
});
it("should reuse initialised oidc client", async () => {
@@ -179,7 +175,7 @@ describe("OidcClientStore", () => {
fetchMock.resetHistory();
fetchMock.post(
authConfig.revocation_endpoint,
metadata.revocation_endpoint,
{
status: 200,
},
@@ -201,7 +197,7 @@ describe("OidcClientStore", () => {
await store.revokeTokens(accessToken, refreshToken);
expect(fetchMock).toHaveFetchedTimes(2, authConfig.revocation_endpoint);
expect(fetchMock).toHaveFetchedTimes(2, metadata.revocation_endpoint);
expect(OidcClient.prototype.revokeToken).toHaveBeenCalledWith(accessToken, "access_token");
expect(OidcClient.prototype.revokeToken).toHaveBeenCalledWith(refreshToken, "refresh_token");
});
@@ -210,14 +206,14 @@ describe("OidcClientStore", () => {
// fail once, then succeed
fetchMock
.postOnce(
authConfig.revocation_endpoint,
metadata.revocation_endpoint,
{
status: 404,
},
{ overwriteRoutes: true, sendAsJson: true },
)
.post(
authConfig.revocation_endpoint,
metadata.revocation_endpoint,
{
status: 200,
},
@@ -230,7 +226,7 @@ describe("OidcClientStore", () => {
"Failed to revoke tokens",
);
expect(fetchMock).toHaveFetchedTimes(2, authConfig.revocation_endpoint);
expect(fetchMock).toHaveFetchedTimes(2, metadata.revocation_endpoint);
expect(OidcClient.prototype.revokeToken).toHaveBeenCalledWith(accessToken, "access_token");
});
});
@@ -241,10 +237,7 @@ describe("OidcClientStore", () => {
});
it("should resolve account management endpoint", async () => {
mockClient.getAuthMetadata.mockResolvedValue({
...authConfig,
account_management_uri: account,
});
mockClient.getAuthIssuer.mockResolvedValue({ issuer: metadata.issuer });
const store = new OidcClientStore(mockClient);
await store.readyPromise;
expect(store.accountManagementEndpoint).toBe(account);

View File

@@ -355,19 +355,21 @@ describe("AutoDiscoveryUtils", () => {
hsNameIsDifferent: true,
hsName: serverName,
delegatedAuthentication: expect.objectContaining({
issuer,
account_management_actions_supported: [
accountManagementActionsSupported: [
"org.matrix.profile",
"org.matrix.sessions_list",
"org.matrix.session_view",
"org.matrix.session_end",
"org.matrix.cross_signing_reset",
],
account_management_uri: "https://auth.matrix.org/account/",
authorization_endpoint: "https://auth.matrix.org/auth",
registration_endpoint: "https://auth.matrix.org/registration",
accountManagementEndpoint: "https://auth.matrix.org/account/",
authorizationEndpoint: "https://auth.matrix.org/auth",
metadata: expect.objectContaining({
issuer,
}),
registrationEndpoint: "https://auth.matrix.org/registration",
signingKeys: [],
token_endpoint: "https://auth.matrix.org/token",
tokenEndpoint: "https://auth.matrix.org/token",
}),
warning: null,
});

View File

@@ -38,7 +38,7 @@ describe("TokenRefresher", () => {
};
beforeEach(() => {
fetchMock.get(`${issuer}.well-known/openid-configuration`, authConfig);
fetchMock.get(`${issuer}.well-known/openid-configuration`, authConfig.metadata);
fetchMock.get(`${issuer}jwks`, {
status: 200,
headers: {

View File

@@ -49,7 +49,7 @@ describe("OIDC authorization", () => {
origin: baseUrl,
};
jest.spyOn(randomStringUtils, "secureRandomString").mockRestore();
jest.spyOn(randomStringUtils, "randomString").mockRestore();
mockPlatformPeg();
Object.defineProperty(window, "crypto", {
value: {
@@ -61,7 +61,10 @@ describe("OIDC authorization", () => {
});
beforeAll(() => {
fetchMock.get(`${delegatedAuthConfig.issuer}.well-known/openid-configuration`, delegatedAuthConfig);
fetchMock.get(
`${delegatedAuthConfig.metadata.issuer}.well-known/openid-configuration`,
delegatedAuthConfig.metadata,
);
});
afterAll(() => {

View File

@@ -58,7 +58,7 @@ describe("getOidcClientId()", () => {
const authConfigWithoutRegistration: OidcClientConfig = makeDelegatedAuthConfig(
"https://issuerWithoutStaticClientId.org/",
);
authConfigWithoutRegistration.registration_endpoint = undefined;
authConfigWithoutRegistration.registrationEndpoint = undefined;
await expect(getOidcClientId(authConfigWithoutRegistration, staticOidcClients)).rejects.toThrow(
OidcError.DynamicRegistrationNotSupported,
);
@@ -69,7 +69,7 @@ describe("getOidcClientId()", () => {
it("should handle when staticOidcClients object is falsy", async () => {
const authConfigWithoutRegistration: OidcClientConfig = {
...delegatedAuthConfig,
registration_endpoint: undefined,
registrationEndpoint: undefined,
};
await expect(getOidcClientId(authConfigWithoutRegistration)).rejects.toThrow(
OidcError.DynamicRegistrationNotSupported,
@@ -79,14 +79,14 @@ describe("getOidcClientId()", () => {
});
it("should make correct request to register client", async () => {
fetchMockJest.post(delegatedAuthConfig.registration_endpoint!, {
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
status: 200,
body: JSON.stringify({ client_id: dynamicClientId }),
});
expect(await getOidcClientId(delegatedAuthConfig)).toEqual(dynamicClientId);
// didn't try to register
expect(fetchMockJest).toHaveBeenCalledWith(
delegatedAuthConfig.registration_endpoint!,
delegatedAuthConfig.registrationEndpoint!,
expect.objectContaining({
headers: {
"Accept": "application/json",
@@ -111,14 +111,14 @@ describe("getOidcClientId()", () => {
});
it("should throw when registration request fails", async () => {
fetchMockJest.post(delegatedAuthConfig.registration_endpoint!, {
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
status: 500,
});
await expect(getOidcClientId(delegatedAuthConfig)).rejects.toThrow(OidcError.DynamicRegistrationFailed);
});
it("should throw when registration response is invalid", async () => {
fetchMockJest.post(delegatedAuthConfig.registration_endpoint!, {
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
status: 200,
// no clientId in response
body: "{}",

1872
yarn.lock

File diff suppressed because it is too large Load Diff