Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2025-12-02 16:54:55 +00:00
parent d0235f83d8
commit 39a24dbbe1
No known key found for this signature in database
GPG Key ID: A2B008A5F49F5D0D
35 changed files with 515 additions and 50 deletions

View File

@ -16,6 +16,7 @@ import * as Rooms from "../Rooms";
import { _t } from "../languageHandler";
import { type AsyncActionPayload } from "../dispatcher/payloads";
import ErrorDialog from "../components/views/dialogs/ErrorDialog";
import { DefaultTagID, type TagID } from "../utils/room/tagRoom.ts";
export default class RoomListActions {
/**
@ -39,28 +40,9 @@ export default class RoomListActions {
room: Room,
oldTag: TagID | null,
newTag: TagID | null,
newIndex: number,
): AsyncActionPayload {
let metaData: Parameters<MatrixClient["setRoomTag"]>[2] | undefined;
// Is the tag ordered manually?
const store = RoomListStore.instance;
if (newTag && store.getTagSorting(newTag) === SortAlgorithm.Manual) {
const newList = [...store.orderedLists[newTag]];
newList.sort((a, b) => a.tags[newTag].order - b.tags[newTag].order);
const indexBefore = newIndex - 1;
const indexAfter = newIndex;
const prevOrder = indexBefore <= 0 ? 0 : newList[indexBefore].tags[newTag].order;
const nextOrder = indexAfter >= newList.length ? 1 : newList[indexAfter].tags[newTag].order;
metaData = {
order: (prevOrder + nextOrder) / 2.0,
};
}
return asyncAction(
"RoomListActions.tagRoom",
() => {

View File

@ -67,6 +67,7 @@ import { MatrixClientContextProvider } from "./MatrixClientContextProvider";
import { Landmark, LandmarkNavigation } from "../../accessibility/LandmarkNavigation";
import { ModuleApi } from "../../modules/Api.ts";
import { SDKContext } from "../../contexts/SDKContext.ts";
import { DefaultTagID } from "../../utils/room/tagRoom.ts";
// We need to fetch each pinned message individually (if we don't already have it)
// so each pinned message may trigger a request. Limit the number per room for sanity.

View File

@ -10,6 +10,7 @@ import { useEffect, useState } from "react";
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
import { useDmMember, usePresence, type Presence } from "../../views/avatars/WithPresenceIndicator";
import { DefaultTagID } from "../../../utils/room/tagRoom.ts";
export enum AvatarBadgeDecoration {
LowPriority = "LowPriority",

View File

@ -30,7 +30,7 @@ import { type RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"
import { ReportRoomDialog } from "../../views/dialogs/ReportRoomDialog";
import { Key } from "../../../Keyboard";
import { usePinnedEvents } from "../../../hooks/usePinnedEvents";
import { tagRoom } from "../../../utils/room/tagRoom";
import { DefaultTagID, tagRoom } from "../../../utils/room/tagRoom";
import { inviteToRoom } from "../../../utils/room/inviteToRoom";
export interface RoomSummaryCardState {
@ -164,9 +164,7 @@ export function useRoomSummaryCardViewModel(
// value to check if the user can invite to the room
const canInviteToState = useEventEmitterState(room, RoomStateEvent.Update, () => canInviteTo(room));
const roomTags = useEventEmitterState(RoomListStore.instance, LISTS_UPDATE_EVENT, () =>
RoomListStore.instance.getTagsForRoom(room),
);
const roomTags = useEventEmitterState(RoomListStore.instance, LISTS_UPDATE_EVENT, () => getTagsForRoom(room));
const isFavorite = roomTags.includes(DefaultTagID.Favourite);
const isDirectMessage = useIsDirectMessage(room);

View File

@ -13,14 +13,13 @@ import { useEventEmitterState } from "../../../hooks/useEventEmitter";
import { useUnreadNotifications } from "../../../hooks/useUnreadNotifications";
import { hasAccessToNotificationMenu, hasAccessToOptionsMenu } from "./utils";
import DMRoomMap from "../../../utils/DMRoomMap";
import { DefaultTagID } from "../../../stores/room-list/models";
import { NotificationLevel } from "../../../stores/notifications/NotificationLevel";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
import { UIComponent } from "../../../settings/UIFeature";
import dispatcher from "../../../dispatcher/dispatcher";
import { clearRoomNotification, setMarkedUnreadState } from "../../../utils/notifications";
import PosthogTrackers from "../../../PosthogTrackers";
import { tagRoom } from "../../../utils/room/tagRoom";
import { DefaultTagID, tagRoom } from "../../../utils/room/tagRoom";
import { RoomNotifState } from "../../../RoomNotifs";
import { useNotificationState } from "../../../hooks/useRoomNotificationState";

View File

@ -18,13 +18,13 @@ import { type RoomNotificationState } from "../../../stores/notifications/RoomNo
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
import { useEventEmitter, useEventEmitterState, useTypedEventEmitter } from "../../../hooks/useEventEmitter";
import { DefaultTagID } from "../../../stores/room-list/models";
import { useCall, useConnectionState, useParticipantCount } from "../../../hooks/useCall";
import { CallEvent, type ConnectionState } from "../../../models/Call";
import { NotificationStateEvents } from "../../../stores/notifications/NotificationState";
import DMRoomMap from "../../../utils/DMRoomMap";
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
import { useMessagePreviewToggle } from "./useMessagePreviewToggle";
import { DefaultTagID } from "../../../utils/room/tagRoom.ts";
export interface RoomListItemViewState {
/**

View File

@ -19,7 +19,6 @@ import { useUnreadNotifications } from "../../../hooks/useUnreadNotifications";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { _t } from "../../../languageHandler";
import { NotificationLevel } from "../../../stores/notifications/NotificationLevel";
import { DefaultTagID, type TagID } from "../../../stores/room-list/models";
import DMRoomMap from "../../../utils/DMRoomMap";
import { clearRoomNotification, setMarkedUnreadState } from "../../../utils/notifications";
import { type IProps as IContextMenuProps } from "../../structures/ContextMenu";
@ -33,6 +32,7 @@ import { shouldShowComponent } from "../../../customisations/helpers/UIComponent
import { UIComponent } from "../../../settings/UIFeature";
import { DeveloperToolsOption } from "./DeveloperToolsOption";
import { useSettingValue } from "../../../hooks/useSettings";
import { DefaultTagID, getTagsForRoom, type TagID } from "../../../utils/room/tagRoom.ts";
export interface RoomGeneralContextMenuProps extends IContextMenuProps {
room: Room;
@ -110,9 +110,7 @@ export const RoomGeneralContextMenu: React.FC<RoomGeneralContextMenuProps> = ({
...props
}) => {
const cli = useContext(MatrixClientContext);
const roomTags = useEventEmitterState(RoomListStore.instance, LISTS_UPDATE_EVENT, () =>
RoomListStore.instance.getTagsForRoom(room),
);
const roomTags = useEventEmitterState(RoomListStore.instance, LISTS_UPDATE_EVENT, () => getTagsForRoom(room));
const isDm = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
const wrapHandler = (
handler: (ev: ButtonEvent) => void,
@ -137,10 +135,10 @@ export const RoomGeneralContextMenu: React.FC<RoomGeneralContextMenuProps> = ({
if (!cli) return;
if (tagId === DefaultTagID.Favourite || tagId === DefaultTagID.LowPriority) {
const inverseTag = tagId === DefaultTagID.Favourite ? DefaultTagID.LowPriority : DefaultTagID.Favourite;
const isApplied = RoomListStore.instance.getTagsForRoom(room).includes(tagId);
const isApplied = getTagsForRoom(room).includes(tagId);
const removeTag = isApplied ? tagId : inverseTag;
const addTag = isApplied ? null : tagId;
dis.dispatch(RoomListActions.tagRoom(cli, room, removeTag, addTag, 0));
dis.dispatch(RoomListActions.tagRoom(cli, room, removeTag, addTag));
} else {
logger.warn(`Unexpected tag ${tagId} applied to ${room.roomId}`);
}

View File

@ -26,7 +26,6 @@ import { abbreviateUrl } from "../../../utils/UrlUtils";
import IdentityAuthClient from "../../../IdentityAuthClient";
import { type IInviteResult, inviteMultipleToRoom, showAnyInviteErrors } from "../../../RoomInvite";
import { Action } from "../../../dispatcher/actions";
import { DefaultTagID } from "../../../stores/room-list/models";
import SettingsStore from "../../../settings/SettingsStore";
import { UIFeature } from "../../../settings/UIFeature";
import { mediaFromMxc } from "../../../customisations/Media";
@ -63,6 +62,7 @@ import AskInviteAnywayDialog, { type UnknownProfiles } from "./AskInviteAnywayDi
import { SdkContextClass } from "../../../contexts/SDKContext";
import { type UserProfilesStore } from "../../../stores/UserProfilesStore";
import InviteProgressBody from "./InviteProgressBody.tsx";
import { DefaultTagID } from "../../../utils/room/tagRoom.ts";
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
/* eslint-disable camelcase */

View File

@ -11,13 +11,13 @@ import { type Room, ClientEvent, SyncState, type EmptyObject } from "matrix-js-s
import { type ActionPayload } from "../../dispatcher/payloads";
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
import defaultDispatcher, { type MatrixDispatcher } from "../../dispatcher/dispatcher";
import { DefaultTagID, type TagID } from "../room-list/models";
import { type FetchRoomFn, ListNotificationState } from "./ListNotificationState";
import { RoomNotificationState } from "./RoomNotificationState";
import { SummarizedNotificationState } from "./SummarizedNotificationState";
import { VisibilityProvider } from "../room-list/filters/VisibilityProvider";
import { PosthogAnalytics } from "../../PosthogAnalytics";
import SettingsStore from "../../settings/SettingsStore";
import { DefaultTagID, type TagID } from "../../utils/room/tagRoom.ts";
export const UPDATE_STATUS_INDICATOR = Symbol("update-status-indicator");

View File

@ -12,8 +12,8 @@ import { NotificationLevel } from "./NotificationLevel";
import { arrayDiff } from "../../utils/arrays";
import { type RoomNotificationState } from "./RoomNotificationState";
import { NotificationState, NotificationStateEvents } from "./NotificationState";
import { DefaultTagID } from "../room-list/models";
import { RoomNotificationStateStore } from "./RoomNotificationStateStore";
import { DefaultTagID, getTagsForRoom } from "../../utils/room/tagRoom.ts";
export class SpaceNotificationState extends NotificationState {
public rooms: Room[] = []; // exposed only for tests
@ -69,7 +69,7 @@ export class SpaceNotificationState extends NotificationState {
this._level = NotificationLevel.None;
for (const [roomId, state] of Object.entries(this.states)) {
const room = this.rooms.find((r) => r.roomId === roomId);
const roomTags = room ? RoomListStore.instance.getTagsForRoom(room) : [];
const roomTags = room ? getTagsForRoom(room) : [];
// We ignore unreads in LowPriority rooms, see https://github.com/vector-im/element-web/issues/16836
if (roomTags.includes(DefaultTagID.LowPriority) && state.level === NotificationLevel.Activity) continue;

View File

@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
import type { Room } from "matrix-js-sdk/src/matrix";
import type { Filter } from ".";
import { FilterKey } from ".";
import { DefaultTagID } from "../../../room-list/models";
import { DefaultTagID } from "../../../../utils/room/tagRoom.ts";
export class FavouriteFilter implements Filter {
public matches(room: Room): boolean {

View File

@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
import type { Room } from "matrix-js-sdk/src/matrix";
import type { Filter } from ".";
import { FilterKey } from ".";
import { DefaultTagID } from "../../../room-list/models";
import { DefaultTagID } from "../../../../utils/room/tagRoom.ts";
export class LowPriorityFilter implements Filter {
public matches(room: Room): boolean {

View File

@ -7,9 +7,8 @@ Please see LICENSE files in the repository root for full details.
import type { Room } from "matrix-js-sdk/src/matrix";
import { type Sorter, SortingAlgorithm } from ".";
import { getLastTs } from "../../../room-list/algorithms/tag-sorting/RecentAlgorithm";
import { RoomNotificationStateStore } from "../../../notifications/RoomNotificationStateStore";
import { DefaultTagID } from "../../../room-list/models";
import { DefaultTagID } from "../../../../utils/room/tagRoom.ts";
export class RecencySorter implements Sorter {
public constructor(private myUserId: string) {}

View File

@ -21,7 +21,6 @@ import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
import defaultDispatcher from "../../dispatcher/dispatcher";
import { MessageEventPreview } from "./previews/MessageEventPreview";
import { PollStartEventPreview } from "./previews/PollStartEventPreview";
import { type TagID } from "./models";
import { LegacyCallInviteEventPreview } from "./previews/LegacyCallInviteEventPreview";
import { LegacyCallAnswerEventPreview } from "./previews/LegacyCallAnswerEventPreview";
import { LegacyCallHangupEvent } from "./previews/LegacyCallHangupEvent";
@ -30,6 +29,7 @@ import { ReactionEventPreview } from "./previews/ReactionEventPreview";
import { UPDATE_EVENT } from "../AsyncStore";
import { type IPreview } from "./previews/IPreview";
import shouldHideEvent from "../../shouldHideEvent";
import { TagID } from "../../utils/room/tagRoom.ts";
// Emitted event for when a room's preview has changed. First argument will the room for which
// the change happened.

View File

@ -0,0 +1,34 @@
/*
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.
*/
import { type Room } from "matrix-js-sdk/src/matrix";
import { type EventEmitter } from "events";
export const FILTER_CHANGED = "filter_changed";
/**
* A filter condition for the room list, determining if a room
* should be shown or not.
*
* All filter conditions are expected to be stable executions,
* meaning that given the same input the same answer will be
* returned (thus allowing caching). As such, filter conditions
* can, but shouldn't, do heavier logic and not worry about being
* called constantly by the room list. When the condition changes
* such that different inputs lead to different answers (such
* as a change in the user's input), this emits FILTER_CHANGED.
*/
export interface IFilterCondition extends EventEmitter {
/**
* Determines if a given room should be visible under this
* condition.
* @param room The room to check.
* @returns True if the room should be visible.
*/
isVisible(room: Room): boolean;
}

View File

@ -0,0 +1,72 @@
/*
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 { EventEmitter } from "events";
import { type Room } from "matrix-js-sdk/src/matrix";
import { FILTER_CHANGED, type IFilterCondition } from "./IFilterCondition";
import { type IDestroyable } from "../../../utils/IDestroyable";
import SpaceStore from "../../spaces/SpaceStore";
import { isMetaSpace, MetaSpace, type SpaceKey } from "../../spaces";
import { setHasDiff } from "../../../utils/sets";
import SettingsStore from "../../../settings/SettingsStore";
/**
* A filter condition for the room list which reveals rooms which
* are a member of a given space or if no space is selected shows:
* + Orphaned rooms (ones not in any space you are a part of)
* + All DMs
*/
export class SpaceFilterCondition extends EventEmitter implements IFilterCondition, IDestroyable {
private roomIds = new Set<string>();
private userIds = new Set<string>();
private showPeopleInSpace = true;
private space: SpaceKey = MetaSpace.Home;
public isVisible(room: Room): boolean {
return SpaceStore.instance.isRoomInSpace(this.space, room.roomId);
}
private onStoreUpdate = async (forceUpdate = false): Promise<void> => {
const beforeRoomIds = this.roomIds;
// clone the set as it may be mutated by the space store internally
this.roomIds = new Set(SpaceStore.instance.getSpaceFilteredRoomIds(this.space));
const beforeUserIds = this.userIds;
// clone the set as it may be mutated by the space store internally
this.userIds = new Set(SpaceStore.instance.getSpaceFilteredUserIds(this.space));
const beforeShowPeopleInSpace = this.showPeopleInSpace;
this.showPeopleInSpace =
isMetaSpace(this.space[0]) || SettingsStore.getValue("Spaces.showPeopleInSpace", this.space);
if (
forceUpdate ||
beforeShowPeopleInSpace !== this.showPeopleInSpace ||
setHasDiff(beforeRoomIds, this.roomIds) ||
setHasDiff(beforeUserIds, this.userIds)
) {
this.emit(FILTER_CHANGED);
// XXX: Room List Store has a bug where updates to the pre-filter during a local echo of a
// tags transition seem to be ignored, so refire in the next tick to work around it
setTimeout(() => {
this.emit(FILTER_CHANGED);
});
}
};
public updateSpace(space: SpaceKey): void {
SpaceStore.instance.off(this.space, this.onStoreUpdate);
SpaceStore.instance.on((this.space = space), this.onStoreUpdate);
this.onStoreUpdate(true); // initial update from the change to the space
}
public destroy(): void {
SpaceStore.instance.off(this.space, this.onStoreUpdate);
}
}

View File

@ -0,0 +1,48 @@
/*
* 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 { type Room } from "matrix-js-sdk/src/matrix";
import { RoomListCustomisations } from "../../../customisations/RoomList";
import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom";
export class VisibilityProvider {
private static internalInstance: VisibilityProvider;
private constructor() {}
public static get instance(): VisibilityProvider {
if (!VisibilityProvider.internalInstance) {
VisibilityProvider.internalInstance = new VisibilityProvider();
}
return VisibilityProvider.internalInstance;
}
public isRoomVisible(room?: Room): boolean {
if (!room) {
return false;
}
// hide space rooms as they'll be shown in the SpacePanel
if (room.isSpaceRoom()) {
return false;
}
if (isLocalRoom(room)) {
// local rooms shouldn't show up anywhere
return false;
}
const isVisibleFn = RoomListCustomisations.isRoomVisible;
if (isVisibleFn) {
return isVisibleFn(room);
}
return true; // default
}
}

View File

@ -0,0 +1,25 @@
/*
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 { type MatrixEvent } from "matrix-js-sdk/src/matrix";
import { type TagID } from "../../../utils/room/tagRoom.ts";
/**
* Represents an event preview.
*/
export interface IPreview {
/**
* Gets the text which represents the event as a preview.
* @param event The event to preview.
* @param tagId Optional. The tag where the room the event was sent in resides.
* @param isThread Optional. Whether the preview being generated is for a thread summary.
* @returns The preview.
*/
getTextFor(event: MatrixEvent, tagId?: TagID, isThread?: boolean): string | null;
}

View File

@ -0,0 +1,28 @@
/*
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 { type MatrixEvent } from "matrix-js-sdk/src/matrix";
import { type IPreview } from "./IPreview";
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
import { _t } from "../../../languageHandler";
import { type TagID } from "../../../utils/room/tagRoom.ts";
export class LegacyCallAnswerEventPreview implements IPreview {
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
if (shouldPrefixMessagesIn(event.getRoomId()!, tagId)) {
if (isSelf(event)) {
return _t("event_preview|m.call.answer|you");
} else {
return _t("event_preview|m.call.answer|user", { senderName: getSenderName(event) });
}
} else {
return _t("event_preview|m.call.answer|dm");
}
}
}

View File

@ -0,0 +1,28 @@
/*
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 { type MatrixEvent } from "matrix-js-sdk/src/matrix";
import { type IPreview } from "./IPreview";
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
import { _t } from "../../../languageHandler";
import { type TagID } from "../../../utils/room/tagRoom.ts";
export class LegacyCallHangupEvent implements IPreview {
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
if (shouldPrefixMessagesIn(event.getRoomId()!, tagId)) {
if (isSelf(event)) {
return _t("event_preview|m.call.hangup|you");
} else {
return _t("event_preview|m.call.hangup|user", { senderName: getSenderName(event) });
}
} else {
return _t("timeline|m.call.hangup|dm");
}
}
}

View File

@ -0,0 +1,32 @@
/*
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 { type MatrixEvent } from "matrix-js-sdk/src/matrix";
import { type IPreview } from "./IPreview";
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
import { _t } from "../../../languageHandler";
import { type TagID } from "../../../utils/room/tagRoom.ts";
export class LegacyCallInviteEventPreview implements IPreview {
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
if (shouldPrefixMessagesIn(event.getRoomId()!, tagId)) {
if (isSelf(event)) {
return _t("event_preview|m.call.invite|you");
} else {
return _t("event_preview|m.call.invite|user", { senderName: getSenderName(event) });
}
} else {
if (isSelf(event)) {
return _t("event_preview|m.call.invite|dm_send");
} else {
return _t("event_preview|m.call.invite|dm_receive", { senderName: getSenderName(event) });
}
}
}
}

View File

@ -9,11 +9,11 @@ Please see LICENSE files in the repository root for full details.
import { type MatrixEvent, MsgType, RelationType } from "matrix-js-sdk/src/matrix";
import { type IPreview } from "./IPreview";
import { type TagID } from "../models";
import { _t, sanitizeForTranslation } from "../../../languageHandler";
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
import { getHtmlText } from "../../../HtmlUtils";
import { stripHTMLReply, stripPlainReply } from "../../../utils/Reply";
import { type TagID } from "../../../utils/room/tagRoom.ts";
export class MessageEventPreview implements IPreview {
public getTextFor(event: MatrixEvent, tagId?: TagID, isThread?: boolean): string | null {

View File

@ -0,0 +1,57 @@
/*
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 { type MatrixEvent, type PollStartEventContent } from "matrix-js-sdk/src/matrix";
import { InvalidEventError } from "matrix-js-sdk/src/extensible_events_v1/InvalidEventError";
import { PollStartEvent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent";
import { type IPreview } from "./IPreview";
import { _t, sanitizeForTranslation } from "../../../languageHandler";
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { TagID } from "../../../utils/room/tagRoom.ts";
export class PollStartEventPreview implements IPreview {
public static contextType = MatrixClientContext;
declare public context: React.ContextType<typeof MatrixClientContext>;
public getTextFor(event: MatrixEvent, tagId?: TagID, isThread?: boolean): string | null {
let eventContent = event.getContent();
if (event.isRelation("m.replace")) {
// It's an edit, generate the preview on the new text
eventContent = event.getContent()["m.new_content"];
}
// Check we have the information we need, and bail out if not
if (!eventContent) {
return null;
}
try {
const poll = new PollStartEvent({
type: event.getType(),
content: eventContent as PollStartEventContent,
});
let question = poll.question.text.trim();
question = sanitizeForTranslation(question);
if (isThread || isSelf(event) || !shouldPrefixMessagesIn(event.getRoomId()!, tagId)) {
return question;
} else {
return _t("event_preview|m.text", { senderName: getSenderName(event), message: question });
}
} catch (e) {
if (e instanceof InvalidEventError) {
return null;
}
throw e; // re-throw unknown errors
}
}
}

View File

@ -0,0 +1,48 @@
/*
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 { type MatrixEvent } from "matrix-js-sdk/src/matrix";
import { type IPreview } from "./IPreview";
import { getSenderName, isSelf } from "./utils";
import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { MessagePreviewStore } from "../MessagePreviewStore";
import { TagID } from "../../../utils/room/tagRoom.ts";
export class ReactionEventPreview implements IPreview {
public getTextFor(event: MatrixEvent, tagId?: TagID, isThread?: boolean): string | null {
const roomId = event.getRoomId();
if (!roomId) return null; // not a room event
const relation = event.getRelation();
if (!relation) return null; // invalid reaction (probably redacted)
const reaction = relation.key;
if (!reaction) return null; // invalid reaction (unknown format)
const cli = MatrixClientPeg.get();
const room = cli?.getRoom(roomId);
const relatedEvent = relation.event_id ? room?.findEventById(relation.event_id) : null;
if (!relatedEvent) return null;
const message = MessagePreviewStore.instance.generatePreviewForEvent(relatedEvent);
if (isSelf(event)) {
return _t("event_preview|m.reaction|you", {
reaction,
message,
});
}
return _t("event_preview|m.reaction|user", {
sender: getSenderName(event),
reaction,
message,
});
}
}

View File

@ -0,0 +1,27 @@
/*
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 { type MatrixEvent } from "matrix-js-sdk/src/matrix";
import { type IPreview } from "./IPreview";
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
import { _t } from "../../../languageHandler";
import { type TagID } from "../../../utils/room/tagRoom.ts";
export class StickerEventPreview implements IPreview {
public getTextFor(event: MatrixEvent, tagId?: TagID, isThread?: boolean): string | null {
const stickerName = event.getContent()["body"];
if (!stickerName) return null;
if (isThread || isSelf(event) || !shouldPrefixMessagesIn(event.getRoomId()!, tagId)) {
return stickerName;
} else {
return _t("event_preview|m.sticker", { senderName: getSenderName(event), stickerName });
}
}
}

View File

@ -0,0 +1,33 @@
/*
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 { type MatrixEvent } from "matrix-js-sdk/src/matrix";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { DefaultTagID, type TagID } from "../../../utils/room/tagRoom.ts";
export function isSelf(event: MatrixEvent): boolean {
const selfUserId = MatrixClientPeg.safeGet().getSafeUserId();
if (event.getType() === "m.room.member") {
return event.getStateKey() === selfUserId;
}
return event.getSender() === selfUserId;
}
export function shouldPrefixMessagesIn(roomId: string, tagId?: TagID): boolean {
if (tagId !== DefaultTagID.DM) return true;
// We don't prefix anything in 1:1s
const room = MatrixClientPeg.safeGet().getRoom(roomId);
if (!room) return true;
return room.currentState.getJoinedMemberCount() !== 2;
}
export function getSenderName(event: MatrixEvent): string {
return event.sender?.name ?? event.getSender() ?? "";
}

View File

@ -28,7 +28,6 @@ import SettingsStore from "../../settings/SettingsStore";
import DMRoomMap from "../../utils/DMRoomMap";
import { SpaceNotificationState } from "../notifications/SpaceNotificationState";
import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore";
import { DefaultTagID } from "../room-list/models";
import { EnhancedMap, mapDiff } from "../../utils/maps";
import { setDiff, setHasDiff } from "../../utils/sets";
import { Action } from "../../dispatcher/actions";
@ -62,6 +61,7 @@ import { type AfterLeaveRoomPayload } from "../../dispatcher/payloads/AfterLeave
import { SdkContextClass } from "../../contexts/SDKContext";
import { ModuleApi } from "../../modules/Api.ts";
import RoomListStoreV3 from "../room-list-v3/RoomListStoreV3.ts";
import { DefaultTagID } from "../../utils/room/tagRoom.ts";
const ACTIVE_SPACE_LS_KEY = "mx_active_space";

View File

@ -6,12 +6,63 @@ 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 { type Room } from "matrix-js-sdk/src/matrix";
import { JoinRule, type Room } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import { DefaultTagID, type TagID } from "../../stores/room-list/models";
import RoomListActions from "../../actions/RoomListActions";
import dis from "../../dispatcher/dispatcher";
import DMRoomMap from "../DMRoomMap.ts";
import { EffectiveMembership, getEffectiveMembership, getEffectiveMembershipTag } from "../membership.ts";
export enum DefaultTagID {
Invite = "im.vector.fake.invite",
Untagged = "im.vector.fake.recent", // legacy: used to just be 'recent rooms' but now it's all untagged rooms
Archived = "im.vector.fake.archived",
LowPriority = "m.lowpriority",
Favourite = "m.favourite",
DM = "im.vector.fake.direct",
Conference = "im.vector.fake.conferences",
ServerNotice = "m.server_notice",
Suggested = "im.vector.fake.suggested",
}
export type TagID = string | DefaultTagID;
export function getTagsForRoom(room: Room): TagID[] {
const tags: TagID[] = [];
if (!getEffectiveMembership(room.getMyMembership())) return []; // peeked room has no tags
const membership = getEffectiveMembershipTag(room);
if (membership === EffectiveMembership.Invite) {
tags.push(DefaultTagID.Invite);
} else if (membership === EffectiveMembership.Leave) {
tags.push(DefaultTagID.Archived);
} else {
tags.push(...getTagsOfJoinedRoom(room));
}
if (!tags.length) tags.push(DefaultTagID.Untagged);
return tags;
}
function getTagsOfJoinedRoom(room: Room): TagID[] {
let tags = Object.keys(room.tags || {});
if (tags.length === 0) {
// Check to see if it's a DM if it isn't anything else
if (DMRoomMap.shared().getUserIdForRoomId(room.roomId)) {
tags = [DefaultTagID.DM];
}
}
if (room.isCallRoom() && (room.getJoinRule() === JoinRule.Public || room.getJoinRule() === JoinRule.Knock)) {
tags.push(DefaultTagID.Conference);
}
return tags;
}
/**
* Toggle tag for a given room
@ -21,10 +72,10 @@ import dis from "../../dispatcher/dispatcher";
export function tagRoom(room: Room, tagId: TagID): void {
if (tagId === DefaultTagID.Favourite || tagId === DefaultTagID.LowPriority) {
const inverseTag = tagId === DefaultTagID.Favourite ? DefaultTagID.LowPriority : DefaultTagID.Favourite;
const isApplied = RoomListStore.instance.getTagsForRoom(room).includes(tagId);
const isApplied = getTagsForRoom(room).includes(tagId);
const removeTag = isApplied ? tagId : inverseTag;
const addTag = isApplied ? null : tagId;
dis.dispatch(RoomListActions.tagRoom(room.client, room, removeTag, addTag, 0));
dis.dispatch(RoomListActions.tagRoom(room.client, room, removeTag, addTag));
} else {
logger.warn(`Unexpected tag ${tagId} applied to ${room.roomId}`);
}

View File

@ -15,6 +15,7 @@ import {
import { createTestClient, mkStubRoom } from "../../../../test-utils";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import * as PresenceIndicatorModule from "../../../../../src/components/views/avatars/WithPresenceIndicator";
import { DefaultTagID } from "../../../../../src/utils/room/tagRoom.ts";
jest.mock("../../../../../src/utils/room/getJoinedNonFunctionalMembers", () => ({
getJoinedNonFunctionalMembers: jest.fn().mockReturnValue([]),

View File

@ -19,7 +19,7 @@ import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import { useUnreadNotifications } from "../../../../../src/hooks/useUnreadNotifications";
import { NotificationLevel } from "../../../../../src/stores/notifications/NotificationLevel";
import { clearRoomNotification, setMarkedUnreadState } from "../../../../../src/utils/notifications";
import { tagRoom } from "../../../../../src/utils/room/tagRoom";
import { DefaultTagID, tagRoom } from "../../../../../src/utils/room/tagRoom";
import dispatcher from "../../../../../src/dispatcher/dispatcher";
import { useNotificationState } from "../../../../../src/hooks/useRoomNotificationState";
import { RoomNotifState } from "../../../../../src/RoomNotifs";

View File

@ -27,6 +27,7 @@ import { shouldShowComponent } from "../../../../../src/customisations/helpers/U
import { UIComponent } from "../../../../../src/settings/UIFeature";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { clearAllModals } from "../../../../test-utils";
import { DefaultTagID } from "../../../../../src/utils/room/tagRoom.ts";
jest.mock("../../../../../src/customisations/helpers/UIComponents", () => ({
shouldShowComponent: jest.fn(),

View File

@ -40,6 +40,7 @@ import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
import { RoomNotificationStateStore } from "../../../src/stores/notifications/RoomNotificationStateStore";
import { NotificationLevel } from "../../../src/stores/notifications/NotificationLevel";
import { storeRoomAliasInCache } from "../../../src/RoomAliasCache.ts";
import { DefaultTagID } from "../../../src/utils/room/tagRoom.ts";
jest.useFakeTimers();

View File

@ -28,6 +28,7 @@ import SettingsStore from "../../../../src/settings/SettingsStore";
import * as utils from "../../../../src/utils/notifications";
import { Action } from "../../../../src/dispatcher/actions";
import { SettingLevel } from "../../../../src/settings/SettingLevel.ts";
import { DefaultTagID } from "../../../../src/utils/room/tagRoom.ts";
describe("RoomListStoreV3", () => {
async function getRoomListStore() {

View File

@ -20,8 +20,8 @@ import {
import { MessagePreviewStore } from "../../../../src/stores/room-list/MessagePreviewStore";
import { mkEvent, mkMessage, mkReaction, setupAsyncStoreWithClient, stubClient } from "../../../test-utils";
import { DefaultTagID } from "../../../../src/stores/room-list/models";
import { mkThread } from "../../../test-utils/threads";
import { DefaultTagID } from "../../../../src/utils/room/tagRoom.ts";
describe("MessagePreviewStore", () => {
let client: Mocked<MatrixClient>;

View File

@ -10,7 +10,7 @@ import { Room } from "matrix-js-sdk/src/matrix";
import RoomListActions from "../../../../src/actions/RoomListActions";
import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
import { tagRoom } from "../../../../src/utils/room/tagRoom";
import { DefaultTagID, type TagID, tagRoom } from "../../../../src/utils/room/tagRoom";
import { getMockClientWithEventEmitter } from "../../../test-utils";
describe("tagRoom()", () => {