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:
@@ -77,7 +77,6 @@
|
|||||||
"html-entities": "^2.0.0",
|
"html-entities": "^2.0.0",
|
||||||
"html-react-parser": "^6.0.0",
|
"html-react-parser": "^6.0.0",
|
||||||
"is-ip": "^5.0.0",
|
"is-ip": "^5.0.0",
|
||||||
"js-xxhash": "^5.0.0",
|
|
||||||
"jsrsasign": "^11.0.0",
|
"jsrsasign": "^11.0.0",
|
||||||
"jszip": "^3.7.0",
|
"jszip": "^3.7.0",
|
||||||
"katex": "^0.17.0",
|
"katex": "^0.17.0",
|
||||||
|
|||||||
@@ -165,7 +165,6 @@
|
|||||||
@import "./views/dialogs/_SpotlightDialog.pcss";
|
@import "./views/dialogs/_SpotlightDialog.pcss";
|
||||||
@import "./views/dialogs/_TermsDialog.pcss";
|
@import "./views/dialogs/_TermsDialog.pcss";
|
||||||
@import "./views/dialogs/_UnpinAllDialog.pcss";
|
@import "./views/dialogs/_UnpinAllDialog.pcss";
|
||||||
@import "./views/dialogs/_UntrustedDeviceDialog.pcss";
|
|
||||||
@import "./views/dialogs/_UploadConfirmDialog.pcss";
|
@import "./views/dialogs/_UploadConfirmDialog.pcss";
|
||||||
@import "./views/dialogs/_UserSettingsDialog.pcss";
|
@import "./views/dialogs/_UserSettingsDialog.pcss";
|
||||||
@import "./views/dialogs/_VerifyEMailDialog.pcss";
|
@import "./views/dialogs/_VerifyEMailDialog.pcss";
|
||||||
@@ -279,7 +278,6 @@
|
|||||||
@import "./views/rooms/_ReplyPreview.pcss";
|
@import "./views/rooms/_ReplyPreview.pcss";
|
||||||
@import "./views/rooms/_ReplyTile.pcss";
|
@import "./views/rooms/_ReplyTile.pcss";
|
||||||
@import "./views/rooms/_RoomBreadcrumbs.pcss";
|
@import "./views/rooms/_RoomBreadcrumbs.pcss";
|
||||||
@import "./views/rooms/_RoomCallBanner.pcss";
|
|
||||||
@import "./views/rooms/_RoomHeader.pcss";
|
@import "./views/rooms/_RoomHeader.pcss";
|
||||||
@import "./views/rooms/_RoomInfoLine.pcss";
|
@import "./views/rooms/_RoomInfoLine.pcss";
|
||||||
@import "./views/rooms/_RoomKnocksBar.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -79,12 +79,3 @@ export function isOnlyCtrlOrCmdKeyEvent(ev: React.KeyboardEvent | KeyboardEvent)
|
|||||||
return ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
|
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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -115,7 +115,10 @@ dis.register((payload) => {
|
|||||||
*/
|
*/
|
||||||
let sessionLockStolen = false;
|
let sessionLockStolen = false;
|
||||||
|
|
||||||
// this is exposed solely for unit tests.
|
/**
|
||||||
|
* this is exposed solely for unit tests.
|
||||||
|
* @knipignore
|
||||||
|
*/
|
||||||
export function setSessionLockNotStolen(): void {
|
export function setSessionLockNotStolen(): void {
|
||||||
sessionLockStolen = false;
|
sessionLockStolen = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ export const getKeyboardShortcuts = (): IKeyboardShortcuts => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets keyboard shortcuts that should be presented to the user in the UI.
|
* Gets keyboard shortcuts that should be presented to the user in the UI.
|
||||||
|
* @knipignore - exported for tests
|
||||||
*/
|
*/
|
||||||
export const getKeyboardShortcutsForUI = (): IKeyboardShortcuts => {
|
export const getKeyboardShortcutsForUI = (): IKeyboardShortcuts => {
|
||||||
const entries = [...Object.entries(getUIOnlyShortcuts()), ...Object.entries(getKeyboardShortcuts())] as [
|
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 };
|
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,
|
// 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?)
|
// and either above or below: wherever there is more space (maybe this should be aboveOrBelowLeftOf?)
|
||||||
export const aboveLeftOf = (
|
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;
|
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 {
|
export interface IRoomPermissions {
|
||||||
modifyLevelMax: number;
|
modifyLevelMax: number;
|
||||||
canEdit: boolean;
|
canEdit: boolean;
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>] => {
|
export const useLocalStorageState = <T>(key: string, initialValue: T): [T, Dispatch<T>] => {
|
||||||
const lsKey = "mx_" + key;
|
const lsKey = "mx_" + key;
|
||||||
|
|
||||||
|
|||||||
@@ -1013,14 +1013,6 @@
|
|||||||
"set_up_toast_title": "Set up Secure Backup",
|
"set_up_toast_title": "Set up Secure Backup",
|
||||||
"turn_on_key_storage": "Turn on key storage",
|
"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.",
|
"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",
|
"unable_to_setup_keys_error": "Unable to set up keys",
|
||||||
"verification": {
|
"verification": {
|
||||||
"accepting": "Accepting…",
|
"accepting": "Accepting…",
|
||||||
@@ -4077,13 +4069,7 @@
|
|||||||
"close_to_view_right_panel": "Close this widget to view it in this panel",
|
"close_to_view_right_panel": "Close this widget to view it in this panel",
|
||||||
"context_menu": {
|
"context_menu": {
|
||||||
"delete": "Delete widget",
|
"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?",
|
"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"
|
|
||||||
},
|
},
|
||||||
"cookie_warning": "This widget may use cookies.",
|
"cookie_warning": "This widget may use cookies.",
|
||||||
"error_hangup_description": "You were disconnected from the call. (Error: %(message)s)",
|
"error_hangup_description": "You were disconnected from the call. (Error: %(message)s)",
|
||||||
|
|||||||
@@ -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
|
* Allow overriding the text displayed when no translation exists
|
||||||
// the translations in element-web
|
* 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 {
|
export function setMissingEntryGenerator(f: (value: string) => void): void {
|
||||||
setMissingEntryGeneratorSharedComponents(f);
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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.
|
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";
|
import UIStore from "../../../../src/stores/UIStore";
|
||||||
|
|
||||||
describe("ContextMenu", () => {
|
describe("ContextMenu", () => {
|
||||||
@@ -26,16 +26,6 @@ describe("ContextMenu", () => {
|
|||||||
UIStore.instance.windowWidth = 1280;
|
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", () => {
|
describe("toRightOf", () => {
|
||||||
it("should return the correct positioning", () => {
|
it("should return the correct positioning", () => {
|
||||||
expect(toRightOf(rect)).toEqual({
|
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)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
-37
@@ -19,7 +19,6 @@ import { KnownMembership } from "matrix-js-sdk/src/types";
|
|||||||
import { MatrixClientPeg } from "../../../../../../../src/MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../../../../src/MatrixClientPeg";
|
||||||
import { type RoomAdminToolsProps } from "../../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoAdminToolsContainerViewModel";
|
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 { 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";
|
import { withClientContextRenderOptions } from "../../../../../../test-utils";
|
||||||
|
|
||||||
describe("useMuteButtonViewModel", () => {
|
describe("useMuteButtonViewModel", () => {
|
||||||
@@ -191,40 +190,4 @@ describe("useMuteButtonViewModel", () => {
|
|||||||
|
|
||||||
expect(mockClient.setPowerLevel).toHaveBeenCalledWith(mockRoom.roomId, defaultMember.userId, -1);
|
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");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-203
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -5,9 +5,23 @@ process.env.GITHUB_ACTIONS = "1";
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
workspaces: {
|
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": {
|
"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: [
|
ignoreDependencies: [
|
||||||
// Used in playwright-screenshots.sh
|
// Used in playwright-screenshots.sh
|
||||||
"wait-on",
|
"wait-on",
|
||||||
@@ -17,20 +31,32 @@ export default {
|
|||||||
"packages/module-api": {},
|
"packages/module-api": {},
|
||||||
"apps/web": {
|
"apps/web": {
|
||||||
entry: [
|
entry: [
|
||||||
"src/serviceworker/index.ts",
|
"src/serviceworker/index.ts!",
|
||||||
"src/workers/*.worker.ts",
|
"src/workers/*.worker.ts!",
|
||||||
"src/utils/exportUtils/exportJS.js",
|
"src/utils/exportUtils/exportJS.js!",
|
||||||
"src/vector/localstorage-fix.ts",
|
"src/vector/localstorage-fix.ts!",
|
||||||
"scripts/**",
|
"scripts/**",
|
||||||
"playwright/**",
|
"playwright/**",
|
||||||
"test/**",
|
"test/**",
|
||||||
"res/decoder-ring/**",
|
"res/decoder-ring/**",
|
||||||
"res/jitsi_external_api.min.js",
|
"res/jitsi_external_api.min.js",
|
||||||
"res/themes/*/css/*.pcss",
|
"res/themes/*/css/*.pcss",
|
||||||
],
|
"I18nWebpackPlugin.ts!",
|
||||||
ignore: [
|
"module_system/**!",
|
||||||
// Keep for now
|
// 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: [
|
ignoreDependencies: [
|
||||||
// False positive
|
// False positive
|
||||||
@@ -49,7 +75,7 @@ export default {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
"apps/desktop": {
|
"apps/desktop": {
|
||||||
entry: ["src/preload.cts", "electron-builder.ts", "scripts/**", "hak/**"],
|
entry: ["src/preload.cts!", "electron-builder.ts!", "scripts/**", "hak/**"],
|
||||||
project: ["**/*.{js,ts}"],
|
project: ["**/*.{js,ts}"],
|
||||||
ignoreDependencies: [
|
ignoreDependencies: [
|
||||||
// Brought in via hak scripts
|
// Brought in via hak scripts
|
||||||
@@ -65,9 +91,12 @@ export default {
|
|||||||
"lipo",
|
"lipo",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
"modules": {},
|
"modules": {
|
||||||
|
project: ["**/*.{js,cjs,mjs,jsx,ts,cts,mts,tsx}!", "!playwright/**!"],
|
||||||
|
},
|
||||||
"modules/*": {
|
"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/**"],
|
entry: ["scripts/**", "docs/**"],
|
||||||
@@ -95,4 +124,6 @@ export default {
|
|||||||
config: ["playwright.config.ts", "playwright-merge.config.ts"],
|
config: ["playwright.config.ts", "playwright-merge.config.ts"],
|
||||||
},
|
},
|
||||||
tags: ["-knipignore"],
|
tags: ["-knipignore"],
|
||||||
|
treatConfigHintsAsErrors: true,
|
||||||
|
treatTagHintsAsErrors: true,
|
||||||
} satisfies KnipConfig;
|
} satisfies KnipConfig;
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ import { constructWidgetPermissions } from "./utils/constructWidgetPermissions";
|
|||||||
import { matchPattern } from "./utils/matchPattern";
|
import { matchPattern } from "./utils/matchPattern";
|
||||||
import { name as ModuleName } from "../package.json";
|
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<
|
export type WidgetLifecycleApiAdapter = Pick<
|
||||||
WidgetLifecycleApi,
|
WidgetLifecycleApi,
|
||||||
"registerPreloadApprover" | "registerIdentityApprover" | "registerCapabilitiesApprover"
|
"registerPreloadApprover" | "registerIdentityApprover" | "registerCapabilitiesApprover"
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@
|
|||||||
"lint:fmt": "oxfmt --check",
|
"lint:fmt": "oxfmt --check",
|
||||||
"lint:fmt-fix": "oxfmt",
|
"lint:fmt-fix": "oxfmt",
|
||||||
"lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) -print -exec action-validator {} ';'",
|
"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",
|
"install:git-hooks": "husky",
|
||||||
"postinstall": "node scripts/pnpm-link.ts && pnpm run -r sane-postinstall",
|
"postinstall": "node scripts/pnpm-link.ts && pnpm run -r sane-postinstall",
|
||||||
"docs:dev": "vitepress dev docs",
|
"docs:dev": "vitepress dev docs",
|
||||||
|
|||||||
Generated
-9
@@ -615,9 +615,6 @@ importers:
|
|||||||
is-ip:
|
is-ip:
|
||||||
specifier: ^5.0.0
|
specifier: ^5.0.0
|
||||||
version: 5.0.1
|
version: 5.0.1
|
||||||
js-xxhash:
|
|
||||||
specifier: ^5.0.0
|
|
||||||
version: 5.0.1
|
|
||||||
jsrsasign:
|
jsrsasign:
|
||||||
specifier: ^11.0.0
|
specifier: ^11.0.0
|
||||||
version: 11.1.3
|
version: 11.1.3
|
||||||
@@ -10157,10 +10154,6 @@ packages:
|
|||||||
js-tokens@4.0.0:
|
js-tokens@4.0.0:
|
||||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
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:
|
js-yaml@3.14.2:
|
||||||
resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==}
|
resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -23889,8 +23882,6 @@ snapshots:
|
|||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
|
|
||||||
js-xxhash@5.0.1: {}
|
|
||||||
|
|
||||||
js-yaml@3.14.2:
|
js-yaml@3.14.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
argparse: 1.0.10
|
argparse: 1.0.10
|
||||||
|
|||||||
Reference in New Issue
Block a user