mirror of
https://github.com/element-hq/element-web.git
synced 2025-09-17 11:04:05 +02:00
Compare commits
8 Commits
hs/persist
...
hs/customi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcf9854a4c | ||
|
|
b589757c34 | ||
|
|
0d576b217b | ||
|
|
80cc0a928f | ||
|
|
9cccbeb799 | ||
|
|
a8c170f8be | ||
|
|
f5402b4ec4 | ||
|
|
131b28ede8 |
1
.github/workflows/deploy.yml
vendored
1
.github/workflows/deploy.yml
vendored
@@ -96,4 +96,3 @@ jobs:
|
||||
projectName: ${{ env.SITE == 'staging.element.io' && 'element-web-staging' || 'element-web' }}
|
||||
directory: _deploy
|
||||
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: main
|
||||
|
||||
2
.github/workflows/dockerhub.yaml
vendored
2
.github/workflows/dockerhub.yaml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -104,7 +104,7 @@ jobs:
|
||||
|
||||
- name: Skip SonarCloud in merge queue
|
||||
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true'
|
||||
uses: guibranco/github-status-action-v2@ecd54a02cf761e85a8fb328fe937710fd4227cda
|
||||
uses: guibranco/github-status-action-v2@56cd38caf0615dd03f49d42ed301f1469911ac61
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: success
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
43
playwright/e2e/branding/title.spec.ts
Normal file
43
playwright/e2e/branding/title.spec.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 |
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
</>;
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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") {
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
};
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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++;
|
||||
});
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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++;
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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()),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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: "{}",
|
||||
|
||||
Reference in New Issue
Block a user