From 4dd214506b1dbf2c70a3d402704b929ad386b7fb Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 5 May 2023 08:45:14 +0100 Subject: [PATCH] Move reaction message previews out of labs (#10601) * Update reaction message previews to match designs * Delabs reaction message previews * tsc strict * Iterate * Add test * Iterate --- .../context_menus/MessageContextMenu.tsx | 4 +- src/i18n/strings/en_EN.json | 6 +- src/settings/Settings.tsx | 18 --- .../previews/ReactionEventPreview.ts | 38 ++--- src/stores/room-list/previews/utils.ts | 4 +- .../tabs/user/LabsUserSettingsTab-test.tsx | 2 +- .../previews/PollStartEventPreview-test.ts | 1 + .../previews/ReactionEventPreview-test.ts | 139 ++++++++++++++++++ 8 files changed, 167 insertions(+), 45 deletions(-) create mode 100644 test/stores/room-list/previews/ReactionEventPreview-test.ts diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index 3ffe63a235..a4cc689349 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -114,7 +114,7 @@ interface IProps extends MenuProps { // True if the menu is being used as a right click menu rightClick?: boolean; // The Relations model from the JS SDK for reactions to `mxEvent` - reactions?: Relations | null | undefined; + reactions?: Relations | null; // A permalink to this event or an href of an anchor element the user has clicked link?: string; @@ -556,7 +556,7 @@ export default class MessageContextMenu extends React.Component } let jumpToRelatedEventButton: JSX.Element | undefined; - const relatedEventId = mxEvent.getWireContent()?.["m.relates_to"]?.event_id; + const relatedEventId = mxEvent.relationEventId; if (relatedEventId && SettingsStore.getValue("developerMode")) { jumpToRelatedEventButton = ( = { [LabGroup.VoiceAndVideo]: _td("Voice & Video"), [LabGroup.Moderation]: _td("Moderation"), [LabGroup.Analytics]: _td("Analytics"), - [LabGroup.MessagePreviews]: _td("Message Previews"), [LabGroup.Themes]: _td("Themes"), [LabGroup.Encryption]: _td("Encryption"), [LabGroup.Experimental]: _td("Experimental"), @@ -298,22 +296,6 @@ export const SETTINGS: { [setting: string]: ISetting } = { supportedLevels: LEVELS_FEATURE, default: false, }, - "feature_roomlist_preview_reactions_dms": { - isFeature: true, - labsGroup: LabGroup.MessagePreviews, - displayName: _td("Show message previews for reactions in DMs"), - supportedLevels: LEVELS_FEATURE, - default: false, - // this option is a subset of `feature_roomlist_preview_reactions_all` so disable it when that one is enabled - controller: new IncompatibleController("feature_roomlist_preview_reactions_all"), - }, - "feature_roomlist_preview_reactions_all": { - isFeature: true, - labsGroup: LabGroup.MessagePreviews, - displayName: _td("Show message previews for reactions in all rooms"), - supportedLevels: LEVELS_FEATURE, - default: false, - }, "feature_dehydration": { isFeature: true, labsGroup: LabGroup.Encryption, diff --git a/src/stores/room-list/previews/ReactionEventPreview.ts b/src/stores/room-list/previews/ReactionEventPreview.ts index 6af7ebab70..a9f0658141 100644 --- a/src/stores/room-list/previews/ReactionEventPreview.ts +++ b/src/stores/room-list/previews/ReactionEventPreview.ts @@ -18,37 +18,39 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { IPreview } from "./IPreview"; import { TagID } from "../models"; -import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils"; +import { getSenderName, isSelf } from "./utils"; import { _t } from "../../../languageHandler"; -import SettingsStore from "../../../settings/SettingsStore"; -import DMRoomMap from "../../../utils/DMRoomMap"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import { MessagePreviewStore } from "../MessagePreviewStore"; export class ReactionEventPreview implements IPreview { public getTextFor(event: MatrixEvent, tagId?: TagID, isThread?: boolean): string | null { - const showDms = SettingsStore.getValue("feature_roomlist_preview_reactions_dms"); - const showAll = SettingsStore.getValue("feature_roomlist_preview_reactions_all"); - const roomId = event.getRoomId(); if (!roomId) return null; // not a room event - // If we're not showing all reactions, see if we're showing DMs instead - if (!showAll) { - // If we're not showing reactions on DMs, or we are and the room isn't a DM, skip - if (!(showDms && DMRoomMap.shared().getUserIdForRoomId(roomId))) { - return null; - } - } - const relation = event.getRelation(); if (!relation) return null; // invalid reaction (probably redacted) const reaction = relation.key; if (!reaction) return null; // invalid reaction (unknown format) - if (isThread || isSelf(event) || !shouldPrefixMessagesIn(roomId, tagId)) { - return reaction; - } else { - return _t("%(senderName)s: %(reaction)s", { senderName: getSenderName(event), reaction }); + 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("You reacted %(reaction)s to %(message)s", { + reaction, + message, + }); } + + return _t("%(sender)s reacted %(reaction)s to %(message)s", { + sender: getSenderName(event), + reaction, + message, + }); } } diff --git a/src/stores/room-list/previews/utils.ts b/src/stores/room-list/previews/utils.ts index 2dfa75966e..ec8ddb28e3 100644 --- a/src/stores/room-list/previews/utils.ts +++ b/src/stores/room-list/previews/utils.ts @@ -20,7 +20,7 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { DefaultTagID, TagID } from "../models"; export function isSelf(event: MatrixEvent): boolean { - const selfUserId = MatrixClientPeg.get().getUserId(); + const selfUserId = MatrixClientPeg.get().getSafeUserId(); if (event.getType() === "m.room.member") { return event.getStateKey() === selfUserId; } @@ -37,5 +37,5 @@ export function shouldPrefixMessagesIn(roomId: string, tagId?: TagID): boolean { } export function getSenderName(event: MatrixEvent): string { - return event.sender ? event.sender.name : event.getSender() || ""; + return event.sender?.name ?? event.getSender() ?? ""; } diff --git a/test/components/views/settings/tabs/user/LabsUserSettingsTab-test.tsx b/test/components/views/settings/tabs/user/LabsUserSettingsTab-test.tsx index ddf6105274..a8001481f6 100644 --- a/test/components/views/settings/tabs/user/LabsUserSettingsTab-test.tsx +++ b/test/components/views/settings/tabs/user/LabsUserSettingsTab-test.tsx @@ -70,7 +70,7 @@ describe("", () => { const { container } = render(getComponent()); const labsSections = container.getElementsByClassName("mx_SettingsTab_section"); - expect(labsSections).toHaveLength(12); + expect(labsSections).toHaveLength(11); }); it("allow setting a labs flag which requires unstable support once support is confirmed", async () => { diff --git a/test/stores/room-list/previews/PollStartEventPreview-test.ts b/test/stores/room-list/previews/PollStartEventPreview-test.ts index 324f42d7b6..dc9bcabfe3 100644 --- a/test/stores/room-list/previews/PollStartEventPreview-test.ts +++ b/test/stores/room-list/previews/PollStartEventPreview-test.ts @@ -22,6 +22,7 @@ import { makePollStartEvent } from "../../../test-utils"; jest.spyOn(MatrixClientPeg, "get").mockReturnValue({ getUserId: () => "@me:example.com", + getSafeUserId: () => "@me:example.com", } as unknown as MatrixClient); describe("PollStartEventPreview", () => { diff --git a/test/stores/room-list/previews/ReactionEventPreview-test.ts b/test/stores/room-list/previews/ReactionEventPreview-test.ts new file mode 100644 index 0000000000..e49dba342c --- /dev/null +++ b/test/stores/room-list/previews/ReactionEventPreview-test.ts @@ -0,0 +1,139 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { RelationType, Room, RoomMember } from "matrix-js-sdk/src/matrix"; +import { mocked } from "jest-mock"; + +import { mkEvent, stubClient } from "../../../test-utils"; +import { ReactionEventPreview } from "../../../../src/stores/room-list/previews/ReactionEventPreview"; +import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; + +describe("ReactionEventPreview", () => { + const preview = new ReactionEventPreview(); + const userId = "@user:example.com"; + const roomId = "!room:example.com"; + + beforeAll(() => { + stubClient(); + }); + + describe("getTextFor", () => { + it("should return null for non-relations", () => { + const event = mkEvent({ + event: true, + content: {}, + user: userId, + type: "m.room.message", + room: roomId, + }); + expect(preview.getTextFor(event)).toBeNull(); + }); + + it("should return null for non-reactions", () => { + const event = mkEvent({ + event: true, + content: { + "body": "", + "m.relates_to": { + rel_type: RelationType.Thread, + event_id: "$foo:bar", + }, + }, + user: userId, + type: "m.room.message", + room: roomId, + }); + expect(preview.getTextFor(event)).toBeNull(); + }); + + it("should use 'You' for your own reactions", () => { + const cli = MatrixClientPeg.get(); + const room = new Room(roomId, cli, userId); + mocked(cli.getRoom).mockReturnValue(room); + + const message = mkEvent({ + event: true, + content: { + "body": "duck duck goose", + "m.relates_to": { + rel_type: RelationType.Thread, + event_id: "$foo:bar", + }, + }, + user: userId, + type: "m.room.message", + room: roomId, + }); + + room.getUnfilteredTimelineSet().addLiveEvent(message, {}); + + const event = mkEvent({ + event: true, + content: { + "m.relates_to": { + rel_type: RelationType.Annotation, + key: "🪿", + event_id: message.getId(), + }, + }, + user: cli.getSafeUserId(), + type: "m.reaction", + room: roomId, + }); + expect(preview.getTextFor(event)).toMatchInlineSnapshot(`"You reacted 🪿 to duck duck goose"`); + }); + + it("should use display name for your others' reactions", () => { + const cli = MatrixClientPeg.get(); + const room = new Room(roomId, cli, userId); + mocked(cli.getRoom).mockReturnValue(room); + + const message = mkEvent({ + event: true, + content: { + "body": "duck duck goose", + "m.relates_to": { + rel_type: RelationType.Thread, + event_id: "$foo:bar", + }, + }, + user: userId, + type: "m.room.message", + room: roomId, + }); + + room.getUnfilteredTimelineSet().addLiveEvent(message, {}); + + const event = mkEvent({ + event: true, + content: { + "m.relates_to": { + rel_type: RelationType.Annotation, + key: "🪿", + event_id: message.getId(), + }, + }, + user: userId, + type: "m.reaction", + room: roomId, + }); + event.sender = new RoomMember(roomId, userId); + event.sender.name = "Bob"; + + expect(preview.getTextFor(event)).toMatchInlineSnapshot(`"Bob reacted 🪿 to duck duck goose"`); + }); + }); +});