diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx index b9295be3ed4..0e9dc1cf155 100644 --- a/src/TextForEvent.tsx +++ b/src/TextForEvent.tsx @@ -441,6 +441,15 @@ function textForPowerEvent(event: MatrixEvent): () => string | null { }); } +const onPinnedOrUnpinnedMessageClick = (messageId: string, roomId: string): void => { + defaultDispatcher.dispatch({ + action: 'view_room', + event_id: messageId, + highlighted: true, + room_id: roomId, + }); +}; + const onPinnedMessagesClick = (): void => { defaultDispatcher.dispatch({ action: Action.SetRightPanelPhase, @@ -452,17 +461,77 @@ const onPinnedMessagesClick = (): void => { function textForPinnedEvent(event: MatrixEvent, allowJSX: boolean): () => string | JSX.Element | null { if (!SettingsStore.getValue("feature_pinning")) return null; const senderName = event.sender ? event.sender.name : event.getSender(); + const roomId = event.getRoomId(); + + const pinned = event.getContent().pinned ?? []; + const previouslyPinned = event.getPrevContent().pinned ?? []; + const newlyPinned = pinned.filter(item => previouslyPinned.indexOf(item) < 0); + const newlyUnpinned = previouslyPinned.filter(item => pinned.indexOf(item) < 0); + + if (newlyPinned.length === 1 && newlyUnpinned.length === 0) { + // A single message was pinned, include a link to that message. + if (allowJSX) { + const messageId = newlyPinned.pop(); + + return () => ( + + { _t( + "%(senderName)s pinned a message to this room. See all pinned messages.", + { senderName }, + { + "a": (sub) => + onPinnedOrUnpinnedMessageClick(messageId, roomId)}> + { sub } + , + "b": (sub) => + + { sub } + , + }, + ) } + + ); + } + + return () => _t("%(senderName)s pinned a message to this room. See all pinned messages.", { senderName }); + } + + if (newlyUnpinned.length === 1 && newlyPinned.length === 0) { + // A single message was unpinned, include a link to that message. + if (allowJSX) { + const messageId = newlyUnpinned.pop(); + + return () => ( + + { _t( + "%(senderName)s unpinned a message from this room. See all pinned messages.", + { senderName }, + { + "a": (sub) => + onPinnedOrUnpinnedMessageClick(messageId, roomId)}> + { sub } + , + "b": (sub) => + + { sub } + , + }, + ) } + + ); + } + + return () => _t("%(senderName)s unpinned a message from this room. See all pinned messages.", { senderName }); + } if (allowJSX) { return () => ( - { - _t( - "%(senderName)s changed the pinned messages for the room.", - { senderName }, - { "a": (sub) => { sub } }, - ) - } + { _t( + "%(senderName)s changed the pinned messages for the room.", + { senderName }, + { "a": (sub) => { sub } }, + ) } ); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 6d8ded4748e..e1accd3d29e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -544,6 +544,10 @@ "%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s made future room history visible to unknown (%(visibility)s).", "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s changed the power level of %(powerLevelDiffText)s.", "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s", + "%(senderName)s pinned a message to this room. See all pinned messages.": "%(senderName)s pinned a message to this room. See all pinned messages.", + "%(senderName)s pinned a message to this room. See all pinned messages.": "%(senderName)s pinned a message to this room. See all pinned messages.", + "%(senderName)s unpinned a message from this room. See all pinned messages.": "%(senderName)s unpinned a message from this room. See all pinned messages.", + "%(senderName)s unpinned a message from this room. See all pinned messages.": "%(senderName)s unpinned a message from this room. See all pinned messages.", "%(senderName)s changed the pinned messages for the room.": "%(senderName)s changed the pinned messages for the room.", "%(senderName)s changed the pinned messages for the room.": "%(senderName)s changed the pinned messages for the room.", "%(widgetName)s widget modified by %(senderName)s": "%(widgetName)s widget modified by %(senderName)s", diff --git a/test/TextForEvent-test.ts b/test/TextForEvent-test.ts new file mode 100644 index 00000000000..b8a459af67a --- /dev/null +++ b/test/TextForEvent-test.ts @@ -0,0 +1,129 @@ +import './skinned-sdk'; + +import { textForEvent } from "../src/TextForEvent"; +import { MatrixEvent } from "matrix-js-sdk"; +import SettingsStore from "../src/settings/SettingsStore"; +import { SettingLevel } from "../src/settings/SettingLevel"; +import renderer from 'react-test-renderer'; + +function mockPinnedEvent( + pinnedMessageIds?: string[], + prevPinnedMessageIds?: string[], +): MatrixEvent { + return new MatrixEvent({ + type: "m.room.pinned_events", + state_key: "", + sender: "@foo:example.com", + content: { + pinned: pinnedMessageIds, + }, + prev_content: { + pinned: prevPinnedMessageIds, + }, + }); +} + +// Helper function that renders a component to a plain text string. +// Once snapshots are introduced in tests, this function will no longer be necessary, +// and should be replaced with snapshots. +function renderComponent(component): string { + const serializeObject = (object): string => { + if (typeof object === 'string') { + return object === ' ' ? '' : object; + } + + if (Array.isArray(object) && object.length === 1 && typeof object[0] === 'string') { + return object[0]; + } + + if (object['type'] !== undefined && typeof object['children'] !== undefined) { + return serializeObject(object.children); + } + + if (!Array.isArray(object)) { + return ''; + } + + return object.map(child => { + return serializeObject(child); + }).join(''); + }; + + return serializeObject(component.toJSON()); +} + +describe('TextForEvent', () => { + describe("TextForPinnedEvent", () => { + SettingsStore.setValue("feature_pinning", null, SettingLevel.DEVICE, true); + + it("mentions message when a single message was pinned, with no previously pinned messages", () => { + const event = mockPinnedEvent(['message-1']); + const plainText = textForEvent(event); + const component = renderer.create(textForEvent(event, true)); + + const expectedText = "@foo:example.com pinned a message to this room. See all pinned messages."; + expect(plainText).toBe(expectedText); + expect(renderComponent(component)).toBe(expectedText); + }); + + it("mentions message when a single message was pinned, with multiple previously pinned messages", () => { + const event = mockPinnedEvent(['message-1', 'message-2', 'message-3'], ['message-1', 'message-2']); + const plainText = textForEvent(event); + const component = renderer.create(textForEvent(event, true)); + + const expectedText = "@foo:example.com pinned a message to this room. See all pinned messages."; + expect(plainText).toBe(expectedText); + expect(renderComponent(component)).toBe(expectedText); + }); + + it("mentions message when a single message was unpinned, with a single message previously pinned", () => { + const event = mockPinnedEvent([], ['message-1']); + const plainText = textForEvent(event); + const component = renderer.create(textForEvent(event, true)); + + const expectedText = "@foo:example.com unpinned a message from this room. See all pinned messages."; + expect(plainText).toBe(expectedText); + expect(renderComponent(component)).toBe(expectedText); + }); + + it("mentions message when a single message was unpinned, with multiple previously pinned messages", () => { + const event = mockPinnedEvent(['message-2'], ['message-1', 'message-2']); + const plainText = textForEvent(event); + const component = renderer.create(textForEvent(event, true)); + + const expectedText = "@foo:example.com unpinned a message from this room. See all pinned messages."; + expect(plainText).toBe(expectedText); + expect(renderComponent(component)).toBe(expectedText); + }); + + it("shows generic text when multiple messages were pinned", () => { + const event = mockPinnedEvent(['message-1', 'message-2', 'message-3'], ['message-1']); + const plainText = textForEvent(event); + const component = renderer.create(textForEvent(event, true)); + + const expectedText = "@foo:example.com changed the pinned messages for the room."; + expect(plainText).toBe(expectedText); + expect(renderComponent(component)).toBe(expectedText); + }); + + it("shows generic text when multiple messages were unpinned", () => { + const event = mockPinnedEvent(['message-3'], ['message-1', 'message-2', 'message-3']); + const plainText = textForEvent(event); + const component = renderer.create(textForEvent(event, true)); + + const expectedText = "@foo:example.com changed the pinned messages for the room."; + expect(plainText).toBe(expectedText); + expect(renderComponent(component)).toBe(expectedText); + }); + + it("shows generic text when one message was pinned, and another unpinned", () => { + const event = mockPinnedEvent(['message-2'], ['message-1']); + const plainText = textForEvent(event); + const component = renderer.create(textForEvent(event, true)); + + const expectedText = "@foo:example.com changed the pinned messages for the room."; + expect(plainText).toBe(expectedText); + expect(renderComponent(component)).toBe(expectedText); + }); + }); +});