From 488a08a25e6cd9092cf7a78e7c886c2504dce33c Mon Sep 17 00:00:00 2001
From: Michael Weimann <michaelw@matrix.org>
Date: Thu, 5 Jan 2023 08:40:09 +0100
Subject: [PATCH] Allow reactions for broadcasts (#9856)

---
 .../views/messages/MessageActionBar.tsx        |  4 +++-
 src/events/forward/getForwardableEvent.ts      |  3 +++
 src/utils/EventUtils.ts                        |  5 ++++-
 .../context_menus/MessageContextMenu-test.tsx  | 16 ++++++++++++++--
 .../views/messages/MessageActionBar-test.tsx   | 16 ++++++++++++++--
 test/utils/EventUtils-test.ts                  | 18 ++++++++++++++++++
 test/voice-broadcast/utils/test-utils.ts       |  2 +-
 7 files changed, 57 insertions(+), 7 deletions(-)

diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx
index 3cac66a79c..1ec7fae751 100644
--- a/src/components/views/messages/MessageActionBar.tsx
+++ b/src/components/views/messages/MessageActionBar.tsx
@@ -59,6 +59,7 @@ import { Action } from "../../../dispatcher/actions";
 import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload";
 import useFavouriteMessages from "../../../hooks/useFavouriteMessages";
 import { GetRelationsForEvent } from "../rooms/EventTile";
+import { VoiceBroadcastInfoEventType } from "../../../voice-broadcast/types";
 
 interface IOptionsButtonProps {
     mxEvent: MatrixEvent;
@@ -394,7 +395,8 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
              * until cross-platform support
              * (PSF-1041)
              */
-            !M_BEACON_INFO.matches(this.props.mxEvent.getType());
+            !M_BEACON_INFO.matches(this.props.mxEvent.getType()) &&
+            !(this.props.mxEvent.getType() === VoiceBroadcastInfoEventType);
 
         return inNotThreadTimeline && isAllowedMessageType;
     }
diff --git a/src/events/forward/getForwardableEvent.ts b/src/events/forward/getForwardableEvent.ts
index ac6132de11..7d1782d7ae 100644
--- a/src/events/forward/getForwardableEvent.ts
+++ b/src/events/forward/getForwardableEvent.ts
@@ -19,6 +19,7 @@ import { M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon";
 import { MatrixEvent, MatrixClient } from "matrix-js-sdk/src/matrix";
 
 import { getShareableLocationEventForBeacon } from "../../utils/beacon/getShareableLocation";
+import { VoiceBroadcastInfoEventType } from "../../voice-broadcast/types";
 
 /**
  * Get forwardable event for a given event
@@ -29,6 +30,8 @@ export const getForwardableEvent = (event: MatrixEvent, cli: MatrixClient): Matr
         return null;
     }
 
+    if (event.getType() === VoiceBroadcastInfoEventType) return null;
+
     // Live location beacons should forward their latest location as a static pin location
     // If the beacon is not live, or doesn't have a location forwarding is not allowed
     if (M_BEACON_INFO.matches(event.getType())) {
diff --git a/src/utils/EventUtils.ts b/src/utils/EventUtils.ts
index eeebabd5c1..2459a9f745 100644
--- a/src/utils/EventUtils.ts
+++ b/src/utils/EventUtils.ts
@@ -32,6 +32,7 @@ import { TimelineRenderingType } from "../contexts/RoomContext";
 import { launchPollEditor } from "../components/views/messages/MPollBody";
 import { Action } from "../dispatcher/actions";
 import { ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload";
+import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../voice-broadcast/types";
 
 /**
  * Returns whether an event should allow actions like reply, reactions, edit, etc.
@@ -56,7 +57,9 @@ export function isContentActionable(mxEvent: MatrixEvent): boolean {
         } else if (
             mxEvent.getType() === "m.sticker" ||
             M_POLL_START.matches(mxEvent.getType()) ||
-            M_BEACON_INFO.matches(mxEvent.getType())
+            M_BEACON_INFO.matches(mxEvent.getType()) ||
+            (mxEvent.getType() === VoiceBroadcastInfoEventType &&
+                mxEvent.getContent()?.state === VoiceBroadcastInfoState.Started)
         ) {
             return true;
         }
diff --git a/test/components/views/context_menus/MessageContextMenu-test.tsx b/test/components/views/context_menus/MessageContextMenu-test.tsx
index 46a17ec786..3fdf26832d 100644
--- a/test/components/views/context_menus/MessageContextMenu-test.tsx
+++ b/test/components/views/context_menus/MessageContextMenu-test.tsx
@@ -42,14 +42,15 @@ import dispatcher from "../../../../src/dispatcher/dispatcher";
 import SettingsStore from "../../../../src/settings/SettingsStore";
 import { ReadPinsEventId } from "../../../../src/components/views/right_panel/types";
 import { Action } from "../../../../src/dispatcher/actions";
+import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
+import { VoiceBroadcastInfoState } from "../../../../src/voice-broadcast";
 
 jest.mock("../../../../src/utils/strings", () => ({
     copyPlaintext: jest.fn(),
     getSelectedText: jest.fn(),
 }));
 jest.mock("../../../../src/utils/EventUtils", () => ({
-    // @ts-ignore don't mock everything
-    ...jest.requireActual("../../../../src/utils/EventUtils"),
+    ...(jest.requireActual("../../../../src/utils/EventUtils") as object),
     canEditContent: jest.fn(),
 }));
 jest.mock("../../../../src/dispatcher/dispatcher");
@@ -241,6 +242,17 @@ describe("MessageContextMenu", () => {
             expect(menu.find('div[aria-label="Forward"]')).toHaveLength(0);
         });
 
+        it("should not allow forwarding a voice broadcast", () => {
+            const broadcastStartEvent = mkVoiceBroadcastInfoStateEvent(
+                roomId,
+                VoiceBroadcastInfoState.Started,
+                "@user:example.com",
+                "ABC123",
+            );
+            const menu = createMenu(broadcastStartEvent);
+            expect(menu.find('div[aria-label="Forward"]')).toHaveLength(0);
+        });
+
         describe("forwarding beacons", () => {
             const aliceId = "@alice:server.org";
 
diff --git a/test/components/views/messages/MessageActionBar-test.tsx b/test/components/views/messages/MessageActionBar-test.tsx
index a3a3ffe141..8b64f205f0 100644
--- a/test/components/views/messages/MessageActionBar-test.tsx
+++ b/test/components/views/messages/MessageActionBar-test.tsx
@@ -15,8 +15,7 @@ limitations under the License.
 */
 
 import React from "react";
-import { render, fireEvent } from "@testing-library/react";
-import { act } from "react-test-renderer";
+import { act, render, fireEvent } from "@testing-library/react";
 import { EventType, EventStatus, MatrixEvent, MatrixEventEvent, MsgType, Room } from "matrix-js-sdk/src/matrix";
 import { FeatureSupport, Thread } from "matrix-js-sdk/src/models/thread";
 
@@ -34,6 +33,8 @@ import dispatcher from "../../../../src/dispatcher/dispatcher";
 import SettingsStore from "../../../../src/settings/SettingsStore";
 import { Action } from "../../../../src/dispatcher/actions";
 import { UserTab } from "../../../../src/components/views/dialogs/UserTab";
+import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
+import { VoiceBroadcastInfoState } from "../../../../src/voice-broadcast";
 
 jest.mock("../../../../src/dispatcher/dispatcher");
 
@@ -405,6 +406,17 @@ describe("<MessageActionBar />", () => {
                 expect(queryByLabelText("Reply in thread")).toBeTruthy();
             });
 
+            it("does not render thread button for a voice broadcast", () => {
+                const broadcastEvent = mkVoiceBroadcastInfoStateEvent(
+                    roomId,
+                    VoiceBroadcastInfoState.Started,
+                    userId,
+                    "ABC123",
+                );
+                const { queryByLabelText } = getComponent({ mxEvent: broadcastEvent });
+                expect(queryByLabelText("Reply in thread")).not.toBeInTheDocument();
+            });
+
             it("opens user settings on click", () => {
                 jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
                 const { getByLabelText } = getComponent({ mxEvent: alicesMessageEvent });
diff --git a/test/utils/EventUtils-test.ts b/test/utils/EventUtils-test.ts
index ca77e64662..decf42931a 100644
--- a/test/utils/EventUtils-test.ts
+++ b/test/utils/EventUtils-test.ts
@@ -43,6 +43,8 @@ import {
 import { getMockClientWithEventEmitter, makeBeaconInfoEvent, makePollStartEvent, stubClient } from "../test-utils";
 import dis from "../../src/dispatcher/dispatcher";
 import { Action } from "../../src/dispatcher/actions";
+import { mkVoiceBroadcastInfoStateEvent } from "../voice-broadcast/utils/test-utils";
+import { VoiceBroadcastInfoState } from "../../src/voice-broadcast/types";
 
 jest.mock("../../src/dispatcher/dispatcher");
 
@@ -151,6 +153,20 @@ describe("EventUtils", () => {
         },
     });
 
+    const voiceBroadcastStart = mkVoiceBroadcastInfoStateEvent(
+        "!room:example.com",
+        VoiceBroadcastInfoState.Started,
+        "@user:example.com",
+        "ABC123",
+    );
+
+    const voiceBroadcastStop = mkVoiceBroadcastInfoStateEvent(
+        "!room:example.com",
+        VoiceBroadcastInfoState.Stopped,
+        "@user:example.com",
+        "ABC123",
+    );
+
     describe("isContentActionable()", () => {
         type TestCase = [string, MatrixEvent];
         it.each<TestCase>([
@@ -161,6 +177,7 @@ describe("EventUtils", () => {
             ["room member event", roomMemberEvent],
             ["event without msgtype", noMsgType],
             ["event without content body property", noContentBody],
+            ["broadcast stop event", voiceBroadcastStop],
         ])("returns false for %s", (_description, event) => {
             expect(isContentActionable(event)).toBe(false);
         });
@@ -171,6 +188,7 @@ describe("EventUtils", () => {
             ["event with empty content body", emptyContentBody],
             ["event with a content body", niceTextMessage],
             ["beacon_info event", beaconInfoEvent],
+            ["broadcast start event", voiceBroadcastStart],
         ])("returns true for %s", (_description, event) => {
             expect(isContentActionable(event)).toBe(true);
         });
diff --git a/test/voice-broadcast/utils/test-utils.ts b/test/voice-broadcast/utils/test-utils.ts
index cbf0a5989a..fc1ffd4b15 100644
--- a/test/voice-broadcast/utils/test-utils.ts
+++ b/test/voice-broadcast/utils/test-utils.ts
@@ -21,7 +21,7 @@ import {
     VoiceBroadcastChunkEventType,
     VoiceBroadcastInfoEventType,
     VoiceBroadcastInfoState,
-} from "../../../src/voice-broadcast";
+} from "../../../src/voice-broadcast/types";
 import { mkEvent } from "../../test-utils";
 
 // timestamp incremented on each call to prevent duplicate timestamp