From 6ae90c0aeccb561032c5f54e76f91943fdfab39b Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Mon, 12 Dec 2022 15:56:06 +0100 Subject: [PATCH 1/6] When deleting a voice broadcast, also delete related events --- .../views/dialogs/ConfirmRedactDialog.tsx | 23 ++++- src/voice-broadcast/index.ts | 1 + .../utils/isVoiceBroadcastStartedEvent.ts | 24 +++++ .../dialogs/ConfirmRedactDialog-test.tsx | 98 +++++++++++++++++++ test/test-utils/test-utils.ts | 1 + 5 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 src/voice-broadcast/utils/isVoiceBroadcastStartedEvent.ts create mode 100644 test/components/views/dialogs/ConfirmRedactDialog-test.tsx diff --git a/src/components/views/dialogs/ConfirmRedactDialog.tsx b/src/components/views/dialogs/ConfirmRedactDialog.tsx index e1052e8d36c..51e6ef810bd 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"; import ErrorDialog from "./ErrorDialog"; import TextInputDialog from "./TextInputDialog"; @@ -62,9 +64,26 @@ 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 } : {}); + await cli.redactEvent(mxEvent.getRoomId(), mxEvent.getId(), undefined, { + ...(reason ? { reason } : {}), + ...withRelations, + }); } catch (e) { const code = e.errcode || e.statusCode; // only show the dialog if failing for something other than a network error diff --git a/src/voice-broadcast/index.ts b/src/voice-broadcast/index.ts index 1f156ffa4ab..e0be5f821ba 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 00000000000..ddac137ea1b --- /dev/null +++ b/src/voice-broadcast/utils/isVoiceBroadcastStartedEvent.ts @@ -0,0 +1,24 @@ +/* +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 ".."; + +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 00000000000..0eebbf8a448 --- /dev/null +++ b/test/components/views/dialogs/ConfirmRedactDialog-test.tsx @@ -0,0 +1,98 @@ +/* +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, 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 () => { + setUpVoiceBroadcastStartedEvent(); + 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(); + }); + + describe("when 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("when 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 ee34e207279..1d5851b6f5e 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); From 2a164dcd2a191e3c18336d655db9e6054ae090e0 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Tue, 13 Dec 2022 10:10:03 +0100 Subject: [PATCH 2/6] Set JS SDK version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f54c2264829..821c7c17e0d 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "maplibre-gl": "^2.0.0", "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "0.0.1", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#cfe9bf8d4d59548c5f9bb904fc1da9ac79e58429", "matrix-widget-api": "^1.1.1", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", From 39a8fcffbc961fba3dbc4eaba70494283ae56b50 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Tue, 20 Dec 2022 09:25:34 +0100 Subject: [PATCH 3/6] Switch back to JS SDK develop branch --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 821c7c17e0d..f54c2264829 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "maplibre-gl": "^2.0.0", "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "0.0.1", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#cfe9bf8d4d59548c5f9bb904fc1da9ac79e58429", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "matrix-widget-api": "^1.1.1", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", From e78ff365a3e41661ceb265445cbf21b0c1aef039 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Thu, 22 Dec 2022 12:04:44 +0100 Subject: [PATCH 4/6] Fix code style --- .../utils/isVoiceBroadcastStartedEvent.ts | 5 +++-- .../views/dialogs/ConfirmRedactDialog-test.tsx | 18 ++++-------------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/src/voice-broadcast/utils/isVoiceBroadcastStartedEvent.ts b/src/voice-broadcast/utils/isVoiceBroadcastStartedEvent.ts index ddac137ea1b..423fd6a78d6 100644 --- a/src/voice-broadcast/utils/isVoiceBroadcastStartedEvent.ts +++ b/src/voice-broadcast/utils/isVoiceBroadcastStartedEvent.ts @@ -19,6 +19,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/matrix"; import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from ".."; export const isVoiceBroadcastStartedEvent = (event: MatrixEvent): boolean => { - return event.getType() === VoiceBroadcastInfoEventType - && event.getContent()?.state === VoiceBroadcastInfoState.Started; + 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 index 0eebbf8a448..bb1a6b8c3e4 100644 --- a/test/components/views/dialogs/ConfirmRedactDialog-test.tsx +++ b/test/components/views/dialogs/ConfirmRedactDialog-test.tsx @@ -63,12 +63,7 @@ describe("ConfirmRedactDialog", () => { }); it("should call redact without `with_relations`", () => { - expect(client.redactEvent).toHaveBeenCalledWith( - roomId, - mxEvent.getId(), - undefined, - {}, - ); + expect(client.redactEvent).toHaveBeenCalledWith(roomId, mxEvent.getId(), undefined, {}); }); }); }); @@ -84,14 +79,9 @@ describe("ConfirmRedactDialog", () => { }); it("should call redact with `with_relations`", () => { - expect(client.redactEvent).toHaveBeenCalledWith( - roomId, - mxEvent.getId(), - undefined, - { - with_relations: [RelationType.Reference], - }, - ); + expect(client.redactEvent).toHaveBeenCalledWith(roomId, mxEvent.getId(), undefined, { + with_relations: [RelationType.Reference], + }); }); }); }); From 80cdd9babc5b5c0e8f4dd8784bc29fcb44fea863 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Thu, 22 Dec 2022 12:33:58 +0100 Subject: [PATCH 5/6] Tweak imports to prevent cyclic deps --- src/components/views/dialogs/ConfirmRedactDialog.tsx | 2 +- src/voice-broadcast/utils/isVoiceBroadcastStartedEvent.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/ConfirmRedactDialog.tsx b/src/components/views/dialogs/ConfirmRedactDialog.tsx index 51e6ef810bd..89d383d852b 100644 --- a/src/components/views/dialogs/ConfirmRedactDialog.tsx +++ b/src/components/views/dialogs/ConfirmRedactDialog.tsx @@ -21,7 +21,7 @@ import React from "react"; import { _t } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import Modal from "../../../Modal"; -import { isVoiceBroadcastStartedEvent } from "../../../voice-broadcast"; +import { isVoiceBroadcastStartedEvent } from "../../../voice-broadcast/utils/isVoiceBroadcastStartedEvent"; import ErrorDialog from "./ErrorDialog"; import TextInputDialog from "./TextInputDialog"; diff --git a/src/voice-broadcast/utils/isVoiceBroadcastStartedEvent.ts b/src/voice-broadcast/utils/isVoiceBroadcastStartedEvent.ts index 423fd6a78d6..08e3bcec355 100644 --- a/src/voice-broadcast/utils/isVoiceBroadcastStartedEvent.ts +++ b/src/voice-broadcast/utils/isVoiceBroadcastStartedEvent.ts @@ -16,7 +16,7 @@ limitations under the License. import { MatrixEvent } from "matrix-js-sdk/src/matrix"; -import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from ".."; +import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../types"; export const isVoiceBroadcastStartedEvent = (event: MatrixEvent): boolean => { return ( From c0306bc6495034c2fa15d724a8c97e7bfd001338 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Thu, 22 Dec 2022 15:57:07 +0100 Subject: [PATCH 6/6] Fix ConfirmRedactDialog strict type checks --- .../views/dialogs/ConfirmRedactDialog.tsx | 12 ++- .../dialogs/ConfirmRedactDialog-test.tsx | 75 +++++++++++++------ 2 files changed, 64 insertions(+), 23 deletions(-) diff --git a/src/components/views/dialogs/ConfirmRedactDialog.tsx b/src/components/views/dialogs/ConfirmRedactDialog.tsx index 89d383d852b..c4b62bc94d3 100644 --- a/src/components/views/dialogs/ConfirmRedactDialog.tsx +++ b/src/components/views/dialogs/ConfirmRedactDialog.tsx @@ -57,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, { @@ -80,11 +88,11 @@ export function createRedactEventDialog({ try { onCloseDialog?.(); - await cli.redactEvent(mxEvent.getRoomId(), mxEvent.getId(), undefined, { + await cli.redactEvent(roomId, eventId, undefined, { ...(reason ? { reason } : {}), ...withRelations, }); - } catch (e) { + } 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/test/components/views/dialogs/ConfirmRedactDialog-test.tsx b/test/components/views/dialogs/ConfirmRedactDialog-test.tsx index bb1a6b8c3e4..33fc989966b 100644 --- a/test/components/views/dialogs/ConfirmRedactDialog-test.tsx +++ b/test/components/views/dialogs/ConfirmRedactDialog-test.tsx @@ -19,7 +19,7 @@ import { MatrixClient, MatrixEvent, RelationType } from "matrix-js-sdk/src/matri import { screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import { flushPromises, stubClient } from "../../../test-utils"; +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"; @@ -39,7 +39,6 @@ describe("ConfirmRedactDialog", () => { }; const confirmDeleteVoiceBroadcastStartedEvent = async () => { - setUpVoiceBroadcastStartedEvent(); createRedactEventDialog({ mxEvent }); // double-flush promises required for the dialog to show up await flushPromises(); @@ -52,35 +51,69 @@ describe("ConfirmRedactDialog", () => { client = stubClient(); }); - describe("when the server does not support relation based redactions", () => { - beforeEach(() => { - client.canSupport.set(Feature.RelationBasedRedactions, ServerSupport.Unsupported); + 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"); + }); - 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, {}); - }); + 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 the server supports relation based redactions", () => { + describe("when redacting a voice broadcast started event", () => { beforeEach(() => { - client.canSupport.set(Feature.RelationBasedRedactions, ServerSupport.Unstable); + setUpVoiceBroadcastStartedEvent(); }); - describe("and displaying and confirm the dialog for a voice broadcast", () => { - beforeEach(async () => { - await confirmDeleteVoiceBroadcastStartedEvent(); + describe("and the server does not support relation based redactions", () => { + beforeEach(() => { + client.canSupport.set(Feature.RelationBasedRedactions, ServerSupport.Unsupported); }); - it("should call redact with `with_relations`", () => { - expect(client.redactEvent).toHaveBeenCalledWith(roomId, mxEvent.getId(), undefined, { - with_relations: [RelationType.Reference], + 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], + }); }); }); });