Merge branch 'develop' into t3chguy/safari-compat
This commit is contained in:
1
.github/workflows/deploy.yml
vendored
1
.github/workflows/deploy.yml
vendored
@@ -96,3 +96,4 @@ jobs:
|
|||||||
projectName: ${{ env.SITE == 'staging.element.io' && 'element-web-staging' || 'element-web' }}
|
projectName: ${{ env.SITE == 'staging.element.io' && 'element-web-staging' || 'element-web' }}
|
||||||
directory: _deploy
|
directory: _deploy
|
||||||
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
|
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
|
- name: Build and push
|
||||||
id: build-and-push
|
id: build-and-push
|
||||||
uses: docker/build-push-action@b32b51a8eda65d6793cd0494a773d4f6bcef32dc # v6
|
uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
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
|
- name: Skip SonarCloud in merge queue
|
||||||
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true'
|
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true'
|
||||||
uses: guibranco/github-status-action-v2@56cd38caf0615dd03f49d42ed301f1469911ac61
|
uses: guibranco/github-status-action-v2@ecd54a02cf761e85a8fb328fe937710fd4227cda
|
||||||
with:
|
with:
|
||||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||||
state: success
|
state: success
|
||||||
|
|||||||
@@ -8,11 +8,13 @@
|
|||||||
|
|
||||||
#### develop
|
#### develop
|
||||||
|
|
||||||
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.
|
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`.
|
||||||
|
|
||||||
#### staging
|
#### 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.
|
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
|
#### master
|
||||||
|
|
||||||
@@ -215,7 +217,7 @@ We ship Element Web to dockerhub, `*.element.io`, and packages.element.io.
|
|||||||
We ship Element Desktop to packages.element.io.
|
We ship Element Desktop to packages.element.io.
|
||||||
|
|
||||||
- [ ] Check that element-web has shipped to dockerhub
|
- [ ] Check that element-web has shipped to dockerhub
|
||||||
- [ ] Deploy staging.element.io. [See docs.](https://handbook.element.io/books/element-web-team/page/deploying-appstagingelementio)
|
- [ ] Check that the staging [deployment](https://github.com/element-hq/element-web/actions/workflows/deploy.yml) has completed successfully
|
||||||
- [ ] Test staging.element.io
|
- [ ] Test staging.element.io
|
||||||
|
|
||||||
For final releases additionally do these steps:
|
For final releases additionally do these steps:
|
||||||
@@ -225,6 +227,9 @@ For final releases additionally do these steps:
|
|||||||
- [ ] Ensure Element Web package has shipped to packages.element.io
|
- [ ] Ensure Element Web package has shipped to packages.element.io
|
||||||
- [ ] Ensure Element Desktop packages have 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
|
# Housekeeping
|
||||||
|
|
||||||
We have some manual housekeeping to do in order to prepare for the next release.
|
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",
|
"@types/react-dom": "18.3.5",
|
||||||
"oidc-client-ts": "3.1.0",
|
"oidc-client-ts": "3.1.0",
|
||||||
"jwt-decode": "4.0.0",
|
"jwt-decode": "4.0.0",
|
||||||
"caniuse-lite": "1.0.30001690",
|
"caniuse-lite": "1.0.30001692",
|
||||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
|
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
|
||||||
"wrap-ansi": "npm:wrap-ansi@^7.0.0"
|
"wrap-ansi": "npm:wrap-ansi@^7.0.0"
|
||||||
},
|
},
|
||||||
@@ -178,7 +178,7 @@
|
|||||||
"@peculiar/webcrypto": "^1.4.3",
|
"@peculiar/webcrypto": "^1.4.3",
|
||||||
"@playwright/test": "^1.40.1",
|
"@playwright/test": "^1.40.1",
|
||||||
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
|
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
|
||||||
"@sentry/webpack-plugin": "^2.7.1",
|
"@sentry/webpack-plugin": "^3.0.0",
|
||||||
"@stylistic/eslint-plugin": "^2.9.0",
|
"@stylistic/eslint-plugin": "^2.9.0",
|
||||||
"@svgr/webpack": "^8.0.0",
|
"@svgr/webpack": "^8.0.0",
|
||||||
"@testcontainers/postgresql": "^10.16.0",
|
"@testcontainers/postgresql": "^10.16.0",
|
||||||
@@ -230,7 +230,7 @@
|
|||||||
"dotenv": "^16.0.2",
|
"dotenv": "^16.0.2",
|
||||||
"eslint": "8.57.1",
|
"eslint": "8.57.1",
|
||||||
"eslint-config-google": "^0.14.0",
|
"eslint-config-google": "^0.14.0",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^10.0.0",
|
||||||
"eslint-plugin-deprecate": "0.8.5",
|
"eslint-plugin-deprecate": "0.8.5",
|
||||||
"eslint-plugin-import": "^2.25.4",
|
"eslint-plugin-import": "^2.25.4",
|
||||||
"eslint-plugin-jest": "^28.0.0",
|
"eslint-plugin-jest": "^28.0.0",
|
||||||
@@ -287,7 +287,7 @@
|
|||||||
"terser-webpack-plugin": "^5.3.9",
|
"terser-webpack-plugin": "^5.3.9",
|
||||||
"testcontainers": "^10.16.0",
|
"testcontainers": "^10.16.0",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "5.7.2",
|
"typescript": "5.7.3",
|
||||||
"util": "^0.12.5",
|
"util": "^0.12.5",
|
||||||
"web-streams-polyfill": "^4.0.0",
|
"web-streams-polyfill": "^4.0.0",
|
||||||
"webpack": "^5.89.0",
|
"webpack": "^5.89.0",
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ test.describe("Memberlist", () => {
|
|||||||
await app.viewRoomByName(ROOM_NAME);
|
await app.viewRoomByName(ROOM_NAME);
|
||||||
const memberlist = await app.toggleMemberlistPanel();
|
const memberlist = await app.toggleMemberlistPanel();
|
||||||
await expect(memberlist.locator(".mx_MemberTileView")).toHaveCount(4);
|
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");
|
await expect(page.locator(".mx_MemberListView")).toMatchScreenshot("with-four-members.png");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -69,11 +69,6 @@ const test = base.extend<{
|
|||||||
});
|
});
|
||||||
|
|
||||||
test.describe("Sliding Sync", () => {
|
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) => {
|
const checkOrder = async (wantOrder: string[], page: Page) => {
|
||||||
await expect(page.getByRole("group", { name: "Rooms" }).locator(".mx_RoomTile_title")).toHaveText(wantOrder);
|
await expect(page.getByRole("group", { name: "Rooms" }).locator(".mx_RoomTile_title")).toHaveText(wantOrder);
|
||||||
};
|
};
|
||||||
|
|||||||
Binary file not shown.
|
Before 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 { StartedMatrixAuthenticationServiceContainer } from "./mas.ts";
|
||||||
import { Api, ClientServerApi, Verb } from "../plugins/utils/api.ts";
|
import { Api, ClientServerApi, Verb } from "../plugins/utils/api.ts";
|
||||||
|
|
||||||
const TAG = "develop@sha256:e48308d68dec00af6ce43a05785d475de21a37bc2afaabb440d3a575bcc3d57d";
|
const TAG = "develop@sha256:3594fba0d21ad44f407225baed4be0542da8abcb6e1a7e2e16d3be35c278a7cb";
|
||||||
|
|
||||||
const DEFAULT_CONFIG = {
|
const DEFAULT_CONFIG = {
|
||||||
server_name: "localhost",
|
server_name: "localhost",
|
||||||
|
|||||||
@@ -283,6 +283,7 @@
|
|||||||
@import "./views/rooms/_EventTile.pcss";
|
@import "./views/rooms/_EventTile.pcss";
|
||||||
@import "./views/rooms/_HistoryTile.pcss";
|
@import "./views/rooms/_HistoryTile.pcss";
|
||||||
@import "./views/rooms/_IRCLayout.pcss";
|
@import "./views/rooms/_IRCLayout.pcss";
|
||||||
|
@import "./views/rooms/_InvitedIconView.pcss";
|
||||||
@import "./views/rooms/_JumpToBottomButton.pcss";
|
@import "./views/rooms/_JumpToBottomButton.pcss";
|
||||||
@import "./views/rooms/_LinkPreviewGroup.pcss";
|
@import "./views/rooms/_LinkPreviewGroup.pcss";
|
||||||
@import "./views/rooms/_LinkPreviewWidget.pcss";
|
@import "./views/rooms/_LinkPreviewWidget.pcss";
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.mx_UserPill_me,
|
&.mx_UserPill_me,
|
||||||
&.mx_AtRoomPill {
|
&.mx_AtRoomPill,
|
||||||
|
&.mx_KeywordPill {
|
||||||
background-color: var(--cpd-color-bg-critical-primary) !important; /* To override .markdown-body */
|
background-color: var(--cpd-color-bg-critical-primary) !important; /* To override .markdown-body */
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +46,8 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* We don't want to indicate clickability */
|
/* We don't want to indicate clickability */
|
||||||
&.mx_AtRoomPill:hover {
|
&.mx_AtRoomPill:hover,
|
||||||
|
&.mx_KeywordPill:hover {
|
||||||
background-color: var(--cpd-color-bg-critical-primary) !important; /* To override .markdown-body */
|
background-color: var(--cpd-color-bg-critical-primary) !important; /* To override .markdown-body */
|
||||||
cursor: unset;
|
cursor: unset;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
.mx_DisambiguatedProfile_mxid {
|
.mx_DisambiguatedProfile_mxid {
|
||||||
margin-inline-start: 0;
|
margin-inline-start: 0;
|
||||||
font: var(--cpd-font-body-sm-regular);
|
font: var(--cpd-font-body-sm-regular);
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
span:not(.mx_DisambiguatedProfile_mxid) {
|
span:not(.mx_DisambiguatedProfile_mxid) {
|
||||||
|
|||||||
@@ -135,12 +135,6 @@ $left-gutter: 64px;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_EventTile_highlight,
|
|
||||||
&.mx_EventTile_highlight .markdown-body,
|
|
||||||
&.mx_EventTile_highlight .mx_EventTile_edited {
|
|
||||||
color: $alert;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mx_EventTile_bubbleContainer {
|
&.mx_EventTile_bubbleContainer {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 100px;
|
grid-template-columns: 1fr 100px;
|
||||||
|
|||||||
10
res/css/views/rooms/_InvitedIconView.pcss
Normal file
10
res/css/views/rooms/_InvitedIconView.pcss
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/*
|
||||||
|
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,4 +14,10 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
.mx_MemberListView_container {
|
.mx_MemberListView_container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_MemberListView_separator {
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
border-top: 2px solid var(--cpd-color-bg-subtle-primary);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,9 +31,11 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MemberTileView_user_label {
|
.mx_MemberTileView_userLabel {
|
||||||
font: var(--cpd-font-body-sm-regular);
|
font: var(--cpd-font-body-sm-regular);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
color: var(--cpd-color-text-secondary);
|
||||||
|
margin-left: var(--cpd-space-4x);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MemberTileView_avatar {
|
.mx_MemberTileView_avatar {
|
||||||
@@ -41,18 +43,4 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
height: 32px;
|
height: 32px;
|
||||||
width: 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,8 +99,12 @@ export function sdkRoomMemberToRoomMember(member: SdkRoomMember): Member {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const SEPARATOR = "SEPARATOR";
|
||||||
|
export type MemberWithSeparator = Member | typeof SEPARATOR;
|
||||||
|
|
||||||
export interface MemberListViewState {
|
export interface MemberListViewState {
|
||||||
members: Member[];
|
members: MemberWithSeparator[];
|
||||||
|
memberCount: number;
|
||||||
search: (searchQuery: string) => void;
|
search: (searchQuery: string) => void;
|
||||||
isPresenceEnabled: boolean;
|
isPresenceEnabled: boolean;
|
||||||
shouldShowInvite: boolean;
|
shouldShowInvite: boolean;
|
||||||
@@ -118,10 +122,16 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sdkContext = useContext(SDKContext);
|
const sdkContext = useContext(SDKContext);
|
||||||
const [memberMap, setMemberMap] = useState<Map<string, Member>>(new Map());
|
const [memberMap, setMemberMap] = useState<Map<string, MemberWithSeparator>>(new Map());
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
// This is the last known total number of members in this room.
|
// This is the last known total number of members in this room.
|
||||||
const [totalMemberCount, setTotalMemberCount] = useState(0);
|
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(
|
const loadMembers = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -131,24 +141,34 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
|
|||||||
roomId,
|
roomId,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
);
|
);
|
||||||
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);
|
|
||||||
}
|
|
||||||
// Then add the third party invites
|
|
||||||
const threePidInvited = getPending3PidInvites(room, searchQuery);
|
const threePidInvited = getPending3PidInvites(room, searchQuery);
|
||||||
for (const invited of threePidInvited) {
|
|
||||||
const key = invited.threePidInvite!.event.getContent().display_name;
|
const newMemberMap = new Map<string, MemberWithSeparator>();
|
||||||
newMemberMap.set(key, invited);
|
|
||||||
}
|
// First add the joined room members
|
||||||
// Finally add the joined room members
|
|
||||||
for (const member of joinedSdk) {
|
for (const member of joinedSdk) {
|
||||||
const roomMember = sdkRoomMemberToRoomMember(member);
|
const roomMember = sdkRoomMemberToRoomMember(member);
|
||||||
newMemberMap.set(member.userId, roomMember);
|
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
|
||||||
|
for (const member of invitedSdk) {
|
||||||
|
const roomMember = sdkRoomMemberToRoomMember(member);
|
||||||
|
newMemberMap.set(member.userId, roomMember);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally add the third party invites
|
||||||
|
for (const invited of threePidInvited) {
|
||||||
|
const key = invited.threePidInvite!.event.getContent().display_name;
|
||||||
|
newMemberMap.set(key, invited);
|
||||||
|
}
|
||||||
|
|
||||||
setMemberMap(newMemberMap);
|
setMemberMap(newMemberMap);
|
||||||
|
setMemberCount(joinedSdk.length + invitedSdk.length + threePidInvited.length);
|
||||||
if (!searchQuery) {
|
if (!searchQuery) {
|
||||||
/**
|
/**
|
||||||
* Since searching for members only gives you the relevant
|
* Since searching for members only gives you the relevant
|
||||||
@@ -241,6 +261,7 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
members: Array.from(memberMap.values()),
|
members: Array.from(memberMap.values()),
|
||||||
|
memberCount,
|
||||||
search: loadMembers,
|
search: loadMembers,
|
||||||
shouldShowInvite,
|
shouldShowInvite,
|
||||||
isPresenceEnabled,
|
isPresenceEnabled,
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ export function useMemberTileViewModel(props: MemberTileViewModelProps): MemberT
|
|||||||
userLabel = _t(PowerLabel[powerStatus]);
|
userLabel = _t(PowerLabel[powerStatus]);
|
||||||
}
|
}
|
||||||
if (props.member.isInvite) {
|
if (props.member.isInvite) {
|
||||||
userLabel = `(${_t("member_list|invited_label")})`;
|
userLabel = _t("member_list|invited_label");
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
import dis from "../../../../dispatcher/dispatcher";
|
import dis from "../../../../dispatcher/dispatcher";
|
||||||
import { Action } from "../../../../dispatcher/actions";
|
import { Action } from "../../../../dispatcher/actions";
|
||||||
import { ThreePIDInvite } from "../../../../models/rooms/ThreePIDInvite";
|
import { ThreePIDInvite } from "../../../../models/rooms/ThreePIDInvite";
|
||||||
|
import { _t } from "../../../../languageHandler";
|
||||||
|
|
||||||
interface ThreePidTileViewModelProps {
|
interface ThreePidTileViewModelProps {
|
||||||
threePidInvite: ThreePIDInvite;
|
threePidInvite: ThreePIDInvite;
|
||||||
@@ -16,6 +17,7 @@ interface ThreePidTileViewModelProps {
|
|||||||
export interface ThreePidTileViewState {
|
export interface ThreePidTileViewState {
|
||||||
name: string;
|
name: string;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
|
userLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useThreePidTileViewModel(props: ThreePidTileViewModelProps): ThreePidTileViewState {
|
export function useThreePidTileViewModel(props: ThreePidTileViewModelProps): ThreePidTileViewState {
|
||||||
@@ -28,8 +30,11 @@ export function useThreePidTileViewModel(props: ThreePidTileViewModelProps): Thr
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const userLabel = _t("member_list|invited_label");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
onClick,
|
onClick,
|
||||||
|
userLabel,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
|
||||||
|
|
||||||
import ToggleSwitch from "./ToggleSwitch";
|
import ToggleSwitch from "./ToggleSwitch";
|
||||||
import { Caption } from "../typography/Caption";
|
import { Caption } from "../typography/Caption";
|
||||||
@@ -36,7 +36,7 @@ interface IProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default class LabelledToggleSwitch extends React.PureComponent<IProps> {
|
export default class LabelledToggleSwitch extends React.PureComponent<IProps> {
|
||||||
private readonly id = `mx_LabelledToggleSwitch_${randomString(12)}`;
|
private readonly id = `mx_LabelledToggleSwitch_${secureRandomString(12)}`;
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
public render(): React.ReactNode {
|
||||||
// This is a minimal version of a SettingsFlag
|
// This is a minimal version of a SettingsFlag
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export enum PillType {
|
|||||||
AtRoomMention = "TYPE_AT_ROOM_MENTION", // '@room' mention
|
AtRoomMention = "TYPE_AT_ROOM_MENTION", // '@room' mention
|
||||||
EventInSameRoom = "TYPE_EVENT_IN_SAME_ROOM",
|
EventInSameRoom = "TYPE_EVENT_IN_SAME_ROOM",
|
||||||
EventInOtherRoom = "TYPE_EVENT_IN_OTHER_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 => {
|
export const pillRoomNotifPos = (text: string | null): number => {
|
||||||
@@ -76,14 +77,32 @@ export interface PillProps {
|
|||||||
room?: Room;
|
room?: Room;
|
||||||
// Whether to include an avatar in the pill
|
// Whether to include an avatar in the pill
|
||||||
shouldShowPillAvatar?: boolean;
|
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 }) => {
|
export const Pill: React.FC<PillProps> = ({
|
||||||
const { event, member, onClick, resourceId, targetRoom, text, type } = usePermalink({
|
type: propType,
|
||||||
|
url,
|
||||||
|
inMessage,
|
||||||
|
room,
|
||||||
|
shouldShowPillAvatar = true,
|
||||||
|
text: customPillText,
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
event,
|
||||||
|
member,
|
||||||
|
onClick,
|
||||||
|
resourceId,
|
||||||
|
targetRoom,
|
||||||
|
text: linkText,
|
||||||
|
type,
|
||||||
|
} = usePermalink({
|
||||||
room,
|
room,
|
||||||
type: propType,
|
type: propType,
|
||||||
url,
|
url,
|
||||||
});
|
});
|
||||||
|
const text = customPillText ?? linkText;
|
||||||
|
|
||||||
if (!type || !text) {
|
if (!type || !text) {
|
||||||
return null;
|
return null;
|
||||||
@@ -96,6 +115,7 @@ export const Pill: React.FC<PillProps> = ({ type: propType, url, inMessage, room
|
|||||||
mx_UserPill: type === PillType.UserMention,
|
mx_UserPill: type === PillType.UserMention,
|
||||||
mx_UserPill_me: resourceId === MatrixClientPeg.safeGet().getUserId(),
|
mx_UserPill_me: resourceId === MatrixClientPeg.safeGet().getUserId(),
|
||||||
mx_EventPill: type === PillType.EventInOtherRoom || type === PillType.EventInSameRoom,
|
mx_EventPill: type === PillType.EventInOtherRoom || type === PillType.EventInSameRoom,
|
||||||
|
mx_KeywordPill: type === PillType.Keyword,
|
||||||
});
|
});
|
||||||
|
|
||||||
let avatar: ReactElement | null = null;
|
let avatar: ReactElement | null = null;
|
||||||
@@ -131,6 +151,8 @@ export const Pill: React.FC<PillProps> = ({ type: propType, url, inMessage, room
|
|||||||
case PillType.UserMention:
|
case PillType.UserMention:
|
||||||
avatar = <PillMemberAvatar shouldShowPillAvatar={shouldShowPillAvatar} member={member} />;
|
avatar = <PillMemberAvatar shouldShowPillAvatar={shouldShowPillAvatar} member={member} />;
|
||||||
break;
|
break;
|
||||||
|
case PillType.Keyword:
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
|
||||||
|
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
@@ -35,7 +35,7 @@ interface IState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default class SettingsFlag extends React.Component<IProps, IState> {
|
export default class SettingsFlag extends React.Component<IProps, IState> {
|
||||||
private readonly id = `mx_SettingsFlag_${randomString(12)}`;
|
private readonly id = `mx_SettingsFlag_${secureRandomString(12)}`;
|
||||||
|
|
||||||
public constructor(props: IProps) {
|
public constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { Ref } from "react";
|
import React, { Ref } from "react";
|
||||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
|
||||||
import classnames from "classnames";
|
import classnames from "classnames";
|
||||||
|
|
||||||
export enum CheckboxStyle {
|
export enum CheckboxStyle {
|
||||||
@@ -33,7 +33,7 @@ export default class StyledCheckbox extends React.PureComponent<IProps, IState>
|
|||||||
public constructor(props: IProps) {
|
public constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
// 56^10 so unlikely chance of collision.
|
// 56^10 so unlikely chance of collision.
|
||||||
this.id = this.props.id || "checkbox_" + randomString(10);
|
this.id = this.props.id || "checkbox_" + secureRandomString(10);
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
public render(): React.ReactNode {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
ContentHelpers,
|
ContentHelpers,
|
||||||
M_BEACON,
|
M_BEACON,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
@@ -81,10 +81,10 @@ const useBeaconState = (
|
|||||||
// eg thread and main timeline, reply
|
// eg thread and main timeline, reply
|
||||||
// maplibregl needs a unique id to attach the map instance to
|
// maplibregl needs a unique id to attach the map instance to
|
||||||
const useUniqueId = (eventId: string): string => {
|
const useUniqueId = (eventId: string): string => {
|
||||||
const [id, setId] = useState(`${eventId}_${randomString(8)}`);
|
const [id, setId] = useState(`${eventId}_${secureRandomString(8)}`);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setId(`${eventId}_${randomString(8)}`);
|
setId(`${eventId}_${secureRandomString(8)}`);
|
||||||
}, [eventId]);
|
}, [eventId]);
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { MatrixEvent, ClientEvent, ClientEventHandlerMap } from "matrix-js-sdk/src/matrix";
|
import { MatrixEvent, ClientEvent, ClientEventHandlerMap } from "matrix-js-sdk/src/matrix";
|
||||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
|
||||||
import { Tooltip } from "@vector-im/compound-web";
|
import { Tooltip } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
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
|
// multiple instances of same map might be in document
|
||||||
// eg thread and main timeline, reply
|
// eg thread and main timeline, reply
|
||||||
const idSuffix = `${props.mxEvent.getId()}_${randomString(8)}`;
|
const idSuffix = `${props.mxEvent.getId()}_${secureRandomString(8)}`;
|
||||||
this.mapId = `mx_MLocationBody_${idSuffix}`;
|
this.mapId = `mx_MLocationBody_${idSuffix}`;
|
||||||
|
|
||||||
this.reconnectedListener = createReconnectedListener(this.clearError);
|
this.reconnectedListener = createReconnectedListener(this.clearError);
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createRef, SyntheticEvent, MouseEvent, StrictMode } from "react";
|
import React, { createRef, SyntheticEvent, MouseEvent, StrictMode } from "react";
|
||||||
import { MsgType } from "matrix-js-sdk/src/matrix";
|
import { MsgType, PushRuleKind } from "matrix-js-sdk/src/matrix";
|
||||||
import { TooltipProvider } from "@vector-im/compound-web";
|
import { TooltipProvider } from "@vector-im/compound-web";
|
||||||
|
import { globToRegexp } from "matrix-js-sdk/src/utils";
|
||||||
|
|
||||||
import * as HtmlUtils from "../../../HtmlUtils";
|
import * as HtmlUtils from "../../../HtmlUtils";
|
||||||
import { formatDate } from "../../../DateUtils";
|
import { formatDate } from "../../../DateUtils";
|
||||||
@@ -35,6 +36,7 @@ import { EditWysiwygComposer } from "../rooms/wysiwyg_composer";
|
|||||||
import { IEventTileOps } from "../rooms/EventTile";
|
import { IEventTileOps } from "../rooms/EventTile";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import CodeBlock from "./CodeBlock";
|
import CodeBlock from "./CodeBlock";
|
||||||
|
import { Pill, PillType } from "../elements/Pill";
|
||||||
import { ReactRootManager } from "../../../utils/react";
|
import { ReactRootManager } from "../../../utils/react";
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
@@ -100,6 +102,16 @@ 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 {
|
private addCodeElement(pre: HTMLPreElement): void {
|
||||||
@@ -210,6 +222,55 @@ 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[] {
|
private findLinks(nodes: ArrayLike<Element>): string[] {
|
||||||
let links: string[] = [];
|
let links: string[] = [];
|
||||||
|
|
||||||
|
|||||||
@@ -88,12 +88,10 @@ function getHeaderLabelJSX(vm: MemberListViewState): React.ReactNode {
|
|||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (vm.memberCount === 0) {
|
||||||
const filteredMemberCount = vm.members.length;
|
|
||||||
if (filteredMemberCount === 0) {
|
|
||||||
return _t("member_list|no_matches");
|
return _t("member_list|no_matches");
|
||||||
}
|
}
|
||||||
return _t("member_list|count", { count: filteredMemberCount });
|
return _t("member_list|count", { count: vm.memberCount });
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MemberListHeaderView: React.FC<Props> = (props: Props) => {
|
export const MemberListHeaderView: React.FC<Props> = (props: Props) => {
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ import { List, ListRowProps } from "react-virtualized/dist/commonjs/List";
|
|||||||
import { AutoSizer } from "react-virtualized";
|
import { AutoSizer } from "react-virtualized";
|
||||||
|
|
||||||
import { Flex } from "../../../utils/Flex";
|
import { Flex } from "../../../utils/Flex";
|
||||||
import { useMemberListViewModel } from "../../../viewmodels/memberlist/MemberListViewModel";
|
import {
|
||||||
|
MemberWithSeparator,
|
||||||
|
SEPARATOR,
|
||||||
|
useMemberListViewModel,
|
||||||
|
} from "../../../viewmodels/memberlist/MemberListViewModel";
|
||||||
import { RoomMemberTileView } from "./tiles/RoomMemberTileView";
|
import { RoomMemberTileView } from "./tiles/RoomMemberTileView";
|
||||||
import { ThreePidInviteTileView } from "./tiles/ThreePidInviteTileView";
|
import { ThreePidInviteTileView } from "./tiles/ThreePidInviteTileView";
|
||||||
import { MemberListHeaderView } from "./MemberListHeaderView";
|
import { MemberListHeaderView } from "./MemberListHeaderView";
|
||||||
@@ -26,10 +30,41 @@ interface IProps {
|
|||||||
const MemberListView: React.FC<IProps> = (props: IProps) => {
|
const MemberListView: React.FC<IProps> = (props: IProps) => {
|
||||||
const vm = useMemberListViewModel(props.roomId);
|
const vm = useMemberListViewModel(props.roomId);
|
||||||
|
|
||||||
const memberCount = vm.members.length;
|
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 rowRenderer = ({ key, index, style }: ListRowProps): React.JSX.Element => {
|
const rowRenderer = ({ key, index, style }: ListRowProps): React.JSX.Element => {
|
||||||
if (index === memberCount) {
|
if (index === totalRows) {
|
||||||
// We've rendered all the members,
|
// We've rendered all the members,
|
||||||
// now we render an empty div to add some space to the end of the list.
|
// now we render an empty div to add some space to the end of the list.
|
||||||
return <div key={key} style={style} />;
|
return <div key={key} style={style} />;
|
||||||
@@ -37,11 +72,7 @@ const MemberListView: React.FC<IProps> = (props: IProps) => {
|
|||||||
const item = vm.members[index];
|
const item = vm.members[index];
|
||||||
return (
|
return (
|
||||||
<div key={key} style={style}>
|
<div key={key} style={style}>
|
||||||
{item.member ? (
|
{getRowComponent(item)}
|
||||||
<RoomMemberTileView member={item.member} showPresence={vm.isPresenceEnabled} />
|
|
||||||
) : (
|
|
||||||
<ThreePidInviteTileView threePidInvite={item.threePidInvite} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -63,11 +94,9 @@ const MemberListView: React.FC<IProps> = (props: IProps) => {
|
|||||||
{({ height, width }) => (
|
{({ height, width }) => (
|
||||||
<List
|
<List
|
||||||
rowRenderer={rowRenderer}
|
rowRenderer={rowRenderer}
|
||||||
// All the member tiles will have a height of 56px.
|
rowHeight={getRowHeight}
|
||||||
// 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.
|
// The +1 refers to the additional empty div that we render at the end of the list.
|
||||||
rowCount={memberCount + 1}
|
rowCount={totalRows + 1}
|
||||||
// Subtract the height of MemberlistHeaderView so that the parent div does not overflow.
|
// Subtract the height of MemberlistHeaderView so that the parent div does not overflow.
|
||||||
height={height - 113}
|
height={height - 113}
|
||||||
width={width}
|
width={width}
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ import { E2EIconView } from "./common/E2EIconView";
|
|||||||
import AvatarPresenceIconView from "./common/PresenceIconView";
|
import AvatarPresenceIconView from "./common/PresenceIconView";
|
||||||
import BaseAvatar from "../../../avatars/BaseAvatar";
|
import BaseAvatar from "../../../avatars/BaseAvatar";
|
||||||
import { _t } from "../../../../../languageHandler";
|
import { _t } from "../../../../../languageHandler";
|
||||||
import { MemberTileLayout } from "./common/MemberTileLayout";
|
import { MemberTileView } from "./common/MemberTileView";
|
||||||
|
import { InvitedIconView } from "./common/InvitedIconView";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
member: RoomMember;
|
member: RoomMember;
|
||||||
@@ -43,25 +44,23 @@ export function RoomMemberTileView(props: IProps): JSX.Element {
|
|||||||
presenceJSX = <AvatarPresenceIconView presenceState={presenceState} />;
|
presenceJSX = <AvatarPresenceIconView presenceState={presenceState} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
let userLabelJSX;
|
let iconJsx;
|
||||||
if (vm.userLabel) {
|
|
||||||
userLabelJSX = <div className="mx_MemberTileView_user_label">{vm.userLabel}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
let e2eIcon;
|
|
||||||
if (vm.e2eStatus) {
|
if (vm.e2eStatus) {
|
||||||
e2eIcon = <E2EIconView status={vm.e2eStatus} />;
|
iconJsx = <E2EIconView status={vm.e2eStatus} />;
|
||||||
|
}
|
||||||
|
if (member.isInvite) {
|
||||||
|
iconJsx = <InvitedIconView isThreePid={false} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MemberTileLayout
|
<MemberTileView
|
||||||
title={vm.title}
|
title={vm.title}
|
||||||
onClick={vm.onClick}
|
onClick={vm.onClick}
|
||||||
avatarJsx={av}
|
avatarJsx={av}
|
||||||
presenceJsx={presenceJSX}
|
presenceJsx={presenceJSX}
|
||||||
nameJsx={nameJSX}
|
nameJsx={nameJSX}
|
||||||
userLabelJsx={userLabelJSX}
|
userLabel={vm.userLabel}
|
||||||
e2eIconJsx={e2eIcon}
|
iconJsx={iconJsx}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import React from "react";
|
|||||||
import { useThreePidTileViewModel } from "../../../../viewmodels/memberlist/tiles/ThreePidTileViewModel";
|
import { useThreePidTileViewModel } from "../../../../viewmodels/memberlist/tiles/ThreePidTileViewModel";
|
||||||
import { ThreePIDInvite } from "../../../../../models/rooms/ThreePIDInvite";
|
import { ThreePIDInvite } from "../../../../../models/rooms/ThreePIDInvite";
|
||||||
import BaseAvatar from "../../../avatars/BaseAvatar";
|
import BaseAvatar from "../../../avatars/BaseAvatar";
|
||||||
import { MemberTileLayout } from "./common/MemberTileLayout";
|
import { MemberTileView } from "./common/MemberTileView";
|
||||||
|
import { InvitedIconView } from "./common/InvitedIconView";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
threePidInvite: ThreePIDInvite;
|
threePidInvite: ThreePIDInvite;
|
||||||
@@ -19,5 +20,15 @@ interface Props {
|
|||||||
export function ThreePidInviteTileView(props: Props): JSX.Element {
|
export function ThreePidInviteTileView(props: Props): JSX.Element {
|
||||||
const vm = useThreePidTileViewModel(props);
|
const vm = useThreePidTileViewModel(props);
|
||||||
const av = <BaseAvatar name={vm.name} size="32px" aria-hidden="true" />;
|
const av = <BaseAvatar name={vm.name} size="32px" aria-hidden="true" />;
|
||||||
return <MemberTileLayout nameJsx={vm.name} avatarJsx={av} onClick={vm.onClick} />;
|
const iconJsx = <InvitedIconView isThreePid={true} />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MemberTileView
|
||||||
|
nameJsx={vm.name}
|
||||||
|
avatarJsx={av}
|
||||||
|
onClick={vm.onClick}
|
||||||
|
userLabel={vm.userLabel}
|
||||||
|
iconJsx={iconJsx}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
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,11 +15,16 @@ interface Props {
|
|||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
title?: string;
|
title?: string;
|
||||||
presenceJsx?: JSX.Element;
|
presenceJsx?: JSX.Element;
|
||||||
userLabelJsx?: JSX.Element;
|
userLabel?: React.ReactNode;
|
||||||
e2eIconJsx?: JSX.Element;
|
iconJsx?: 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 (
|
return (
|
||||||
// The wrapping div is required to make the magic mouse listener work, for some reason.
|
// The wrapping div is required to make the magic mouse listener work, for some reason.
|
||||||
<div>
|
<div>
|
||||||
@@ -31,8 +36,8 @@ export function MemberTileLayout(props: Props): JSX.Element {
|
|||||||
<div className="mx_MemberTileView_name">{props.nameJsx}</div>
|
<div className="mx_MemberTileView_name">{props.nameJsx}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_MemberTileView_right">
|
<div className="mx_MemberTileView_right">
|
||||||
{props.userLabelJsx}
|
{userLabelJsx}
|
||||||
{props.e2eIconJsx}
|
{props.iconJsx}
|
||||||
</div>
|
</div>
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -31,8 +31,7 @@ export function shouldShowQr(
|
|||||||
): boolean {
|
): boolean {
|
||||||
const msc4108Supported = !!versions?.unstable_features?.["org.matrix.msc4108"];
|
const msc4108Supported = !!versions?.unstable_features?.["org.matrix.msc4108"];
|
||||||
|
|
||||||
const deviceAuthorizationGrantSupported =
|
const deviceAuthorizationGrantSupported = oidcClientConfig?.grant_types_supported.includes(DEVICE_CODE_SCOPE);
|
||||||
oidcClientConfig?.metadata?.grant_types_supported.includes(DEVICE_CODE_SCOPE);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
!!deviceAuthorizationGrantSupported &&
|
!!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 React, { lazy, Suspense, useCallback, useContext, useEffect, useRef, useState } from "react";
|
||||||
import { discoverAndValidateOIDCIssuerWellKnown, MatrixClient } from "matrix-js-sdk/src/matrix";
|
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { defer } from "matrix-js-sdk/src/utils";
|
import { defer } from "matrix-js-sdk/src/utils";
|
||||||
|
|
||||||
@@ -163,10 +163,7 @@ const SessionManagerTab: React.FC<{
|
|||||||
const clientVersions = useAsyncMemo(() => matrixClient.getVersions(), [matrixClient]);
|
const clientVersions = useAsyncMemo(() => matrixClient.getVersions(), [matrixClient]);
|
||||||
const oidcClientConfig = useAsyncMemo(async () => {
|
const oidcClientConfig = useAsyncMemo(async () => {
|
||||||
try {
|
try {
|
||||||
const authIssuer = await matrixClient?.getAuthIssuer();
|
return await matrixClient?.getAuthMetadata();
|
||||||
if (authIssuer) {
|
|
||||||
return discoverAndValidateOIDCIssuerWellKnown(authIssuer.issuer);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error("Failed to discover OIDC metadata", e);
|
logger.error("Failed to discover OIDC metadata", e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import { KnownMembership, Membership } from "matrix-js-sdk/src/types";
|
import { KnownMembership, Membership } from "matrix-js-sdk/src/types";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
|
||||||
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||||
import { NamespacedValue } from "matrix-js-sdk/src/NamespacedValue";
|
import { NamespacedValue } from "matrix-js-sdk/src/NamespacedValue";
|
||||||
import { IWidgetApiRequest } from "matrix-widget-api";
|
import { IWidgetApiRequest } from "matrix-widget-api";
|
||||||
@@ -743,7 +743,7 @@ export class ElementCall extends Call {
|
|||||||
const url = ElementCall.generateWidgetUrl(client, roomId);
|
const url = ElementCall.generateWidgetUrl(client, roomId);
|
||||||
return WidgetStore.instance.addVirtualWidget(
|
return WidgetStore.instance.addVirtualWidget(
|
||||||
{
|
{
|
||||||
id: randomString(24), // So that it's globally unique
|
id: secureRandomString(24), // So that it's globally unique
|
||||||
creatorUserId: client.getUserId()!,
|
creatorUserId: client.getUserId()!,
|
||||||
name: "Element Call",
|
name: "Element Call",
|
||||||
type: WidgetType.CALL.preferred,
|
type: WidgetType.CALL.preferred,
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
// the frequency with which we flush to indexeddb
|
// the frequency with which we flush to indexeddb
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
|
||||||
|
|
||||||
import { getCircularReplacer } from "../utils/JSON";
|
import { getCircularReplacer } from "../utils/JSON";
|
||||||
|
|
||||||
@@ -135,7 +135,7 @@ export class IndexedDBLogStore {
|
|||||||
private indexedDB: IDBFactory,
|
private indexedDB: IDBFactory,
|
||||||
private logger: ConsoleLogger,
|
private logger: ConsoleLogger,
|
||||||
) {
|
) {
|
||||||
this.id = "instance-" + randomString(16);
|
this.id = "instance-" + secureRandomString(16);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -50,11 +50,8 @@ export class OidcClientStore {
|
|||||||
} else {
|
} else {
|
||||||
// We are not in OIDC Native mode, as we have no locally stored issuer. Check if the server delegates auth to OIDC.
|
// We are not in OIDC Native mode, as we have no locally stored issuer. Check if the server delegates auth to OIDC.
|
||||||
try {
|
try {
|
||||||
const authIssuer = await this.matrixClient.getAuthIssuer();
|
const authMetadata = await this.matrixClient.getAuthMetadata();
|
||||||
const { accountManagementEndpoint, metadata } = await discoverAndValidateOIDCIssuerWellKnown(
|
this.setAccountManagementEndpoint(authMetadata.account_management_uri, authMetadata.issuer);
|
||||||
authIssuer.issuer,
|
|
||||||
);
|
|
||||||
this.setAccountManagementEndpoint(accountManagementEndpoint, metadata.issuer);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Auth issuer not found", e);
|
console.log("Auth issuer not found", e);
|
||||||
}
|
}
|
||||||
@@ -153,14 +150,11 @@ export class OidcClientStore {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const clientId = getStoredOidcClientId();
|
const clientId = getStoredOidcClientId();
|
||||||
const { accountManagementEndpoint, metadata, signingKeys } = await discoverAndValidateOIDCIssuerWellKnown(
|
const authMetadata = await discoverAndValidateOIDCIssuerWellKnown(this.authenticatedIssuer);
|
||||||
this.authenticatedIssuer,
|
this.setAccountManagementEndpoint(authMetadata.account_management_uri, authMetadata.issuer);
|
||||||
);
|
|
||||||
this.setAccountManagementEndpoint(accountManagementEndpoint, metadata.issuer);
|
|
||||||
this.oidcClient = new OidcClient({
|
this.oidcClient = new OidcClient({
|
||||||
...metadata,
|
authority: authMetadata.issuer,
|
||||||
authority: metadata.issuer,
|
signingKeys: authMetadata.signingKeys ?? undefined,
|
||||||
signingKeys,
|
|
||||||
redirect_uri: PlatformPeg.get()!.getOidcCallbackUrl().href,
|
redirect_uri: PlatformPeg.get()!.getOidcCallbackUrl().href,
|
||||||
client_id: clientId,
|
client_id: clientId,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,14 @@
|
|||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Room, MatrixEvent, MatrixEventEvent, MatrixClient, ClientEvent } from "matrix-js-sdk/src/matrix";
|
import {
|
||||||
|
Room,
|
||||||
|
MatrixEvent,
|
||||||
|
MatrixEventEvent,
|
||||||
|
MatrixClient,
|
||||||
|
ClientEvent,
|
||||||
|
RoomStateEvent,
|
||||||
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||||
import {
|
import {
|
||||||
ClientWidgetApi,
|
ClientWidgetApi,
|
||||||
@@ -26,7 +33,6 @@ import {
|
|||||||
WidgetApiFromWidgetAction,
|
WidgetApiFromWidgetAction,
|
||||||
WidgetKind,
|
WidgetKind,
|
||||||
} from "matrix-widget-api";
|
} from "matrix-widget-api";
|
||||||
import { Optional } from "matrix-events-sdk";
|
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
@@ -56,6 +62,7 @@ import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
|
|||||||
import Modal from "../../Modal";
|
import Modal from "../../Modal";
|
||||||
import ErrorDialog from "../../components/views/dialogs/ErrorDialog";
|
import ErrorDialog from "../../components/views/dialogs/ErrorDialog";
|
||||||
import { SdkContextClass } from "../../contexts/SDKContext";
|
import { SdkContextClass } from "../../contexts/SDKContext";
|
||||||
|
import { UPDATE_EVENT } from "../AsyncStore";
|
||||||
|
|
||||||
// TODO: Destroy all of this code
|
// TODO: Destroy all of this code
|
||||||
|
|
||||||
@@ -151,6 +158,9 @@ export class StopGapWidget extends EventEmitter {
|
|||||||
private mockWidget: ElementWidget;
|
private mockWidget: ElementWidget;
|
||||||
private scalarToken?: string;
|
private scalarToken?: string;
|
||||||
private roomId?: 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 kind: WidgetKind;
|
||||||
private readonly virtual: boolean;
|
private readonly virtual: boolean;
|
||||||
private readUpToMap: { [roomId: string]: string } = {}; // room ID to event ID
|
private readUpToMap: { [roomId: string]: string } = {}; // room ID to event ID
|
||||||
@@ -177,17 +187,6 @@ export class StopGapWidget extends EventEmitter {
|
|||||||
this.stickyPromise = appTileProps.stickyPromise;
|
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 {
|
public get widgetApi(): ClientWidgetApi | null {
|
||||||
return this.messaging;
|
return this.messaging;
|
||||||
}
|
}
|
||||||
@@ -259,6 +258,17 @@ 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.
|
* This starts the messaging for the widget if it is not in the state `started` yet.
|
||||||
* @param iframe the iframe the widget should use
|
* @param iframe the iframe the widget should use
|
||||||
@@ -285,6 +295,17 @@ export class StopGapWidget extends EventEmitter {
|
|||||||
this.messaging.on("capabilitiesNotified", () => this.emit("capabilitiesNotified"));
|
this.messaging.on("capabilitiesNotified", () => this.emit("capabilitiesNotified"));
|
||||||
this.messaging.on(`action:${WidgetApiFromWidgetAction.OpenModalWidget}`, this.onOpenModal);
|
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
|
// Always attach a handler for ViewRoom, but permission check it internally
|
||||||
this.messaging.on(`action:${ElementWidgetActions.ViewRoom}`, (ev: CustomEvent<IViewRoomApiRequest>) => {
|
this.messaging.on(`action:${ElementWidgetActions.ViewRoom}`, (ev: CustomEvent<IViewRoomApiRequest>) => {
|
||||||
ev.preventDefault(); // stop the widget API from auto-rejecting this
|
ev.preventDefault(); // stop the widget API from auto-rejecting this
|
||||||
@@ -329,6 +350,7 @@ export class StopGapWidget extends EventEmitter {
|
|||||||
// Attach listeners for feeding events - the underlying widget classes handle permissions for us
|
// Attach listeners for feeding events - the underlying widget classes handle permissions for us
|
||||||
this.client.on(ClientEvent.Event, this.onEvent);
|
this.client.on(ClientEvent.Event, this.onEvent);
|
||||||
this.client.on(MatrixEventEvent.Decrypted, this.onEventDecrypted);
|
this.client.on(MatrixEventEvent.Decrypted, this.onEventDecrypted);
|
||||||
|
this.client.on(RoomStateEvent.Events, this.onStateUpdate);
|
||||||
this.client.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
|
this.client.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
|
||||||
|
|
||||||
this.messaging.on(
|
this.messaging.on(
|
||||||
@@ -457,8 +479,11 @@ export class StopGapWidget extends EventEmitter {
|
|||||||
WidgetMessagingStore.instance.stopMessaging(this.mockWidget, this.roomId);
|
WidgetMessagingStore.instance.stopMessaging(this.mockWidget, this.roomId);
|
||||||
this.messaging = null;
|
this.messaging = null;
|
||||||
|
|
||||||
|
SdkContextClass.instance.roomViewStore.off(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
||||||
|
|
||||||
this.client.off(ClientEvent.Event, this.onEvent);
|
this.client.off(ClientEvent.Event, this.onEvent);
|
||||||
this.client.off(MatrixEventEvent.Decrypted, this.onEventDecrypted);
|
this.client.off(MatrixEventEvent.Decrypted, this.onEventDecrypted);
|
||||||
|
this.client.off(RoomStateEvent.Events, this.onStateUpdate);
|
||||||
this.client.off(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
|
this.client.off(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,6 +496,14 @@ export class StopGapWidget extends EventEmitter {
|
|||||||
this.feedEvent(ev);
|
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> => {
|
private onToDeviceEvent = async (ev: MatrixEvent): Promise<void> => {
|
||||||
await this.client.decryptEventIfNeeded(ev);
|
await this.client.decryptEventIfNeeded(ev);
|
||||||
if (ev.isDecryptionFailure()) return;
|
if (ev.isDecryptionFailure()) return;
|
||||||
@@ -570,7 +603,7 @@ export class StopGapWidget extends EventEmitter {
|
|||||||
this.eventsToFeed.add(ev);
|
this.eventsToFeed.add(ev);
|
||||||
} else {
|
} else {
|
||||||
const raw = ev.getEffectiveEvent();
|
const raw = ev.getEffectiveEvent();
|
||||||
this.messaging.feedEvent(raw as IRoomEvent, this.eventListenerRoomId!).catch((e) => {
|
this.messaging.feedEvent(raw as IRoomEvent).catch((e) => {
|
||||||
logger.error("Error sending event to widget: ", e);
|
logger.error("Error sending event to widget: ", e);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
MatrixCapabilities,
|
MatrixCapabilities,
|
||||||
OpenIDRequestState,
|
OpenIDRequestState,
|
||||||
SimpleObservable,
|
SimpleObservable,
|
||||||
Symbols,
|
|
||||||
Widget,
|
Widget,
|
||||||
WidgetDriver,
|
WidgetDriver,
|
||||||
WidgetEventCapability,
|
WidgetEventCapability,
|
||||||
@@ -36,7 +35,6 @@ import {
|
|||||||
IContent,
|
IContent,
|
||||||
MatrixError,
|
MatrixError,
|
||||||
MatrixEvent,
|
MatrixEvent,
|
||||||
Room,
|
|
||||||
Direction,
|
Direction,
|
||||||
THREAD_RELATION_TYPE,
|
THREAD_RELATION_TYPE,
|
||||||
SendDelayedEventResponse,
|
SendDelayedEventResponse,
|
||||||
@@ -469,70 +467,69 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private pickRooms(roomIds?: (string | Symbols.AnyRoom)[]): Room[] {
|
/**
|
||||||
const client = MatrixClientPeg.get();
|
* Reads all events of the given type, and optionally `msgtype` (if applicable/defined),
|
||||||
if (!client) throw new Error("Not attached to a client");
|
* 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,
|
||||||
const targetRooms = roomIds
|
* but not more.
|
||||||
? roomIds.includes(Symbols.AnyRoom)
|
* @param roomId The ID of the room to look within.
|
||||||
? client.getVisibleRooms(SettingsStore.getValue("feature_dynamic_room_predecessors"))
|
* @param eventType The event type to be read.
|
||||||
: roomIds.map((r) => client.getRoom(r))
|
* @param msgtype The msgtype of the events to be read, if applicable/defined.
|
||||||
: [client.getRoom(SdkContextClass.instance.roomViewStore.getRoomId()!)];
|
* @param stateKey The state key of the events to be read, if applicable/defined.
|
||||||
return targetRooms.filter((r) => !!r) as Room[];
|
* @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.
|
||||||
public async readRoomEvents(
|
* 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,
|
eventType: string,
|
||||||
msgtype: string | undefined,
|
msgtype: string | undefined,
|
||||||
limitPerRoom: number,
|
stateKey: string | undefined,
|
||||||
roomIds?: (string | Symbols.AnyRoom)[],
|
limit: number,
|
||||||
|
since: string | undefined,
|
||||||
): Promise<IRoomEvent[]> {
|
): Promise<IRoomEvent[]> {
|
||||||
limitPerRoom = limitPerRoom > 0 ? Math.min(limitPerRoom, Number.MAX_SAFE_INTEGER) : Number.MAX_SAFE_INTEGER; // relatively arbitrary
|
limit = limit > 0 ? Math.min(limit, Number.MAX_SAFE_INTEGER) : Number.MAX_SAFE_INTEGER; // relatively arbitrary
|
||||||
|
|
||||||
const rooms = this.pickRooms(roomIds);
|
const room = MatrixClientPeg.safeGet().getRoom(roomId);
|
||||||
const allResults: IRoomEvent[] = [];
|
if (room === null) return [];
|
||||||
for (const room of rooms) {
|
|
||||||
const results: MatrixEvent[] = [];
|
const results: MatrixEvent[] = [];
|
||||||
const events = room.getLiveTimeline().getEvents(); // timelines are most recent last
|
const events = room.getLiveTimeline().getEvents(); // timelines are most recent last
|
||||||
for (let i = events.length - 1; i > 0; i--) {
|
for (let i = events.length - 1; i >= 0; i--) {
|
||||||
if (results.length >= limitPerRoom) break;
|
|
||||||
|
|
||||||
const ev = events[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 (ev.getType() !== eventType || ev.isState()) continue;
|
||||||
if (eventType === EventType.RoomMessage && msgtype && msgtype !== ev.getContent()["msgtype"]) continue;
|
if (eventType === EventType.RoomMessage && msgtype && msgtype !== ev.getContent()["msgtype"]) continue;
|
||||||
|
if (ev.getStateKey() !== undefined && stateKey !== undefined && ev.getStateKey() !== stateKey) continue;
|
||||||
results.push(ev);
|
results.push(ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
results.forEach((e) => allResults.push(e.getEffectiveEvent() as IRoomEvent));
|
return results.map((e) => e.getEffectiveEvent() as IRoomEvent);
|
||||||
}
|
|
||||||
return allResults;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async readStateEvents(
|
/**
|
||||||
eventType: string,
|
* Reads the current values of all matching room state entries.
|
||||||
stateKey: string | undefined,
|
* @param roomId The ID of the room.
|
||||||
limitPerRoom: number,
|
* @param eventType The event type of the entries to be read.
|
||||||
roomIds?: (string | Symbols.AnyRoom)[],
|
* @param stateKey The state key of the entry to be read. If undefined,
|
||||||
): Promise<IRoomEvent[]> {
|
* all room state entries with a matching event type should be returned.
|
||||||
limitPerRoom = limitPerRoom > 0 ? Math.min(limitPerRoom, Number.MAX_SAFE_INTEGER) : Number.MAX_SAFE_INTEGER; // relatively arbitrary
|
* @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 [];
|
||||||
|
|
||||||
const rooms = this.pickRooms(roomIds);
|
if (stateKey === undefined)
|
||||||
const allResults: IRoomEvent[] = [];
|
return state.getStateEvents(eventType).map((e) => e.getEffectiveEvent() as IRoomEvent);
|
||||||
for (const room of rooms) {
|
const event = state.getStateEvents(eventType, stateKey);
|
||||||
const results: MatrixEvent[] = [];
|
return event === null ? [] : [event.getEffectiveEvent() as IRoomEvent];
|
||||||
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> {
|
public async askOpenID(observer: SimpleObservable<IOpenIDUpdate>): Promise<void> {
|
||||||
@@ -693,6 +690,17 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
|||||||
return { file: blob };
|
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
|
* Expresses a {@link MatrixError} as a JSON payload
|
||||||
* for use by Widget API error responses.
|
* for use by Widget API error responses.
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
AutoDiscovery,
|
AutoDiscovery,
|
||||||
AutoDiscoveryError,
|
AutoDiscoveryError,
|
||||||
ClientConfig,
|
ClientConfig,
|
||||||
discoverAndValidateOIDCIssuerWellKnown,
|
|
||||||
IClientWellKnown,
|
IClientWellKnown,
|
||||||
MatrixClient,
|
MatrixClient,
|
||||||
MatrixError,
|
MatrixError,
|
||||||
@@ -293,8 +292,7 @@ export default class AutoDiscoveryUtils {
|
|||||||
let delegatedAuthenticationError: Error | undefined;
|
let delegatedAuthenticationError: Error | undefined;
|
||||||
try {
|
try {
|
||||||
const tempClient = new MatrixClient({ baseUrl: preferredHomeserverUrl });
|
const tempClient = new MatrixClient({ baseUrl: preferredHomeserverUrl });
|
||||||
const { issuer } = await tempClient.getAuthIssuer();
|
delegatedAuthentication = await tempClient.getAuthMetadata();
|
||||||
delegatedAuthentication = await discoverAndValidateOIDCIssuerWellKnown(issuer);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof MatrixError && e.httpStatus === 404 && e.errcode === "M_UNRECOGNIZED") {
|
if (e instanceof MatrixError && e.httpStatus === 404 && e.errcode === "M_UNRECOGNIZED") {
|
||||||
// 404 M_UNRECOGNIZED means the server does not support OIDC
|
// 404 M_UNRECOGNIZED means the server does not support OIDC
|
||||||
|
|||||||
@@ -9,12 +9,13 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { base32 } from "rfc4648";
|
import { base32 } from "rfc4648";
|
||||||
|
import { capitalize } from "lodash";
|
||||||
import { IWidget, IWidgetData } from "matrix-widget-api";
|
import { IWidget, IWidgetData } from "matrix-widget-api";
|
||||||
import { Room, ClientEvent, MatrixClient, RoomStateEvent, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
import { Room, ClientEvent, MatrixClient, RoomStateEvent, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||||
import { randomString, randomLowercaseString, randomUppercaseString } from "matrix-js-sdk/src/randomstring";
|
import { LOWERCASE, secureRandomString, secureRandomStringFrom } from "matrix-js-sdk/src/randomstring";
|
||||||
|
|
||||||
import PlatformPeg from "../PlatformPeg";
|
import PlatformPeg from "../PlatformPeg";
|
||||||
import SdkConfig from "../SdkConfig";
|
import SdkConfig from "../SdkConfig";
|
||||||
@@ -427,7 +428,10 @@ export default class WidgetUtils {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const domain = Jitsi.getInstance().preferredDomain;
|
const domain = Jitsi.getInstance().preferredDomain;
|
||||||
const auth = (await Jitsi.getInstance().getJitsiAuth()) ?? undefined;
|
const auth = (await Jitsi.getInstance().getJitsiAuth()) ?? undefined;
|
||||||
const widgetId = randomString(24); // Must be globally unique
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
let confId: string;
|
let confId: string;
|
||||||
if (auth === "openidtoken-jwt") {
|
if (auth === "openidtoken-jwt") {
|
||||||
@@ -437,8 +441,8 @@ export default class WidgetUtils {
|
|||||||
// https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification
|
// https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification
|
||||||
confId = base32.stringify(new TextEncoder().encode(roomId), { pad: false });
|
confId = base32.stringify(new TextEncoder().encode(roomId), { pad: false });
|
||||||
} else {
|
} else {
|
||||||
// Create a random conference ID
|
// Create a random conference ID (capitalised so the name looks sensible in Jitsi)
|
||||||
confId = `Jitsi${randomUppercaseString(1)}${randomLowercaseString(23)}`;
|
confId = `Jitsi${capitalize(secureRandomStringFrom(24, LOWERCASE))}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Remove URL hacks when the mobile clients eventually support v2 widgets
|
// 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 { completeAuthorizationCodeGrant, generateOidcAuthorizationUrl } from "matrix-js-sdk/src/oidc/authorize";
|
||||||
import { QueryDict } from "matrix-js-sdk/src/utils";
|
import { QueryDict } from "matrix-js-sdk/src/utils";
|
||||||
import { OidcClientConfig } from "matrix-js-sdk/src/matrix";
|
import { OidcClientConfig } from "matrix-js-sdk/src/matrix";
|
||||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
|
||||||
import { IdTokenClaims } from "oidc-client-ts";
|
import { IdTokenClaims } from "oidc-client-ts";
|
||||||
|
|
||||||
import { OidcClientError } from "./error";
|
import { OidcClientError } from "./error";
|
||||||
@@ -34,12 +34,12 @@ export const startOidcLogin = async (
|
|||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const redirectUri = PlatformPeg.get()!.getOidcCallbackUrl().href;
|
const redirectUri = PlatformPeg.get()!.getOidcCallbackUrl().href;
|
||||||
|
|
||||||
const nonce = randomString(10);
|
const nonce = secureRandomString(10);
|
||||||
|
|
||||||
const prompt = isRegistration ? "create" : undefined;
|
const prompt = isRegistration ? "create" : undefined;
|
||||||
|
|
||||||
const authorizationUrl = await generateOidcAuthorizationUrl({
|
const authorizationUrl = await generateOidcAuthorizationUrl({
|
||||||
metadata: delegatedAuthConfig.metadata,
|
metadata: delegatedAuthConfig,
|
||||||
redirectUri,
|
redirectUri,
|
||||||
clientId,
|
clientId,
|
||||||
homeserverUrl,
|
homeserverUrl,
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ import { OidcClientConfig } from "matrix-js-sdk/src/matrix";
|
|||||||
* @returns whether user registration is supported
|
* @returns whether user registration is supported
|
||||||
*/
|
*/
|
||||||
export const isUserRegistrationSupported = (delegatedAuthConfig: OidcClientConfig): boolean => {
|
export const isUserRegistrationSupported = (delegatedAuthConfig: OidcClientConfig): boolean => {
|
||||||
// The OidcMetadata type from oidc-client-ts does not include `prompt_values_supported`
|
const supportedPrompts = delegatedAuthConfig.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");
|
return Array.isArray(supportedPrompts) && supportedPrompts?.includes("create");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -40,9 +40,9 @@ export const getOidcClientId = async (
|
|||||||
delegatedAuthConfig: OidcClientConfig,
|
delegatedAuthConfig: OidcClientConfig,
|
||||||
staticOidcClients?: IConfigOptions["oidc_static_clients"],
|
staticOidcClients?: IConfigOptions["oidc_static_clients"],
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
const staticClientId = getStaticOidcClientId(delegatedAuthConfig.metadata.issuer, staticOidcClients);
|
const staticClientId = getStaticOidcClientId(delegatedAuthConfig.issuer, staticOidcClients);
|
||||||
if (staticClientId) {
|
if (staticClientId) {
|
||||||
logger.debug(`Using static clientId for issuer ${delegatedAuthConfig.metadata.issuer}`);
|
logger.debug(`Using static clientId for issuer ${delegatedAuthConfig.issuer}`);
|
||||||
return staticClientId;
|
return staticClientId;
|
||||||
}
|
}
|
||||||
return await registerOidcClient(delegatedAuthConfig, await PlatformPeg.get()!.getOidcClientMetadata());
|
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 { MatrixClient, Room, MatrixEvent, OidcRegistrationClientMetadata } from "matrix-js-sdk/src/matrix";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
import BasePlatform, { UpdateCheckStatus, UpdateStatus } from "../../BasePlatform";
|
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 ipc = new IPCManager("ipcCall", "ipcReply");
|
||||||
private readonly eventIndexManager: BaseEventIndexManager = new SeshatIndexManager();
|
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
|
// 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 = randomString(32);
|
private readonly ssoID: string = secureRandomString(32);
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
super();
|
super();
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
import "blob-polyfill";
|
import "blob-polyfill";
|
||||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
|
||||||
import { mocked } from "jest-mock";
|
import { mocked } from "jest-mock";
|
||||||
|
|
||||||
import { PredictableRandom } from "./test-utils/predictableRandom"; // https://github.com/jsdom/jsdom/issues/2555
|
import { PredictableRandom } from "./test-utils/predictableRandom"; // https://github.com/jsdom/jsdom/issues/2555
|
||||||
@@ -25,7 +25,8 @@ jest.mock("matrix-js-sdk/src/randomstring");
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
const mockRandom = new PredictableRandom();
|
const mockRandom = new PredictableRandom();
|
||||||
mocked(randomString).mockImplementation((len) => {
|
// needless to say, the mock is not cryptographically secure
|
||||||
|
mocked(secureRandomString).mockImplementation((len) => {
|
||||||
let ret = "";
|
let ret = "";
|
||||||
for (let i = 0; i < len; ++i) {
|
for (let i = 0; i < len; ++i) {
|
||||||
const v = mockRandom.get() * chars.length;
|
const v = mockRandom.get() * chars.length;
|
||||||
|
|||||||
@@ -6,41 +6,4 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { OidcClientConfig } from "matrix-js-sdk/src/matrix";
|
export { makeDelegatedAuthConfig, mockOpenIdConfiguration } from "matrix-js-sdk/src/testing";
|
||||||
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_POLL_RESPONSE,
|
||||||
M_TEXT,
|
M_TEXT,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
|
||||||
|
|
||||||
import { flushPromises } from "./utilities";
|
import { flushPromises } from "./utilities";
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ export const makePollEndEvent = (
|
|||||||
id?: string,
|
id?: string,
|
||||||
): MatrixEvent => {
|
): MatrixEvent => {
|
||||||
return new MatrixEvent({
|
return new MatrixEvent({
|
||||||
event_id: id || randomString(16),
|
event_id: id || secureRandomString(16),
|
||||||
room_id: roomId,
|
room_id: roomId,
|
||||||
origin_server_ts: ts,
|
origin_server_ts: ts,
|
||||||
type: M_POLL_END.name,
|
type: M_POLL_END.name,
|
||||||
@@ -91,7 +91,7 @@ export const makePollResponseEvent = (
|
|||||||
ts = 0,
|
ts = 0,
|
||||||
): MatrixEvent =>
|
): MatrixEvent =>
|
||||||
new MatrixEvent({
|
new MatrixEvent({
|
||||||
event_id: randomString(16),
|
event_id: secureRandomString(16),
|
||||||
room_id: roomId,
|
room_id: roomId,
|
||||||
origin_server_ts: ts,
|
origin_server_ts: ts,
|
||||||
type: M_POLL_RESPONSE.name,
|
type: M_POLL_RESPONSE.name,
|
||||||
|
|||||||
@@ -749,11 +749,8 @@ describe("Lifecycle", () => {
|
|||||||
"eyJhbGciOiJSUzI1NiIsImtpZCI6Imh4ZEhXb0Y5bW4ifQ.eyJzdWIiOiIwMUhQUDJGU0JZREU5UDlFTU04REQ3V1pIUiIsImlzcyI6Imh0dHBzOi8vYXV0aC1vaWRjLmxhYi5lbGVtZW50LmRldi8iLCJpYXQiOjE3MTUwNzE5ODUsImF1dGhfdGltZSI6MTcwNzk5MDMxMiwiY19oYXNoIjoidGt5R1RhUjU5aTk3YXoyTU4yMGdidyIsImV4cCI6MTcxNTA3NTU4NSwibm9uY2UiOiJxaXhwM0hFMmVaIiwiYXVkIjoiMDFIWDk0Mlg3QTg3REgxRUs2UDRaNjI4WEciLCJhdF9oYXNoIjoiNFlFUjdPRlVKTmRTeEVHV2hJUDlnZyJ9.HxODneXvSTfWB5Vc4cf7b8GiN2gdwUuTiyVqZuupWske2HkZiJZUt5Lsxg9BW3gz28POkE0Ln17snlkmy02B_AD3DQxKOOxQCzIIARHdfFvZxgGWsMdFcVQZDW7rtXcqgj-SpVaUQ_8acsgxSrz_DF2o0O4tto0PT6wVUiw8KlBmgWTscWPeAWe-39T-8EiQ8Wi16h6oSPcz2NzOQ7eOM_S9fDkOorgcBkRGLl1nrahrPSdWJSGAeruk5mX4YxN714YThFDyEA2t9YmKpjaiSQ2tT-Xkd7tgsZqeirNs2ni9mIiFX3bRX6t2AhUNzA7MaX9ZyizKGa6go3BESO_oDg";
|
"eyJhbGciOiJSUzI1NiIsImtpZCI6Imh4ZEhXb0Y5bW4ifQ.eyJzdWIiOiIwMUhQUDJGU0JZREU5UDlFTU04REQ3V1pIUiIsImlzcyI6Imh0dHBzOi8vYXV0aC1vaWRjLmxhYi5lbGVtZW50LmRldi8iLCJpYXQiOjE3MTUwNzE5ODUsImF1dGhfdGltZSI6MTcwNzk5MDMxMiwiY19oYXNoIjoidGt5R1RhUjU5aTk3YXoyTU4yMGdidyIsImV4cCI6MTcxNTA3NTU4NSwibm9uY2UiOiJxaXhwM0hFMmVaIiwiYXVkIjoiMDFIWDk0Mlg3QTg3REgxRUs2UDRaNjI4WEciLCJhdF9oYXNoIjoiNFlFUjdPRlVKTmRTeEVHV2hJUDlnZyJ9.HxODneXvSTfWB5Vc4cf7b8GiN2gdwUuTiyVqZuupWske2HkZiJZUt5Lsxg9BW3gz28POkE0Ln17snlkmy02B_AD3DQxKOOxQCzIIARHdfFvZxgGWsMdFcVQZDW7rtXcqgj-SpVaUQ_8acsgxSrz_DF2o0O4tto0PT6wVUiw8KlBmgWTscWPeAWe-39T-8EiQ8Wi16h6oSPcz2NzOQ7eOM_S9fDkOorgcBkRGLl1nrahrPSdWJSGAeruk5mX4YxN714YThFDyEA2t9YmKpjaiSQ2tT-Xkd7tgsZqeirNs2ni9mIiFX3bRX6t2AhUNzA7MaX9ZyizKGa6go3BESO_oDg";
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
fetchMock.get(
|
fetchMock.get(`${delegatedAuthConfig.issuer}.well-known/openid-configuration`, delegatedAuthConfig);
|
||||||
`${delegatedAuthConfig.metadata.issuer}.well-known/openid-configuration`,
|
fetchMock.get(`${delegatedAuthConfig.issuer}jwks`, {
|
||||||
delegatedAuthConfig.metadata,
|
|
||||||
);
|
|
||||||
fetchMock.get(`${delegatedAuthConfig.metadata.issuer}jwks`, {
|
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -772,9 +769,7 @@ describe("Lifecycle", () => {
|
|||||||
await setLoggedIn(credentials);
|
await setLoggedIn(credentials);
|
||||||
|
|
||||||
// didn't try to initialise token refresher
|
// didn't try to initialise token refresher
|
||||||
expect(fetchMock).not.toHaveFetched(
|
expect(fetchMock).not.toHaveFetched(`${delegatedAuthConfig.issuer}.well-known/openid-configuration`);
|
||||||
`${delegatedAuthConfig.metadata.issuer}.well-known/openid-configuration`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not try to create a token refresher without a deviceId", async () => {
|
it("should not try to create a token refresher without a deviceId", async () => {
|
||||||
@@ -785,9 +780,7 @@ describe("Lifecycle", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// didn't try to initialise token refresher
|
// didn't try to initialise token refresher
|
||||||
expect(fetchMock).not.toHaveFetched(
|
expect(fetchMock).not.toHaveFetched(`${delegatedAuthConfig.issuer}.well-known/openid-configuration`);
|
||||||
`${delegatedAuthConfig.metadata.issuer}.well-known/openid-configuration`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not try to create a token refresher without an issuer in session storage", async () => {
|
it("should not try to create a token refresher without an issuer in session storage", async () => {
|
||||||
@@ -803,9 +796,7 @@ describe("Lifecycle", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// didn't try to initialise token refresher
|
// didn't try to initialise token refresher
|
||||||
expect(fetchMock).not.toHaveFetched(
|
expect(fetchMock).not.toHaveFetched(`${delegatedAuthConfig.issuer}.well-known/openid-configuration`);
|
||||||
`${delegatedAuthConfig.metadata.issuer}.well-known/openid-configuration`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should create a client with a tokenRefreshFunction", async () => {
|
it("should create a client with a tokenRefreshFunction", async () => {
|
||||||
|
|||||||
@@ -384,7 +384,7 @@ describe("Login", function () {
|
|||||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||||
|
|
||||||
// didn't try to register
|
// didn't try to register
|
||||||
expect(fetchMock).not.toHaveBeenCalledWith(delegatedAuth.registrationEndpoint);
|
expect(fetchMock).not.toHaveBeenCalledWith(delegatedAuth.registration_endpoint);
|
||||||
// continued with normal setup
|
// continued with normal setup
|
||||||
expect(mockClient.loginFlows).toHaveBeenCalled();
|
expect(mockClient.loginFlows).toHaveBeenCalled();
|
||||||
// normal password login rendered
|
// normal password login rendered
|
||||||
@@ -394,25 +394,25 @@ describe("Login", function () {
|
|||||||
it("should attempt to register oidc client", async () => {
|
it("should attempt to register oidc client", async () => {
|
||||||
// dont mock, spy so we can check config values were correctly passed
|
// dont mock, spy so we can check config values were correctly passed
|
||||||
jest.spyOn(registerClientUtils, "getOidcClientId");
|
jest.spyOn(registerClientUtils, "getOidcClientId");
|
||||||
fetchMock.post(delegatedAuth.registrationEndpoint!, { status: 500 });
|
fetchMock.post(delegatedAuth.registration_endpoint!, { status: 500 });
|
||||||
getComponent(hsUrl, isUrl, delegatedAuth);
|
getComponent(hsUrl, isUrl, delegatedAuth);
|
||||||
|
|
||||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||||
|
|
||||||
// tried to register
|
// tried to register
|
||||||
expect(fetchMock).toHaveBeenCalledWith(delegatedAuth.registrationEndpoint, expect.any(Object));
|
expect(fetchMock).toHaveBeenCalledWith(delegatedAuth.registration_endpoint, expect.any(Object));
|
||||||
// called with values from config
|
// called with values from config
|
||||||
expect(registerClientUtils.getOidcClientId).toHaveBeenCalledWith(delegatedAuth, oidcStaticClientsConfig);
|
expect(registerClientUtils.getOidcClientId).toHaveBeenCalledWith(delegatedAuth, oidcStaticClientsConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should fallback to normal login when client registration fails", async () => {
|
it("should fallback to normal login when client registration fails", async () => {
|
||||||
fetchMock.post(delegatedAuth.registrationEndpoint!, { status: 500 });
|
fetchMock.post(delegatedAuth.registration_endpoint!, { status: 500 });
|
||||||
getComponent(hsUrl, isUrl, delegatedAuth);
|
getComponent(hsUrl, isUrl, delegatedAuth);
|
||||||
|
|
||||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||||
|
|
||||||
// tried to register
|
// tried to register
|
||||||
expect(fetchMock).toHaveBeenCalledWith(delegatedAuth.registrationEndpoint, expect.any(Object));
|
expect(fetchMock).toHaveBeenCalledWith(delegatedAuth.registration_endpoint, expect.any(Object));
|
||||||
expect(logger.error).toHaveBeenCalledWith(new Error(OidcError.DynamicRegistrationFailed));
|
expect(logger.error).toHaveBeenCalledWith(new Error(OidcError.DynamicRegistrationFailed));
|
||||||
|
|
||||||
// continued with normal setup
|
// continued with normal setup
|
||||||
@@ -423,7 +423,7 @@ describe("Login", function () {
|
|||||||
|
|
||||||
// short term during active development, UI will be added in next PRs
|
// 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 () => {
|
it("should show continue button when oidc native flow is correctly configured", async () => {
|
||||||
fetchMock.post(delegatedAuth.registrationEndpoint!, { client_id: "abc123" });
|
fetchMock.post(delegatedAuth.registration_endpoint!, { client_id: "abc123" });
|
||||||
getComponent(hsUrl, isUrl, delegatedAuth);
|
getComponent(hsUrl, isUrl, delegatedAuth);
|
||||||
|
|
||||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||||
@@ -455,7 +455,7 @@ describe("Login", function () {
|
|||||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||||
|
|
||||||
// didn't try to register
|
// didn't try to register
|
||||||
expect(fetchMock).not.toHaveBeenCalledWith(delegatedAuth.registrationEndpoint);
|
expect(fetchMock).not.toHaveBeenCalledWith(delegatedAuth.registration_endpoint);
|
||||||
// continued with normal setup
|
// continued with normal setup
|
||||||
expect(mockClient.loginFlows).toHaveBeenCalled();
|
expect(mockClient.loginFlows).toHaveBeenCalled();
|
||||||
// oidc-aware 'continue' button displayed
|
// oidc-aware 'continue' button displayed
|
||||||
|
|||||||
@@ -158,24 +158,26 @@ describe("Registration", function () {
|
|||||||
describe("when delegated authentication is configured and enabled", () => {
|
describe("when delegated authentication is configured and enabled", () => {
|
||||||
const authConfig = makeDelegatedAuthConfig();
|
const authConfig = makeDelegatedAuthConfig();
|
||||||
const clientId = "test-client-id";
|
const clientId = "test-client-id";
|
||||||
// @ts-ignore
|
authConfig.prompt_values_supported = ["create"];
|
||||||
authConfig.metadata["prompt_values_supported"] = ["create"];
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// mock a statically registered client to avoid dynamic registration
|
// mock a statically registered client to avoid dynamic registration
|
||||||
SdkConfig.put({
|
SdkConfig.put({
|
||||||
oidc_static_clients: {
|
oidc_static_clients: {
|
||||||
[authConfig.metadata.issuer]: {
|
[authConfig.issuer]: {
|
||||||
client_id: clientId,
|
client_id: clientId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
fetchMock.get(`${defaultHsUrl}/_matrix/client/unstable/org.matrix.msc2965/auth_issuer`, {
|
fetchMock.get(`${defaultHsUrl}/_matrix/client/unstable/org.matrix.msc2965/auth_issuer`, {
|
||||||
issuer: authConfig.metadata.issuer,
|
issuer: authConfig.issuer,
|
||||||
});
|
});
|
||||||
fetchMock.get("https://auth.org/.well-known/openid-configuration", authConfig.metadata);
|
fetchMock.get("https://auth.org/.well-known/openid-configuration", {
|
||||||
fetchMock.get(authConfig.metadata.jwks_uri!, { keys: [] });
|
...authConfig,
|
||||||
|
signingKeys: undefined,
|
||||||
|
});
|
||||||
|
fetchMock.get(authConfig.jwks_uri!, { keys: [] });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should display oidc-native continue button", async () => {
|
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 React from "react";
|
||||||
import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
import { MatrixClient, MatrixEvent, PushRuleKind } from "matrix-js-sdk/src/matrix";
|
||||||
import { mocked, MockedObject } from "jest-mock";
|
import { mocked, MockedObject } from "jest-mock";
|
||||||
import { render, waitFor } from "jest-matrix-react";
|
import { render, waitFor } from "jest-matrix-react";
|
||||||
|
|
||||||
@@ -228,6 +228,23 @@ describe("<TextualBody />", () => {
|
|||||||
const content = container.querySelector(".mx_EventTile_body");
|
const content = container.querySelector(".mx_EventTile_body");
|
||||||
expect(content.innerHTML.replace(defaultEvent.getId(), "%event_id%")).toMatchSnapshot();
|
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", () => {
|
describe("renders formatted m.text correctly", () => {
|
||||||
|
|||||||
@@ -224,7 +224,29 @@ exports[`MemberTileView ThreePidInviteTileView renders ThreePidInvite correctly
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="mx_MemberTileView_right"
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
IThreepid,
|
IThreepid,
|
||||||
ThreepidMedium,
|
ThreepidMedium,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
|
||||||
import {
|
import {
|
||||||
act,
|
act,
|
||||||
fireEvent,
|
fireEvent,
|
||||||
@@ -287,7 +287,7 @@ describe("<Notifications />", () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
let i = 0;
|
let i = 0;
|
||||||
mocked(randomString).mockImplementation(() => {
|
mocked(secureRandomString).mockImplementation(() => {
|
||||||
return "testid_" + i++;
|
return "testid_" + i++;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ import SettingsStore from "../../../../../../../src/settings/SettingsStore";
|
|||||||
import { getClientInformationEventType } from "../../../../../../../src/utils/device/clientInformation";
|
import { getClientInformationEventType } from "../../../../../../../src/utils/device/clientInformation";
|
||||||
import { SDKContext, SdkContextClass } from "../../../../../../../src/contexts/SDKContext";
|
import { SDKContext, SdkContextClass } from "../../../../../../../src/contexts/SDKContext";
|
||||||
import { OidcClientStore } from "../../../../../../../src/stores/oidc/OidcClientStore";
|
import { OidcClientStore } from "../../../../../../../src/stores/oidc/OidcClientStore";
|
||||||
import { mockOpenIdConfiguration } from "../../../../../../test-utils/oidc";
|
import { makeDelegatedAuthConfig } from "../../../../../../test-utils/oidc";
|
||||||
import MatrixClientContext from "../../../../../../../src/contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../../../../../src/contexts/MatrixClientContext";
|
||||||
|
|
||||||
mockPlatformPeg();
|
mockPlatformPeg();
|
||||||
@@ -215,7 +215,7 @@ describe("<SessionManagerTab />", () => {
|
|||||||
getPushers: jest.fn(),
|
getPushers: jest.fn(),
|
||||||
setPusher: jest.fn(),
|
setPusher: jest.fn(),
|
||||||
setLocalNotificationSettings: jest.fn(),
|
setLocalNotificationSettings: jest.fn(),
|
||||||
getAuthIssuer: jest.fn().mockReturnValue(new Promise(() => {})),
|
getAuthMetadata: jest.fn().mockRejectedValue(new MatrixError({ errcode: "M_UNRECOGNIZED" }, 404)),
|
||||||
});
|
});
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
jest.spyOn(logger, "error").mockRestore();
|
jest.spyOn(logger, "error").mockRestore();
|
||||||
@@ -1615,7 +1615,6 @@ describe("<SessionManagerTab />", () => {
|
|||||||
describe("MSC4108 QR code login", () => {
|
describe("MSC4108 QR code login", () => {
|
||||||
const settingsValueSpy = jest.spyOn(SettingsStore, "getValue");
|
const settingsValueSpy = jest.spyOn(SettingsStore, "getValue");
|
||||||
const issuer = "https://issuer.org";
|
const issuer = "https://issuer.org";
|
||||||
const openIdConfiguration = mockOpenIdConfiguration(issuer);
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
settingsValueSpy.mockClear().mockReturnValue(true);
|
settingsValueSpy.mockClear().mockReturnValue(true);
|
||||||
@@ -1631,16 +1630,16 @@ describe("<SessionManagerTab />", () => {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
mockClient.getAuthIssuer.mockResolvedValue({ issuer });
|
const delegatedAuthConfig = makeDelegatedAuthConfig(issuer);
|
||||||
mockCrypto.exportSecretsBundle = jest.fn();
|
mockClient.getAuthMetadata.mockResolvedValue({
|
||||||
fetchMock.mock(`${issuer}/.well-known/openid-configuration`, {
|
...delegatedAuthConfig,
|
||||||
...openIdConfiguration,
|
|
||||||
grant_types_supported: [
|
grant_types_supported: [
|
||||||
...openIdConfiguration.grant_types_supported,
|
...delegatedAuthConfig.grant_types_supported,
|
||||||
"urn:ietf:params:oauth:grant-type:device_code",
|
"urn:ietf:params:oauth:grant-type:device_code",
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
fetchMock.mock(openIdConfiguration.jwks_uri!, {
|
mockCrypto.exportSecretsBundle = jest.fn();
|
||||||
|
fetchMock.mock(delegatedAuthConfig.jwks_uri!, {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { mocked } from "jest-mock";
|
import { mocked } from "jest-mock";
|
||||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
|
||||||
import { act, fireEvent, render, RenderResult } from "jest-matrix-react";
|
import { act, fireEvent, render, RenderResult } from "jest-matrix-react";
|
||||||
import { EventType, MatrixClient, Room, GuestAccess, HistoryVisibility, JoinRule } from "matrix-js-sdk/src/matrix";
|
import { EventType, MatrixClient, Room, GuestAccess, HistoryVisibility, JoinRule } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ describe("<SpaceSettingsVisibilityTab />", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
let i = 0;
|
let i = 0;
|
||||||
mocked(randomString).mockImplementation(() => {
|
mocked(secureRandomString).mockImplementation(() => {
|
||||||
return "testid_" + i++;
|
return "testid_" + i++;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { OidcError } from "matrix-js-sdk/src/oidc/error";
|
|||||||
|
|
||||||
import { OidcClientStore } from "../../../../src/stores/oidc/OidcClientStore";
|
import { OidcClientStore } from "../../../../src/stores/oidc/OidcClientStore";
|
||||||
import { flushPromises, getMockClientWithEventEmitter, mockPlatformPeg } from "../../../test-utils";
|
import { flushPromises, getMockClientWithEventEmitter, mockPlatformPeg } from "../../../test-utils";
|
||||||
import { mockOpenIdConfiguration } from "../../../test-utils/oidc";
|
import { makeDelegatedAuthConfig } from "../../../test-utils/oidc";
|
||||||
|
|
||||||
jest.mock("matrix-js-sdk/src/matrix", () => ({
|
jest.mock("matrix-js-sdk/src/matrix", () => ({
|
||||||
...jest.requireActual("matrix-js-sdk/src/matrix"),
|
...jest.requireActual("matrix-js-sdk/src/matrix"),
|
||||||
@@ -24,28 +24,30 @@ jest.mock("matrix-js-sdk/src/matrix", () => ({
|
|||||||
|
|
||||||
describe("OidcClientStore", () => {
|
describe("OidcClientStore", () => {
|
||||||
const clientId = "test-client-id";
|
const clientId = "test-client-id";
|
||||||
const metadata = mockOpenIdConfiguration();
|
const authConfig = makeDelegatedAuthConfig();
|
||||||
const account = metadata.issuer + "account";
|
const account = authConfig.issuer + "account";
|
||||||
|
|
||||||
const mockClient = getMockClientWithEventEmitter({
|
const mockClient = getMockClientWithEventEmitter({
|
||||||
getAuthIssuer: jest.fn(),
|
getAuthMetadata: jest.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
localStorage.setItem("mx_oidc_client_id", clientId);
|
localStorage.setItem("mx_oidc_client_id", clientId);
|
||||||
localStorage.setItem("mx_oidc_token_issuer", metadata.issuer);
|
localStorage.setItem("mx_oidc_token_issuer", authConfig.issuer);
|
||||||
|
|
||||||
mocked(discoverAndValidateOIDCIssuerWellKnown).mockClear().mockResolvedValue({
|
mocked(discoverAndValidateOIDCIssuerWellKnown)
|
||||||
metadata,
|
.mockClear()
|
||||||
accountManagementEndpoint: account,
|
.mockResolvedValue({
|
||||||
authorizationEndpoint: "authorization-endpoint",
|
...authConfig,
|
||||||
tokenEndpoint: "token-endpoint",
|
account_management_uri: account,
|
||||||
|
authorization_endpoint: "authorization-endpoint",
|
||||||
|
token_endpoint: "token-endpoint",
|
||||||
});
|
});
|
||||||
jest.spyOn(logger, "error").mockClear();
|
jest.spyOn(logger, "error").mockClear();
|
||||||
|
|
||||||
fetchMock.get(`${metadata.issuer}.well-known/openid-configuration`, metadata);
|
fetchMock.get(`${authConfig.issuer}.well-known/openid-configuration`, authConfig);
|
||||||
fetchMock.get(`${metadata.issuer}jwks`, { keys: [] });
|
fetchMock.get(`${authConfig.issuer}jwks`, { keys: [] });
|
||||||
mockPlatformPeg();
|
mockPlatformPeg();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -116,7 +118,7 @@ describe("OidcClientStore", () => {
|
|||||||
const client = await store.getOidcClient();
|
const client = await store.getOidcClient();
|
||||||
|
|
||||||
expect(client?.settings.client_id).toEqual(clientId);
|
expect(client?.settings.client_id).toEqual(clientId);
|
||||||
expect(client?.settings.authority).toEqual(metadata.issuer);
|
expect(client?.settings.authority).toEqual(authConfig.issuer);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should set account management endpoint when configured", async () => {
|
it("should set account management endpoint when configured", async () => {
|
||||||
@@ -129,17 +131,19 @@ describe("OidcClientStore", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should set account management endpoint to issuer when not configured", async () => {
|
it("should set account management endpoint to issuer when not configured", async () => {
|
||||||
mocked(discoverAndValidateOIDCIssuerWellKnown).mockClear().mockResolvedValue({
|
mocked(discoverAndValidateOIDCIssuerWellKnown)
|
||||||
metadata,
|
.mockClear()
|
||||||
accountManagementEndpoint: undefined,
|
.mockResolvedValue({
|
||||||
authorizationEndpoint: "authorization-endpoint",
|
...authConfig,
|
||||||
tokenEndpoint: "token-endpoint",
|
account_management_uri: undefined,
|
||||||
|
authorization_endpoint: "authorization-endpoint",
|
||||||
|
token_endpoint: "token-endpoint",
|
||||||
});
|
});
|
||||||
const store = new OidcClientStore(mockClient);
|
const store = new OidcClientStore(mockClient);
|
||||||
|
|
||||||
await store.readyPromise;
|
await store.readyPromise;
|
||||||
|
|
||||||
expect(store.accountManagementEndpoint).toEqual(metadata.issuer);
|
expect(store.accountManagementEndpoint).toEqual(authConfig.issuer);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should reuse initialised oidc client", async () => {
|
it("should reuse initialised oidc client", async () => {
|
||||||
@@ -175,7 +179,7 @@ describe("OidcClientStore", () => {
|
|||||||
|
|
||||||
fetchMock.resetHistory();
|
fetchMock.resetHistory();
|
||||||
fetchMock.post(
|
fetchMock.post(
|
||||||
metadata.revocation_endpoint,
|
authConfig.revocation_endpoint,
|
||||||
{
|
{
|
||||||
status: 200,
|
status: 200,
|
||||||
},
|
},
|
||||||
@@ -197,7 +201,7 @@ describe("OidcClientStore", () => {
|
|||||||
|
|
||||||
await store.revokeTokens(accessToken, refreshToken);
|
await store.revokeTokens(accessToken, refreshToken);
|
||||||
|
|
||||||
expect(fetchMock).toHaveFetchedTimes(2, metadata.revocation_endpoint);
|
expect(fetchMock).toHaveFetchedTimes(2, authConfig.revocation_endpoint);
|
||||||
expect(OidcClient.prototype.revokeToken).toHaveBeenCalledWith(accessToken, "access_token");
|
expect(OidcClient.prototype.revokeToken).toHaveBeenCalledWith(accessToken, "access_token");
|
||||||
expect(OidcClient.prototype.revokeToken).toHaveBeenCalledWith(refreshToken, "refresh_token");
|
expect(OidcClient.prototype.revokeToken).toHaveBeenCalledWith(refreshToken, "refresh_token");
|
||||||
});
|
});
|
||||||
@@ -206,14 +210,14 @@ describe("OidcClientStore", () => {
|
|||||||
// fail once, then succeed
|
// fail once, then succeed
|
||||||
fetchMock
|
fetchMock
|
||||||
.postOnce(
|
.postOnce(
|
||||||
metadata.revocation_endpoint,
|
authConfig.revocation_endpoint,
|
||||||
{
|
{
|
||||||
status: 404,
|
status: 404,
|
||||||
},
|
},
|
||||||
{ overwriteRoutes: true, sendAsJson: true },
|
{ overwriteRoutes: true, sendAsJson: true },
|
||||||
)
|
)
|
||||||
.post(
|
.post(
|
||||||
metadata.revocation_endpoint,
|
authConfig.revocation_endpoint,
|
||||||
{
|
{
|
||||||
status: 200,
|
status: 200,
|
||||||
},
|
},
|
||||||
@@ -226,7 +230,7 @@ describe("OidcClientStore", () => {
|
|||||||
"Failed to revoke tokens",
|
"Failed to revoke tokens",
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(fetchMock).toHaveFetchedTimes(2, metadata.revocation_endpoint);
|
expect(fetchMock).toHaveFetchedTimes(2, authConfig.revocation_endpoint);
|
||||||
expect(OidcClient.prototype.revokeToken).toHaveBeenCalledWith(accessToken, "access_token");
|
expect(OidcClient.prototype.revokeToken).toHaveBeenCalledWith(accessToken, "access_token");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -237,7 +241,10 @@ describe("OidcClientStore", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should resolve account management endpoint", async () => {
|
it("should resolve account management endpoint", async () => {
|
||||||
mockClient.getAuthIssuer.mockResolvedValue({ issuer: metadata.issuer });
|
mockClient.getAuthMetadata.mockResolvedValue({
|
||||||
|
...authConfig,
|
||||||
|
account_management_uri: account,
|
||||||
|
});
|
||||||
const store = new OidcClientStore(mockClient);
|
const store = new OidcClientStore(mockClient);
|
||||||
await store.readyPromise;
|
await store.readyPromise;
|
||||||
expect(store.accountManagementEndpoint).toBe(account);
|
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.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { mocked, MockedObject } from "jest-mock";
|
import { mocked, MockedFunction, MockedObject } from "jest-mock";
|
||||||
import { last } from "lodash";
|
import { last } from "lodash";
|
||||||
import {
|
import {
|
||||||
MatrixEvent,
|
MatrixEvent,
|
||||||
@@ -15,15 +15,20 @@ import {
|
|||||||
EventTimeline,
|
EventTimeline,
|
||||||
EventType,
|
EventType,
|
||||||
MatrixEventEvent,
|
MatrixEventEvent,
|
||||||
|
RoomStateEvent,
|
||||||
|
RoomState,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import { ClientWidgetApi, WidgetApiFromWidgetAction } from "matrix-widget-api";
|
import { ClientWidgetApi, WidgetApiFromWidgetAction } from "matrix-widget-api";
|
||||||
import { waitFor } from "jest-matrix-react";
|
import { waitFor } from "jest-matrix-react";
|
||||||
|
import { Optional } from "matrix-events-sdk";
|
||||||
|
|
||||||
import { stubClient, mkRoom, mkEvent } from "../../../test-utils";
|
import { stubClient, mkRoom, mkEvent } from "../../../test-utils";
|
||||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||||
import { StopGapWidget } from "../../../../src/stores/widgets/StopGapWidget";
|
import { StopGapWidget } from "../../../../src/stores/widgets/StopGapWidget";
|
||||||
import ActiveWidgetStore from "../../../../src/stores/ActiveWidgetStore";
|
import ActiveWidgetStore from "../../../../src/stores/ActiveWidgetStore";
|
||||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
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");
|
jest.mock("matrix-widget-api/lib/ClientWidgetApi");
|
||||||
|
|
||||||
@@ -53,6 +58,7 @@ describe("StopGapWidget", () => {
|
|||||||
// Start messaging without an iframe, since ClientWidgetApi is mocked
|
// Start messaging without an iframe, since ClientWidgetApi is mocked
|
||||||
widget.startMessaging(null as unknown as HTMLIFrameElement);
|
widget.startMessaging(null as unknown as HTMLIFrameElement);
|
||||||
messaging = mocked(last(mocked(ClientWidgetApi).mock.instances)!);
|
messaging = mocked(last(mocked(ClientWidgetApi).mock.instances)!);
|
||||||
|
messaging.feedStateUpdate.mockResolvedValue();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -84,6 +90,20 @@ describe("StopGapWidget", () => {
|
|||||||
expect(messaging.feedToDevice).toHaveBeenCalledWith(event.getEffectiveEvent(), false);
|
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", () => {
|
describe("feed event", () => {
|
||||||
let event1: MatrixEvent;
|
let event1: MatrixEvent;
|
||||||
let event2: MatrixEvent;
|
let event2: MatrixEvent;
|
||||||
@@ -118,24 +138,24 @@ describe("StopGapWidget", () => {
|
|||||||
|
|
||||||
it("feeds incoming event to the widget", async () => {
|
it("feeds incoming event to the widget", async () => {
|
||||||
client.emit(ClientEvent.Event, event1);
|
client.emit(ClientEvent.Event, event1);
|
||||||
expect(messaging.feedEvent).toHaveBeenCalledWith(event1.getEffectiveEvent(), "!1:example.org");
|
expect(messaging.feedEvent).toHaveBeenCalledWith(event1.getEffectiveEvent());
|
||||||
|
|
||||||
client.emit(ClientEvent.Event, event2);
|
client.emit(ClientEvent.Event, event2);
|
||||||
expect(messaging.feedEvent).toHaveBeenCalledTimes(2);
|
expect(messaging.feedEvent).toHaveBeenCalledTimes(2);
|
||||||
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2.getEffectiveEvent(), "!1:example.org");
|
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2.getEffectiveEvent());
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not feed incoming event to the widget if seen already", async () => {
|
it("should not feed incoming event to the widget if seen already", async () => {
|
||||||
client.emit(ClientEvent.Event, event1);
|
client.emit(ClientEvent.Event, event1);
|
||||||
expect(messaging.feedEvent).toHaveBeenCalledWith(event1.getEffectiveEvent(), "!1:example.org");
|
expect(messaging.feedEvent).toHaveBeenCalledWith(event1.getEffectiveEvent());
|
||||||
|
|
||||||
client.emit(ClientEvent.Event, event2);
|
client.emit(ClientEvent.Event, event2);
|
||||||
expect(messaging.feedEvent).toHaveBeenCalledTimes(2);
|
expect(messaging.feedEvent).toHaveBeenCalledTimes(2);
|
||||||
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2.getEffectiveEvent(), "!1:example.org");
|
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2.getEffectiveEvent());
|
||||||
|
|
||||||
client.emit(ClientEvent.Event, event1);
|
client.emit(ClientEvent.Event, event1);
|
||||||
expect(messaging.feedEvent).toHaveBeenCalledTimes(2);
|
expect(messaging.feedEvent).toHaveBeenCalledTimes(2);
|
||||||
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2.getEffectiveEvent(), "!1:example.org");
|
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2.getEffectiveEvent());
|
||||||
});
|
});
|
||||||
|
|
||||||
it("feeds decrypted events asynchronously", async () => {
|
it("feeds decrypted events asynchronously", async () => {
|
||||||
@@ -165,7 +185,7 @@ describe("StopGapWidget", () => {
|
|||||||
decryptingSpy2.mockReturnValue(false);
|
decryptingSpy2.mockReturnValue(false);
|
||||||
client.emit(MatrixEventEvent.Decrypted, event2Encrypted);
|
client.emit(MatrixEventEvent.Decrypted, event2Encrypted);
|
||||||
expect(messaging.feedEvent).toHaveBeenCalledTimes(1);
|
expect(messaging.feedEvent).toHaveBeenCalledTimes(1);
|
||||||
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2Encrypted.getEffectiveEvent(), "!1:example.org");
|
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2Encrypted.getEffectiveEvent());
|
||||||
// …then event 1
|
// …then event 1
|
||||||
event1Encrypted.event.type = event1.getType();
|
event1Encrypted.event.type = event1.getType();
|
||||||
event1Encrypted.event.content = event1.getContent();
|
event1Encrypted.event.content = event1.getContent();
|
||||||
@@ -175,7 +195,7 @@ describe("StopGapWidget", () => {
|
|||||||
// doesn't have to be blocked on the decryption of event 1 (or
|
// doesn't have to be blocked on the decryption of event 1 (or
|
||||||
// worse, dropped)
|
// worse, dropped)
|
||||||
expect(messaging.feedEvent).toHaveBeenCalledTimes(2);
|
expect(messaging.feedEvent).toHaveBeenCalledTimes(2);
|
||||||
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event1Encrypted.getEffectiveEvent(), "!1:example.org");
|
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event1Encrypted.getEffectiveEvent());
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not feed incoming event if not in timeline", () => {
|
it("should not feed incoming event if not in timeline", () => {
|
||||||
@@ -191,7 +211,7 @@ describe("StopGapWidget", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
client.emit(ClientEvent.Event, event);
|
client.emit(ClientEvent.Event, event);
|
||||||
expect(messaging.feedEvent).toHaveBeenCalledWith(event.getEffectiveEvent(), "!1:example.org");
|
expect(messaging.feedEvent).toHaveBeenCalledWith(event.getEffectiveEvent());
|
||||||
});
|
});
|
||||||
|
|
||||||
it("feeds incoming event that is not in timeline but relates to unknown parent to the widget", async () => {
|
it("feeds incoming event that is not in timeline but relates to unknown parent to the widget", async () => {
|
||||||
@@ -211,18 +231,19 @@ describe("StopGapWidget", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
client.emit(ClientEvent.Event, event1);
|
client.emit(ClientEvent.Event, event1);
|
||||||
expect(messaging.feedEvent).toHaveBeenCalledWith(event1.getEffectiveEvent(), "!1:example.org");
|
expect(messaging.feedEvent).toHaveBeenCalledWith(event1.getEffectiveEvent());
|
||||||
|
|
||||||
client.emit(ClientEvent.Event, event);
|
client.emit(ClientEvent.Event, event);
|
||||||
expect(messaging.feedEvent).toHaveBeenCalledTimes(2);
|
expect(messaging.feedEvent).toHaveBeenCalledTimes(2);
|
||||||
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event.getEffectiveEvent(), "!1:example.org");
|
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event.getEffectiveEvent());
|
||||||
|
|
||||||
client.emit(ClientEvent.Event, event1);
|
client.emit(ClientEvent.Event, event1);
|
||||||
expect(messaging.feedEvent).toHaveBeenCalledTimes(2);
|
expect(messaging.feedEvent).toHaveBeenCalledTimes(2);
|
||||||
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event.getEffectiveEvent(), "!1:example.org");
|
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event.getEffectiveEvent());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("StopGapWidget with stickyPromise", () => {
|
describe("StopGapWidget with stickyPromise", () => {
|
||||||
let client: MockedObject<MatrixClient>;
|
let client: MockedObject<MatrixClient>;
|
||||||
let widget: StopGapWidget;
|
let widget: StopGapWidget;
|
||||||
@@ -288,3 +309,49 @@ describe("StopGapWidget with stickyPromise", () => {
|
|||||||
waitFor(() => expect(setPersistenceSpy).toHaveBeenCalled(), { interval: 5 });
|
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,6 +17,7 @@ import {
|
|||||||
MatrixEvent,
|
MatrixEvent,
|
||||||
MsgType,
|
MsgType,
|
||||||
RelationType,
|
RelationType,
|
||||||
|
Room,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import {
|
import {
|
||||||
Widget,
|
Widget,
|
||||||
@@ -38,7 +39,7 @@ import {
|
|||||||
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
|
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
|
||||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||||
import { StopGapWidgetDriver } from "../../../../src/stores/widgets/StopGapWidgetDriver";
|
import { StopGapWidgetDriver } from "../../../../src/stores/widgets/StopGapWidgetDriver";
|
||||||
import { stubClient } from "../../../test-utils";
|
import { mkEvent, stubClient } from "../../../test-utils";
|
||||||
import { ModuleRunner } from "../../../../src/modules/ModuleRunner";
|
import { ModuleRunner } from "../../../../src/modules/ModuleRunner";
|
||||||
import dis from "../../../../src/dispatcher/dispatcher";
|
import dis from "../../../../src/dispatcher/dispatcher";
|
||||||
import Modal from "../../../../src/Modal";
|
import Modal from "../../../../src/Modal";
|
||||||
@@ -569,7 +570,7 @@ describe("StopGapWidgetDriver", () => {
|
|||||||
|
|
||||||
it("passes the flag through to getVisibleRooms", () => {
|
it("passes the flag through to getVisibleRooms", () => {
|
||||||
const driver = mkDefaultDriver();
|
const driver = mkDefaultDriver();
|
||||||
driver.readRoomEvents(EventType.CallAnswer, "", 0, ["*"]);
|
driver.getKnownRooms();
|
||||||
expect(client.getVisibleRooms).toHaveBeenCalledWith(false);
|
expect(client.getVisibleRooms).toHaveBeenCalledWith(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -584,7 +585,7 @@ describe("StopGapWidgetDriver", () => {
|
|||||||
|
|
||||||
it("passes the flag through to getVisibleRooms", () => {
|
it("passes the flag through to getVisibleRooms", () => {
|
||||||
const driver = mkDefaultDriver();
|
const driver = mkDefaultDriver();
|
||||||
driver.readRoomEvents(EventType.CallAnswer, "", 0, ["*"]);
|
driver.getKnownRooms();
|
||||||
expect(client.getVisibleRooms).toHaveBeenCalledWith(true);
|
expect(client.getVisibleRooms).toHaveBeenCalledWith(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -692,4 +693,107 @@ describe("StopGapWidgetDriver", () => {
|
|||||||
await expect(file.text()).resolves.toEqual("test contents");
|
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,21 +355,19 @@ describe("AutoDiscoveryUtils", () => {
|
|||||||
hsNameIsDifferent: true,
|
hsNameIsDifferent: true,
|
||||||
hsName: serverName,
|
hsName: serverName,
|
||||||
delegatedAuthentication: expect.objectContaining({
|
delegatedAuthentication: expect.objectContaining({
|
||||||
accountManagementActionsSupported: [
|
issuer,
|
||||||
|
account_management_actions_supported: [
|
||||||
"org.matrix.profile",
|
"org.matrix.profile",
|
||||||
"org.matrix.sessions_list",
|
"org.matrix.sessions_list",
|
||||||
"org.matrix.session_view",
|
"org.matrix.session_view",
|
||||||
"org.matrix.session_end",
|
"org.matrix.session_end",
|
||||||
"org.matrix.cross_signing_reset",
|
"org.matrix.cross_signing_reset",
|
||||||
],
|
],
|
||||||
accountManagementEndpoint: "https://auth.matrix.org/account/",
|
account_management_uri: "https://auth.matrix.org/account/",
|
||||||
authorizationEndpoint: "https://auth.matrix.org/auth",
|
authorization_endpoint: "https://auth.matrix.org/auth",
|
||||||
metadata: expect.objectContaining({
|
registration_endpoint: "https://auth.matrix.org/registration",
|
||||||
issuer,
|
|
||||||
}),
|
|
||||||
registrationEndpoint: "https://auth.matrix.org/registration",
|
|
||||||
signingKeys: [],
|
signingKeys: [],
|
||||||
tokenEndpoint: "https://auth.matrix.org/token",
|
token_endpoint: "https://auth.matrix.org/token",
|
||||||
}),
|
}),
|
||||||
warning: null,
|
warning: null,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ describe("TokenRefresher", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fetchMock.get(`${issuer}.well-known/openid-configuration`, authConfig.metadata);
|
fetchMock.get(`${issuer}.well-known/openid-configuration`, authConfig);
|
||||||
fetchMock.get(`${issuer}jwks`, {
|
fetchMock.get(`${issuer}jwks`, {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ describe("OIDC authorization", () => {
|
|||||||
origin: baseUrl,
|
origin: baseUrl,
|
||||||
};
|
};
|
||||||
|
|
||||||
jest.spyOn(randomStringUtils, "randomString").mockRestore();
|
jest.spyOn(randomStringUtils, "secureRandomString").mockRestore();
|
||||||
mockPlatformPeg();
|
mockPlatformPeg();
|
||||||
Object.defineProperty(window, "crypto", {
|
Object.defineProperty(window, "crypto", {
|
||||||
value: {
|
value: {
|
||||||
@@ -61,10 +61,7 @@ describe("OIDC authorization", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
fetchMock.get(
|
fetchMock.get(`${delegatedAuthConfig.issuer}.well-known/openid-configuration`, delegatedAuthConfig);
|
||||||
`${delegatedAuthConfig.metadata.issuer}.well-known/openid-configuration`,
|
|
||||||
delegatedAuthConfig.metadata,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ describe("getOidcClientId()", () => {
|
|||||||
const authConfigWithoutRegistration: OidcClientConfig = makeDelegatedAuthConfig(
|
const authConfigWithoutRegistration: OidcClientConfig = makeDelegatedAuthConfig(
|
||||||
"https://issuerWithoutStaticClientId.org/",
|
"https://issuerWithoutStaticClientId.org/",
|
||||||
);
|
);
|
||||||
authConfigWithoutRegistration.registrationEndpoint = undefined;
|
authConfigWithoutRegistration.registration_endpoint = undefined;
|
||||||
await expect(getOidcClientId(authConfigWithoutRegistration, staticOidcClients)).rejects.toThrow(
|
await expect(getOidcClientId(authConfigWithoutRegistration, staticOidcClients)).rejects.toThrow(
|
||||||
OidcError.DynamicRegistrationNotSupported,
|
OidcError.DynamicRegistrationNotSupported,
|
||||||
);
|
);
|
||||||
@@ -69,7 +69,7 @@ describe("getOidcClientId()", () => {
|
|||||||
it("should handle when staticOidcClients object is falsy", async () => {
|
it("should handle when staticOidcClients object is falsy", async () => {
|
||||||
const authConfigWithoutRegistration: OidcClientConfig = {
|
const authConfigWithoutRegistration: OidcClientConfig = {
|
||||||
...delegatedAuthConfig,
|
...delegatedAuthConfig,
|
||||||
registrationEndpoint: undefined,
|
registration_endpoint: undefined,
|
||||||
};
|
};
|
||||||
await expect(getOidcClientId(authConfigWithoutRegistration)).rejects.toThrow(
|
await expect(getOidcClientId(authConfigWithoutRegistration)).rejects.toThrow(
|
||||||
OidcError.DynamicRegistrationNotSupported,
|
OidcError.DynamicRegistrationNotSupported,
|
||||||
@@ -79,14 +79,14 @@ describe("getOidcClientId()", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should make correct request to register client", async () => {
|
it("should make correct request to register client", async () => {
|
||||||
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
|
fetchMockJest.post(delegatedAuthConfig.registration_endpoint!, {
|
||||||
status: 200,
|
status: 200,
|
||||||
body: JSON.stringify({ client_id: dynamicClientId }),
|
body: JSON.stringify({ client_id: dynamicClientId }),
|
||||||
});
|
});
|
||||||
expect(await getOidcClientId(delegatedAuthConfig)).toEqual(dynamicClientId);
|
expect(await getOidcClientId(delegatedAuthConfig)).toEqual(dynamicClientId);
|
||||||
// didn't try to register
|
// didn't try to register
|
||||||
expect(fetchMockJest).toHaveBeenCalledWith(
|
expect(fetchMockJest).toHaveBeenCalledWith(
|
||||||
delegatedAuthConfig.registrationEndpoint!,
|
delegatedAuthConfig.registration_endpoint!,
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
headers: {
|
headers: {
|
||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
@@ -111,14 +111,14 @@ describe("getOidcClientId()", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should throw when registration request fails", async () => {
|
it("should throw when registration request fails", async () => {
|
||||||
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
|
fetchMockJest.post(delegatedAuthConfig.registration_endpoint!, {
|
||||||
status: 500,
|
status: 500,
|
||||||
});
|
});
|
||||||
await expect(getOidcClientId(delegatedAuthConfig)).rejects.toThrow(OidcError.DynamicRegistrationFailed);
|
await expect(getOidcClientId(delegatedAuthConfig)).rejects.toThrow(OidcError.DynamicRegistrationFailed);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw when registration response is invalid", async () => {
|
it("should throw when registration response is invalid", async () => {
|
||||||
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
|
fetchMockJest.post(delegatedAuthConfig.registration_endpoint!, {
|
||||||
status: 200,
|
status: 200,
|
||||||
// no clientId in response
|
// no clientId in response
|
||||||
body: "{}",
|
body: "{}",
|
||||||
|
|||||||
Reference in New Issue
Block a user