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

* Make codebase comply to `knip --strict`

Deletes a few bits of dead code

* Trim i18n strings

* Fix missing export

* Remove test of dead code
This commit is contained in:
Michael Telatynski
2026-06-18 09:35:47 +00:00
committed by GitHub
parent 5de654636a
commit 7a1a3f518c
31 changed files with 64 additions and 1859 deletions
-9
View File
@@ -79,12 +79,3 @@ export function isOnlyCtrlOrCmdKeyEvent(ev: React.KeyboardEvent | KeyboardEvent)
return ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
}
}
/**
* Checks if the given keyboard event is a modified key event (i.e., if any modifier keys are active).
* @param ev The keyboard event to check
* @returns True if the event is a modified key event, false otherwise
*/
export function isModifiedKeyEvent(ev: React.KeyboardEvent | KeyboardEvent): boolean {
return ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey;
}
+4 -1
View File
@@ -115,7 +115,10 @@ dis.register((payload) => {
*/
let sessionLockStolen = false;
// this is exposed solely for unit tests.
/**
* this is exposed solely for unit tests.
* @knipignore
*/
export function setSessionLockNotStolen(): void {
sessionLockStolen = false;
}
@@ -103,6 +103,7 @@ export const getKeyboardShortcuts = (): IKeyboardShortcuts => {
/**
* Gets keyboard shortcuts that should be presented to the user in the UI.
* @knipignore - exported for tests
*/
export const getKeyboardShortcutsForUI = (): IKeyboardShortcuts => {
const entries = [...Object.entries(getUIOnlyShortcuts()), ...Object.entries(getKeyboardShortcuts())] as [
@@ -484,35 +484,6 @@ export const toRightOf = (elementRect: Pick<DOMRect, "right" | "top" | "height">
return { left, top, chevronOffset };
};
export type ToLeftOf = {
chevronOffset: number;
right: number;
top: number;
};
// Placement method for <ContextMenu /> to position context menu to left of elementRect with chevronOffset
export const toLeftOf = (elementRect: DOMRect, chevronOffset = 12): ToLeftOf => {
const right = UIStore.instance.windowWidth - elementRect.left + window.scrollX - 3;
let top = elementRect.top + elementRect.height / 2 + window.scrollY;
top -= chevronOffset + 8; // where 8 is half the height of the chevron
return { right, top, chevronOffset };
};
/**
* Placement method for <ContextMenu /> to position context menu of or right of elementRect
* depending on which side has more space.
*/
export const toLeftOrRightOf = (elementRect: DOMRect, chevronOffset = 12): ToRightOf | ToLeftOf => {
const spaceToTheLeft = elementRect.left;
const spaceToTheRight = UIStore.instance.windowWidth - elementRect.right;
if (spaceToTheLeft > spaceToTheRight) {
return toLeftOf(elementRect, chevronOffset);
}
return toRightOf(elementRect, chevronOffset);
};
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect,
// and either above or below: wherever there is more space (maybe this should be aboveOrBelowLeftOf?)
export const aboveLeftOf = (
@@ -1,120 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React, { useCallback } from "react";
import { type Room } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import { VideoCallSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { _t } from "../../../languageHandler";
import AccessibleButton, { type ButtonEvent } from "../elements/AccessibleButton";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { Action } from "../../../dispatcher/actions";
import { ConnectionState, type ElementCall } from "../../../models/Call";
import { useCall } from "../../../hooks/useCall";
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
import { OwnBeaconStore, OwnBeaconStoreEvent } from "../../../stores/OwnBeaconStore";
import { SessionDuration } from "../voip/CallDuration";
import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext";
interface RoomCallBannerProps {
roomId: Room["roomId"];
call: ElementCall;
}
const RoomCallBannerInner: React.FC<RoomCallBannerProps> = ({ roomId, call }) => {
const connect = useCallback(
(ev: ButtonEvent) => {
ev.preventDefault();
defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: roomId,
view_call: true,
skipLobby: ("shiftKey" in ev && ev.shiftKey) || undefined,
metricsTrigger: undefined,
});
},
[roomId],
);
// TODO matrix rtc
const onClick = useCallback(() => {
logger.log("clicking on the call banner is not supported anymore - there are no timeline events anymore.");
let messageLikeEventId: string | undefined;
if (!messageLikeEventId) {
// Until we have a timeline event for calls this will always be true.
// We will never jump to the non existing timeline event.
logger.error("Couldn't find a group call event to jump to");
return;
}
defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: roomId,
metricsTrigger: undefined,
event_id: messageLikeEventId,
scroll_into_view: true,
highlighted: true,
});
}, [roomId]);
return (
<div className="mx_RoomCallBanner" onClick={onClick}>
<div className="mx_RoomCallBanner_text">
<span className="mx_RoomCallBanner_label">
<VideoCallSolidIcon />
{_t("voip|video_call")}
</span>
<SessionDuration session={call.session} />
</div>
<AccessibleButton onClick={connect} kind="primary" element="button" disabled={false}>
{_t("action|join")}
</AccessibleButton>
</div>
);
};
interface Props {
roomId: Room["roomId"];
}
const RoomCallBanner: React.FC<Props> = ({ roomId }) => {
const call = useCall(roomId);
const { roomViewStore } = useScopedRoomContext("roomViewStore");
// this section is to check if we have a live location share. If so, we dont show the call banner
const isMonitoringLiveLocation = useEventEmitterState(
OwnBeaconStore.instance,
OwnBeaconStoreEvent.MonitoringLivePosition,
() => OwnBeaconStore.instance.isMonitoringLiveLocation,
);
const liveBeaconIds = useEventEmitterState(OwnBeaconStore.instance, OwnBeaconStoreEvent.LivenessChange, () =>
OwnBeaconStore.instance.getLiveBeaconIds(roomId),
);
if (isMonitoringLiveLocation && liveBeaconIds.length) {
return null;
}
// Check if the call is already showing. No banner is needed in this case.
if (roomViewStore.isViewingCall()) {
return null;
}
// Split into outer/inner to avoid watching various parts if there is no call
// No banner if the call is connected (or connecting/disconnecting)
if (call !== null && call.connectionState === ConnectionState.Disconnected) {
return <RoomCallBannerInner call={call as ElementCall} roomId={roomId} />;
}
return null;
};
export default RoomCallBanner;
@@ -1,255 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2020 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React, { type JSX, type ComponentProps, useContext } from "react";
import { type IWidget, MatrixCapabilities } from "matrix-widget-api";
import { logger } from "matrix-js-sdk/src/logger";
import { type ApprovalOpts, WidgetLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle";
import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu";
import { ChevronFace } from "../../structures/ContextMenu";
import { _t } from "../../../languageHandler";
import { isAppWidget } from "../../../stores/WidgetStore";
import WidgetUtils from "../../../utils/WidgetUtils";
import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
import dis from "../../../dispatcher/dispatcher";
import SettingsStore from "../../../settings/SettingsStore";
import Modal from "../../../Modal";
import QuestionDialog from "../dialogs/QuestionDialog";
import ErrorDialog from "../dialogs/ErrorDialog";
import { WidgetType } from "../../../widgets/WidgetType";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
import { getConfigLivestreamUrl, startJitsiAudioLivestream } from "../../../Livestream";
import { ModuleRunner } from "../../../modules/ModuleRunner";
import { ElementWidget, type WidgetMessaging } from "../../../stores/widgets/WidgetMessaging";
import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx";
interface IProps extends Omit<ComponentProps<typeof IconizedContextMenu>, "children"> {
app: IWidget;
userWidget?: boolean;
showUnpin?: boolean;
// override delete handler
onDeleteClick?(this: void): void;
// override edit handler
onEditClick?(this: void): void;
}
const showStreamAudioStreamButton = (app: IWidget): boolean => {
return !!getConfigLivestreamUrl() && WidgetType.JITSI.matches(app.type);
};
const showEditButton = (app: IWidget, canModify: boolean): boolean => {
return canModify && WidgetUtils.isManagedByManager(app);
};
const showRevokeButton = (
cli: MatrixClient,
roomId: string | undefined,
app: IWidget,
userWidget: boolean | undefined,
): boolean => {
const isAllowedWidget =
(isAppWidget(app) &&
app.eventId !== undefined &&
(SettingsStore.getValue("allowedWidgets", roomId)[app.eventId] ?? false)) ||
app.creatorUserId === cli?.getUserId();
const isLocalWidget = WidgetType.JITSI.matches(app.type);
return !userWidget && !isLocalWidget && isAllowedWidget;
};
const showDeleteButton = (canModify: boolean, onDeleteClick: undefined | (() => void)): boolean => {
return !!onDeleteClick || canModify;
};
const showSnapshotButton = (widgetMessaging: WidgetMessaging | undefined): boolean => {
return (
SettingsStore.getValue("enableWidgetScreenshots") &&
!!widgetMessaging?.widgetApi?.hasCapability(MatrixCapabilities.Screenshots)
);
};
const showMoveButtons = (app: IWidget, room: Room | undefined, showUnpin: boolean | undefined): [boolean, boolean] => {
if (!showUnpin) return [false, false];
const pinnedWidgets = room ? WidgetLayoutStore.instance.getContainerWidgets(room, "top") : [];
const widgetIndex = pinnedWidgets.findIndex((widget) => widget.id === app.id);
return [widgetIndex > 0, widgetIndex < pinnedWidgets.length - 1];
};
export const WidgetContextMenu: React.FC<IProps> = ({
onFinished,
app,
userWidget,
onDeleteClick,
onEditClick,
showUnpin,
...props
}) => {
const cli = useContext(MatrixClientContext);
const { room, roomId } = useScopedRoomContext("room", "roomId");
const widgetMessaging = WidgetMessagingStore.instance.getMessagingForUid(WidgetUtils.getWidgetUid(app));
const canModify = userWidget || WidgetUtils.canUserModifyWidgets(cli, roomId);
let streamAudioStreamButton: JSX.Element | undefined;
if (roomId && showStreamAudioStreamButton(app)) {
const onStreamAudioClick = async (): Promise<void> => {
try {
await startJitsiAudioLivestream(cli, widgetMessaging!.widgetApi!, roomId);
} catch (err) {
logger.error("Failed to start livestream", err);
// XXX: won't i18n well, but looks like widget api only support 'message'?
const message =
err instanceof Error ? err.message : _t("widget|error_unable_start_audio_stream_description");
Modal.createDialog(ErrorDialog, {
title: _t("widget|error_unable_start_audio_stream_title"),
description: message,
});
}
onFinished();
};
streamAudioStreamButton = (
<IconizedContextMenuOption
onClick={onStreamAudioClick}
label={_t("widget|context_menu|start_audio_stream")}
/>
);
}
let editButton: JSX.Element | undefined;
if (showEditButton(app, canModify)) {
const _onEditClick = (): void => {
if (onEditClick) {
onEditClick();
} else if (room) {
WidgetUtils.editWidget(room, app);
}
onFinished();
};
editButton = <IconizedContextMenuOption onClick={_onEditClick} label={_t("action|edit")} />;
}
let snapshotButton: JSX.Element | undefined;
if (showSnapshotButton(widgetMessaging)) {
const onSnapshotClick = (): void => {
widgetMessaging?.widgetApi
?.takeScreenshot()
.then((data) => {
dis.dispatch({
action: "picture_snapshot",
file: data.screenshot,
});
})
.catch((err) => {
logger.error("Failed to take screenshot: ", err);
});
onFinished();
};
snapshotButton = (
<IconizedContextMenuOption onClick={onSnapshotClick} label={_t("widget|context_menu|screenshot")} />
);
}
let deleteButton: JSX.Element | undefined;
if (showDeleteButton(canModify, onDeleteClick)) {
const _onDeleteClick = (): void => {
if (onDeleteClick) {
onDeleteClick();
} else if (roomId) {
// Show delete confirmation dialog
const { finished } = Modal.createDialog(QuestionDialog, {
title: _t("widget|context_menu|delete"),
description: _t("widget|context_menu|delete_warning"),
button: _t("widget|context_menu|delete"),
});
finished.then(([confirmed]) => {
if (!confirmed) return;
WidgetUtils.setRoomWidget(cli, roomId, app.id);
});
}
onFinished();
};
deleteButton = (
<IconizedContextMenuOption
onClick={_onDeleteClick}
label={userWidget ? _t("action|remove") : _t("widget|context_menu|remove")}
/>
);
}
let revokeButton: JSX.Element | undefined;
if (showRevokeButton(cli, roomId, app, userWidget)) {
const opts: ApprovalOpts = { approved: undefined };
ModuleRunner.instance.invoke(WidgetLifecycle.PreLoadRequest, opts, new ElementWidget(app));
if (!opts.approved) {
const onRevokeClick = (): void => {
const eventId = isAppWidget(app) ? app.eventId : undefined;
logger.info("Revoking permission for widget to load: " + eventId);
const current = SettingsStore.getValue("allowedWidgets", roomId);
if (eventId !== undefined) current[eventId] = false;
const level = SettingsStore.firstSupportedLevel("allowedWidgets");
if (!level) throw new Error("level must be defined");
SettingsStore.setValue("allowedWidgets", roomId ?? null, level, current).catch((err) => {
logger.error(err);
// We don't really need to do anything about this - the user will just hit the button again.
});
onFinished();
};
revokeButton = (
<IconizedContextMenuOption onClick={onRevokeClick} label={_t("widget|context_menu|revoke")} />
);
}
}
const [showMoveLeftButton, showMoveRightButton] = showMoveButtons(app, room, showUnpin);
let moveLeftButton: JSX.Element | undefined;
if (showMoveLeftButton) {
const onClick = (): void => {
if (!room) throw new Error("room must be defined");
WidgetLayoutStore.instance.moveWithinContainer(room, "top", app, -1);
onFinished();
};
moveLeftButton = <IconizedContextMenuOption onClick={onClick} label={_t("widget|context_menu|move_left")} />;
}
let moveRightButton: JSX.Element | undefined;
if (showMoveRightButton) {
const onClick = (): void => {
if (!room) throw new Error("room must be defined");
WidgetLayoutStore.instance.moveWithinContainer(room, "top", app, 1);
onFinished();
};
moveRightButton = <IconizedContextMenuOption onClick={onClick} label={_t("widget|context_menu|move_right")} />;
}
return (
<IconizedContextMenu {...props} chevronFace={ChevronFace.None} onFinished={onFinished}>
<IconizedContextMenuOptionList>
{streamAudioStreamButton}
{editButton}
{revokeButton}
{deleteButton}
{snapshotButton}
{moveLeftButton}
{moveRightButton}
</IconizedContextMenuOptionList>
</IconizedContextMenu>
);
};
@@ -1,82 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2019, 2020 , 2021 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { type User } from "matrix-js-sdk/src/matrix";
import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import E2EIcon from "../rooms/E2EIcon";
import AccessibleButton from "../elements/AccessibleButton";
import BaseDialog from "./BaseDialog";
import { type IDevice } from "../right_panel/UserInfo";
import { E2EStatus } from "../../../utils/ShieldUtils";
interface IProps {
/**
* The user whose device is untrusted.
*/
user: User;
/**
* The device that is untrusted.
*/
device: IDevice;
/**
* Callback for when the dialog is dismissed.
* If mode is "sas", the user wants to verify the device with SAS. Otherwise, the dialog was dismissed normally.
* @param mode The mode of dismissal.
*/
onFinished(this: void, mode?: "sas"): void;
}
const UntrustedDeviceDialog: React.FC<IProps> = ({ device, user, onFinished }) => {
let askToVerifyText: string;
let newSessionText: string;
if (MatrixClientPeg.safeGet().getUserId() === user.userId) {
newSessionText = _t("encryption|udd|own_new_session_text");
askToVerifyText = _t("encryption|udd|own_ask_verify_text");
} else {
newSessionText = _t("encryption|udd|other_new_session_text", {
name: user.displayName,
userId: user.userId,
});
askToVerifyText = _t("encryption|udd|other_ask_verify_text");
}
return (
<BaseDialog
onFinished={onFinished}
className="mx_UntrustedDeviceDialog"
title={
<>
<E2EIcon status={E2EStatus.Warning} isUser size={24} hideTooltip={true} />
{_t("encryption|udd|title")}
</>
}
>
<div className="mx_Dialog_content" id="mx_Dialog_content">
<p>{newSessionText}</p>
<p>
{device.displayName} ({device.deviceId})
</p>
<p>{askToVerifyText}</p>
</div>
<div className="mx_Dialog_buttons">
<AccessibleButton kind="primary_outline" onClick={() => onFinished("sas")}>
{_t("encryption|udd|interactive_verification_button")}
</AccessibleButton>
<AccessibleButton kind="primary" onClick={() => onFinished()}>
{_t("action|done")}
</AccessibleButton>
</div>
</BaseDialog>
);
};
export default UntrustedDeviceDialog;
@@ -88,22 +88,6 @@ export interface IPowerLevelsContent {
redact?: number;
}
export const isMuted = (member: RoomMember, powerLevelContent: IPowerLevelsContent): boolean => {
if (!powerLevelContent || !member) return false;
const levelToSend =
(powerLevelContent.events ? powerLevelContent.events["m.room.message"] : null) ||
powerLevelContent.events_default;
// levelToSend could be undefined as .events_default is optional. Coercing in this case using
// Number() would always return false, so this preserves behaviour
// FIXME: per the spec, if `events_default` is unset, it defaults to zero. If
// the member has a negative powerlevel, this will give an incorrect result.
if (levelToSend === undefined) return false;
return member.powerLevel < levelToSend;
};
export interface IRoomPermissions {
modifyLevelMax: number;
canEdit: boolean;
-135
View File
@@ -1,135 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import {
type MatrixClient,
type MatrixEvent,
MatrixEventEvent,
type RelationType,
TypedEventEmitter,
type Relations,
RelationsEvent,
} from "matrix-js-sdk/src/matrix";
import { type IDestroyable } from "../utils/IDestroyable";
export enum RelationsHelperEvent {
Add = "add",
}
interface EventMap {
[RelationsHelperEvent.Add]: (event: MatrixEvent) => void;
}
/**
* Helper class that manages a specific event type relation for an event.
* Just create an instance and listen for new events for that relation.
* Optionally receive the current events by calling emitCurrent().
* Clean up everything by calling destroy().
*/
export class RelationsHelper extends TypedEventEmitter<RelationsHelperEvent, EventMap> implements IDestroyable {
private relations?: Relations;
private eventId: string;
private roomId: string;
public constructor(
private event: MatrixEvent,
private relationType: RelationType,
private relationEventType: string,
private client: MatrixClient,
) {
super();
const eventId = event.getId();
if (!eventId) {
throw new Error("unable to create RelationsHelper: missing event ID");
}
const roomId = event.getRoomId();
if (!roomId) {
throw new Error("unable to create RelationsHelper: missing room ID");
}
this.eventId = eventId;
this.roomId = roomId;
this.setUpRelations();
}
private setUpRelations = (): void => {
this.setRelations();
if (this.relations) {
this.relations.on(RelationsEvent.Add, this.onRelationsAdd);
} else {
this.event.once(MatrixEventEvent.RelationsCreated, this.onRelationsCreated);
}
};
private onRelationsCreated = (): void => {
this.setRelations();
if (this.relations) {
this.relations.on(RelationsEvent.Add, this.onRelationsAdd);
this.emitCurrent();
} else {
this.event.once(MatrixEventEvent.RelationsCreated, this.onRelationsCreated);
}
};
private setRelations(): void {
const room = this.client.getRoom(this.event.getRoomId());
this.relations = room
?.getUnfilteredTimelineSet()
?.relations?.getChildEventsForEvent(this.eventId, this.relationType, this.relationEventType);
}
private onRelationsAdd = (event: MatrixEvent): void => {
this.emit(RelationsHelperEvent.Add, event);
};
public emitCurrent(): void {
this.relations?.getRelations()?.forEach((e) => this.emit(RelationsHelperEvent.Add, e));
}
public getCurrent(): MatrixEvent[] {
return this.relations?.getRelations() || [];
}
/**
* Fetches all related events from the server and emits them.
*/
public async emitFetchCurrent(): Promise<void> {
let nextBatch: string | undefined = undefined;
do {
const response = await this.client.relations(
this.roomId,
this.eventId,
this.relationType,
this.relationEventType,
{
from: nextBatch,
limit: 50,
},
);
nextBatch = response?.nextBatch ?? undefined;
response?.events.forEach((e) => this.emit(RelationsHelperEvent.Add, e));
} while (nextBatch);
}
public destroy(): void {
this.removeAllListeners();
this.event.off(MatrixEventEvent.RelationsCreated, this.onRelationsCreated);
if (this.relations) {
this.relations.off(RelationsEvent.Add, this.onRelationsAdd);
}
}
}
+3 -1
View File
@@ -17,7 +17,9 @@ const getValue = <T>(key: string, initialValue: T): T => {
}
};
// Hook behaving like useState but persisting the value to localStorage. Returns same as useState
/**
* Hook behaving like useState but persisting the value to localStorage. Returns same as useState
*/
export const useLocalStorageState = <T>(key: string, initialValue: T): [T, Dispatch<T>] => {
const lsKey = "mx_" + key;
+1 -15
View File
@@ -1013,14 +1013,6 @@
"set_up_toast_title": "Set up Secure Backup",
"turn_on_key_storage": "Turn on key storage",
"turn_on_key_storage_description": "This will allow you to view your chat history on any new devices and is required for backup of chats and digital identity.",
"udd": {
"interactive_verification_button": "Interactively verify by emoji",
"other_ask_verify_text": "Ask this user to verify their session, or manually verify it below.",
"other_new_session_text": "%(name)s (%(userId)s) signed in to a new session without verifying it:",
"own_ask_verify_text": "Verify your other session using one of the options below.",
"own_new_session_text": "You signed in to a new session without verifying it:",
"title": "Not Trusted"
},
"unable_to_setup_keys_error": "Unable to set up keys",
"verification": {
"accepting": "Accepting…",
@@ -4077,13 +4069,7 @@
"close_to_view_right_panel": "Close this widget to view it in this panel",
"context_menu": {
"delete": "Delete widget",
"delete_warning": "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?",
"move_left": "Move left",
"move_right": "Move right",
"remove": "Remove for everyone",
"revoke": "Revoke permissions",
"screenshot": "Take a picture",
"start_audio_stream": "Start audio stream"
"delete_warning": "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?"
},
"cookie_warning": "This widget may use cookies.",
"error_hangup_description": "You were disconnected from the call. (Error: %(message)s)",
+6 -3
View File
@@ -96,9 +96,12 @@ export function getUserLanguage(): string {
}
}
// Allow overriding the text displayed when no translation exists
// Currently only used in unit tests to avoid having to load
// the translations in element-web
/**
* Allow overriding the text displayed when no translation exists
* Currently only used in unit tests to avoid having to load
* the translations in element-web
* @knipignore
*/
export function setMissingEntryGenerator(f: (value: string) => void): void {
setMissingEntryGeneratorSharedComponents(f);
}
@@ -1,54 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { xxHash32 } from "js-xxhash";
/**
* The PhasedRolloutFeature class is used to manage the phased rollout of a new feature.
*
* It uses a hash of the user's identifier and the feature name to determine if a feature is enabled for a specific user.
* The rollout percentage determines the probability that a user will be enabled for the feature.
* The feature will be enabled for all users if the rollout percentage is 100, and for no users if the percentage is 0.
* If a user is enabled for a feature at x% rollout, it will also be for any greater than x percent.
*
* The process ensures a uniform distribution of enabled features across users.
*
* @property featureName - The name of the feature to be rolled out.
* @property rolloutPercentage - The int percentage (0..100) of users for whom the feature should be enabled.
*/
export class PhasedRolloutFeature {
public readonly featureName: string;
private readonly rolloutPercentage: number;
private readonly seed: number;
public constructor(featureName: string, rolloutPercentage: number) {
this.featureName = featureName;
if (!Number.isInteger(rolloutPercentage) || rolloutPercentage < 0 || rolloutPercentage > 100) {
throw new Error("Rollout percentage must be an integer between 0 and 100");
}
this.rolloutPercentage = rolloutPercentage;
// We add the feature name for the seed to ensure that the hash is different for each feature
this.seed = Array.from(featureName).reduce((sum, char) => sum + char.charCodeAt(0), 0);
}
/**
* Returns true if the feature should be enabled for the given user.
* @param userIdentifier - Some unique identifier for the user, e.g. their user ID or device ID.
*/
public isFeatureEnabled(userIdentifier: string): boolean {
/*
* We use a hash function to convert the unique user ID string into an integer.
* This integer can then be used as a basis for deciding whether the user should have access to the new feature.
* We need some hash with good uniform distribution properties, security is not a concern here.
* We use xxHash32, which is fast and has good distribution properties.
*/
const hash = xxHash32(userIdentifier, this.seed);
// We use the hash modulo 100 to get a number between 0 and 99.
// Modulo is simple and effective and the distribution should be uniform enough for our purposes.
return hash % 100 < this.rolloutPercentage;
}
}
-61
View File
@@ -1,61 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2021 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { describe, it, expect } from "vitest";
import { getEnumValues, isEnumValue } from "./enums";
enum TestStringEnum {
First = "__first__",
Second = "__second__",
}
enum TestNumberEnum {
FirstKey = 10,
SecondKey = 20,
}
describe("enums", () => {
describe("getEnumValues", () => {
it("should work on string enums", () => {
const result = getEnumValues(TestStringEnum);
expect(result).toBeDefined();
expect(result).toHaveLength(2);
expect(result).toEqual(["__first__", "__second__"]);
});
it("should work on number enums", () => {
const result = getEnumValues(TestNumberEnum);
expect(result).toBeDefined();
expect(result).toHaveLength(2);
expect(result).toEqual([10, 20]);
});
});
describe("isEnumValue", () => {
it("should return true on values in a string enum", () => {
const result = isEnumValue(TestStringEnum, "__first__");
expect(result).toBe(true);
});
it("should return false on values not in a string enum", () => {
const result = isEnumValue(TestStringEnum, "not a value");
expect(result).toBe(false);
});
it("should return true on values in a number enum", () => {
const result = isEnumValue(TestNumberEnum, 10);
expect(result).toBe(true);
});
it("should return false on values not in a number enum", () => {
const result = isEnumValue(TestStringEnum, 99);
expect(result).toBe(false);
});
});
});
-41
View File
@@ -1,41 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
/**
* Get the values for an enum.
* @param e The enum.
* @returns The enum values.
*/
export function getEnumValues(e: any): (string | number)[] {
// String-based enums will simply be objects ({Key: "value"}), but number-based
// enums will instead map themselves twice: in one direction for {Key: 12} and
// the reverse for easy lookup, presumably ({12: Key}). In the reverse mapping,
// the key is a string, not a number.
//
// For this reason, we try to determine what kind of enum we're dealing with.
const keys = Object.keys(e);
const values: (string | number)[] = [];
for (const key of keys) {
const value = e[key];
if (Number.isFinite(value) || e[value.toString()] !== Number(key)) {
values.push(value);
}
}
return values;
}
/**
* Determines if a given value is a valid value for the provided enum.
* @param e The enum to check against.
* @param val The value to search for.
* @returns True if the enum contains the value.
*/
export function isEnumValue<T>(e: T, val: string | number): boolean {
return getEnumValues(e).includes(val);
}