Merge branch 'develop' into t3chguy/wat/230.1
This commit is contained in:
commit
7feb5a0b49
31 changed files with 523 additions and 155 deletions
|
@ -6,9 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Page } from "@playwright/test";
|
||||||
|
|
||||||
import { expect, test } from "../../element-web-test";
|
import { expect, test } from "../../element-web-test";
|
||||||
import { autoJoin, createSharedRoomWithUser, enableKeyBackup, logIntoElement, logOutOfElement, verify } from "./utils";
|
import { autoJoin, createSharedRoomWithUser, enableKeyBackup, logIntoElement, logOutOfElement, verify } from "./utils";
|
||||||
import { Bot } from "../../pages/bot";
|
import { Bot } from "../../pages/bot";
|
||||||
|
import { HomeserverInstance } from "../../plugins/homeserver";
|
||||||
|
|
||||||
test.describe("Cryptography", function () {
|
test.describe("Cryptography", function () {
|
||||||
test.use({
|
test.use({
|
||||||
|
@ -41,16 +44,14 @@ test.describe("Cryptography", function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should show the correct shield on e2e events", async ({ page, app, bot: bob, homeserver }) => {
|
test("should show the correct shield on e2e events", async ({
|
||||||
|
page,
|
||||||
|
app,
|
||||||
|
bot: bob,
|
||||||
|
homeserver,
|
||||||
|
}, workerInfo) => {
|
||||||
// Bob has a second, not cross-signed, device
|
// Bob has a second, not cross-signed, device
|
||||||
const bobSecondDevice = new Bot(page, homeserver, {
|
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
|
||||||
bootstrapSecretStorage: false,
|
|
||||||
bootstrapCrossSigning: false,
|
|
||||||
});
|
|
||||||
bobSecondDevice.setCredentials(
|
|
||||||
await homeserver.loginUser(bob.credentials.userId, bob.credentials.password),
|
|
||||||
);
|
|
||||||
await bobSecondDevice.prepareClient();
|
|
||||||
|
|
||||||
await bob.sendEvent(testRoomId, null, "m.room.encrypted", {
|
await bob.sendEvent(testRoomId, null, "m.room.encrypted", {
|
||||||
algorithm: "m.megolm.v1.aes-sha2",
|
algorithm: "m.megolm.v1.aes-sha2",
|
||||||
|
@ -117,7 +118,10 @@ test.describe("Cryptography", function () {
|
||||||
await lastTileE2eIcon.focus();
|
await lastTileE2eIcon.focus();
|
||||||
await expect(page.getByRole("tooltip")).toContainText("Encrypted by a device not verified by its owner.");
|
await expect(page.getByRole("tooltip")).toContainText("Encrypted by a device not verified by its owner.");
|
||||||
|
|
||||||
/* Should show a grey padlock for a message from an unknown device */
|
/* In legacy crypto: should show a grey padlock for a message from a deleted device.
|
||||||
|
* In rust crypto: should show a red padlock for a message from an unverified device.
|
||||||
|
* Rust crypto remembers the verification state of the sending device, so it will know that the device was
|
||||||
|
* unverified, even if it gets deleted. */
|
||||||
// bob deletes his second device
|
// bob deletes his second device
|
||||||
await bobSecondDevice.evaluate((cli) => cli.logout(true));
|
await bobSecondDevice.evaluate((cli) => cli.logout(true));
|
||||||
|
|
||||||
|
@ -148,7 +152,11 @@ test.describe("Cryptography", function () {
|
||||||
await expect(last).toContainText("test encrypted from unverified");
|
await expect(last).toContainText("test encrypted from unverified");
|
||||||
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
||||||
await lastE2eIcon.focus();
|
await lastE2eIcon.focus();
|
||||||
await expect(page.getByRole("tooltip")).toContainText("Encrypted by an unknown or deleted device.");
|
await expect(page.getByRole("tooltip")).toContainText(
|
||||||
|
workerInfo.project.name === "Legacy Crypto"
|
||||||
|
? "Encrypted by an unknown or deleted device."
|
||||||
|
: "Encrypted by a device not verified by its owner.",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Should show a grey padlock for a key restored from backup", async ({
|
test("Should show a grey padlock for a key restored from backup", async ({
|
||||||
|
@ -204,14 +212,7 @@ test.describe("Cryptography", function () {
|
||||||
|
|
||||||
test("should show the correct shield on edited e2e events", async ({ page, app, bot: bob, homeserver }) => {
|
test("should show the correct shield on edited e2e events", async ({ page, app, bot: bob, homeserver }) => {
|
||||||
// bob has a second, not cross-signed, device
|
// bob has a second, not cross-signed, device
|
||||||
const bobSecondDevice = new Bot(page, homeserver, {
|
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
|
||||||
bootstrapSecretStorage: false,
|
|
||||||
bootstrapCrossSigning: false,
|
|
||||||
});
|
|
||||||
bobSecondDevice.setCredentials(
|
|
||||||
await homeserver.loginUser(bob.credentials.userId, bob.credentials.password),
|
|
||||||
);
|
|
||||||
await bobSecondDevice.prepareClient();
|
|
||||||
|
|
||||||
// verify Bob
|
// verify Bob
|
||||||
await verify(app, bob);
|
await verify(app, bob);
|
||||||
|
@ -257,5 +258,51 @@ test.describe("Cryptography", function () {
|
||||||
page.locator(".mx_EventTile", { hasText: "Hee!" }).locator(".mx_EventTile_e2eIcon_warning"),
|
page.locator(".mx_EventTile", { hasText: "Hee!" }).locator(".mx_EventTile_e2eIcon_warning"),
|
||||||
).not.toBeVisible();
|
).not.toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should show correct shields on events sent by devices which have since been deleted", async ({
|
||||||
|
page,
|
||||||
|
app,
|
||||||
|
bot: bob,
|
||||||
|
homeserver,
|
||||||
|
}) => {
|
||||||
|
// Our app is blocked from syncing while Bob sends his messages.
|
||||||
|
await app.client.network.goOffline();
|
||||||
|
|
||||||
|
// Bob sends a message from his verified device
|
||||||
|
await bob.sendMessage(testRoomId, "test encrypted from verified");
|
||||||
|
|
||||||
|
// And one from a second, not cross-signed, device
|
||||||
|
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
|
||||||
|
await bobSecondDevice.waitForNextSync(); // make sure the client knows the room is encrypted
|
||||||
|
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from unverified");
|
||||||
|
|
||||||
|
// ... and then logs out both devices.
|
||||||
|
await bob.evaluate((cli) => cli.logout(true));
|
||||||
|
await bobSecondDevice.evaluate((cli) => cli.logout(true));
|
||||||
|
|
||||||
|
// Let our app start syncing again
|
||||||
|
await app.client.network.goOnline();
|
||||||
|
|
||||||
|
// Wait for the messages to arrive
|
||||||
|
const last = page.locator(".mx_EventTile_last");
|
||||||
|
await expect(last).toContainText("test encrypted from unverified");
|
||||||
|
const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon");
|
||||||
|
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
||||||
|
await lastE2eIcon.focus();
|
||||||
|
await expect(page.getByRole("tooltip")).toContainText("Encrypted by a device not verified by its owner.");
|
||||||
|
|
||||||
|
const penultimate = page.locator(".mx_EventTile").filter({ hasText: "test encrypted from verified" });
|
||||||
|
await expect(penultimate.locator(".mx_EventTile_e2eIcon")).not.toBeVisible();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function createSecondBotDevice(page: Page, homeserver: HomeserverInstance, bob: Bot) {
|
||||||
|
const bobSecondDevice = new Bot(page, homeserver, {
|
||||||
|
bootstrapSecretStorage: false,
|
||||||
|
bootstrapCrossSigning: false,
|
||||||
|
});
|
||||||
|
bobSecondDevice.setCredentials(await homeserver.loginUser(bob.credentials.userId, bob.credentials.password));
|
||||||
|
await bobSecondDevice.prepareClient();
|
||||||
|
return bobSecondDevice;
|
||||||
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ import { randB64Bytes } from "../../utils/rand";
|
||||||
// Docker tag to use for synapse docker image.
|
// Docker tag to use for synapse docker image.
|
||||||
// We target a specific digest as every now and then a Synapse update will break our CI.
|
// We target a specific digest as every now and then a Synapse update will break our CI.
|
||||||
// This digest is updated by the playwright-image-updates.yaml workflow periodically.
|
// This digest is updated by the playwright-image-updates.yaml workflow periodically.
|
||||||
const DOCKER_TAG = "develop@sha256:e69f01d085a69269c892dfa899cb274a593f0fbb4c518eac2b530319fa43c7cb";
|
const DOCKER_TAG = "develop@sha256:117a94ee66e4049eb6f40d04cc70d4fc83f7022dacc9871448c141e7756492f9";
|
||||||
|
|
||||||
async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise<Omit<HomeserverConfig, "dockerUrl">> {
|
async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise<Omit<HomeserverConfig, "dockerUrl">> {
|
||||||
const templateDir = path.join(__dirname, "templates", opts.template);
|
const templateDir = path.join(__dirname, "templates", opts.template);
|
||||||
|
|
|
@ -94,6 +94,7 @@
|
||||||
@import "./structures/auth/_ConfirmSessionLockTheftView.pcss";
|
@import "./structures/auth/_ConfirmSessionLockTheftView.pcss";
|
||||||
@import "./structures/auth/_Login.pcss";
|
@import "./structures/auth/_Login.pcss";
|
||||||
@import "./structures/auth/_LoginSplashView.pcss";
|
@import "./structures/auth/_LoginSplashView.pcss";
|
||||||
|
@import "./structures/auth/_MobileRegistration.pcss";
|
||||||
@import "./structures/auth/_Registration.pcss";
|
@import "./structures/auth/_Registration.pcss";
|
||||||
@import "./structures/auth/_SessionLockStolenView.pcss";
|
@import "./structures/auth/_SessionLockStolenView.pcss";
|
||||||
@import "./structures/auth/_SetupEncryptionBody.pcss";
|
@import "./structures/auth/_SetupEncryptionBody.pcss";
|
||||||
|
|
10
res/css/structures/auth/_MobileRegistration.pcss
Normal file
10
res/css/structures/auth/_MobileRegistration.pcss
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_MobileRegister_body {
|
||||||
|
padding: 32px;
|
||||||
|
}
|
|
@ -561,7 +561,6 @@ const onPinnedMessagesClick = (): void => {
|
||||||
};
|
};
|
||||||
|
|
||||||
function textForPinnedEvent(event: MatrixEvent, client: MatrixClient, allowJSX: boolean): (() => Renderable) | null {
|
function textForPinnedEvent(event: MatrixEvent, client: MatrixClient, allowJSX: boolean): (() => Renderable) | null {
|
||||||
if (!SettingsStore.getValue("feature_pinning")) return null;
|
|
||||||
const senderName = getSenderName(event);
|
const senderName = getSenderName(event);
|
||||||
const roomId = event.getRoomId()!;
|
const roomId = event.getRoomId()!;
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
import getEntryComponentForLoginType, {
|
import getEntryComponentForLoginType, {
|
||||||
ContinueKind,
|
ContinueKind,
|
||||||
|
CustomAuthType,
|
||||||
IStageComponent,
|
IStageComponent,
|
||||||
} from "../views/auth/InteractiveAuthEntryComponents";
|
} from "../views/auth/InteractiveAuthEntryComponents";
|
||||||
import Spinner from "../views/elements/Spinner";
|
import Spinner from "../views/elements/Spinner";
|
||||||
|
@ -75,11 +76,11 @@ export interface InteractiveAuthProps<T> {
|
||||||
// Called when the stage changes, or the stage's phase changes. First
|
// Called when the stage changes, or the stage's phase changes. First
|
||||||
// argument is the stage, second is the phase. Some stages do not have
|
// argument is the stage, second is the phase. Some stages do not have
|
||||||
// phases and will be counted as 0 (numeric).
|
// phases and will be counted as 0 (numeric).
|
||||||
onStagePhaseChange?(stage: AuthType | null, phase: number): void;
|
onStagePhaseChange?(stage: AuthType | CustomAuthType | null, phase: number): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
authStage?: AuthType;
|
authStage?: CustomAuthType | AuthType;
|
||||||
stageState?: IStageStatus;
|
stageState?: IStageStatus;
|
||||||
busy: boolean;
|
busy: boolean;
|
||||||
errorText?: string;
|
errorText?: string;
|
||||||
|
|
|
@ -140,7 +140,7 @@ import { cleanUpDraftsIfRequired } from "../../DraftCleaner";
|
||||||
// legacy export
|
// legacy export
|
||||||
export { default as Views } from "../../Views";
|
export { default as Views } from "../../Views";
|
||||||
|
|
||||||
const AUTH_SCREENS = ["register", "login", "forgot_password", "start_sso", "start_cas", "welcome"];
|
const AUTH_SCREENS = ["register", "mobile_register", "login", "forgot_password", "start_sso", "start_cas", "welcome"];
|
||||||
|
|
||||||
// Actions that are redirected through the onboarding process prior to being
|
// Actions that are redirected through the onboarding process prior to being
|
||||||
// re-dispatched. NOTE: some actions are non-trivial and would require
|
// re-dispatched. NOTE: some actions are non-trivial and would require
|
||||||
|
@ -189,6 +189,7 @@ interface IState {
|
||||||
register_session_id?: string;
|
register_session_id?: string;
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
register_id_sid?: string;
|
register_id_sid?: string;
|
||||||
|
isMobileRegistration?: boolean;
|
||||||
// When showing Modal dialogs we need to set aria-hidden on the root app element
|
// When showing Modal dialogs we need to set aria-hidden on the root app element
|
||||||
// and disable it when there are no dialogs
|
// and disable it when there are no dialogs
|
||||||
hideToSRUsers: boolean;
|
hideToSRUsers: boolean;
|
||||||
|
@ -243,6 +244,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
currentUserId: null,
|
currentUserId: null,
|
||||||
|
|
||||||
hideToSRUsers: false,
|
hideToSRUsers: false,
|
||||||
|
isMobileRegistration: false,
|
||||||
|
|
||||||
syncError: null, // If the current syncing status is ERROR, the error object, otherwise null.
|
syncError: null, // If the current syncing status is ERROR, the error object, otherwise null.
|
||||||
resizeNotifier: new ResizeNotifier(),
|
resizeNotifier: new ResizeNotifier(),
|
||||||
|
@ -650,6 +652,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
case "require_registration":
|
case "require_registration":
|
||||||
startAnyRegistrationFlow(payload as any);
|
startAnyRegistrationFlow(payload as any);
|
||||||
break;
|
break;
|
||||||
|
case "start_mobile_registration":
|
||||||
|
this.startRegistration(payload.params || {}, true);
|
||||||
|
break;
|
||||||
case "start_registration":
|
case "start_registration":
|
||||||
if (Lifecycle.isSoftLogout()) {
|
if (Lifecycle.isSoftLogout()) {
|
||||||
this.onSoftLogout();
|
this.onSoftLogout();
|
||||||
|
@ -946,19 +951,28 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async startRegistration(params: { [key: string]: string }): Promise<void> {
|
private async startRegistration(params: { [key: string]: string }, isMobileRegistration?: boolean): Promise<void> {
|
||||||
if (!SettingsStore.getValue(UIFeature.Registration)) {
|
if (!SettingsStore.getValue(UIFeature.Registration)) {
|
||||||
this.showScreen("welcome");
|
this.showScreen("welcome");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const isMobileRegistrationAllowed =
|
||||||
|
isMobileRegistration && SettingsStore.getValue("Registration.mobileRegistrationHelper");
|
||||||
|
|
||||||
const newState: Partial<IState> = {
|
const newState: Partial<IState> = {
|
||||||
view: Views.REGISTER,
|
view: Views.REGISTER,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only honour params if they are all present, otherwise we reset
|
if (isMobileRegistrationAllowed && params.hs_url) {
|
||||||
// HS and IS URLs when switching to registration.
|
try {
|
||||||
if (params.client_secret && params.session_id && params.hs_url && params.is_url && params.sid) {
|
const config = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(params.hs_url);
|
||||||
|
newState.serverConfig = config;
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn("Failed to load hs_url param:", params.hs_url);
|
||||||
|
}
|
||||||
|
} else if (params.client_secret && params.session_id && params.hs_url && params.is_url && params.sid) {
|
||||||
|
// Only honour params if they are all present, otherwise we reset
|
||||||
|
// HS and IS URLs when switching to registration.
|
||||||
newState.serverConfig = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(
|
newState.serverConfig = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(
|
||||||
params.hs_url,
|
params.hs_url,
|
||||||
params.is_url,
|
params.is_url,
|
||||||
|
@ -978,10 +992,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
newState.register_id_sid = params.sid;
|
newState.register_id_sid = params.sid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newState.isMobileRegistration = isMobileRegistrationAllowed;
|
||||||
|
|
||||||
this.setStateForNewView(newState);
|
this.setStateForNewView(newState);
|
||||||
ThemeController.isLogin = true;
|
ThemeController.isLogin = true;
|
||||||
this.themeWatcher.recheck();
|
this.themeWatcher.recheck();
|
||||||
this.notifyNewScreen("register");
|
this.notifyNewScreen(isMobileRegistrationAllowed ? "mobile_register" : "register");
|
||||||
}
|
}
|
||||||
|
|
||||||
// switch view to the given room
|
// switch view to the given room
|
||||||
|
@ -1721,6 +1737,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
params: params,
|
params: params,
|
||||||
});
|
});
|
||||||
PerformanceMonitor.instance.start(PerformanceEntryNames.REGISTER);
|
PerformanceMonitor.instance.start(PerformanceEntryNames.REGISTER);
|
||||||
|
} else if (screen === "mobile_register") {
|
||||||
|
dis.dispatch({
|
||||||
|
action: "start_mobile_registration",
|
||||||
|
params: params,
|
||||||
|
});
|
||||||
} else if (screen === "login") {
|
} else if (screen === "login") {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: "start_login",
|
action: "start_login",
|
||||||
|
@ -2080,6 +2101,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
onServerConfigChange={this.onServerConfigChange}
|
onServerConfigChange={this.onServerConfigChange}
|
||||||
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
|
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
|
||||||
fragmentAfterLogin={fragmentAfterLogin}
|
fragmentAfterLogin={fragmentAfterLogin}
|
||||||
|
mobileRegister={this.state.isMobileRegistration}
|
||||||
{...this.getServerProperties()}
|
{...this.getServerProperties()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -17,7 +17,6 @@ import RightPanelStore from "../../stores/right-panel/RightPanelStore";
|
||||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
|
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
|
||||||
import WidgetCard from "../views/right_panel/WidgetCard";
|
import WidgetCard from "../views/right_panel/WidgetCard";
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
|
||||||
import MemberList from "../views/rooms/MemberList";
|
import MemberList from "../views/rooms/MemberList";
|
||||||
import UserInfo from "../views/right_panel/UserInfo";
|
import UserInfo from "../views/right_panel/UserInfo";
|
||||||
import ThirdPartyMemberInfo from "../views/rooms/ThirdPartyMemberInfo";
|
import ThirdPartyMemberInfo from "../views/rooms/ThirdPartyMemberInfo";
|
||||||
|
@ -220,7 +219,7 @@ export default class RightPanel extends React.Component<Props, IState> {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case RightPanelPhases.PinnedMessages:
|
case RightPanelPhases.PinnedMessages:
|
||||||
if (!!this.props.room && SettingsStore.getValue("feature_pinning")) {
|
if (!!this.props.room) {
|
||||||
card = (
|
card = (
|
||||||
<PinnedMessagesCard
|
<PinnedMessagesCard
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
|
|
|
@ -2408,13 +2408,9 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
</AuxPanel>
|
</AuxPanel>
|
||||||
);
|
);
|
||||||
|
|
||||||
const isPinningEnabled = SettingsStore.getValue<boolean>("feature_pinning");
|
const pinnedMessageBanner = (
|
||||||
let pinnedMessageBanner;
|
<PinnedMessageBanner room={this.state.room} permalinkCreator={this.permalinkCreator} />
|
||||||
if (isPinningEnabled) {
|
);
|
||||||
pinnedMessageBanner = (
|
|
||||||
<PinnedMessageBanner room={this.state.room} permalinkCreator={this.permalinkCreator} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let messageComposer;
|
let messageComposer;
|
||||||
const showComposer =
|
const showComposer =
|
||||||
|
|
|
@ -53,6 +53,13 @@ const debuglog = (...args: any[]): void => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface MobileRegistrationResponse {
|
||||||
|
user_id: string;
|
||||||
|
home_server: string;
|
||||||
|
access_token: string;
|
||||||
|
device_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
serverConfig: ValidatedServerConfig;
|
serverConfig: ValidatedServerConfig;
|
||||||
defaultDeviceDisplayName?: string;
|
defaultDeviceDisplayName?: string;
|
||||||
|
@ -62,7 +69,7 @@ interface IProps {
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
idSid?: string;
|
idSid?: string;
|
||||||
fragmentAfterLogin?: string;
|
fragmentAfterLogin?: string;
|
||||||
|
mobileRegister?: boolean;
|
||||||
// Called when the user has logged in. Params:
|
// Called when the user has logged in. Params:
|
||||||
// - object with userId, deviceId, homeserverUrl, identityServerUrl, accessToken
|
// - object with userId, deviceId, homeserverUrl, identityServerUrl, accessToken
|
||||||
// - The user's password, if available and applicable (may be cached in memory
|
// - The user's password, if available and applicable (may be cached in memory
|
||||||
|
@ -410,18 +417,33 @@ export default class Registration extends React.Component<IProps, IState> {
|
||||||
debuglog("Registration: ui auth finished:", { hasEmail, hasAccessToken });
|
debuglog("Registration: ui auth finished:", { hasEmail, hasAccessToken });
|
||||||
// don’t log in if we found a session for a different user
|
// don’t log in if we found a session for a different user
|
||||||
if (hasAccessToken && !newState.differentLoggedInUserId) {
|
if (hasAccessToken && !newState.differentLoggedInUserId) {
|
||||||
await this.props.onLoggedIn(
|
if (this.props.mobileRegister) {
|
||||||
{
|
const mobileResponse: MobileRegistrationResponse = {
|
||||||
userId,
|
user_id: userId,
|
||||||
deviceId: (response as RegisterResponse).device_id!,
|
home_server: this.state.matrixClient.getHomeserverUrl(),
|
||||||
homeserverUrl: this.state.matrixClient.getHomeserverUrl(),
|
access_token: accessToken,
|
||||||
identityServerUrl: this.state.matrixClient.getIdentityServerUrl(),
|
device_id: (response as RegisterResponse).device_id!,
|
||||||
accessToken,
|
};
|
||||||
},
|
const event = new CustomEvent<MobileRegistrationResponse>("mobileregistrationresponse", {
|
||||||
this.state.formVals.password!,
|
detail: mobileResponse,
|
||||||
);
|
});
|
||||||
|
window.dispatchEvent(event);
|
||||||
|
newState.busy = false;
|
||||||
|
newState.completedNoSignin = true;
|
||||||
|
} else {
|
||||||
|
await this.props.onLoggedIn(
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
deviceId: (response as RegisterResponse).device_id!,
|
||||||
|
homeserverUrl: this.state.matrixClient.getHomeserverUrl(),
|
||||||
|
identityServerUrl: this.state.matrixClient.getIdentityServerUrl(),
|
||||||
|
accessToken,
|
||||||
|
},
|
||||||
|
this.state.formVals.password!,
|
||||||
|
);
|
||||||
|
|
||||||
this.setupPushers();
|
this.setupPushers();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
newState.busy = false;
|
newState.busy = false;
|
||||||
newState.completedNoSignin = true;
|
newState.completedNoSignin = true;
|
||||||
|
@ -558,7 +580,7 @@ export default class Registration extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
} else if (this.state.matrixClient && this.state.flows.length) {
|
} else if (this.state.matrixClient && this.state.flows.length) {
|
||||||
let ssoSection: JSX.Element | undefined;
|
let ssoSection: JSX.Element | undefined;
|
||||||
if (this.state.ssoFlow) {
|
if (!this.props.mobileRegister && this.state.ssoFlow) {
|
||||||
let continueWithSection;
|
let continueWithSection;
|
||||||
const providers = this.state.ssoFlow.identity_providers || [];
|
const providers = this.state.ssoFlow.identity_providers || [];
|
||||||
// when there is only a single (or 0) providers we show a wide button with `Continue with X` text
|
// when there is only a single (or 0) providers we show a wide button with `Continue with X` text
|
||||||
|
@ -591,7 +613,6 @@ export default class Registration extends React.Component<IProps, IState> {
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{ssoSection}
|
{ssoSection}
|
||||||
|
@ -660,7 +681,9 @@ export default class Registration extends React.Component<IProps, IState> {
|
||||||
let body;
|
let body;
|
||||||
if (this.state.completedNoSignin) {
|
if (this.state.completedNoSignin) {
|
||||||
let regDoneText;
|
let regDoneText;
|
||||||
if (this.state.differentLoggedInUserId) {
|
if (this.props.mobileRegister) {
|
||||||
|
regDoneText = undefined;
|
||||||
|
} else if (this.state.differentLoggedInUserId) {
|
||||||
regDoneText = (
|
regDoneText = (
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
|
@ -717,6 +740,15 @@ export default class Registration extends React.Component<IProps, IState> {
|
||||||
{regDoneText}
|
{regDoneText}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
} else if (this.props.mobileRegister) {
|
||||||
|
body = (
|
||||||
|
<Fragment>
|
||||||
|
<h1>{_t("auth|mobile_create_account_title", { hsName: this.props.serverConfig.hsName })}</h1>
|
||||||
|
{errorText}
|
||||||
|
{serverDeadSection}
|
||||||
|
{this.renderRegisterComponent()}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
body = (
|
body = (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
@ -746,7 +778,9 @@ export default class Registration extends React.Component<IProps, IState> {
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (this.props.mobileRegister) {
|
||||||
|
return <div className="mx_MobileRegister_body">{body}</div>;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<AuthPage>
|
<AuthPage>
|
||||||
<AuthHeader />
|
<AuthHeader />
|
||||||
|
|
|
@ -18,7 +18,6 @@ import DateSeparator from "../../views/messages/DateSeparator";
|
||||||
import HistoryTile from "../../views/rooms/HistoryTile";
|
import HistoryTile from "../../views/rooms/HistoryTile";
|
||||||
import EventListSummary from "../../views/elements/EventListSummary";
|
import EventListSummary from "../../views/elements/EventListSummary";
|
||||||
import { SeparatorKind } from "../../views/messages/TimelineSeparator";
|
import { SeparatorKind } from "../../views/messages/TimelineSeparator";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
|
||||||
|
|
||||||
const groupedStateEvents = [
|
const groupedStateEvents = [
|
||||||
EventType.RoomMember,
|
EventType.RoomMember,
|
||||||
|
@ -91,7 +90,7 @@ export class MainGrouper extends BaseGrouper {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ev.getType() === EventType.RoomPinnedEvents && !SettingsStore.getValue("feature_pinning")) {
|
if (ev.getType() === EventType.RoomPinnedEvents) {
|
||||||
// If pinned messages are disabled, don't show the summary
|
// If pinned messages are disabled, don't show the summary
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,8 @@ import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
import { AuthType, AuthDict, IInputs, IStageStatus } from "matrix-js-sdk/src/interactive-auth";
|
import { AuthType, AuthDict, IInputs, IStageStatus } from "matrix-js-sdk/src/interactive-auth";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import React, { ChangeEvent, createRef, FormEvent, Fragment } from "react";
|
import React, { ChangeEvent, createRef, FormEvent, Fragment } from "react";
|
||||||
|
import { Button, Text } from "@vector-im/compound-web";
|
||||||
|
import PopOutIcon from "@vector-im/compound-design-tokens/assets/web/icons/pop-out";
|
||||||
|
|
||||||
import EmailPromptIcon from "../../../../res/img/element-icons/email-prompt.svg";
|
import EmailPromptIcon from "../../../../res/img/element-icons/email-prompt.svg";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
|
@ -21,6 +23,7 @@ import AccessibleButton, { AccessibleButtonKind, ButtonEvent } from "../elements
|
||||||
import Field from "../elements/Field";
|
import Field from "../elements/Field";
|
||||||
import Spinner from "../elements/Spinner";
|
import Spinner from "../elements/Spinner";
|
||||||
import CaptchaForm from "./CaptchaForm";
|
import CaptchaForm from "./CaptchaForm";
|
||||||
|
import { Flex } from "../../utils/Flex";
|
||||||
|
|
||||||
/* This file contains a collection of components which are used by the
|
/* This file contains a collection of components which are used by the
|
||||||
* InteractiveAuth to prompt the user to enter the information needed
|
* InteractiveAuth to prompt the user to enter the information needed
|
||||||
|
@ -905,11 +908,11 @@ export class SSOAuthEntry extends React.Component<ISSOAuthEntryProps, ISSOAuthEn
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FallbackAuthEntry extends React.Component<IAuthEntryProps> {
|
export class FallbackAuthEntry<T = {}> extends React.Component<IAuthEntryProps & T> {
|
||||||
private popupWindow: Window | null;
|
protected popupWindow: Window | null;
|
||||||
private fallbackButton = createRef<HTMLButtonElement>();
|
protected fallbackButton = createRef<HTMLButtonElement>();
|
||||||
|
|
||||||
public constructor(props: IAuthEntryProps) {
|
public constructor(props: IAuthEntryProps & T) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
// we have to make the user click a button, as browsers will block
|
// we have to make the user click a button, as browsers will block
|
||||||
|
@ -967,6 +970,50 @@ export class FallbackAuthEntry extends React.Component<IAuthEntryProps> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum CustomAuthType {
|
||||||
|
// Workaround for MAS requiring non-UIA authentication for resetting cross-signing.
|
||||||
|
MasCrossSigningReset = "org.matrix.cross_signing_reset",
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MasUnlockCrossSigningAuthEntry extends FallbackAuthEntry<{
|
||||||
|
stageParams?: {
|
||||||
|
url?: string;
|
||||||
|
};
|
||||||
|
}> {
|
||||||
|
public static LOGIN_TYPE = CustomAuthType.MasCrossSigningReset;
|
||||||
|
|
||||||
|
private onGoToAccountClick = (): void => {
|
||||||
|
if (!this.props.stageParams?.url) return;
|
||||||
|
this.popupWindow = window.open(this.props.stageParams.url, "_blank");
|
||||||
|
};
|
||||||
|
|
||||||
|
private onRetryClick = (): void => {
|
||||||
|
this.props.submitAuthDict({});
|
||||||
|
};
|
||||||
|
|
||||||
|
public render(): React.ReactNode {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text>{_t("auth|uia|mas_cross_signing_reset_description")}</Text>
|
||||||
|
<Flex gap="var(--cpd-space-4x)">
|
||||||
|
<Button
|
||||||
|
Icon={PopOutIcon}
|
||||||
|
onClick={this.onGoToAccountClick}
|
||||||
|
autoFocus
|
||||||
|
kind="primary"
|
||||||
|
className="mx_Dialog_nonDialogButton"
|
||||||
|
>
|
||||||
|
{_t("auth|uia|mas_cross_signing_reset_cta")}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={this.onRetryClick} kind="secondary" className="mx_Dialog_nonDialogButton">
|
||||||
|
{_t("action|retry")}
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface IStageComponentProps extends IAuthEntryProps {
|
export interface IStageComponentProps extends IAuthEntryProps {
|
||||||
stageParams?: Record<string, any>;
|
stageParams?: Record<string, any>;
|
||||||
inputs?: IInputs;
|
inputs?: IInputs;
|
||||||
|
@ -983,8 +1030,10 @@ export interface IStageComponent extends React.ComponentClass<React.PropsWithRef
|
||||||
focus?(): void;
|
focus?(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function getEntryComponentForLoginType(loginType: AuthType): IStageComponent {
|
export default function getEntryComponentForLoginType(loginType: AuthType | CustomAuthType): IStageComponent {
|
||||||
switch (loginType) {
|
switch (loginType) {
|
||||||
|
case CustomAuthType.MasCrossSigningReset:
|
||||||
|
return MasUnlockCrossSigningAuthEntry;
|
||||||
case AuthType.Password:
|
case AuthType.Password:
|
||||||
return PasswordAuthEntry;
|
return PasswordAuthEntry;
|
||||||
case AuthType.Recaptcha:
|
case AuthType.Recaptcha:
|
||||||
|
|
|
@ -26,7 +26,6 @@ import { EchoChamber } from "../../../stores/local-echo/EchoChamber";
|
||||||
import { RoomNotifState } from "../../../RoomNotifs";
|
import { RoomNotifState } from "../../../RoomNotifs";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import ExportDialog from "../dialogs/ExportDialog";
|
import ExportDialog from "../dialogs/ExportDialog";
|
||||||
import { useFeatureEnabled } from "../../../hooks/useSettings";
|
|
||||||
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
||||||
import { RoomSettingsTab } from "../dialogs/RoomSettingsDialog";
|
import { RoomSettingsTab } from "../dialogs/RoomSettingsDialog";
|
||||||
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
|
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||||
|
@ -261,11 +260,10 @@ const RoomContextMenu: React.FC<IProps> = ({ room, onFinished, ...props }) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pinningEnabled = useFeatureEnabled("feature_pinning");
|
const pinCount = usePinnedEvents(room).length;
|
||||||
const pinCount = usePinnedEvents(pinningEnabled ? room : undefined)?.length;
|
|
||||||
|
|
||||||
let pinsOption: JSX.Element | undefined;
|
let pinsOption: JSX.Element | undefined;
|
||||||
if (pinningEnabled && !isVideoRoom) {
|
if (!isVideoRoom) {
|
||||||
pinsOption = (
|
pinsOption = (
|
||||||
<IconizedContextMenuOption
|
<IconizedContextMenuOption
|
||||||
onClick={(ev: ButtonEvent) => {
|
onClick={(ev: ButtonEvent) => {
|
||||||
|
|
|
@ -21,7 +21,6 @@ import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePha
|
||||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||||
import { showThreadPanel } from "../../../dispatcher/dispatch-actions/threads";
|
import { showThreadPanel } from "../../../dispatcher/dispatch-actions/threads";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
|
||||||
import {
|
import {
|
||||||
RoomNotificationStateStore,
|
RoomNotificationStateStore,
|
||||||
UPDATE_STATUS_INDICATOR,
|
UPDATE_STATUS_INDICATOR,
|
||||||
|
@ -245,17 +244,16 @@ export default class LegacyRoomHeaderButtons extends HeaderButtons<IProps> {
|
||||||
|
|
||||||
const rightPanelPhaseButtons: Map<RightPanelPhases, any> = new Map();
|
const rightPanelPhaseButtons: Map<RightPanelPhases, any> = new Map();
|
||||||
|
|
||||||
if (SettingsStore.getValue("feature_pinning")) {
|
rightPanelPhaseButtons.set(
|
||||||
rightPanelPhaseButtons.set(
|
RightPanelPhases.PinnedMessages,
|
||||||
RightPanelPhases.PinnedMessages,
|
<PinnedMessagesHeaderButton
|
||||||
<PinnedMessagesHeaderButton
|
key="pinnedMessagesButton"
|
||||||
key="pinnedMessagesButton"
|
room={this.props.room}
|
||||||
room={this.props.room}
|
isHighlighted={this.isPhase(RightPanelPhases.PinnedMessages)}
|
||||||
isHighlighted={this.isPhase(RightPanelPhases.PinnedMessages)}
|
onClick={this.onPinnedMessagesClicked}
|
||||||
onClick={this.onPinnedMessagesClicked}
|
/>,
|
||||||
/>,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
rightPanelPhaseButtons.set(
|
rightPanelPhaseButtons.set(
|
||||||
RightPanelPhases.Timeline,
|
RightPanelPhases.Timeline,
|
||||||
<TimelineCardHeaderButton
|
<TimelineCardHeaderButton
|
||||||
|
|
|
@ -49,7 +49,6 @@ import { useEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||||
import { E2EStatus } from "../../../utils/ShieldUtils";
|
import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||||
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||||
import { useFeatureEnabled } from "../../../hooks/useSettings";
|
|
||||||
import RoomName from "../elements/RoomName";
|
import RoomName from "../elements/RoomName";
|
||||||
import ExportDialog from "../dialogs/ExportDialog";
|
import ExportDialog from "../dialogs/ExportDialog";
|
||||||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||||
|
@ -73,6 +72,7 @@ import { Key } from "../../../Keyboard";
|
||||||
import { useTransition } from "../../../hooks/useTransition";
|
import { useTransition } from "../../../hooks/useTransition";
|
||||||
import { useIsVideoRoom } from "../../../utils/video-rooms";
|
import { useIsVideoRoom } from "../../../utils/video-rooms";
|
||||||
import { usePinnedEvents } from "../../../hooks/usePinnedEvents";
|
import { usePinnedEvents } from "../../../hooks/usePinnedEvents";
|
||||||
|
import { ReleaseAnnouncement } from "../../structures/ReleaseAnnouncement.tsx";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -314,8 +314,7 @@ const RoomSummaryCard: React.FC<IProps> = ({
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
|
||||||
const pinningEnabled = useFeatureEnabled("feature_pinning");
|
const pinCount = usePinnedEvents(room).length;
|
||||||
const pinCount = usePinnedEvents(pinningEnabled ? room : undefined)?.length;
|
|
||||||
|
|
||||||
const roomTags = useEventEmitterState(RoomListStore.instance, LISTS_UPDATE_EVENT, () =>
|
const roomTags = useEventEmitterState(RoomListStore.instance, LISTS_UPDATE_EVENT, () =>
|
||||||
RoomListStore.instance.getTagsForRoom(room),
|
RoomListStore.instance.getTagsForRoom(room),
|
||||||
|
@ -382,17 +381,25 @@ const RoomSummaryCard: React.FC<IProps> = ({
|
||||||
|
|
||||||
{!isVideoRoom && (
|
{!isVideoRoom && (
|
||||||
<>
|
<>
|
||||||
{pinningEnabled && (
|
<ReleaseAnnouncement
|
||||||
<MenuItem
|
feature="pinningMessageList"
|
||||||
Icon={PinIcon}
|
header={_t("right_panel|pinned_messages|release_announcement|title")}
|
||||||
label={_t("right_panel|pinned_messages_button")}
|
description={_t("right_panel|pinned_messages|release_announcement|description")}
|
||||||
onSelect={onRoomPinsClick}
|
closeLabel={_t("right_panel|pinned_messages|release_announcement|close")}
|
||||||
>
|
placement="top"
|
||||||
<Text as="span" size="sm">
|
>
|
||||||
{pinCount}
|
<div>
|
||||||
</Text>
|
<MenuItem
|
||||||
</MenuItem>
|
Icon={PinIcon}
|
||||||
)}
|
label={_t("right_panel|pinned_messages_button")}
|
||||||
|
onSelect={onRoomPinsClick}
|
||||||
|
>
|
||||||
|
<Text as="span" size="sm">
|
||||||
|
{pinCount}
|
||||||
|
</Text>
|
||||||
|
</MenuItem>
|
||||||
|
</div>
|
||||||
|
</ReleaseAnnouncement>
|
||||||
<MenuItem Icon={FilesIcon} label={_t("right_panel|files_button")} onSelect={onRoomFilesClick} />
|
<MenuItem Icon={FilesIcon} label={_t("right_panel|files_button")} onSelect={onRoomFilesClick} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -267,15 +267,13 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
||||||
[EventType.RoomServerAcl]: _td("room_settings|permissions|m.room.server_acl"),
|
[EventType.RoomServerAcl]: _td("room_settings|permissions|m.room.server_acl"),
|
||||||
[EventType.Reaction]: _td("room_settings|permissions|m.reaction"),
|
[EventType.Reaction]: _td("room_settings|permissions|m.reaction"),
|
||||||
[EventType.RoomRedaction]: _td("room_settings|permissions|m.room.redaction"),
|
[EventType.RoomRedaction]: _td("room_settings|permissions|m.room.redaction"),
|
||||||
|
[EventType.RoomPinnedEvents]: _td("room_settings|permissions|m.room.pinned_events"),
|
||||||
|
|
||||||
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
|
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
|
||||||
"im.vector.modular.widgets": isSpaceRoom ? null : _td("room_settings|permissions|m.widget"),
|
"im.vector.modular.widgets": isSpaceRoom ? null : _td("room_settings|permissions|m.widget"),
|
||||||
[VoiceBroadcastInfoEventType]: _td("room_settings|permissions|io.element.voice_broadcast_info"),
|
[VoiceBroadcastInfoEventType]: _td("room_settings|permissions|io.element.voice_broadcast_info"),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (SettingsStore.getValue("feature_pinning")) {
|
|
||||||
plEventsToLabels[EventType.RoomPinnedEvents] = _td("room_settings|permissions|m.room.pinned_events");
|
|
||||||
}
|
|
||||||
// MSC3401: Native Group VoIP signaling
|
// MSC3401: Native Group VoIP signaling
|
||||||
if (SettingsStore.getValue("feature_group_calls")) {
|
if (SettingsStore.getValue("feature_group_calls")) {
|
||||||
plEventsToLabels[ElementCall.CALL_EVENT_TYPE.name] = _td("room_settings|permissions|m.call");
|
plEventsToLabels[ElementCall.CALL_EVENT_TYPE.name] = _td("room_settings|permissions|m.call");
|
||||||
|
|
|
@ -29,7 +29,7 @@ import { MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix";
|
||||||
*/
|
*/
|
||||||
export const useUserTimezone = (cli: MatrixClient, userId: string): { timezone: string; friendly: string } | null => {
|
export const useUserTimezone = (cli: MatrixClient, userId: string): { timezone: string; friendly: string } | null => {
|
||||||
const [timezone, setTimezone] = useState<string>();
|
const [timezone, setTimezone] = useState<string>();
|
||||||
const [updateInterval, setUpdateInterval] = useState<number>();
|
const [updateInterval, setUpdateInterval] = useState<ReturnType<typeof setTimeout>>();
|
||||||
const [friendly, setFriendly] = useState<string>();
|
const [friendly, setFriendly] = useState<string>();
|
||||||
const [supported, setSupported] = useState<boolean>();
|
const [supported, setSupported] = useState<boolean>();
|
||||||
|
|
||||||
|
|
|
@ -229,6 +229,7 @@
|
||||||
},
|
},
|
||||||
"misconfigured_body": "Ask your %(brand)s admin to check <a>your config</a> for incorrect or duplicate entries.",
|
"misconfigured_body": "Ask your %(brand)s admin to check <a>your config</a> for incorrect or duplicate entries.",
|
||||||
"misconfigured_title": "Your %(brand)s is misconfigured",
|
"misconfigured_title": "Your %(brand)s is misconfigured",
|
||||||
|
"mobile_create_account_title": "You're about to create an account on %(hsName)s",
|
||||||
"msisdn_field_description": "Other users can invite you to rooms using your contact details",
|
"msisdn_field_description": "Other users can invite you to rooms using your contact details",
|
||||||
"msisdn_field_label": "Phone",
|
"msisdn_field_label": "Phone",
|
||||||
"msisdn_field_number_invalid": "That phone number doesn't look quite right, please check and try again",
|
"msisdn_field_number_invalid": "That phone number doesn't look quite right, please check and try again",
|
||||||
|
@ -369,6 +370,8 @@
|
||||||
"email_resend_prompt": "Did not receive it? <a>Resend it</a>",
|
"email_resend_prompt": "Did not receive it? <a>Resend it</a>",
|
||||||
"email_resent": "Resent!",
|
"email_resent": "Resent!",
|
||||||
"fallback_button": "Start authentication",
|
"fallback_button": "Start authentication",
|
||||||
|
"mas_cross_signing_reset_cta": "Go to your account",
|
||||||
|
"mas_cross_signing_reset_description": "Reset your identity through your account provider and then come back and click “Retry”.",
|
||||||
"msisdn": "A text message has been sent to %(msisdn)s",
|
"msisdn": "A text message has been sent to %(msisdn)s",
|
||||||
"msisdn_token_incorrect": "Token incorrect",
|
"msisdn_token_incorrect": "Token incorrect",
|
||||||
"msisdn_token_prompt": "Please enter the code it contains:",
|
"msisdn_token_prompt": "Please enter the code it contains:",
|
||||||
|
@ -1465,7 +1468,6 @@
|
||||||
"notifications": "Enable the notifications panel in the room header",
|
"notifications": "Enable the notifications panel in the room header",
|
||||||
"oidc_native_flow": "OIDC native authentication",
|
"oidc_native_flow": "OIDC native authentication",
|
||||||
"oidc_native_flow_description": "⚠ WARNING: Experimental. Use OIDC native authentication when supported by the server.",
|
"oidc_native_flow_description": "⚠ WARNING: Experimental. Use OIDC native authentication when supported by the server.",
|
||||||
"pinning": "Message Pinning",
|
|
||||||
"release_announcement": "Release announcement",
|
"release_announcement": "Release announcement",
|
||||||
"render_reaction_images": "Render custom images in reactions",
|
"render_reaction_images": "Render custom images in reactions",
|
||||||
"render_reaction_images_description": "Sometimes referred to as \"custom emojis\".",
|
"render_reaction_images_description": "Sometimes referred to as \"custom emojis\".",
|
||||||
|
@ -1851,6 +1853,11 @@
|
||||||
"other": "You can only pin up to %(count)s widgets"
|
"other": "You can only pin up to %(count)s widgets"
|
||||||
},
|
},
|
||||||
"menu": "Open menu",
|
"menu": "Open menu",
|
||||||
|
"release_announcement": {
|
||||||
|
"close": "Ok",
|
||||||
|
"description": "Find all pinned messages here. Rollover any message and select “Pin” to add it.",
|
||||||
|
"title": "All new pinned messages"
|
||||||
|
},
|
||||||
"reply_thread": "Reply to a <link>thread message</link>",
|
"reply_thread": "Reply to a <link>thread message</link>",
|
||||||
"title": "Pinned messages",
|
"title": "Pinned messages",
|
||||||
"unpin_all": {
|
"unpin_all": {
|
||||||
|
|
|
@ -275,14 +275,6 @@ export const SETTINGS: { [setting: string]: ISetting } = {
|
||||||
supportedLevelsAreOrdered: true,
|
supportedLevelsAreOrdered: true,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
"feature_pinning": {
|
|
||||||
isFeature: true,
|
|
||||||
labsGroup: LabGroup.Messaging,
|
|
||||||
displayName: _td("labs|pinning"),
|
|
||||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG_PRIORITISED,
|
|
||||||
supportedLevelsAreOrdered: true,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
"feature_wysiwyg_composer": {
|
"feature_wysiwyg_composer": {
|
||||||
isFeature: true,
|
isFeature: true,
|
||||||
labsGroup: LabGroup.Messaging,
|
labsGroup: LabGroup.Messaging,
|
||||||
|
@ -876,6 +868,10 @@ export const SETTINGS: { [setting: string]: ISetting } = {
|
||||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
"Registration.mobileRegistrationHelper": {
|
||||||
|
supportedLevels: [SettingLevel.CONFIG],
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
"autocompleteDelay": {
|
"autocompleteDelay": {
|
||||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
|
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
|
||||||
default: 200,
|
default: 200,
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { Features } from "../settings/Settings";
|
||||||
/**
|
/**
|
||||||
* The features are shown in the array order.
|
* The features are shown in the array order.
|
||||||
*/
|
*/
|
||||||
const FEATURES = ["threadsActivityCentre"] as const;
|
const FEATURES = ["threadsActivityCentre", "pinningMessageList"] as const;
|
||||||
/**
|
/**
|
||||||
* All the features that can be shown in the release announcements.
|
* All the features that can be shown in the release announcements.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -9,7 +9,6 @@ Please see LICENSE files in the repository root for full details.
|
||||||
import { MatrixEvent, EventType, M_POLL_START, MatrixClient, EventTimeline, Room } from "matrix-js-sdk/src/matrix";
|
import { MatrixEvent, EventType, M_POLL_START, MatrixClient, EventTimeline, Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import { isContentActionable } from "./EventUtils";
|
import { isContentActionable } from "./EventUtils";
|
||||||
import SettingsStore from "../settings/SettingsStore";
|
|
||||||
import { ReadPinsEventId } from "../components/views/right_panel/types";
|
import { ReadPinsEventId } from "../components/views/right_panel/types";
|
||||||
|
|
||||||
export default class PinningUtils {
|
export default class PinningUtils {
|
||||||
|
@ -70,7 +69,6 @@ export default class PinningUtils {
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private static canPinOrUnpin(matrixClient: MatrixClient, mxEvent: MatrixEvent): boolean {
|
private static canPinOrUnpin(matrixClient: MatrixClient, mxEvent: MatrixEvent): boolean {
|
||||||
if (!SettingsStore.getValue("feature_pinning")) return false;
|
|
||||||
if (!isContentActionable(mxEvent)) return false;
|
if (!isContentActionable(mxEvent)) return false;
|
||||||
|
|
||||||
const room = matrixClient.getRoom(mxEvent.getRoomId());
|
const room = matrixClient.getRoom(mxEvent.getRoomId());
|
||||||
|
|
|
@ -65,11 +65,6 @@ describe("TextForEvent", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("TextForPinnedEvent", () => {
|
describe("TextForPinnedEvent", () => {
|
||||||
beforeAll(() => {
|
|
||||||
// enable feature_pinning setting
|
|
||||||
(SettingsStore.getValue as jest.Mock).mockImplementation((feature) => feature === "feature_pinning");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("mentions message when a single message was pinned, with no previously pinned messages", () => {
|
it("mentions message when a single message was pinned, with no previously pinned messages", () => {
|
||||||
const event = mockPinnedEvent(["message-1"]);
|
const event = mockPinnedEvent(["message-1"]);
|
||||||
const plainText = textForEvent(event, mockClient);
|
const plainText = textForEvent(event, mockClient);
|
||||||
|
|
|
@ -7,11 +7,14 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { render, screen, waitFor, act } from "@testing-library/react";
|
import { render, screen, waitFor, act, fireEvent } from "@testing-library/react";
|
||||||
import { AuthType } from "matrix-js-sdk/src/interactive-auth";
|
import { AuthType } from "matrix-js-sdk/src/interactive-auth";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
|
|
||||||
import { EmailIdentityAuthEntry } from "../../../../src/components/views/auth/InteractiveAuthEntryComponents";
|
import {
|
||||||
|
EmailIdentityAuthEntry,
|
||||||
|
MasUnlockCrossSigningAuthEntry,
|
||||||
|
} from "../../../../src/components/views/auth/InteractiveAuthEntryComponents";
|
||||||
import { createTestClient } from "../../../test-utils";
|
import { createTestClient } from "../../../test-utils";
|
||||||
|
|
||||||
describe("<EmailIdentityAuthEntry/>", () => {
|
describe("<EmailIdentityAuthEntry/>", () => {
|
||||||
|
@ -55,3 +58,44 @@ describe("<EmailIdentityAuthEntry/>", () => {
|
||||||
await waitFor(() => expect(screen.queryByRole("button", { name: "Resend" })).toBeInTheDocument());
|
await waitFor(() => expect(screen.queryByRole("button", { name: "Resend" })).toBeInTheDocument());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("<MasUnlockCrossSigningAuthEntry/>", () => {
|
||||||
|
const renderAuth = (props = {}) => {
|
||||||
|
const matrixClient = createTestClient();
|
||||||
|
|
||||||
|
return render(
|
||||||
|
<MasUnlockCrossSigningAuthEntry
|
||||||
|
matrixClient={matrixClient}
|
||||||
|
loginType={AuthType.Email}
|
||||||
|
onPhaseChange={jest.fn()}
|
||||||
|
submitAuthDict={jest.fn()}
|
||||||
|
fail={jest.fn()}
|
||||||
|
clientSecret="my secret"
|
||||||
|
showContinue={true}
|
||||||
|
stageParams={{ url: "https://example.com" }}
|
||||||
|
{...props}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
test("should render", () => {
|
||||||
|
const { container } = renderAuth();
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should open idp in new tab on click", async () => {
|
||||||
|
const spy = jest.spyOn(global.window, "open");
|
||||||
|
renderAuth();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Go to your account" }));
|
||||||
|
expect(spy).toHaveBeenCalledWith("https://example.com", "_blank");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should retry uia request on click", async () => {
|
||||||
|
const submitAuthDict = jest.fn();
|
||||||
|
renderAuth({ submitAuthDict });
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Retry" }));
|
||||||
|
expect(submitAuthDict).toHaveBeenCalledWith({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -32,3 +32,53 @@ exports[`<EmailIdentityAuthEntry/> should render 1`] = `
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`<MasUnlockCrossSigningAuthEntry/> should render 1`] = `
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<p
|
||||||
|
class="_typography_yh5dq_162 _font-body-md-regular_yh5dq_59"
|
||||||
|
>
|
||||||
|
Reset your identity through your account provider and then come back and click “Retry”.
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
class="mx_Flex"
|
||||||
|
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-4x);"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="_button_zt6rp_17 mx_Dialog_nonDialogButton _has-icon_zt6rp_61"
|
||||||
|
data-kind="primary"
|
||||||
|
data-size="lg"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="currentColor"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M5 3h6a1 1 0 1 1 0 2H5v14h14v-6a1 1 0 1 1 2 0v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M15 3h5a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0V6.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L17.586 5H15a1 1 0 1 1 0-2Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Go to your account
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="_button_zt6rp_17 mx_Dialog_nonDialogButton"
|
||||||
|
data-kind="secondary"
|
||||||
|
data-size="lg"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
|
@ -116,22 +116,6 @@ describe("MessageContextMenu", () => {
|
||||||
expect(screen.queryByRole("menuitem", { name: "Pin" })).toBeFalsy();
|
expect(screen.queryByRole("menuitem", { name: "Pin" })).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not show pin option when pinning feature is disabled", () => {
|
|
||||||
const eventContent = createMessageEventContent("hello");
|
|
||||||
const pinnableEvent = new MatrixEvent({
|
|
||||||
type: EventType.RoomMessage,
|
|
||||||
content: eventContent,
|
|
||||||
room_id: roomId,
|
|
||||||
});
|
|
||||||
|
|
||||||
// disable pinning feature
|
|
||||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
|
|
||||||
|
|
||||||
createMenu(pinnableEvent, { rightClick: true }, {}, undefined, room);
|
|
||||||
|
|
||||||
expect(screen.queryByRole("menuitem", { name: "Pin" })).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows pin option when pinning feature is enabled", () => {
|
it("shows pin option when pinning feature is enabled", () => {
|
||||||
const eventContent = createMessageEventContent("hello");
|
const eventContent = createMessageEventContent("hello");
|
||||||
const pinnableEvent = new MatrixEvent({
|
const pinnableEvent = new MatrixEvent({
|
||||||
|
|
|
@ -259,8 +259,7 @@ describe("<RoomSummaryCard />", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("pinning", () => {
|
describe("pinning", () => {
|
||||||
it("renders pins options when pinning feature is enabled", () => {
|
it("renders pins options", () => {
|
||||||
mocked(settingsHooks.useFeatureEnabled).mockImplementation((feature) => feature === "feature_pinning");
|
|
||||||
const { getByText } = getComponent();
|
const { getByText } = getComponent();
|
||||||
|
|
||||||
expect(getByText("Pinned messages")).toBeInTheDocument();
|
expect(getByText("Pinned messages")).toBeInTheDocument();
|
||||||
|
@ -291,9 +290,7 @@ describe("<RoomSummaryCard />", () => {
|
||||||
describe("video rooms", () => {
|
describe("video rooms", () => {
|
||||||
it("does not render irrelevant options for element video room", () => {
|
it("does not render irrelevant options for element video room", () => {
|
||||||
jest.spyOn(room, "isElementVideoRoom").mockReturnValue(true);
|
jest.spyOn(room, "isElementVideoRoom").mockReturnValue(true);
|
||||||
mocked(settingsHooks.useFeatureEnabled).mockImplementation(
|
mocked(settingsHooks.useFeatureEnabled).mockImplementation((feature) => feature === "feature_video_rooms");
|
||||||
(feature) => feature === "feature_video_rooms" || feature === "feature_pinning",
|
|
||||||
);
|
|
||||||
const { queryByText } = getComponent();
|
const { queryByText } = getComponent();
|
||||||
|
|
||||||
// options not rendered
|
// options not rendered
|
||||||
|
@ -305,10 +302,7 @@ describe("<RoomSummaryCard />", () => {
|
||||||
it("does not render irrelevant options for element call room", () => {
|
it("does not render irrelevant options for element call room", () => {
|
||||||
jest.spyOn(room, "isCallRoom").mockReturnValue(true);
|
jest.spyOn(room, "isCallRoom").mockReturnValue(true);
|
||||||
mocked(settingsHooks.useFeatureEnabled).mockImplementation(
|
mocked(settingsHooks.useFeatureEnabled).mockImplementation(
|
||||||
(feature) =>
|
(feature) => feature === "feature_element_call_video_rooms" || feature === "feature_video_rooms",
|
||||||
feature === "feature_element_call_video_rooms" ||
|
|
||||||
feature === "feature_video_rooms" ||
|
|
||||||
feature === "feature_pinning",
|
|
||||||
);
|
);
|
||||||
const { queryByText } = getComponent();
|
const { queryByText } = getComponent();
|
||||||
|
|
||||||
|
|
|
@ -186,6 +186,55 @@ exports[`<RoomSummaryCard /> has button to edit topic 1`] = `
|
||||||
data-orientation="horizontal"
|
data-orientation="horizontal"
|
||||||
role="separator"
|
role="separator"
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-haspopup="dialog"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="_item_1gwvj_17 _interactive_1gwvj_36"
|
||||||
|
data-kind="primary"
|
||||||
|
role="menuitem"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="_icon_1gwvj_44"
|
||||||
|
fill="currentColor"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M6.119 2a.5.5 0 0 0-.35.857L7.85 4.9a.5.5 0 0 1 .15.357v4.487a.5.5 0 0 1-.15.356l-3.7 3.644A.5.5 0 0 0 4 14.1v1.4a.5.5 0 0 0 .5.5H11v6a1 1 0 1 0 2 0v-6h6.5a.5.5 0 0 0 .5-.5v-1.4a.5.5 0 0 0-.15-.356l-3.7-3.644a.5.5 0 0 1-.15-.356V5.257a.5.5 0 0 1 .15-.357l2.081-2.043a.5.5 0 0 0-.35-.857H6.119ZM10 4h4v5.744a2.5 2.5 0 0 0 .746 1.781L17.26 14H6.74l2.514-2.475A2.5 2.5 0 0 0 10 9.744V4Z"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span
|
||||||
|
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 _label_1gwvj_53"
|
||||||
|
>
|
||||||
|
Pinned messages
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="_nav-hint_1gwvj_60"
|
||||||
|
fill="currentColor"
|
||||||
|
height="24"
|
||||||
|
viewBox="8 0 8 24"
|
||||||
|
width="8"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8.7 17.3a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7l3.9-3.9-3.9-3.9a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l4.6 4.6c.1.1.17.208.213.325.041.117.062.242.062.375s-.02.258-.063.375a.877.877 0 0 1-.212.325l-4.6 4.6a.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span
|
||||||
|
class="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40"
|
||||||
|
>
|
||||||
|
0
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
class="_item_1gwvj_17 _interactive_1gwvj_36"
|
class="_item_1gwvj_17 _interactive_1gwvj_36"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
|
@ -584,6 +633,55 @@ exports[`<RoomSummaryCard /> renders the room summary 1`] = `
|
||||||
data-orientation="horizontal"
|
data-orientation="horizontal"
|
||||||
role="separator"
|
role="separator"
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-haspopup="dialog"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="_item_1gwvj_17 _interactive_1gwvj_36"
|
||||||
|
data-kind="primary"
|
||||||
|
role="menuitem"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="_icon_1gwvj_44"
|
||||||
|
fill="currentColor"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M6.119 2a.5.5 0 0 0-.35.857L7.85 4.9a.5.5 0 0 1 .15.357v4.487a.5.5 0 0 1-.15.356l-3.7 3.644A.5.5 0 0 0 4 14.1v1.4a.5.5 0 0 0 .5.5H11v6a1 1 0 1 0 2 0v-6h6.5a.5.5 0 0 0 .5-.5v-1.4a.5.5 0 0 0-.15-.356l-3.7-3.644a.5.5 0 0 1-.15-.356V5.257a.5.5 0 0 1 .15-.357l2.081-2.043a.5.5 0 0 0-.35-.857H6.119ZM10 4h4v5.744a2.5 2.5 0 0 0 .746 1.781L17.26 14H6.74l2.514-2.475A2.5 2.5 0 0 0 10 9.744V4Z"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span
|
||||||
|
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 _label_1gwvj_53"
|
||||||
|
>
|
||||||
|
Pinned messages
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="_nav-hint_1gwvj_60"
|
||||||
|
fill="currentColor"
|
||||||
|
height="24"
|
||||||
|
viewBox="8 0 8 24"
|
||||||
|
width="8"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8.7 17.3a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7l3.9-3.9-3.9-3.9a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l4.6 4.6c.1.1.17.208.213.325.041.117.062.242.062.375s-.02.258-.063.375a.877.877 0 0 1-.212.325l-4.6 4.6a.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span
|
||||||
|
class="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40"
|
||||||
|
>
|
||||||
|
0
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
class="_item_1gwvj_17 _interactive_1gwvj_36"
|
class="_item_1gwvj_17 _interactive_1gwvj_36"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
|
@ -1009,6 +1107,55 @@ exports[`<RoomSummaryCard /> renders the room topic in the summary 1`] = `
|
||||||
data-orientation="horizontal"
|
data-orientation="horizontal"
|
||||||
role="separator"
|
role="separator"
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-haspopup="dialog"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="_item_1gwvj_17 _interactive_1gwvj_36"
|
||||||
|
data-kind="primary"
|
||||||
|
role="menuitem"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="_icon_1gwvj_44"
|
||||||
|
fill="currentColor"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M6.119 2a.5.5 0 0 0-.35.857L7.85 4.9a.5.5 0 0 1 .15.357v4.487a.5.5 0 0 1-.15.356l-3.7 3.644A.5.5 0 0 0 4 14.1v1.4a.5.5 0 0 0 .5.5H11v6a1 1 0 1 0 2 0v-6h6.5a.5.5 0 0 0 .5-.5v-1.4a.5.5 0 0 0-.15-.356l-3.7-3.644a.5.5 0 0 1-.15-.356V5.257a.5.5 0 0 1 .15-.357l2.081-2.043a.5.5 0 0 0-.35-.857H6.119ZM10 4h4v5.744a2.5 2.5 0 0 0 .746 1.781L17.26 14H6.74l2.514-2.475A2.5 2.5 0 0 0 10 9.744V4Z"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span
|
||||||
|
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 _label_1gwvj_53"
|
||||||
|
>
|
||||||
|
Pinned messages
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="_nav-hint_1gwvj_60"
|
||||||
|
fill="currentColor"
|
||||||
|
height="24"
|
||||||
|
viewBox="8 0 8 24"
|
||||||
|
width="8"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8.7 17.3a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7l3.9-3.9-3.9-3.9a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l4.6 4.6c.1.1.17.208.213.325.041.117.062.242.062.375s-.02.258-.063.375a.877.877 0 0 1-.212.325l-4.6 4.6a.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span
|
||||||
|
class="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40"
|
||||||
|
>
|
||||||
|
0
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
class="_item_1gwvj_17 _interactive_1gwvj_36"
|
class="_item_1gwvj_17 _interactive_1gwvj_36"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
|
|
|
@ -19,7 +19,6 @@ import dis from "../../../../src/dispatcher/dispatcher";
|
||||||
import { Action } from "../../../../src/dispatcher/actions";
|
import { Action } from "../../../../src/dispatcher/actions";
|
||||||
import { getForwardableEvent } from "../../../../src/events";
|
import { getForwardableEvent } from "../../../../src/events";
|
||||||
import { createRedactEventDialog } from "../../../../src/components/views/dialogs/ConfirmRedactDialog";
|
import { createRedactEventDialog } from "../../../../src/components/views/dialogs/ConfirmRedactDialog";
|
||||||
import SettingsStore from "../../../../src/settings/SettingsStore.ts";
|
|
||||||
|
|
||||||
jest.mock("../../../../src/components/views/dialogs/ConfirmRedactDialog", () => ({
|
jest.mock("../../../../src/components/views/dialogs/ConfirmRedactDialog", () => ({
|
||||||
createRedactEventDialog: jest.fn(),
|
createRedactEventDialog: jest.fn(),
|
||||||
|
@ -38,8 +37,6 @@ describe("<PinnedEventTile />", () => {
|
||||||
permalinkCreator = new RoomPermalinkCreator(room);
|
permalinkCreator = new RoomPermalinkCreator(room);
|
||||||
mockClient.getRoom = jest.fn().mockReturnValue(room);
|
mockClient.getRoom = jest.fn().mockReturnValue(room);
|
||||||
jest.spyOn(dis, "dispatch").mockReturnValue(undefined);
|
jest.spyOn(dis, "dispatch").mockReturnValue(undefined);
|
||||||
// Enable feature_pinning
|
|
||||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -89,7 +89,13 @@ describe("ReleaseAnnouncementStore", () => {
|
||||||
// Sanity check
|
// Sanity check
|
||||||
expect(releaseAnnouncementStore.getReleaseAnnouncement()).toBe("threadsActivityCentre");
|
expect(releaseAnnouncementStore.getReleaseAnnouncement()).toBe("threadsActivityCentre");
|
||||||
|
|
||||||
const promise = listenReleaseAnnouncementChanged();
|
let promise = listenReleaseAnnouncementChanged();
|
||||||
|
await releaseAnnouncementStore.nextReleaseAnnouncement();
|
||||||
|
|
||||||
|
expect(await promise).toBe("pinningMessageList");
|
||||||
|
expect(releaseAnnouncementStore.getReleaseAnnouncement()).toBe("pinningMessageList");
|
||||||
|
|
||||||
|
promise = listenReleaseAnnouncementChanged();
|
||||||
await releaseAnnouncementStore.nextReleaseAnnouncement();
|
await releaseAnnouncementStore.nextReleaseAnnouncement();
|
||||||
|
|
||||||
expect(await promise).toBeNull();
|
expect(await promise).toBeNull();
|
||||||
|
@ -108,7 +114,7 @@ describe("ReleaseAnnouncementStore", () => {
|
||||||
const promise = listenReleaseAnnouncementChanged();
|
const promise = listenReleaseAnnouncementChanged();
|
||||||
await secondStore.nextReleaseAnnouncement();
|
await secondStore.nextReleaseAnnouncement();
|
||||||
|
|
||||||
expect(await promise).toBeNull();
|
expect(await promise).toBe("pinningMessageList");
|
||||||
expect(releaseAnnouncementStore.getReleaseAnnouncement()).toBeNull();
|
expect(releaseAnnouncementStore.getReleaseAnnouncement()).toBe("pinningMessageList");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -376,8 +376,8 @@ describe("Rageshakes", () => {
|
||||||
const mockSettingsStore = mocked(SettingsStore);
|
const mockSettingsStore = mocked(SettingsStore);
|
||||||
|
|
||||||
it("should collect labs from settings store", async () => {
|
it("should collect labs from settings store", async () => {
|
||||||
const someFeatures: string[] = ["feature_video_rooms", "feature_notification_settings2", "feature_pinning"];
|
const someFeatures: string[] = ["feature_video_rooms", "feature_notification_settings2"];
|
||||||
const enabledFeatures: string[] = ["feature_video_rooms", "feature_pinning"];
|
const enabledFeatures: string[] = ["feature_video_rooms"];
|
||||||
jest.spyOn(mockSettingsStore, "getFeatureSettingNames").mockReturnValue(someFeatures);
|
jest.spyOn(mockSettingsStore, "getFeatureSettingNames").mockReturnValue(someFeatures);
|
||||||
jest.spyOn(mockSettingsStore, "getValue").mockImplementation((settingName): any => {
|
jest.spyOn(mockSettingsStore, "getValue").mockImplementation((settingName): any => {
|
||||||
return enabledFeatures.includes(settingName);
|
return enabledFeatures.includes(settingName);
|
||||||
|
|
|
@ -141,14 +141,6 @@ describe("PinningUtils", () => {
|
||||||
|
|
||||||
describe("canPin & canUnpin", () => {
|
describe("canPin & canUnpin", () => {
|
||||||
describe("canPin", () => {
|
describe("canPin", () => {
|
||||||
test("should return false if pinning is disabled", () => {
|
|
||||||
// Disable feature pinning
|
|
||||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
|
|
||||||
const event = makePinEvent();
|
|
||||||
|
|
||||||
expect(PinningUtils.canPin(matrixClient, event)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should return false if event is not actionable", () => {
|
test("should return false if event is not actionable", () => {
|
||||||
mockedIsContentActionable.mockImplementation(() => false);
|
mockedIsContentActionable.mockImplementation(() => false);
|
||||||
const event = makePinEvent();
|
const event = makePinEvent();
|
||||||
|
|
Loading…
Reference in a new issue