diff --git a/src/components/views/messages/MessageEvent.tsx b/src/components/views/messages/MessageEvent.tsx index f93cd71017..4e09bfac49 100644 --- a/src/components/views/messages/MessageEvent.tsx +++ b/src/components/views/messages/MessageEvent.tsx @@ -43,6 +43,8 @@ import MjolnirBody from "./MjolnirBody"; import MBeaconBody from "./MBeaconBody"; import { IEventTileOps } from "../rooms/EventTile"; import { VoiceBroadcastBody, VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from '../../../voice-broadcast'; +import { Features } from '../../../settings/Settings'; +import { SettingLevel } from '../../../settings/SettingLevel'; // onMessageAllowed is handled internally interface IProps extends Omit { @@ -60,6 +62,10 @@ export interface IOperableEventTile { getEventTileOps(): IEventTileOps; } +interface State { + voiceBroadcastEnabled: boolean; +} + const baseBodyTypes = new Map([ [MsgType.Text, TextualBody], [MsgType.Notice, TextualBody], @@ -78,7 +84,7 @@ const baseEvTypes = new Map>>([ [VoiceBroadcastInfoEventType, VoiceBroadcastBody], ]); -export default class MessageEvent extends React.Component implements IMediaBody, IOperableEventTile { +export default class MessageEvent extends React.Component implements IMediaBody, IOperableEventTile { private body: React.RefObject = createRef(); private mediaHelper: MediaEventHelper; private bodyTypes = new Map(baseBodyTypes.entries()); @@ -86,6 +92,7 @@ export default class MessageEvent extends React.Component implements IMe public static contextType = MatrixClientContext; public context!: React.ContextType; + private voiceBroadcastSettingWatcherRef: string; public constructor(props: IProps, context: React.ContextType) { super(props, context); @@ -95,15 +102,29 @@ export default class MessageEvent extends React.Component implements IMe } this.updateComponentMaps(); + + this.state = { + // only check voice broadcast settings for a voice broadcast event + voiceBroadcastEnabled: this.props.mxEvent.getType() === VoiceBroadcastInfoEventType + && SettingsStore.getValue(Features.VoiceBroadcast), + }; } public componentDidMount(): void { this.props.mxEvent.addListener(MatrixEventEvent.Decrypted, this.onDecrypted); + + if (this.props.mxEvent.getType() === VoiceBroadcastInfoEventType) { + this.watchVoiceBroadcastFeatureSetting(); + } } public componentWillUnmount() { this.props.mxEvent.removeListener(MatrixEventEvent.Decrypted, this.onDecrypted); this.mediaHelper?.destroy(); + + if (this.voiceBroadcastSettingWatcherRef) { + SettingsStore.unwatchSetting(this.voiceBroadcastSettingWatcherRef); + } } public componentDidUpdate(prevProps: Readonly) { @@ -147,6 +168,16 @@ export default class MessageEvent extends React.Component implements IMe this.forceUpdate(); }; + private watchVoiceBroadcastFeatureSetting(): void { + this.voiceBroadcastSettingWatcherRef = SettingsStore.watchSetting( + Features.VoiceBroadcast, + null, + (settingName: string, roomId: string, atLevel: SettingLevel, newValAtLevel, newValue: boolean) => { + this.setState({ voiceBroadcastEnabled: newValue }); + }, + ); + } + public render() { const content = this.props.mxEvent.getContent(); const type = this.props.mxEvent.getType(); @@ -174,7 +205,11 @@ export default class MessageEvent extends React.Component implements IMe BodyType = MLocationBody; } - if (type === VoiceBroadcastInfoEventType && content?.state === VoiceBroadcastInfoState.Started) { + if ( + this.state.voiceBroadcastEnabled + && type === VoiceBroadcastInfoEventType + && content?.state === VoiceBroadcastInfoState.Started + ) { BodyType = VoiceBroadcastBody; } } diff --git a/test/components/views/messages/MessageEvent-test.tsx b/test/components/views/messages/MessageEvent-test.tsx new file mode 100644 index 0000000000..82442855fc --- /dev/null +++ b/test/components/views/messages/MessageEvent-test.tsx @@ -0,0 +1,133 @@ +/* +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 React from "react"; +import { render, RenderResult } from "@testing-library/react"; +import { mocked } from "jest-mock"; +import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; + +import { Features } from "../../../../src/settings/Settings"; +import SettingsStore, { CallbackFn } from "../../../../src/settings/SettingsStore"; +import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../../../../src/voice-broadcast"; +import { mkEvent, mkRoom, stubClient } from "../../../test-utils"; +import MessageEvent from "../../../../src/components/views/messages/MessageEvent"; +import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks"; + +jest.mock("../../../../src/components/views/messages/UnknownBody", () => ({ + __esModule: true, + default: () => (
), +})); + +jest.mock("../../../../src/voice-broadcast/components/VoiceBroadcastBody", () => ({ + VoiceBroadcastBody: () => (
), +})); + +describe("MessageEvent", () => { + let room: Room; + let client: MatrixClient; + let event: MatrixEvent; + + const renderMessageEvent = (): RenderResult => { + return render(); + }; + + beforeEach(() => { + client = stubClient(); + room = mkRoom(client, "!room:example.com"); + jest.spyOn(SettingsStore, "getValue"); + jest.spyOn(SettingsStore, "watchSetting"); + jest.spyOn(SettingsStore, "unwatchSetting").mockImplementation(jest.fn()); + }); + + describe("when a voice broadcast start event occurs", () => { + const voiceBroadcastSettingWatcherRef = "vb ref"; + let onVoiceBroadcastSettingChanged: CallbackFn; + + beforeEach(() => { + event = mkEvent({ + event: true, + type: VoiceBroadcastInfoEventType, + user: client.getUserId(), + room: room.roomId, + content: { + state: VoiceBroadcastInfoState.Started, + }, + }); + + mocked(SettingsStore.watchSetting).mockImplementation( + (settingName: string, roomId: string | null, callbackFn: CallbackFn) => { + if (settingName === Features.VoiceBroadcast) { + onVoiceBroadcastSettingChanged = callbackFn; + return voiceBroadcastSettingWatcherRef; + } + }, + ); + }); + + describe("and the voice broadcast feature is enabled", () => { + let result: RenderResult; + + beforeEach(() => { + mocked(SettingsStore.getValue).mockImplementation((settingName: string) => { + return settingName === Features.VoiceBroadcast; + }); + result = renderMessageEvent(); + }); + + it("should render a VoiceBroadcast component", () => { + result.getByTestId("voice-broadcast-body"); + }); + + describe("and switching the voice broadcast feature off", () => { + beforeEach(() => { + onVoiceBroadcastSettingChanged(Features.VoiceBroadcast, null, null, null, false); + }); + + it("should render an UnknownBody component", () => { + const result = renderMessageEvent(); + result.getByTestId("unknown-body"); + }); + }); + + describe("and unmounted", () => { + beforeEach(() => { + result.unmount(); + }); + + it("should unregister the settings watcher", () => { + expect(SettingsStore.unwatchSetting).toHaveBeenCalled(); + }); + }); + }); + + describe("and the voice broadcast feature is disabled", () => { + beforeEach(() => { + mocked(SettingsStore.getValue).mockImplementation((settingName: string) => { + return false; + }); + }); + + it("should render an UnknownBody component", () => { + const result = renderMessageEvent(); + result.getByTestId("unknown-body"); + }); + }); + }); +});