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