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:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user