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
80 changed files with 1417 additions and 2069 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"
},
@@ -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

@@ -26,8 +26,7 @@ Please see LICENSE files in the repository root for full details.
}
&.mx_UserPill_me,
&.mx_AtRoomPill,
&.mx_KeywordPill {
&.mx_AtRoomPill {
background-color: var(--cpd-color-bg-critical-primary) !important; /* To override .markdown-body */
}
@@ -46,8 +45,7 @@ Please see LICENSE files in the repository root for full details.
}
/* We don't want to indicate clickability */
&.mx_AtRoomPill:hover,
&.mx_KeywordPill:hover {
&.mx_AtRoomPill:hover {
background-color: var(--cpd-color-bg-critical-primary) !important; /* To override .markdown-body */
cursor: unset;
}

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

@@ -135,6 +135,12 @@ $left-gutter: 64px;
}
}
&.mx_EventTile_highlight,
&.mx_EventTile_highlight .markdown-body,
&.mx_EventTile_highlight .mx_EventTile_edited {
color: $alert;
}
&.mx_EventTile_bubbleContainer {
display: grid;
grid-template-columns: 1fr 100px;

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

@@ -19,11 +19,6 @@ Please see LICENSE files in the repository root for full details.
width: 100%;
gap: 0;
}
.mx_UserProfileSettings_profile_statusMessage {
flex-grow: 1;
width: 100%;
gap: 0;
}
}
.mx_UserProfileSettings_profile_controls {

View File

@@ -54,7 +54,6 @@ import { deop, op } from "./slash-commands/op";
import { CommandCategories } from "./slash-commands/interface";
import { Command } from "./slash-commands/command";
import { goto, join } from "./slash-commands/join";
import { SdkContextClass } from "./contexts/SDKContext";
export { CommandCategories, Command };
@@ -884,25 +883,6 @@ export const Commands = [
},
renderingTypes: [TimelineRenderingType.Room],
}),
new Command({
command: "statusmsg",
description: _td("slash_command|status_msg"),
category: CommandCategories.actions,
args: "<message>",
isEnabled: (cli) => !isCurrentLocalRoom(cli),
runFn: function (cli, _roomId, _threadId, args) {
return success((async () => {
const supported = await cli.doesServerSupportExtendedProfiles();
if (!supported) return reject(new UserFriendlyError("slash_command|extended_profile_not_supported"));
if (!args) return reject(new UserFriendlyError("slash_command|no_status_message"));
await cli.setExtendedProfileProperty("uk.half-shot.status", args);
// Refresh profile.
void SdkContextClass.instance.userProfilesStore.fetchProfile(cli.getSafeUserId());
})());
},
renderingTypes: [TimelineRenderingType.Room],
}),
// Command definitions for autocompletion ONLY:
// /me is special because its not handled by SlashCommands.js and is instead done inside the Composer classes

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

@@ -18,7 +18,6 @@ import { RoomMember } from "../../../../models/rooms/RoomMember";
import { _t, _td, TranslationKey } from "../../../../languageHandler";
import UserIdentifierCustomisations from "../../../../customisations/UserIdentifier";
import { E2EStatus } from "../../../../utils/ShieldUtils";
import { useUserProfileValue } from "../../../../hooks/useUserProfileValue";
interface MemberTileViewModelProps {
member: RoomMember;
@@ -31,7 +30,6 @@ export interface MemberTileViewState extends MemberTileViewModelProps {
onClick: () => void;
title?: string;
userLabel?: string;
statusMessage?: string;
}
export enum PowerStatus {
@@ -46,10 +44,10 @@ const PowerLabel: Record<PowerStatus, TranslationKey> = {
export function useMemberTileViewModel(props: MemberTileViewModelProps): MemberTileViewState {
const [e2eStatus, setE2eStatus] = useState<E2EStatus | undefined>();
const cli = MatrixClientPeg.safeGet();
const statusMessage = useUserProfileValue(cli, "uk.half-shot.status", props.member.userId);
useEffect(() => {
const cli = MatrixClientPeg.safeGet();
const updateE2EStatus = async (): Promise<void> => {
const { userId } = props.member;
const isMe = userId === cli.getUserId();
@@ -105,7 +103,7 @@ export function useMemberTileViewModel(props: MemberTileViewModelProps): MemberT
cli.removeListener(CryptoEvent.UserTrustStatusChanged, onUserTrustStatusChanged);
}
};
}, [props.member, cli]);
}, [props.member]);
const onClick = (): void => {
dis.dispatch({
@@ -147,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 {
@@ -158,6 +156,5 @@ export function useMemberTileViewModel(props: MemberTileViewModelProps): MemberT
e2eStatus,
showPresence: props.showPresence,
userLabel,
statusMessage: statusMessage ?? undefined,
};
}

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

@@ -25,7 +25,6 @@ export enum PillType {
AtRoomMention = "TYPE_AT_ROOM_MENTION", // '@room' mention
EventInSameRoom = "TYPE_EVENT_IN_SAME_ROOM",
EventInOtherRoom = "TYPE_EVENT_IN_OTHER_ROOM",
Keyword = "TYPE_KEYWORD", // Used to highlight keywords that triggered a notification rule
}
export const pillRoomNotifPos = (text: string | null): number => {
@@ -77,32 +76,14 @@ export interface PillProps {
room?: Room;
// Whether to include an avatar in the pill
shouldShowPillAvatar?: boolean;
// Explicitly-provided text to display in the pill
text?: string;
}
export const Pill: React.FC<PillProps> = ({
type: propType,
url,
inMessage,
room,
shouldShowPillAvatar = true,
text: customPillText,
}) => {
const {
event,
member,
onClick,
resourceId,
targetRoom,
text: linkText,
type,
} = usePermalink({
export const Pill: React.FC<PillProps> = ({ type: propType, url, inMessage, room, shouldShowPillAvatar = true }) => {
const { event, member, onClick, resourceId, targetRoom, text, type } = usePermalink({
room,
type: propType,
url,
});
const text = customPillText ?? linkText;
if (!type || !text) {
return null;
@@ -115,7 +96,6 @@ export const Pill: React.FC<PillProps> = ({
mx_UserPill: type === PillType.UserMention,
mx_UserPill_me: resourceId === MatrixClientPeg.safeGet().getUserId(),
mx_EventPill: type === PillType.EventInOtherRoom || type === PillType.EventInSameRoom,
mx_KeywordPill: type === PillType.Keyword,
});
let avatar: ReactElement | null = null;
@@ -151,8 +131,6 @@ export const Pill: React.FC<PillProps> = ({
case PillType.UserMention:
avatar = <PillMemberAvatar shouldShowPillAvatar={shouldShowPillAvatar} member={member} />;
break;
case PillType.Keyword:
break;
default:
return null;
}

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

@@ -28,12 +28,11 @@ interface IProps {
colored?: boolean;
emphasizeDisplayName?: boolean;
withTooltip?: boolean;
statusMessage?: string;
}
export default class DisambiguatedProfile extends React.Component<IProps> {
public render(): React.ReactNode {
const { fallbackName, member, colored, emphasizeDisplayName, withTooltip, onClick, statusMessage } = this.props;
const { fallbackName, member, colored, emphasizeDisplayName, withTooltip, onClick } = this.props;
const rawDisplayName = member?.rawDisplayName || fallbackName;
const mxid = member?.userId;
@@ -43,7 +42,6 @@ export default class DisambiguatedProfile extends React.Component<IProps> {
}
let mxidElement;
let statusElement;
let title: string | undefined;
if (mxid) {
@@ -61,10 +59,6 @@ export default class DisambiguatedProfile extends React.Component<IProps> {
});
}
if (statusMessage) {
statusElement = <span className="mx_DisambiguatedProfile_mxid">({statusMessage})</span>;
}
const displayNameClasses = classNames(colorClass, {
mx_DisambiguatedProfile_displayName: emphasizeDisplayName,
});
@@ -75,7 +69,6 @@ export default class DisambiguatedProfile extends React.Component<IProps> {
{rawDisplayName}
</span>
{mxidElement}
{statusElement}
</div>
);
}

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

@@ -12,8 +12,6 @@ import { MatrixEvent, MsgType } from "matrix-js-sdk/src/matrix";
import DisambiguatedProfile from "./DisambiguatedProfile";
import { useRoomMemberProfile } from "../../../hooks/room/useRoomMemberProfile";
import { useUserProfileValue } from "../../../hooks/useUserProfileValue";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
interface IProps {
mxEvent: MatrixEvent;
@@ -26,11 +24,9 @@ export default function SenderProfile({ mxEvent, onClick, withTooltip }: IProps)
userId: mxEvent.getSender(),
member: mxEvent.sender,
});
const statusMessage = useUserProfileValue(MatrixClientPeg.safeGet(), "uk.half-shot.status", mxEvent.sender?.userId);
return mxEvent.getContent().msgtype !== MsgType.Emote ? (
<DisambiguatedProfile
statusMessage={statusMessage ?? undefined}
fallbackName={mxEvent.getSender() ?? ""}
onClick={onClick}
member={member}

View File

@@ -7,9 +7,8 @@ Please see LICENSE files in the repository root for full details.
*/
import React, { createRef, SyntheticEvent, MouseEvent, StrictMode } from "react";
import { MsgType, PushRuleKind } from "matrix-js-sdk/src/matrix";
import { MsgType } from "matrix-js-sdk/src/matrix";
import { TooltipProvider } from "@vector-im/compound-web";
import { globToRegexp } from "matrix-js-sdk/src/utils";
import * as HtmlUtils from "../../../HtmlUtils";
import { formatDate } from "../../../DateUtils";
@@ -36,7 +35,6 @@ import { EditWysiwygComposer } from "../rooms/wysiwyg_composer";
import { IEventTileOps } from "../rooms/EventTile";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import CodeBlock from "./CodeBlock";
import { Pill, PillType } from "../elements/Pill";
import { ReactRootManager } from "../../../utils/react";
interface IState {
@@ -102,16 +100,6 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
}
}
}
// Highlight notification keywords using pills
const pushDetails = this.props.mxEvent.getPushDetails();
if (
pushDetails.rule?.enabled &&
pushDetails.rule.kind === PushRuleKind.ContentSpecific &&
pushDetails.rule.pattern
) {
this.pillifyNotificationKeywords([content], this.regExpForKeywordPattern(pushDetails.rule.pattern));
}
}
private addCodeElement(pre: HTMLPreElement): void {
@@ -222,55 +210,6 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
}
}
/**
* Marks the text that activated a push-notification keyword pattern.
*/
private pillifyNotificationKeywords(nodes: ArrayLike<Element>, exp: RegExp): void {
let node: Node | null = nodes[0];
while (node) {
if (node.nodeType === Node.TEXT_NODE) {
const text = node.nodeValue;
if (!text) {
node = node.nextSibling;
continue;
}
const match = text.match(exp);
if (!match || match.length < 3) {
node = node.nextSibling;
continue;
}
const keywordText = match[2];
const idx = match.index! + match[1].length;
const before = text.substring(0, idx);
const after = text.substring(idx + keywordText.length);
const container = document.createElement("span");
const newContent = (
<>
{before}
<TooltipProvider>
<Pill text={keywordText} type={PillType.Keyword} />
</TooltipProvider>
{after}
</>
);
this.reactRoots.render(newContent, container, node);
node.parentNode?.replaceChild(container, node);
} else if (node.childNodes && node.childNodes.length) {
this.pillifyNotificationKeywords(node.childNodes as NodeListOf<Element>, exp);
}
node = node.nextSibling;
}
}
private regExpForKeywordPattern(pattern: string): RegExp {
// Reflects the push notification pattern-matching implementation at
// https://github.com/matrix-org/matrix-js-sdk/blob/dbd7d26968b94700827bac525c39afff2c198e61/src/pushprocessor.ts#L570
return new RegExp("(^|\\W)(" + globToRegexp(pattern) + ")(\\W|$)", "i");
}
private findLinks(nodes: ArrayLike<Element>): string[] {
let links: string[] = [];

View File

@@ -85,7 +85,6 @@ import { asyncSome } from "../../../utils/arrays";
import { Flex } from "../../utils/Flex";
import CopyableText from "../elements/CopyableText";
import { useUserTimezone } from "../../../hooks/useUserTimezone";
import { useUserProfileValue } from "../../../hooks/useUserProfileValue";
export interface IDevice extends Device {
ambiguous?: boolean;
}
@@ -1635,8 +1634,6 @@ export const UserInfoHeader: React.FC<{
roomId?: string;
}> = ({ member, e2eStatus, roomId }) => {
const cli = useContext(MatrixClientContext);
console.log(cli, member);
const statusMessage = useUserProfileValue(cli, "uk.half-shot.status", member.userId, true);
const onMemberAvatarClick = useCallback(() => {
const avatarUrl = (member as RoomMember).getMxcAvatarUrl
@@ -1670,7 +1667,6 @@ export const UserInfoHeader: React.FC<{
if (enablePresenceByHsUrl && enablePresenceByHsUrl[cli.baseUrl] !== undefined) {
showPresence = enablePresenceByHsUrl[cli.baseUrl];
}
let presenceLabel: JSX.Element | undefined;
if (showPresence) {
@@ -1679,13 +1675,10 @@ export const UserInfoHeader: React.FC<{
activeAgo={presenceLastActiveAgo}
currentlyActive={presenceCurrentlyActive}
presenceState={presenceState}
statusMessage={statusMessage ?? undefined}
className="mx_UserInfo_profileStatus"
coloured
/>
);
} else if (statusMessage) {
presenceLabel = <span>{statusMessage}</span>
}
const timezoneInfo = useUserTimezone(cli, member.userId);

View File

@@ -19,10 +19,9 @@ interface IEmojiButtonProps {
addEmoji: (unicode: string) => boolean;
menuPosition?: MenuProps;
className?: string;
single?: boolean;
}
export function EmojiButton({ addEmoji, menuPosition, className, single }: IEmojiButtonProps): JSX.Element {
export function EmojiButton({ addEmoji, menuPosition, className }: IEmojiButtonProps): JSX.Element {
const overflowMenuCloser = useContext(OverflowMenuContext);
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
@@ -33,19 +32,10 @@ export function EmojiButton({ addEmoji, menuPosition, className, single }: IEmoj
closeMenu();
overflowMenuCloser?.();
};
const onChoose = (emoji: string) => {
try {
return addEmoji(emoji);
} finally {
if (single) {
onFinished();
}
}
};
contextMenu = (
<ContextMenu {...position} onFinished={onFinished} managed={false}>
<EmojiPicker onChoose={onChoose} onFinished={onFinished} />
<EmojiPicker onChoose={addEmoji} onFinished={onFinished} />
</ContextMenu>
);
}

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;
@@ -36,7 +35,7 @@ export function RoomMemberTileView(props: IProps): JSX.Element {
/>
);
const name = vm.name;
const nameJSX = <DisambiguatedProfile statusMessage={vm.statusMessage} member={member} fallbackName={name || ""} />;
const nameJSX = <DisambiguatedProfile member={member} fallbackName={name || ""} />;
const presenceState = member.presenceState;
let presenceJSX: JSX.Element | undefined;
@@ -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

@@ -27,7 +27,6 @@ interface IProps {
// whether to apply colouring to the label
coloured?: boolean;
className?: string;
statusMessage?: string;
}
export default class PresenceLabel extends React.Component<IProps> {
@@ -58,7 +57,7 @@ export default class PresenceLabel extends React.Component<IProps> {
}
public render(): React.ReactNode {
return <>
return (
<div
className={classNames("mx_PresenceLabel", this.props.className, {
mx_PresenceLabel_online: this.props.coloured && this.props.presenceState === "online",
@@ -66,8 +65,6 @@ export default class PresenceLabel extends React.Component<IProps> {
>
{this.getPrettyPresence(this.props.presenceState, this.props.activeAgo, this.props.currentlyActive)}
</div>
<p>{this.props.statusMessage}</p>
</>;
);
}
}

View File

@@ -27,9 +27,6 @@ import LogoutDialog, { shouldShowLogoutDialog } from "../dialogs/LogoutDialog";
import Modal from "../../../Modal";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { Flex } from "../../utils/Flex";
import { useUserProfileValue } from "../../../hooks/useUserProfileValue";
import { SdkContextClass } from "../../../contexts/SDKContext";
import { EmojiButton } from "../rooms/EmojiButton";
const SpinnerToast: React.FC<{ children?: ReactNode }> = ({ children }) => (
<>
@@ -117,21 +114,9 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({
const [maxUploadSize, setMaxUploadSize] = useState<number | undefined>();
const [displayNameError, setDisplayNameError] = useState<boolean>(false);
const [statusMessageSupported, setStatusMessageSupported] = useState<boolean>(false);
const [statusMessageError, setStatusMessageError] = useState<boolean>(false);
const toastRack = useToastContext();
const client = useMatrixClientContext();
const currentStatusMessage = useUserProfileValue(client, "uk.half-shot.status", client.getSafeUserId(), true);
const [statusMessage, setStatusMessage] = useState<string>(currentStatusMessage ?? "");
useEffect(() => {
if (currentStatusMessage) {
setStatusMessage(currentStatusMessage);
}
}, [currentStatusMessage]);
useEffect(() => {
(async () => {
@@ -144,13 +129,6 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({
})();
}, [client]);
useEffect(() => {
(async () => {
const supported = await client.doesServerSupportExtendedProfiles();
setStatusMessageSupported(supported);
})();
}, [client]);
const onAvatarRemove = useCallback(async () => {
const removeToast = toastRack.displayToast(
<SpinnerToast>{_t("settings|general|avatar_remove_progress")}</SpinnerToast>,
@@ -212,31 +190,6 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({
[client],
);
const onStatusChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setStatusMessage(e.target.value);
}, []);
const onStatusAddEmoji = useCallback((emoij: string) => {
setStatusMessage(s => s + emoij);
return true;
}, []);
const onStatusCancelled = useCallback(() => {
setStatusMessage(currentStatusMessage ?? "");
}, [currentStatusMessage]);
const onStatusSave = useCallback(async (): Promise<void> => {
try {
setStatusMessageError(false);
await client.setExtendedProfileProperty("uk.half-shot.status", statusMessage);
// Force profile to be refetched.
SdkContextClass.instance.userProfilesStore.fetchProfile(client.getSafeUserId());
} catch (e) {
setStatusMessageError(true);
throw e;
}
}, [statusMessage, client]);
const someFieldsDisabled = !canSetDisplayName || !canSetAvatar;
return (
@@ -272,22 +225,6 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({
>
{displayNameError && <ErrorMessage>{_t("settings|general|display_name_error")}</ErrorMessage>}
</EditInPlace>
<EditInPlace
className="mx_UserProfileSettings_profile_statusMessage"
label={_t("settings|general|status_message")}
value={statusMessage}
saveButtonLabel={_t("common|save")}
cancelButtonLabel={_t("common|cancel")}
savedLabel={_t("common|saved")}
savingLabel={_t("common|updating")}
onChange={onStatusChanged}
onCancel={onStatusCancelled}
onSave={onStatusSave}
disabled={!statusMessageSupported}
>
{statusMessageError && <ErrorMessage>{_t("settings|general|status_message_error")}</ErrorMessage>}
</EditInPlace>
{statusMessageSupported ? <EmojiButton single addEmoji={onStatusAddEmoji}/> : null}
</div>
{avatarError && (
<Alert title={_t("settings|general|avatar_upload_error_title")} type="critical">

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

@@ -1,77 +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 { useEffect, useState } from "react";
import { MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix";
import { SdkContextClass } from "../contexts/SDKContext";
import { UserProfilesStoreEvents } from "../stores/UserProfilesStore";
import { useTypedEventEmitter } from "./useEventEmitter";
/**
* Fetch a key from a user's extended profile, and regularly refetch at a
* given interval.
*
* @param cli The Matrix Client instance.
* @param key The key to fetch.
* @param userId The user who's profile we're interested in.
* @param ignoreCache Ignore the cache on first load.
* @returns The value from the profile, or null if not set.
*/
export const useUserProfileValue = (cli: MatrixClient, key: string, userId?: string, ignoreCache?: boolean): string|null => {
const [currentValue, setProfileValue] = useState<string|null>(null);
const profilesStore = SdkContextClass.instance.userProfilesStore;
useEffect(() => {
if (!userId) {
return;
}
profilesStore.subscribeToProfile(userId);
return () => profilesStore.unsubscribeToProfile(userId);
}, [userId]);
useTypedEventEmitter(profilesStore, UserProfilesStoreEvents.ProfileUpdated, (updatedUserId, updatedProfile) => {
if (userId !== updatedUserId) {
return;
}
const value = (updatedProfile as any)?.[key];
if (!value) {
return;
}
if (typeof value !== "string") {
// Err, definitely not a tz.
throw Error("Profile value was not a string");
}
setProfileValue(value);
});
useEffect(() => {
if (!userId) {
return;
}
(async () => {
try {
const profile = await (ignoreCache ? profilesStore.fetchProfile(userId) : profilesStore.getOrFetchProfile(userId));
// TODO: Types.
const value = (profile as any)?.[key];
if (!value) {
return;
}
if (typeof value !== "string") {
// Err, definitely not a tz.
throw Error("Profile value was not a string");
}
setProfileValue(value);
} catch (ex) {
if (ex instanceof MatrixError && ex.errcode === "M_NOT_FOUND") {
// No timezone set, ignore.
setProfileValue(null);
return;
}
}
})();
}, [userId, key, cli]);
return currentValue;
};

View File

@@ -48,6 +48,7 @@ export const useUserTimezone = (cli: MatrixClient, userId: string): { timezone:
return;
}
(async () => {
console.log("Trying to fetch TZ");
try {
const tz = await cli.getExtendedProfileProperty(userId, "us.cloke.msc4175.tz");
if (typeof tz !== "string") {

View File

@@ -2532,8 +2532,6 @@
"remove_email_prompt": "Remove %(email)s?",
"remove_msisdn_prompt": "Remove %(phone)s?",
"spell_check_locale_placeholder": "Choose a locale",
"status_message": "Current status",
"status_message_error": "Failed to set status message",
"unable_to_load_emails": "Unable to load email addresses",
"unable_to_load_msisdns": "Unable to load phone numbers",
"username": "Username"
@@ -2963,7 +2961,6 @@
"error_invalid_room": "Command failed: Unable to find room (%(roomId)s)",
"error_invalid_runfn": "Command error: Unable to handle slash command.",
"error_invalid_user_in_room": "Could not find user in room",
"extended_profile_not_supported": "Status messages are not supported on this server",
"help": "Displays list of commands with usages and descriptions",
"help_dialog_title": "Command Help",
"holdcall": "Places the call in the current room on hold",
@@ -2987,7 +2984,6 @@
"myroomnick": "Changes your display nickname in the current room only",
"nick": "Changes your display nickname",
"no_active_call": "No active call in this room",
"no_status_message": "You need to provide a status message",
"op": "Define the power level of a user",
"part_unknown_alias": "Unrecognised room address: %(roomAlias)s",
"plain": "Sends a message as plain text, without interpreting it as markdown",
@@ -3003,7 +2999,6 @@
"server_error_detail": "Server unavailable, overloaded, or something else went wrong.",
"shrug": "Prepends ¯\\_(ツ)_/¯ to a plain-text message",
"spoiler": "Sends the given message as a spoiler",
"status_msg": "Update your status message in your profile",
"tableflip": "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message",
"topic": "Gets or sets the room topic",
"topic_none": "This room has no topic.",

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

@@ -14,9 +14,6 @@ import {
MatrixEvent,
RoomMember,
RoomMemberEvent,
SyncState,
TypedEventEmitter,
ClientEvent,
} from "matrix-js-sdk/src/matrix";
import { LruCache } from "../utils/LruCache";
@@ -30,67 +27,19 @@ interface GetOptions {
shouldThrow: boolean;
}
export enum UserProfilesStoreEvents {
ProfileUpdated = "profile_updated",
}
interface UserProfilesStoreEventsMap {
[UserProfilesStoreEvents.ProfileUpdated]: (userId: string, profile: IMatrixProfile|null) => void,
}
/**
* This store provides cached access to user profiles.
* Listens for membership events and invalidates the cache for a profile on update with different profile values.
*/
export class UserProfilesStore extends TypedEventEmitter<UserProfilesStoreEvents, UserProfilesStoreEventsMap> {
export class UserProfilesStore {
private profiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
private profileLookupErrors = new LruCache<string, MatrixError>(cacheSize);
private knownProfiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
private readonly profileSubscriptions = new Map<string, {subs: number, lastSync: number}>();
public constructor(private client: MatrixClient) {
super();
client.on(RoomMemberEvent.Membership, this.onRoomMembershipEvent);
client.on(ClientEvent.Sync, (state) => {
if (state !== SyncState.Syncing) {
return;
}
const time = Date.now();
for (const [userId, entry] of this.profileSubscriptions.entries()) {
if (time - entry.lastSync < 60000) {
continue;
}
void this.fetchProfile(userId, { shouldThrow: false});
entry.lastSync = time;
}
})
}
public subscribeToProfile(userId: string) {
const existingProfile = this.profileSubscriptions.get(userId);
console.log(`Sub for ${userId} incremented to ${(existingProfile?.subs ?? 0) + 1}`);
if (existingProfile) {
existingProfile.subs += 1;
} else {
this.profileSubscriptions.set(userId, { subs: 1, lastSync: 0})
}
console.log(`Number of subs: ${this.profileSubscriptions.size}`);
}
public unsubscribeToProfile(userId: string) {
const existingProfile = this.profileSubscriptions.get(userId);
if (!existingProfile) {
return;
}
existingProfile.subs -= 1;
console.log(`Sub for ${userId} reduced to ${existingProfile.subs}`);
if (existingProfile.subs === 0) {
this.profileSubscriptions.delete(userId);
}
}
/**
* Synchronously get a profile from the store cache.
*
@@ -155,7 +104,6 @@ export class UserProfilesStore extends TypedEventEmitter<UserProfilesStoreEvents
public async fetchProfile(userId: string, options?: GetOptions): Promise<IMatrixProfile | null> {
const profile = await this.fetchProfileFromApi(userId, options);
this.profiles.set(userId, profile);
this.emit(UserProfilesStoreEvents.ProfileUpdated, userId, profile);
return profile;
}

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

@@ -6,14 +6,7 @@
* Please see LICENSE files in the repository root for full details.
*/
import {
Room,
MatrixEvent,
MatrixEventEvent,
MatrixClient,
ClientEvent,
RoomStateEvent,
} from "matrix-js-sdk/src/matrix";
import { Room, MatrixEvent, MatrixEventEvent, MatrixClient, ClientEvent } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import {
ClientWidgetApi,
@@ -33,6 +26,7 @@ import {
WidgetApiFromWidgetAction,
WidgetKind,
} from "matrix-widget-api";
import { Optional } from "matrix-events-sdk";
import { EventEmitter } from "events";
import { logger } from "matrix-js-sdk/src/logger";
@@ -62,7 +56,6 @@ import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
import Modal from "../../Modal";
import ErrorDialog from "../../components/views/dialogs/ErrorDialog";
import { SdkContextClass } from "../../contexts/SDKContext";
import { UPDATE_EVENT } from "../AsyncStore";
// TODO: Destroy all of this code
@@ -158,9 +151,6 @@ export class StopGapWidget extends EventEmitter {
private mockWidget: ElementWidget;
private scalarToken?: string;
private roomId?: string;
// The room that we're currently allowing the widget to interact with. Only
// used for account widgets, which may follow the user to different rooms.
private viewedRoomId: string | null = null;
private kind: WidgetKind;
private readonly virtual: boolean;
private readUpToMap: { [roomId: string]: string } = {}; // room ID to event ID
@@ -187,6 +177,17 @@ export class StopGapWidget extends EventEmitter {
this.stickyPromise = appTileProps.stickyPromise;
}
private get eventListenerRoomId(): Optional<string> {
// When widgets are listening to events, we need to make sure they're only
// receiving events for the right room. In particular, room widgets get locked
// to the room they were added in while account widgets listen to the currently
// active room.
if (this.roomId) return this.roomId;
return SdkContextClass.instance.roomViewStore.getRoomId();
}
public get widgetApi(): ClientWidgetApi | null {
return this.messaging;
}
@@ -258,17 +259,6 @@ export class StopGapWidget extends EventEmitter {
});
}
};
// This listener is only active for account widgets, which may follow the
// user to different rooms
private onRoomViewStoreUpdate = (): void => {
const roomId = SdkContextClass.instance.roomViewStore.getRoomId() ?? null;
if (roomId !== this.viewedRoomId) {
this.messaging!.setViewedRoomId(roomId);
this.viewedRoomId = roomId;
}
};
/**
* This starts the messaging for the widget if it is not in the state `started` yet.
* @param iframe the iframe the widget should use
@@ -295,17 +285,6 @@ export class StopGapWidget extends EventEmitter {
this.messaging.on("capabilitiesNotified", () => this.emit("capabilitiesNotified"));
this.messaging.on(`action:${WidgetApiFromWidgetAction.OpenModalWidget}`, this.onOpenModal);
// When widgets are listening to events, we need to make sure they're only
// receiving events for the right room
if (this.roomId === undefined) {
// Account widgets listen to the currently active room
this.messaging.setViewedRoomId(SdkContextClass.instance.roomViewStore.getRoomId() ?? null);
SdkContextClass.instance.roomViewStore.on(UPDATE_EVENT, this.onRoomViewStoreUpdate);
} else {
// Room widgets get locked to the room they were added in
this.messaging.setViewedRoomId(this.roomId);
}
// Always attach a handler for ViewRoom, but permission check it internally
this.messaging.on(`action:${ElementWidgetActions.ViewRoom}`, (ev: CustomEvent<IViewRoomApiRequest>) => {
ev.preventDefault(); // stop the widget API from auto-rejecting this
@@ -350,7 +329,6 @@ export class StopGapWidget extends EventEmitter {
// Attach listeners for feeding events - the underlying widget classes handle permissions for us
this.client.on(ClientEvent.Event, this.onEvent);
this.client.on(MatrixEventEvent.Decrypted, this.onEventDecrypted);
this.client.on(RoomStateEvent.Events, this.onStateUpdate);
this.client.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
this.messaging.on(
@@ -479,11 +457,8 @@ export class StopGapWidget extends EventEmitter {
WidgetMessagingStore.instance.stopMessaging(this.mockWidget, this.roomId);
this.messaging = null;
SdkContextClass.instance.roomViewStore.off(UPDATE_EVENT, this.onRoomViewStoreUpdate);
this.client.off(ClientEvent.Event, this.onEvent);
this.client.off(MatrixEventEvent.Decrypted, this.onEventDecrypted);
this.client.off(RoomStateEvent.Events, this.onStateUpdate);
this.client.off(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
}
@@ -496,14 +471,6 @@ export class StopGapWidget extends EventEmitter {
this.feedEvent(ev);
};
private onStateUpdate = (ev: MatrixEvent): void => {
if (this.messaging === null) return;
const raw = ev.getEffectiveEvent();
this.messaging.feedStateUpdate(raw as IRoomEvent).catch((e) => {
logger.error("Error sending state update to widget: ", e);
});
};
private onToDeviceEvent = async (ev: MatrixEvent): Promise<void> => {
await this.client.decryptEventIfNeeded(ev);
if (ev.isDecryptionFailure()) return;
@@ -603,7 +570,7 @@ export class StopGapWidget extends EventEmitter {
this.eventsToFeed.add(ev);
} else {
const raw = ev.getEffectiveEvent();
this.messaging.feedEvent(raw as IRoomEvent).catch((e) => {
this.messaging.feedEvent(raw as IRoomEvent, this.eventListenerRoomId!).catch((e) => {
logger.error("Error sending event to widget: ", e);
});
}

View File

@@ -19,6 +19,7 @@ import {
MatrixCapabilities,
OpenIDRequestState,
SimpleObservable,
Symbols,
Widget,
WidgetDriver,
WidgetEventCapability,
@@ -35,6 +36,7 @@ import {
IContent,
MatrixError,
MatrixEvent,
Room,
Direction,
THREAD_RELATION_TYPE,
SendDelayedEventResponse,
@@ -467,69 +469,70 @@ export class StopGapWidgetDriver extends WidgetDriver {
}
}
/**
* Reads all events of the given type, and optionally `msgtype` (if applicable/defined),
* the user has access to. The widget API will have already verified that the widget is
* capable of receiving the events. Less events than the limit are allowed to be returned,
* but not more.
* @param roomId The ID of the room to look within.
* @param eventType The event type to be read.
* @param msgtype The msgtype of the events to be read, if applicable/defined.
* @param stateKey The state key of the events to be read, if applicable/defined.
* @param limit The maximum number of events to retrieve. Will be zero to denote "as many as
* possible".
* @param since When null, retrieves the number of events specified by the "limit" parameter.
* Otherwise, the event ID at which only subsequent events will be returned, as many as specified
* in "limit".
* @returns {Promise<IRoomEvent[]>} Resolves to the room events, or an empty array.
*/
public async readRoomTimeline(
roomId: string,
eventType: string,
msgtype: string | undefined,
stateKey: string | undefined,
limit: number,
since: string | undefined,
): Promise<IRoomEvent[]> {
limit = limit > 0 ? Math.min(limit, Number.MAX_SAFE_INTEGER) : Number.MAX_SAFE_INTEGER; // relatively arbitrary
private pickRooms(roomIds?: (string | Symbols.AnyRoom)[]): Room[] {
const client = MatrixClientPeg.get();
if (!client) throw new Error("Not attached to a client");
const room = MatrixClientPeg.safeGet().getRoom(roomId);
if (room === null) return [];
const results: MatrixEvent[] = [];
const events = room.getLiveTimeline().getEvents(); // timelines are most recent last
for (let i = events.length - 1; i >= 0; i--) {
const ev = events[i];
if (results.length >= limit) break;
if (since !== undefined && ev.getId() === since) break;
if (ev.getType() !== eventType || ev.isState()) continue;
if (eventType === EventType.RoomMessage && msgtype && msgtype !== ev.getContent()["msgtype"]) continue;
if (ev.getStateKey() !== undefined && stateKey !== undefined && ev.getStateKey() !== stateKey) continue;
results.push(ev);
}
return results.map((e) => e.getEffectiveEvent() as IRoomEvent);
const targetRooms = roomIds
? roomIds.includes(Symbols.AnyRoom)
? client.getVisibleRooms(SettingsStore.getValue("feature_dynamic_room_predecessors"))
: roomIds.map((r) => client.getRoom(r))
: [client.getRoom(SdkContextClass.instance.roomViewStore.getRoomId()!)];
return targetRooms.filter((r) => !!r) as Room[];
}
/**
* Reads the current values of all matching room state entries.
* @param roomId The ID of the room.
* @param eventType The event type of the entries to be read.
* @param stateKey The state key of the entry to be read. If undefined,
* all room state entries with a matching event type should be returned.
* @returns {Promise<IRoomEvent[]>} Resolves to the events representing the
* current values of the room state entries.
*/
public async readRoomState(roomId: string, eventType: string, stateKey: string | undefined): Promise<IRoomEvent[]> {
const room = MatrixClientPeg.safeGet().getRoom(roomId);
if (room === null) return [];
const state = room.getLiveTimeline().getState(Direction.Forward);
if (state === undefined) return [];
public async readRoomEvents(
eventType: string,
msgtype: string | undefined,
limitPerRoom: number,
roomIds?: (string | Symbols.AnyRoom)[],
): Promise<IRoomEvent[]> {
limitPerRoom = limitPerRoom > 0 ? Math.min(limitPerRoom, Number.MAX_SAFE_INTEGER) : Number.MAX_SAFE_INTEGER; // relatively arbitrary
if (stateKey === undefined)
return state.getStateEvents(eventType).map((e) => e.getEffectiveEvent() as IRoomEvent);
const event = state.getStateEvents(eventType, stateKey);
return event === null ? [] : [event.getEffectiveEvent() as IRoomEvent];
const rooms = this.pickRooms(roomIds);
const allResults: IRoomEvent[] = [];
for (const room of rooms) {
const results: MatrixEvent[] = [];
const events = room.getLiveTimeline().getEvents(); // timelines are most recent last
for (let i = events.length - 1; i > 0; i--) {
if (results.length >= limitPerRoom) break;
const ev = events[i];
if (ev.getType() !== eventType || ev.isState()) continue;
if (eventType === EventType.RoomMessage && msgtype && msgtype !== ev.getContent()["msgtype"]) continue;
results.push(ev);
}
results.forEach((e) => allResults.push(e.getEffectiveEvent() as IRoomEvent));
}
return allResults;
}
public async readStateEvents(
eventType: string,
stateKey: string | undefined,
limitPerRoom: number,
roomIds?: (string | Symbols.AnyRoom)[],
): Promise<IRoomEvent[]> {
limitPerRoom = limitPerRoom > 0 ? Math.min(limitPerRoom, Number.MAX_SAFE_INTEGER) : Number.MAX_SAFE_INTEGER; // relatively arbitrary
const rooms = this.pickRooms(roomIds);
const allResults: IRoomEvent[] = [];
for (const room of rooms) {
const results: MatrixEvent[] = [];
const state = room.currentState.events.get(eventType);
if (state) {
if (stateKey === "" || !!stateKey) {
const forKey = state.get(stateKey);
if (forKey) results.push(forKey);
} else {
results.push(...Array.from(state.values()));
}
}
results.slice(0, limitPerRoom).forEach((e) => allResults.push(e.getEffectiveEvent() as IRoomEvent));
}
return allResults;
}
public async askOpenID(observer: SimpleObservable<IOpenIDUpdate>): Promise<void> {
@@ -690,17 +693,6 @@ export class StopGapWidgetDriver extends WidgetDriver {
return { file: blob };
}
/**
* Gets the IDs of all joined or invited rooms currently known to the
* client.
* @returns The room IDs.
*/
public getKnownRooms(): string[] {
return MatrixClientPeg.safeGet()
.getVisibleRooms(SettingsStore.getValue("feature_dynamic_room_predecessors"))
.map((r) => r.roomId);
}
/**
* Expresses a {@link MatrixError} as a JSON payload
* for use by Widget API error responses.

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

@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { MatrixClient, MatrixEvent, PushRuleKind } from "matrix-js-sdk/src/matrix";
import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { mocked, MockedObject } from "jest-mock";
import { render, waitFor } from "jest-matrix-react";
@@ -228,23 +228,6 @@ describe("<TextualBody />", () => {
const content = container.querySelector(".mx_EventTile_body");
expect(content.innerHTML.replace(defaultEvent.getId(), "%event_id%")).toMatchSnapshot();
});
it("should pillify a keyword responsible for triggering a notification", () => {
const ev = mkRoomTextMessage("foo bar baz");
ev.setPushDetails(undefined, {
actions: [],
pattern: "bar",
rule_id: "bar",
default: false,
enabled: true,
kind: PushRuleKind.ContentSpecific,
});
const { container } = getComponent({ mxEvent: ev });
const content = container.querySelector(".mx_EventTile_body");
expect(content.innerHTML).toMatchInlineSnapshot(
`"<span>foo <bdi><span tabindex="0"><span class="mx_Pill mx_KeywordPill"><span class="mx_Pill_text">bar</span></span></span></bdi> baz</span>"`,
);
});
});
describe("renders formatted m.text correctly", () => {

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

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import { mocked, MockedFunction, MockedObject } from "jest-mock";
import { mocked, MockedObject } from "jest-mock";
import { last } from "lodash";
import {
MatrixEvent,
@@ -15,20 +15,15 @@ import {
EventTimeline,
EventType,
MatrixEventEvent,
RoomStateEvent,
RoomState,
} from "matrix-js-sdk/src/matrix";
import { ClientWidgetApi, WidgetApiFromWidgetAction } from "matrix-widget-api";
import { waitFor } from "jest-matrix-react";
import { Optional } from "matrix-events-sdk";
import { stubClient, mkRoom, mkEvent } from "../../../test-utils";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import { StopGapWidget } from "../../../../src/stores/widgets/StopGapWidget";
import ActiveWidgetStore from "../../../../src/stores/ActiveWidgetStore";
import SettingsStore from "../../../../src/settings/SettingsStore";
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
import { UPDATE_EVENT } from "../../../../src/stores/AsyncStore";
jest.mock("matrix-widget-api/lib/ClientWidgetApi");
@@ -58,7 +53,6 @@ describe("StopGapWidget", () => {
// Start messaging without an iframe, since ClientWidgetApi is mocked
widget.startMessaging(null as unknown as HTMLIFrameElement);
messaging = mocked(last(mocked(ClientWidgetApi).mock.instances)!);
messaging.feedStateUpdate.mockResolvedValue();
});
afterEach(() => {
@@ -90,20 +84,6 @@ describe("StopGapWidget", () => {
expect(messaging.feedToDevice).toHaveBeenCalledWith(event.getEffectiveEvent(), false);
});
it("feeds incoming state updates to the widget", () => {
const event = mkEvent({
event: true,
type: "org.example.foo",
skey: "",
user: "@alice:example.org",
content: { hello: "world" },
room: "!1:example.org",
});
client.emit(RoomStateEvent.Events, event, {} as unknown as RoomState, null);
expect(messaging.feedStateUpdate).toHaveBeenCalledWith(event.getEffectiveEvent());
});
describe("feed event", () => {
let event1: MatrixEvent;
let event2: MatrixEvent;
@@ -138,24 +118,24 @@ describe("StopGapWidget", () => {
it("feeds incoming event to the widget", async () => {
client.emit(ClientEvent.Event, event1);
expect(messaging.feedEvent).toHaveBeenCalledWith(event1.getEffectiveEvent());
expect(messaging.feedEvent).toHaveBeenCalledWith(event1.getEffectiveEvent(), "!1:example.org");
client.emit(ClientEvent.Event, event2);
expect(messaging.feedEvent).toHaveBeenCalledTimes(2);
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2.getEffectiveEvent());
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2.getEffectiveEvent(), "!1:example.org");
});
it("should not feed incoming event to the widget if seen already", async () => {
client.emit(ClientEvent.Event, event1);
expect(messaging.feedEvent).toHaveBeenCalledWith(event1.getEffectiveEvent());
expect(messaging.feedEvent).toHaveBeenCalledWith(event1.getEffectiveEvent(), "!1:example.org");
client.emit(ClientEvent.Event, event2);
expect(messaging.feedEvent).toHaveBeenCalledTimes(2);
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2.getEffectiveEvent());
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2.getEffectiveEvent(), "!1:example.org");
client.emit(ClientEvent.Event, event1);
expect(messaging.feedEvent).toHaveBeenCalledTimes(2);
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2.getEffectiveEvent());
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2.getEffectiveEvent(), "!1:example.org");
});
it("feeds decrypted events asynchronously", async () => {
@@ -185,7 +165,7 @@ describe("StopGapWidget", () => {
decryptingSpy2.mockReturnValue(false);
client.emit(MatrixEventEvent.Decrypted, event2Encrypted);
expect(messaging.feedEvent).toHaveBeenCalledTimes(1);
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2Encrypted.getEffectiveEvent());
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2Encrypted.getEffectiveEvent(), "!1:example.org");
// …then event 1
event1Encrypted.event.type = event1.getType();
event1Encrypted.event.content = event1.getContent();
@@ -195,7 +175,7 @@ describe("StopGapWidget", () => {
// doesn't have to be blocked on the decryption of event 1 (or
// worse, dropped)
expect(messaging.feedEvent).toHaveBeenCalledTimes(2);
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event1Encrypted.getEffectiveEvent());
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event1Encrypted.getEffectiveEvent(), "!1:example.org");
});
it("should not feed incoming event if not in timeline", () => {
@@ -211,7 +191,7 @@ describe("StopGapWidget", () => {
});
client.emit(ClientEvent.Event, event);
expect(messaging.feedEvent).toHaveBeenCalledWith(event.getEffectiveEvent());
expect(messaging.feedEvent).toHaveBeenCalledWith(event.getEffectiveEvent(), "!1:example.org");
});
it("feeds incoming event that is not in timeline but relates to unknown parent to the widget", async () => {
@@ -231,19 +211,18 @@ describe("StopGapWidget", () => {
});
client.emit(ClientEvent.Event, event1);
expect(messaging.feedEvent).toHaveBeenCalledWith(event1.getEffectiveEvent());
expect(messaging.feedEvent).toHaveBeenCalledWith(event1.getEffectiveEvent(), "!1:example.org");
client.emit(ClientEvent.Event, event);
expect(messaging.feedEvent).toHaveBeenCalledTimes(2);
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event.getEffectiveEvent());
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event.getEffectiveEvent(), "!1:example.org");
client.emit(ClientEvent.Event, event1);
expect(messaging.feedEvent).toHaveBeenCalledTimes(2);
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event.getEffectiveEvent());
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event.getEffectiveEvent(), "!1:example.org");
});
});
});
describe("StopGapWidget with stickyPromise", () => {
let client: MockedObject<MatrixClient>;
let widget: StopGapWidget;
@@ -309,49 +288,3 @@ describe("StopGapWidget with stickyPromise", () => {
waitFor(() => expect(setPersistenceSpy).toHaveBeenCalled(), { interval: 5 });
});
});
describe("StopGapWidget as an account widget", () => {
let widget: StopGapWidget;
let messaging: MockedObject<ClientWidgetApi>;
let getRoomId: MockedFunction<() => Optional<string>>;
beforeEach(() => {
stubClient();
// I give up, getting the return type of spyOn right is hopeless
getRoomId = jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId") as unknown as MockedFunction<
() => Optional<string>
>;
getRoomId.mockReturnValue("!1:example.org");
widget = new StopGapWidget({
app: {
id: "test",
creatorUserId: "@alice:example.org",
type: "example",
url: "https://example.org?user-id=$matrix_user_id&device-id=$org.matrix.msc3819.matrix_device_id&base-url=$org.matrix.msc4039.matrix_base_url&theme=$org.matrix.msc2873.client_theme",
roomId: "!1:example.org",
},
userId: "@alice:example.org",
creatorUserId: "@alice:example.org",
waitForIframeLoad: true,
userWidget: false,
});
// Start messaging without an iframe, since ClientWidgetApi is mocked
widget.startMessaging(null as unknown as HTMLIFrameElement);
messaging = mocked(last(mocked(ClientWidgetApi).mock.instances)!);
});
afterEach(() => {
widget.stopMessaging();
getRoomId.mockRestore();
});
it("updates viewed room", () => {
expect(messaging.setViewedRoomId).toHaveBeenCalledTimes(1);
expect(messaging.setViewedRoomId).toHaveBeenLastCalledWith("!1:example.org");
getRoomId.mockReturnValue("!2:example.org");
SdkContextClass.instance.roomViewStore.emit(UPDATE_EVENT);
expect(messaging.setViewedRoomId).toHaveBeenCalledTimes(2);
expect(messaging.setViewedRoomId).toHaveBeenLastCalledWith("!2:example.org");
});
});

View File

@@ -17,7 +17,6 @@ import {
MatrixEvent,
MsgType,
RelationType,
Room,
} from "matrix-js-sdk/src/matrix";
import {
Widget,
@@ -39,7 +38,7 @@ import {
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import { StopGapWidgetDriver } from "../../../../src/stores/widgets/StopGapWidgetDriver";
import { mkEvent, stubClient } from "../../../test-utils";
import { stubClient } from "../../../test-utils";
import { ModuleRunner } from "../../../../src/modules/ModuleRunner";
import dis from "../../../../src/dispatcher/dispatcher";
import Modal from "../../../../src/Modal";
@@ -570,7 +569,7 @@ describe("StopGapWidgetDriver", () => {
it("passes the flag through to getVisibleRooms", () => {
const driver = mkDefaultDriver();
driver.getKnownRooms();
driver.readRoomEvents(EventType.CallAnswer, "", 0, ["*"]);
expect(client.getVisibleRooms).toHaveBeenCalledWith(false);
});
});
@@ -585,7 +584,7 @@ describe("StopGapWidgetDriver", () => {
it("passes the flag through to getVisibleRooms", () => {
const driver = mkDefaultDriver();
driver.getKnownRooms();
driver.readRoomEvents(EventType.CallAnswer, "", 0, ["*"]);
expect(client.getVisibleRooms).toHaveBeenCalledWith(true);
});
});
@@ -693,107 +692,4 @@ describe("StopGapWidgetDriver", () => {
await expect(file.text()).resolves.toEqual("test contents");
});
});
describe("readRoomTimeline", () => {
const event1 = mkEvent({
event: true,
id: "$event-id1",
type: "org.example.foo",
user: "@alice:example.org",
content: { hello: "world" },
room: "!1:example.org",
});
const event2 = mkEvent({
event: true,
id: "$event-id2",
type: "org.example.foo",
user: "@alice:example.org",
content: { hello: "world" },
room: "!1:example.org",
});
let driver: WidgetDriver;
beforeEach(() => {
driver = mkDefaultDriver();
client.getRoom.mockReturnValue({
getLiveTimeline: () => ({ getEvents: () => [event1, event2] }),
} as unknown as Room);
});
it("reads all events", async () => {
expect(
await driver.readRoomTimeline("!1:example.org", "org.example.foo", undefined, undefined, 10, undefined),
).toEqual([event2, event1].map((e) => e.getEffectiveEvent()));
});
it("reads up to a limit", async () => {
expect(
await driver.readRoomTimeline("!1:example.org", "org.example.foo", undefined, undefined, 1, undefined),
).toEqual([event2.getEffectiveEvent()]);
});
it("reads up to a specific event", async () => {
expect(
await driver.readRoomTimeline(
"!1:example.org",
"org.example.foo",
undefined,
undefined,
10,
event1.getId(),
),
).toEqual([event2.getEffectiveEvent()]);
});
});
describe("readRoomState", () => {
const event1 = mkEvent({
event: true,
id: "$event-id1",
type: "org.example.foo",
user: "@alice:example.org",
content: { hello: "world" },
skey: "1",
room: "!1:example.org",
});
const event2 = mkEvent({
event: true,
id: "$event-id2",
type: "org.example.foo",
user: "@alice:example.org",
content: { hello: "world" },
skey: "2",
room: "!1:example.org",
});
let driver: WidgetDriver;
let getStateEvents: jest.Mock;
beforeEach(() => {
driver = mkDefaultDriver();
getStateEvents = jest.fn();
client.getRoom.mockReturnValue({
getLiveTimeline: () => ({ getState: () => ({ getStateEvents }) }),
} as unknown as Room);
});
it("reads a specific state key", async () => {
getStateEvents.mockImplementation((eventType, stateKey) => {
if (eventType === "org.example.foo" && stateKey === "1") return event1;
return undefined;
});
expect(await driver.readRoomState("!1:example.org", "org.example.foo", "1")).toEqual([
event1.getEffectiveEvent(),
]);
});
it("reads all state keys", async () => {
getStateEvents.mockImplementation((eventType, stateKey) => {
if (eventType === "org.example.foo" && stateKey === undefined) return [event1, event2];
return [];
});
expect(await driver.readRoomState("!1:example.org", "org.example.foo", undefined)).toEqual(
[event1, event2].map((e) => e.getEffectiveEvent()),
);
});
});
});

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: "{}",

1969
yarn.lock

File diff suppressed because it is too large Load Diff