Make codebase comply to knip --strict (#33893)

* Make codebase comply to `knip --strict`

Deletes a few bits of dead code

* Trim i18n strings

* Fix missing export

* Remove test of dead code
This commit is contained in:
Michael Telatynski
2026-06-18 09:35:47 +00:00
committed by GitHub
parent 5de654636a
commit 7a1a3f518c
31 changed files with 64 additions and 1859 deletions
-1
View File
@@ -77,7 +77,6 @@
"html-entities": "^2.0.0",
"html-react-parser": "^6.0.0",
"is-ip": "^5.0.0",
"js-xxhash": "^5.0.0",
"jsrsasign": "^11.0.0",
"jszip": "^3.7.0",
"katex": "^0.17.0",
-2
View File
@@ -165,7 +165,6 @@
@import "./views/dialogs/_SpotlightDialog.pcss";
@import "./views/dialogs/_TermsDialog.pcss";
@import "./views/dialogs/_UnpinAllDialog.pcss";
@import "./views/dialogs/_UntrustedDeviceDialog.pcss";
@import "./views/dialogs/_UploadConfirmDialog.pcss";
@import "./views/dialogs/_UserSettingsDialog.pcss";
@import "./views/dialogs/_VerifyEMailDialog.pcss";
@@ -279,7 +278,6 @@
@import "./views/rooms/_ReplyPreview.pcss";
@import "./views/rooms/_ReplyTile.pcss";
@import "./views/rooms/_RoomBreadcrumbs.pcss";
@import "./views/rooms/_RoomCallBanner.pcss";
@import "./views/rooms/_RoomHeader.pcss";
@import "./views/rooms/_RoomInfoLine.pcss";
@import "./views/rooms/_RoomKnocksBar.pcss";
@@ -1,24 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2021 The Matrix.org Foundation C.I.C.
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_UntrustedDeviceDialog {
.mx_Dialog_title {
display: flex;
align-items: center;
.mx_E2EIcon {
margin-left: 0;
}
}
.mx_Dialog_buttons {
display: flex;
justify-content: flex-end;
gap: 8px;
}
}
@@ -1,42 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
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_RoomCallBanner {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
box-sizing: border-box;
padding: $spacing-12 $spacing-16;
color: $primary-content;
background-color: $system;
cursor: pointer;
}
.mx_RoomCallBanner_text {
display: flex;
flex: 1;
align-items: center;
}
.mx_RoomCallBanner_label {
color: $primary-content;
font-weight: var(--cpd-font-weight-semibold);
padding-right: $spacing-8;
svg {
display: inline-block;
vertical-align: middle;
color: $secondary-content;
width: 16px;
height: 16px;
margin-right: 8px;
}
}
-9
View File
@@ -79,12 +79,3 @@ export function isOnlyCtrlOrCmdKeyEvent(ev: React.KeyboardEvent | KeyboardEvent)
return ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
}
}
/**
* Checks if the given keyboard event is a modified key event (i.e., if any modifier keys are active).
* @param ev The keyboard event to check
* @returns True if the event is a modified key event, false otherwise
*/
export function isModifiedKeyEvent(ev: React.KeyboardEvent | KeyboardEvent): boolean {
return ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey;
}
+4 -1
View File
@@ -115,7 +115,10 @@ dis.register((payload) => {
*/
let sessionLockStolen = false;
// this is exposed solely for unit tests.
/**
* this is exposed solely for unit tests.
* @knipignore
*/
export function setSessionLockNotStolen(): void {
sessionLockStolen = false;
}
@@ -103,6 +103,7 @@ export const getKeyboardShortcuts = (): IKeyboardShortcuts => {
/**
* Gets keyboard shortcuts that should be presented to the user in the UI.
* @knipignore - exported for tests
*/
export const getKeyboardShortcutsForUI = (): IKeyboardShortcuts => {
const entries = [...Object.entries(getUIOnlyShortcuts()), ...Object.entries(getKeyboardShortcuts())] as [
@@ -484,35 +484,6 @@ export const toRightOf = (elementRect: Pick<DOMRect, "right" | "top" | "height">
return { left, top, chevronOffset };
};
export type ToLeftOf = {
chevronOffset: number;
right: number;
top: number;
};
// Placement method for <ContextMenu /> to position context menu to left of elementRect with chevronOffset
export const toLeftOf = (elementRect: DOMRect, chevronOffset = 12): ToLeftOf => {
const right = UIStore.instance.windowWidth - elementRect.left + window.scrollX - 3;
let top = elementRect.top + elementRect.height / 2 + window.scrollY;
top -= chevronOffset + 8; // where 8 is half the height of the chevron
return { right, top, chevronOffset };
};
/**
* Placement method for <ContextMenu /> to position context menu of or right of elementRect
* depending on which side has more space.
*/
export const toLeftOrRightOf = (elementRect: DOMRect, chevronOffset = 12): ToRightOf | ToLeftOf => {
const spaceToTheLeft = elementRect.left;
const spaceToTheRight = UIStore.instance.windowWidth - elementRect.right;
if (spaceToTheLeft > spaceToTheRight) {
return toLeftOf(elementRect, chevronOffset);
}
return toRightOf(elementRect, chevronOffset);
};
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect,
// and either above or below: wherever there is more space (maybe this should be aboveOrBelowLeftOf?)
export const aboveLeftOf = (
@@ -1,120 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
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, { useCallback } from "react";
import { type Room } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import { VideoCallSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { _t } from "../../../languageHandler";
import AccessibleButton, { type ButtonEvent } from "../elements/AccessibleButton";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { Action } from "../../../dispatcher/actions";
import { ConnectionState, type ElementCall } from "../../../models/Call";
import { useCall } from "../../../hooks/useCall";
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
import { OwnBeaconStore, OwnBeaconStoreEvent } from "../../../stores/OwnBeaconStore";
import { SessionDuration } from "../voip/CallDuration";
import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext";
interface RoomCallBannerProps {
roomId: Room["roomId"];
call: ElementCall;
}
const RoomCallBannerInner: React.FC<RoomCallBannerProps> = ({ roomId, call }) => {
const connect = useCallback(
(ev: ButtonEvent) => {
ev.preventDefault();
defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: roomId,
view_call: true,
skipLobby: ("shiftKey" in ev && ev.shiftKey) || undefined,
metricsTrigger: undefined,
});
},
[roomId],
);
// TODO matrix rtc
const onClick = useCallback(() => {
logger.log("clicking on the call banner is not supported anymore - there are no timeline events anymore.");
let messageLikeEventId: string | undefined;
if (!messageLikeEventId) {
// Until we have a timeline event for calls this will always be true.
// We will never jump to the non existing timeline event.
logger.error("Couldn't find a group call event to jump to");
return;
}
defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: roomId,
metricsTrigger: undefined,
event_id: messageLikeEventId,
scroll_into_view: true,
highlighted: true,
});
}, [roomId]);
return (
<div className="mx_RoomCallBanner" onClick={onClick}>
<div className="mx_RoomCallBanner_text">
<span className="mx_RoomCallBanner_label">
<VideoCallSolidIcon />
{_t("voip|video_call")}
</span>
<SessionDuration session={call.session} />
</div>
<AccessibleButton onClick={connect} kind="primary" element="button" disabled={false}>
{_t("action|join")}
</AccessibleButton>
</div>
);
};
interface Props {
roomId: Room["roomId"];
}
const RoomCallBanner: React.FC<Props> = ({ roomId }) => {
const call = useCall(roomId);
const { roomViewStore } = useScopedRoomContext("roomViewStore");
// this section is to check if we have a live location share. If so, we dont show the call banner
const isMonitoringLiveLocation = useEventEmitterState(
OwnBeaconStore.instance,
OwnBeaconStoreEvent.MonitoringLivePosition,
() => OwnBeaconStore.instance.isMonitoringLiveLocation,
);
const liveBeaconIds = useEventEmitterState(OwnBeaconStore.instance, OwnBeaconStoreEvent.LivenessChange, () =>
OwnBeaconStore.instance.getLiveBeaconIds(roomId),
);
if (isMonitoringLiveLocation && liveBeaconIds.length) {
return null;
}
// Check if the call is already showing. No banner is needed in this case.
if (roomViewStore.isViewingCall()) {
return null;
}
// Split into outer/inner to avoid watching various parts if there is no call
// No banner if the call is connected (or connecting/disconnecting)
if (call !== null && call.connectionState === ConnectionState.Disconnected) {
return <RoomCallBannerInner call={call as ElementCall} roomId={roomId} />;
}
return null;
};
export default RoomCallBanner;
@@ -1,255 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2020 The Matrix.org Foundation C.I.C.
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, { type JSX, type ComponentProps, useContext } from "react";
import { type IWidget, MatrixCapabilities } from "matrix-widget-api";
import { logger } from "matrix-js-sdk/src/logger";
import { type ApprovalOpts, WidgetLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle";
import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu";
import { ChevronFace } from "../../structures/ContextMenu";
import { _t } from "../../../languageHandler";
import { isAppWidget } from "../../../stores/WidgetStore";
import WidgetUtils from "../../../utils/WidgetUtils";
import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
import dis from "../../../dispatcher/dispatcher";
import SettingsStore from "../../../settings/SettingsStore";
import Modal from "../../../Modal";
import QuestionDialog from "../dialogs/QuestionDialog";
import ErrorDialog from "../dialogs/ErrorDialog";
import { WidgetType } from "../../../widgets/WidgetType";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
import { getConfigLivestreamUrl, startJitsiAudioLivestream } from "../../../Livestream";
import { ModuleRunner } from "../../../modules/ModuleRunner";
import { ElementWidget, type WidgetMessaging } from "../../../stores/widgets/WidgetMessaging";
import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx";
interface IProps extends Omit<ComponentProps<typeof IconizedContextMenu>, "children"> {
app: IWidget;
userWidget?: boolean;
showUnpin?: boolean;
// override delete handler
onDeleteClick?(this: void): void;
// override edit handler
onEditClick?(this: void): void;
}
const showStreamAudioStreamButton = (app: IWidget): boolean => {
return !!getConfigLivestreamUrl() && WidgetType.JITSI.matches(app.type);
};
const showEditButton = (app: IWidget, canModify: boolean): boolean => {
return canModify && WidgetUtils.isManagedByManager(app);
};
const showRevokeButton = (
cli: MatrixClient,
roomId: string | undefined,
app: IWidget,
userWidget: boolean | undefined,
): boolean => {
const isAllowedWidget =
(isAppWidget(app) &&
app.eventId !== undefined &&
(SettingsStore.getValue("allowedWidgets", roomId)[app.eventId] ?? false)) ||
app.creatorUserId === cli?.getUserId();
const isLocalWidget = WidgetType.JITSI.matches(app.type);
return !userWidget && !isLocalWidget && isAllowedWidget;
};
const showDeleteButton = (canModify: boolean, onDeleteClick: undefined | (() => void)): boolean => {
return !!onDeleteClick || canModify;
};
const showSnapshotButton = (widgetMessaging: WidgetMessaging | undefined): boolean => {
return (
SettingsStore.getValue("enableWidgetScreenshots") &&
!!widgetMessaging?.widgetApi?.hasCapability(MatrixCapabilities.Screenshots)
);
};
const showMoveButtons = (app: IWidget, room: Room | undefined, showUnpin: boolean | undefined): [boolean, boolean] => {
if (!showUnpin) return [false, false];
const pinnedWidgets = room ? WidgetLayoutStore.instance.getContainerWidgets(room, "top") : [];
const widgetIndex = pinnedWidgets.findIndex((widget) => widget.id === app.id);
return [widgetIndex > 0, widgetIndex < pinnedWidgets.length - 1];
};
export const WidgetContextMenu: React.FC<IProps> = ({
onFinished,
app,
userWidget,
onDeleteClick,
onEditClick,
showUnpin,
...props
}) => {
const cli = useContext(MatrixClientContext);
const { room, roomId } = useScopedRoomContext("room", "roomId");
const widgetMessaging = WidgetMessagingStore.instance.getMessagingForUid(WidgetUtils.getWidgetUid(app));
const canModify = userWidget || WidgetUtils.canUserModifyWidgets(cli, roomId);
let streamAudioStreamButton: JSX.Element | undefined;
if (roomId && showStreamAudioStreamButton(app)) {
const onStreamAudioClick = async (): Promise<void> => {
try {
await startJitsiAudioLivestream(cli, widgetMessaging!.widgetApi!, roomId);
} catch (err) {
logger.error("Failed to start livestream", err);
// XXX: won't i18n well, but looks like widget api only support 'message'?
const message =
err instanceof Error ? err.message : _t("widget|error_unable_start_audio_stream_description");
Modal.createDialog(ErrorDialog, {
title: _t("widget|error_unable_start_audio_stream_title"),
description: message,
});
}
onFinished();
};
streamAudioStreamButton = (
<IconizedContextMenuOption
onClick={onStreamAudioClick}
label={_t("widget|context_menu|start_audio_stream")}
/>
);
}
let editButton: JSX.Element | undefined;
if (showEditButton(app, canModify)) {
const _onEditClick = (): void => {
if (onEditClick) {
onEditClick();
} else if (room) {
WidgetUtils.editWidget(room, app);
}
onFinished();
};
editButton = <IconizedContextMenuOption onClick={_onEditClick} label={_t("action|edit")} />;
}
let snapshotButton: JSX.Element | undefined;
if (showSnapshotButton(widgetMessaging)) {
const onSnapshotClick = (): void => {
widgetMessaging?.widgetApi
?.takeScreenshot()
.then((data) => {
dis.dispatch({
action: "picture_snapshot",
file: data.screenshot,
});
})
.catch((err) => {
logger.error("Failed to take screenshot: ", err);
});
onFinished();
};
snapshotButton = (
<IconizedContextMenuOption onClick={onSnapshotClick} label={_t("widget|context_menu|screenshot")} />
);
}
let deleteButton: JSX.Element | undefined;
if (showDeleteButton(canModify, onDeleteClick)) {
const _onDeleteClick = (): void => {
if (onDeleteClick) {
onDeleteClick();
} else if (roomId) {
// Show delete confirmation dialog
const { finished } = Modal.createDialog(QuestionDialog, {
title: _t("widget|context_menu|delete"),
description: _t("widget|context_menu|delete_warning"),
button: _t("widget|context_menu|delete"),
});
finished.then(([confirmed]) => {
if (!confirmed) return;
WidgetUtils.setRoomWidget(cli, roomId, app.id);
});
}
onFinished();
};
deleteButton = (
<IconizedContextMenuOption
onClick={_onDeleteClick}
label={userWidget ? _t("action|remove") : _t("widget|context_menu|remove")}
/>
);
}
let revokeButton: JSX.Element | undefined;
if (showRevokeButton(cli, roomId, app, userWidget)) {
const opts: ApprovalOpts = { approved: undefined };
ModuleRunner.instance.invoke(WidgetLifecycle.PreLoadRequest, opts, new ElementWidget(app));
if (!opts.approved) {
const onRevokeClick = (): void => {
const eventId = isAppWidget(app) ? app.eventId : undefined;
logger.info("Revoking permission for widget to load: " + eventId);
const current = SettingsStore.getValue("allowedWidgets", roomId);
if (eventId !== undefined) current[eventId] = false;
const level = SettingsStore.firstSupportedLevel("allowedWidgets");
if (!level) throw new Error("level must be defined");
SettingsStore.setValue("allowedWidgets", roomId ?? null, level, current).catch((err) => {
logger.error(err);
// We don't really need to do anything about this - the user will just hit the button again.
});
onFinished();
};
revokeButton = (
<IconizedContextMenuOption onClick={onRevokeClick} label={_t("widget|context_menu|revoke")} />
);
}
}
const [showMoveLeftButton, showMoveRightButton] = showMoveButtons(app, room, showUnpin);
let moveLeftButton: JSX.Element | undefined;
if (showMoveLeftButton) {
const onClick = (): void => {
if (!room) throw new Error("room must be defined");
WidgetLayoutStore.instance.moveWithinContainer(room, "top", app, -1);
onFinished();
};
moveLeftButton = <IconizedContextMenuOption onClick={onClick} label={_t("widget|context_menu|move_left")} />;
}
let moveRightButton: JSX.Element | undefined;
if (showMoveRightButton) {
const onClick = (): void => {
if (!room) throw new Error("room must be defined");
WidgetLayoutStore.instance.moveWithinContainer(room, "top", app, 1);
onFinished();
};
moveRightButton = <IconizedContextMenuOption onClick={onClick} label={_t("widget|context_menu|move_right")} />;
}
return (
<IconizedContextMenu {...props} chevronFace={ChevronFace.None} onFinished={onFinished}>
<IconizedContextMenuOptionList>
{streamAudioStreamButton}
{editButton}
{revokeButton}
{deleteButton}
{snapshotButton}
{moveLeftButton}
{moveRightButton}
</IconizedContextMenuOptionList>
</IconizedContextMenu>
);
};
@@ -1,82 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2019, 2020 , 2021 The Matrix.org Foundation C.I.C.
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 { type User } from "matrix-js-sdk/src/matrix";
import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import E2EIcon from "../rooms/E2EIcon";
import AccessibleButton from "../elements/AccessibleButton";
import BaseDialog from "./BaseDialog";
import { type IDevice } from "../right_panel/UserInfo";
import { E2EStatus } from "../../../utils/ShieldUtils";
interface IProps {
/**
* The user whose device is untrusted.
*/
user: User;
/**
* The device that is untrusted.
*/
device: IDevice;
/**
* Callback for when the dialog is dismissed.
* If mode is "sas", the user wants to verify the device with SAS. Otherwise, the dialog was dismissed normally.
* @param mode The mode of dismissal.
*/
onFinished(this: void, mode?: "sas"): void;
}
const UntrustedDeviceDialog: React.FC<IProps> = ({ device, user, onFinished }) => {
let askToVerifyText: string;
let newSessionText: string;
if (MatrixClientPeg.safeGet().getUserId() === user.userId) {
newSessionText = _t("encryption|udd|own_new_session_text");
askToVerifyText = _t("encryption|udd|own_ask_verify_text");
} else {
newSessionText = _t("encryption|udd|other_new_session_text", {
name: user.displayName,
userId: user.userId,
});
askToVerifyText = _t("encryption|udd|other_ask_verify_text");
}
return (
<BaseDialog
onFinished={onFinished}
className="mx_UntrustedDeviceDialog"
title={
<>
<E2EIcon status={E2EStatus.Warning} isUser size={24} hideTooltip={true} />
{_t("encryption|udd|title")}
</>
}
>
<div className="mx_Dialog_content" id="mx_Dialog_content">
<p>{newSessionText}</p>
<p>
{device.displayName} ({device.deviceId})
</p>
<p>{askToVerifyText}</p>
</div>
<div className="mx_Dialog_buttons">
<AccessibleButton kind="primary_outline" onClick={() => onFinished("sas")}>
{_t("encryption|udd|interactive_verification_button")}
</AccessibleButton>
<AccessibleButton kind="primary" onClick={() => onFinished()}>
{_t("action|done")}
</AccessibleButton>
</div>
</BaseDialog>
);
};
export default UntrustedDeviceDialog;
@@ -88,22 +88,6 @@ export interface IPowerLevelsContent {
redact?: number;
}
export const isMuted = (member: RoomMember, powerLevelContent: IPowerLevelsContent): boolean => {
if (!powerLevelContent || !member) return false;
const levelToSend =
(powerLevelContent.events ? powerLevelContent.events["m.room.message"] : null) ||
powerLevelContent.events_default;
// levelToSend could be undefined as .events_default is optional. Coercing in this case using
// Number() would always return false, so this preserves behaviour
// FIXME: per the spec, if `events_default` is unset, it defaults to zero. If
// the member has a negative powerlevel, this will give an incorrect result.
if (levelToSend === undefined) return false;
return member.powerLevel < levelToSend;
};
export interface IRoomPermissions {
modifyLevelMax: number;
canEdit: boolean;
-135
View File
@@ -1,135 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
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 {
type MatrixClient,
type MatrixEvent,
MatrixEventEvent,
type RelationType,
TypedEventEmitter,
type Relations,
RelationsEvent,
} from "matrix-js-sdk/src/matrix";
import { type IDestroyable } from "../utils/IDestroyable";
export enum RelationsHelperEvent {
Add = "add",
}
interface EventMap {
[RelationsHelperEvent.Add]: (event: MatrixEvent) => void;
}
/**
* Helper class that manages a specific event type relation for an event.
* Just create an instance and listen for new events for that relation.
* Optionally receive the current events by calling emitCurrent().
* Clean up everything by calling destroy().
*/
export class RelationsHelper extends TypedEventEmitter<RelationsHelperEvent, EventMap> implements IDestroyable {
private relations?: Relations;
private eventId: string;
private roomId: string;
public constructor(
private event: MatrixEvent,
private relationType: RelationType,
private relationEventType: string,
private client: MatrixClient,
) {
super();
const eventId = event.getId();
if (!eventId) {
throw new Error("unable to create RelationsHelper: missing event ID");
}
const roomId = event.getRoomId();
if (!roomId) {
throw new Error("unable to create RelationsHelper: missing room ID");
}
this.eventId = eventId;
this.roomId = roomId;
this.setUpRelations();
}
private setUpRelations = (): void => {
this.setRelations();
if (this.relations) {
this.relations.on(RelationsEvent.Add, this.onRelationsAdd);
} else {
this.event.once(MatrixEventEvent.RelationsCreated, this.onRelationsCreated);
}
};
private onRelationsCreated = (): void => {
this.setRelations();
if (this.relations) {
this.relations.on(RelationsEvent.Add, this.onRelationsAdd);
this.emitCurrent();
} else {
this.event.once(MatrixEventEvent.RelationsCreated, this.onRelationsCreated);
}
};
private setRelations(): void {
const room = this.client.getRoom(this.event.getRoomId());
this.relations = room
?.getUnfilteredTimelineSet()
?.relations?.getChildEventsForEvent(this.eventId, this.relationType, this.relationEventType);
}
private onRelationsAdd = (event: MatrixEvent): void => {
this.emit(RelationsHelperEvent.Add, event);
};
public emitCurrent(): void {
this.relations?.getRelations()?.forEach((e) => this.emit(RelationsHelperEvent.Add, e));
}
public getCurrent(): MatrixEvent[] {
return this.relations?.getRelations() || [];
}
/**
* Fetches all related events from the server and emits them.
*/
public async emitFetchCurrent(): Promise<void> {
let nextBatch: string | undefined = undefined;
do {
const response = await this.client.relations(
this.roomId,
this.eventId,
this.relationType,
this.relationEventType,
{
from: nextBatch,
limit: 50,
},
);
nextBatch = response?.nextBatch ?? undefined;
response?.events.forEach((e) => this.emit(RelationsHelperEvent.Add, e));
} while (nextBatch);
}
public destroy(): void {
this.removeAllListeners();
this.event.off(MatrixEventEvent.RelationsCreated, this.onRelationsCreated);
if (this.relations) {
this.relations.off(RelationsEvent.Add, this.onRelationsAdd);
}
}
}
+3 -1
View File
@@ -17,7 +17,9 @@ const getValue = <T>(key: string, initialValue: T): T => {
}
};
// Hook behaving like useState but persisting the value to localStorage. Returns same as useState
/**
* Hook behaving like useState but persisting the value to localStorage. Returns same as useState
*/
export const useLocalStorageState = <T>(key: string, initialValue: T): [T, Dispatch<T>] => {
const lsKey = "mx_" + key;
+1 -15
View File
@@ -1013,14 +1013,6 @@
"set_up_toast_title": "Set up Secure Backup",
"turn_on_key_storage": "Turn on key storage",
"turn_on_key_storage_description": "This will allow you to view your chat history on any new devices and is required for backup of chats and digital identity.",
"udd": {
"interactive_verification_button": "Interactively verify by emoji",
"other_ask_verify_text": "Ask this user to verify their session, or manually verify it below.",
"other_new_session_text": "%(name)s (%(userId)s) signed in to a new session without verifying it:",
"own_ask_verify_text": "Verify your other session using one of the options below.",
"own_new_session_text": "You signed in to a new session without verifying it:",
"title": "Not Trusted"
},
"unable_to_setup_keys_error": "Unable to set up keys",
"verification": {
"accepting": "Accepting…",
@@ -4077,13 +4069,7 @@
"close_to_view_right_panel": "Close this widget to view it in this panel",
"context_menu": {
"delete": "Delete widget",
"delete_warning": "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?",
"move_left": "Move left",
"move_right": "Move right",
"remove": "Remove for everyone",
"revoke": "Revoke permissions",
"screenshot": "Take a picture",
"start_audio_stream": "Start audio stream"
"delete_warning": "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?"
},
"cookie_warning": "This widget may use cookies.",
"error_hangup_description": "You were disconnected from the call. (Error: %(message)s)",
+6 -3
View File
@@ -96,9 +96,12 @@ export function getUserLanguage(): string {
}
}
// Allow overriding the text displayed when no translation exists
// Currently only used in unit tests to avoid having to load
// the translations in element-web
/**
* Allow overriding the text displayed when no translation exists
* Currently only used in unit tests to avoid having to load
* the translations in element-web
* @knipignore
*/
export function setMissingEntryGenerator(f: (value: string) => void): void {
setMissingEntryGeneratorSharedComponents(f);
}
@@ -1,54 +0,0 @@
/*
Copyright 2024 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 { xxHash32 } from "js-xxhash";
/**
* The PhasedRolloutFeature class is used to manage the phased rollout of a new feature.
*
* It uses a hash of the user's identifier and the feature name to determine if a feature is enabled for a specific user.
* The rollout percentage determines the probability that a user will be enabled for the feature.
* The feature will be enabled for all users if the rollout percentage is 100, and for no users if the percentage is 0.
* If a user is enabled for a feature at x% rollout, it will also be for any greater than x percent.
*
* The process ensures a uniform distribution of enabled features across users.
*
* @property featureName - The name of the feature to be rolled out.
* @property rolloutPercentage - The int percentage (0..100) of users for whom the feature should be enabled.
*/
export class PhasedRolloutFeature {
public readonly featureName: string;
private readonly rolloutPercentage: number;
private readonly seed: number;
public constructor(featureName: string, rolloutPercentage: number) {
this.featureName = featureName;
if (!Number.isInteger(rolloutPercentage) || rolloutPercentage < 0 || rolloutPercentage > 100) {
throw new Error("Rollout percentage must be an integer between 0 and 100");
}
this.rolloutPercentage = rolloutPercentage;
// We add the feature name for the seed to ensure that the hash is different for each feature
this.seed = Array.from(featureName).reduce((sum, char) => sum + char.charCodeAt(0), 0);
}
/**
* Returns true if the feature should be enabled for the given user.
* @param userIdentifier - Some unique identifier for the user, e.g. their user ID or device ID.
*/
public isFeatureEnabled(userIdentifier: string): boolean {
/*
* We use a hash function to convert the unique user ID string into an integer.
* This integer can then be used as a basis for deciding whether the user should have access to the new feature.
* We need some hash with good uniform distribution properties, security is not a concern here.
* We use xxHash32, which is fast and has good distribution properties.
*/
const hash = xxHash32(userIdentifier, this.seed);
// We use the hash modulo 100 to get a number between 0 and 99.
// Modulo is simple and effective and the distribution should be uniform enough for our purposes.
return hash % 100 < this.rolloutPercentage;
}
}
-61
View File
@@ -1,61 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2021 The Matrix.org Foundation C.I.C.
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 { describe, it, expect } from "vitest";
import { getEnumValues, isEnumValue } from "./enums";
enum TestStringEnum {
First = "__first__",
Second = "__second__",
}
enum TestNumberEnum {
FirstKey = 10,
SecondKey = 20,
}
describe("enums", () => {
describe("getEnumValues", () => {
it("should work on string enums", () => {
const result = getEnumValues(TestStringEnum);
expect(result).toBeDefined();
expect(result).toHaveLength(2);
expect(result).toEqual(["__first__", "__second__"]);
});
it("should work on number enums", () => {
const result = getEnumValues(TestNumberEnum);
expect(result).toBeDefined();
expect(result).toHaveLength(2);
expect(result).toEqual([10, 20]);
});
});
describe("isEnumValue", () => {
it("should return true on values in a string enum", () => {
const result = isEnumValue(TestStringEnum, "__first__");
expect(result).toBe(true);
});
it("should return false on values not in a string enum", () => {
const result = isEnumValue(TestStringEnum, "not a value");
expect(result).toBe(false);
});
it("should return true on values in a number enum", () => {
const result = isEnumValue(TestNumberEnum, 10);
expect(result).toBe(true);
});
it("should return false on values not in a number enum", () => {
const result = isEnumValue(TestStringEnum, 99);
expect(result).toBe(false);
});
});
});
-41
View File
@@ -1,41 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
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.
*/
/**
* Get the values for an enum.
* @param e The enum.
* @returns The enum values.
*/
export function getEnumValues(e: any): (string | number)[] {
// String-based enums will simply be objects ({Key: "value"}), but number-based
// enums will instead map themselves twice: in one direction for {Key: 12} and
// the reverse for easy lookup, presumably ({12: Key}). In the reverse mapping,
// the key is a string, not a number.
//
// For this reason, we try to determine what kind of enum we're dealing with.
const keys = Object.keys(e);
const values: (string | number)[] = [];
for (const key of keys) {
const value = e[key];
if (Number.isFinite(value) || e[value.toString()] !== Number(key)) {
values.push(value);
}
}
return values;
}
/**
* Determines if a given value is a valid value for the provided enum.
* @param e The enum to check against.
* @param val The value to search for.
* @returns True if the enum contains the value.
*/
export function isEnumValue<T>(e: T, val: string | number): boolean {
return getEnumValues(e).includes(val);
}
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import { toLeftOf, toLeftOrRightOf, toRightOf } from "../../../../src/components/structures/ContextMenu";
import { toRightOf } from "../../../../src/components/structures/ContextMenu";
import UIStore from "../../../../src/stores/UIStore";
describe("ContextMenu", () => {
@@ -26,16 +26,6 @@ describe("ContextMenu", () => {
UIStore.instance.windowWidth = 1280;
});
describe("toLeftOf", () => {
it("should return the correct positioning", () => {
expect(toLeftOf(rect)).toEqual({
chevronOffset: 12,
right: 1285, // 1280 - 23 + 31 - 3
top: 303, // 42 + (480 / 2) + 41 - (12 + 8)
});
});
});
describe("toRightOf", () => {
it("should return the correct positioning", () => {
expect(toRightOf(rect)).toEqual({
@@ -45,35 +35,4 @@ describe("ContextMenu", () => {
});
});
});
describe("toLeftOrRightOf", () => {
describe("when there is more space to the right", () => {
// default case from test setup
it("should return a position to the right", () => {
expect(toLeftOrRightOf(rect)).toEqual({
chevronOffset: 12,
left: 80, // 46 + 31 + 3
top: 303, // 42 + (480 / 2) + 41 - (12 + 8)
});
});
});
describe("when there is more space to the left", () => {
beforeEach(() => {
// @ts-ignore
rect.left = 500;
// @ts-ignore
rect.right = 1000;
});
it("should return a position to the left", () => {
expect(toLeftOrRightOf(rect)).toEqual({
chevronOffset: 12,
right: 808, // 1280 - 500 + 31 - 3
top: 303, // 42 + (480 / 2) + 41 - (12 + 8)
});
});
});
});
});
@@ -19,7 +19,6 @@ import { KnownMembership } from "matrix-js-sdk/src/types";
import { MatrixClientPeg } from "../../../../../../../src/MatrixClientPeg";
import { type RoomAdminToolsProps } from "../../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoAdminToolsContainerViewModel";
import { useMuteButtonViewModel } from "../../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoMuteButtonViewModel";
import { isMuted } from "../../../../../../../src/components/views/right_panel/UserInfo";
import { withClientContextRenderOptions } from "../../../../../../test-utils";
describe("useMuteButtonViewModel", () => {
@@ -191,40 +190,4 @@ describe("useMuteButtonViewModel", () => {
expect(mockClient.setPowerLevel).toHaveBeenCalledWith(mockRoom.roomId, defaultMember.userId, -1);
});
it("returns false if either argument is falsy", () => {
// @ts-ignore to let us purposely pass incorrect args
expect(isMuted(defaultMember, null)).toBe(false);
// @ts-ignore to let us purposely pass incorrect args
expect(isMuted(null, {})).toBe(false);
});
it("when powerLevelContent.events and .events_default are undefined, returns false", () => {
const powerLevelContents = {};
expect(isMuted(defaultMember, powerLevelContents)).toBe(false);
});
it("when powerLevelContent.events is undefined, uses .events_default", () => {
const higherPowerLevelContents = { events_default: 10 };
expect(isMuted(defaultMember, higherPowerLevelContents)).toBe(true);
const lowerPowerLevelContents = { events_default: -10 };
expect(isMuted(defaultMember, lowerPowerLevelContents)).toBe(false);
});
it("when powerLevelContent.events is defined but '.m.room.message' isn't, uses .events_default", () => {
const higherPowerLevelContents = { events: {}, events_default: 10 };
expect(isMuted(defaultMember, higherPowerLevelContents)).toBe(true);
const lowerPowerLevelContents = { events: {}, events_default: -10 };
expect(isMuted(defaultMember, lowerPowerLevelContents)).toBe(false);
});
it("when powerLevelContent.events and '.m.room.message' are defined, uses the value", () => {
const higherPowerLevelContents = { events: { "m.room.message": -10 }, events_default: 10 };
expect(isMuted(defaultMember, higherPowerLevelContents)).toBe(false);
const lowerPowerLevelContents = { events: { "m.room.message": 10 }, events_default: -10 };
expect(isMuted(defaultMember, lowerPowerLevelContents)).toBe(true);
});
});
@@ -1,154 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
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 {
Room,
PendingEventOrdering,
type MatrixClient,
type RoomMember,
RoomStateEvent,
} from "matrix-js-sdk/src/matrix";
import { Widget } from "matrix-widget-api";
import { act, cleanup, render, screen } from "jest-matrix-react";
import { mocked, type Mocked } from "jest-mock";
import {
mkRoomMember,
MockedCall,
setupAsyncStoreWithClient,
stubClient,
useMockedCalls,
} from "../../../../test-utils";
import RoomCallBanner from "../../../../../src/components/views/beacon/RoomCallBanner";
import { CallStore } from "../../../../../src/stores/CallStore";
import { WidgetMessagingStore } from "../../../../../src/stores/widgets/WidgetMessagingStore";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import { ConnectionState } from "../../../../../src/models/Call";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext";
import RoomContext, { type RoomContextType } from "../../../../../src/contexts/RoomContext";
import { type WidgetMessaging } from "../../../../../src/stores/widgets/WidgetMessaging";
describe("<RoomCallBanner />", () => {
let client: Mocked<MatrixClient>;
let room: Room;
let alice: RoomMember;
useMockedCalls();
const defaultProps = {
roomId: "!1:example.org",
};
const mockRoomViewStore = {
isViewingCall: jest.fn().mockReturnValue(false),
on: jest.fn(),
off: jest.fn(),
emit: jest.fn(),
};
let roomContext: RoomContextType;
beforeEach(() => {
stubClient();
client = mocked(MatrixClientPeg.safeGet());
room = new Room("!1:example.org", client, "@alice:example.org", {
pendingEventOrdering: PendingEventOrdering.Detached,
});
alice = mkRoomMember(room.roomId, "@alice:example.org");
jest.spyOn(room, "getMember").mockImplementation((userId) => (userId === alice.userId ? alice : null));
client.getRoom.mockImplementation((roomId) => (roomId === room.roomId ? room : null));
client.getRooms.mockReturnValue([room]);
client.reEmitter.reEmit(room, [RoomStateEvent.Events]);
setupAsyncStoreWithClient(CallStore.instance, client);
setupAsyncStoreWithClient(WidgetMessagingStore.instance, client);
// Reset the mock RoomViewStore
mockRoomViewStore.isViewingCall.mockReturnValue(false);
// Create a stable room context for this test
roomContext = {
...RoomContext,
roomId: room.roomId,
roomViewStore: mockRoomViewStore,
} as unknown as RoomContextType;
});
afterEach(async () => {
client.reEmitter.stopReEmitting(room, [RoomStateEvent.Events]);
});
const renderBanner = async (props = {}): Promise<void> => {
render(
<ScopedRoomContextProvider {...roomContext}>
<RoomCallBanner {...defaultProps} {...props} />
</ScopedRoomContextProvider>,
);
await act(() => Promise.resolve()); // Let effects settle
};
it("renders nothing when there is no call", async () => {
await renderBanner();
const banner = await screen.queryByText("Video call");
expect(banner).toBeFalsy();
});
describe("call started", () => {
let call: MockedCall;
let widget: Widget;
beforeEach(() => {
MockedCall.create(room, "1");
const maybeCall = CallStore.instance.getCall(room.roomId);
if (!(maybeCall instanceof MockedCall)) {
throw new Error("Failed to create call");
}
call = maybeCall;
widget = new Widget(call.widget);
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, {
stop: () => {},
} as unknown as WidgetMessaging);
});
afterEach(() => {
cleanup(); // Unmount before we do any cleanup that might update the component
call.destroy();
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
});
it("renders if there is a call", async () => {
await renderBanner();
await screen.findByText("Video call");
});
it("shows Join button if the user has not joined", async () => {
await renderBanner();
await screen.findByText("Join");
});
it("doesn't show banner if the call is connected", async () => {
call.setConnectionState(ConnectionState.Connected);
await renderBanner();
const banner = await screen.queryByText("Video call");
expect(banner).toBeFalsy();
});
it("doesn't show banner if the call is shown", async () => {
mockRoomViewStore.isViewingCall.mockReturnValue(true);
await renderBanner();
const banner = await screen.queryByText("Video call");
expect(banner).toBeFalsy();
});
});
// TODO: test clicking buttons
// TODO: add live location share warning test (should not render if there is an active live location share)
});
@@ -1,174 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 Mikhail Aheichyk
Copyright 2023 Nordeck IT + Consulting GmbH.
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, { type JSX, type ComponentProps } from "react";
import { screen, render } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { type Room, type MatrixClient } from "matrix-js-sdk/src/matrix";
import { MatrixWidgetType } from "matrix-widget-api";
import {
type ApprovalOpts,
type WidgetInfo,
WidgetLifecycle,
} from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle";
import { WidgetContextMenu } from "../../../../../src/components/views/context_menus/WidgetContextMenu";
import { type IApp } from "../../../../../src/stores/WidgetStore";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import WidgetUtils from "../../../../../src/utils/WidgetUtils";
import { ModuleRunner } from "../../../../../src/modules/ModuleRunner";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { WidgetLayoutStore } from "../../../../../src/stores/widgets/WidgetLayoutStore";
import { mkStubRoom } from "../../../../test-utils/test-utils.ts";
import { type RoomContextType } from "../../../../../src/contexts/RoomContext.ts";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
describe("<WidgetContextMenu />", () => {
const widgetId = "w1";
const eventId = "e1";
const roomId = "r1";
const userId = "@user-id:server";
const app: IApp = {
id: widgetId,
eventId,
roomId,
type: MatrixWidgetType.Custom,
url: "https://example.com",
name: "Example 1",
creatorUserId: userId,
avatar_url: undefined,
};
let mockClient: MatrixClient;
let room: Room;
let onFinished: () => void;
let roomContext: RoomContextType;
beforeEach(() => {
onFinished = jest.fn();
jest.spyOn(WidgetUtils, "canUserModifyWidgets").mockReturnValue(true);
mockClient = {
getUserId: jest.fn().mockReturnValue(userId),
} as unknown as MatrixClient;
room = mkStubRoom(roomId, "Test Room", mockClient);
roomContext = {
room,
roomId,
} as unknown as RoomContextType;
});
afterEach(() => {
jest.restoreAllMocks();
});
function getComponent(props: Partial<ComponentProps<typeof WidgetContextMenu>> = {}): JSX.Element {
return (
<MatrixClientContext.Provider value={mockClient}>
<ScopedRoomContextProvider {...roomContext}>
<WidgetContextMenu app={app} onFinished={onFinished} {...props} />
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>
);
}
it("renders revoke button", async () => {
const { rerender } = render(getComponent());
const revokeButton = screen.getByLabelText("Revoke permissions");
expect(revokeButton).toBeInTheDocument();
jest.spyOn(ModuleRunner.instance, "invoke").mockImplementation((lifecycleEvent, opts, widgetInfo) => {
if (lifecycleEvent === WidgetLifecycle.PreLoadRequest && (widgetInfo as WidgetInfo).id === widgetId) {
(opts as ApprovalOpts).approved = true;
}
});
rerender(getComponent());
expect(revokeButton).not.toBeInTheDocument();
});
it("revokes permissions", async () => {
render(getComponent());
await userEvent.click(screen.getByLabelText("Revoke permissions"));
expect(onFinished).toHaveBeenCalled();
expect(SettingsStore.getValue("allowedWidgets", roomId)[eventId]).toBe(false);
});
it("shows the move left button when the widget can be moved left", () => {
// Place our widget second so it can move left but not right.
jest.spyOn(WidgetLayoutStore.instance, "getContainerWidgets").mockReturnValue([
{ id: "someOtherWidget", type: "m.custom", creatorUserId: userId, url: "" },
{ id: widgetId, type: "m.custom", creatorUserId: userId, url: "" },
]);
render(getComponent({ showUnpin: true }));
expect(screen.getByLabelText("Move left")).toBeInTheDocument();
expect(screen.queryByLabelText("Move right")).not.toBeInTheDocument();
});
it("shows the move right button when the widget can be moved right", () => {
// Place our widget first so it can move right but not left.
jest.spyOn(WidgetLayoutStore.instance, "getContainerWidgets").mockReturnValue([
{ id: widgetId, type: "m.custom", creatorUserId: userId, url: "" },
{ id: "someOtherWidget", type: "m.custom", creatorUserId: userId, url: "" },
]);
render(getComponent({ showUnpin: true }));
expect(screen.getByLabelText("Move right")).toBeInTheDocument();
expect(screen.queryByLabelText("Move left")).not.toBeInTheDocument();
});
it("moves widget left when move left button is clicked", async () => {
// Place our widget second so move left is visible.
jest.spyOn(WidgetLayoutStore.instance, "getContainerWidgets").mockReturnValue([
{ id: "someOtherWidget", type: "m.custom", creatorUserId: userId, url: "" },
{ id: widgetId, type: "m.custom", creatorUserId: userId, url: "" },
]);
// Mock moveWithinContainer to verify it's called with the correct arguments.
const moveWithinContainerSpy = jest
.spyOn(WidgetLayoutStore.instance, "moveWithinContainer")
.mockImplementation();
render(getComponent({ showUnpin: true }));
await userEvent.click(screen.getByLabelText("Move left"));
expect(moveWithinContainerSpy).toHaveBeenCalledWith(room, "top", app, -1);
expect(onFinished).toHaveBeenCalled();
});
it("moves widget right when move right button is clicked", async () => {
// Place our widget first so move right is visible.
jest.spyOn(WidgetLayoutStore.instance, "getContainerWidgets").mockReturnValue([
{ id: widgetId, type: "m.custom", creatorUserId: userId, url: "" },
{ id: "someOtherWidget", type: "m.custom", creatorUserId: userId, url: "" },
]);
// Mock moveWithinContainer to verify it's called with the correct arguments.
const moveWithinContainerSpy = jest
.spyOn(WidgetLayoutStore.instance, "moveWithinContainer")
.mockImplementation();
render(getComponent({ showUnpin: true }));
await userEvent.click(screen.getByLabelText("Move right"));
expect(moveWithinContainerSpy).toHaveBeenCalledWith(room, "top", app, 1);
expect(onFinished).toHaveBeenCalled();
});
});
@@ -1,59 +0,0 @@
/*
* Copyright 2024 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 { Device, type MatrixClient, User } from "matrix-js-sdk/src/matrix";
import { render, screen } from "jest-matrix-react";
import { stubClient } from "../../../../test-utils";
import UntrustedDeviceDialog from "../../../../../src/components/views/dialogs/UntrustedDeviceDialog.tsx";
describe("<UntrustedDeviceDialog />", () => {
let client: MatrixClient;
let user: User;
let device: Device;
const onFinished = jest.fn();
beforeEach(() => {
client = stubClient();
user = User.createUser("@alice:example.org", client);
user.setDisplayName("Alice");
device = new Device({ deviceId: "device_id", userId: user.userId, algorithms: [], keys: new Map() });
});
afterEach(() => {
onFinished.mockReset();
});
function renderComponent() {
return render(<UntrustedDeviceDialog user={user} device={device} onFinished={onFinished} />);
}
it("should display the dialog for the device of another user", () => {
const { asFragment } = renderComponent();
expect(asFragment()).toMatchSnapshot();
});
it("should display the dialog for the device of the current user", () => {
jest.spyOn(client, "getUserId").mockReturnValue(user.userId);
const { asFragment } = renderComponent();
expect(asFragment()).toMatchSnapshot();
});
it("should call onFinished without parameter when Done is clicked", () => {
renderComponent();
screen.getByRole("button", { name: "Done" }).click();
expect(onFinished).toHaveBeenCalledWith();
});
it("should call onFinished with sas when Interactively verify by emoji is clicked", () => {
renderComponent();
screen.getByRole("button", { name: "Interactively verify by emoji" }).click();
expect(onFinished).toHaveBeenCalledWith("sas");
});
});
@@ -1,203 +0,0 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`<UntrustedDeviceDialog /> should display the dialog for the device of another user 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_UntrustedDeviceDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
tabindex="-1"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
<div
class="mx_E2EIcon"
data-testid="e2e-icon"
style="width: 24px; height: 24px;"
>
<svg
color="var(--cpd-color-icon-critical-primary)"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
</div>
Not Trusted
</h1>
</div>
<div
class="mx_Dialog_content"
id="mx_Dialog_content"
>
<p>
Alice (@alice:example.org) signed in to a new session without verifying it:
</p>
<p>
(device_id)
</p>
<p>
Ask this user to verify their session, or manually verify it below.
</p>
</div>
<div
class="mx_Dialog_buttons"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
role="button"
tabindex="0"
>
Interactively verify by emoji
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
Done
</div>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414"
/>
</svg>
</div>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
exports[`<UntrustedDeviceDialog /> should display the dialog for the device of the current user 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_UntrustedDeviceDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
tabindex="-1"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
<div
class="mx_E2EIcon"
data-testid="e2e-icon"
style="width: 24px; height: 24px;"
>
<svg
color="var(--cpd-color-icon-critical-primary)"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
</div>
Not Trusted
</h1>
</div>
<div
class="mx_Dialog_content"
id="mx_Dialog_content"
>
<p>
You signed in to a new session without verifying it:
</p>
<p>
(device_id)
</p>
<p>
Verify your other session using one of the options below.
</p>
</div>
<div
class="mx_Dialog_buttons"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
role="button"
tabindex="0"
>
Interactively verify by emoji
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
Done
</div>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414"
/>
</svg>
</div>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
@@ -1,195 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
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 { mocked } from "jest-mock";
import {
EventType,
type MatrixClient,
type MatrixEvent,
MatrixEventEvent,
RelationType,
Room,
} from "matrix-js-sdk/src/matrix";
import { RelationsHelper, RelationsHelperEvent } from "../../../src/events/RelationsHelper";
import { mkEvent, stubClient } from "../../test-utils";
describe("RelationsHelper", () => {
const roomId = "!room:example.com";
let event: MatrixEvent;
let relatedEvent1: MatrixEvent;
let relatedEvent2: MatrixEvent;
let relatedEvent3: MatrixEvent;
let room: Room;
let client: MatrixClient;
let relationsHelper: RelationsHelper;
let onAdd: (event: MatrixEvent) => void;
beforeEach(() => {
client = stubClient();
mocked(client.relations).mockClear();
room = new Room(roomId, client, client.getSafeUserId());
mocked(client.getRoom).mockImplementation((getRoomId?: string): Room | null => {
if (getRoomId === roomId) return room;
return null;
});
event = mkEvent({
event: true,
type: EventType.RoomMessage,
room: roomId,
user: client.getSafeUserId(),
content: {},
});
relatedEvent1 = mkEvent({
event: true,
type: EventType.RoomMessage,
room: roomId,
user: client.getSafeUserId(),
content: {
["m.relates_to"]: {
rel_type: RelationType.Reference,
event_id: event.getId(),
},
},
});
relatedEvent2 = mkEvent({
event: true,
type: EventType.RoomMessage,
room: roomId,
user: client.getSafeUserId(),
content: {
["m.relates_to"]: {
rel_type: RelationType.Reference,
event_id: event.getId(),
},
},
});
relatedEvent3 = mkEvent({
event: true,
type: EventType.RoomMessage,
room: roomId,
user: client.getSafeUserId(),
content: {
["m.relates_to"]: {
rel_type: RelationType.Reference,
event_id: event.getId(),
},
},
});
onAdd = jest.fn();
});
afterEach(() => {
relationsHelper?.destroy();
});
describe("when there is an event without ID", () => {
it("should raise an error", () => {
jest.spyOn(event, "getId").mockReturnValue(undefined);
expect(() => {
new RelationsHelper(event, RelationType.Reference, EventType.RoomMessage, client);
}).toThrow("unable to create RelationsHelper: missing event ID");
});
});
describe("when there is an event without room ID", () => {
it("should raise an error", () => {
jest.spyOn(event, "getRoomId").mockReturnValue(undefined);
expect(() => {
new RelationsHelper(event, RelationType.Reference, EventType.RoomMessage, client);
}).toThrow("unable to create RelationsHelper: missing room ID");
});
});
describe("when there is an event without relations", () => {
beforeEach(() => {
relationsHelper = new RelationsHelper(event, RelationType.Reference, EventType.RoomMessage, client);
relationsHelper.on(RelationsHelperEvent.Add, onAdd);
});
describe("emitCurrent", () => {
beforeEach(() => {
relationsHelper.emitCurrent();
});
it("should not emit any event", () => {
expect(onAdd).not.toHaveBeenCalled();
});
});
describe("and a new event appears", () => {
beforeEach(() => {
room.relations.aggregateChildEvent(relatedEvent2);
event.emit(MatrixEventEvent.RelationsCreated, RelationType.Reference, EventType.RoomMessage);
});
it("should emit the new event", () => {
expect(onAdd).toHaveBeenCalledWith(relatedEvent2);
});
});
});
describe("when there is an event with two pages server side relations", () => {
beforeEach(() => {
mocked(client.relations)
.mockResolvedValueOnce({
events: [relatedEvent1, relatedEvent2],
nextBatch: "next",
})
.mockResolvedValueOnce({
events: [relatedEvent3],
nextBatch: null,
});
relationsHelper = new RelationsHelper(event, RelationType.Reference, EventType.RoomMessage, client);
relationsHelper.on(RelationsHelperEvent.Add, onAdd);
});
describe("emitFetchCurrent", () => {
beforeEach(async () => {
await relationsHelper.emitFetchCurrent();
});
it("should emit the server side events", () => {
expect(onAdd).toHaveBeenCalledWith(relatedEvent1);
expect(onAdd).toHaveBeenCalledWith(relatedEvent2);
expect(onAdd).toHaveBeenCalledWith(relatedEvent3);
});
});
});
describe("when there is an event with relations", () => {
beforeEach(() => {
room.relations.aggregateChildEvent(relatedEvent1);
relationsHelper = new RelationsHelper(event, RelationType.Reference, EventType.RoomMessage, client);
relationsHelper.on(RelationsHelperEvent.Add, onAdd);
});
describe("emitCurrent", () => {
beforeEach(() => {
relationsHelper.emitCurrent();
});
it("should emit the related event", () => {
expect(onAdd).toHaveBeenCalledWith(relatedEvent1);
});
});
describe("and a new event appears", () => {
beforeEach(() => {
room.relations.aggregateChildEvent(relatedEvent2);
event.emit(MatrixEventEvent.RelationsCreated, RelationType.Reference, EventType.RoomMessage);
});
it("should emit the new event", () => {
expect(onAdd).toHaveBeenCalledWith(relatedEvent2);
});
});
});
});
@@ -1,81 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2024 The Matrix.org Foundation C.I.C.
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 { PhasedRolloutFeature } from "../../../src/utils/PhasedRolloutFeature";
describe("Test PhasedRolloutFeature", () => {
function randomUserId() {
const characters = "abcdefghijklmnopqrstuvwxyz0123456789.=_-/+";
let result = "";
const charactersLength = characters.length;
const idLength = Math.floor(Math.random() * 15) + 6; // Random number between 6 and 20
for (let i = 0; i < idLength; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return "@" + result + ":matrix.org";
}
function randomDeviceId() {
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
let result = "";
const charactersLength = characters.length;
for (let i = 0; i < 10; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
it("should only accept valid percentage", () => {
expect(() => new PhasedRolloutFeature("test", 0.8)).toThrow();
expect(() => new PhasedRolloutFeature("test", -1)).toThrow();
expect(() => new PhasedRolloutFeature("test", 123)).toThrow();
});
it("should enable for all if percentage is 100", () => {
const phasedRolloutFeature = new PhasedRolloutFeature("test", 100);
for (let i = 0; i < 1000; i++) {
expect(phasedRolloutFeature.isFeatureEnabled(randomUserId())).toBeTruthy();
}
});
it("should not enable for anyone if percentage is 0", () => {
const phasedRolloutFeature = new PhasedRolloutFeature("test", 0);
for (let i = 0; i < 1000; i++) {
expect(phasedRolloutFeature.isFeatureEnabled(randomUserId())).toBeFalsy();
}
});
it("should enable for more users if percentage grows", () => {
let rolloutPercentage = 0;
let previousBatch: string[] = [];
const allUsers = new Array(1000).fill(0).map(() => randomDeviceId());
while (rolloutPercentage <= 90) {
rolloutPercentage += 10;
const nextRollout = new PhasedRolloutFeature("test", rolloutPercentage);
const nextBatch = allUsers.filter((userId) => nextRollout.isFeatureEnabled(userId));
expect(previousBatch.length).toBeLessThan(nextBatch.length);
expect(previousBatch.every((user) => nextBatch.includes(user))).toBeTruthy();
previousBatch = nextBatch;
}
});
it("should distribute differently depending on the feature name", () => {
const allUsers = new Array(1000).fill(0).map(() => randomUserId());
const featureARollout = new PhasedRolloutFeature("FeatureA", 50);
const featureBRollout = new PhasedRolloutFeature("FeatureB", 50);
const featureAUsers = allUsers.filter((userId) => featureARollout.isFeatureEnabled(userId));
const featureBUsers = allUsers.filter((userId) => featureBRollout.isFeatureEnabled(userId));
expect(featureAUsers).not.toEqual(featureBUsers);
});
});
+43 -12
View File
@@ -5,9 +5,23 @@ process.env.GITHUB_ACTIONS = "1";
export default {
workspaces: {
"packages/shared-components": {},
"packages/shared-components": {
entry: ["src/index.ts!", "scripts/**"],
project: [
"**/*.{js,cjs,mjs,jsx,ts,cts,mts,tsx}!",
"!scripts/**!",
"!src/test/**!",
"!src/**/test-*!",
"!src/**/*-{mock,mocks,snapshot,actions}.*!",
],
},
"packages/playwright-common": {
entry: ["src/fixtures/index.ts", "src/testcontainers/index.ts"],
entry: ["src/fixtures/index.ts!", "src/testcontainers/index.ts!"],
project: [
"**/*.{js,cjs,mjs,jsx,ts,cts,mts,tsx}!",
"!src/flaky-reporter.ts!",
"!src/stale-screenshot-reporter.ts!",
],
ignoreDependencies: [
// Used in playwright-screenshots.sh
"wait-on",
@@ -17,20 +31,32 @@ export default {
"packages/module-api": {},
"apps/web": {
entry: [
"src/serviceworker/index.ts",
"src/workers/*.worker.ts",
"src/utils/exportUtils/exportJS.js",
"src/vector/localstorage-fix.ts",
"src/serviceworker/index.ts!",
"src/workers/*.worker.ts!",
"src/utils/exportUtils/exportJS.js!",
"src/vector/localstorage-fix.ts!",
"scripts/**",
"playwright/**",
"test/**",
"res/decoder-ring/**",
"res/jitsi_external_api.min.js",
"res/themes/*/css/*.pcss",
],
ignore: [
"I18nWebpackPlugin.ts!",
"module_system/**!",
// Keep for now
"src/hooks/useLocalStorageState.ts",
"src/hooks/useLocalStorageState.ts!",
"src/hooks/useIsReleaseAnnouncementOpen.ts!",
"src/components/structures/ReleaseAnnouncement.tsx!",
"src/utils/arrays.ts!",
"src/utils/EventPresentationContextProvider.tsx!",
// This is just an awful side-effect import
"src/stores/LifecycleStore.ts!",
],
project: [
"**/*.{js,cjs,mjs,jsx,ts,cts,mts,tsx}!",
"!scripts/**!",
"!src/test/**!",
"!recorder-worklet-loader.cjs!",
],
ignoreDependencies: [
// False positive
@@ -49,7 +75,7 @@ export default {
],
},
"apps/desktop": {
entry: ["src/preload.cts", "electron-builder.ts", "scripts/**", "hak/**"],
entry: ["src/preload.cts!", "electron-builder.ts!", "scripts/**", "hak/**"],
project: ["**/*.{js,ts}"],
ignoreDependencies: [
// Brought in via hak scripts
@@ -65,9 +91,12 @@ export default {
"lipo",
],
},
"modules": {},
"modules": {
project: ["**/*.{js,cjs,mjs,jsx,ts,cts,mts,tsx}!", "!playwright/**!"],
},
"modules/*": {
entry: "src/index.ts{x,}",
entry: ["src/index.ts{x,}!"],
project: ["**/*.{js,cjs,mjs,jsx,ts,cts,mts,tsx}!", "!src/tests/**!", "!e2e/**!"],
},
".": {
entry: ["scripts/**", "docs/**"],
@@ -95,4 +124,6 @@ export default {
config: ["playwright.config.ts", "playwright-merge.config.ts"],
},
tags: ["-knipignore"],
treatConfigHintsAsErrors: true,
treatTagHintsAsErrors: true,
} satisfies KnipConfig;
@@ -11,7 +11,10 @@ import { constructWidgetPermissions } from "./utils/constructWidgetPermissions";
import { matchPattern } from "./utils/matchPattern";
import { name as ModuleName } from "../package.json";
/** Subset of {@link WidgetLifecycleApi} used by the module for registration only. */
/**
* Subset of {@link WidgetLifecycleApi} used by the module for registration only.
* @internal
*/
export type WidgetLifecycleApiAdapter = Pick<
WidgetLifecycleApi,
"registerPreloadApprover" | "registerIdentityApprover" | "registerCapabilitiesApprover"
+1 -1
View File
@@ -17,7 +17,7 @@
"lint:fmt": "oxfmt --check",
"lint:fmt-fix": "oxfmt",
"lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) -print -exec action-validator {} ';'",
"lint:knip": "knip",
"lint:knip": "knip --no-tag-hints && knip --strict --exclude unlisted,dependencies,binaries",
"install:git-hooks": "husky",
"postinstall": "node scripts/pnpm-link.ts && pnpm run -r sane-postinstall",
"docs:dev": "vitepress dev docs",
-9
View File
@@ -615,9 +615,6 @@ importers:
is-ip:
specifier: ^5.0.0
version: 5.0.1
js-xxhash:
specifier: ^5.0.0
version: 5.0.1
jsrsasign:
specifier: ^11.0.0
version: 11.1.3
@@ -10157,10 +10154,6 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-xxhash@5.0.1:
resolution: {integrity: sha512-OGuTCR9wqYqF2FrhjtGb6fx8Rm1jzhdpUEsY2sLkXbNYLzOS+XWcYT4KTUL7vnaRTmBuGuVpW1AK2zzxjgZXGg==}
engines: {node: '>=20.0.0'}
js-yaml@3.14.2:
resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==}
hasBin: true
@@ -23889,8 +23882,6 @@ snapshots:
js-tokens@4.0.0: {}
js-xxhash@5.0.1: {}
js-yaml@3.14.2:
dependencies:
argparse: 1.0.10