diff --git a/src/components/views/dialogs/ConfirmRedactDialog.tsx b/src/components/views/dialogs/ConfirmRedactDialog.tsx index e1052e8d36..c4b62bc94d 100644 --- a/src/components/views/dialogs/ConfirmRedactDialog.tsx +++ b/src/components/views/dialogs/ConfirmRedactDialog.tsx @@ -14,12 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { Feature, ServerSupport } from "matrix-js-sdk/src/feature"; +import { MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix"; import React from "react"; import { _t } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import Modal from "../../../Modal"; +import { isVoiceBroadcastStartedEvent } from "../../../voice-broadcast/utils/isVoiceBroadcastStartedEvent"; import ErrorDialog from "./ErrorDialog"; import TextInputDialog from "./TextInputDialog"; @@ -55,6 +57,14 @@ export function createRedactEventDialog({ mxEvent: MatrixEvent; onCloseDialog?: () => void; }) { + const eventId = mxEvent.getId(); + + if (!eventId) throw new Error("cannot redact event without ID"); + + const roomId = mxEvent.getRoomId(); + + if (!roomId) throw new Error(`cannot redact event ${mxEvent.getId()} without room ID`); + Modal.createDialog( ConfirmRedactDialog, { @@ -62,10 +72,27 @@ export function createRedactEventDialog({ if (!proceed) return; const cli = MatrixClientPeg.get(); + const withRelations: { with_relations?: RelationType[] } = {}; + + // redact related events if this is a voice broadcast started event and + // server has support for relation based redactions + if (isVoiceBroadcastStartedEvent(mxEvent)) { + const relationBasedRedactionsSupport = cli.canSupport.get(Feature.RelationBasedRedactions); + if ( + relationBasedRedactionsSupport && + relationBasedRedactionsSupport !== ServerSupport.Unsupported + ) { + withRelations.with_relations = [RelationType.Reference]; + } + } + try { onCloseDialog?.(); - await cli.redactEvent(mxEvent.getRoomId(), mxEvent.getId(), undefined, reason ? { reason } : {}); - } catch (e) { + await cli.redactEvent(roomId, eventId, undefined, { + ...(reason ? { reason } : {}), + ...withRelations, + }); + } catch (e: any) { const code = e.errcode || e.statusCode; // only show the dialog if failing for something other than a network error // (e.g. no errcode or statusCode) as in that case the redactions end up in the diff --git a/src/voice-broadcast/index.ts b/src/voice-broadcast/index.ts index 1f156ffa4a..e0be5f821b 100644 --- a/src/voice-broadcast/index.ts +++ b/src/voice-broadcast/index.ts @@ -48,6 +48,7 @@ export * from "./utils/doMaybeSetCurrentVoiceBroadcastPlayback"; export * from "./utils/getChunkLength"; export * from "./utils/getMaxBroadcastLength"; export * from "./utils/hasRoomLiveVoiceBroadcast"; +export * from "./utils/isVoiceBroadcastStartedEvent"; export * from "./utils/findRoomLiveVoiceBroadcastFromUserAndDevice"; export * from "./utils/retrieveStartedInfoEvent"; export * from "./utils/shouldDisplayAsVoiceBroadcastRecordingTile"; diff --git a/src/voice-broadcast/utils/isVoiceBroadcastStartedEvent.ts b/src/voice-broadcast/utils/isVoiceBroadcastStartedEvent.ts new file mode 100644 index 0000000000..08e3bcec35 --- /dev/null +++ b/src/voice-broadcast/utils/isVoiceBroadcastStartedEvent.ts @@ -0,0 +1,25 @@ +/* +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 { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../types"; + +export const isVoiceBroadcastStartedEvent = (event: MatrixEvent): boolean => { + return ( + event.getType() === VoiceBroadcastInfoEventType && event.getContent()?.state === VoiceBroadcastInfoState.Started + ); +}; diff --git a/test/components/views/dialogs/ConfirmRedactDialog-test.tsx b/test/components/views/dialogs/ConfirmRedactDialog-test.tsx new file mode 100644 index 0000000000..33fc989966 --- /dev/null +++ b/test/components/views/dialogs/ConfirmRedactDialog-test.tsx @@ -0,0 +1,121 @@ +/* +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 { Feature, ServerSupport } from "matrix-js-sdk/src/feature"; +import { MatrixClient, MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix"; +import { screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; + +import { flushPromises, mkEvent, stubClient } from "../../../test-utils"; +import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils"; +import { VoiceBroadcastInfoState } from "../../../../src/voice-broadcast"; +import { createRedactEventDialog } from "../../../../src/components/views/dialogs/ConfirmRedactDialog"; + +describe("ConfirmRedactDialog", () => { + const roomId = "!room:example.com"; + let client: MatrixClient; + let mxEvent: MatrixEvent; + + const setUpVoiceBroadcastStartedEvent = () => { + mxEvent = mkVoiceBroadcastInfoStateEvent( + roomId, + VoiceBroadcastInfoState.Started, + client.getUserId()!, + client.deviceId!, + ); + }; + + const confirmDeleteVoiceBroadcastStartedEvent = async () => { + createRedactEventDialog({ mxEvent }); + // double-flush promises required for the dialog to show up + await flushPromises(); + await flushPromises(); + + await userEvent.click(screen.getByTestId("dialog-primary-button")); + }; + + beforeEach(() => { + client = stubClient(); + }); + + it("should raise an error for an event without ID", async () => { + mxEvent = mkEvent({ + event: true, + type: "m.room.message", + room: roomId, + content: {}, + user: client.getSafeUserId(), + }); + jest.spyOn(mxEvent, "getId").mockReturnValue(undefined); + expect(async () => { + await confirmDeleteVoiceBroadcastStartedEvent(); + }).rejects.toThrow("cannot redact event without ID"); + }); + + it("should raise an error for an event without room-ID", async () => { + mxEvent = mkEvent({ + event: true, + type: "m.room.message", + room: roomId, + content: {}, + user: client.getSafeUserId(), + }); + jest.spyOn(mxEvent, "getRoomId").mockReturnValue(undefined); + expect(async () => { + await confirmDeleteVoiceBroadcastStartedEvent(); + }).rejects.toThrow(`cannot redact event ${mxEvent.getId()} without room ID`); + }); + + describe("when redacting a voice broadcast started event", () => { + beforeEach(() => { + setUpVoiceBroadcastStartedEvent(); + }); + + describe("and the server does not support relation based redactions", () => { + beforeEach(() => { + client.canSupport.set(Feature.RelationBasedRedactions, ServerSupport.Unsupported); + }); + + describe("and displaying and confirm the dialog for a voice broadcast", () => { + beforeEach(async () => { + await confirmDeleteVoiceBroadcastStartedEvent(); + }); + + it("should call redact without `with_relations`", () => { + expect(client.redactEvent).toHaveBeenCalledWith(roomId, mxEvent.getId(), undefined, {}); + }); + }); + }); + + describe("and the server supports relation based redactions", () => { + beforeEach(() => { + client.canSupport.set(Feature.RelationBasedRedactions, ServerSupport.Unstable); + }); + + describe("and displaying and confirm the dialog for a voice broadcast", () => { + beforeEach(async () => { + await confirmDeleteVoiceBroadcastStartedEvent(); + }); + + it("should call redact with `with_relations`", () => { + expect(client.redactEvent).toHaveBeenCalledWith(roomId, mxEvent.getId(), undefined, { + with_relations: [RelationType.Reference], + }); + }); + }); + }); + }); +}); diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index 6e914189a4..25a581ba6f 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -206,6 +206,7 @@ export function createTestClient(): MatrixClient { requestPasswordEmailToken: jest.fn().mockRejectedValue({}), setPassword: jest.fn().mockRejectedValue({}), groupCallEventHandler: { groupCalls: new Map() }, + redactEvent: jest.fn(), } as unknown as MatrixClient; client.reEmitter = new ReEmitter(client);