From 51554399fb13200252a035cf4468e39cedeb489a Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Fri, 16 Dec 2022 12:01:16 +0100 Subject: [PATCH] Implement broadcast message preview (#9762) --- src/TextForEvent.tsx | 5 +- src/i18n/strings/en_EN.json | 2 + src/stores/room-list/MessagePreviewStore.ts | 6 ++ .../room-list/previews/MessageEventPreview.ts | 6 +- .../previews/VoiceBroadcastPreview.ts | 31 ++++++ src/utils/event/getSenderName.ts | 23 +++++ src/voice-broadcast/index.ts | 27 +----- src/voice-broadcast/types.ts | 40 ++++++++ .../textForVoiceBroadcastStoppedEvent.tsx | 2 +- ...orVoiceBroadcastStoppedEventWithoutLink.ts | 31 ++++++ test/TextForEvent-test.ts | 3 +- .../previews/MessageEventPreview-test.ts | 96 +++++++++++++++++++ .../previews/VoiceBroadcastPreview-test.ts | 57 +++++++++++ test/test-utils/test-utils.ts | 1 + ...ceBroadcastStoppedEventWithoutLink-test.ts | 55 +++++++++++ 15 files changed, 353 insertions(+), 32 deletions(-) create mode 100644 src/stores/room-list/previews/VoiceBroadcastPreview.ts create mode 100644 src/utils/event/getSenderName.ts create mode 100644 src/voice-broadcast/types.ts create mode 100644 src/voice-broadcast/utils/textForVoiceBroadcastStoppedEventWithoutLink.ts create mode 100644 test/stores/room-list/previews/MessageEventPreview-test.ts create mode 100644 test/stores/room-list/previews/VoiceBroadcastPreview-test.ts create mode 100644 test/voice-broadcast/utils/textForVoiceBroadcastStoppedEventWithoutLink-test.ts diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx index 7d4ad5eb4f..01b7fe4eaf 100644 --- a/src/TextForEvent.tsx +++ b/src/TextForEvent.tsx @@ -45,10 +45,7 @@ import RightPanelStore from "./stores/right-panel/RightPanelStore"; import { highlightEvent, isLocationEvent } from "./utils/EventUtils"; import { ElementCall } from "./models/Call"; import { textForVoiceBroadcastStoppedEvent, VoiceBroadcastInfoEventType } from "./voice-broadcast"; - -export function getSenderName(event: MatrixEvent): string { - return event.sender?.name ?? event.getSender() ?? _t("Someone"); -} +import { getSenderName } from "./utils/event/getSenderName"; function getRoomMemberDisplayname(event: MatrixEvent, userId = event.getSender()): string { const client = MatrixClientPeg.get(); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index abee5e48db..888910a517 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -650,6 +650,8 @@ "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.", "You ended a voice broadcast": "You ended a voice broadcast", "%(senderName)s ended a voice broadcast": "%(senderName)s ended a voice broadcast", + "You ended a voice broadcast": "You ended a voice broadcast", + "%(senderName)s ended a voice broadcast": "%(senderName)s ended a voice broadcast", "Stop live broadcasting?": "Stop live broadcasting?", "Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.", "Yes, stop broadcast": "Yes, stop broadcast", diff --git a/src/stores/room-list/MessagePreviewStore.ts b/src/stores/room-list/MessagePreviewStore.ts index 955298a837..035e342747 100644 --- a/src/stores/room-list/MessagePreviewStore.ts +++ b/src/stores/room-list/MessagePreviewStore.ts @@ -32,6 +32,8 @@ import { StickerEventPreview } from "./previews/StickerEventPreview"; import { ReactionEventPreview } from "./previews/ReactionEventPreview"; import { UPDATE_EVENT } from "../AsyncStore"; import { IPreview } from "./previews/IPreview"; +import { VoiceBroadcastInfoEventType } from "../../voice-broadcast"; +import { VoiceBroadcastPreview } from "./previews/VoiceBroadcastPreview"; // Emitted event for when a room's preview has changed. First argument will the room for which // the change happened. @@ -76,6 +78,10 @@ const PREVIEWS: Record< isState: false, previewer: new PollStartEventPreview(), }, + [VoiceBroadcastInfoEventType]: { + isState: true, + previewer: new VoiceBroadcastPreview(), + }, }; // The maximum number of events we're willing to look back on to get a preview. diff --git a/src/stores/room-list/previews/MessageEventPreview.ts b/src/stores/room-list/previews/MessageEventPreview.ts index e33560e73e..af572ed0b0 100644 --- a/src/stores/room-list/previews/MessageEventPreview.ts +++ b/src/stores/room-list/previews/MessageEventPreview.ts @@ -23,11 +23,15 @@ import { _t, sanitizeForTranslation } from "../../../languageHandler"; import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils"; import { getHtmlText } from "../../../HtmlUtils"; import { stripHTMLReply, stripPlainReply } from "../../../utils/Reply"; +import { VoiceBroadcastChunkEventType } from "../../../voice-broadcast/types"; export class MessageEventPreview implements IPreview { - public getTextFor(event: MatrixEvent, tagId?: TagID, isThread?: boolean): string { + public getTextFor(event: MatrixEvent, tagId?: TagID, isThread?: boolean): string | null { let eventContent = event.getContent(); + // no preview for broadcast chunks + if (eventContent[VoiceBroadcastChunkEventType]) return null; + if (event.isRelation(RelationType.Replace)) { // It's an edit, generate the preview on the new text eventContent = event.getContent()["m.new_content"]; diff --git a/src/stores/room-list/previews/VoiceBroadcastPreview.ts b/src/stores/room-list/previews/VoiceBroadcastPreview.ts new file mode 100644 index 0000000000..b05ef4473d --- /dev/null +++ b/src/stores/room-list/previews/VoiceBroadcastPreview.ts @@ -0,0 +1,31 @@ +/* +Copyright 2022 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 { MatrixEvent } from "matrix-js-sdk/src/matrix"; + +import { VoiceBroadcastInfoState } from "../../../voice-broadcast/types"; +import { textForVoiceBroadcastStoppedEventWithoutLink } from "../../../voice-broadcast/utils/textForVoiceBroadcastStoppedEventWithoutLink"; +import { IPreview } from "./IPreview"; + +export class VoiceBroadcastPreview implements IPreview { + getTextFor(event: MatrixEvent, tagId?: string, isThread?: boolean): string | null { + if (!event.isRedacted() && event.getContent()?.state === VoiceBroadcastInfoState.Stopped) { + return textForVoiceBroadcastStoppedEventWithoutLink(event); + } + + return null; + } +} diff --git a/src/utils/event/getSenderName.ts b/src/utils/event/getSenderName.ts new file mode 100644 index 0000000000..18bfc8d927 --- /dev/null +++ b/src/utils/event/getSenderName.ts @@ -0,0 +1,23 @@ +/* +Copyright 2022 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 { MatrixEvent } from "matrix-js-sdk/src/matrix"; + +import { _t } from "../../languageHandler"; + +export function getSenderName(event: MatrixEvent): string { + return event.sender?.name ?? event.getSender() ?? _t("Someone"); +} diff --git a/src/voice-broadcast/index.ts b/src/voice-broadcast/index.ts index 952a3969af..bba5bf02b2 100644 --- a/src/voice-broadcast/index.ts +++ b/src/voice-broadcast/index.ts @@ -19,8 +19,7 @@ limitations under the License. * {@link https://github.com/vector-im/element-meta/discussions/632} */ -import { RelationType } from "matrix-js-sdk/src/matrix"; - +export * from "./types"; export * from "./models/VoiceBroadcastPlayback"; export * from "./models/VoiceBroadcastPreRecording"; export * from "./models/VoiceBroadcastRecording"; @@ -55,27 +54,5 @@ export * from "./utils/shouldDisplayAsVoiceBroadcastTile"; export * from "./utils/shouldDisplayAsVoiceBroadcastStoppedText"; export * from "./utils/startNewVoiceBroadcastRecording"; export * from "./utils/textForVoiceBroadcastStoppedEvent"; +export * from "./utils/textForVoiceBroadcastStoppedEventWithoutLink"; export * from "./utils/VoiceBroadcastResumer"; - -export const VoiceBroadcastInfoEventType = "io.element.voice_broadcast_info"; -export const VoiceBroadcastChunkEventType = "io.element.voice_broadcast_chunk"; - -export type VoiceBroadcastLiveness = "live" | "not-live" | "grey"; - -export enum VoiceBroadcastInfoState { - Started = "started", - Paused = "paused", - Resumed = "resumed", - Stopped = "stopped", -} - -export interface VoiceBroadcastInfoEventContent { - device_id: string; - state: VoiceBroadcastInfoState; - chunk_length?: number; - last_chunk_sequence?: number; - ["m.relates_to"]?: { - rel_type: RelationType; - event_id: string; - }; -} diff --git a/src/voice-broadcast/types.ts b/src/voice-broadcast/types.ts new file mode 100644 index 0000000000..a54e5513c9 --- /dev/null +++ b/src/voice-broadcast/types.ts @@ -0,0 +1,40 @@ +/* +Copyright 2022 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 } from "matrix-js-sdk/src/matrix"; + +export const VoiceBroadcastInfoEventType = "io.element.voice_broadcast_info"; +export const VoiceBroadcastChunkEventType = "io.element.voice_broadcast_chunk"; + +export type VoiceBroadcastLiveness = "live" | "not-live" | "grey"; + +export enum VoiceBroadcastInfoState { + Started = "started", + Paused = "paused", + Resumed = "resumed", + Stopped = "stopped", +} + +export interface VoiceBroadcastInfoEventContent { + device_id: string; + state: VoiceBroadcastInfoState; + chunk_length?: number; + last_chunk_sequence?: number; + ["m.relates_to"]?: { + rel_type: RelationType; + event_id: string; + }; +} diff --git a/src/voice-broadcast/utils/textForVoiceBroadcastStoppedEvent.tsx b/src/voice-broadcast/utils/textForVoiceBroadcastStoppedEvent.tsx index 611908b750..4564c73d13 100644 --- a/src/voice-broadcast/utils/textForVoiceBroadcastStoppedEvent.tsx +++ b/src/voice-broadcast/utils/textForVoiceBroadcastStoppedEvent.tsx @@ -20,8 +20,8 @@ import { MatrixEvent } from "matrix-js-sdk/src/matrix"; import { MatrixClientPeg } from "../../MatrixClientPeg"; import AccessibleButton from "../../components/views/elements/AccessibleButton"; import { highlightEvent } from "../../utils/EventUtils"; -import { getSenderName } from "../../TextForEvent"; import { _t } from "../../languageHandler"; +import { getSenderName } from "../../utils/event/getSenderName"; export const textForVoiceBroadcastStoppedEvent = (event: MatrixEvent): (() => ReactNode) => { return (): ReactNode => { diff --git a/src/voice-broadcast/utils/textForVoiceBroadcastStoppedEventWithoutLink.ts b/src/voice-broadcast/utils/textForVoiceBroadcastStoppedEventWithoutLink.ts new file mode 100644 index 0000000000..f0ecbc4e83 --- /dev/null +++ b/src/voice-broadcast/utils/textForVoiceBroadcastStoppedEventWithoutLink.ts @@ -0,0 +1,31 @@ +/* +Copyright 2022 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 { MatrixEvent } from "matrix-js-sdk/src/matrix"; + +import { _t } from "../../languageHandler"; +import { MatrixClientPeg } from "../../MatrixClientPeg"; +import { getSenderName } from "../../utils/event/getSenderName"; + +export const textForVoiceBroadcastStoppedEventWithoutLink = (event: MatrixEvent): string => { + const ownUserId = MatrixClientPeg.get()?.getUserId(); + + if (ownUserId && ownUserId === event.getSender()) { + return _t("You ended a voice broadcast", {}); + } + + return _t("%(senderName)s ended a voice broadcast", { senderName: getSenderName(event) }); +}; diff --git a/test/TextForEvent-test.ts b/test/TextForEvent-test.ts index 53455ba5b0..e05ae0b339 100644 --- a/test/TextForEvent-test.ts +++ b/test/TextForEvent-test.ts @@ -19,12 +19,13 @@ import TestRenderer from "react-test-renderer"; import { ReactElement } from "react"; import { mocked } from "jest-mock"; -import { getSenderName, textForEvent } from "../src/TextForEvent"; +import { textForEvent } from "../src/TextForEvent"; import SettingsStore from "../src/settings/SettingsStore"; import { createTestClient, stubClient } from "./test-utils"; import { MatrixClientPeg } from "../src/MatrixClientPeg"; import UserIdentifierCustomisations from "../src/customisations/UserIdentifier"; import { ElementCall } from "../src/models/Call"; +import { getSenderName } from "../src/utils/event/getSenderName"; jest.mock("../src/settings/SettingsStore"); jest.mock("../src/customisations/UserIdentifier", () => ({ diff --git a/test/stores/room-list/previews/MessageEventPreview-test.ts b/test/stores/room-list/previews/MessageEventPreview-test.ts new file mode 100644 index 0000000000..48f9c03036 --- /dev/null +++ b/test/stores/room-list/previews/MessageEventPreview-test.ts @@ -0,0 +1,96 @@ +/* +Copyright 2022 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 } from "matrix-js-sdk/src/matrix"; + +import { MessageEventPreview } from "../../../../src/stores/room-list/previews/MessageEventPreview"; +import { mkEvent, stubClient } from "../../../test-utils"; + +describe("MessageEventPreview", () => { + const preview = new MessageEventPreview(); + const userId = "@user:example.com"; + + beforeAll(() => { + stubClient(); + }); + + describe("getTextFor", () => { + it("when called with an event with empty content should return null", () => { + const event = mkEvent({ + event: true, + content: {}, + user: userId, + type: "m.room.message", + }); + expect(preview.getTextFor(event)).toBeNull(); + }); + + it("when called with an event with empty body should return null", () => { + const event = mkEvent({ + event: true, + content: { + body: "", + }, + user: userId, + type: "m.room.message", + }); + expect(preview.getTextFor(event)).toBeNull(); + }); + + it("when called with an event with body should return »user: body«", () => { + const event = mkEvent({ + event: true, + content: { + body: "test body", + }, + user: userId, + type: "m.room.message", + }); + expect(preview.getTextFor(event)).toBe(`${userId}: test body`); + }); + + it("when called for a replaced event with new content should return the new content body", () => { + const event = mkEvent({ + event: true, + content: { + ["m.new_content"]: { + body: "test new content body", + }, + ["m.relates_to"]: { + rel_type: RelationType.Replace, + event_id: "$asd123", + }, + }, + user: userId, + type: "m.room.message", + }); + expect(preview.getTextFor(event)).toBe(`${userId}: test new content body`); + }); + + it("when called with a broadcast chunk event it should return null", () => { + const event = mkEvent({ + event: true, + content: { + body: "test body", + ["io.element.voice_broadcast_chunk"]: {}, + }, + user: userId, + type: "m.room.message", + }); + expect(preview.getTextFor(event)).toBeNull(); + }); + }); +}); diff --git a/test/stores/room-list/previews/VoiceBroadcastPreview-test.ts b/test/stores/room-list/previews/VoiceBroadcastPreview-test.ts new file mode 100644 index 0000000000..ccffdeaa76 --- /dev/null +++ b/test/stores/room-list/previews/VoiceBroadcastPreview-test.ts @@ -0,0 +1,57 @@ +/* +Copyright 2022 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 { VoiceBroadcastPreview } from "../../../../src/stores/room-list/previews/VoiceBroadcastPreview"; +import { VoiceBroadcastInfoState } from "../../../../src/voice-broadcast"; +import { mkEvent } from "../../../test-utils"; +import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils"; + +describe("VoiceBroadcastPreview.getTextFor", () => { + const roomId = "!room:example.com"; + const userId = "@user:example.com"; + const deviceId = "d42"; + let preview: VoiceBroadcastPreview; + + beforeAll(() => { + preview = new VoiceBroadcastPreview(); + }); + + it("when passing an event with empty content, it should return null", () => { + const event = mkEvent({ + event: true, + content: {}, + user: userId, + type: "m.room.message", + }); + expect(preview.getTextFor(event)).toBeNull(); + }); + + it("when passing a broadcast started event, it should return null", () => { + const event = mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Started, userId, deviceId); + expect(preview.getTextFor(event)).toBeNull(); + }); + + it("when passing a broadcast stopped event, it should return the expected text", () => { + const event = mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Stopped, userId, deviceId); + expect(preview.getTextFor(event)).toBe("@user:example.com ended a voice broadcast"); + }); + + it("when passing a redacted broadcast stopped event, it should return null", () => { + const event = mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Stopped, userId, deviceId); + event.makeRedacted(mkEvent({ event: true, content: {}, user: userId, type: "m.room.redaction" })); + expect(preview.getTextFor(event)).toBeNull(); + }); +}); diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index d0c7d34b45..21de3983fb 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -88,6 +88,7 @@ export function createTestClient(): MatrixClient { getIdentityServerUrl: jest.fn(), getDomain: jest.fn().mockReturnValue("matrix.org"), getUserId: jest.fn().mockReturnValue("@userId:matrix.org"), + getSafeUserId: jest.fn().mockReturnValue("@userId:matrix.org"), getUserIdLocalpart: jest.fn().mockResolvedValue("userId"), getUser: jest.fn().mockReturnValue({ on: jest.fn() }), getDeviceId: jest.fn().mockReturnValue("ABCDEFGHI"), diff --git a/test/voice-broadcast/utils/textForVoiceBroadcastStoppedEventWithoutLink-test.ts b/test/voice-broadcast/utils/textForVoiceBroadcastStoppedEventWithoutLink-test.ts new file mode 100644 index 0000000000..8f17e3c43a --- /dev/null +++ b/test/voice-broadcast/utils/textForVoiceBroadcastStoppedEventWithoutLink-test.ts @@ -0,0 +1,55 @@ +/* +Copyright 2022 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 { mocked } from "jest-mock"; +import { MatrixClient } from "matrix-js-sdk/src/client"; + +import { textForVoiceBroadcastStoppedEventWithoutLink, VoiceBroadcastInfoState } from "../../../src/voice-broadcast"; +import { stubClient } from "../../test-utils"; +import { mkVoiceBroadcastInfoStateEvent } from "./test-utils"; + +describe("textForVoiceBroadcastStoppedEventWithoutLink", () => { + const otherUserId = "@other:example.com"; + const roomId = "!room:example.com"; + let client: MatrixClient; + + beforeAll(() => { + client = stubClient(); + }); + + const getText = (senderId: string, startEventId?: string) => { + const event = mkVoiceBroadcastInfoStateEvent( + roomId, + VoiceBroadcastInfoState.Stopped, + senderId, + client.deviceId!, + ); + return textForVoiceBroadcastStoppedEventWithoutLink(event); + }; + + it("when called for an own broadcast it should return the expected text", () => { + expect(getText(client.getUserId()!)).toBe("You ended a voice broadcast"); + }); + + it("when called for other ones broadcast it should return the expected text", () => { + expect(getText(otherUserId)).toBe(`${otherUserId} ended a voice broadcast`); + }); + + it("when not logged in it should return the exptected text", () => { + mocked(client.getUserId).mockReturnValue(null); + expect(getText(otherUserId)).toBe(`${otherUserId} ended a voice broadcast`); + }); +});