From 044eaf7eb598ca4609c740f51ab1863f40881c32 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 6 Nov 2024 14:55:21 +0000 Subject: [PATCH 01/27] Update calls to addEventToTimeline and addLiveEvents for new signature Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/FilePanel.tsx | 6 ++- src/indexing/EventIndex.ts | 6 ++- src/utils/dm/createDmLocalRoom.ts | 2 +- test/test-utils/call.ts | 50 +++++++++++-------- test/test-utils/threads.ts | 2 +- test/unit-tests/Notifier-test.ts | 3 +- test/unit-tests/RoomNotifs-test.ts | 10 ++-- test/unit-tests/Unread-test.ts | 14 +++--- .../components/structures/RoomView-test.tsx | 24 +++++---- .../structures/ThreadPanel-test.tsx | 12 ++--- .../structures/TimelinePanel-test.tsx | 35 ++++++++----- .../RoomGeneralContextMenu-test.tsx | 2 +- .../views/dialogs/InviteDialog-test.tsx | 23 +++++---- .../components/views/elements/Pill-test.tsx | 2 +- .../views/elements/RoomTopic-test.tsx | 2 +- .../UnreadNotificationBadge-test.tsx | 2 +- .../views/rooms/RoomHeader-test.tsx | 2 +- .../views/settings/Notifications-test.tsx | 2 +- .../notifications/Notifications2-test.tsx | 2 +- .../events/EventTileFactory-test.ts | 2 +- test/unit-tests/models/Call-test.ts | 2 +- .../unit-tests/stores/MemberListStore-test.ts | 1 + test/unit-tests/stores/RoomViewStore-test.ts | 2 +- .../room-list/MessagePreviewStore-test.ts | 2 +- .../algorithms/RecentAlgorithm-test.ts | 18 +++---- .../previews/ReactionEventPreview-test.ts | 4 +- test/unit-tests/useTopic-test.tsx | 4 +- .../utils/exportUtils/HTMLExport-test.ts | 27 +++++----- test/unit-tests/utils/notifications-test.ts | 6 +-- .../components/VoiceBroadcastBody-test.tsx | 6 +-- .../models/VoiceBroadcastPlayback-test.tsx | 14 +++--- .../utils/hasRoomLiveVoiceBroadcast-test.ts | 2 +- .../utils/isRelatedToVoiceBroadcast-test.ts | 4 +- .../utils/retrieveStartedInfoEvent-test.ts | 2 +- 34 files changed, 166 insertions(+), 131 deletions(-) diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx index 74a91d8cbc..07a20315f5 100644 --- a/src/components/structures/FilePanel.tsx +++ b/src/components/structures/FilePanel.tsx @@ -104,7 +104,11 @@ class FilePanel extends React.Component { } if (!this.state.timelineSet.eventIdToTimeline(ev.getId()!)) { - this.state.timelineSet.addEventToTimeline(ev, timeline, false); + this.state.timelineSet.addEventToTimeline(ev, timeline, { + fromCache: false, + addToState: false, + toStartOfTimeline: false, + }); } } diff --git a/src/indexing/EventIndex.ts b/src/indexing/EventIndex.ts index ec3935cd68..0b1a29e9a4 100644 --- a/src/indexing/EventIndex.ts +++ b/src/indexing/EventIndex.ts @@ -820,7 +820,11 @@ export default class EventIndex extends EventEmitter { // Add the events to the timeline of the file panel. matrixEvents.forEach((e) => { if (!timelineSet.eventIdToTimeline(e.getId()!)) { - timelineSet.addEventToTimeline(e, timeline, direction == EventTimeline.BACKWARDS); + timelineSet.addEventToTimeline(e, timeline, { + toStartOfTimeline: direction == EventTimeline.BACKWARDS, + fromCache: false, + addToState: false, + }); } }); diff --git a/src/utils/dm/createDmLocalRoom.ts b/src/utils/dm/createDmLocalRoom.ts index 6d6cf0712b..0a3d312368 100644 --- a/src/utils/dm/createDmLocalRoom.ts +++ b/src/utils/dm/createDmLocalRoom.ts @@ -109,7 +109,7 @@ export async function createDmLocalRoom(client: MatrixClient, targets: Member[]) localRoom.targets = targets; localRoom.updateMyMembership(KnownMembership.Join); - localRoom.addLiveEvents(events); + localRoom.addLiveEvents(events, { addToState: true }); localRoom.currentState.setStateEvents(events); localRoom.name = localRoom.getDefaultRoomName(client.getUserId()!); client.store.storeRoom(localRoom); diff --git a/test/test-utils/call.ts b/test/test-utils/call.ts index 0262e5537f..df87fcaa55 100644 --- a/test/test-utils/call.ts +++ b/test/test-utils/call.ts @@ -44,17 +44,20 @@ export class MockedCall extends Call { } public static create(room: Room, id: string) { - room.addLiveEvents([ - mkEvent({ - event: true, - type: this.EVENT_TYPE, - room: room.roomId, - user: "@alice:example.org", - content: { "m.type": "m.video", "m.intent": "m.prompt" }, - skey: id, - ts: Date.now(), - }), - ]); + room.addLiveEvents( + [ + mkEvent({ + event: true, + type: this.EVENT_TYPE, + room: room.roomId, + user: "@alice:example.org", + content: { "m.type": "m.video", "m.intent": "m.prompt" }, + skey: id, + ts: Date.now(), + }), + ], + { addToState: true }, + ); // @ts-ignore deliberately calling a private method // Let CallStore know that a call might now exist CallStore.instance.updateRoom(room); @@ -81,17 +84,20 @@ export class MockedCall extends Call { public destroy() { // Terminate the call for good measure - this.room.addLiveEvents([ - mkEvent({ - event: true, - type: MockedCall.EVENT_TYPE, - room: this.room.roomId, - user: "@alice:example.org", - content: { ...this.event.getContent(), "m.terminated": "Call ended" }, - skey: this.widget.id, - ts: Date.now(), - }), - ]); + this.room.addLiveEvents( + [ + mkEvent({ + event: true, + type: MockedCall.EVENT_TYPE, + room: this.room.roomId, + user: "@alice:example.org", + content: { ...this.event.getContent(), "m.terminated": "Call ended" }, + skey: this.widget.id, + ts: Date.now(), + }), + ], + { addToState: true }, + ); super.destroy(); } diff --git a/test/test-utils/threads.ts b/test/test-utils/threads.ts index d2459653e5..83313b1b8d 100644 --- a/test/test-utils/threads.ts +++ b/test/test-utils/threads.ts @@ -157,6 +157,6 @@ export const populateThread = async ({ // that it is already loaded, and send the events again to the room // so they are added to the thread timeline. ret.thread.initialEventsFetched = true; - await room.addLiveEvents(ret.events); + await room.addLiveEvents(ret.events, { addToState: false }); return ret; }; diff --git a/test/unit-tests/Notifier-test.ts b/test/unit-tests/Notifier-test.ts index 7bfde2afb3..2833659792 100644 --- a/test/unit-tests/Notifier-test.ts +++ b/test/unit-tests/Notifier-test.ts @@ -624,8 +624,7 @@ describe("Notifier", () => { content: { body: "this is a thread root" }, }), testRoom.threadsTimelineSets[0]!.getLiveTimeline(), - false, - false, + { toStartOfTimeline: false, fromCache: false, addToState: true }, ); expect(fn).not.toHaveBeenCalled(); diff --git a/test/unit-tests/RoomNotifs-test.ts b/test/unit-tests/RoomNotifs-test.ts index 51416ab7fd..65089eba94 100644 --- a/test/unit-tests/RoomNotifs-test.ts +++ b/test/unit-tests/RoomNotifs-test.ts @@ -147,7 +147,7 @@ describe("RoomNotifs test", () => { const itShouldCountPredecessorHighlightWhenThereIsAPredecessorInTheCreateEvent = (): void => { it("and there is a predecessor in the create event, it should count predecessor highlight", () => { - room.addLiveEvents([mkCreateEvent(OLD_ROOM_ID)]); + room.addLiveEvents([mkCreateEvent(OLD_ROOM_ID)], { addToState: true }); expect(getUnreadNotificationCount(room, NotificationCountType.Total, false)).toBe(8); expect(getUnreadNotificationCount(room, NotificationCountType.Highlight, false)).toBe(7); @@ -157,7 +157,7 @@ describe("RoomNotifs test", () => { const itShouldCountPredecessorHighlightWhenThereIsAPredecessorEvent = (): void => { it("and there is a predecessor event, it should count predecessor highlight", () => { client.getVisibleRooms(); - room.addLiveEvents([mkCreateEvent(OLD_ROOM_ID)]); + room.addLiveEvents([mkCreateEvent(OLD_ROOM_ID)], { addToState: true }); upsertRoomStateEvents(room, [mkPredecessorEvent(OLD_ROOM_ID)]); expect(getUnreadNotificationCount(room, NotificationCountType.Total, false)).toBe(8); @@ -185,7 +185,7 @@ describe("RoomNotifs test", () => { itShouldCountPredecessorHighlightWhenThereIsAPredecessorEvent(); it("and there is only a predecessor event, it should not count predecessor highlight", () => { - room.addLiveEvents([mkCreateEvent()]); + room.addLiveEvents([mkCreateEvent()], { addToState: true }); upsertRoomStateEvents(room, [mkPredecessorEvent(OLD_ROOM_ID)]); expect(getUnreadNotificationCount(room, NotificationCountType.Total, false)).toBe(2); @@ -204,7 +204,7 @@ describe("RoomNotifs test", () => { itShouldCountPredecessorHighlightWhenThereIsAPredecessorEvent(); it("and there is only a predecessor event, it should count predecessor highlight", () => { - room.addLiveEvents([mkCreateEvent()]); + room.addLiveEvents([mkCreateEvent()], { addToState: true }); upsertRoomStateEvents(room, [mkPredecessorEvent(OLD_ROOM_ID)]); expect(getUnreadNotificationCount(room, NotificationCountType.Total, false)).toBe(8); @@ -212,7 +212,7 @@ describe("RoomNotifs test", () => { }); it("and there is an unknown room in the predecessor event, it should not count predecessor highlight", () => { - room.addLiveEvents([mkCreateEvent()]); + room.addLiveEvents([mkCreateEvent()], { addToState: true }); upsertRoomStateEvents(room, [mkPredecessorEvent("!unknon:example.com")]); expect(getUnreadNotificationCount(room, NotificationCountType.Total, false)).toBe(2); diff --git a/test/unit-tests/Unread-test.ts b/test/unit-tests/Unread-test.ts index 8719da06ef..15d3dab8f5 100644 --- a/test/unit-tests/Unread-test.ts +++ b/test/unit-tests/Unread-test.ts @@ -138,7 +138,7 @@ describe("Unread", () => { room: roomId, content: {}, }); - room.addLiveEvents([event]); + room.addLiveEvents([event], { addToState: true }); // Don't care about the code path of hidden events. mocked(haveRendererForEvent).mockClear().mockReturnValue(true); @@ -157,7 +157,7 @@ describe("Unread", () => { content: {}, }); // Only for timeline events. - room.addLiveEvents([event]); + room.addLiveEvents([event], { addToState: true }); expect(doesRoomHaveUnreadMessages(room, false)).toBe(false); }); @@ -201,7 +201,7 @@ describe("Unread", () => { content: {}, }); // Only for timeline events. - room.addLiveEvents([event2]); + room.addLiveEvents([event2], { addToState: true }); expect(doesRoomHaveUnreadMessages(room, false)).toBe(true); }); @@ -403,7 +403,7 @@ describe("Unread", () => { redactedEvent.makeRedacted(redactedEvent, room); console.log("Event Id", redactedEvent.getId()); // Only for timeline events. - room.addLiveEvents([redactedEvent]); + room.addLiveEvents([redactedEvent], { addToState: true }); expect(doesRoomHaveUnreadMessages(room, true)).toBe(true); expect(logger.warn).toHaveBeenCalledWith( @@ -448,7 +448,7 @@ describe("Unread", () => { room: roomId, content: {}, }); - room.addLiveEvents([event]); + room.addLiveEvents([event], { addToState: true }); }); it("an unthreaded receipt for the event makes the room read", () => { @@ -502,7 +502,7 @@ describe("Unread", () => { ts: 100, currentUserId: myId, }); - room.addLiveEvents(events); + room.addLiveEvents(events, { addToState: true }); threadEvent = events[1]; }); @@ -555,7 +555,7 @@ describe("Unread", () => { room: roomId, content: {}, }); - room.addLiveEvents([event]); + room.addLiveEvents([event], { addToState: true }); // It still returns false expect(doesRoomHaveUnreadThreads(room)).toBe(false); diff --git a/test/unit-tests/components/structures/RoomView-test.tsx b/test/unit-tests/components/structures/RoomView-test.tsx index b6a0f28637..dfc663dc23 100644 --- a/test/unit-tests/components/structures/RoomView-test.tsx +++ b/test/unit-tests/components/structures/RoomView-test.tsx @@ -249,15 +249,19 @@ describe("RoomView", () => { cli.isRoomEncrypted.mockReturnValue(true); // and fake an encryption event into the room to prompt it to re-check - room.addLiveEvents([ - new MatrixEvent({ - type: "m.room.encryption", - sender: cli.getUserId()!, - content: {}, - event_id: "someid", - room_id: room.roomId, - }), - ]); + room.addLiveEvents( + [ + new MatrixEvent({ + type: "m.room.encryption", + sender: cli.getUserId()!, + content: {}, + event_id: "someid", + room_id: room.roomId, + state_key: "", + }), + ], + { addToState: true }, + ); // URL previews should now be disabled expect(roomViewInstance.state.showUrlPreview).toBe(false); @@ -440,7 +444,7 @@ describe("RoomView", () => { skey: id, ts, }); - room.addLiveEvents([widgetEvent]); + room.addLiveEvents([widgetEvent], { addToState: true }); room.currentState.setStateEvents([widgetEvent]); cli.emit(RoomStateEvent.Events, widgetEvent, room.currentState, null); await flushPromises(); diff --git a/test/unit-tests/components/structures/ThreadPanel-test.tsx b/test/unit-tests/components/structures/ThreadPanel-test.tsx index 1b4d59d9af..ddb6058759 100644 --- a/test/unit-tests/components/structures/ThreadPanel-test.tsx +++ b/test/unit-tests/components/structures/ThreadPanel-test.tsx @@ -209,11 +209,11 @@ describe("ThreadPanel", () => { return event ? Promise.resolve(event) : Promise.reject(); }); const [allThreads, myThreads] = room.threadsTimelineSets; - allThreads!.addLiveEvent(otherThread.rootEvent); - allThreads!.addLiveEvent(mixedThread.rootEvent); - allThreads!.addLiveEvent(ownThread.rootEvent); - myThreads!.addLiveEvent(mixedThread.rootEvent); - myThreads!.addLiveEvent(ownThread.rootEvent); + allThreads!.addLiveEvent(otherThread.rootEvent, { addToState: true }); + allThreads!.addLiveEvent(mixedThread.rootEvent, { addToState: true }); + allThreads!.addLiveEvent(ownThread.rootEvent, { addToState: true }); + myThreads!.addLiveEvent(mixedThread.rootEvent, { addToState: true }); + myThreads!.addLiveEvent(ownThread.rootEvent, { addToState: true }); let events: EventData[] = []; const renderResult = render(); @@ -259,7 +259,7 @@ describe("ThreadPanel", () => { return event ? Promise.resolve(event) : Promise.reject(); }); const [allThreads] = room.threadsTimelineSets; - allThreads!.addLiveEvent(otherThread.rootEvent); + allThreads!.addLiveEvent(otherThread.rootEvent, { addToState: true }); let events: EventData[] = []; const renderResult = render(); diff --git a/test/unit-tests/components/structures/TimelinePanel-test.tsx b/test/unit-tests/components/structures/TimelinePanel-test.tsx index 4a66351779..8f52c1bf25 100644 --- a/test/unit-tests/components/structures/TimelinePanel-test.tsx +++ b/test/unit-tests/components/structures/TimelinePanel-test.tsx @@ -66,7 +66,7 @@ const mkTimeline = (room: Room, events: MatrixEvent[]): [EventTimeline, EventTim getPendingEvents: () => [] as MatrixEvent[], } as unknown as EventTimelineSet; const timeline = new EventTimeline(timelineSet); - events.forEach((event) => timeline.addEvent(event, { toStartOfTimeline: false })); + events.forEach((event) => timeline.addEvent(event, { toStartOfTimeline: false, addToState: true })); return [timeline, timelineSet]; }; @@ -150,9 +150,11 @@ const setupPagination = ( mocked(client).paginateEventTimeline.mockImplementation(async (tl, { backwards }) => { if (tl === timeline) { if (backwards) { - forEachRight(previousPage ?? [], (event) => tl.addEvent(event, { toStartOfTimeline: true })); + forEachRight(previousPage ?? [], (event) => + tl.addEvent(event, { toStartOfTimeline: true, addToState: true }), + ); } else { - (nextPage ?? []).forEach((event) => tl.addEvent(event, { toStartOfTimeline: false })); + (nextPage ?? []).forEach((event) => tl.addEvent(event, { toStartOfTimeline: false, addToState: true })); } // Prevent any further pagination attempts in this direction tl.setPaginationToken(null, backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS); @@ -256,7 +258,7 @@ describe("TimelinePanel", () => { describe("and reading the timeline", () => { beforeEach(async () => { await renderTimelinePanel(); - timelineSet.addLiveEvent(ev1, {}); + timelineSet.addLiveEvent(ev1, { addToState: true }); await flushPromises(); // @ts-ignore @@ -285,11 +287,11 @@ describe("TimelinePanel", () => { }); it("and forgetting the read markers, should send the stored marker again", async () => { - timelineSet.addLiveEvent(ev2, {}); + timelineSet.addLiveEvent(ev2, { addToState: true }); // Add the event to the room as well as the timeline, so we can find it when we // call findEventById in getEventReadUpTo. This is odd because in our test // setup, timelineSet is not actually the timelineSet of the room. - await room.addLiveEvents([ev2], {}); + await room.addLiveEvents([ev2], { addToState: true }); room.addEphemeralEvents([newReceipt(ev2.getId()!, userId, 222, 200)]); await timelinePanel.forgetReadMarker(); expect(client.setRoomReadMarkers).toHaveBeenCalledWith(roomId, ev2.getId()); @@ -315,7 +317,7 @@ describe("TimelinePanel", () => { it("should send a fully read marker and a private receipt", async () => { await renderTimelinePanel(); - timelineSet.addLiveEvent(ev1, {}); + timelineSet.addLiveEvent(ev1, { addToState: true }); await flushPromises(); // @ts-ignore @@ -361,7 +363,7 @@ describe("TimelinePanel", () => { it("should send receipts but no fully_read when reading the thread timeline", async () => { await renderTimelinePanel(); - timelineSet.addLiveEvent(threadEv1, {}); + timelineSet.addLiveEvent(threadEv1, { addToState: true }); await flushPromises(); // @ts-ignore @@ -871,7 +873,9 @@ describe("TimelinePanel", () => { // @ts-ignore thread.fetchEditsWhereNeeded = () => Promise.resolve(); await thread.addEvent(reply1, false, true); - await allThreads.getLiveTimeline().addEvent(thread.rootEvent!, { toStartOfTimeline: true }); + await allThreads + .getLiveTimeline() + .addEvent(thread.rootEvent!, { toStartOfTimeline: true, addToState: true }); const replyToEvent = jest.spyOn(thread, "replyToEvent", "get"); const dom = render( @@ -907,7 +911,9 @@ describe("TimelinePanel", () => { // @ts-ignore realThread.fetchEditsWhereNeeded = () => Promise.resolve(); await realThread.addEvent(reply1, true); - await allThreads.getLiveTimeline().addEvent(realThread.rootEvent!, { toStartOfTimeline: true }); + await allThreads + .getLiveTimeline() + .addEvent(realThread.rootEvent!, { toStartOfTimeline: true, addToState: true }); const replyToEvent = jest.spyOn(realThread, "replyToEvent", "get"); // @ts-ignore @@ -968,7 +974,9 @@ describe("TimelinePanel", () => { events.push(rootEvent); - events.forEach((event) => timelineSet.getLiveTimeline().addEvent(event, { toStartOfTimeline: true })); + events.forEach((event) => + timelineSet.getLiveTimeline().addEvent(event, { toStartOfTimeline: true, addToState: true }), + ); const roomMembership = mkMembership({ mship: KnownMembership.Join, @@ -988,7 +996,10 @@ describe("TimelinePanel", () => { jest.spyOn(roomState, "getMember").mockReturnValue(member); jest.spyOn(timelineSet.getLiveTimeline(), "getState").mockReturnValue(roomState); - timelineSet.addEventToTimeline(roomMembership, timelineSet.getLiveTimeline(), { toStartOfTimeline: false }); + timelineSet.addEventToTimeline(roomMembership, timelineSet.getLiveTimeline(), { + toStartOfTimeline: false, + addToState: true, + }); for (const event of events) { jest.spyOn(event, "isDecryptionFailure").mockReturnValue(true); diff --git a/test/unit-tests/components/views/context_menus/RoomGeneralContextMenu-test.tsx b/test/unit-tests/components/views/context_menus/RoomGeneralContextMenu-test.tsx index 10de3996e6..97517ff9d6 100644 --- a/test/unit-tests/components/views/context_menus/RoomGeneralContextMenu-test.tsx +++ b/test/unit-tests/components/views/context_menus/RoomGeneralContextMenu-test.tsx @@ -127,7 +127,7 @@ describe("RoomGeneralContextMenu", () => { user: "@user:id", ts: 1000, }); - room.addLiveEvents([event], {}); + room.addLiveEvents([event], { addToState: true }); const { container } = getComponent({}); diff --git a/test/unit-tests/components/views/dialogs/InviteDialog-test.tsx b/test/unit-tests/components/views/dialogs/InviteDialog-test.tsx index 62cb01b46e..7ed57a7d70 100644 --- a/test/unit-tests/components/views/dialogs/InviteDialog-test.tsx +++ b/test/unit-tests/components/views/dialogs/InviteDialog-test.tsx @@ -141,16 +141,19 @@ describe("InviteDialog", () => { jest.clearAllMocks(); room = new Room(roomId, mockClient, mockClient.getSafeUserId()); - room.addLiveEvents([ - mkMessage({ - msg: "Hello", - relatesTo: undefined, - event: true, - room: roomId, - user: mockClient.getSafeUserId(), - ts: Date.now(), - }), - ]); + room.addLiveEvents( + [ + mkMessage({ + msg: "Hello", + relatesTo: undefined, + event: true, + room: roomId, + user: mockClient.getSafeUserId(), + ts: Date.now(), + }), + ], + { addToState: true }, + ); room.currentState.setStateEvents([ mkRoomCreateEvent(bobId, roomId), mkMembership({ diff --git a/test/unit-tests/components/views/elements/Pill-test.tsx b/test/unit-tests/components/views/elements/Pill-test.tsx index 24fb2ca5dd..ee50417123 100644 --- a/test/unit-tests/components/views/elements/Pill-test.tsx +++ b/test/unit-tests/components/views/elements/Pill-test.tsx @@ -86,7 +86,7 @@ describe("", () => { room: room1Id, msg: "Room 1 Message", }); - room1.addLiveEvents([room1Message]); + room1.addLiveEvents([room1Message], { addToState: true }); room2 = new Room(room2Id, client, user1Id); room2.currentState.setStateEvents([mkRoomMemberJoinEvent(user2Id, room2Id)]); diff --git a/test/unit-tests/components/views/elements/RoomTopic-test.tsx b/test/unit-tests/components/views/elements/RoomTopic-test.tsx index dcf80a95b9..0fe833fe4e 100644 --- a/test/unit-tests/components/views/elements/RoomTopic-test.tsx +++ b/test/unit-tests/components/views/elements/RoomTopic-test.tsx @@ -41,7 +41,7 @@ describe("", () => { ts: 123, event: true, }); - room.addLiveEvents([topicEvent]); + room.addLiveEvents([topicEvent], { addToState: true }); return room; } diff --git a/test/unit-tests/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx b/test/unit-tests/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx index ea54dc1085..5b2dddc73a 100644 --- a/test/unit-tests/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx +++ b/test/unit-tests/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx @@ -165,7 +165,7 @@ describe("UnreadNotificationBadge", () => { }, ts: 5, }); - room.addLiveEvents([event]); + room.addLiveEvents([event], { addToState: true }); const { container } = render(getComponent(THREAD_ID)); expect(container.querySelector(".mx_NotificationBadge_dot")).toBeTruthy(); diff --git a/test/unit-tests/components/views/rooms/RoomHeader-test.tsx b/test/unit-tests/components/views/rooms/RoomHeader-test.tsx index a7e556e452..3cb4b159cd 100644 --- a/test/unit-tests/components/views/rooms/RoomHeader-test.tsx +++ b/test/unit-tests/components/views/rooms/RoomHeader-test.tsx @@ -579,7 +579,7 @@ describe("RoomHeader", () => { state_key: "", room_id: room.roomId, }); - room.addLiveEvents([joinRuleEvent]); + room.addLiveEvents([joinRuleEvent], { addToState: true }); render(, getWrapper()); diff --git a/test/unit-tests/components/views/settings/Notifications-test.tsx b/test/unit-tests/components/views/settings/Notifications-test.tsx index 69b45135cb..e3d9716901 100644 --- a/test/unit-tests/components/views/settings/Notifications-test.tsx +++ b/test/unit-tests/components/views/settings/Notifications-test.tsx @@ -915,7 +915,7 @@ describe("", () => { user: "@alice:example.org", ts: 1, }); - await room.addLiveEvents([message]); + await room.addLiveEvents([message], { addToState: true }); const { container } = await getComponentAndWait(); const clearNotificationEl = getByTestId(container, "clear-notifications"); diff --git a/test/unit-tests/components/views/settings/notifications/Notifications2-test.tsx b/test/unit-tests/components/views/settings/notifications/Notifications2-test.tsx index 029777c153..2507aa173f 100644 --- a/test/unit-tests/components/views/settings/notifications/Notifications2-test.tsx +++ b/test/unit-tests/components/views/settings/notifications/Notifications2-test.tsx @@ -716,7 +716,7 @@ describe("", () => { user: "@alice:example.org", ts: 1, }); - room.addLiveEvents([message]); + room.addLiveEvents([message], { addToState: true }); room.setUnreadNotificationCount(NotificationCountType.Total, 1); const user = userEvent.setup(); diff --git a/test/unit-tests/events/EventTileFactory-test.ts b/test/unit-tests/events/EventTileFactory-test.ts index 9c8f7718af..7044a883d0 100644 --- a/test/unit-tests/events/EventTileFactory-test.ts +++ b/test/unit-tests/events/EventTileFactory-test.ts @@ -88,7 +88,7 @@ describe("pickFactory", () => { client.getUserId()!, client.deviceId!, ); - room.addLiveEvents([voiceBroadcastStartedEvent]); + room.addLiveEvents([voiceBroadcastStartedEvent], { addToState: true }); voiceBroadcastStoppedEvent = mkVoiceBroadcastInfoStateEvent( roomId, VoiceBroadcastInfoState.Stopped, diff --git a/test/unit-tests/models/Call-test.ts b/test/unit-tests/models/Call-test.ts index 40e929fb4a..316adbbea9 100644 --- a/test/unit-tests/models/Call-test.ts +++ b/test/unit-tests/models/Call-test.ts @@ -119,7 +119,7 @@ const setUpClientRoomAndStores = (): { skey: stateKey, content: content as IContent, }); - room.addLiveEvents([event]); + room.addLiveEvents([event], { addToState: true }); return { event_id: event.getId()! }; }); diff --git a/test/unit-tests/stores/MemberListStore-test.ts b/test/unit-tests/stores/MemberListStore-test.ts index 889a9d3505..184576f317 100644 --- a/test/unit-tests/stores/MemberListStore-test.ts +++ b/test/unit-tests/stores/MemberListStore-test.ts @@ -202,6 +202,7 @@ describe("MemberListStore", () => { function addEventToRoom(room: Room, ev: MatrixEvent) { room.getLiveTimeline().addEvent(ev, { toStartOfTimeline: false, + addToState: true, }); } diff --git a/test/unit-tests/stores/RoomViewStore-test.ts b/test/unit-tests/stores/RoomViewStore-test.ts index 7d397397dc..f7ed1dfbcd 100644 --- a/test/unit-tests/stores/RoomViewStore-test.ts +++ b/test/unit-tests/stores/RoomViewStore-test.ts @@ -397,7 +397,7 @@ describe("RoomViewStore", function () { mockClient.getSafeUserId(), "ABC123", ); - room2.addLiveEvents([broadcastEvent]); + room2.addLiveEvents([broadcastEvent], { addToState: true }); stores.voiceBroadcastPlaybacksStore.getByInfoEvent(broadcastEvent, mockClient); dis.dispatch({ action: Action.ViewRoom, room_id: roomId2 }); diff --git a/test/unit-tests/stores/room-list/MessagePreviewStore-test.ts b/test/unit-tests/stores/room-list/MessagePreviewStore-test.ts index 75013bd4e1..861b6dcd11 100644 --- a/test/unit-tests/stores/room-list/MessagePreviewStore-test.ts +++ b/test/unit-tests/stores/room-list/MessagePreviewStore-test.ts @@ -35,7 +35,7 @@ describe("MessagePreviewStore", () => { event: MatrixEvent, fireAction = true, ): Promise { - room.addLiveEvents([event]); + room.addLiveEvents([event], { addToState: true }); if (fireAction) { // @ts-ignore private access await store.onAction({ diff --git a/test/unit-tests/stores/room-list/algorithms/RecentAlgorithm-test.ts b/test/unit-tests/stores/room-list/algorithms/RecentAlgorithm-test.ts index 18a073b632..e0e06bbaf7 100644 --- a/test/unit-tests/stores/room-list/algorithms/RecentAlgorithm-test.ts +++ b/test/unit-tests/stores/room-list/algorithms/RecentAlgorithm-test.ts @@ -47,11 +47,11 @@ describe("RecentAlgorithm", () => { room.getMyMembership = () => KnownMembership.Join; - room.addLiveEvents([event1]); + room.addLiveEvents([event1], { addToState: true }); expect(algorithm.getLastTs(room, "@jane:matrix.org")).toBe(5); expect(algorithm.getLastTs(room, "@john:matrix.org")).toBe(5); - room.addLiveEvents([event2]); + room.addLiveEvents([event2], { addToState: true }); expect(algorithm.getLastTs(room, "@jane:matrix.org")).toBe(10); expect(algorithm.getLastTs(room, "@john:matrix.org")).toBe(10); @@ -94,8 +94,8 @@ describe("RecentAlgorithm", () => { event: true, }); - room1.addLiveEvents([evt]); - room2.addLiveEvents([evt2]); + room1.addLiveEvents([evt], { addToState: true }); + room2.addLiveEvents([evt2], { addToState: true }); expect(algorithm.sortRooms([room2, room1], DefaultTagID.Untagged)).toEqual([room1, room2]); }); @@ -115,7 +115,7 @@ describe("RecentAlgorithm", () => { event: true, }); - room1.addLiveEvents([evt]); + room1.addLiveEvents([evt], { addToState: true }); expect(algorithm.sortRooms([room2, room1], DefaultTagID.Untagged)).toEqual([room2, room1]); @@ -127,7 +127,7 @@ describe("RecentAlgorithm", () => { ts: 12, }); - room1.addLiveEvents(events); + room1.addLiveEvents(events, { addToState: true }); }); it("orders rooms based on thread replies too", () => { @@ -145,7 +145,7 @@ describe("RecentAlgorithm", () => { ts: 12, length: 5, }); - room1.addLiveEvents(events1); + room1.addLiveEvents(events1, { addToState: true }); const { events: events2 } = mkThread({ room: room2, @@ -155,7 +155,7 @@ describe("RecentAlgorithm", () => { ts: 14, length: 10, }); - room2.addLiveEvents(events2); + room2.addLiveEvents(events2, { addToState: true }); expect(algorithm.sortRooms([room1, room2], DefaultTagID.Untagged)).toEqual([room2, room1]); @@ -169,7 +169,7 @@ describe("RecentAlgorithm", () => { // replies are 1ms after each other ts: 50, }); - room1.addLiveEvents([threadReply]); + room1.addLiveEvents([threadReply], { addToState: true }); expect(algorithm.sortRooms([room1, room2], DefaultTagID.Untagged)).toEqual([room1, room2]); }); diff --git a/test/unit-tests/stores/room-list/previews/ReactionEventPreview-test.ts b/test/unit-tests/stores/room-list/previews/ReactionEventPreview-test.ts index b3facccb06..7a7ce9e2b3 100644 --- a/test/unit-tests/stores/room-list/previews/ReactionEventPreview-test.ts +++ b/test/unit-tests/stores/room-list/previews/ReactionEventPreview-test.ts @@ -70,7 +70,7 @@ describe("ReactionEventPreview", () => { room: roomId, }); - room.getUnfilteredTimelineSet().addLiveEvent(message, {}); + room.getUnfilteredTimelineSet().addLiveEvent(message, { addToState: true }); const event = mkEvent({ event: true, @@ -107,7 +107,7 @@ describe("ReactionEventPreview", () => { room: roomId, }); - room.getUnfilteredTimelineSet().addLiveEvent(message, {}); + room.getUnfilteredTimelineSet().addLiveEvent(message, { addToState: true }); const event = mkEvent({ event: true, diff --git a/test/unit-tests/useTopic-test.tsx b/test/unit-tests/useTopic-test.tsx index cbef9dba60..81afaae985 100644 --- a/test/unit-tests/useTopic-test.tsx +++ b/test/unit-tests/useTopic-test.tsx @@ -29,7 +29,7 @@ describe("useTopic", () => { event: true, }); - room.addLiveEvents([topic]); + room.addLiveEvents([topic], { addToState: true }); function RoomTopic() { const topic = useTopic(room); @@ -52,7 +52,7 @@ describe("useTopic", () => { }); act(() => { - room.addLiveEvents([updatedTopic]); + room.addLiveEvents([updatedTopic], { addToState: true }); }); expect(screen.queryByText("New topic")).toBeInTheDocument(); diff --git a/test/unit-tests/utils/exportUtils/HTMLExport-test.ts b/test/unit-tests/utils/exportUtils/HTMLExport-test.ts index 0fc96e4db7..d169b55a7c 100644 --- a/test/unit-tests/utils/exportUtils/HTMLExport-test.ts +++ b/test/unit-tests/utils/exportUtils/HTMLExport-test.ts @@ -593,18 +593,21 @@ describe("HTMLExport", () => { it("should not make /messages requests when exporting 'Current Timeline'", async () => { client.createMessagesRequest.mockRejectedValue(new Error("Should never be called")); - room.addLiveEvents([ - new MatrixEvent({ - event_id: `$eventId`, - type: EventType.RoomMessage, - sender: client.getSafeUserId(), - origin_server_ts: 123456789, - content: { - msgtype: "m.text", - body: `testing testing`, - }, - }), - ]); + room.addLiveEvents( + [ + new MatrixEvent({ + event_id: `$eventId`, + type: EventType.RoomMessage, + sender: client.getSafeUserId(), + origin_server_ts: 123456789, + content: { + msgtype: "m.text", + body: `testing testing`, + }, + }), + ], + { addToState: true }, + ); const exporter = new HTMLExporter( room, diff --git a/test/unit-tests/utils/notifications-test.ts b/test/unit-tests/utils/notifications-test.ts index 67948ed217..68553de166 100644 --- a/test/unit-tests/utils/notifications-test.ts +++ b/test/unit-tests/utils/notifications-test.ts @@ -121,7 +121,7 @@ describe("notifications", () => { user: USER_ID, msg: "Hello", }); - room.addLiveEvents([message]); + room.addLiveEvents([message], { addToState: true }); sendReadReceiptSpy = jest.spyOn(client, "sendReadReceipt").mockResolvedValue({}); jest.spyOn(client, "getRooms").mockReturnValue([room]); jest.spyOn(SettingsStore, "getValue").mockImplementation((name) => { @@ -187,7 +187,7 @@ describe("notifications", () => { user: USER_ID, ts: 1, }); - room.addLiveEvents([message]); + room.addLiveEvents([message], { addToState: true }); room.setUnreadNotificationCount(NotificationCountType.Total, 1); await clearAllNotifications(client); @@ -202,7 +202,7 @@ describe("notifications", () => { user: USER_ID, ts: 1, }); - room.addLiveEvents([message]); + room.addLiveEvents([message], { addToState: true }); room.setUnreadNotificationCount(NotificationCountType.Total, 1); jest.spyOn(SettingsStore, "getValue").mockReset().mockReturnValue(false); diff --git a/test/unit-tests/voice-broadcast/components/VoiceBroadcastBody-test.tsx b/test/unit-tests/voice-broadcast/components/VoiceBroadcastBody-test.tsx index bf55c45b68..fd416de8ce 100644 --- a/test/unit-tests/voice-broadcast/components/VoiceBroadcastBody-test.tsx +++ b/test/unit-tests/voice-broadcast/components/VoiceBroadcastBody-test.tsx @@ -85,7 +85,7 @@ describe("VoiceBroadcastBody", () => { deviceId, infoEvent, ); - room.addEventsToTimeline([infoEvent], true, room.getLiveTimeline()); + room.addEventsToTimeline([infoEvent], true, true, room.getLiveTimeline()); testRecording = new VoiceBroadcastRecording(infoEvent, client); testPlayback = new VoiceBroadcastPlayback(infoEvent, client, new VoiceBroadcastRecordingsStore()); mocked(VoiceBroadcastRecordingBody).mockImplementation(({ recording }): ReactElement | null => { @@ -127,7 +127,7 @@ describe("VoiceBroadcastBody", () => { describe("when there is a stopped voice broadcast", () => { beforeEach(() => { - room.addEventsToTimeline([stoppedEvent], true, room.getLiveTimeline()); + room.addEventsToTimeline([stoppedEvent], true, true, room.getLiveTimeline()); renderVoiceBroadcast(); }); @@ -148,7 +148,7 @@ describe("VoiceBroadcastBody", () => { describe("and the recordings ends", () => { beforeEach(() => { act(() => { - room.addEventsToTimeline([stoppedEvent], true, room.getLiveTimeline()); + room.addEventsToTimeline([stoppedEvent], true, true, room.getLiveTimeline()); }); }); diff --git a/test/unit-tests/voice-broadcast/models/VoiceBroadcastPlayback-test.tsx b/test/unit-tests/voice-broadcast/models/VoiceBroadcastPlayback-test.tsx index c967189271..9ce24e5921 100644 --- a/test/unit-tests/voice-broadcast/models/VoiceBroadcastPlayback-test.tsx +++ b/test/unit-tests/voice-broadcast/models/VoiceBroadcastPlayback-test.tsx @@ -243,7 +243,7 @@ describe("VoiceBroadcastPlayback", () => { beforeEach(async () => { infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Resumed); createChunkEvents(); - room.addLiveEvents([infoEvent]); + room.addLiveEvents([infoEvent], { addToState: true }); playback = await mkPlayback(); }); @@ -331,7 +331,7 @@ describe("VoiceBroadcastPlayback", () => { infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Resumed); createChunkEvents(); setUpChunkEvents([chunk2Event, chunk1Event]); - room.addLiveEvents([infoEvent, chunk1Event, chunk2Event]); + room.addLiveEvents([infoEvent, chunk1Event, chunk2Event], { addToState: true }); room.relations.aggregateChildEvent(chunk2Event); room.relations.aggregateChildEvent(chunk1Event); playback = await mkPlayback(); @@ -372,7 +372,7 @@ describe("VoiceBroadcastPlayback", () => { describe("and an event with the same transaction Id occurs", () => { beforeEach(() => { - room.addLiveEvents([chunk2BEvent]); + room.addLiveEvents([chunk2BEvent], { addToState: true }); room.relations.aggregateChildEvent(chunk2BEvent); }); @@ -404,7 +404,7 @@ describe("VoiceBroadcastPlayback", () => { infoEvent, 2, ); - room.addLiveEvents([stoppedEvent]); + room.addLiveEvents([stoppedEvent], { addToState: true }); room.relations.aggregateChildEvent(stoppedEvent); chunk2Playback.emit(PlaybackState.Stopped); }); @@ -426,7 +426,7 @@ describe("VoiceBroadcastPlayback", () => { infoEvent, 3, ); - room.addLiveEvents([stoppedEvent]); + room.addLiveEvents([stoppedEvent], { addToState: true }); room.relations.aggregateChildEvent(stoppedEvent); chunk2Playback.emit(PlaybackState.Stopped); }); @@ -435,7 +435,7 @@ describe("VoiceBroadcastPlayback", () => { describe("and the next chunk arrives", () => { beforeEach(() => { - room.addLiveEvents([chunk3Event]); + room.addLiveEvents([chunk3Event], { addToState: true }); room.relations.aggregateChildEvent(chunk3Event); }); @@ -521,7 +521,7 @@ describe("VoiceBroadcastPlayback", () => { createChunkEvents(); // use delayed first chunk here to simulate loading time setUpChunkEvents([chunk2Event, deplayedChunk1Event, chunk3Event]); - room.addLiveEvents([infoEvent, deplayedChunk1Event, chunk2Event, chunk3Event]); + room.addLiveEvents([infoEvent, deplayedChunk1Event, chunk2Event, chunk3Event], { addToState: true }); playback = await mkPlayback(true); }); diff --git a/test/unit-tests/voice-broadcast/utils/hasRoomLiveVoiceBroadcast-test.ts b/test/unit-tests/voice-broadcast/utils/hasRoomLiveVoiceBroadcast-test.ts index 589e1c46a7..37fbf0c277 100644 --- a/test/unit-tests/voice-broadcast/utils/hasRoomLiveVoiceBroadcast-test.ts +++ b/test/unit-tests/voice-broadcast/utils/hasRoomLiveVoiceBroadcast-test.ts @@ -32,7 +32,7 @@ describe("hasRoomLiveVoiceBroadcast", () => { startedEvent?: MatrixEvent, ): MatrixEvent => { const infoEvent = mkVoiceBroadcastInfoStateEvent(room.roomId, state, userId, deviceId, startedEvent); - room.addLiveEvents([infoEvent]); + room.addLiveEvents([infoEvent], { addToState: true }); room.currentState.setStateEvents([infoEvent]); room.relations.aggregateChildEvent(infoEvent); return infoEvent; diff --git a/test/unit-tests/voice-broadcast/utils/isRelatedToVoiceBroadcast-test.ts b/test/unit-tests/voice-broadcast/utils/isRelatedToVoiceBroadcast-test.ts index 17160b8b2b..eae78df109 100644 --- a/test/unit-tests/voice-broadcast/utils/isRelatedToVoiceBroadcast-test.ts +++ b/test/unit-tests/voice-broadcast/utils/isRelatedToVoiceBroadcast-test.ts @@ -31,7 +31,7 @@ const mkRelatedEvent = ( }, user: client.getSafeUserId(), }); - room.addLiveEvents([event]); + room.addLiveEvents([event], { addToState: true }); return event; }; @@ -65,7 +65,7 @@ describe("isRelatedToVoiceBroadcast", () => { user: client.getSafeUserId(), }); - room.addLiveEvents([broadcastEvent, nonBroadcastEvent]); + room.addLiveEvents([broadcastEvent, nonBroadcastEvent], { addToState: true }); }); it("should return true if related (reference) to a broadcast event", () => { diff --git a/test/unit-tests/voice-broadcast/utils/retrieveStartedInfoEvent-test.ts b/test/unit-tests/voice-broadcast/utils/retrieveStartedInfoEvent-test.ts index 9bac7aed3e..70316f3b29 100644 --- a/test/unit-tests/voice-broadcast/utils/retrieveStartedInfoEvent-test.ts +++ b/test/unit-tests/voice-broadcast/utils/retrieveStartedInfoEvent-test.ts @@ -67,7 +67,7 @@ describe("retrieveStartedInfoEvent", () => { it("when the room contains the event, it should return it", async () => { const startEvent = mkStartEvent(); const stopEvent = mkStopEvent(startEvent); - room.addLiveEvents([startEvent]); + room.addLiveEvents([startEvent], { addToState: true }); expect(await retrieveStartedInfoEvent(stopEvent, client)).toBe(startEvent); }); From 0b636aec4bcf4991354c23d4a32986f3fc192e16 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 8 Nov 2024 16:41:57 +0000 Subject: [PATCH 02/27] Improve coverage Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../components/structures/FilePanel-test.tsx | 43 ++++++++++++++++++- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/test/unit-tests/components/structures/FilePanel-test.tsx b/test/unit-tests/components/structures/FilePanel-test.tsx index 1dce220682..25bdd99676 100644 --- a/test/unit-tests/components/structures/FilePanel-test.tsx +++ b/test/unit-tests/components/structures/FilePanel-test.tsx @@ -7,13 +7,13 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; -import { EventTimelineSet, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix"; +import { EventTimelineSet, PendingEventOrdering, Room, RoomEvent } from "matrix-js-sdk/src/matrix"; import { screen, render, waitFor } from "jest-matrix-react"; import { mocked } from "jest-mock"; import FilePanel from "../../../../src/components/structures/FilePanel"; import ResizeNotifier from "../../../../src/utils/ResizeNotifier"; -import { stubClient } from "../../../test-utils"; +import { mkEvent, stubClient } from "../../../test-utils"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; jest.mock("matrix-js-sdk/src/matrix", () => ({ @@ -47,4 +47,43 @@ describe("FilePanel", () => { }); expect(asFragment()).toMatchSnapshot(); }); + + describe("addEncryptedLiveEvent", () => { + it("should add file msgtype event to filtered timelineSet", async () => { + const cli = MatrixClientPeg.safeGet(); + const room = new Room("!room:server", cli, cli.getSafeUserId(), { + pendingEventOrdering: PendingEventOrdering.Detached, + }); + cli.reEmitter.reEmit(room, [RoomEvent.Timeline]); + const timelineSet = new EventTimelineSet(room); + room.getOrCreateFilteredTimelineSet = jest.fn().mockReturnValue(timelineSet); + mocked(cli.getRoom).mockReturnValue(room); + + let filePanel: FilePanel | null; + render( + (filePanel = ref)} + />, + ); + await screen.findByText("No files visible in this room"); + + const event = mkEvent({ + type: "m.room.message", + user: cli.getSafeUserId(), + room: room.roomId, + content: { + body: "hello", + url: "mxc://matrix.org/1234", + msgtype: "m.file", + }, + event: true, + }); + filePanel!.addEncryptedLiveEvent(event); + + expect(timelineSet.getLiveTimeline().getEvents()).toContain(event); + }); + }); }); From 3f221891f73bcee5e82105fe51dc5e61c8af1323 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 25 Nov 2024 17:29:32 +0000 Subject: [PATCH 03/27] Remove space-specific right panel store handling This is no longer needed as the right panel always corresponds to the currently viewed room/space only. Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/RightPanel.tsx | 25 ++++--------------- src/components/structures/RoomView.tsx | 8 +++--- src/components/structures/SpaceRoomView.tsx | 4 +-- src/components/structures/UserView.tsx | 2 +- .../views/right_panel/RoomSummaryCard.tsx | 2 +- src/components/views/right_panel/UserInfo.tsx | 11 +++----- src/components/views/rooms/RoomHeader.tsx | 2 +- src/components/views/rooms/RoomInfoLine.tsx | 2 +- .../views/toasts/VerificationRequestToast.tsx | 2 +- src/stores/right-panel/RightPanelStore.ts | 13 +++++----- .../right-panel/RightPanelStoreIPanelState.ts | 4 --- .../right-panel/RightPanelStorePhases.ts | 16 +++++------- .../action-handlers/View3pidInvite.ts | 4 +-- src/verification.ts | 2 +- .../components/structures/RightPanel-test.tsx | 4 +-- .../right_panel/RoomSummaryCard-test.tsx | 5 +--- .../views/right_panel/UserInfo-test.tsx | 6 ++--- .../views/rooms/PinnedMessageBanner-test.tsx | 2 +- .../views/rooms/RoomHeader-test.tsx | 2 +- .../right-panel/RightPanelStore-test.ts | 16 ++++++------ .../action-handlers/View3pidInvite-test.ts | 4 +-- 21 files changed, 53 insertions(+), 83 deletions(-) diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index 9a9f29f82e..8acff9ae44 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -109,10 +109,10 @@ export default class RightPanel extends React.Component { } // redraw the badge on the membership list - if (this.state.phase === RightPanelPhases.RoomMemberList) { + if (this.state.phase === RightPanelPhases.MemberList) { this.delayedUpdate(); } else if ( - this.state.phase === RightPanelPhases.RoomMemberInfo && + this.state.phase === RightPanelPhases.MemberInfo && member.userId === this.state.cardState?.member?.userId ) { // refresh the member info (e.g. new power level) @@ -157,7 +157,7 @@ export default class RightPanel extends React.Component { const phase = this.props.overwriteCard?.phase ?? this.state.phase; const cardState = this.props.overwriteCard?.state ?? this.state.cardState; switch (phase) { - case RightPanelPhases.RoomMemberList: + case RightPanelPhases.MemberList: if (!!roomId) { card = ( { ); } break; - case RightPanelPhases.SpaceMemberList: - if (!!cardState?.spaceId || !!roomId) { - card = ( - - ); - } - break; - case RightPanelPhases.RoomMemberInfo: - case RightPanelPhases.SpaceMemberInfo: + case RightPanelPhases.MemberInfo: case RightPanelPhases.EncryptionPanel: { if (!!cardState?.member) { const roomMember = cardState.member instanceof RoomMember ? cardState.member : undefined; @@ -203,8 +189,7 @@ export default class RightPanel extends React.Component { } break; } - case RightPanelPhases.Room3pidMemberInfo: - case RightPanelPhases.Space3pidMemberInfo: + case RightPanelPhases.ThreePidMemberInfo: if (!!cardState?.memberInfoEvent) { card = ( diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 470b73de7c..af37226d7d 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -1214,18 +1214,18 @@ export class RoomView extends React.Component { if (payload.member) { if (payload.push) { RightPanelStore.instance.pushCard({ - phase: RightPanelPhases.RoomMemberInfo, + phase: RightPanelPhases.MemberInfo, state: { member: payload.member }, }); } else { RightPanelStore.instance.setCards([ { phase: RightPanelPhases.RoomSummary }, - { phase: RightPanelPhases.RoomMemberList }, - { phase: RightPanelPhases.RoomMemberInfo, state: { member: payload.member } }, + { phase: RightPanelPhases.MemberList }, + { phase: RightPanelPhases.MemberInfo, state: { member: payload.member } }, ]); } } else { - RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomMemberList); + RightPanelStore.instance.showOrHidePhase(RightPanelPhases.MemberList); } break; case Action.View3pidInvite: diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 3ea2a03c1a..6afa7dfdf0 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -208,7 +208,7 @@ const SpaceLanding: React.FC<{ space: Room }> = ({ space }) => { const storeIsShowingSpaceMembers = useCallback( () => RightPanelStore.instance.isOpenForRoom(space.roomId) && - RightPanelStore.instance.currentCardForRoom(space.roomId)?.phase === RightPanelPhases.SpaceMemberList, + RightPanelStore.instance.currentCardForRoom(space.roomId)?.phase === RightPanelPhases.MemberList, [space.roomId], ); const isShowingMembers = useEventEmitterState(RightPanelStore.instance, UPDATE_EVENT, storeIsShowingSpaceMembers); @@ -251,7 +251,7 @@ const SpaceLanding: React.FC<{ space: Room }> = ({ space }) => { } const onMembersClick = (): void => { - RightPanelStore.instance.setCard({ phase: RightPanelPhases.SpaceMemberList }); + RightPanelStore.instance.setCard({ phase: RightPanelPhases.MemberList }); }; return ( diff --git a/src/components/structures/UserView.tsx b/src/components/structures/UserView.tsx index 635115877e..66fefbc320 100644 --- a/src/components/structures/UserView.tsx +++ b/src/components/structures/UserView.tsx @@ -82,7 +82,7 @@ export default class UserView extends React.Component { } else if (this.state.member) { const panel = ( ); diff --git a/src/components/views/right_panel/RoomSummaryCard.tsx b/src/components/views/right_panel/RoomSummaryCard.tsx index 664977bbe2..e4ab5bcf3d 100644 --- a/src/components/views/right_panel/RoomSummaryCard.tsx +++ b/src/components/views/right_panel/RoomSummaryCard.tsx @@ -86,7 +86,7 @@ interface IProps { } const onRoomMembersClick = (): void => { - RightPanelStore.instance.pushCard({ phase: RightPanelPhases.RoomMemberList }, true); + RightPanelStore.instance.pushCard({ phase: RightPanelPhases.MemberList }, true); }; const onRoomThreadsClick = (): void => { diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index d07b3566e2..0005e2efce 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -1739,13 +1739,13 @@ export const UserInfoHeader: React.FC<{ interface IProps { user: Member; room?: Room; - phase: RightPanelPhases.RoomMemberInfo | RightPanelPhases.SpaceMemberInfo | RightPanelPhases.EncryptionPanel; + phase: RightPanelPhases.MemberInfo | RightPanelPhases.MemberInfo | RightPanelPhases.EncryptionPanel; onClose(): void; verificationRequest?: VerificationRequest; verificationRequestPromise?: Promise; } -const UserInfo: React.FC = ({ user, room, onClose, phase = RightPanelPhases.RoomMemberInfo, ...props }) => { +const UserInfo: React.FC = ({ user, room, onClose, phase = RightPanelPhases.MemberInfo, ...props }) => { const cli = useContext(MatrixClientContext); // fetch latest room member if we have a room, so we don't show historical information, falling back to user @@ -1767,8 +1767,6 @@ const UserInfo: React.FC = ({ user, room, onClose, phase = RightPanelPha // We have no previousPhase for when viewing a UserInfo without a Room at this time if (room && phase === RightPanelPhases.EncryptionPanel) { cardState = { member }; - } else if (room?.isSpaceRoom()) { - cardState = { spaceId: room.roomId }; } const onEncryptionPanelClose = (): void => { @@ -1777,8 +1775,7 @@ const UserInfo: React.FC = ({ user, room, onClose, phase = RightPanelPha let content: JSX.Element | undefined; switch (phase) { - case RightPanelPhases.RoomMemberInfo: - case RightPanelPhases.SpaceMemberInfo: + case RightPanelPhases.MemberInfo: content = ( = ({ user, room, onClose, phase = RightPanelPha closeLabel={closeLabel} cardState={cardState} onBack={(ev: ButtonEvent) => { - if (RightPanelStore.instance.previousCard.phase === RightPanelPhases.RoomMemberList) { + if (RightPanelStore.instance.previousCard.phase === RightPanelPhases.MemberList) { PosthogTrackers.trackInteraction("WebRightPanelRoomUserInfoBackButton", ev); } }} diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index c2642ea733..70002259de 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -392,7 +392,7 @@ export default function RoomHeader({ viewUserOnClick={false} tooltipLabel={_t("room|header_face_pile_tooltip")} onClick={(e: ButtonEvent) => { - RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomMemberList); + RightPanelStore.instance.showOrHidePhase(RightPanelPhases.MemberList); e.stopPropagation(); }} aria-label={_t("common|n_members", { count: memberCount })} diff --git a/src/components/views/rooms/RoomInfoLine.tsx b/src/components/views/rooms/RoomInfoLine.tsx index 710ef61758..1487bfe15b 100644 --- a/src/components/views/rooms/RoomInfoLine.tsx +++ b/src/components/views/rooms/RoomInfoLine.tsx @@ -64,7 +64,7 @@ const RoomInfoLine: FC = ({ room }) => { // summary is not still loading const viewMembers = (): void => RightPanelStore.instance.setCard({ - phase: room.isSpaceRoom() ? RightPanelPhases.SpaceMemberList : RightPanelPhases.RoomMemberList, + phase: RightPanelPhases.MemberList, }); members = ( diff --git a/src/components/views/toasts/VerificationRequestToast.tsx b/src/components/views/toasts/VerificationRequestToast.tsx index 54932a12ed..7d31aa6764 100644 --- a/src/components/views/toasts/VerificationRequestToast.tsx +++ b/src/components/views/toasts/VerificationRequestToast.tsx @@ -117,7 +117,7 @@ export default class VerificationRequestToast extends React.PureComponent needs to be changed to RightPanelPhases.EncryptionPanel if there is a pending verification request const { member } = card.state; const pendingRequest = member @@ -385,8 +385,7 @@ export default class RightPanelStore extends ReadyWatchingStore { if (panel?.history) { panel.history = panel.history.filter( (card: IRightPanelCard) => - card.phase != RightPanelPhases.RoomMemberInfo && - card.phase != RightPanelPhases.Room3pidMemberInfo, + card.phase != RightPanelPhases.MemberInfo && card.phase != RightPanelPhases.ThreePidMemberInfo, ); } } diff --git a/src/stores/right-panel/RightPanelStoreIPanelState.ts b/src/stores/right-panel/RightPanelStoreIPanelState.ts index afb7442563..0d205abd2f 100644 --- a/src/stores/right-panel/RightPanelStoreIPanelState.ts +++ b/src/stores/right-panel/RightPanelStoreIPanelState.ts @@ -16,7 +16,6 @@ export interface IRightPanelCardState { verificationRequest?: VerificationRequest; verificationRequestPromise?: Promise; widgetId?: string; - spaceId?: string; // Room3pidMemberInfo, Space3pidMemberInfo, memberInfoEvent?: MatrixEvent; // threads @@ -32,7 +31,6 @@ export interface IRightPanelCardStateStored { memberId?: string; // we do not store the things associated with verification widgetId?: string; - spaceId?: string; // 3pidMemberInfo memberInfoEventId?: string; // threads @@ -80,7 +78,6 @@ export function convertCardToStore(panelState: IRightPanelCard): IRightPanelCard const state = panelState.state ?? {}; const stateStored: IRightPanelCardStateStored = { widgetId: state.widgetId, - spaceId: state.spaceId, isInitialEventHighlighted: state.isInitialEventHighlighted, initialEventScrollIntoView: state.initialEventScrollIntoView, threadHeadEventId: !!state?.threadHeadEvent?.getId() ? state.threadHeadEvent.getId() : undefined, @@ -97,7 +94,6 @@ function convertStoreToCard(panelStateStore: IRightPanelCardStored, room: Room): const stateStored = panelStateStore.state ?? {}; const state: IRightPanelCardState = { widgetId: stateStored.widgetId, - spaceId: stateStored.spaceId, isInitialEventHighlighted: stateStored.isInitialEventHighlighted, initialEventScrollIntoView: stateStored.initialEventScrollIntoView, threadHeadEvent: !!stateStored?.threadHeadEventId diff --git a/src/stores/right-panel/RightPanelStorePhases.ts b/src/stores/right-panel/RightPanelStorePhases.ts index 60b9e50baf..9e7a5697bf 100644 --- a/src/stores/right-panel/RightPanelStorePhases.ts +++ b/src/stores/right-panel/RightPanelStorePhases.ts @@ -10,11 +10,14 @@ import { _t } from "../../languageHandler"; // These are in their own file because of circular imports being a problem. export enum RightPanelPhases { + // Room & Space stuff + MemberList = "MemberList", + MemberInfo = "MemberInfo", + ThreePidMemberInfo = "ThreePidMemberInfo", + // Room stuff - RoomMemberList = "RoomMemberList", FilePanel = "FilePanel", NotificationPanel = "NotificationPanel", - RoomMemberInfo = "RoomMemberInfo", EncryptionPanel = "EncryptionPanel", RoomSummary = "RoomSummary", Widget = "Widget", @@ -22,13 +25,6 @@ export enum RightPanelPhases { Timeline = "Timeline", Extensions = "Extensions", - Room3pidMemberInfo = "Room3pidMemberInfo", - - // Space stuff - SpaceMemberList = "SpaceMemberList", - SpaceMemberInfo = "SpaceMemberInfo", - Space3pidMemberInfo = "Space3pidMemberInfo", - // Thread stuff ThreadView = "ThreadView", ThreadPanel = "ThreadPanel", @@ -42,7 +38,7 @@ export function backLabelForPhase(phase: RightPanelPhases | null): string | null return _t("chat_card_back_action_label"); case RightPanelPhases.RoomSummary: return _t("room_summary_card_back_action_label"); - case RightPanelPhases.RoomMemberList: + case RightPanelPhases.MemberList: return _t("member_list_back_action_label"); case RightPanelPhases.ThreadView: return _t("thread_view_back_action_label"); diff --git a/src/stores/right-panel/action-handlers/View3pidInvite.ts b/src/stores/right-panel/action-handlers/View3pidInvite.ts index e2aa191acb..0f6661819f 100644 --- a/src/stores/right-panel/action-handlers/View3pidInvite.ts +++ b/src/stores/right-panel/action-handlers/View3pidInvite.ts @@ -20,10 +20,10 @@ import { RightPanelPhases } from "../RightPanelStorePhases"; export const onView3pidInvite = (payload: ActionPayload, rightPanelStore: RightPanelStore): void => { if (payload.event) { rightPanelStore.pushCard({ - phase: RightPanelPhases.Room3pidMemberInfo, + phase: RightPanelPhases.ThreePidMemberInfo, state: { memberInfoEvent: payload.event }, }); } else { - rightPanelStore.showOrHidePhase(RightPanelPhases.RoomMemberList); + rightPanelStore.showOrHidePhase(RightPanelPhases.MemberList); } }; diff --git a/src/verification.ts b/src/verification.ts index e446186f80..9c14a64c51 100644 --- a/src/verification.ts +++ b/src/verification.ts @@ -81,7 +81,7 @@ function setRightPanel(state: IRightPanelCardState): void { } else { RightPanelStore.instance.setCards([ { phase: RightPanelPhases.RoomSummary }, - { phase: RightPanelPhases.RoomMemberInfo, state: { member: state.member } }, + { phase: RightPanelPhases.MemberInfo, state: { member: state.member } }, { phase: RightPanelPhases.EncryptionPanel, state }, ]); } diff --git a/test/unit-tests/components/structures/RightPanel-test.tsx b/test/unit-tests/components/structures/RightPanel-test.tsx index e569369db5..ad29791ee9 100644 --- a/test/unit-tests/components/structures/RightPanel-test.tsx +++ b/test/unit-tests/components/structures/RightPanel-test.tsx @@ -91,7 +91,7 @@ describe("RightPanel", () => { if (name !== "RightPanel.phases") return realGetValue(name, roomId); if (roomId === "r1") { return { - history: [{ phase: RightPanelPhases.RoomMemberList }], + history: [{ phase: RightPanelPhases.MemberList }], isOpen: true, }; } @@ -123,7 +123,7 @@ describe("RightPanel", () => { await rpsUpdated; await waitFor(() => expect(screen.queryByTestId("spinner")).not.toBeInTheDocument()); - // room one will be in the RoomMemberList phase - confirm this is rendered + // room one will be in the MemberList phase - confirm this is rendered expect(container.getElementsByClassName("mx_MemberList")).toHaveLength(1); // wait for RPS room 2 updates to fire, then rerender diff --git a/test/unit-tests/components/views/right_panel/RoomSummaryCard-test.tsx b/test/unit-tests/components/views/right_panel/RoomSummaryCard-test.tsx index 4026149f98..d31fc6ed8c 100644 --- a/test/unit-tests/components/views/right_panel/RoomSummaryCard-test.tsx +++ b/test/unit-tests/components/views/right_panel/RoomSummaryCard-test.tsx @@ -254,10 +254,7 @@ describe("", () => { fireEvent.click(getByText("People")); - expect(RightPanelStore.instance.pushCard).toHaveBeenCalledWith( - { phase: RightPanelPhases.RoomMemberList }, - true, - ); + expect(RightPanelStore.instance.pushCard).toHaveBeenCalledWith({ phase: RightPanelPhases.MemberList }, true); }); it("opens room threads list on button click", () => { diff --git a/test/unit-tests/components/views/right_panel/UserInfo-test.tsx b/test/unit-tests/components/views/right_panel/UserInfo-test.tsx index 7e23679fc2..38d38e2a32 100644 --- a/test/unit-tests/components/views/right_panel/UserInfo-test.tsx +++ b/test/unit-tests/components/views/right_panel/UserInfo-test.tsx @@ -188,7 +188,7 @@ describe("", () => { const defaultProps = { user: defaultUser, // idk what is wrong with this type - phase: RightPanelPhases.RoomMemberInfo as RightPanelPhases.RoomMemberInfo, + phase: RightPanelPhases.MemberInfo as RightPanelPhases.MemberInfo, onClose: jest.fn(), }; @@ -455,7 +455,7 @@ describe("", () => { mockCrypto.getUserVerificationStatus.mockResolvedValue(new UserVerificationStatus(false, false, false)); const { container } = renderComponent({ - phase: RightPanelPhases.SpaceMemberInfo, + phase: RightPanelPhases.MemberInfo, verificationRequest, room: mockRoom, }); @@ -649,7 +649,7 @@ describe("", () => { mockClient.getDomain.mockReturnValue("example.com"); const { container } = renderComponent({ - phase: RightPanelPhases.RoomMemberInfo, + phase: RightPanelPhases.MemberInfo, room: mockRoom, }); diff --git a/test/unit-tests/components/views/rooms/PinnedMessageBanner-test.tsx b/test/unit-tests/components/views/rooms/PinnedMessageBanner-test.tsx index f59b9f3a9f..8d380c76bb 100644 --- a/test/unit-tests/components/views/rooms/PinnedMessageBanner-test.tsx +++ b/test/unit-tests/components/views/rooms/PinnedMessageBanner-test.tsx @@ -224,7 +224,7 @@ describe("", () => { // The Right panel is opened on another card jest.spyOn(RightPanelStore.instance, "isOpenForRoom").mockReturnValue(true); jest.spyOn(RightPanelStore.instance, "currentCard", "get").mockReturnValue({ - phase: RightPanelPhases.RoomMemberList, + phase: RightPanelPhases.MemberList, }); renderBanner(); diff --git a/test/unit-tests/components/views/rooms/RoomHeader-test.tsx b/test/unit-tests/components/views/rooms/RoomHeader-test.tsx index 1be9c77713..26ef7ec4fb 100644 --- a/test/unit-tests/components/views/rooms/RoomHeader-test.tsx +++ b/test/unit-tests/components/views/rooms/RoomHeader-test.tsx @@ -158,7 +158,7 @@ describe("RoomHeader", () => { fireEvent.click(facePile); - expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.RoomMemberList }); + expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.MemberList }); }); it("has room info icon that opens the room info panel", async () => { diff --git a/test/unit-tests/stores/right-panel/RightPanelStore-test.ts b/test/unit-tests/stores/right-panel/RightPanelStore-test.ts index 92299426ba..7fb32bcd6e 100644 --- a/test/unit-tests/stores/right-panel/RightPanelStore-test.ts +++ b/test/unit-tests/stores/right-panel/RightPanelStore-test.ts @@ -97,7 +97,7 @@ describe("RightPanelStore", () => { it("does nothing if given an invalid state", async () => { await viewRoom("!1:example.org"); // Needs a member specified to be valid - store.setCard({ phase: RightPanelPhases.RoomMemberInfo }, true, "!1:example.org"); + store.setCard({ phase: RightPanelPhases.MemberInfo }, true, "!1:example.org"); expect(store.roomPhaseHistory).toEqual([]); }); it("only creates a single history entry if given the same card twice", async () => { @@ -114,15 +114,15 @@ describe("RightPanelStore", () => { it("overwrites history if changing the phase", async () => { await viewRoom("!1:example.org"); store.setCard({ phase: RightPanelPhases.RoomSummary }, true, "!1:example.org"); - store.setCard({ phase: RightPanelPhases.RoomMemberList }, true, "!1:example.org"); - expect(store.roomPhaseHistory).toEqual([{ phase: RightPanelPhases.RoomMemberList, state: {} }]); + store.setCard({ phase: RightPanelPhases.MemberList }, true, "!1:example.org"); + expect(store.roomPhaseHistory).toEqual([{ phase: RightPanelPhases.MemberList, state: {} }]); }); }); describe("setCards", () => { it("overwrites history", async () => { await viewRoom("!1:example.org"); - store.setCard({ phase: RightPanelPhases.RoomMemberList }, true, "!1:example.org"); + store.setCard({ phase: RightPanelPhases.MemberList }, true, "!1:example.org"); store.setCards( [{ phase: RightPanelPhases.RoomSummary }, { phase: RightPanelPhases.PinnedMessages }], true, @@ -200,21 +200,21 @@ describe("RightPanelStore", () => { store.setCards( [ { - phase: RightPanelPhases.RoomMemberList, + phase: RightPanelPhases.MemberList, }, { - phase: RightPanelPhases.RoomMemberInfo, + phase: RightPanelPhases.MemberInfo, state: { member: new RoomMember("!1:example.org", "@alice:example.org") }, }, ], true, "!1:example.org", ); - expect(store.currentCardForRoom("!1:example.org").phase).toEqual(RightPanelPhases.RoomMemberInfo); + expect(store.currentCardForRoom("!1:example.org").phase).toEqual(RightPanelPhases.MemberInfo); // Switch away and back await viewRoom("!2:example.org"); await viewRoom("!1:example.org"); - expect(store.currentCardForRoom("!1:example.org").phase).toEqual(RightPanelPhases.RoomMemberList); + expect(store.currentCardForRoom("!1:example.org").phase).toEqual(RightPanelPhases.MemberList); }); }); diff --git a/test/unit-tests/stores/right-panel/action-handlers/View3pidInvite-test.ts b/test/unit-tests/stores/right-panel/action-handlers/View3pidInvite-test.ts index d0b721243f..9503bb4ae3 100644 --- a/test/unit-tests/stores/right-panel/action-handlers/View3pidInvite-test.ts +++ b/test/unit-tests/stores/right-panel/action-handlers/View3pidInvite-test.ts @@ -30,7 +30,7 @@ describe("onView3pidInvite()", () => { }; onView3pidInvite(payload, rightPanelStore); - expect(rightPanelStore.showOrHidePhase).toHaveBeenCalledWith(RightPanelPhases.RoomMemberList); + expect(rightPanelStore.showOrHidePhase).toHaveBeenCalledWith(RightPanelPhases.MemberList); expect(rightPanelStore.pushCard).not.toHaveBeenCalled(); }); @@ -43,7 +43,7 @@ describe("onView3pidInvite()", () => { expect(rightPanelStore.showOrHidePhase).not.toHaveBeenCalled(); expect(rightPanelStore.pushCard).toHaveBeenCalledWith({ - phase: RightPanelPhases.Room3pidMemberInfo, + phase: RightPanelPhases.ThreePidMemberInfo, state: { memberInfoEvent: payload.event }, }); }); From a478463b751fe6ee44293a0351e44f2a967864b0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 25 Nov 2024 17:43:09 +0000 Subject: [PATCH 04/27] Remove duplicates Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/right_panel/UserInfo.tsx | 2 +- src/stores/right-panel/RightPanelStore.ts | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index 0005e2efce..b4a775367c 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -1739,7 +1739,7 @@ export const UserInfoHeader: React.FC<{ interface IProps { user: Member; room?: Room; - phase: RightPanelPhases.MemberInfo | RightPanelPhases.MemberInfo | RightPanelPhases.EncryptionPanel; + phase: RightPanelPhases.MemberInfo | RightPanelPhases.EncryptionPanel; onClose(): void; verificationRequest?: VerificationRequest; verificationRequestPromise?: Promise; diff --git a/src/stores/right-panel/RightPanelStore.ts b/src/stores/right-panel/RightPanelStore.ts index ea74aaa3ae..99b2d7fe50 100644 --- a/src/stores/right-panel/RightPanelStore.ts +++ b/src/stores/right-panel/RightPanelStore.ts @@ -305,13 +305,11 @@ export default class RightPanelStore extends ReadyWatchingStore { } return !!card.state?.threadHeadEvent; case RightPanelPhases.MemberInfo: - case RightPanelPhases.MemberInfo: case RightPanelPhases.EncryptionPanel: if (!card.state?.member) { logger.warn("removed card from right panel because of missing member in card state"); } return !!card.state?.member; - case RightPanelPhases.ThreePidMemberInfo: case RightPanelPhases.ThreePidMemberInfo: if (!card.state?.memberInfoEvent) { logger.warn("removed card from right panel because of missing memberInfoEvent in card state"); From bb54a0e0637a5fc0ec0fef8cf46284715dfb649d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 27 Nov 2024 09:38:26 +0000 Subject: [PATCH 05/27] Update typescript-eslint monorepo to v8.15.0 (#28567) * Update typescript-eslint monorepo to v8.15.0 * Add linter exception Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- src/rageshake/rageshake.ts | 1 + yarn.lock | 99 +++++++++++--------------------------- 2 files changed, 28 insertions(+), 72 deletions(-) diff --git a/src/rageshake/rageshake.ts b/src/rageshake/rageshake.ts index 763df51d95..c68fa8503c 100644 --- a/src/rageshake/rageshake.ts +++ b/src/rageshake/rageshake.ts @@ -97,6 +97,7 @@ export class ConsoleLogger { // run. // Example line: // 2017-01-18T11:23:53.214Z W Failed to set badge count + // eslint-disable-next-line @typescript-eslint/no-base-to-string let line = `${ts} ${level} ${args.join(" ")}\n`; // Do some cleanup line = line.replace(/token=[a-zA-Z0-9-]+/gm, "token=xxxxx"); diff --git a/yarn.lock b/yarn.lock index 7984b6ed5b..4a20a16be3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3271,39 +3271,31 @@ "@types/yargs-parser" "*" "@typescript-eslint/eslint-plugin@^8.0.0": - version "8.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.14.0.tgz#7dc0e419c87beadc8f554bf5a42e5009ed3748dc" - integrity sha512-tqp8H7UWFaZj0yNO6bycd5YjMwxa6wIHOLZvWPkidwbgLCsBMetQoGj7DPuAlWa2yGO3H48xmPwjhsSPPCGU5w== + version "8.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.16.0.tgz#ac56825bcdf3b392fc76a94b1315d4a162f201a6" + integrity sha512-5YTHKV8MYlyMI6BaEG7crQ9BhSc8RxzshOReKwZwRWN0+XvvTOm+L/UYLCYxFpfwYuAAqhxiq4yae0CMFwbL7Q== dependencies: "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "8.14.0" - "@typescript-eslint/type-utils" "8.14.0" - "@typescript-eslint/utils" "8.14.0" - "@typescript-eslint/visitor-keys" "8.14.0" + "@typescript-eslint/scope-manager" "8.16.0" + "@typescript-eslint/type-utils" "8.16.0" + "@typescript-eslint/utils" "8.16.0" + "@typescript-eslint/visitor-keys" "8.16.0" graphemer "^1.4.0" ignore "^5.3.1" natural-compare "^1.4.0" ts-api-utils "^1.3.0" "@typescript-eslint/parser@^8.0.0": - version "8.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.14.0.tgz#0a7e9dbc11bc07716ab2d7b1226217e9f6b51fc8" - integrity sha512-2p82Yn9juUJq0XynBXtFCyrBDb6/dJombnz6vbo6mgQEtWHfvHbQuEa9kAOVIt1c9YFwi7H6WxtPj1kg+80+RA== + version "8.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.16.0.tgz#ee5b2d6241c1ab3e2e53f03fd5a32d8e266d8e06" + integrity sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w== dependencies: - "@typescript-eslint/scope-manager" "8.14.0" - "@typescript-eslint/types" "8.14.0" - "@typescript-eslint/typescript-estree" "8.14.0" - "@typescript-eslint/visitor-keys" "8.14.0" + "@typescript-eslint/scope-manager" "8.16.0" + "@typescript-eslint/types" "8.16.0" + "@typescript-eslint/typescript-estree" "8.16.0" + "@typescript-eslint/visitor-keys" "8.16.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@8.14.0": - version "8.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.14.0.tgz#01f37c147a735cd78f0ff355e033b9457da1f373" - integrity sha512-aBbBrnW9ARIDn92Zbo7rguLnqQ/pOrUguVpbUwzOhkFg2npFDwTgPGqFqE0H5feXcOoJOfX3SxlJaKEVtq54dw== - dependencies: - "@typescript-eslint/types" "8.14.0" - "@typescript-eslint/visitor-keys" "8.14.0" - "@typescript-eslint/scope-manager@8.16.0": version "8.16.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.16.0.tgz#ebc9a3b399a69a6052f3d88174456dd399ef5905" @@ -3320,21 +3312,16 @@ "@typescript-eslint/types" "8.9.0" "@typescript-eslint/visitor-keys" "8.9.0" -"@typescript-eslint/type-utils@8.14.0": - version "8.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.14.0.tgz#455c6af30c336b24a1af28bc4f81b8dd5d74d94d" - integrity sha512-Xcz9qOtZuGusVOH5Uk07NGs39wrKkf3AxlkK79RBK6aJC1l03CobXjJbwBPSidetAOV+5rEVuiT1VSBUOAsanQ== +"@typescript-eslint/type-utils@8.16.0": + version "8.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.16.0.tgz#585388735f7ac390f07c885845c3d185d1b64740" + integrity sha512-IqZHGG+g1XCWX9NyqnI/0CX5LL8/18awQqmkZSl2ynn8F76j579dByc0jhfVSnSnhf7zv76mKBQv9HQFKvDCgg== dependencies: - "@typescript-eslint/typescript-estree" "8.14.0" - "@typescript-eslint/utils" "8.14.0" + "@typescript-eslint/typescript-estree" "8.16.0" + "@typescript-eslint/utils" "8.16.0" debug "^4.3.4" ts-api-utils "^1.3.0" -"@typescript-eslint/types@8.14.0": - version "8.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.14.0.tgz#0d33d8d0b08479c424e7d654855fddf2c71e4021" - integrity sha512-yjeB9fnO/opvLJFAsPNYlKPnEM8+z4og09Pk504dkqonT02AyL5Z9SSqlE0XqezS93v6CXn49VHvB2G7XSsl0g== - "@typescript-eslint/types@8.16.0": version "8.16.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.16.0.tgz#49c92ae1b57942458ab83d9ec7ccab3005e64737" @@ -3345,20 +3332,6 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.9.0.tgz#b733af07fb340b32e962c6c63b1062aec2dc0fe6" integrity sha512-SjgkvdYyt1FAPhU9c6FiYCXrldwYYlIQLkuc+LfAhCna6ggp96ACncdtlbn8FmnG72tUkXclrDExOpEYf1nfJQ== -"@typescript-eslint/typescript-estree@8.14.0": - version "8.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.14.0.tgz#a7a3a5a53a6c09313e12fb4531d4ff582ee3c312" - integrity sha512-OPXPLYKGZi9XS/49rdaCbR5j/S14HazviBlUQFvSKz3npr3NikF+mrgK7CFVur6XEt95DZp/cmke9d5i3vtVnQ== - dependencies: - "@typescript-eslint/types" "8.14.0" - "@typescript-eslint/visitor-keys" "8.14.0" - debug "^4.3.4" - fast-glob "^3.3.2" - is-glob "^4.0.3" - minimatch "^9.0.4" - semver "^7.6.0" - ts-api-utils "^1.3.0" - "@typescript-eslint/typescript-estree@8.16.0": version "8.16.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.16.0.tgz#9d741e56e5b13469b5190e763432ce5551a9300c" @@ -3387,15 +3360,15 @@ semver "^7.6.0" ts-api-utils "^1.3.0" -"@typescript-eslint/utils@8.14.0": - version "8.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.14.0.tgz#ac2506875e03aba24e602364e43b2dfa45529dbd" - integrity sha512-OGqj6uB8THhrHj0Fk27DcHPojW7zKwKkPmHXHvQ58pLYp4hy8CSUdTKykKeh+5vFqTTVmjz0zCOOPKRovdsgHA== +"@typescript-eslint/utils@8.16.0", "@typescript-eslint/utils@^8.13.0": + version "8.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.16.0.tgz#c71264c437157feaa97842809836254a6fc833c3" + integrity sha512-C1zRy/mOL8Pj157GiX4kaw7iyRLKfJXBR3L82hk5kS/GyHcOFmy4YUq/zfZti72I9wnuQtA/+xzft4wCC8PJdA== dependencies: "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "8.14.0" - "@typescript-eslint/types" "8.14.0" - "@typescript-eslint/typescript-estree" "8.14.0" + "@typescript-eslint/scope-manager" "8.16.0" + "@typescript-eslint/types" "8.16.0" + "@typescript-eslint/typescript-estree" "8.16.0" "@typescript-eslint/utils@^6.0.0 || ^7.0.0 || ^8.0.0": version "8.9.0" @@ -3407,24 +3380,6 @@ "@typescript-eslint/types" "8.9.0" "@typescript-eslint/typescript-estree" "8.9.0" -"@typescript-eslint/utils@^8.13.0": - version "8.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.16.0.tgz#c71264c437157feaa97842809836254a6fc833c3" - integrity sha512-C1zRy/mOL8Pj157GiX4kaw7iyRLKfJXBR3L82hk5kS/GyHcOFmy4YUq/zfZti72I9wnuQtA/+xzft4wCC8PJdA== - dependencies: - "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "8.16.0" - "@typescript-eslint/types" "8.16.0" - "@typescript-eslint/typescript-estree" "8.16.0" - -"@typescript-eslint/visitor-keys@8.14.0": - version "8.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.14.0.tgz#2418d5a54669af9658986ade4e6cfb7767d815ad" - integrity sha512-vG0XZo8AdTH9OE6VFRwAZldNc7qtJ/6NLGWak+BtENuEUXGZgFpihILPiBvKXvJ2nFu27XNGC6rKiwuaoMbYzQ== - dependencies: - "@typescript-eslint/types" "8.14.0" - eslint-visitor-keys "^3.4.3" - "@typescript-eslint/visitor-keys@8.16.0": version "8.16.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.16.0.tgz#d5086afc060b01ff7a4ecab8d49d13d5a7b07705" From 2b883a8aa0ee1db8e872841e21d80bd224999d7d Mon Sep 17 00:00:00 2001 From: ElementRobot Date: Wed, 27 Nov 2024 09:40:33 +0000 Subject: [PATCH 06/27] [create-pull-request] automated change (#28572) Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com> Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/plugins/homeserver/synapse/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright/plugins/homeserver/synapse/index.ts b/playwright/plugins/homeserver/synapse/index.ts index f11d94a703..941a08d276 100644 --- a/playwright/plugins/homeserver/synapse/index.ts +++ b/playwright/plugins/homeserver/synapse/index.ts @@ -20,7 +20,7 @@ import { randB64Bytes } from "../../utils/rand"; // Docker tag to use for synapse docker image. // We target a specific digest as every now and then a Synapse update will break our CI. // This digest is updated by the playwright-image-updates.yaml workflow periodically. -const DOCKER_TAG = "develop@sha256:e163b15bf4905e4067dece856cca00e6ac8d1d655f4f1307978eee256b3ea775"; +const DOCKER_TAG = "develop@sha256:b261d81d9a3615a7716fc92423ee5689b0b450ed49f87a4887e49ecab7aefe45"; async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise> { const templateDir = path.join(__dirname, "templates", opts.template); From 8e213c5d341c3acf6bae5df52904d63da70ac53a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 27 Nov 2024 14:16:05 +0000 Subject: [PATCH 07/27] Add source-map-loader for easier debugging (#28580) of matrix-widget-api and other libs Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- package.json | 1 + webpack.config.js | 8 ++++++++ yarn.lock | 12 ++++++++++-- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 3108751241..8417edc6f6 100644 --- a/package.json +++ b/package.json @@ -273,6 +273,7 @@ "raw-loader": "^4.0.2", "rimraf": "^6.0.0", "semver": "^7.5.2", + "source-map-loader": "^5.0.0", "stylelint": "^16.1.0", "stylelint-config-standard": "^36.0.0", "stylelint-scss": "^6.0.0", diff --git a/webpack.config.js b/webpack.config.js index 14f472a189..0ed4dc3650 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -238,6 +238,9 @@ module.exports = (env, argv) => { }, }, + // Some of our deps have broken source maps, so we have to ignore warnings or exclude them one-by-one + ignoreWarnings: [/Failed to parse source map/], + module: { noParse: [ // for cross platform compatibility use [\\\/] as the path separator @@ -250,6 +253,11 @@ module.exports = (env, argv) => { /highlight\.js[\\/]lib[\\/]languages/, ], rules: [ + { + test: /\.js$/, + enforce: "pre", + use: ["source-map-loader"], + }, { test: /\.(ts|js)x?$/, include: (f) => { diff --git a/yarn.lock b/yarn.lock index 4a20a16be3..b21301342c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6858,7 +6858,7 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@0.6.3, iconv-lite@^0.6: +iconv-lite@0.6.3, iconv-lite@^0.6, iconv-lite@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== @@ -10787,11 +10787,19 @@ sockjs@^0.3.24: uuid "^8.3.2" websocket-driver "^0.7.4" -source-map-js@^1.0.1, source-map-js@^1.2.0, source-map-js@^1.2.1: +source-map-js@^1.0.1, source-map-js@^1.0.2, source-map-js@^1.2.0, source-map-js@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== +source-map-loader@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-5.0.0.tgz#f593a916e1cc54471cfc8851b905c8a845fc7e38" + integrity sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA== + dependencies: + iconv-lite "^0.6.3" + source-map-js "^1.0.2" + source-map-support@0.5.13: version "0.5.13" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" From bfac727307932cb534a5bf4c69a6595eb46f4625 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 27 Nov 2024 14:35:49 +0000 Subject: [PATCH 08/27] Apply release blocker checks to cut branches workflow (#28551) * Apply release blocker checks to cut branches workflow Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .github/workflows/release_prepare.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/release_prepare.yml b/.github/workflows/release_prepare.yml index b655bb4206..2d04852258 100644 --- a/.github/workflows/release_prepare.yml +++ b/.github/workflows/release_prepare.yml @@ -19,8 +19,23 @@ on: default: true permissions: {} # Uses ELEMENT_BOT_TOKEN instead jobs: + checks: + name: Sanity checks + strategy: + matrix: + repo: + - matrix-org/matrix-js-sdk + - element-hq/element-web + - element-hq/element-desktop + uses: matrix-org/matrix-js-sdk/.github/workflows/release-checks.yml@develop + secrets: + GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + with: + repository: ${{ matrix.repo }} + prepare: runs-on: ubuntu-24.04 + needs: checks env: # The order is specified bottom-up to avoid any races for allchange REPOS: matrix-js-sdk element-web element-desktop From 9c74110969e9474a3a7d67090dec992de2cc1c9b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 27 Nov 2024 15:17:25 +0000 Subject: [PATCH 09/27] Add Modernizr warning when running in non-secure context (#28581) Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/vector/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vector/index.ts b/src/vector/index.ts index 60f0868eeb..42b69af70e 100644 --- a/src/vector/index.ts +++ b/src/vector/index.ts @@ -67,6 +67,10 @@ function checkBrowserFeatures(): boolean { // although this would start to make (more) assumptions about how rust-crypto loads its wasm. window.Modernizr.addTest("wasm", () => typeof WebAssembly === "object" && typeof WebAssembly.Module === "function"); + // Check that the session is in a secure context otherwise most Crypto & WebRTC APIs will be unavailable + // https://developer.mozilla.org/en-US/docs/Web/API/Window/isSecureContext + window.Modernizr.addTest("securecontext", () => window.isSecureContext); + const featureList = Object.keys(window.Modernizr) as Array; let featureComplete = true; From 6798239aa82c1cb2f992af97dfdc5e3d133c9f5a Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 27 Nov 2024 16:22:45 +0100 Subject: [PATCH 10/27] Check room encryption earlier (#28579) --- src/components/structures/RoomView.tsx | 32 ++++++++++++++------------ 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 58e37606b2..54c2de7619 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -671,6 +671,7 @@ export class RoomView extends React.Component { // the RoomView instance if (initial) { newState.room = this.context.client!.getRoom(newState.roomId) || undefined; + newState.isRoomEncrypted = null; if (newState.room) { newState.showApps = this.shouldShowApps(newState.room); this.onRoomLoaded(newState.room); @@ -713,6 +714,14 @@ export class RoomView extends React.Component { if (initial) { this.setupRoom(newState.room, newState.roomId, !!newState.joining, !!newState.shouldPeek); } + + // We don't block the initial setup but we want to make it early to not block the timeline rendering + const isRoomEncrypted = await this.getIsRoomEncrypted(newState.roomId); + this.setState({ + isRoomEncrypted, + ...(isRoomEncrypted && + newState.roomId && { e2eStatus: RoomView.e2eStatusCache.get(newState.roomId) ?? E2EStatus.Warning }), + }); }; private onConnectedCalls = (): void => { @@ -863,7 +872,7 @@ export class RoomView extends React.Component { return isManuallyShown && widgets.length > 0; } - public async componentDidMount(): Promise { + public componentDidMount(): void { this.unmounted = false; this.dispatcherRef = defaultDispatcher.register(this.onAction); @@ -1482,24 +1491,17 @@ export class RoomView extends React.Component { private async updateE2EStatus(room: Room): Promise { if (!this.context.client || !this.state.isRoomEncrypted) return; - - // If crypto is not currently enabled, we aren't tracking devices at all, - // so we don't know what the answer is. Let's error on the safe side and show - // a warning for this case. - let e2eStatus = RoomView.e2eStatusCache.get(room.roomId) ?? E2EStatus.Warning; - // set the state immediately then update, so we don't scare the user into thinking the room is unencrypted + const e2eStatus = await this.cacheAndGetE2EStatus(room, this.context.client); + if (this.unmounted) return; this.setState({ e2eStatus }); - - if (this.context.client.getCrypto()) { - /* At this point, the user has encryption on and cross-signing on */ - e2eStatus = await this.cacheAndGetE2EStatus(room, this.context.client); - if (this.unmounted) return; - this.setState({ e2eStatus }); - } } private async cacheAndGetE2EStatus(room: Room, client: MatrixClient): Promise { - const e2eStatus = await shieldStatusForRoom(client, room); + let e2eStatus = RoomView.e2eStatusCache.get(room.roomId); + // set the state immediately then update, so we don't scare the user into thinking the room is unencrypted + if (e2eStatus) this.setState({ e2eStatus }); + + e2eStatus = await shieldStatusForRoom(client, room); RoomView.e2eStatusCache.set(room.roomId, e2eStatus); return e2eStatus; } From 08418c16c92a6c93126680b613ae756a17056e06 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 27 Nov 2024 17:56:20 +0100 Subject: [PATCH 11/27] Remove `Features.RustCrypto` (#28582) --- src/MatrixClientPeg.ts | 7 ------- src/settings/Settings.tsx | 11 ----------- test/unit-tests/MatrixClientPeg-test.ts | 11 ----------- 3 files changed, 29 deletions(-) diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 9804ab5d82..ce87953118 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -35,13 +35,11 @@ import IdentityAuthClient from "./IdentityAuthClient"; import { crossSigningCallbacks } from "./SecurityManager"; import { SlidingSyncManager } from "./SlidingSyncManager"; import { _t, UserFriendlyError } from "./languageHandler"; -import { SettingLevel } from "./settings/SettingLevel"; import MatrixClientBackedController from "./settings/controllers/MatrixClientBackedController"; import ErrorDialog from "./components/views/dialogs/ErrorDialog"; import PlatformPeg from "./PlatformPeg"; import { formatList } from "./utils/FormattingUtils"; import SdkConfig from "./SdkConfig"; -import { Features } from "./settings/Settings"; import { setDeviceIsolationMode } from "./settings/controllers/DeviceIsolationModeController.ts"; export interface IMatrixClientCreds { @@ -333,11 +331,6 @@ class MatrixClientPegClass implements IMatrixClientPeg { logger.error("Warning! Not using an encryption key for rust crypto store."); } - // Record the fact that we used the Rust crypto stack with this client. This just guards against people - // rolling back to versions of EW that did not default to Rust crypto (which would lead to an error, since - // we cannot migrate from Rust to Legacy crypto). - await SettingsStore.setValue(Features.RustCrypto, null, SettingLevel.DEVICE, true); - await this.matrixClient.initRustCrypto({ storageKey: rustCryptoStoreKey, storagePassword: rustCryptoStorePassword, diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 1c27f03e88..08cff0c1bb 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -90,13 +90,6 @@ export enum Features { NotificationSettings2 = "feature_notification_settings2", OidcNativeFlow = "feature_oidc_native_flow", ReleaseAnnouncement = "feature_release_announcement", - - /** If true, use the Rust crypto implementation. - * - * This is no longer read, but we continue to populate it on all devices, to guard against people rolling back to - * old versions of EW that do not use rust crypto by default. - */ - RustCrypto = "feature_rust_crypto", } export const labGroupNames: Record = { @@ -469,10 +462,6 @@ export const SETTINGS: { [setting: string]: ISetting } = { description: _td("labs|oidc_native_flow_description"), default: false, }, - [Features.RustCrypto]: { - supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, - default: true, - }, /** * @deprecated in favor of {@link fontSizeDelta} */ diff --git a/test/unit-tests/MatrixClientPeg-test.ts b/test/unit-tests/MatrixClientPeg-test.ts index 5a19b568c0..4653340574 100644 --- a/test/unit-tests/MatrixClientPeg-test.ts +++ b/test/unit-tests/MatrixClientPeg-test.ts @@ -11,8 +11,6 @@ import fetchMockJest from "fetch-mock-jest"; import { advanceDateAndTime, stubClient } from "../test-utils"; import { IMatrixClientPeg, MatrixClientPeg as peg } from "../../src/MatrixClientPeg"; -import SettingsStore from "../../src/settings/SettingsStore"; -import { SettingLevel } from "../../src/settings/SettingLevel"; jest.useFakeTimers(); @@ -81,27 +79,18 @@ describe("MatrixClientPeg", () => { }); it("should initialise the rust crypto library by default", async () => { - const mockSetValue = jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined); - const mockInitRustCrypto = jest.spyOn(testPeg.safeGet(), "initRustCrypto").mockResolvedValue(undefined); const cryptoStoreKey = new Uint8Array([1, 2, 3, 4]); await testPeg.start({ rustCryptoStoreKey: cryptoStoreKey }); expect(mockInitRustCrypto).toHaveBeenCalledWith({ storageKey: cryptoStoreKey }); - - // we should have stashed the setting in the settings store - expect(mockSetValue).toHaveBeenCalledWith("feature_rust_crypto", null, SettingLevel.DEVICE, true); }); it("Should migrate existing login", async () => { - const mockSetValue = jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined); const mockInitRustCrypto = jest.spyOn(testPeg.safeGet(), "initRustCrypto").mockResolvedValue(undefined); await testPeg.start(); expect(mockInitRustCrypto).toHaveBeenCalledTimes(1); - - // we should have stashed the setting in the settings store - expect(mockSetValue).toHaveBeenCalledWith("feature_rust_crypto", null, SettingLevel.DEVICE, true); }); }); }); From 95175caf0cf5f6459842b88be63b563f6ba46b21 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 28 Nov 2024 08:39:51 +0000 Subject: [PATCH 12/27] Remove redundant MSC implementation for io.element.rendezvous (#28583) Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/auth/LoginWithQR.tsx | 3 --- .../settings/devices/LoginWithQRSection.tsx | 23 ++++--------------- .../settings/tabs/user/SessionManagerTab.tsx | 4 +--- .../devices/LoginWithQRSection-test.tsx | 1 - 4 files changed, 5 insertions(+), 26 deletions(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index e931b40a71..e18b0e24ab 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -108,12 +108,9 @@ export default class LoginWithQR extends React.Component { private generateAndShowCode = async (): Promise => { let rendezvous: MSC4108SignInWithQR; try { - const fallbackRzServer = this.props.client?.getClientWellKnown()?.["io.element.rendezvous"]?.server; - const transport = new MSC4108RendezvousSession({ onFailure: this.onFailure, client: this.props.client, - fallbackRzServer, }); await transport.send(""); const channel = new MSC4108SecureChannel(transport, undefined, this.onFailure); diff --git a/src/components/views/settings/devices/LoginWithQRSection.tsx b/src/components/views/settings/devices/LoginWithQRSection.tsx index a164ff894b..033aa8e32a 100644 --- a/src/components/views/settings/devices/LoginWithQRSection.tsx +++ b/src/components/views/settings/devices/LoginWithQRSection.tsx @@ -7,13 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; -import { - IServerVersions, - IClientWellKnown, - OidcClientConfig, - MatrixClient, - DEVICE_CODE_SCOPE, -} from "matrix-js-sdk/src/matrix"; +import { IServerVersions, OidcClientConfig, MatrixClient, DEVICE_CODE_SCOPE } from "matrix-js-sdk/src/matrix"; import QrCodeIcon from "@vector-im/compound-design-tokens/assets/web/icons/qr-code"; import { Text } from "@vector-im/compound-web"; @@ -25,7 +19,6 @@ import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext interface IProps { onShowQr: () => void; versions?: IServerVersions; - wellKnown?: IClientWellKnown; oidcClientConfig?: OidcClientConfig; isCrossSigningReady?: boolean; } @@ -35,10 +28,8 @@ export function shouldShowQr( isCrossSigningReady: boolean, oidcClientConfig?: OidcClientConfig, versions?: IServerVersions, - wellKnown?: IClientWellKnown, ): boolean { - const msc4108Supported = - !!versions?.unstable_features?.["org.matrix.msc4108"] || !!wellKnown?.["io.element.rendezvous"]?.server; + const msc4108Supported = !!versions?.unstable_features?.["org.matrix.msc4108"]; const deviceAuthorizationGrantSupported = oidcClientConfig?.metadata?.grant_types_supported.includes(DEVICE_CODE_SCOPE); @@ -51,15 +42,9 @@ export function shouldShowQr( ); } -const LoginWithQRSection: React.FC = ({ - onShowQr, - versions, - wellKnown, - oidcClientConfig, - isCrossSigningReady, -}) => { +const LoginWithQRSection: React.FC = ({ onShowQr, versions, oidcClientConfig, isCrossSigningReady }) => { const cli = useMatrixClientContext(); - const offerShowQr = shouldShowQr(cli, !!isCrossSigningReady, oidcClientConfig, versions, wellKnown); + const offerShowQr = shouldShowQr(cli, !!isCrossSigningReady, oidcClientConfig, versions); return ( diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index 2e16f45762..5e9445bb99 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { lazy, Suspense, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; +import React, { lazy, Suspense, useCallback, useContext, useEffect, useRef, useState } from "react"; import { discoverAndValidateOIDCIssuerWellKnown, MatrixClient } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { defer } from "matrix-js-sdk/src/utils"; @@ -184,7 +184,6 @@ const SessionManagerTab: React.FC<{ const userId = matrixClient?.getUserId(); const currentUserMember = (userId && matrixClient?.getUser(userId)) || undefined; const clientVersions = useAsyncMemo(() => matrixClient.getVersions(), [matrixClient]); - const wellKnown = useMemo(() => matrixClient?.getClientWellKnown(), [matrixClient]); const oidcClientConfig = useAsyncMemo(async () => { try { const authIssuer = await matrixClient?.getAuthIssuer(); @@ -305,7 +304,6 @@ const SessionManagerTab: React.FC<{ diff --git a/test/unit-tests/components/views/settings/devices/LoginWithQRSection-test.tsx b/test/unit-tests/components/views/settings/devices/LoginWithQRSection-test.tsx index dd6caef1ce..edaf7b39d1 100644 --- a/test/unit-tests/components/views/settings/devices/LoginWithQRSection-test.tsx +++ b/test/unit-tests/components/views/settings/devices/LoginWithQRSection-test.tsx @@ -56,7 +56,6 @@ describe("", () => { const defaultProps = { onShowQr: () => {}, versions: makeVersions({ "org.matrix.msc4108": true }), - wellKnown: {}, }; const getComponent = (props = {}) => ; From d68c5a26af1aa8e17300eb3e0d3f76a804f89c3d Mon Sep 17 00:00:00 2001 From: ElementRobot Date: Fri, 29 Nov 2024 06:20:43 +0000 Subject: [PATCH 13/27] [create-pull-request] automated change (#28586) Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com> --- playwright/plugins/homeserver/synapse/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright/plugins/homeserver/synapse/index.ts b/playwright/plugins/homeserver/synapse/index.ts index 941a08d276..9a219332e0 100644 --- a/playwright/plugins/homeserver/synapse/index.ts +++ b/playwright/plugins/homeserver/synapse/index.ts @@ -20,7 +20,7 @@ import { randB64Bytes } from "../../utils/rand"; // Docker tag to use for synapse docker image. // We target a specific digest as every now and then a Synapse update will break our CI. // This digest is updated by the playwright-image-updates.yaml workflow periodically. -const DOCKER_TAG = "develop@sha256:b261d81d9a3615a7716fc92423ee5689b0b450ed49f87a4887e49ecab7aefe45"; +const DOCKER_TAG = "develop@sha256:42c540d8c3362155dd23cff96207c08b99113e957d213f426018cd437f39af6f"; async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise> { const templateDir = path.join(__dirname, "templates", opts.template); From 2b4ce627b8a8e3715293d4e5c18f4bd12c313efa Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Fri, 29 Nov 2024 12:56:52 +0100 Subject: [PATCH 14/27] Fix format bar position (#28591) --- src/components/views/rooms/MessageComposerFormatBar.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/MessageComposerFormatBar.tsx b/src/components/views/rooms/MessageComposerFormatBar.tsx index 34798cc608..0ab359d9dd 100644 --- a/src/components/views/rooms/MessageComposerFormatBar.tsx +++ b/src/components/views/rooms/MessageComposerFormatBar.tsx @@ -33,6 +33,12 @@ interface IState { export default class MessageComposerFormatBar extends React.PureComponent { private readonly formatBarRef = createRef(); + /** + * The height of the format bar in pixels. + * Height 32px + 2px border + * @private + */ + private readonly BAR_HEIGHT = 34; public constructor(props: IProps) { super(props); @@ -96,7 +102,7 @@ export default class MessageComposerFormatBar extends React.PureComponent Date: Sat, 30 Nov 2024 06:20:20 +0000 Subject: [PATCH 15/27] [create-pull-request] automated change (#28600) Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com> --- playwright/plugins/homeserver/synapse/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright/plugins/homeserver/synapse/index.ts b/playwright/plugins/homeserver/synapse/index.ts index 9a219332e0..f789ca543b 100644 --- a/playwright/plugins/homeserver/synapse/index.ts +++ b/playwright/plugins/homeserver/synapse/index.ts @@ -20,7 +20,7 @@ import { randB64Bytes } from "../../utils/rand"; // Docker tag to use for synapse docker image. // We target a specific digest as every now and then a Synapse update will break our CI. // This digest is updated by the playwright-image-updates.yaml workflow periodically. -const DOCKER_TAG = "develop@sha256:42c540d8c3362155dd23cff96207c08b99113e957d213f426018cd437f39af6f"; +const DOCKER_TAG = "develop@sha256:489fe921e03440af87e001106c41c70ffc55a1e8078d1a7f45e16fbaddc5088a"; async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise> { const templateDir = path.join(__dirname, "templates", opts.template); From 70f898c71db687b4322e4a9f1110416c923569fd Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 2 Dec 2024 09:13:33 +0000 Subject: [PATCH 16/27] Improve coverage Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../right-panel/RightPanelStore-test.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/unit-tests/stores/right-panel/RightPanelStore-test.ts b/test/unit-tests/stores/right-panel/RightPanelStore-test.ts index 7fb32bcd6e..27c4d730a9 100644 --- a/test/unit-tests/stores/right-panel/RightPanelStore-test.ts +++ b/test/unit-tests/stores/right-panel/RightPanelStore-test.ts @@ -18,6 +18,9 @@ import { ActiveRoomChangedPayload } from "../../../../src/dispatcher/payloads/Ac import RightPanelStore from "../../../../src/stores/right-panel/RightPanelStore"; import { RightPanelPhases } from "../../../../src/stores/right-panel/RightPanelStorePhases"; import SettingsStore from "../../../../src/settings/SettingsStore"; +import { pendingVerificationRequestForUser } from "../../../../src/verification.ts"; + +jest.mock("../../../../src/verification"); describe("RightPanelStore", () => { // Mock out the settings store so the right panel store can't persist values between tests @@ -217,4 +220,23 @@ describe("RightPanelStore", () => { await viewRoom("!1:example.org"); expect(store.currentCardForRoom("!1:example.org").phase).toEqual(RightPanelPhases.MemberList); }); + + it("should redirect to verification if set to phase MemberInfo for a user with a pending verification", async () => { + const member = new RoomMember("!1:example.org", "@alice:example.org"); + const verificationRequest = { mockVerificationRequest: true } as any; + mocked(pendingVerificationRequestForUser).mockReturnValue(verificationRequest); + await viewRoom("!1:example.org"); + store.setCard( + { + phase: RightPanelPhases.MemberInfo, + state: { member }, + }, + true, + "!1:example.org", + ); + expect(store.currentCard).toEqual({ + phase: RightPanelPhases.EncryptionPanel, + state: { member, verificationRequest }, + }); + }); }); From 418f121f96a05d84557697aae0d8d0ac26d7ad83 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 09:39:36 +0000 Subject: [PATCH 17/27] Update all non-major dependencies (#28556) * Update all non-major dependencies * Prettier Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- .github/PULL_REQUEST_TEMPLATE.md | 8 +- CONTRIBUTING.md | 52 +- README.md | 64 +- code_style.md | 6 +- docs/SUMMARY.md | 70 +- docs/app-load.md | 18 +- docs/choosing-an-issue.md | 20 +- docs/config.md | 66 +- docs/customisations.md | 6 +- docs/e2ee.md | 6 +- docs/feature-flags.md | 8 +- docs/features/composer.md | 56 +- docs/icons.md | 6 +- docs/jitsi.md | 36 +- docs/playwright.md | 26 +- docs/release.md | 78 +- docs/review.md | 92 +- docs/room-list-store.md | 26 +- docs/settings.md | 14 +- docs/translating-dev.md | 28 +- docs/translating.md | 6 +- package.json | 2 +- playwright/e2e/read-receipts/readme.md | 28 +- playwright/plugins/oauth_server/README.md | 16 +- res/themes/light/css/_light.pcss | 4 +- src/components/structures/EmbeddedPage.tsx | 2 +- src/components/structures/FilePanel.tsx | 2 +- src/components/structures/MessagePanel.tsx | 2 +- .../structures/NotificationPanel.tsx | 2 +- src/components/structures/RightPanel.tsx | 2 +- src/components/structures/RoomStatusBar.tsx | 2 +- src/components/structures/RoomView.tsx | 2 +- src/components/structures/SpaceRoomView.tsx | 2 +- src/components/structures/ThreadView.tsx | 2 +- src/components/structures/TimelinePanel.tsx | 2 +- src/components/structures/UserMenu.tsx | 2 +- src/components/structures/UserView.tsx | 2 +- src/components/structures/auth/SoftLogout.tsx | 2 +- .../context_menus/MessageContextMenu.tsx | 2 +- src/components/views/elements/AppTile.tsx | 2 +- .../views/elements/EventListSummary.tsx | 2 +- .../views/elements/PersistentApp.tsx | 2 +- src/components/views/elements/ReplyChain.tsx | 2 +- .../views/elements/RoomAliasField.tsx | 2 +- .../views/emojipicker/ReactionPicker.tsx | 2 +- src/components/views/emojipicker/Search.tsx | 2 +- .../views/location/LocationPicker.tsx | 2 +- .../views/messages/EditHistoryMessage.tsx | 2 +- src/components/views/messages/MAudioBody.tsx | 2 +- src/components/views/messages/MFileBody.tsx | 2 +- src/components/views/messages/MImageBody.tsx | 2 +- .../views/messages/MLocationBody.tsx | 2 +- src/components/views/messages/MPollBody.tsx | 2 +- src/components/views/messages/MVideoBody.tsx | 2 +- .../views/messages/MessageActionBar.tsx | 2 +- .../views/messages/MessageEvent.tsx | 2 +- .../views/messages/ReactionsRow.tsx | 2 +- .../views/messages/ReactionsRowButton.tsx | 2 +- .../messages/ReactionsRowButtonTooltip.tsx | 2 +- src/components/views/messages/TextualBody.tsx | 2 +- .../views/messages/TextualEvent.tsx | 2 +- .../views/right_panel/TimelineCard.tsx | 2 +- .../views/room_settings/AliasSettings.tsx | 2 +- src/components/views/rooms/Autocomplete.tsx | 2 +- .../views/rooms/EditMessageComposer.tsx | 2 +- src/components/views/rooms/EventTile.tsx | 2 +- src/components/views/rooms/MemberList.tsx | 2 +- .../views/rooms/MessageComposer.tsx | 2 +- .../views/rooms/MessageComposerButtons.tsx | 2 +- src/components/views/rooms/ReplyPreview.tsx | 2 +- src/components/views/rooms/RoomList.tsx | 2 +- .../views/rooms/RoomUpgradeWarningBar.tsx | 2 +- .../views/rooms/SearchResultTile.tsx | 2 +- .../views/rooms/SendMessageComposer.tsx | 2 +- .../views/rooms/VoiceRecordComposerTile.tsx | 2 +- .../views/settings/CryptographyPanel.tsx | 2 +- .../settings/tabs/room/BridgeSettingsTab.tsx | 2 +- .../tabs/room/GeneralRoomSettingsTab.tsx | 2 +- .../tabs/room/NotificationSettingsTab.tsx | 2 +- .../tabs/room/RolesRoomSettingsTab.tsx | 4 +- .../tabs/room/SecurityRoomSettingsTab.tsx | 2 +- .../tabs/user/HelpUserSettingsTab.tsx | 2 +- .../tabs/user/VoiceUserSettingsTab.tsx | 2 +- .../previews/PollStartEventPreview.ts | 2 +- test/unit-tests/TestSdkContext.ts | 24 +- yarn.lock | 860 ++++++++---------- 86 files changed, 820 insertions(+), 926 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 16036541f8..ebde627fde 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,7 +2,7 @@ ## Checklist -- [ ] Tests written for new code (and old code if feasible). -- [ ] New or updated `public`/`exported` symbols have accurate [TSDoc](https://tsdoc.org/) documentation. -- [ ] Linter and other CI checks pass. -- [ ] I have licensed the changes to Element by completing the [Contributor License Agreement (CLA)](https://cla-assistant.io/element-hq/element-web) +- [ ] Tests written for new code (and old code if feasible). +- [ ] New or updated `public`/`exported` symbols have accurate [TSDoc](https://tsdoc.org/) documentation. +- [ ] Linter and other CI checks pass. +- [ ] I have licensed the changes to Element by completing the [Contributor License Agreement (CLA)](https://cla-assistant.io/element-hq/element-web) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 49741b073c..fa887929fb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,26 +20,26 @@ Definitely don't use the GitHub default of "Update file.ts". As for your PR description, it should include these things: -- References to any bugs fixed by the change (in GitHub's `Fixes` notation) -- Describe the why and what is changing in the PR description so it's easy for - onlookers and reviewers to onboard and context switch. This information is - also helpful when we come back to look at this in 6 months and ask "why did - we do it like that?" we have a chance of finding out. - - Why didn't it work before? Why does it work now? What use cases does it - unlock? - - If you find yourself adding information on how the code works or why you - chose to do it the way you did, make sure this information is instead - written as comments in the code itself. - - Sometimes a PR can change considerably as it is developed. In this case, - the description should be updated to reflect the most recent state of - the PR. (It can be helpful to retain the old content under a suitable - heading, for additional context.) -- Include both **before** and **after** screenshots to easily compare and discuss - what's changing. -- Include a step-by-step testing strategy so that a reviewer can check out the - code locally and easily get to the point of testing your change. -- Add comments to the diff for the reviewer that might help them to understand - why the change is necessary or how they might better understand and review it. +- References to any bugs fixed by the change (in GitHub's `Fixes` notation) +- Describe the why and what is changing in the PR description so it's easy for + onlookers and reviewers to onboard and context switch. This information is + also helpful when we come back to look at this in 6 months and ask "why did + we do it like that?" we have a chance of finding out. + - Why didn't it work before? Why does it work now? What use cases does it + unlock? + - If you find yourself adding information on how the code works or why you + chose to do it the way you did, make sure this information is instead + written as comments in the code itself. + - Sometimes a PR can change considerably as it is developed. In this case, + the description should be updated to reflect the most recent state of + the PR. (It can be helpful to retain the old content under a suitable + heading, for additional context.) +- Include both **before** and **after** screenshots to easily compare and discuss + what's changing. +- Include a step-by-step testing strategy so that a reviewer can check out the + code locally and easily get to the point of testing your change. +- Add comments to the diff for the reviewer that might help them to understand + why the change is necessary or how they might better understand and review it. ### Changelogs @@ -79,8 +79,8 @@ element-web notes: Fix a bug where the 'Herd' button only worked on Tuesdays This example is for Element Web. You can specify: -- element-web -- element-desktop +- element-web +- element-desktop If your PR introduces a breaking change, use the `Notes` section in the same way, additionally adding the `X-Breaking-Change` label (see below). There's no need @@ -96,10 +96,10 @@ Notes: Remove legacy `Camelopard` class. `Giraffe` should be used instead. Other metadata can be added using labels. -- `X-Breaking-Change`: A breaking change - adding this label will mean the change causes a _major_ version bump. -- `T-Enhancement`: A new feature - adding this label will mean the change causes a _minor_ version bump. -- `T-Defect`: A bug fix (in either code or docs). -- `T-Task`: No user-facing changes, eg. code comments, CI fixes, refactors or tests. Won't have a changelog entry unless you specify one. +- `X-Breaking-Change`: A breaking change - adding this label will mean the change causes a _major_ version bump. +- `T-Enhancement`: A new feature - adding this label will mean the change causes a _minor_ version bump. +- `T-Defect`: A bug fix (in either code or docs). +- `T-Task`: No user-facing changes, eg. code comments, CI fixes, refactors or tests. Won't have a changelog entry unless you specify one. If you don't have permission to add labels, your PR reviewer(s) can work with you to add them: ask in the PR description or comments. diff --git a/README.md b/README.md index fa4ac89ff9..87e451c9ff 100644 --- a/README.md +++ b/README.md @@ -16,28 +16,28 @@ JS SDK](https://github.com/matrix-org/matrix-js-sdk). Element has several tiers of support for different environments: -- Supported - - Definition: - - Issues **actively triaged**, regressions **block** the release - - Last 2 major versions of Chrome, Firefox, and Edge on desktop OSes - - Last 2 versions of Safari - - Latest release of official Element Desktop app on desktop OSes - - Desktop OSes means macOS, Windows, and Linux versions for desktop devices - that are actively supported by the OS vendor and receive security updates -- Best effort - - Definition: - - Issues **accepted**, regressions **do not block** the release - - The wider Element Products(including Element Call and the Enterprise Server Suite) do still not officially support these browsers. - - The element web project and its contributors should keep the client functioning and gracefully degrade where other sibling features (E.g. Element Call) may not function. - - Last major release of Firefox ESR and Chrome/Edge Extended Stable -- Community Supported - - Definition: - - Issues **accepted**, regressions **do not block** the release - - Community contributions are welcome to support these issues - - Mobile web for current stable version of Chrome, Firefox, and Safari on Android, iOS, and iPadOS -- Not supported - - Definition: Issues only affecting unsupported environments are **closed** - - Everything else +- Supported + - Definition: + - Issues **actively triaged**, regressions **block** the release + - Last 2 major versions of Chrome, Firefox, and Edge on desktop OSes + - Last 2 versions of Safari + - Latest release of official Element Desktop app on desktop OSes + - Desktop OSes means macOS, Windows, and Linux versions for desktop devices + that are actively supported by the OS vendor and receive security updates +- Best effort + - Definition: + - Issues **accepted**, regressions **do not block** the release + - The wider Element Products(including Element Call and the Enterprise Server Suite) do still not officially support these browsers. + - The element web project and its contributors should keep the client functioning and gracefully degrade where other sibling features (E.g. Element Call) may not function. + - Last major release of Firefox ESR and Chrome/Edge Extended Stable +- Community Supported + - Definition: + - Issues **accepted**, regressions **do not block** the release + - Community contributions are welcome to support these issues + - Mobile web for current stable version of Chrome, Firefox, and Safari on Android, iOS, and iPadOS +- Not supported + - Definition: Issues only affecting unsupported environments are **closed** + - Everything else The period of support for these tiers should last until the releases specified above, plus 1 app release cycle(2 weeks). In the case of Firefox ESR this is extended further to allow it land in Debian Stable. @@ -74,16 +74,16 @@ situation, but it's still not good practice to do it in the first place. See Unless you have special requirements, you will want to add the following to your web server configuration when hosting Element Web: -- The `X-Frame-Options: SAMEORIGIN` header, to prevent Element Web from being - framed and protect from [clickjacking][owasp-clickjacking]. -- The `frame-ancestors 'self'` directive to your `Content-Security-Policy` - header, as the modern replacement for `X-Frame-Options` (though both should be - included since not all browsers support it yet, see - [this][owasp-clickjacking-csp]). -- The `X-Content-Type-Options: nosniff` header, to [disable MIME - sniffing][mime-sniffing]. -- The `X-XSS-Protection: 1; mode=block;` header, for basic XSS protection in - legacy browsers. +- The `X-Frame-Options: SAMEORIGIN` header, to prevent Element Web from being + framed and protect from [clickjacking][owasp-clickjacking]. +- The `frame-ancestors 'self'` directive to your `Content-Security-Policy` + header, as the modern replacement for `X-Frame-Options` (though both should be + included since not all browsers support it yet, see + [this][owasp-clickjacking-csp]). +- The `X-Content-Type-Options: nosniff` header, to [disable MIME + sniffing][mime-sniffing]. +- The `X-XSS-Protection: 1; mode=block;` header, for basic XSS protection in + legacy browsers. [mime-sniffing]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#mime_sniffing [owasp-clickjacking-csp]: https://cheatsheetseries.owasp.org/cheatsheets/Clickjacking_Defense_Cheat_Sheet.html#content-security-policy-frame-ancestors-examples diff --git a/code_style.md b/code_style.md index e5f7485cec..9aa6836442 100644 --- a/code_style.md +++ b/code_style.md @@ -3,9 +3,9 @@ This code style applies to projects which the element-web team directly maintains or is reasonably adjacent to. As of writing, these are: -- element-desktop -- element-web -- matrix-js-sdk +- element-desktop +- element-web +- matrix-js-sdk Other projects might extend this code style for increased strictness. For example, matrix-events-sdk has stricter code organization to reduce the maintenance burden. These projects will declare their code diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 23229bf00a..57b017bc1c 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -1,55 +1,55 @@ # Summary -- [Introduction](../README.md) +- [Introduction](../README.md) # Usage -- [Betas](betas.md) -- [Labs](labs.md) +- [Betas](betas.md) +- [Labs](labs.md) # Setup -- [Install](install.md) -- [Config](config.md) -- [Custom home page](custom-home.md) -- [Kubernetes](kubernetes.md) -- [Jitsi](jitsi.md) -- [Encryption](e2ee.md) +- [Install](install.md) +- [Config](config.md) +- [Custom home page](custom-home.md) +- [Kubernetes](kubernetes.md) +- [Jitsi](jitsi.md) +- [Encryption](e2ee.md) # Build -- [Customisations](customisations.md) -- [Modules](modules.md) -- [Native Node modules](native-node-modules.md) +- [Customisations](customisations.md) +- [Modules](modules.md) +- [Native Node modules](native-node-modules.md) # Contribution -- [Choosing an issue](choosing-an-issue.md) -- [Translation](translating.md) -- [Netlify builds](pr-previews.md) -- [Code review](review.md) +- [Choosing an issue](choosing-an-issue.md) +- [Translation](translating.md) +- [Netlify builds](pr-previews.md) +- [Code review](review.md) # Development -- [App load order](app-load.md) -- [Translation](translating-dev.md) -- [Theming](theming.md) -- [Playwright end to end tests](playwright.md) -- [Memory profiling](memory-profiles-and-leaks.md) -- [Jitsi](jitsi-dev.md) -- [Feature flags](feature-flags.md) -- [OIDC and delegated authentication](oidc.md) -- [Release Process](release.md) +- [App load order](app-load.md) +- [Translation](translating-dev.md) +- [Theming](theming.md) +- [Playwright end to end tests](playwright.md) +- [Memory profiling](memory-profiles-and-leaks.md) +- [Jitsi](jitsi-dev.md) +- [Feature flags](feature-flags.md) +- [OIDC and delegated authentication](oidc.md) +- [Release Process](release.md) # Deep dive -- [Skinning](skinning.md) -- [Cider editor](ciderEditor.md) -- [Iconography](icons.md) -- [Jitsi](jitsi.md) -- [Local echo](local-echo-dev.md) -- [Media](media-handling.md) -- [Room List Store](room-list-store.md) -- [Scrolling](scrolling.md) -- [Usercontent](usercontent.md) -- [Widget layouts](widget-layouts.md) +- [Skinning](skinning.md) +- [Cider editor](ciderEditor.md) +- [Iconography](icons.md) +- [Jitsi](jitsi.md) +- [Local echo](local-echo-dev.md) +- [Media](media-handling.md) +- [Room List Store](room-list-store.md) +- [Scrolling](scrolling.md) +- [Usercontent](usercontent.md) +- [Widget layouts](widget-layouts.md) diff --git a/docs/app-load.md b/docs/app-load.md index 849e95cb8d..7f72b3fea4 100644 --- a/docs/app-load.md +++ b/docs/app-load.md @@ -61,18 +61,18 @@ flowchart TD Key: -- Parallelogram: async/await task -- Box: sync task -- Diamond: conditional branch -- Circle: user interaction -- Blue arrow: async task is allowed to settle but allowed to fail -- Red arrow: async task success is asserted +- Parallelogram: async/await task +- Box: sync task +- Diamond: conditional branch +- Circle: user interaction +- Blue arrow: async task is allowed to settle but allowed to fail +- Red arrow: async task success is asserted Notes: -- A task begins when all its dependencies (arrows going into it) are fulfilled. -- The success of setting up rageshake is never asserted, element-web has a fallback path for running without IDB (and thus rageshake). -- Everything is awaited to be settled before the Modernizr check, to allow it to make use of things like i18n if they are successful. +- A task begins when all its dependencies (arrows going into it) are fulfilled. +- The success of setting up rageshake is never asserted, element-web has a fallback path for running without IDB (and thus rageshake). +- Everything is awaited to be settled before the Modernizr check, to allow it to make use of things like i18n if they are successful. Underlying dependencies: diff --git a/docs/choosing-an-issue.md b/docs/choosing-an-issue.md index 9d008782a1..ca17979367 100644 --- a/docs/choosing-an-issue.md +++ b/docs/choosing-an-issue.md @@ -32,19 +32,19 @@ someone to add something. When you're looking through the list, here are some things that might make an issue a **GOOD** choice: -- It is a problem or feature you care about. -- It concerns a type of code you know a little about. -- You think you can understand what's needed. -- It already has approval from Element Web's designers (look for comments from - members of the - [Product](https://github.com/orgs/element-hq/teams/product/members) or - [Design](https://github.com/orgs/element-hq/teams/design/members) teams). +- It is a problem or feature you care about. +- It concerns a type of code you know a little about. +- You think you can understand what's needed. +- It already has approval from Element Web's designers (look for comments from + members of the + [Product](https://github.com/orgs/element-hq/teams/product/members) or + [Design](https://github.com/orgs/element-hq/teams/design/members) teams). Here are some things that might make it a **BAD** choice: -- You don't understand it (maybe add a comment asking a clarifying question). -- It sounds difficult, or is part of a larger change you don't know about. -- **It is tagged with `X-Needs-Design` or `X-Needs-Product`.** +- You don't understand it (maybe add a comment asking a clarifying question). +- It sounds difficult, or is part of a larger change you don't know about. +- **It is tagged with `X-Needs-Design` or `X-Needs-Product`.** **Element Web's Design and Product teams tend to be very busy**, so if you make changes that require approval from one of those teams, you will probably have diff --git a/docs/config.md b/docs/config.md index cc40179740..7a5445f303 100644 --- a/docs/config.md +++ b/docs/config.md @@ -455,7 +455,7 @@ If you would like to use Scalar, the integration manager maintained by Element, For widgets in general (from an integration manager or not) there is also: -- `default_widget_container_height` +- `default_widget_container_height` This controls the height that the top widget panel initially appears as and is the height in pixels, default 280. @@ -551,38 +551,38 @@ preferences. Currently, the following UI feature flags are supported: -- `UIFeature.urlPreviews` - Whether URL previews are enabled across the entire application. -- `UIFeature.feedback` - Whether prompts to supply feedback are shown. -- `UIFeature.voip` - Whether or not VoIP is shown readily to the user. When disabled, - Jitsi widgets will still work though they cannot easily be added. -- `UIFeature.widgets` - Whether or not widgets will be shown. -- `UIFeature.advancedSettings` - Whether or not sections titled "advanced" in room and - user settings are shown to the user. -- `UIFeature.shareQrCode` - Whether or not the QR code on the share room/event dialog - is shown. -- `UIFeature.shareSocial` - Whether or not the social icons on the share room/event dialog - are shown. -- `UIFeature.identityServer` - Whether or not functionality requiring an identity server - is shown. When disabled, the user will not be able to interact with the identity - server (sharing email addresses, 3PID invites, etc). -- `UIFeature.thirdPartyId` - Whether or not UI relating to third party identifiers (3PIDs) - is shown. Typically this is considered "contact information" on the homeserver, and is - not directly related to the identity server. -- `UIFeature.registration` - Whether or not the registration page is accessible. Typically - useful if accounts are managed externally. -- `UIFeature.passwordReset` - Whether or not the password reset page is accessible. Typically - useful if accounts are managed externally. -- `UIFeature.deactivate` - Whether or not the deactivate account button is accessible. Typically - useful if accounts are managed externally. -- `UIFeature.advancedEncryption` - Whether or not advanced encryption options are shown to the - user. -- `UIFeature.roomHistorySettings` - Whether or not the room history settings are shown to the user. - This should only be used if the room history visibility options are managed by the server. -- `UIFeature.TimelineEnableRelativeDates` - Display relative date separators (eg: 'Today', 'Yesterday') in the - timeline for recent messages. When false day dates will be used. -- `UIFeature.BulkUnverifiedSessionsReminder` - Display popup reminders to verify or remove unverified sessions. Defaults - to true. -- `UIFeature.locationSharing` - Whether or not location sharing menus will be shown. +- `UIFeature.urlPreviews` - Whether URL previews are enabled across the entire application. +- `UIFeature.feedback` - Whether prompts to supply feedback are shown. +- `UIFeature.voip` - Whether or not VoIP is shown readily to the user. When disabled, + Jitsi widgets will still work though they cannot easily be added. +- `UIFeature.widgets` - Whether or not widgets will be shown. +- `UIFeature.advancedSettings` - Whether or not sections titled "advanced" in room and + user settings are shown to the user. +- `UIFeature.shareQrCode` - Whether or not the QR code on the share room/event dialog + is shown. +- `UIFeature.shareSocial` - Whether or not the social icons on the share room/event dialog + are shown. +- `UIFeature.identityServer` - Whether or not functionality requiring an identity server + is shown. When disabled, the user will not be able to interact with the identity + server (sharing email addresses, 3PID invites, etc). +- `UIFeature.thirdPartyId` - Whether or not UI relating to third party identifiers (3PIDs) + is shown. Typically this is considered "contact information" on the homeserver, and is + not directly related to the identity server. +- `UIFeature.registration` - Whether or not the registration page is accessible. Typically + useful if accounts are managed externally. +- `UIFeature.passwordReset` - Whether or not the password reset page is accessible. Typically + useful if accounts are managed externally. +- `UIFeature.deactivate` - Whether or not the deactivate account button is accessible. Typically + useful if accounts are managed externally. +- `UIFeature.advancedEncryption` - Whether or not advanced encryption options are shown to the + user. +- `UIFeature.roomHistorySettings` - Whether or not the room history settings are shown to the user. + This should only be used if the room history visibility options are managed by the server. +- `UIFeature.TimelineEnableRelativeDates` - Display relative date separators (eg: 'Today', 'Yesterday') in the + timeline for recent messages. When false day dates will be used. +- `UIFeature.BulkUnverifiedSessionsReminder` - Display popup reminders to verify or remove unverified sessions. Defaults + to true. +- `UIFeature.locationSharing` - Whether or not location sharing menus will be shown. ## Undocumented / developer options diff --git a/docs/customisations.md b/docs/customisations.md index a6f72ab1ab..42cb8c7c5c 100644 --- a/docs/customisations.md +++ b/docs/customisations.md @@ -50,9 +50,9 @@ that properties/state machines won't change. UI for some actions can be hidden via the ComponentVisibility customisation: -- inviting users to rooms and spaces, -- creating rooms, -- creating spaces, +- inviting users to rooms and spaces, +- creating rooms, +- creating spaces, To customise visibility create a customisation module from [ComponentVisibility](https://github.com/element-hq/element-web/blob/master/src/customisations/ComponentVisibility.ts) following the instructions above. diff --git a/docs/e2ee.md b/docs/e2ee.md index 1229f55f38..835c38a1d5 100644 --- a/docs/e2ee.md +++ b/docs/e2ee.md @@ -31,9 +31,9 @@ Set the following on your homeserver's When `force_disable` is true: -- all rooms will be created with encryption disabled, and it will not be possible to enable - encryption from room settings. -- any `io.element.e2ee.default` value will be disregarded. +- all rooms will be created with encryption disabled, and it will not be possible to enable + encryption from room settings. +- any `io.element.e2ee.default` value will be disregarded. Note: If the server is configured to forcibly enable encryption for some or all rooms, this behaviour will be overridden. diff --git a/docs/feature-flags.md b/docs/feature-flags.md index 46e5f1243e..54d54e3b1b 100644 --- a/docs/feature-flags.md +++ b/docs/feature-flags.md @@ -5,10 +5,10 @@ flexibility and control over when and where those features are enabled. For example, flags make the following things possible: -- Extended testing of a feature via labs on develop -- Enabling features when ready instead of the first moment the code is released -- Testing a feature with a specific set of users (by enabling only on a specific - Element instance) +- Extended testing of a feature via labs on develop +- Enabling features when ready instead of the first moment the code is released +- Testing a feature with a specific set of users (by enabling only on a specific + Element instance) The size of the feature controlled by a feature flag may vary widely: it could be a large project like reactions or a smaller change to an existing algorithm. diff --git a/docs/features/composer.md b/docs/features/composer.md index 408c78a8d9..1af4c9c894 100644 --- a/docs/features/composer.md +++ b/docs/features/composer.md @@ -2,37 +2,37 @@ ## Auto Complete -- Hitting tab tries to auto-complete the word before the caret as a room member - - If no matching name is found, a visual bell is shown -- @ + a letter opens auto complete for members starting with the given letter - - When inserting a user pill at the start in the composer, a colon and space is appended to the pill - - When inserting a user pill anywhere else in composer, only a space is appended to the pill -- # + a letter opens auto complete for rooms starting with the given letter -- : open auto complete for emoji -- Pressing arrow-up/arrow-down while the autocomplete is open navigates between auto complete options -- Pressing tab while the autocomplete is open goes to the next autocomplete option, - wrapping around at the end after reverting to the typed text first. +- Hitting tab tries to auto-complete the word before the caret as a room member + - If no matching name is found, a visual bell is shown +- @ + a letter opens auto complete for members starting with the given letter + - When inserting a user pill at the start in the composer, a colon and space is appended to the pill + - When inserting a user pill anywhere else in composer, only a space is appended to the pill +- # + a letter opens auto complete for rooms starting with the given letter +- : open auto complete for emoji +- Pressing arrow-up/arrow-down while the autocomplete is open navigates between auto complete options +- Pressing tab while the autocomplete is open goes to the next autocomplete option, + wrapping around at the end after reverting to the typed text first. ## Formatting -- When selecting text, a formatting bar appears above the selection. -- The formatting bar allows to format the selected test as: - bold, italic, strikethrough, a block quote, and a code block (inline if no linebreak is selected). -- Formatting is applied as markdown syntax. -- Hitting ctrl/cmd+B also marks the selected text as bold -- Hitting ctrl/cmd+I also marks the selected text as italic -- Hitting ctrl/cmd+> also marks the selected text as a blockquote +- When selecting text, a formatting bar appears above the selection. +- The formatting bar allows to format the selected test as: + bold, italic, strikethrough, a block quote, and a code block (inline if no linebreak is selected). +- Formatting is applied as markdown syntax. +- Hitting ctrl/cmd+B also marks the selected text as bold +- Hitting ctrl/cmd+I also marks the selected text as italic +- Hitting ctrl/cmd+> also marks the selected text as a blockquote ## Misc -- When hitting the arrow-up button while having the caret at the start in the composer, - the last message sent by the syncing user is edited. -- Clicking a display name on an event in the timeline inserts a user pill into the composer -- Emoticons (like :-), >:-), :-/, ...) are replaced by emojis while typing if the relevant setting is enabled -- Typing in the composer sends typing notifications in the room -- Pressing ctrl/mod+z and ctrl/mod+y undoes/redoes modifications -- Pressing shift+enter inserts a line break -- Pressing enter sends the message. -- Choosing "Quote" in the context menu of an event inserts a quote of the event body in the composer. -- Choosing "Reply" in the context menu of an event shows a preview above the composer to reply to. -- Pressing alt+arrow up/arrow down navigates in previously sent messages, putting them in the composer. +- When hitting the arrow-up button while having the caret at the start in the composer, + the last message sent by the syncing user is edited. +- Clicking a display name on an event in the timeline inserts a user pill into the composer +- Emoticons (like :-), >:-), :-/, ...) are replaced by emojis while typing if the relevant setting is enabled +- Typing in the composer sends typing notifications in the room +- Pressing ctrl/mod+z and ctrl/mod+y undoes/redoes modifications +- Pressing shift+enter inserts a line break +- Pressing enter sends the message. +- Choosing "Quote" in the context menu of an event inserts a quote of the event body in the composer. +- Choosing "Reply" in the context menu of an event shows a preview above the composer to reply to. +- Pressing alt+arrow up/arrow down navigates in previously sent messages, putting them in the composer. diff --git a/docs/icons.md b/docs/icons.md index b0582356ce..449663e24a 100644 --- a/docs/icons.md +++ b/docs/icons.md @@ -8,9 +8,9 @@ Icons have `role="presentation"` and `aria-hidden` automatically applied. These SVG file recommendations: -- Colours should not be defined absolutely. Use `currentColor` instead. -- SVG files should be taken from the design compound as they are. Some icons contain special padding. - This means that there should be icons for each size, e.g. warning-16px and warning-32px. +- Colours should not be defined absolutely. Use `currentColor` instead. +- SVG files should be taken from the design compound as they are. Some icons contain special padding. + This means that there should be icons for each size, e.g. warning-16px and warning-32px. Example usage: diff --git a/docs/jitsi.md b/docs/jitsi.md index 48d1a7bf3e..20e64db379 100644 --- a/docs/jitsi.md +++ b/docs/jitsi.md @@ -81,27 +81,27 @@ which takes several parameters: _Query string_: -- `widgetId`: The ID of the widget. This is needed for communication back to the - react-sdk. -- `parentUrl`: The URL of the parent window. This is also needed for - communication back to the react-sdk. +- `widgetId`: The ID of the widget. This is needed for communication back to the + react-sdk. +- `parentUrl`: The URL of the parent window. This is also needed for + communication back to the react-sdk. _Hash/fragment (formatted as a query string)_: -- `conferenceDomain`: The domain to connect Jitsi Meet to. -- `conferenceId`: The room or conference ID to connect Jitsi Meet to. -- `isAudioOnly`: Boolean for whether this is a voice-only conference. May not - be present, should default to `false`. -- `startWithAudioMuted`: Boolean for whether the calls start with audio - muted. May not be present. -- `startWithVideoMuted`: Boolean for whether the calls start with video - muted. May not be present. -- `displayName`: The display name of the user viewing the widget. May not - be present or could be null. -- `avatarUrl`: The HTTP(S) URL for the avatar of the user viewing the widget. May - not be present or could be null. -- `userId`: The MXID of the user viewing the widget. May not be present or could - be null. +- `conferenceDomain`: The domain to connect Jitsi Meet to. +- `conferenceId`: The room or conference ID to connect Jitsi Meet to. +- `isAudioOnly`: Boolean for whether this is a voice-only conference. May not + be present, should default to `false`. +- `startWithAudioMuted`: Boolean for whether the calls start with audio + muted. May not be present. +- `startWithVideoMuted`: Boolean for whether the calls start with video + muted. May not be present. +- `displayName`: The display name of the user viewing the widget. May not + be present or could be null. +- `avatarUrl`: The HTTP(S) URL for the avatar of the user viewing the widget. May + not be present or could be null. +- `userId`: The MXID of the user viewing the widget. May not be present or could + be null. The react-sdk will assume that `jitsi.html` is at the path of wherever it is currently being served. For example, `https://develop.element.io/jitsi.html` or `vector://webapp/jitsi.html`. diff --git a/docs/playwright.md b/docs/playwright.md index 7eae8e783d..1454c4868f 100644 --- a/docs/playwright.md +++ b/docs/playwright.md @@ -2,10 +2,10 @@ ## Contents -- How to run the tests -- How the tests work -- How to write great Playwright tests -- Visual testing +- How to run the tests +- How the tests work +- How to write great Playwright tests +- Visual testing ## Running the Tests @@ -123,15 +123,15 @@ When a Synapse instance is started, it's given a config generated from one of th templates in `playwright/plugins/homeserver/synapse/templates`. There are a couple of special files in these templates: -- `homeserver.yaml`: - Template substitution happens in this file. Template variables are: - - `REGISTRATION_SECRET`: The secret used to register users via the REST API. - - `MACAROON_SECRET_KEY`: Generated each time for security - - `FORM_SECRET`: Generated each time for security - - `PUBLIC_BASEURL`: The localhost url + port combination the synapse is accessible at -- `localhost.signing.key`: A signing key is auto-generated and saved to this file. - Config templates should not contain a signing key and instead assume that one will exist - in this file. +- `homeserver.yaml`: + Template substitution happens in this file. Template variables are: + - `REGISTRATION_SECRET`: The secret used to register users via the REST API. + - `MACAROON_SECRET_KEY`: Generated each time for security + - `FORM_SECRET`: Generated each time for security + - `PUBLIC_BASEURL`: The localhost url + port combination the synapse is accessible at +- `localhost.signing.key`: A signing key is auto-generated and saved to this file. + Config templates should not contain a signing key and instead assume that one will exist + in this file. All other files in the template are copied recursively to `/data/`, so the file `foo.html` in a template can be referenced in the config as `/data/foo.html`. diff --git a/docs/release.md b/docs/release.md index 5074039374..b2c797b66b 100644 --- a/docs/release.md +++ b/docs/release.md @@ -82,28 +82,28 @@ This label will automagically convert to `X-Release-Blocker` at the conclusion o This release process revolves around our main repositories: -- [Element Desktop](https://github.com/element-hq/element-desktop/) -- [Element Web](https://github.com/element-hq/element-web/) -- [Matrix JS SDK](https://github.com/matrix-org/matrix-js-sdk/) +- [Element Desktop](https://github.com/element-hq/element-desktop/) +- [Element Web](https://github.com/element-hq/element-web/) +- [Matrix JS SDK](https://github.com/matrix-org/matrix-js-sdk/) We own other repositories, but they have more ad-hoc releases and are not part of the bi-weekly cycle: -- https://github.com/matrix-org/matrix-web-i18n/ -- https://github.com/matrix-org/matrix-react-sdk-module-api +- https://github.com/matrix-org/matrix-web-i18n/ +- https://github.com/matrix-org/matrix-react-sdk-module-api

Prerequisites

-- You must be part of the 2 Releasers GitHub groups: - - - - -- You will need access to the **VPN** ([docs](https://gitlab.matrix.org/new-vector/internal/-/wikis/SRE/Tailscale)) to be able to follow the instructions under Deploy below. -- You will need the ability to **SSH** in to the production machines to be able to follow the instructions under Deploy below. Ensure that your SSH key has a non-empty passphrase, and you registered your SSH key with Ops. Log a ticket at https://github.com/matrix-org/matrix-ansible-private and ask for: - - Two-factor authentication to be set up on your SSH key. (This is needed to get access to production). - - SSH access to `horme` (staging.element.io and app.element.io) - - Permission to sudo on horme as the user `element` -- You need "**jumphost**" configuration in your local `~/.ssh/config`. This should have been set up as part of your onboarding. +- You must be part of the 2 Releasers GitHub groups: + - + - +- You will need access to the **VPN** ([docs](https://gitlab.matrix.org/new-vector/internal/-/wikis/SRE/Tailscale)) to be able to follow the instructions under Deploy below. +- You will need the ability to **SSH** in to the production machines to be able to follow the instructions under Deploy below. Ensure that your SSH key has a non-empty passphrase, and you registered your SSH key with Ops. Log a ticket at https://github.com/matrix-org/matrix-ansible-private and ask for: + - Two-factor authentication to be set up on your SSH key. (This is needed to get access to production). + - SSH access to `horme` (staging.element.io and app.element.io) + - Permission to sudo on horme as the user `element` +- You need "**jumphost**" configuration in your local `~/.ssh/config`. This should have been set up as part of your onboarding.
@@ -177,7 +177,7 @@ For security, you may wish to merge the security advisory private fork or apply It is worth noting that at the end of the Final/Hotfix/Security release `staging` is merged to `master` which is merged back into `develop` - this means that any commit which goes to `staging` will eventually make its way back to the default branch. -- [ ] The staging branch is prepared +- [ ] The staging branch is prepared # Releasing @@ -192,21 +192,21 @@ switched back to the version of the dependency from the master branch to not lea ### Matrix JS SDK -- [ ] Check the draft release which has been generated by [the automation](https://github.com/matrix-org/matrix-js-sdk/actions/workflows/release-drafter.yml) -- [ ] Make any changes to the release notes in the draft release as are necessary - **Do not click publish, only save draft** -- [ ] Kick off a release using [the automation](https://github.com/matrix-org/matrix-js-sdk/actions/workflows/release.yml) - making sure to select the right type of release. For anything other than an RC: choose final. You should not need to ever switch off either of the Publishing options. +- [ ] Check the draft release which has been generated by [the automation](https://github.com/matrix-org/matrix-js-sdk/actions/workflows/release-drafter.yml) +- [ ] Make any changes to the release notes in the draft release as are necessary - **Do not click publish, only save draft** +- [ ] Kick off a release using [the automation](https://github.com/matrix-org/matrix-js-sdk/actions/workflows/release.yml) - making sure to select the right type of release. For anything other than an RC: choose final. You should not need to ever switch off either of the Publishing options. ### Element Web -- [ ] Check the draft release which has been generated by [the automation](https://github.com/element-hq/element-web/actions/workflows/release-drafter.yml) -- [ ] Make any changes to the release notes in the draft release as are necessary - **Do not click publish, only save draft** -- [ ] Kick off a release using [the automation](https://github.com/element-hq/element-web/actions/workflows/release.yml) - making sure to select the right type of release. For anything other than an RC: choose final. You should not need to ever switch off either of the Publishing options. +- [ ] Check the draft release which has been generated by [the automation](https://github.com/element-hq/element-web/actions/workflows/release-drafter.yml) +- [ ] Make any changes to the release notes in the draft release as are necessary - **Do not click publish, only save draft** +- [ ] Kick off a release using [the automation](https://github.com/element-hq/element-web/actions/workflows/release.yml) - making sure to select the right type of release. For anything other than an RC: choose final. You should not need to ever switch off either of the Publishing options. ### Element Desktop -- [ ] Check the draft release which has been generated by [the automation](https://github.com/element-hq/element-desktop/actions/workflows/release-drafter.yml) -- [ ] Make any changes to the release notes in the draft release as are necessary - **Do not click publish, only save draft** -- [ ] Kick off a release using [the automation](https://github.com/element-hq/element-desktop/actions/workflows/release.yml) - making sure to select the right type of release. For anything other than an RC: choose final. You should not need to ever switch off either of the Publishing options. +- [ ] Check the draft release which has been generated by [the automation](https://github.com/element-hq/element-desktop/actions/workflows/release-drafter.yml) +- [ ] Make any changes to the release notes in the draft release as are necessary - **Do not click publish, only save draft** +- [ ] Kick off a release using [the automation](https://github.com/element-hq/element-desktop/actions/workflows/release.yml) - making sure to select the right type of release. For anything other than an RC: choose final. You should not need to ever switch off either of the Publishing options. # Deploying @@ -214,23 +214,23 @@ We ship the SDKs to npm, this happens as part of the release process. We ship Element Web to dockerhub, `*.element.io`, and packages.element.io. We ship Element Desktop to packages.element.io. -- [ ] Check that element-web has shipped to dockerhub -- [ ] Deploy staging.element.io. [See docs.](https://handbook.element.io/books/element-web-team/page/deploying-appstagingelementio) -- [ ] Test staging.element.io +- [ ] Check that element-web has shipped to dockerhub +- [ ] Deploy staging.element.io. [See docs.](https://handbook.element.io/books/element-web-team/page/deploying-appstagingelementio) +- [ ] Test staging.element.io For final releases additionally do these steps: -- [ ] Deploy app.element.io. [See docs.](https://handbook.element.io/books/element-web-team/page/deploying-appstagingelementio) -- [ ] Test app.element.io -- [ ] Ensure Element Web package has shipped to packages.element.io -- [ ] Ensure Element Desktop packages have shipped to packages.element.io +- [ ] Deploy app.element.io. [See docs.](https://handbook.element.io/books/element-web-team/page/deploying-appstagingelementio) +- [ ] Test app.element.io +- [ ] Ensure Element Web package has shipped to packages.element.io +- [ ] Ensure Element Desktop packages have shipped to packages.element.io # Housekeeping We have some manual housekeeping to do in order to prepare for the next release. -- [ ] Update topics using [the automation](https://github.com/element-hq/element-web/actions/workflows/update-topics.yaml). It will autodetect the current latest version. Don't forget the date you supply should be e.g. September 5th (including the "th") for the script to work. -- [ ] Announce the release in [#element-web-announcements:matrix.org](https://matrix.to/#/#element-web-announcements:matrix.org) +- [ ] Update topics using [the automation](https://github.com/element-hq/element-web/actions/workflows/update-topics.yaml). It will autodetect the current latest version. Don't forget the date you supply should be e.g. September 5th (including the "th") for the script to work. +- [ ] Announce the release in [#element-web-announcements:matrix.org](https://matrix.to/#/#element-web-announcements:matrix.org)
(show) @@ -246,15 +246,15 @@ With wording like: For the first RC of a given release cycle do these steps: -- [ ] Go to the [matrix-js-sdk Renovate dashboard](https://github.com/matrix-org/matrix-js-sdk/issues/2406) and click the checkbox to create/update its PRs. +- [ ] Go to the [matrix-js-sdk Renovate dashboard](https://github.com/matrix-org/matrix-js-sdk/issues/2406) and click the checkbox to create/update its PRs. -- [ ] Go to the [element-web Renovate dashboard](https://github.com/element-hq/element-web/issues/22941) and click the checkbox to create/update its PRs. +- [ ] Go to the [element-web Renovate dashboard](https://github.com/element-hq/element-web/issues/22941) and click the checkbox to create/update its PRs. -- [ ] Go to the [element-desktop Renovate dashboard](https://github.com/element-hq/element-desktop/issues/465) and click the checkbox to create/update its PRs. +- [ ] Go to the [element-desktop Renovate dashboard](https://github.com/element-hq/element-desktop/issues/465) and click the checkbox to create/update its PRs. -- [ ] Later, check back and merge the PRs that succeeded to build. The ones that failed will get picked up by the [maintainer](https://docs.google.com/document/d/1V5VINWXATMpz9UBw4IKmVVB8aw3CxM0Jt7igtHnDfSk/edit#). +- [ ] Later, check back and merge the PRs that succeeded to build. The ones that failed will get picked up by the [maintainer](https://docs.google.com/document/d/1V5VINWXATMpz9UBw4IKmVVB8aw3CxM0Jt7igtHnDfSk/edit#). For final releases additionally do these steps: -- [ ] Archive done column on the [team board](https://github.com/orgs/element-hq/projects/67/views/34) _Note: this should be automated_ -- [ ] Add entry to the [milestones diary](https://docs.google.com/document/d/1cpRFJdfNCo2Ps6jqzQmatzbYEToSrQpyBug0aP_iwZE/edit#heading=h.6y55fw4t283z). The document says only to add significant releases, but we add all of them just in case. +- [ ] Archive done column on the [team board](https://github.com/orgs/element-hq/projects/67/views/34) _Note: this should be automated_ +- [ ] Add entry to the [milestones diary](https://docs.google.com/document/d/1cpRFJdfNCo2Ps6jqzQmatzbYEToSrQpyBug0aP_iwZE/edit#heading=h.6y55fw4t283z). The document says only to add significant releases, but we add all of them just in case. diff --git a/docs/review.md b/docs/review.md index 8f8dc5f09b..c565db5297 100644 --- a/docs/review.md +++ b/docs/review.md @@ -10,53 +10,53 @@ When reviewing code, here are some things we look for and also things we avoid: ### We review for -- Correctness -- Performance -- Accessibility -- Security -- Quality via automated and manual testing -- Comments and documentation where needed -- Sharing knowledge of different areas among the team -- Ensuring it's something we're comfortable maintaining for the long term -- Progress indicators and local echo where appropriate with network activity +- Correctness +- Performance +- Accessibility +- Security +- Quality via automated and manual testing +- Comments and documentation where needed +- Sharing knowledge of different areas among the team +- Ensuring it's something we're comfortable maintaining for the long term +- Progress indicators and local echo where appropriate with network activity ### We should avoid -- Style nits that are already handled by the linter -- Dramatically increasing scope +- Style nits that are already handled by the linter +- Dramatically increasing scope ### Good practices -- Use empathetic language - - See also [Mindful Communication in Code - Reviews](https://kickstarter.engineering/a-guide-to-mindful-communication-in-code-reviews-48aab5282e5e) - and [How to Do Code Reviews Like a Human](https://mtlynch.io/human-code-reviews-1/) -- Authors should prefer smaller commits for easier reviewing and bisection -- Reviewers should be explicit about required versus optional changes - - Reviews are conversations and the PR author should feel comfortable - discussing and pushing back on changes before making them -- Reviewers are encouraged to ask for tests where they believe it is reasonable -- Core team should lead by example through their tone and language -- Take the time to thank and point out good code changes -- Using softer language like "please" and "what do you think?" goes a long way - towards making others feel like colleagues working towards a common goal +- Use empathetic language + - See also [Mindful Communication in Code + Reviews](https://kickstarter.engineering/a-guide-to-mindful-communication-in-code-reviews-48aab5282e5e) + and [How to Do Code Reviews Like a Human](https://mtlynch.io/human-code-reviews-1/) +- Authors should prefer smaller commits for easier reviewing and bisection +- Reviewers should be explicit about required versus optional changes + - Reviews are conversations and the PR author should feel comfortable + discussing and pushing back on changes before making them +- Reviewers are encouraged to ask for tests where they believe it is reasonable +- Core team should lead by example through their tone and language +- Take the time to thank and point out good code changes +- Using softer language like "please" and "what do you think?" goes a long way + towards making others feel like colleagues working towards a common goal ### Workflow -- Authors should request review from the element-web team by default (if someone on - the team is clearly the expert in an area, a direct review request to them may - be more appropriate) -- Reviewers should remove the team review request and request review from - themselves when starting a review to avoid double review -- If there are multiple related PRs authors should reference each of the PRs in - the others before requesting review. Reviewers might start reviewing from - different places and could miss other required PRs. -- Avoid force pushing to a PR after the first round of review -- Use the GitHub default of merge commits when landing (avoid alternate options - like squash or rebase) -- PR author merges after review (assuming they have write access) -- Assign issues only when in progress to indicate to others what can be picked - up +- Authors should request review from the element-web team by default (if someone on + the team is clearly the expert in an area, a direct review request to them may + be more appropriate) +- Reviewers should remove the team review request and request review from + themselves when starting a review to avoid double review +- If there are multiple related PRs authors should reference each of the PRs in + the others before requesting review. Reviewers might start reviewing from + different places and could miss other required PRs. +- Avoid force pushing to a PR after the first round of review +- Use the GitHub default of merge commits when landing (avoid alternate options + like squash or rebase) +- PR author merges after review (assuming they have write access) +- Assign issues only when in progress to indicate to others what can be picked + up ## Code Quality @@ -64,10 +64,10 @@ In the past, we have occasionally written different kinds of tests for Element and the SDKs, but it hasn't been a consistent focus. Going forward, we'd like to change that. -- For new features, code reviewers will expect some form of automated testing to - be included by default -- For bug fixes, regression tests are of course great to have, but we don't want - to block fixes on this, so we won't require them at this time +- For new features, code reviewers will expect some form of automated testing to + be included by default +- For bug fixes, regression tests are of course great to have, but we don't want + to block fixes on this, so we won't require them at this time The above policy is not a strict rule, but instead it's meant to be a conversation between the author and reviewer. As an author, try to think about @@ -104,10 +104,10 @@ perspective. In more detail, our usual process for changes that affect the UI or alter user functionality is: -- For changes that will go live when merged, always flag Design and Product - teams as appropriate -- For changes guarded by a feature flag, Design and Product review is not - required (though may still be useful) since we can continue tweaking +- For changes that will go live when merged, always flag Design and Product + teams as appropriate +- For changes guarded by a feature flag, Design and Product review is not + required (though may still be useful) since we can continue tweaking As it can be difficult to review design work from looking at just the changed files in a PR, a [preview site](./pr-previews.md) that includes your changes diff --git a/docs/room-list-store.md b/docs/room-list-store.md index b87bf5f7bd..4e131ee309 100644 --- a/docs/room-list-store.md +++ b/docs/room-list-store.md @@ -6,11 +6,11 @@ It's so complicated it needs its own README. Legend: -- Orange = External event. -- Purple = Deterministic flow. -- Green = Algorithm definition. -- Red = Exit condition/point. -- Blue = Process definition. +- Orange = External event. +- Purple = Deterministic flow. +- Green = Algorithm definition. +- Red = Exit condition/point. +- Blue = Process definition. ## Algorithms involved @@ -68,14 +68,14 @@ simply get the manual sorting algorithm applied to them with no further involvem algorithm. There are 4 categories: Red, Grey, Bold, and Idle. Each has their own definition based off relative (perceived) importance to the user: -- **Red**: The room has unread mentions waiting for the user. -- **Grey**: The room has unread notifications waiting for the user. Notifications are simply unread - messages which cause a push notification or badge count. Typically, this is the default as rooms get - set to 'All Messages'. -- **Bold**: The room has unread messages waiting for the user. Essentially this is a grey room without - a badge/notification count (or 'Mentions Only'/'Muted'). -- **Idle**: No useful (see definition of useful above) activity has occurred in the room since the user - last read it. +- **Red**: The room has unread mentions waiting for the user. +- **Grey**: The room has unread notifications waiting for the user. Notifications are simply unread + messages which cause a push notification or badge count. Typically, this is the default as rooms get + set to 'All Messages'. +- **Bold**: The room has unread messages waiting for the user. Essentially this is a grey room without + a badge/notification count (or 'Mentions Only'/'Muted'). +- **Idle**: No useful (see definition of useful above) activity has occurred in the room since the user + last read it. Conveniently, each tag gets ordered by those categories as presented: red rooms appear above grey, grey above bold, etc. diff --git a/docs/settings.md b/docs/settings.md index 3f0636d380..e555cd7c1e 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -10,13 +10,13 @@ of dealing with the different levels and exposes easy to use getters and setters Granular Settings rely on a series of known levels in order to use the correct value for the scenario. These levels, in order of priority, are: -- `device` - The current user's device -- `room-device` - The current user's device, but only when in a specific room -- `room-account` - The current user's account, but only when in a specific room -- `account` - The current user's account -- `room` - A specific room (setting for all members of the room) -- `config` - Values are defined by the `setting_defaults` key (usually) in `config.json` -- `default` - The hardcoded default for the settings +- `device` - The current user's device +- `room-device` - The current user's device, but only when in a specific room +- `room-account` - The current user's account, but only when in a specific room +- `account` - The current user's account +- `room` - A specific room (setting for all members of the room) +- `config` - Values are defined by the `setting_defaults` key (usually) in `config.json` +- `default` - The hardcoded default for the settings Individual settings may control which levels are appropriate for them as part of the defaults. This is often to ensure that room administrators cannot force account-only settings upon participants. diff --git a/docs/translating-dev.md b/docs/translating-dev.md index e2a8e2c82a..fd1ac23294 100644 --- a/docs/translating-dev.md +++ b/docs/translating-dev.md @@ -2,9 +2,9 @@ ## Requirements -- A working [Development Setup](../README.md#setting-up-a-dev-environment) -- Latest LTS version of Node.js installed -- Be able to understand English +- A working [Development Setup](../README.md#setting-up-a-dev-environment) +- Latest LTS version of Node.js installed +- Be able to understand English ## Translating strings vs. marking strings for translation @@ -65,17 +65,17 @@ There you can also require all translations to be redone if the meaning of the s 1. Add it to the array in `_t` for example `_t(TKEY, {variable: this.variable})` 1. Add the variable inside the string. The syntax for variables is `%(variable)s`. Please note the _s_ at the end. The name of the variable has to match the previous used name. -- You can use the special `count` variable to choose between multiple versions of the same string, in order to get the correct pluralization. E.g. `_t('You have %(count)s new messages', { count: 2 })` would show 'You have 2 new messages', while `_t('You have %(count)s new messages', { count: 1 })` would show 'You have one new message' (assuming a singular version of the string has been added to the translation file. See above). Passing in `count` is much preferred over having an if-statement choose the correct string to use, because some languages have much more complicated plural rules than english (e.g. they might need a completely different form if there are three things rather than two). -- If you want to translate text that includes e.g. hyperlinks or other HTML you have to also use tag substitution, e.g. `_t('Click here!', {}, { 'a': (sub) => {sub} })`. If you don't do the tag substitution you will end up showing literally '' rather than making a hyperlink. -- You can also use React components with normal variable substitution if you want to insert HTML markup, e.g. `_t('Your email address is %(emailAddress)s', { emailAddress: {userEmailAddress} })`. +- You can use the special `count` variable to choose between multiple versions of the same string, in order to get the correct pluralization. E.g. `_t('You have %(count)s new messages', { count: 2 })` would show 'You have 2 new messages', while `_t('You have %(count)s new messages', { count: 1 })` would show 'You have one new message' (assuming a singular version of the string has been added to the translation file. See above). Passing in `count` is much preferred over having an if-statement choose the correct string to use, because some languages have much more complicated plural rules than english (e.g. they might need a completely different form if there are three things rather than two). +- If you want to translate text that includes e.g. hyperlinks or other HTML you have to also use tag substitution, e.g. `_t('Click here!', {}, { 'a': (sub) => {sub} })`. If you don't do the tag substitution you will end up showing literally '' rather than making a hyperlink. +- You can also use React components with normal variable substitution if you want to insert HTML markup, e.g. `_t('Your email address is %(emailAddress)s', { emailAddress: {userEmailAddress} })`. ## Things to know/Style Guides -- Do not use `_t()` inside `getDefaultProps`: the translations aren't loaded when `getDefaultProps` is called, leading to missing translations. Use `_td()` to indicate that `_t()` will be called on the string later. -- If using translated strings as constants, translated strings can't be in constants loaded at class-load time since the translations won't be loaded. Mark the strings using `_td()` instead and perform the actual translation later. -- If a string is presented in the UI with punctuation like a full stop, include this in the translation strings, since punctuation varies between languages too. -- Avoid "translation in parts", i.e. concatenating translated strings or using translated strings in variable substitutions. Context is important for translations, and translating partial strings this way is simply not always possible. -- Concatenating strings often also introduces an implicit assumption about word order (e.g. that the subject of the sentence comes first), which is incorrect for many languages. -- Translation 'smell test': If you have a string that does not begin with a capital letter (is not the start of a sentence) or it ends with e.g. ':' or a preposition (e.g. 'to') you should recheck that you are not trying to translate a partial sentence. -- If you have multiple strings, that are almost identical, except some part (e.g. a word or two) it is still better to translate the full sentence multiple times. It may seem like inefficient repetition, but unlike programming where you try to minimize repetition, translation is much faster if you have many, full, clear, sentences to work with, rather than fewer, but incomplete sentence fragments. -- Don't forget curly braces when you assign an expression to JSX attributes in the render method) +- Do not use `_t()` inside `getDefaultProps`: the translations aren't loaded when `getDefaultProps` is called, leading to missing translations. Use `_td()` to indicate that `_t()` will be called on the string later. +- If using translated strings as constants, translated strings can't be in constants loaded at class-load time since the translations won't be loaded. Mark the strings using `_td()` instead and perform the actual translation later. +- If a string is presented in the UI with punctuation like a full stop, include this in the translation strings, since punctuation varies between languages too. +- Avoid "translation in parts", i.e. concatenating translated strings or using translated strings in variable substitutions. Context is important for translations, and translating partial strings this way is simply not always possible. +- Concatenating strings often also introduces an implicit assumption about word order (e.g. that the subject of the sentence comes first), which is incorrect for many languages. +- Translation 'smell test': If you have a string that does not begin with a capital letter (is not the start of a sentence) or it ends with e.g. ':' or a preposition (e.g. 'to') you should recheck that you are not trying to translate a partial sentence. +- If you have multiple strings, that are almost identical, except some part (e.g. a word or two) it is still better to translate the full sentence multiple times. It may seem like inefficient repetition, but unlike programming where you try to minimize repetition, translation is much faster if you have many, full, clear, sentences to work with, rather than fewer, but incomplete sentence fragments. +- Don't forget curly braces when you assign an expression to JSX attributes in the render method) diff --git a/docs/translating.md b/docs/translating.md index 657b8cebbc..2b82453f93 100644 --- a/docs/translating.md +++ b/docs/translating.md @@ -2,9 +2,9 @@ ## Requirements -- Web Browser -- Be able to understand English -- Be able to understand the language you want to translate Element into +- Web Browser +- Be able to understand English +- Be able to understand the language you want to translate Element into ## Join #element-translations:matrix.org diff --git a/package.json b/package.json index 8417edc6f6..12a77864aa 100644 --- a/package.json +++ b/package.json @@ -268,7 +268,7 @@ "postcss-preset-env": "^10.0.0", "postcss-scss": "^4.0.4", "postcss-simple-vars": "^7.0.1", - "prettier": "3.3.3", + "prettier": "3.4.1", "process": "^0.11.10", "raw-loader": "^4.0.2", "rimraf": "^6.0.0", diff --git a/playwright/e2e/read-receipts/readme.md b/playwright/e2e/read-receipts/readme.md index 4e4dce297f..33bcfeb93d 100644 --- a/playwright/e2e/read-receipts/readme.md +++ b/playwright/e2e/read-receipts/readme.md @@ -2,19 +2,19 @@ Tips for writing these tests: -- Break up your tests into the smallest test case possible. The purpose of - these tests is to understand hard-to-find bugs, so small tests are necessary. - We know that Playwright recommends combining tests together for performance, but - that will frustrate our goals here. (We will need to find a different way to - reduce CI time.) +- Break up your tests into the smallest test case possible. The purpose of + these tests is to understand hard-to-find bugs, so small tests are necessary. + We know that Playwright recommends combining tests together for performance, but + that will frustrate our goals here. (We will need to find a different way to + reduce CI time.) -- Try to assert something after every action, to make sure it has completed. - E.g.: - markAsRead(room2); - assertRead(room2); - You should especially follow this rule if you are jumping to a different - room or similar straight afterward. +- Try to assert something after every action, to make sure it has completed. + E.g.: + markAsRead(room2); + assertRead(room2); + You should especially follow this rule if you are jumping to a different + room or similar straight afterward. -- Use assertStillRead() if you are asserting something is read when it was - also read before. This waits a little while to make sure you're not getting a - false positive. +- Use assertStillRead() if you are asserting something is read when it was + also read before. This waits a little while to make sure you're not getting a + false positive. diff --git a/playwright/plugins/oauth_server/README.md b/playwright/plugins/oauth_server/README.md index 541756384f..5260704a66 100644 --- a/playwright/plugins/oauth_server/README.md +++ b/playwright/plugins/oauth_server/README.md @@ -4,16 +4,16 @@ A very simple OAuth identity provider server. The following endpoints are exposed: -- `/oauth/auth.html`: An OAuth2 [authorization endpoint](https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint). - In a proper OAuth2 system, this would prompt the user to log in; we just give a big "Submit" button (and an - auth code that can be changed if we want the next step to fail). It redirects back to the calling application - with a "code". +- `/oauth/auth.html`: An OAuth2 [authorization endpoint](https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint). + In a proper OAuth2 system, this would prompt the user to log in; we just give a big "Submit" button (and an + auth code that can be changed if we want the next step to fail). It redirects back to the calling application + with a "code". -- `/oauth/token`: An OAuth2 [token endpoint](https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint). - Receives the code issued by "auth.html" and, if it is valid, exchanges it for an OAuth2 access token. +- `/oauth/token`: An OAuth2 [token endpoint](https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint). + Receives the code issued by "auth.html" and, if it is valid, exchanges it for an OAuth2 access token. -- `/oauth/userinfo`: An OAuth2 [userinfo endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo). - Returns details about the owner of the offered access token. +- `/oauth/userinfo`: An OAuth2 [userinfo endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo). + Returns details about the owner of the offered access token. To start the server, do: diff --git a/res/themes/light/css/_light.pcss b/res/themes/light/css/_light.pcss index 5f278c6f16..32629a55f7 100644 --- a/res/themes/light/css/_light.pcss +++ b/res/themes/light/css/_light.pcss @@ -10,8 +10,8 @@ /* Noto Color Emoji contains digits, in fixed-width, therefore causing digits in flowed text to stand out. TODO: Consider putting all emoji fonts to the end rather than the front. */ -$font-family: "Inter", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Arial", "Helvetica", sans-serif, - "Noto Color Emoji"; +$font-family: "Inter", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Arial", "Helvetica", + sans-serif, "Noto Color Emoji"; $monospace-font-family: "Inconsolata", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Courier", monospace, "Noto Color Emoji"; diff --git a/src/components/structures/EmbeddedPage.tsx b/src/components/structures/EmbeddedPage.tsx index 5c7e81caf5..c471565d91 100644 --- a/src/components/structures/EmbeddedPage.tsx +++ b/src/components/structures/EmbeddedPage.tsx @@ -36,7 +36,7 @@ interface IState { export default class EmbeddedPage extends React.PureComponent { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private unmounted = false; private dispatcherRef?: string; diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx index 07a20315f5..2a82352447 100644 --- a/src/components/structures/FilePanel.tsx +++ b/src/components/structures/FilePanel.tsx @@ -51,7 +51,7 @@ interface IState { */ class FilePanel extends React.Component { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; // This is used to track if a decrypted event was a live event and should be // added to the timeline. diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index b26de2e645..d2133f4f13 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -196,7 +196,7 @@ interface IReadReceiptForUser { */ export default class MessagePanel extends React.Component { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public static defaultProps = { disableGrouping: false, diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx index edec675b14..d0460412db 100644 --- a/src/components/structures/NotificationPanel.tsx +++ b/src/components/structures/NotificationPanel.tsx @@ -33,7 +33,7 @@ interface IState { */ export default class NotificationPanel extends React.PureComponent { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private card = React.createRef(); diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index 9a9f29f82e..d0c683e0fc 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -63,7 +63,7 @@ interface IState { export default class RightPanel extends React.Component { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public constructor(props: Props, context: React.ContextType) { super(props, context); diff --git a/src/components/structures/RoomStatusBar.tsx b/src/components/structures/RoomStatusBar.tsx index 76f3b0c229..3bd69148ae 100644 --- a/src/components/structures/RoomStatusBar.tsx +++ b/src/components/structures/RoomStatusBar.tsx @@ -89,7 +89,7 @@ interface IState { export default class RoomStatusBar extends React.PureComponent { private unmounted = false; public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public constructor(props: IProps, context: React.ContextType) { super(props, context); diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 54c2de7619..c87580490e 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -378,7 +378,7 @@ export class RoomView extends React.Component { private roomViewBody = createRef(); public static contextType = SDKContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public constructor(props: IRoomProps, context: React.ContextType) { super(props, context); diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 3ea2a03c1a..58c7d6d3fd 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -597,7 +597,7 @@ const SpaceSetupPrivateInvite: React.FC<{ export default class SpaceRoomView extends React.PureComponent { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private dispatcherRef?: string; diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index be538a6669..a4dbd2bfe7 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -75,7 +75,7 @@ interface IState { export default class ThreadView extends React.Component { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private dispatcherRef?: string; private layoutWatcherRef?: string; diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index 1063cceffd..a28089c989 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -229,7 +229,7 @@ interface IEventIndexOpts { */ class TimelinePanel extends React.Component { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; // a map from room id to read marker event timestamp public static roomReadMarkerTsMap: Record = {}; diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index b2c7990746..5cd6ea7484 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -79,7 +79,7 @@ const below = (rect: PartialDOMRect): MenuProps => { export default class UserMenu extends React.Component { public static contextType = SDKContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private dispatcherRef?: string; private themeWatcherRef?: string; diff --git a/src/components/structures/UserView.tsx b/src/components/structures/UserView.tsx index 635115877e..0b4cd4e633 100644 --- a/src/components/structures/UserView.tsx +++ b/src/components/structures/UserView.tsx @@ -32,7 +32,7 @@ interface IState { export default class UserView extends React.Component { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public constructor(props: IProps, context: React.ContextType) { super(props, context); diff --git a/src/components/structures/auth/SoftLogout.tsx b/src/components/structures/auth/SoftLogout.tsx index 117485df7e..db72a0a04b 100644 --- a/src/components/structures/auth/SoftLogout.tsx +++ b/src/components/structures/auth/SoftLogout.tsx @@ -64,7 +64,7 @@ interface IState { export default class SoftLogout extends React.Component { public static contextType = SDKContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public constructor(props: IProps, context: React.ContextType) { super(props, context); diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index d5749658c9..2b559aa74c 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -126,7 +126,7 @@ interface IState { export default class MessageContextMenu extends React.Component { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private reactButtonRef = createRef(); // XXX Ref to a functional component diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx index dae452fd5d..56754f14a6 100644 --- a/src/components/views/elements/AppTile.tsx +++ b/src/components/views/elements/AppTile.tsx @@ -116,7 +116,7 @@ interface IState { export default class AppTile extends React.Component { public static contextType = MatrixClientContext; - public declare context: ContextType; + declare public context: ContextType; public static defaultProps: Partial = { waitForIframeLoad: true, diff --git a/src/components/views/elements/EventListSummary.tsx b/src/components/views/elements/EventListSummary.tsx index 7562f992c1..776908375a 100644 --- a/src/components/views/elements/EventListSummary.tsx +++ b/src/components/views/elements/EventListSummary.tsx @@ -73,7 +73,7 @@ export default class EventListSummary extends React.Component< IProps & Required> > { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public static defaultProps = { summaryLength: 1, diff --git a/src/components/views/elements/PersistentApp.tsx b/src/components/views/elements/PersistentApp.tsx index 6fbf0d31bc..5f720dc85e 100644 --- a/src/components/views/elements/PersistentApp.tsx +++ b/src/components/views/elements/PersistentApp.tsx @@ -25,7 +25,7 @@ interface IProps { export default class PersistentApp extends React.Component { public static contextType = MatrixClientContext; - public declare context: ContextType; + declare public context: ContextType; private room: Room; public constructor(props: IProps, context: ContextType) { diff --git a/src/components/views/elements/ReplyChain.tsx b/src/components/views/elements/ReplyChain.tsx index 71846d6065..8e10ca3af9 100644 --- a/src/components/views/elements/ReplyChain.tsx +++ b/src/components/views/elements/ReplyChain.tsx @@ -65,7 +65,7 @@ interface IState { // be low as each event being loaded (after the first) is triggered by an explicit user action. export default class ReplyChain extends React.Component { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private unmounted = false; private room: Room; diff --git a/src/components/views/elements/RoomAliasField.tsx b/src/components/views/elements/RoomAliasField.tsx index f092f9d25f..faa0ccf1a6 100644 --- a/src/components/views/elements/RoomAliasField.tsx +++ b/src/components/views/elements/RoomAliasField.tsx @@ -33,7 +33,7 @@ interface IState { // Controlled form component wrapping Field for inputting a room alias scoped to a given domain export default class RoomAliasField extends React.PureComponent { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private fieldRef = createRef(); diff --git a/src/components/views/emojipicker/ReactionPicker.tsx b/src/components/views/emojipicker/ReactionPicker.tsx index b62df99e25..bd16634490 100644 --- a/src/components/views/emojipicker/ReactionPicker.tsx +++ b/src/components/views/emojipicker/ReactionPicker.tsx @@ -29,7 +29,7 @@ interface IState { class ReactionPicker extends React.Component { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public constructor(props: IProps, context: React.ContextType) { super(props, context); diff --git a/src/components/views/emojipicker/Search.tsx b/src/components/views/emojipicker/Search.tsx index 87397b6d4b..bce045cb8c 100644 --- a/src/components/views/emojipicker/Search.tsx +++ b/src/components/views/emojipicker/Search.tsx @@ -23,7 +23,7 @@ interface IProps { class Search extends React.PureComponent { public static contextType = RovingTabIndexContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private inputRef = React.createRef(); diff --git a/src/components/views/location/LocationPicker.tsx b/src/components/views/location/LocationPicker.tsx index c45521830d..e812f1c6bd 100644 --- a/src/components/views/location/LocationPicker.tsx +++ b/src/components/views/location/LocationPicker.tsx @@ -42,7 +42,7 @@ const isSharingOwnLocation = (shareType: LocationShareType): boolean => class LocationPicker extends React.Component { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private map?: maplibregl.Map; private geolocate?: maplibregl.GeolocateControl; private marker?: maplibregl.Marker; diff --git a/src/components/views/messages/EditHistoryMessage.tsx b/src/components/views/messages/EditHistoryMessage.tsx index 8316d0835b..fb6f04c08f 100644 --- a/src/components/views/messages/EditHistoryMessage.tsx +++ b/src/components/views/messages/EditHistoryMessage.tsx @@ -45,7 +45,7 @@ interface IState { export default class EditHistoryMessage extends React.PureComponent { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private content = createRef(); private pills = new ReactRootManager(); diff --git a/src/components/views/messages/MAudioBody.tsx b/src/components/views/messages/MAudioBody.tsx index 326b1c38c8..bf0cc9ee54 100644 --- a/src/components/views/messages/MAudioBody.tsx +++ b/src/components/views/messages/MAudioBody.tsx @@ -30,7 +30,7 @@ interface IState { export default class MAudioBody extends React.PureComponent { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public state: IState = {}; diff --git a/src/components/views/messages/MFileBody.tsx b/src/components/views/messages/MFileBody.tsx index fde3ea0184..1235b73b4b 100644 --- a/src/components/views/messages/MFileBody.tsx +++ b/src/components/views/messages/MFileBody.tsx @@ -102,7 +102,7 @@ interface IState { export default class MFileBody extends React.Component { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public state: IState = {}; diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index 7d45e71a4b..f77451108b 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -57,7 +57,7 @@ interface IState { export default class MImageBody extends React.Component { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private unmounted = false; private image = createRef(); diff --git a/src/components/views/messages/MLocationBody.tsx b/src/components/views/messages/MLocationBody.tsx index b226476fa8..7735e64b03 100644 --- a/src/components/views/messages/MLocationBody.tsx +++ b/src/components/views/messages/MLocationBody.tsx @@ -30,7 +30,7 @@ interface IState { export default class MLocationBody extends React.Component { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private unmounted = false; private mapId: string; diff --git a/src/components/views/messages/MPollBody.tsx b/src/components/views/messages/MPollBody.tsx index 9e173e5f4a..ba3962779f 100644 --- a/src/components/views/messages/MPollBody.tsx +++ b/src/components/views/messages/MPollBody.tsx @@ -139,7 +139,7 @@ export function launchPollEditor(mxEvent: MatrixEvent, getRelationsForEvent?: Ge export default class MPollBody extends React.Component { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private seenEventIds: string[] = []; // Events we have already seen public constructor(props: IBodyProps, context: React.ContextType) { diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx index 4036b9ddec..822d2c3f59 100644 --- a/src/components/views/messages/MVideoBody.tsx +++ b/src/components/views/messages/MVideoBody.tsx @@ -34,7 +34,7 @@ interface IState { export default class MVideoBody extends React.PureComponent { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private videoRef = React.createRef(); private sizeWatcher?: string; diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx index fdd0200429..bf92c993c5 100644 --- a/src/components/views/messages/MessageActionBar.tsx +++ b/src/components/views/messages/MessageActionBar.tsx @@ -262,7 +262,7 @@ interface IMessageActionBarProps { export default class MessageActionBar extends React.PureComponent { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public componentDidMount(): void { if (this.props.mxEvent.status && this.props.mxEvent.status !== EventStatus.SENT) { diff --git a/src/components/views/messages/MessageEvent.tsx b/src/components/views/messages/MessageEvent.tsx index 60fcce6493..dfb3fbb009 100644 --- a/src/components/views/messages/MessageEvent.tsx +++ b/src/components/views/messages/MessageEvent.tsx @@ -85,7 +85,7 @@ export default class MessageEvent extends React.Component implements IMe private evTypes = new Map>(baseEvTypes.entries()); public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public constructor(props: IProps, context: React.ContextType) { super(props, context); diff --git a/src/components/views/messages/ReactionsRow.tsx b/src/components/views/messages/ReactionsRow.tsx index eba9499606..605e6a7dfe 100644 --- a/src/components/views/messages/ReactionsRow.tsx +++ b/src/components/views/messages/ReactionsRow.tsx @@ -75,7 +75,7 @@ interface IState { export default class ReactionsRow extends React.PureComponent { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public constructor(props: IProps, context: React.ContextType) { super(props, context); diff --git a/src/components/views/messages/ReactionsRowButton.tsx b/src/components/views/messages/ReactionsRowButton.tsx index 4a1d8d67fe..709edeffd8 100644 --- a/src/components/views/messages/ReactionsRowButton.tsx +++ b/src/components/views/messages/ReactionsRowButton.tsx @@ -38,7 +38,7 @@ export interface IProps { export default class ReactionsRowButton extends React.PureComponent { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public onClick = (): void => { const { mxEvent, myReactionEvent, content } = this.props; diff --git a/src/components/views/messages/ReactionsRowButtonTooltip.tsx b/src/components/views/messages/ReactionsRowButtonTooltip.tsx index 9790356762..5f407e2e20 100644 --- a/src/components/views/messages/ReactionsRowButtonTooltip.tsx +++ b/src/components/views/messages/ReactionsRowButtonTooltip.tsx @@ -28,7 +28,7 @@ interface IProps { export default class ReactionsRowButtonTooltip extends React.PureComponent> { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public render(): React.ReactNode { const { content, reactionEvents, mxEvent, children } = this.props; diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 0c05236176..1a87d43a45 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -55,7 +55,7 @@ export default class TextualBody extends React.Component { private ref = createRef(); public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public state = { links: [], diff --git a/src/components/views/messages/TextualEvent.tsx b/src/components/views/messages/TextualEvent.tsx index 1c1ba26d08..1c54963f76 100644 --- a/src/components/views/messages/TextualEvent.tsx +++ b/src/components/views/messages/TextualEvent.tsx @@ -19,7 +19,7 @@ interface IProps { export default class TextualEvent extends React.Component { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public render(): React.ReactNode { const text = TextForEvent.textForEvent( diff --git a/src/components/views/right_panel/TimelineCard.tsx b/src/components/views/right_panel/TimelineCard.tsx index e0988eeaa5..e745f7a227 100644 --- a/src/components/views/right_panel/TimelineCard.tsx +++ b/src/components/views/right_panel/TimelineCard.tsx @@ -68,7 +68,7 @@ interface IState { export default class TimelineCard extends React.Component { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private dispatcherRef?: string; private layoutWatcherRef?: string; diff --git a/src/components/views/room_settings/AliasSettings.tsx b/src/components/views/room_settings/AliasSettings.tsx index 3c1a745530..0bb29b7f89 100644 --- a/src/components/views/room_settings/AliasSettings.tsx +++ b/src/components/views/room_settings/AliasSettings.tsx @@ -94,7 +94,7 @@ interface IState { export default class AliasSettings extends React.Component { public static contextType = MatrixClientContext; - public declare context: ContextType; + declare public context: ContextType; public static defaultProps = { canSetAliases: false, diff --git a/src/components/views/rooms/Autocomplete.tsx b/src/components/views/rooms/Autocomplete.tsx index 423b5c6272..3ffd6648ea 100644 --- a/src/components/views/rooms/Autocomplete.tsx +++ b/src/components/views/rooms/Autocomplete.tsx @@ -49,7 +49,7 @@ export default class Autocomplete extends React.PureComponent { private completionRefs: Record> = {}; public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public constructor(props: IProps, context: React.ContextType) { super(props, context); diff --git a/src/components/views/rooms/EditMessageComposer.tsx b/src/components/views/rooms/EditMessageComposer.tsx index d62a451b8b..c6321ad53a 100644 --- a/src/components/views/rooms/EditMessageComposer.tsx +++ b/src/components/views/rooms/EditMessageComposer.tsx @@ -121,7 +121,7 @@ interface IState { class EditMessageComposer extends React.Component { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private readonly editorRef = createRef(); private dispatcherRef?: string; diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 78d0ca5b28..d66d90e0e2 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -296,7 +296,7 @@ export class UnwrappedEventTile extends React.Component }; public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private unmounted = false; diff --git a/src/components/views/rooms/MemberList.tsx b/src/components/views/rooms/MemberList.tsx index e503ce2363..5587b56bf8 100644 --- a/src/components/views/rooms/MemberList.tsx +++ b/src/components/views/rooms/MemberList.tsx @@ -75,7 +75,7 @@ export default class MemberList extends React.Component { private unmounted = false; public static contextType = SDKContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private tiles: Map = new Map(); public constructor(props: IProps, context: React.ContextType) { diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 27189000d1..9dcd107fed 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -123,7 +123,7 @@ export class MessageComposer extends React.Component { private _voiceRecording: Optional; public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public static defaultProps = { compact: false, diff --git a/src/components/views/rooms/MessageComposerButtons.tsx b/src/components/views/rooms/MessageComposerButtons.tsx index 003c2afed9..65e53148f4 100644 --- a/src/components/views/rooms/MessageComposerButtons.tsx +++ b/src/components/views/rooms/MessageComposerButtons.tsx @@ -290,7 +290,7 @@ interface IPollButtonProps { class PollButton extends React.PureComponent { public static contextType = OverflowMenuContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private onCreateClick = (): void => { this.context?.(); // close overflow menu diff --git a/src/components/views/rooms/ReplyPreview.tsx b/src/components/views/rooms/ReplyPreview.tsx index c820154b2b..7851f7914d 100644 --- a/src/components/views/rooms/ReplyPreview.tsx +++ b/src/components/views/rooms/ReplyPreview.tsx @@ -31,7 +31,7 @@ interface IProps { export default class ReplyPreview extends React.Component { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public render(): JSX.Element | null { if (!this.props.replyToEvent) return null; diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index 853bebc4fe..f3bde66af9 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -424,7 +424,7 @@ export default class RoomList extends React.PureComponent { private treeRef = createRef(); public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public constructor(props: IProps, context: React.ContextType) { super(props, context); diff --git a/src/components/views/rooms/RoomUpgradeWarningBar.tsx b/src/components/views/rooms/RoomUpgradeWarningBar.tsx index 66519fa766..e92be96cb2 100644 --- a/src/components/views/rooms/RoomUpgradeWarningBar.tsx +++ b/src/components/views/rooms/RoomUpgradeWarningBar.tsx @@ -25,7 +25,7 @@ interface IState { export default class RoomUpgradeWarningBar extends React.PureComponent { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public constructor(props: IProps, context: React.ContextType) { super(props, context); diff --git a/src/components/views/rooms/SearchResultTile.tsx b/src/components/views/rooms/SearchResultTile.tsx index 5ebbaffdd9..94f5e6da9d 100644 --- a/src/components/views/rooms/SearchResultTile.tsx +++ b/src/components/views/rooms/SearchResultTile.tsx @@ -36,7 +36,7 @@ interface IProps { export default class SearchResultTile extends React.Component { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; // A map of private callEventGroupers = new Map(); diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 252957c2c7..6533415a83 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -241,7 +241,7 @@ interface ISendMessageComposerProps extends MatrixClientProps { export class SendMessageComposer extends React.Component { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private readonly prepareToEncrypt?: DebouncedFunc<() => void>; private readonly editorRef = createRef(); diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index a6d4a2fc27..a8335a9902 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -53,7 +53,7 @@ interface IState { */ export default class VoiceRecordComposerTile extends React.PureComponent { public static contextType = RoomContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private voiceRecordingId: string; public constructor(props: IProps, context: React.ContextType) { diff --git a/src/components/views/settings/CryptographyPanel.tsx b/src/components/views/settings/CryptographyPanel.tsx index b418c0b05d..fbd696f243 100644 --- a/src/components/views/settings/CryptographyPanel.tsx +++ b/src/components/views/settings/CryptographyPanel.tsx @@ -32,7 +32,7 @@ interface IState { export default class CryptographyPanel extends React.Component { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public constructor(props: IProps, context: React.ContextType) { super(props); diff --git a/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx b/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx index d29d82853a..0da257607e 100644 --- a/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx @@ -28,7 +28,7 @@ interface IProps { export default class BridgeSettingsTab extends React.Component { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private renderBridgeCard(event: MatrixEvent, room: Room | null): ReactNode { const content = event.getContent(); diff --git a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.tsx index 048fe5df9d..31c361de1b 100644 --- a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.tsx @@ -33,7 +33,7 @@ interface IState { export default class GeneralRoomSettingsTab extends React.Component { public static contextType = MatrixClientContext; - public declare context: ContextType; + declare public context: ContextType; public constructor(props: IProps, context: ContextType) { super(props, context); diff --git a/src/components/views/settings/tabs/room/NotificationSettingsTab.tsx b/src/components/views/settings/tabs/room/NotificationSettingsTab.tsx index f668b1ff07..9aabf1edb0 100644 --- a/src/components/views/settings/tabs/room/NotificationSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/NotificationSettingsTab.tsx @@ -42,7 +42,7 @@ export default class NotificationsSettingsTab extends React.Component(); public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public constructor(props: IProps, context: React.ContextType) { super(props, context); diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx index 8261bfd3eb..5fada0b6bc 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx @@ -81,7 +81,7 @@ interface IBannedUserProps { export class BannedUser extends React.Component { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; private onUnbanClick = (): void => { this.context.unban(this.props.member.roomId, this.props.member.userId).catch((err) => { @@ -134,7 +134,7 @@ interface RolesRoomSettingsTabState { export default class RolesRoomSettingsTab extends React.Component { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public constructor(props: IProps) { super(props); diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx index 7d9f75f828..ece6a7deaf 100644 --- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx @@ -60,7 +60,7 @@ interface IState { export default class SecurityRoomSettingsTab extends React.Component { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public constructor(props: IProps, context: React.ContextType) { super(props, context); diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx index 7866131a01..f19343be20 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx @@ -32,7 +32,7 @@ interface IState { export default class HelpUserSettingsTab extends React.Component { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public constructor(props: IProps, context: React.ContextType) { super(props, context); diff --git a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx index 36d336faa3..9711159a10 100644 --- a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx @@ -51,7 +51,7 @@ const mapDeviceKindToHandlerValue = (deviceKind: MediaDeviceKindEnum): string | export default class VoiceUserSettingsTab extends React.Component<{}, IState> { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public constructor(props: {}, context: React.ContextType) { super(props, context); diff --git a/src/stores/room-list/previews/PollStartEventPreview.ts b/src/stores/room-list/previews/PollStartEventPreview.ts index bb005f4a94..7548cf12f7 100644 --- a/src/stores/room-list/previews/PollStartEventPreview.ts +++ b/src/stores/room-list/previews/PollStartEventPreview.ts @@ -18,7 +18,7 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext"; export class PollStartEventPreview implements IPreview { public static contextType = MatrixClientContext; - public declare context: React.ContextType; + declare public context: React.ContextType; public getTextFor(event: MatrixEvent, tagId?: TagID, isThread?: boolean): string | null { let eventContent = event.getContent(); diff --git a/test/unit-tests/TestSdkContext.ts b/test/unit-tests/TestSdkContext.ts index d4bda4f889..0d7e806514 100644 --- a/test/unit-tests/TestSdkContext.ts +++ b/test/unit-tests/TestSdkContext.ts @@ -27,18 +27,18 @@ import { * replace individual stores. This is useful for tests which need to mock out stores. */ export class TestSdkContext extends SdkContextClass { - public declare _RightPanelStore?: RightPanelStore; - public declare _RoomNotificationStateStore?: RoomNotificationStateStore; - public declare _RoomViewStore?: RoomViewStore; - public declare _WidgetPermissionStore?: WidgetPermissionStore; - public declare _WidgetLayoutStore?: WidgetLayoutStore; - public declare _WidgetStore?: WidgetStore; - public declare _PosthogAnalytics?: PosthogAnalytics; - public declare _SlidingSyncManager?: SlidingSyncManager; - public declare _SpaceStore?: SpaceStoreClass; - public declare _VoiceBroadcastRecordingsStore?: VoiceBroadcastRecordingsStore; - public declare _VoiceBroadcastPreRecordingStore?: VoiceBroadcastPreRecordingStore; - public declare _VoiceBroadcastPlaybacksStore?: VoiceBroadcastPlaybacksStore; + declare public _RightPanelStore?: RightPanelStore; + declare public _RoomNotificationStateStore?: RoomNotificationStateStore; + declare public _RoomViewStore?: RoomViewStore; + declare public _WidgetPermissionStore?: WidgetPermissionStore; + declare public _WidgetLayoutStore?: WidgetLayoutStore; + declare public _WidgetStore?: WidgetStore; + declare public _PosthogAnalytics?: PosthogAnalytics; + declare public _SlidingSyncManager?: SlidingSyncManager; + declare public _SpaceStore?: SpaceStoreClass; + declare public _VoiceBroadcastRecordingsStore?: VoiceBroadcastRecordingsStore; + declare public _VoiceBroadcastPreRecordingStore?: VoiceBroadcastPreRecordingStore; + declare public _VoiceBroadcastPlaybacksStore?: VoiceBroadcastPlaybacksStore; constructor() { super(); diff --git a/yarn.lock b/yarn.lock index b21301342c..c396878bb3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -34,15 +34,7 @@ dependencies: axe-core "~4.10.2" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.25.7.tgz#438f2c524071531d643c6f0188e1e28f130cebc7" - integrity sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g== - dependencies: - "@babel/highlight" "^7.25.7" - picocolors "^1.0.0" - -"@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0": version "7.26.2" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== @@ -51,6 +43,14 @@ js-tokens "^4.0.0" picocolors "^1.0.0" +"@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.25.7.tgz#438f2c524071531d643c6f0188e1e28f130cebc7" + integrity sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g== + dependencies: + "@babel/highlight" "^7.25.7" + picocolors "^1.0.0" + "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.25.9", "@babel/compat-data@^7.26.0": version "7.26.2" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.2.tgz#278b6b13664557de95b8f35b90d96785850bb56e" @@ -254,12 +254,12 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== -"@babel/helper-validator-identifier@^7.24.5", "@babel/helper-validator-identifier@^7.24.7": +"@babel/helper-validator-identifier@^7.24.5": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz#77b7f60c40b15c97df735b38a66ba1d7c3e93da5" integrity sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg== -"@babel/helper-validator-identifier@^7.25.7", "@babel/helper-validator-identifier@^7.25.9": +"@babel/helper-validator-identifier@^7.24.7", "@babel/helper-validator-identifier@^7.25.7", "@babel/helper-validator-identifier@^7.25.9": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== @@ -287,11 +287,11 @@ "@babel/types" "^7.26.0" "@babel/highlight@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.25.7.tgz#20383b5f442aa606e7b5e3043b0b1aafe9f37de5" - integrity sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw== + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.25.9.tgz#8141ce68fc73757946f983b343f1231f4691acc6" + integrity sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw== dependencies: - "@babel/helper-validator-identifier" "^7.25.7" + "@babel/helper-validator-identifier" "^7.25.9" chalk "^2.4.2" js-tokens "^4.0.0" picocolors "^1.0.0" @@ -2409,10 +2409,10 @@ "@sentry/core" "8.40.0" "@sentry/types" "8.40.0" -"@sentry/babel-plugin-component-annotate@2.22.5": - version "2.22.5" - resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.22.5.tgz#494978f4dfd741006368f74fefee2d1858a9c3af" - integrity sha512-+93qwB9vTX1nj4hD8AMWowXZsZVkvmP9OwTqSh5d4kOeiJ+dZftUk4+FKeKkAX9lvY2reyHV8Gms5mo67c27RQ== +"@sentry/babel-plugin-component-annotate@2.22.6": + version "2.22.6" + resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.22.6.tgz#829d6caf2c95c1c46108336de4e1049e6521435e" + integrity sha512-V2g1Y1I5eSe7dtUVMBvAJr8BaLRr4CLrgNgtPaZyMT4Rnps82SrZ5zqmEkLXPumlXhLUWR6qzoMNN2u+RXVXfQ== "@sentry/browser@^8.0.0": version "8.40.0" @@ -2426,13 +2426,13 @@ "@sentry/core" "8.40.0" "@sentry/types" "8.40.0" -"@sentry/bundler-plugin-core@2.22.5": - version "2.22.5" - resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-2.22.5.tgz#ca92e8145921fc5d4498bfd18168cad2c3376659" - integrity sha512-nfvTthV0aNM9/MwgnCi1WjAlCtau1I4kw6+oZIDOwJRDqGNziz517mYRXSsvCUebtGxDZtPcF7hSEBMSHjpncA== +"@sentry/bundler-plugin-core@2.22.6": + version "2.22.6" + resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-2.22.6.tgz#a1ea1fd43700a3ece9e7db016997e79a2782b87d" + integrity sha512-1esQdgSUCww9XAntO4pr7uAM5cfGhLsgTK9MEwAKNfvpMYJi9NUTYa3A7AZmdA8V6107Lo4OD7peIPrDRbaDCg== dependencies: "@babel/core" "^7.18.5" - "@sentry/babel-plugin-component-annotate" "2.22.5" + "@sentry/babel-plugin-component-annotate" "2.22.6" "@sentry/cli" "^2.36.1" dotenv "^16.3.1" find-up "^5.0.0" @@ -2440,45 +2440,45 @@ magic-string "0.30.8" unplugin "1.0.1" -"@sentry/cli-darwin@2.37.0": - version "2.37.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.37.0.tgz#9c890c68abf30ceaad27826212a0963b125b8bbf" - integrity sha512-CsusyMvO0eCPSN7H+sKHXS1pf637PWbS4rZak/7giz/z31/6qiXmeMlcL3f9lLZKtFPJmXVFO9uprn1wbBVF8A== +"@sentry/cli-darwin@2.39.1": + version "2.39.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.39.1.tgz#75c338a53834b4cf72f57599f4c72ffb36cf0781" + integrity sha512-kiNGNSAkg46LNGatfNH5tfsmI/kCAaPA62KQuFZloZiemTNzhy9/6NJP8HZ/GxGs8GDMxic6wNrV9CkVEgFLJQ== -"@sentry/cli-linux-arm64@2.37.0": - version "2.37.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.37.0.tgz#2070155bade6d72d6b706807c6f365c65f9b82ea" - integrity sha512-2vzUWHLZ3Ct5gpcIlfd/2Qsha+y9M8LXvbZE26VxzYrIkRoLAWcnClBv8m4XsHLMURYvz3J9QSZHMZHSO7kAzw== +"@sentry/cli-linux-arm64@2.39.1": + version "2.39.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.39.1.tgz#27db44700c33fcb1e8966257020b43f8494373e6" + integrity sha512-5VbVJDatolDrWOgaffsEM7znjs0cR8bHt9Bq0mStM3tBolgAeSDHE89NgHggfZR+DJ2VWOy4vgCwkObrUD6NQw== -"@sentry/cli-linux-arm@2.37.0": - version "2.37.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.37.0.tgz#a08c2133e8e2566074fd6fe4f68e9ffd0c85664a" - integrity sha512-Dz0qH4Yt+gGUgoVsqVt72oDj4VQynRF1QB1/Sr8g76Vbi+WxWZmUh0iFwivYVwWxdQGu/OQrE0tx946HToCRyA== +"@sentry/cli-linux-arm@2.39.1": + version "2.39.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.39.1.tgz#451683fa9a5a60b1359d104ec71334ed16f4b63c" + integrity sha512-DkENbxyRxUrfLnJLXTA4s5UL/GoctU5Cm4ER1eB7XN7p9WsamFJd/yf2KpltkjEyiTuplv0yAbdjl1KX3vKmEQ== -"@sentry/cli-linux-i686@2.37.0": - version "2.37.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.37.0.tgz#53fff0e7f232b656b0ee3413b66006ee724a4abf" - integrity sha512-MHRLGs4t/CQE1pG+mZBQixyWL6xDZfNalCjO8GMcTTbZFm44S3XRHfYJZNVCgdtnUP7b6OHGcu1v3SWE10LcwQ== +"@sentry/cli-linux-i686@2.39.1": + version "2.39.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.39.1.tgz#9965a81f97a94e8b6d1d15589e43fee158e35201" + integrity sha512-pXWVoKXCRrY7N8vc9H7mETiV9ZCz+zSnX65JQCzZxgYrayQPJTc+NPRnZTdYdk5RlAupXaFicBI2GwOCRqVRkg== -"@sentry/cli-linux-x64@2.37.0": - version "2.37.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.37.0.tgz#2fbaf51ef3884bd6561c987f01ac98f544457150" - integrity sha512-k76ClefKZaDNJZU/H3mGeR8uAzAGPzDRG/A7grzKfBeyhP3JW09L7Nz9IQcSjCK+xr399qLhM2HFCaPWQ6dlMw== +"@sentry/cli-linux-x64@2.39.1": + version "2.39.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.39.1.tgz#31fe008b02f92769543dc9919e2a5cbc4cda7889" + integrity sha512-IwayNZy+it7FWG4M9LayyUmG1a/8kT9+/IEm67sT5+7dkMIMcpmHDqL8rWcPojOXuTKaOBBjkVdNMBTXy0mXlA== -"@sentry/cli-win32-i686@2.37.0": - version "2.37.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.37.0.tgz#fa195664da27ce8c40fdb6db1bf1d125cdf587d9" - integrity sha512-FFyi5RNYQQkEg4GkP2f3BJcgQn0F4fjFDMiWkjCkftNPXQG+HFUEtrGsWr6mnHPdFouwbYg3tEPUWNxAoypvTw== +"@sentry/cli-win32-i686@2.39.1": + version "2.39.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.39.1.tgz#609e8790c49414011445e397130560c777850b35" + integrity sha512-NglnNoqHSmE+Dz/wHeIVRnV2bLMx7tIn3IQ8vXGO5HWA2f8zYJGktbkLq1Lg23PaQmeZLPGlja3gBQfZYSG10Q== -"@sentry/cli-win32-x64@2.37.0": - version "2.37.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.37.0.tgz#84fa4d070b8a4a115c46ab38f42d29580143fd26" - integrity sha512-nSMj4OcfQmyL+Tu/jWCJwhKCXFsCZW1MUk6wjjQlRt9SDLfgeapaMlK1ZvT1eZv5ZH6bj3qJfefwj4U8160uOA== +"@sentry/cli-win32-x64@2.39.1": + version "2.39.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.39.1.tgz#1a874a5570c6d162b35d9d001c96e5389d07d2cb" + integrity sha512-xv0R2CMf/X1Fte3cMWie1NXuHmUyQPDBfCyIt6k6RPFPxAYUgcqgMPznYwVMwWEA1W43PaOkSn3d8ZylsDaETw== "@sentry/cli@^2.36.1": - version "2.37.0" - resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.37.0.tgz#dd01e933cf1caed7d7b6abab5a96044fe1c9c7a1" - integrity sha512-fM3V4gZRJR/s8lafc3O07hhOYRnvkySdPkvL/0e0XW0r+xRwqIAgQ5ECbsZO16A5weUiXVSf03ztDL1FcmbJCQ== + version "2.39.1" + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.39.1.tgz#916bb5b7567ccf7fdf94ef6cf8a2b9ab78370d29" + integrity sha512-JIb3e9vh0+OmQ0KxmexMXg9oZsR/G7HMwxt5BUIKAXZ9m17Xll4ETXTRnRUBT3sf7EpNGAmlQk1xEmVN9pYZYQ== dependencies: https-proxy-agent "^5.0.0" node-fetch "^2.6.7" @@ -2486,13 +2486,13 @@ proxy-from-env "^1.1.0" which "^2.0.2" optionalDependencies: - "@sentry/cli-darwin" "2.37.0" - "@sentry/cli-linux-arm" "2.37.0" - "@sentry/cli-linux-arm64" "2.37.0" - "@sentry/cli-linux-i686" "2.37.0" - "@sentry/cli-linux-x64" "2.37.0" - "@sentry/cli-win32-i686" "2.37.0" - "@sentry/cli-win32-x64" "2.37.0" + "@sentry/cli-darwin" "2.39.1" + "@sentry/cli-linux-arm" "2.39.1" + "@sentry/cli-linux-arm64" "2.39.1" + "@sentry/cli-linux-i686" "2.39.1" + "@sentry/cli-linux-x64" "2.39.1" + "@sentry/cli-win32-i686" "2.39.1" + "@sentry/cli-win32-x64" "2.39.1" "@sentry/core@8.40.0": version "8.40.0" @@ -2507,11 +2507,11 @@ integrity sha512-nuCf3U3deolPM9BjNnwCc33UtFl9ec15/r74ngAkNccn+A2JXdIAsDkGJMO/9mgSFykLe1QyeJ0pQFRisCGOiA== "@sentry/webpack-plugin@^2.7.1": - version "2.22.5" - resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-2.22.5.tgz#aafb3e526954b72759bae38bb34edecf55e68142" - integrity sha512-D8irs8H0IuLZbCS0Te5zsYGu9sABmMJTfCCkRkf7fV8S0BQZQmxnQGf9cVxcTj07RWAgnhhUtsRQzkK7MLuIwg== + version "2.22.6" + resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-2.22.6.tgz#8c9d27d5cd89153a5b6e08cc9dcb3048b122ffbc" + integrity sha512-BiLhAzQYAz/9kCXKj2LeUKWf/9GBVn2dD0DeYK89s+sjDEaxjbcLBBiLlLrzT7eC9QVj2tUZRKOi6puCfc8ysw== dependencies: - "@sentry/bundler-plugin-core" "2.22.5" + "@sentry/bundler-plugin-core" "2.22.6" unplugin "1.0.1" uuid "^9.0.0" @@ -2837,7 +2837,23 @@ resolved "https://registry.yarnpkg.com/@types/escape-html/-/escape-html-1.0.4.tgz#dc7c166b76c7b03b27e32f80edf01d91eb5d9af2" integrity sha512-qZ72SFTgUAZ5a7Tj6kf2SHLetiH5S6f8G5frB2SPQ3EyF02kxdyBFf4Tz4banE3xCgGnKgWLt//a6VuYHKYJTg== -"@types/estree@^1.0.5": +"@types/eslint-scope@^3.7.7": + version "3.7.7" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" + integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*": + version "9.6.1" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-9.6.1.tgz#d5795ad732ce81715f27f75da913004a56751584" + integrity sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*", "@types/estree@^1.0.6": version "1.0.6" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== @@ -2990,7 +3006,7 @@ "@types/tough-cookie" "*" parse5 "^7.0.0" -"@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": +"@types/json-schema@*", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== @@ -3067,9 +3083,9 @@ "@types/node" "*" "@types/node@*": - version "22.10.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.0.tgz#89bfc9e82496b9c7edea3382583fa94f75896e81" - integrity sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA== + version "22.10.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.1.tgz#41ffeee127b8975a05f8c4f83fb89bcb2987d766" + integrity sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ== dependencies: undici-types "~6.20.0" @@ -3304,14 +3320,6 @@ "@typescript-eslint/types" "8.16.0" "@typescript-eslint/visitor-keys" "8.16.0" -"@typescript-eslint/scope-manager@8.9.0": - version "8.9.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.9.0.tgz#c98fef0c4a82a484e6a1eb610a55b154d14d46f3" - integrity sha512-bZu9bUud9ym1cabmOYH9S6TnbWRzpklVmwqICeOulTCZ9ue2/pczWzQvt/cGj2r2o1RdKoZbuEMalJJSYw3pHQ== - dependencies: - "@typescript-eslint/types" "8.9.0" - "@typescript-eslint/visitor-keys" "8.9.0" - "@typescript-eslint/type-utils@8.16.0": version "8.16.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.16.0.tgz#585388735f7ac390f07c885845c3d185d1b64740" @@ -3327,11 +3335,6 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.16.0.tgz#49c92ae1b57942458ab83d9ec7ccab3005e64737" integrity sha512-NzrHj6thBAOSE4d9bsuRNMvk+BvaQvmY4dDglgkgGC0EW/tB3Kelnp3tAKH87GEwzoxgeQn9fNGRyFJM/xd+GQ== -"@typescript-eslint/types@8.9.0": - version "8.9.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.9.0.tgz#b733af07fb340b32e962c6c63b1062aec2dc0fe6" - integrity sha512-SjgkvdYyt1FAPhU9c6FiYCXrldwYYlIQLkuc+LfAhCna6ggp96ACncdtlbn8FmnG72tUkXclrDExOpEYf1nfJQ== - "@typescript-eslint/typescript-estree@8.16.0": version "8.16.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.16.0.tgz#9d741e56e5b13469b5190e763432ce5551a9300c" @@ -3346,21 +3349,7 @@ semver "^7.6.0" ts-api-utils "^1.3.0" -"@typescript-eslint/typescript-estree@8.9.0": - version "8.9.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.9.0.tgz#1714f167e9063062dc0df49c1d25afcbc7a96199" - integrity sha512-9iJYTgKLDG6+iqegehc5+EqE6sqaee7kb8vWpmHZ86EqwDjmlqNNHeqDVqb9duh+BY6WCNHfIGvuVU3Tf9Db0g== - dependencies: - "@typescript-eslint/types" "8.9.0" - "@typescript-eslint/visitor-keys" "8.9.0" - debug "^4.3.4" - fast-glob "^3.3.2" - is-glob "^4.0.3" - minimatch "^9.0.4" - semver "^7.6.0" - ts-api-utils "^1.3.0" - -"@typescript-eslint/utils@8.16.0", "@typescript-eslint/utils@^8.13.0": +"@typescript-eslint/utils@8.16.0", "@typescript-eslint/utils@^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/utils@^8.13.0": version "8.16.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.16.0.tgz#c71264c437157feaa97842809836254a6fc833c3" integrity sha512-C1zRy/mOL8Pj157GiX4kaw7iyRLKfJXBR3L82hk5kS/GyHcOFmy4YUq/zfZti72I9wnuQtA/+xzft4wCC8PJdA== @@ -3370,16 +3359,6 @@ "@typescript-eslint/types" "8.16.0" "@typescript-eslint/typescript-estree" "8.16.0" -"@typescript-eslint/utils@^6.0.0 || ^7.0.0 || ^8.0.0": - version "8.9.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.9.0.tgz#748bbe3ea5bee526d9786d9405cf1b0df081c299" - integrity sha512-PKgMmaSo/Yg/F7kIZvrgrWa1+Vwn036CdNUvYFEkYbPwOH4i8xvkaRlu148W3vtheWK9ckKRIz7PBP5oUlkrvQ== - dependencies: - "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "8.9.0" - "@typescript-eslint/types" "8.9.0" - "@typescript-eslint/typescript-estree" "8.9.0" - "@typescript-eslint/visitor-keys@8.16.0": version "8.16.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.16.0.tgz#d5086afc060b01ff7a4ecab8d49d13d5a7b07705" @@ -3388,14 +3367,6 @@ "@typescript-eslint/types" "8.16.0" eslint-visitor-keys "^4.2.0" -"@typescript-eslint/visitor-keys@8.9.0": - version "8.9.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.9.0.tgz#5f11f4d9db913f37da42776893ffe0dd1ae78f78" - integrity sha512-Ht4y38ubk4L5/U8xKUBfKNYGmvKvA1CANoxiTRMM+tOLk3lbF3DvzZCxJCRSE+2GdCMSh6zq9VZJc3asc1XuAA== - dependencies: - "@typescript-eslint/types" "8.9.0" - eslint-visitor-keys "^3.4.3" - "@ungap/structured-clone@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" @@ -3429,125 +3400,125 @@ dependencies: eslint-plugin-unicorn "^54.0.0" -"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1": - version "1.12.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb" - integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg== +"@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.12.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.14.1.tgz#a9f6a07f2b03c95c8d38c4536a1fdfb521ff55b6" + integrity sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ== dependencies: - "@webassemblyjs/helper-numbers" "1.11.6" - "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/helper-numbers" "1.13.2" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" -"@webassemblyjs/floating-point-hex-parser@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" - integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== +"@webassemblyjs/floating-point-hex-parser@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz#fcca1eeddb1cc4e7b6eed4fc7956d6813b21b9fb" + integrity sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA== -"@webassemblyjs/helper-api-error@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" - integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== +"@webassemblyjs/helper-api-error@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz#e0a16152248bc38daee76dd7e21f15c5ef3ab1e7" + integrity sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ== -"@webassemblyjs/helper-buffer@1.12.1": - version "1.12.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz#6df20d272ea5439bf20ab3492b7fb70e9bfcb3f6" - integrity sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw== +"@webassemblyjs/helper-buffer@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz#822a9bc603166531f7d5df84e67b5bf99b72b96b" + integrity sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA== -"@webassemblyjs/helper-numbers@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" - integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g== +"@webassemblyjs/helper-numbers@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz#dbd932548e7119f4b8a7877fd5a8d20e63490b2d" + integrity sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA== dependencies: - "@webassemblyjs/floating-point-hex-parser" "1.11.6" - "@webassemblyjs/helper-api-error" "1.11.6" + "@webassemblyjs/floating-point-hex-parser" "1.13.2" + "@webassemblyjs/helper-api-error" "1.13.2" "@xtuc/long" "4.2.2" -"@webassemblyjs/helper-wasm-bytecode@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" - integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== +"@webassemblyjs/helper-wasm-bytecode@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz#e556108758f448aae84c850e593ce18a0eb31e0b" + integrity sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA== -"@webassemblyjs/helper-wasm-section@1.12.1": - version "1.12.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz#3da623233ae1a60409b509a52ade9bc22a37f7bf" - integrity sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g== +"@webassemblyjs/helper-wasm-section@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz#9629dda9c4430eab54b591053d6dc6f3ba050348" + integrity sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw== dependencies: - "@webassemblyjs/ast" "1.12.1" - "@webassemblyjs/helper-buffer" "1.12.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.6" - "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/wasm-gen" "1.14.1" -"@webassemblyjs/ieee754@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" - integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg== +"@webassemblyjs/ieee754@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz#1c5eaace1d606ada2c7fd7045ea9356c59ee0dba" + integrity sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw== dependencies: "@xtuc/ieee754" "^1.2.0" -"@webassemblyjs/leb128@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" - integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ== +"@webassemblyjs/leb128@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.13.2.tgz#57c5c3deb0105d02ce25fa3fd74f4ebc9fd0bbb0" + integrity sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw== dependencies: "@xtuc/long" "4.2.2" -"@webassemblyjs/utf8@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" - integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== +"@webassemblyjs/utf8@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.13.2.tgz#917a20e93f71ad5602966c2d685ae0c6c21f60f1" + integrity sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ== "@webassemblyjs/wasm-edit@^1.12.1": - version "1.12.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b" - integrity sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g== + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz#ac6689f502219b59198ddec42dcd496b1004d597" + integrity sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ== dependencies: - "@webassemblyjs/ast" "1.12.1" - "@webassemblyjs/helper-buffer" "1.12.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.6" - "@webassemblyjs/helper-wasm-section" "1.12.1" - "@webassemblyjs/wasm-gen" "1.12.1" - "@webassemblyjs/wasm-opt" "1.12.1" - "@webassemblyjs/wasm-parser" "1.12.1" - "@webassemblyjs/wast-printer" "1.12.1" + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/helper-wasm-section" "1.14.1" + "@webassemblyjs/wasm-gen" "1.14.1" + "@webassemblyjs/wasm-opt" "1.14.1" + "@webassemblyjs/wasm-parser" "1.14.1" + "@webassemblyjs/wast-printer" "1.14.1" -"@webassemblyjs/wasm-gen@1.12.1": - version "1.12.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz#a6520601da1b5700448273666a71ad0a45d78547" - integrity sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w== +"@webassemblyjs/wasm-gen@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz#991e7f0c090cb0bb62bbac882076e3d219da9570" + integrity sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg== dependencies: - "@webassemblyjs/ast" "1.12.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.6" - "@webassemblyjs/ieee754" "1.11.6" - "@webassemblyjs/leb128" "1.11.6" - "@webassemblyjs/utf8" "1.11.6" + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/ieee754" "1.13.2" + "@webassemblyjs/leb128" "1.13.2" + "@webassemblyjs/utf8" "1.13.2" -"@webassemblyjs/wasm-opt@1.12.1": - version "1.12.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz#9e6e81475dfcfb62dab574ac2dda38226c232bc5" - integrity sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg== +"@webassemblyjs/wasm-opt@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz#e6f71ed7ccae46781c206017d3c14c50efa8106b" + integrity sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw== dependencies: - "@webassemblyjs/ast" "1.12.1" - "@webassemblyjs/helper-buffer" "1.12.1" - "@webassemblyjs/wasm-gen" "1.12.1" - "@webassemblyjs/wasm-parser" "1.12.1" + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/wasm-gen" "1.14.1" + "@webassemblyjs/wasm-parser" "1.14.1" -"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1": - version "1.12.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937" - integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ== +"@webassemblyjs/wasm-parser@1.14.1", "@webassemblyjs/wasm-parser@^1.12.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz#b3e13f1893605ca78b52c68e54cf6a865f90b9fb" + integrity sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ== dependencies: - "@webassemblyjs/ast" "1.12.1" - "@webassemblyjs/helper-api-error" "1.11.6" - "@webassemblyjs/helper-wasm-bytecode" "1.11.6" - "@webassemblyjs/ieee754" "1.11.6" - "@webassemblyjs/leb128" "1.11.6" - "@webassemblyjs/utf8" "1.11.6" + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-api-error" "1.13.2" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/ieee754" "1.13.2" + "@webassemblyjs/leb128" "1.13.2" + "@webassemblyjs/utf8" "1.13.2" -"@webassemblyjs/wast-printer@1.12.1": - version "1.12.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz#bcecf661d7d1abdaf989d8341a4833e33e2b31ac" - integrity sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA== +"@webassemblyjs/wast-printer@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz#3bb3e9638a8ae5fdaf9610e7a06b4d9f9aa6fe07" + integrity sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw== dependencies: - "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/ast" "1.14.1" "@xtuc/long" "4.2.2" "@webpack-cli/configtest@^2.1.1": @@ -3613,11 +3584,6 @@ acorn-globals@^7.0.0: acorn "^8.1.0" acorn-walk "^8.0.2" -acorn-import-attributes@^1.9.5: - version "1.9.5" - resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" - integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== - acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -3630,12 +3596,12 @@ acorn-walk@^8.0.0, acorn-walk@^8.0.2, acorn-walk@^8.1.1: dependencies: acorn "^8.11.0" -acorn@^8.0.4, acorn@^8.1.0, acorn@^8.11.0, acorn@^8.12.0, acorn@^8.4.1, acorn@^8.7.1, acorn@^8.8.1, acorn@^8.8.2, acorn@^8.9.0: +acorn@^8.0.4, acorn@^8.1.0, acorn@^8.11.0, acorn@^8.12.0, acorn@^8.4.1, acorn@^8.9.0: version "8.13.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.13.0.tgz#2a30d670818ad16ddd6a35d3842dacec9e5d7ca3" integrity sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w== -acorn@^8.14.0: +acorn@^8.14.0, acorn@^8.8.1, acorn@^8.8.2: version "8.14.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0" integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== @@ -3791,19 +3757,12 @@ aria-query@5.3.0: dependencies: dequal "^2.0.3" -aria-query@^5.0.0: +aria-query@^5.0.0, aria-query@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59" integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw== -aria-query@~5.1.3: - version "5.1.3" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e" - integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ== - dependencies: - deep-equal "^2.0.5" - -array-buffer-byte-length@^1.0.0, array-buffer-byte-length@^1.0.1: +array-buffer-byte-length@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== @@ -3955,12 +3914,7 @@ await-lock@^2.1.0: resolved "https://registry.yarnpkg.com/await-lock/-/await-lock-2.2.2.tgz#a95a9b269bfd2f69d22b17a321686f551152bcef" integrity sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw== -axe-core@^4.10.0: - version "4.10.1" - resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.1.tgz#7d2589b0183f05b0f23e55c2f4cdf97b5bdc66d9" - integrity sha512-qPC9o+kD8Tir0lzNGLeghbOrWMr3ZJpaRlCIb6Uobt/7N4FiEDvqUMnxzCHRHmg8vOg14kr5gVNyScRmbMaJ9g== - -axe-core@~4.10.2: +axe-core@^4.10.0, axe-core@~4.10.2: version "4.10.2" resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.2.tgz#85228e3e1d8b8532a27659b332e39b7fa0e022df" integrity sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w== @@ -4116,16 +4070,14 @@ blob-polyfill@^9.0.0: integrity sha512-DPUO/EjNANCgSVg0geTy1vmUpu5hhp9tV2F7xUSTUd1jwe4XpwupGB+lt5PhVUqpqAk+zK1etqp6Pl/HVf71Ug== bloom-filters@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/bloom-filters/-/bloom-filters-3.0.3.tgz#bef683f04806ccf5f9d32f9693233bec505c1a96" - integrity sha512-whXz8q3iFb0Wc8TsVmkkN03htP1Bzgpue0u6GIv+OZvkvhtkxXf71J3821YxFBb3QzoubltzTRy6cwKxNM2sNA== + version "3.0.4" + resolved "https://registry.yarnpkg.com/bloom-filters/-/bloom-filters-3.0.4.tgz#2712bd8f8092fd0a081fe52a17c63be79dbbfc1e" + integrity sha512-BdnPWo2OpYhlvuP2fRzJBdioMCkm7Zp0HCf8NJgF5Mbyqy7VQ/CnTiVWMMyq4EZCBHwj0Kq6098gW2/3RsZsrA== dependencies: "@types/seedrandom" "^3.0.8" base64-arraybuffer "^1.0.2" is-buffer "^2.0.5" - lodash "^4.17.15" - lodash.eq "^4.0.0" - lodash.indexof "^4.0.5" + lodash "^4.17.21" long "^5.2.0" reflect-metadata "^0.1.13" seedrandom "^3.0.5" @@ -4189,7 +4141,7 @@ braces@^3.0.3, braces@~3.0.2: dependencies: fill-range "^7.1.1" -browserslist@^4.0.0, browserslist@^4.21.10, browserslist@^4.23.1, browserslist@^4.23.2, browserslist@^4.23.3, browserslist@^4.24.0, browserslist@^4.24.2: +browserslist@^4.0.0, browserslist@^4.23.1, browserslist@^4.23.2, browserslist@^4.23.3, browserslist@^4.24.0, browserslist@^4.24.2: version "4.24.2" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.2.tgz#f5845bc91069dbd55ee89faf9822e1d885d16580" integrity sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg== @@ -4375,9 +4327,9 @@ ci-info@^3.2.0: integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== ci-info@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.0.0.tgz#65466f8b280fc019b9f50a5388115d17a63a44f2" - integrity sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg== + version "4.1.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.1.0.tgz#92319d2fa29d2620180ea5afed31f589bc98cf83" + integrity sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A== cjs-module-lexer@^1.0.0: version "1.4.1" @@ -4586,9 +4538,9 @@ concat-map@0.0.1: integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== concurrently@^9.0.0: - version "9.0.1" - resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-9.0.1.tgz#01e171bf6c7af0c022eb85daef95bff04d8185aa" - integrity sha512-wYKvCd/f54sTXJMSfV6Ln/B8UrfLBKOYa+lzc6CHay3Qek+LorVSBdMVfyewFhRbH0Rbabsk4D+3PL/VjQ5gzg== + version "9.1.0" + resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-9.1.0.tgz#8da6d609f4321752912dab9be8710232ac496aa0" + integrity sha512-VxkzwMAn4LP7WyMnJNbHN5mKV9L2IbyDjpzemKr99sXNR3GqRNMMHdm7prV1ws9wg7ETj6WUkNOigZVsptwbgg== dependencies: chalk "^4.1.2" lodash "^4.17.21" @@ -4657,9 +4609,9 @@ core-js-compat@^3.38.0, core-js-compat@^3.38.1: browserslist "^4.24.2" core-js@^3.0.0, core-js@^3.38.1: - version "3.38.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.38.1.tgz#aa375b79a286a670388a1a363363d53677c0383e" - integrity sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw== + version "3.39.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.39.0.tgz#57f7647f4d2d030c32a72ea23a0555b2eaa30f83" + integrity sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g== core-util-is@~1.0.0: version "1.0.3" @@ -4732,9 +4684,9 @@ create-require@^1.1.0: integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== cronstrue@^2.41.0: - version "2.50.0" - resolved "https://registry.yarnpkg.com/cronstrue/-/cronstrue-2.50.0.tgz#eabba0f915f186765258b707b7a3950c663b5573" - integrity sha512-ULYhWIonJzlScCCQrPUG5uMXzXxSixty4djud9SS37DoNxDdkeRocxzHuAo4ImRBUK+mAuU5X9TSwEDccnnuPg== + version "2.52.0" + resolved "https://registry.yarnpkg.com/cronstrue/-/cronstrue-2.52.0.tgz#00af1a8dcf76a1dece149e4416db823105b28cdb" + integrity sha512-NKgHbWkSZXJUcaBHSsyzC8eegD6bBd4O0oCI6XMIJ+y4Bq3v4w7sY3wfWoKPuVlq9pQHRB6od0lmKpIqi8TlKA== cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.5" @@ -4839,15 +4791,7 @@ css-tree@^2.3.1: mdn-data "2.0.30" source-map-js "^1.0.1" -css-tree@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-3.0.0.tgz#079c7b87e465a28cedbc826502f9a227213db0f3" - integrity sha512-o88DVQ6GzsABn1+6+zo2ct801dBO5OASVyxbbvA2W20ue2puSh/VOuqUj90eUeMSX/xqGqBmOKiRQN7tJOuBXw== - dependencies: - mdn-data "2.10.0" - source-map-js "^1.0.1" - -css-tree@^3.0.1: +css-tree@^3.0.0, css-tree@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-3.0.1.tgz#bea6deaea60bb5bcf416adfb1ecf607a8d9471f6" integrity sha512-8Fxxv+tGhORlshCdCwnNJytvlvq46sOLSYEx2ZIGurahWvMucSRnyjPA3AmrMq4VPRYbHVpWj5VkiVasrM2H4Q== @@ -5058,30 +5002,6 @@ dedent@^1.0.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.3.tgz#99aee19eb9bae55a67327717b6e848d0bf777e5a" integrity sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ== -deep-equal@^2.0.5: - version "2.2.3" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.3.tgz#af89dafb23a396c7da3e862abc0be27cf51d56e1" - integrity sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA== - dependencies: - array-buffer-byte-length "^1.0.0" - call-bind "^1.0.5" - es-get-iterator "^1.1.3" - get-intrinsic "^1.2.2" - is-arguments "^1.1.1" - is-array-buffer "^3.0.2" - is-date-object "^1.0.5" - is-regex "^1.1.4" - is-shared-array-buffer "^1.0.2" - isarray "^2.0.5" - object-is "^1.1.5" - object-keys "^1.1.1" - object.assign "^4.1.4" - regexp.prototype.flags "^1.5.1" - side-channel "^1.0.4" - which-boxed-primitive "^1.0.2" - which-collection "^1.0.1" - which-typed-array "^1.1.13" - deep-is@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" @@ -5365,9 +5285,9 @@ ejs@^3.1.8: jake "^10.8.5" electron-to-chromium@^1.5.41: - version "1.5.56" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.56.tgz#3213f369efc3a41091c3b2c05bc0f406108ac1df" - integrity sha512-7lXb9dAvimCFdvUMTyucD4mnIndt/xhRKFAlky0CyFogdnNmdPQNoHI23msF/2V4mpTxMzgMdjK4+YRlFlRQZw== + version "1.5.67" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.67.tgz#66ebd2be4a77469ac2760ef5e9e460ba9a43a845" + integrity sha512-nz88NNBsD7kQSAGGJyp8hS6xSPtWwqNogA0mjtc2nUYeEf3nURK9qpV18TuBdDmEDgVWotS8Wkzf+V52dSQ/LQ== emittery@^0.13.1: version "0.13.1" @@ -5464,10 +5384,10 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2, es-abstract@^1.23.3: - version "1.23.3" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" - integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== +es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.2, es-abstract@^1.23.3, es-abstract@^1.23.5: + version "1.23.5" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.5.tgz#f4599a4946d57ed467515ed10e4f157289cd52fb" + integrity sha512-vlmniQ0WNPwXqA0BnmwV3Ng7HxiGlh6r5U6JcTMNx8OilcAGqVJBHJcPjqOMaczU9fRuRK5Px2BdVyPRnKMMVQ== dependencies: array-buffer-byte-length "^1.0.1" arraybuffer.prototype.slice "^1.0.3" @@ -5484,7 +5404,7 @@ es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23 function.prototype.name "^1.1.6" get-intrinsic "^1.2.4" get-symbol-description "^1.0.2" - globalthis "^1.0.3" + globalthis "^1.0.4" gopd "^1.0.1" has-property-descriptors "^1.0.2" has-proto "^1.0.3" @@ -5500,10 +5420,10 @@ es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23 is-string "^1.0.7" is-typed-array "^1.1.13" is-weakref "^1.0.2" - object-inspect "^1.13.1" + object-inspect "^1.13.3" object-keys "^1.1.1" object.assign "^4.1.5" - regexp.prototype.flags "^1.5.2" + regexp.prototype.flags "^1.5.3" safe-array-concat "^1.1.2" safe-regex-test "^1.0.3" string.prototype.trim "^1.2.9" @@ -5528,25 +5448,10 @@ es-errors@^1.2.1, es-errors@^1.3.0: resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== -es-get-iterator@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6" - integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.3" - has-symbols "^1.0.3" - is-arguments "^1.1.1" - is-map "^2.0.2" - is-set "^2.0.2" - is-string "^1.0.7" - isarray "^2.0.5" - stop-iteration-iterator "^1.0.0" - -es-iterator-helpers@^1.0.19: - version "1.1.0" - resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.1.0.tgz#f6d745d342aea214fe09497e7152170dc333a7a6" - integrity sha512-/SurEfycdyssORP/E+bj4sEu1CWw4EmLDsHynHwSXQ7utgbrMRWW195pTrCjFgFCddf/UkYm3oqKPRq5i8bJbw== +es-iterator-helpers@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.2.0.tgz#2f1a3ab998b30cb2d10b195b587c6d9ebdebf152" + integrity sha512-tpxqxncxnpw3c93u8n3VOzACmRFoVmWJqbWXvX/JfKbkhBw1oslgPrUfeSt2psuqyEJFD6N/9lg5i7bsKpoq+Q== dependencies: call-bind "^1.0.7" define-properties "^1.2.1" @@ -5556,6 +5461,7 @@ es-iterator-helpers@^1.0.19: function-bind "^1.1.2" get-intrinsic "^1.2.4" globalthis "^1.0.4" + gopd "^1.0.1" has-property-descriptors "^1.0.2" has-proto "^1.0.3" has-symbols "^1.0.3" @@ -5592,13 +5498,13 @@ es-shim-unscopables@^1.0.0, es-shim-unscopables@^1.0.2: hasown "^2.0.0" es-to-primitive@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.3.0.tgz#96c89c82cc49fd8794a24835ba3e1ff87f214e18" + integrity sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g== dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" + is-callable "^1.2.7" + is-date-object "^1.0.5" + is-symbol "^1.0.4" escalade@^3.1.1, escalade@^3.2.0: version "3.2.0" @@ -5693,18 +5599,18 @@ eslint-plugin-import@^2.25.4: tsconfig-paths "^3.15.0" eslint-plugin-jest@^28.0.0: - version "28.8.3" - resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-28.8.3.tgz#c5699bba0ad06090ad613535e4f1572f4c2567c0" - integrity sha512-HIQ3t9hASLKm2IhIOqnu+ifw7uLZkIlR7RYNv7fMcEi/p0CIiJmfriStQS2LDkgtY4nyLbIZAD+JL347Yc2ETQ== + version "28.9.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-28.9.0.tgz#19168dfaed124339cd2252c4c4d1ac3688aeb243" + integrity sha512-rLu1s1Wf96TgUUxSw6loVIkNtUjq1Re7A9QdCCHSohnvXEBAjuL420h0T/fMmkQlNsQP2GhQzEUpYHPfxBkvYQ== dependencies: "@typescript-eslint/utils" "^6.0.0 || ^7.0.0 || ^8.0.0" eslint-plugin-jsx-a11y@^6.5.1: - version "6.10.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.0.tgz#36fb9dead91cafd085ddbe3829602fb10ef28339" - integrity sha512-ySOHvXX8eSN6zz8Bywacm7CvGNhUtdjvqfQDVe6020TUK34Cywkw7m0KsCCk1Qtm9G1FayfTN1/7mMYnYO2Bhg== + version "6.10.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz#d2812bb23bf1ab4665f1718ea442e8372e638483" + integrity sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q== dependencies: - aria-query "~5.1.3" + aria-query "^5.3.2" array-includes "^3.1.8" array.prototype.flatmap "^1.3.2" ast-types-flow "^0.0.8" @@ -5712,14 +5618,13 @@ eslint-plugin-jsx-a11y@^6.5.1: axobject-query "^4.1.0" damerau-levenshtein "^1.0.8" emoji-regex "^9.2.2" - es-iterator-helpers "^1.0.19" hasown "^2.0.2" jsx-ast-utils "^3.3.5" language-tags "^1.0.9" minimatch "^3.1.2" object.fromentries "^2.0.8" safe-regex-test "^1.0.3" - string.prototype.includes "^2.0.0" + string.prototype.includes "^2.0.1" eslint-plugin-matrix-org@^2.0.2: version "2.0.2" @@ -5732,16 +5637,16 @@ eslint-plugin-react-hooks@^5.0.0: integrity sha512-hIOwI+5hYGpJEc4uPRmz2ulCjAGD/N13Lukkh8cLV0i2IRk/bdZDYjgLVHj+U9Z704kLIdIO6iueGvxNur0sgw== eslint-plugin-react@^7.28.0: - version "7.37.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.1.tgz#56493d7d69174d0d828bc83afeffe96903fdadbd" - integrity sha512-xwTnwDqzbDRA8uJ7BMxPs/EXRB3i8ZfnOIp8BsxEQkT0nHPp+WWceqGgo6rKb9ctNi8GJLDT4Go5HAWELa/WMg== + version "7.37.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.2.tgz#cd0935987876ba2900df2f58339f6d92305acc7a" + integrity sha512-EsTAnj9fLVr/GZleBLFbj/sSuXeWmp1eXIN60ceYnZveqEaUCyW4X+Vh4WTdUhCkW4xutXYqTXCUSyqD4rB75w== dependencies: array-includes "^3.1.8" array.prototype.findlast "^1.2.5" array.prototype.flatmap "^1.3.2" array.prototype.tosorted "^1.1.4" doctrine "^2.1.0" - es-iterator-helpers "^1.0.19" + es-iterator-helpers "^1.1.0" estraverse "^5.3.0" hasown "^2.0.2" jsx-ast-utils "^2.4.1 || ^3.0.0" @@ -5778,9 +5683,9 @@ eslint-plugin-unicorn@^54.0.0: strip-indent "^3.0.0" eslint-plugin-unicorn@^56.0.0: - version "56.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-unicorn/-/eslint-plugin-unicorn-56.0.0.tgz#9fd3ebe6f478571734541fa745026b743175b59e" - integrity sha512-aXpddVz/PQMmd69uxO98PA4iidiVNvA0xOtbpUoz1WhBd4RxOQQYqN618v68drY0hmy5uU2jy1bheKEVWBjlPw== + version "56.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-unicorn/-/eslint-plugin-unicorn-56.0.1.tgz#d10a3df69ba885939075bdc95a65a0c872e940d4" + integrity sha512-FwVV0Uwf8XPfVnKSGpMg7NtlZh0G0gBarCaFcMUOoqPxXryxdYxTRRv4kH6B9TFCVIrjRXG+emcxIk2ayZilog== dependencies: "@babel/helper-validator-identifier" "^7.24.7" "@eslint-community/eslint-utils" "^4.4.0" @@ -6403,7 +6308,7 @@ get-east-asian-width@^1.0.0: resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz#21b4071ee58ed04ee0db653371b55b4299875389" integrity sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ== -get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: +get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== @@ -6444,9 +6349,9 @@ get-symbol-description@^1.0.2: get-intrinsic "^1.2.4" github-markdown-css@^5.5.1: - version "5.7.0" - resolved "https://registry.yarnpkg.com/github-markdown-css/-/github-markdown-css-5.7.0.tgz#40fa19a8b9826a22874f305c40a5260d4d57e90a" - integrity sha512-GoYhaqELL4YUjz4tZ00PQ4JzFQkMfrBVuEeRB8W74HoikHWNiaGqSgynpwJEc+xom5uf04qoD/tUSS6ziZltaQ== + version "5.8.1" + resolved "https://registry.yarnpkg.com/github-markdown-css/-/github-markdown-css-5.8.1.tgz#2a53cf17f0c9bde5ff9e83710a3310a02f5278a7" + integrity sha512-8G+PFvqigBQSWLQjyzgpa2ThD9bo7+kDsriUIidGcRhXgmcaAWUIpCZf8DavJgc+xifjbCG+GvMyWr0XMXmc7g== gl-matrix@^3.4.3: version "3.4.3" @@ -6549,11 +6454,11 @@ globals@^14.0.0: integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== globals@^15.9.0: - version "15.11.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-15.11.0.tgz#b96ed4c6998540c6fb824b24b5499216d2438d6e" - integrity sha512-yeyNSjdbyVaWurlwCpcA6XNBrHTMIeDdj0/hnvX/OLJ9ekOXYbLsLinH/MucQyGvNnXhidTdNhTtJaffL2sMfw== + version "15.13.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-15.13.0.tgz#bbec719d69aafef188ecd67954aae76a696010fc" + integrity sha512-49TewVEz0UxZjr1WYYsWpPrhyC/B/pA8Bq0fUmet2n+eR7yn0IvNzNaoBwnK6mdkzcN+se7Ez9zUgULTz2QH4g== -globalthis@^1.0.3, globalthis@^1.0.4: +globalthis@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== @@ -6590,12 +6495,12 @@ globjoin@^0.1.4: resolved "https://registry.yarnpkg.com/globjoin/-/globjoin-0.1.4.tgz#2f4494ac8919e3767c5cbb691e9f463324285d43" integrity sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg== -gopd@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" - integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== +gopd@^1.0.1, gopd@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.1.0.tgz#df8f0839c2d48caefc32a025a49294d39606c912" + integrity sha512-FQoVQnqcdk4hVM4JN1eromaun4iuS34oStkdlLENLdpULsuQcTyXj8w7ayhuUfPwEYZ1ZOooOTT6fdA9Vmx/RA== dependencies: - get-intrinsic "^1.1.3" + get-intrinsic "^1.2.4" graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.11" @@ -6642,9 +6547,11 @@ has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: es-define-property "^1.0.0" has-proto@^1.0.1, has-proto@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" - integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.1.0.tgz#deb10494cbbe8809bce168a3b961f42969f5ed43" + integrity sha512-QLdzI9IIO1Jg7f9GT1gXpPpXArAn6cS31R1eEZqz08Gc+uQ8/XiqHWt17Fiw+2p6oTTIq5GXEpQkAlA88YRl/Q== + dependencies: + call-bind "^1.0.7" has-symbols@^1.0.2, has-symbols@^1.0.3: version "1.0.3" @@ -6733,9 +6640,9 @@ html-tags@^3.3.1: integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ== html-webpack-plugin@^5.5.3: - version "5.6.0" - resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz#50a8fa6709245608cb00e811eacecb8e0d7b7ea0" - integrity sha512-iwaY4wzbe48AfKLZ/Cc8k0L+FKG6oSNRaZ8x5A/T/IVDGyXcbHncM9TdDa93wn0FsSm82FhTKW7f3vS61thXAw== + version "5.6.3" + resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.6.3.tgz#a31145f0fee4184d53a794f9513147df1e653685" + integrity sha512-QSf1yjtSAsmf7rYBV7XX86uua4W/vkhIt0xNXKbsi2foEeW7vjJQz4bhnpL3xH+l1ryl1680uNv968Z+X6jSYg== dependencies: "@types/html-minifier-terser" "^6.0.0" html-minifier-terser "^6.0.2" @@ -6842,9 +6749,9 @@ human-signals@^5.0.0: integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== husky@^9.0.0: - version "9.1.6" - resolved "https://registry.yarnpkg.com/husky/-/husky-9.1.6.tgz#e23aa996b6203ab33534bdc82306b0cf2cb07d6c" - integrity sha512-sqbjZKK7kf44hfdE94EoX8MZNk0n7HeW37O4YrVGCF4wzgQjp+akPAkfUK5LZ6KuR/6sqeAVuXHji+RzQgOn5A== + version "9.1.7" + resolved "https://registry.yarnpkg.com/husky/-/husky-9.1.7.tgz#d46a38035d101b46a70456a850ff4201344c0b2d" + integrity sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA== hyperdyperid@^1.2.0: version "1.2.0" @@ -6949,7 +6856,7 @@ ini@^4.1.3: resolved "https://registry.yarnpkg.com/ini/-/ini-4.1.3.tgz#4c359675a6071a46985eb39b14e4a2c0ec98a795" integrity sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg== -internal-slot@^1.0.4, internal-slot@^1.0.7: +internal-slot@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== @@ -6985,7 +6892,7 @@ ipaddr.js@^2.1.0: resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8" integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== -is-arguments@^1.0.4, is-arguments@^1.1.1: +is-arguments@^1.0.4: version "1.1.1" resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== @@ -6993,7 +6900,7 @@ is-arguments@^1.0.4, is-arguments@^1.1.1: call-bind "^1.0.2" has-tostringtag "^1.0.0" -is-array-buffer@^3.0.2, is-array-buffer@^3.0.4: +is-array-buffer@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== @@ -7028,12 +6935,12 @@ is-binary-path@~2.1.0: binary-extensions "^2.0.0" is-boolean-object@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" - integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.2.0.tgz#9743641e80a62c094b5941c5bb791d66a88e497a" + integrity sha512-kR5g0+dXf/+kXnqI+lu0URKYPKgICtHGGNCDSB10AaUFj3o/HkB3u7WfpRBJGFopxxY0oH3ux7ZsDjLtK7xqvw== dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" + call-bind "^1.0.7" + has-tostringtag "^1.0.2" is-buffer@^2.0.5: version "2.0.5" @@ -7047,7 +6954,7 @@ is-builtin-module@^3.2.1: dependencies: builtin-modules "^3.3.0" -is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: +is-callable@^1.1.3, is-callable@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== @@ -7066,7 +6973,7 @@ is-data-view@^1.0.1: dependencies: is-typed-array "^1.1.13" -is-date-object@^1.0.1, is-date-object@^1.0.5: +is-date-object@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== @@ -7083,12 +6990,12 @@ is-extglob@^2.1.1: resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== -is-finalizationregistry@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz#c8749b65f17c133313e661b1289b95ad3dbd62e6" - integrity sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw== +is-finalizationregistry@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.1.0.tgz#d74a7d0c5f3578e34a20729e69202e578d495dc2" + integrity sha512-qfMdqbAQEwBw78ZyReKnlA8ezmPdb9BemzIIip/JkjaZUhitfXDkkr+3QTboW0JrSXT1QWyYShpvnNHGZ4c4yA== dependencies: - call-bind "^1.0.2" + call-bind "^1.0.7" is-fullwidth-code-point@^3.0.0: version "3.0.0" @@ -7140,7 +7047,7 @@ is-ip@^3.1.0: dependencies: ip-regex "^4.0.0" -is-map@^2.0.2, is-map@^2.0.3: +is-map@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== @@ -7156,11 +7063,12 @@ is-network-error@^1.0.0: integrity sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g== is-number-object@^1.0.4: - version "1.0.7" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" - integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.1.0.tgz#5a867e9ecc3d294dda740d9f127835857af7eb05" + integrity sha512-KVSZV0Dunv9DTPkhXwcZ3Q+tUc9TsaE1ZwX5J2WMvsSGS6Md8TFPun5uwh0yRdrNerI6vf/tbJxqSx4c1ZI1Lw== dependencies: - has-tostringtag "^1.0.0" + call-bind "^1.0.7" + has-tostringtag "^1.0.2" is-number@^7.0.0: version "7.0.0" @@ -7195,14 +7103,16 @@ is-potential-custom-element-name@^1.0.1: integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== is-regex@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" - integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.0.tgz#41b9d266e7eb7451312c64efc37e8a7d453077cf" + integrity sha512-B6ohK4ZmoftlUe+uvenXSbPJFo6U37BH7oO1B3nQH8f/7h27N56s85MhUtbFJAziz5dcmuR3i8ovUl35zp8pFA== dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" + call-bind "^1.0.7" + gopd "^1.1.0" + has-tostringtag "^1.0.2" + hasown "^2.0.2" -is-set@^2.0.2, is-set@^2.0.3: +is-set@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== @@ -7225,18 +7135,19 @@ is-stream@^3.0.0: integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== is-string@^1.0.5, is-string@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" - integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.1.0.tgz#8cb83c5d57311bf8058bc6c8db294711641da45d" + integrity sha512-PlfzajuF9vSo5wErv3MJAKD/nqf9ngAs1NFQYm16nUYFO2IzxJ2hcm+IOCg+EEopdykNNUhVq5cz35cAUxU8+g== dependencies: - has-tostringtag "^1.0.0" + call-bind "^1.0.7" + has-tostringtag "^1.0.2" is-subset@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6" integrity sha512-6Ybun0IkarhmEqxXCNw/C0bna6Zb/TkfUX9UbwJtK6ObwAVCxmAP308WWTHviM/zAqXk05cdhYsUsZeGQh99iw== -is-symbol@^1.0.2, is-symbol@^1.0.3: +is-symbol@^1.0.3, is-symbol@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== @@ -7783,9 +7694,9 @@ jiti@^1.20.0: integrity sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w== jiti@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.4.0.tgz#393d595fb6031a11d11171b5e4fc0b989ba3e053" - integrity sha512-H5UpaUI+aHOqZXlYOaFP/8AzKsg+guWu+Pr3Y8i7+Y3zr1aXAvCvTAQ1RxSc6oVD8R8c7brgNtTVP91E7upH/g== + version "2.4.1" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.4.1.tgz#4de9766ccbfa941d9b6390d2b159a4b295a52e6b" + integrity sha512-yPBThwecp1wS9DmoA4x4KR2h3QoslacnDR8ypuFM962kI4/456Iy1oHx2RAgh4jfZNdn0bctsdadceiBUgpU1g== "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" @@ -7970,9 +7881,9 @@ kleur@^3.0.3: integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== knip@^5.36.2: - version "5.36.2" - resolved "https://registry.yarnpkg.com/knip/-/knip-5.36.2.tgz#346ce5eb464bdf34329cdcf3a6d41d0a41dce647" - integrity sha512-MudNTKBSqThAFAV29GuRPSKSebByZeQCFeNgXVRVSd+sXcubehTgQHTGqqiwlXGCt4WBP7vuVekp0ZehfZtHuw== + version "5.38.3" + resolved "https://registry.yarnpkg.com/knip/-/knip-5.38.3.tgz#a139e0c6215c9958d213ce1a9e3985cf5de2774e" + integrity sha512-pg3CMzWlZy4mnuwxieGoK74oOgzFPvsUR/aE8NSqx2oQr56soXTzmw8GsHR277pU52Fe0h4/pho2PMhVeEvj8g== dependencies: "@nodelib/fs.walk" "1.2.8" "@snyk/github-codeowners" "1.1.0" @@ -7985,7 +7896,7 @@ knip@^5.36.2: picocolors "^1.1.0" picomatch "^4.0.1" pretty-ms "^9.0.0" - smol-toml "^1.3.0" + smol-toml "^1.3.1" strip-json-comments "5.0.1" summary "2.1.0" zod "^3.22.4" @@ -8146,16 +8057,6 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== -lodash.eq@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/lodash.eq/-/lodash.eq-4.0.0.tgz#a39f06779e72f9c0d1f310c90cd292c1661d5035" - integrity sha512-vbrJpXL6kQNG6TkInxX12DZRfuYVllSxhwYqjYB78g2zF3UI15nFO/0AgmZnZRnaQ38sZtjCiVjGr2rnKt4v0g== - -lodash.indexof@^4.0.5: - version "4.0.5" - resolved "https://registry.yarnpkg.com/lodash.indexof/-/lodash.indexof-4.0.5.tgz#53714adc2cddd6ed87638f893aa9b6c24e31ef3c" - integrity sha512-t9wLWMQsawdVmf6/IcAgVGqAJkNzYVcn4BHYZKTPW//l7N5Oq7Bq138BaVk19agcsPZePcidSgTTw4NqS1nUAw== - lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" @@ -8186,7 +8087,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== -lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21: +lodash@^4.17.20, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -8388,11 +8289,6 @@ mdn-data@2.0.30: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc" integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA== -mdn-data@2.10.0: - version "2.10.0" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.10.0.tgz#701da407f8fbc7a42aa0ba0c149ec897daef8986" - integrity sha512-qq7C3EtK3yJXMwz1zAab65pjl+UhohqMOctTgcqjLOWABqmwj+me02LSsCuEUxnst9X1lCBpoE0WArGKgdGDzw== - mdn-data@2.12.1: version "2.12.1" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.12.1.tgz#10cb462215c13d95c92ff60d0fb3becac1bbb924" @@ -8737,18 +8633,10 @@ object-assign@^4.1.1: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -object-inspect@^1.13.1: - version "1.13.2" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" - integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== - -object-is@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07" - integrity sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" +object-inspect@^1.13.1, object-inspect@^1.13.3: + version "1.13.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.3.tgz#f14c183de51130243d6d18ae149375ff50ea488a" + integrity sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA== object-keys@^1.1.1: version "1.1.1" @@ -9819,10 +9707,10 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prettier@3.3.3: - version "3.3.3" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105" - integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew== +prettier@3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.4.1.tgz#e211d451d6452db0a291672ca9154bc8c2579f7b" + integrity sha512-G+YdqtITVZmOJje6QkXQWzl3fSfMxFwm1tjTyo9exhkmWSqC4Yhd1+lug++IlR2mvRVAxEDDWYkQdeSztajqgg== pretty-error@^4.0.0: version "4.0.0" @@ -9851,9 +9739,9 @@ pretty-format@^29.0.0, pretty-format@^29.7.0: react-is "^18.0.0" pretty-ms@^9.0.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-9.1.0.tgz#0ad44de6086454f48a168e5abb3c26f8db1b3253" - integrity sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw== + version "9.2.0" + resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-9.2.0.tgz#e14c0aad6493b69ed63114442a84133d7e560ef0" + integrity sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg== dependencies: parse-ms "^4.0.0" @@ -10219,18 +10107,18 @@ reflect-metadata@^0.1.13: resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.14.tgz#24cf721fe60677146bb77eeb0e1f9dece3d65859" integrity sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A== -reflect.getprototypeof@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz#3ab04c32a8390b770712b7a8633972702d278859" - integrity sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg== +reflect.getprototypeof@^1.0.4, reflect.getprototypeof@^1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.7.tgz#04311b33a1b713ca5eb7b5aed9950a86481858e5" + integrity sha512-bMvFGIUKlc/eSfXNX+aZ+EL95/EgZzuwA0OBPTbZZDEJw/0AkentjMuM1oiRfwHrshqk4RzdgiTg5CcDalXN5g== dependencies: call-bind "^1.0.7" define-properties "^1.2.1" - es-abstract "^1.23.1" + es-abstract "^1.23.5" es-errors "^1.3.0" get-intrinsic "^1.2.4" - globalthis "^1.0.3" - which-builtin-type "^1.1.3" + gopd "^1.0.1" + which-builtin-type "^1.1.4" regenerate-unicode-properties@^10.2.0: version "10.2.0" @@ -10261,7 +10149,7 @@ regexp-tree@^0.1.27: resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.27.tgz#2198f0ef54518ffa743fe74d983b56ffd631b6cd" integrity sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA== -regexp.prototype.flags@^1.5.1, regexp.prototype.flags@^1.5.2: +regexp.prototype.flags@^1.5.2, regexp.prototype.flags@^1.5.3: version "1.5.3" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz#b3ae40b1d2499b8350ab2c3fe6ef3845d3a96f42" integrity sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ== @@ -10692,9 +10580,9 @@ shebang-regex@^3.0.0: integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== shell-quote@^1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" - integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== + version "1.8.2" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.2.tgz#d2d83e057959d53ec261311e9e9b8f51dcb2934a" + integrity sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA== side-channel@^1.0.4, side-channel@^1.0.6: version "1.0.6" @@ -10765,10 +10653,10 @@ slice-ansi@^7.1.0: ansi-styles "^6.2.1" is-fullwidth-code-point "^5.0.0" -smol-toml@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/smol-toml/-/smol-toml-1.3.0.tgz#5200e251fffadbb72570c84e9776d2a3eca48143" - integrity sha512-tWpi2TsODPScmi48b/OQZGi2lgUmBCHy6SZrhi/FdnnHiU1GwebbCfuQuxsC3nHaLwtYeJGPrDZDIeodDOc4pA== +smol-toml@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/smol-toml/-/smol-toml-1.3.1.tgz#d9084a9e212142e3cab27ef4e2b8e8ba620bfe15" + integrity sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ== snake-case@^3.0.4: version "3.0.4" @@ -10897,13 +10785,6 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== -stop-iteration-iterator@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4" - integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ== - dependencies: - internal-slot "^1.0.4" - string-argv@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" @@ -10953,7 +10834,7 @@ string-width@^7.0.0: get-east-asian-width "^1.0.0" strip-ansi "^7.1.0" -string.prototype.includes@^2.0.0: +string.prototype.includes@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz#eceef21283640761a81dbe16d6c7171a4edf7d92" integrity sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg== @@ -11433,9 +11314,9 @@ truncate-utf8-bytes@^1.0.0: utf8-byte-length "^1.0.1" ts-api-utils@^1.3.0: - version "1.4.2" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.4.2.tgz#a6a6dff26117ac7965624fc118525971edc6a82a" - integrity sha512-ZF5gQIQa/UmzfvxbHZI3JXN0/Jt+vnAfAviNRAMc491laiK6YCLpCW9ft8oaCRFOTxCZtUTE6XB0ZQAe3olntw== + version "1.4.3" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.4.3.tgz#bfc2215fe6528fecab2b0fba570a2e8a4263b064" + integrity sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw== ts-morph@^13.0.1: version "13.0.3" @@ -11491,12 +11372,12 @@ tsconfig-paths@^3.15.0: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@2: +tslib@2, tslib@^2.0.3, tslib@^2.1.0: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== -tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.6.1, tslib@^2.6.2, tslib@^2.7.0: +tslib@^2.0.0, tslib@^2.4.0, tslib@^2.6.1, tslib@^2.6.2, tslib@^2.7.0: version "2.8.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.0.tgz#d124c86c3c05a40a91e6fdea4021bd31d377971b" integrity sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA== @@ -11562,9 +11443,9 @@ typed-array-byte-length@^1.0.1: is-typed-array "^1.1.13" typed-array-byte-offset@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz#f9ec1acb9259f395093e4567eb3c28a580d02063" - integrity sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA== + version "1.0.3" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.3.tgz#3fa9f22567700cc86aaf86a1e7176f74b59600f2" + integrity sha512-GsvTyUHTriq6o/bHcTd0vM7OQ9JEdlvluu9YISaA7+KzDzPaIzEeDFNkTfhdE3MYcNhNi0vq/LlegYgIs5yPAw== dependencies: available-typed-arrays "^1.0.7" call-bind "^1.0.7" @@ -11572,18 +11453,19 @@ typed-array-byte-offset@^1.0.2: gopd "^1.0.1" has-proto "^1.0.3" is-typed-array "^1.1.13" + reflect.getprototypeof "^1.0.6" typed-array-length@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3" - integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g== + version "1.0.7" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.7.tgz#ee4deff984b64be1e118b0de8c9c877d5ce73d3d" + integrity sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg== dependencies: call-bind "^1.0.7" for-each "^0.3.3" gopd "^1.0.1" - has-proto "^1.0.3" is-typed-array "^1.1.13" possible-typed-array-names "^1.0.0" + reflect.getprototypeof "^1.0.6" typescript@5.6.3: version "5.6.3" @@ -11758,9 +11640,9 @@ utils-merge@1.0.1: integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== uuid@11, uuid@^11.0.0: - version "11.0.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.0.2.tgz#a8d68ba7347d051e7ea716cc8dcbbab634d66875" - integrity sha512-14FfcOJmqdjbBPdDjFQyk/SdT4NySW4eM0zcG+HqbHP5jzuH56xO3J1DGhgs/cEMCfwYi3HQI1gnTO62iaG+tQ== + version "11.0.3" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.0.3.tgz#248451cac9d1a4a4128033e765d137e2b2c49a3d" + integrity sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg== uuid@8.3.2, uuid@^8.3.2: version "8.3.2" @@ -12006,17 +11888,17 @@ webpack-virtual-modules@^0.5.0: integrity sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw== webpack@^5.89.0: - version "5.95.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.95.0.tgz#8fd8c454fa60dad186fbe36c400a55848307b4c0" - integrity sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q== + version "5.96.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.96.1.tgz#3676d1626d8312b6b10d0c18cc049fba7ac01f0c" + integrity sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA== dependencies: - "@types/estree" "^1.0.5" + "@types/eslint-scope" "^3.7.7" + "@types/estree" "^1.0.6" "@webassemblyjs/ast" "^1.12.1" "@webassemblyjs/wasm-edit" "^1.12.1" "@webassemblyjs/wasm-parser" "^1.12.1" - acorn "^8.7.1" - acorn-import-attributes "^1.9.5" - browserslist "^4.21.10" + acorn "^8.14.0" + browserslist "^4.24.0" chrome-trace-event "^1.0.2" enhanced-resolve "^5.17.1" es-module-lexer "^1.2.1" @@ -12101,16 +11983,17 @@ which-boxed-primitive@^1.0.2: is-string "^1.0.5" is-symbol "^1.0.3" -which-builtin-type@^1.1.3: - version "1.1.4" - resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.1.4.tgz#592796260602fc3514a1b5ee7fa29319b72380c3" - integrity sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w== +which-builtin-type@^1.1.4: + version "1.2.0" + resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.2.0.tgz#58042ac9602d78a6d117c7e811349df1268ba63c" + integrity sha512-I+qLGQ/vucCby4tf5HsLmGueEla4ZhwTBSqaooS+Y0BuxN4Cp+okmGuV+8mXZ84KDI9BA+oklo+RzKg0ONdSUA== dependencies: + call-bind "^1.0.7" function.prototype.name "^1.1.6" has-tostringtag "^1.0.2" is-async-function "^2.0.0" is-date-object "^1.0.5" - is-finalizationregistry "^1.0.2" + is-finalizationregistry "^1.1.0" is-generator-function "^1.0.10" is-regex "^1.1.4" is-weakref "^1.0.2" @@ -12119,7 +12002,7 @@ which-builtin-type@^1.1.3: which-collection "^1.0.2" which-typed-array "^1.1.15" -which-collection@^1.0.1, which-collection@^1.0.2: +which-collection@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== @@ -12134,7 +12017,18 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== -which-typed-array@^1.1.13, which-typed-array@^1.1.14, which-typed-array@^1.1.15, which-typed-array@^1.1.2: +which-typed-array@^1.1.14, which-typed-array@^1.1.15: + version "1.1.16" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.16.tgz#db4db429c4706feca2f01677a144278e4a8c216b" + integrity sha512-g+N+GAWiRj66DngFwHvISJd+ITsyphZvD1vChfVg6cEdnzy53GzB3oy0fUNlvhz7H7+MiqhYr26qxQShCpKTTQ== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.2" + +which-typed-array@^1.1.2: version "1.1.15" resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== @@ -12268,9 +12162,9 @@ yaml@^1.10.0: integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== yaml@^2.3.3: - version "2.6.0" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.6.0.tgz#14059ad9d0b1680d0f04d3a60fe00f3a857303c3" - integrity sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ== + version "2.6.1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.6.1.tgz#42f2b1ba89203f374609572d5349fb8686500773" + integrity sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg== yaml@~2.5.0: version "2.5.1" From 8619a22f579b00d223284d6b28c4e6e8e7424887 Mon Sep 17 00:00:00 2001 From: ElementRobot Date: Mon, 2 Dec 2024 09:49:32 +0000 Subject: [PATCH 18/27] Localazy Download (#28608) * [create-pull-request] automated change * Discard changes to src/i18n/strings/en_EN.json --------- Co-authored-by: t3chguy Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- src/i18n/strings/de_DE.json | 323 +++++++++++++++++++++++++++++------- 1 file changed, 259 insertions(+), 64 deletions(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index abe4566f8c..65cb1d8660 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -2,6 +2,7 @@ "a11y": { "emoji_picker": "Emoji-Auswahl", "jump_first_invite": "Zur ersten Einladung springen.", + "message_composer": "Nachrichteneingabe-Feld", "n_unread_messages": { "other": "%(count)s ungelesene Nachrichten.", "one": "1 ungelesene Nachricht." @@ -10,11 +11,13 @@ "other": "%(count)s ungelesene Nachrichten einschließlich Erwähnungen.", "one": "1 ungelesene Erwähnung." }, + "recent_rooms": "Kürzlich besuchte Chatrooms", "room_name": "Raum %(name)s", + "room_status_bar": "Statusleiste des Chatrooms", + "seek_bar_label": "Audio-Suchleiste", "unread_messages": "Ungelesene Nachrichten.", "user_menu": "Benutzermenü" }, - "a11y_jump_first_unread_room": "Zum ersten ungelesenen Raum springen.", "action": { "accept": "Annehmen", "add": "Hinzufügen", @@ -106,6 +109,7 @@ "save": "Speichern", "search": "Suchen", "send_report": "Bericht senden", + "set_avatar": "Profilbild festlegen", "share": "Teilen", "show": "Zeigen", "show_advanced": "Erweiterte Einstellungen", @@ -123,12 +127,13 @@ "trust": "Vertrauen", "try_again": "Erneut versuchen", "unban": "Verbannung aufheben", - "unignore": "Nicht mehr blockieren", + "unignore": "Freigeben", "unpin": "Nicht mehr anheften", "unsubscribe": "Deabonnieren", "update": "Aktualisieren", "upgrade": "Aktualisieren", "upload": "Hochladen", + "upload_file": "Datei hochladen", "verify": "Verifizieren", "view": "Ansicht", "view_all": "Alles anzeigen", @@ -143,7 +148,7 @@ "accept_button": "Das ist okay", "bullet_1": "Wir erfassen und analysieren keine Kontodaten", "bullet_2": "Wir teilen keine Informationen mit Dritten", - "consent_migration": "Sie haben zuvor zugestimmt, anonymisierte Nutzungsdaten mit uns zu teilen. Wir aktualisieren, wie das funktioniert.", + "consent_migration": "Du hast zugestimmt, anonymisierte Nutzungsdaten mit uns zu teilen. Wir aktualisieren die Funktionsweise.", "disable_prompt": "Du kannst dies jederzeit in den Einstellungen deaktivieren", "enable_prompt": "Hilf mit, %(analyticsOwner)s zu verbessern", "learn_more": "Teile Daten anonymisiert um uns zu helfen Probleme zu identifizieren. Nichts persönliches. Keine Dritten. Mehr dazu hier", @@ -204,11 +209,11 @@ "failed_soft_logout_auth": "Erneute Authentifizierung fehlgeschlagen", "failed_soft_logout_homeserver": "Erneute Authentifizierung aufgrund eines Problems des Heim-Servers fehlgeschlagen", "forgot_password_email_invalid": "E-Mail-Adresse scheint ungültig zu sein.", - "forgot_password_email_required": "Es muss die mit dem Benutzerkonto verbundene E-Mail-Adresse eingegeben werden.", + "forgot_password_email_required": "Es muss die mit dem Konto verbundene E-Mail-Adresse eingegeben werden.", "forgot_password_prompt": "Passwort vergessen?", "forgot_password_send_email": "E-Mail senden", "identifier_label": "Anmelden mit", - "incorrect_credentials": "Inkorrekter Nutzername und/oder Passwort.", + "incorrect_credentials": "Benutzername und/oder Passwort falsch.", "incorrect_credentials_detail": "Bitte beachte, dass du dich gerade auf %(hs)s anmeldest, nicht matrix.org.", "incorrect_password": "Ungültiges Passwort", "log_in_new_account": "Mit deinem neuen Konto anmelden.", @@ -223,6 +228,7 @@ }, "misconfigured_body": "Wende dich an deinen %(brand)s-Admin um deine Konfiguration auf ungültige oder doppelte Einträge zu überprüfen.", "misconfigured_title": "Dein %(brand)s ist falsch konfiguriert", + "mobile_create_account_title": "Du bist dabei, auf %(hsName)s ein Konto anzulegen", "msisdn_field_description": "Andere Personen können dich mit deinen Kontaktdaten in Räume einladen", "msisdn_field_label": "Telefon", "msisdn_field_number_invalid": "Diese Telefonummer sieht nicht ganz richtig aus. Bitte überprüfe deine Eingabe und versuche es erneut", @@ -241,12 +247,36 @@ "phone_label": "Telefon", "phone_optional_label": "Telefon (optional)", "qr_code_login": { + "check_code_explainer": "Hierdurch wird überprüft, ob die Verbindung zu Ihrem anderen Gerät sicher ist.", + "check_code_heading": "Gib die Nummer ein, die am anderen Gerät angezeigt wird", + "check_code_input_label": "zweistelliger Code", + "check_code_mismatch": "Die Zahlen stimmen nicht überein", "completing_setup": "Schließe Anmeldung deines neuen Gerätes ab", + "error_etag_missing": "Ein unerwarteter Fehler ist aufgetreten. Dies kann an einer Browsererweiterung, einem Proxyserver oder einer fehlerhaften Serverkonfiguration liegen.", + "error_expired": "Die Anmeldung ist abgelaufen. Bitte versuchen Sie es erneut.", + "error_expired_title": "Die Anmeldung wurde nicht rechtzeitig abgeschlossen", + "error_insecure_channel_detected": "Eine sichere Verbindung zum neuen Gerät konnte nicht hergestellt werden. Ihre vorhandenen Geräte sind weiterhin sicher und Sie müssen sich keine Sorgen um sie machen.", + "error_insecure_channel_detected_instructions": "Was jetzt?", + "error_insecure_channel_detected_instructions_1": "Versuchen Sie erneut, sich mit einem QR-Code auf dem anderen Gerät anzumelden, falls dies ein Netzwerkproblem war", + "error_insecure_channel_detected_instructions_2": "Falls das gleiche Problem auftritt, probiere es mit einem anderen WLAN-Netzwerk oder verwende deine Mobilen Daten.", + "error_insecure_channel_detected_instructions_3": "Falls das nicht funktioniert melde dich manuell an.", + "error_insecure_channel_detected_title": "Verbindung ist nicht sicher", + "error_other_device_already_signed_in": "Sie brauchen nichts weiter zu tun.", + "error_other_device_already_signed_in_title": "Ihr anderes Gerät ist bereits angemeldet", "error_rate_limited": "Zu viele Versuche in zu kurzer Zeit. Warte ein wenig, bevor du es erneut versuchst.", - "error_unexpected": "Ein unerwarteter Fehler ist aufgetreten.", - "scan_code_instruction": "Lese den folgenden QR-Code mit deinem nicht angemeldeten Gerät ein.", - "scan_qr_code": "QR-Code einlesen", - "select_qr_code": "Wähle „%(scanQRCode)s“", + "error_unsupported_protocol": "Dieses Gerät unterstützt die Anmeldung auf einem anderen Gerät mit einem QR-Code nicht.", + "error_user_cancelled": "Der Anmeldevorgang wurde am anderen Gerät abgebrochen.", + "error_user_cancelled_title": "Anmeldungsanfrage abgebrochen", + "error_user_declined": "Du oder der Kontoanbieter haben die Anmeldeanfrage abgelehnt.", + "error_user_declined_title": "Anmeldung abgelehnt", + "point_the_camera": "Scanne den hier angezeigten QR-Code", + "scan_code_instruction": "Scanne den QR-Code mit einem weiteren Gerät.", + "scan_qr_code": "Anmeldung mit QR-Code", + "security_code": "Sicherheitscode", + "security_code_prompt": "Wenn Sie dazu aufgefordert werden, geben Sie den folgenden Code auf Ihrem anderen Gerät ein.", + "select_qr_code": "Wähle \"%(scanQRCode)s\"", + "unsupported_explainer": "Dein Kontoanbieter unterstützt keine Anmeldung bei einem neuen Gerät per QR-Code.", + "unsupported_heading": "QR-Code nicht unterstützt", "waiting_for_device": "Warte auf Anmeldung des Gerätes" }, "register_action": "Konto erstellen", @@ -258,8 +288,6 @@ "registration_disabled": "Registrierungen wurden auf diesem Heim-Server deaktiviert.", "registration_msisdn_field_required_invalid": "Telefonnummer eingeben (auf diesem Heim-Server erforderlich)", "registration_successful": "Registrierung erfolgreich", - "registration_username_in_use": "Jemand anderes nutzt diesen Benutzernamen schon. Probier einen anderen oder wenn du es bist, melde dich unten an.", - "registration_username_unable_check": "Es kann nicht überprüft werden, ob der Nutzername bereits vergeben ist. Bitte versuche es später erneut.", "registration_username_validation": "Verwende nur Kleinbuchstaben, Zahlen, Bindestriche und Unterstriche", "reset_password": { "confirm_new_password": "Neues Passwort bestätigen", @@ -335,6 +363,8 @@ "email_resend_prompt": "Nicht angekommen? Erneut senden", "email_resent": "Verschickt!", "fallback_button": "Authentifizierung beginnen", + "mas_cross_signing_reset_cta": "Gehe zu deinem Konto", + "mas_cross_signing_reset_description": "Lasse deine Identität durch deinen Kontoanbieter zurücksetzen. Kehre dann zurück und klicke auf \"Erneut versuchen\".", "msisdn": "Eine Textnachricht wurde an %(msisdn)s gesendet", "msisdn_token_incorrect": "Token fehlerhaft", "msisdn_token_prompt": "Bitte gib den darin enthaltenen Code ein:", @@ -355,7 +385,6 @@ "unsupported_auth_email": "Dieser Heim-Server unterstützt die Anmeldung per E-Mail-Adresse nicht.", "unsupported_auth_msisdn": "Dieser Server unterstützt keine Authentifizierung per Telefonnummer.", "username_field_required_invalid": "Benutzername eingeben", - "username_in_use": "Dieser Benutzername wird bereits genutzt, bitte versuche es mit einem anderen.", "verify_email_explainer": "Wir müssen wissen, dass du es auch wirklich bist, bevor wir dein Passwort zurücksetzen. Klicke auf den Link in der E-Mail, die wir gerade an %(email)s gesendet haben", "verify_email_heading": "Verifiziere deine E-Mail, um fortzufahren" }, @@ -365,7 +394,7 @@ "collecting_information": "App-Versionsinformationen werden abgerufen", "collecting_logs": "Protokolle werden abgerufen", "create_new_issue": "Bitte erstelle ein neues Issue auf GitHub damit wir diesen Fehler untersuchen können.", - "description": "Fehlerberichte enthalten Nutzungsdaten wie Nutzernamen von dir und anderen Personen, Raum-IDs deiner beigetretenen Räume sowie mit welchen Elementen der Oberfläche du kürzlich interagiert hast. Sie enthalten keine Nachrichten.", + "description": "Fehlerberichte enthalten Applikationsnutzungsdaten wie Ihren Benutzernamen and Ihre Pseudonyme oder jene Ihrer Chatpartner, die IDs und Namen von Chaträumen, in denen Sie Mitglied sind und Elementen der Benutzeroberfläche mit denen Sie kürzlich interagiert haben. Fehlerberichte enthalten keine Nachrichten.", "download_logs": "Protokolle herunterladen", "downloading_logs": "Lade Protokolle herunter", "error_empty": "Bitte teile uns mit, was schief lief - oder besser, beschreibe das Problem auf GitHub in einem \"Issue\".", @@ -425,6 +454,7 @@ "beta": "Beta", "camera": "Kamera", "cameras": "Kameras", + "cancel": "Abbrechen", "capabilities": "Funktionen", "copied": "Kopiert!", "credits": "Danksagungen", @@ -460,11 +490,13 @@ "legal": "Rechtliches", "light": "Hell", "loading": "Lade …", + "lobby": "Lobby", "location": "Standort", "low_priority": "Niedrige Priorität", "matrix": "Matrix", "message": "Nachricht", "message_layout": "Nachrichtenlayout", + "message_timestamp_invalid": "Ungültiger Zeitstempel", "microphone": "Mikrofon", "model": "Modell", "modern": "Modern", @@ -488,7 +520,7 @@ "orphan_rooms": "Andere Räume", "password": "Passwort", "people": "Personen", - "preferences": "Einstellungen", + "preferences": "Präferenzen", "presence": "Anwesenheit", "preview_message": "Hey du. Du bist großartig!", "privacy": "Privatsphäre", @@ -506,6 +538,8 @@ "room": "Raum", "room_name": "Raumname", "rooms": "Räume", + "save": "Speichern", + "saved": "Gespeichert", "saving": "Speichere …", "secure_backup": "Verschlüsselte Sicherung", "security": "Sicherheit", @@ -516,7 +550,7 @@ "show_more": "Mehr zeigen", "someone": "Jemand", "space": "Raum", - "spaces": "Räume", + "spaces": "Spaces", "sticker": "Sticker", "stickerpack": "Sticker-Paket", "success": "Erfolg", @@ -534,6 +568,7 @@ "unnamed_room": "Unbenannter Raum", "unnamed_space": "Unbenannter Space", "unverified": "Nicht verifiziert", + "updating": "Aktualisieren...", "user": "Benutzer", "user_avatar": "Profilbild", "username": "Benutzername", @@ -661,7 +696,7 @@ "private_personal_heading": "Für wen ist dieser Space gedacht?", "private_space": "Für mich und meine Kollegen", "private_space_description": "Ein privater Space für dich und deine Kollegen", - "public_description": "Öffne den Space für alle - am besten für Communities", + "public_description": "Öffne den Space für alle - am besten für Communitys", "public_heading": "Dein öffentlicher Space", "search_public_button": "Öffentliche Spaces suchen", "setup_rooms_community_description": "Lass uns für jedes einen Raum erstellen.", @@ -687,6 +722,7 @@ "twemoji": "Die Twemoji-Emojis sind © Twitter, Inc und weitere Mitwirkende und wird unter den Bedingungen von CC-BY 4.0 verwendet.", "twemoji_colr": "Die Schriftart twemoji-colr ist © Mozilla Foundation und wird unter den Bedingungen von Apache 2.0 verwendet." }, + "desktop_default_device_name": "%(brand)s Desktop: %(platformName)s", "devtools": { "active_widgets": "Aktive Widgets", "category_other": "Sonstiges", @@ -732,6 +768,7 @@ "room_notifications_type": "Typ: ", "room_status": "Raumstatus", "room_unread_status_count": { + "one": "Ungelesen Status des ChatRooms: %(status)s, Anzahl: %(count)s", "other": "Ungelesen-Status im Raum: %(status)s, Anzahl: %(count)s" }, "save_setting_values": "Einstellungswerte speichern", @@ -761,7 +798,7 @@ "toolbox": "Werkzeugkasten", "use_at_own_risk": "Diese Benutzeroberfläche prüft nicht auf richtige Datentypen. Benutzung auf eigene Gefahr.", "user_read_up_to": "Der Benutzer hat gelesen bis: ", - "user_read_up_to_ignore_synthetic": "Der Benutzer hat gelesen bis (ignoreSynthetic): ", + "user_read_up_to_ignore_synthetic": "Der Benutzer hat bis (ignoreSynthetic) gelesen: ", "user_read_up_to_private": "Benutzer las bis (m.read.private): ", "user_read_up_to_private_ignore_synthetic": "Benutzer las bis (m.read.private;ignoreSynthetic): ", "value": "Wert", @@ -860,6 +897,8 @@ "warning": "Wenn du die neue Wiederherstellungsmethode nicht festgelegt hast, versucht ein Angreifer möglicherweise, auf dein Konto zuzugreifen. Ändere dein Kontopasswort und lege sofort eine neue Wiederherstellungsmethode in den Einstellungen fest." }, "not_supported": "", + "pinned_identity_changed": "Die Identität von %(displayName)s (%(userId)s)) hat sich geändert. Mehr erfahren", + "pinned_identity_changed_no_displayname": "Die Identität von %(userId)s hat sich geändert. Mehr erfahren", "recovery_method_removed": { "description_1": "In dieser Sitzung wurde festgestellt, dass deine Sicherheitsphrase und dein Schlüssel für sichere Nachrichten entfernt wurden.", "description_2": "Wenn du dies versehentlich getan hast, kannst du in dieser Sitzung \"sichere Nachrichten\" einrichten, die den Nachrichtenverlauf dieser Sitzung mit einer neuen Wiederherstellungsmethode erneut verschlüsseln.", @@ -929,7 +968,8 @@ "qr_reciprocate_same_shield_device": "Fast geschafft! Zeigen beide Geräte das selbe Wappen an?", "qr_reciprocate_same_shield_user": "Fast geschafft! Wird bei %(displayName)s das gleiche Schild angezeigt?", "request_toast_accept": "Sitzung verifizieren", - "request_toast_decline_counter": "Ignorieren (%(counter)s)", + "request_toast_accept_user": "Benutzer verifizieren", + "request_toast_decline_counter": "Blockiert (%(counter)s)", "request_toast_detail": "%(deviceId)s von %(ip)s", "reset_proceed_prompt": "Mit Zurücksetzen fortfahren", "sas_caption_self": "Verifiziere dieses Gerät, indem du überprüfst, dass die folgende Zahl auf dem Bildschirm erscheint.", @@ -954,7 +994,6 @@ "unverified_sessions_toast_description": "Überprüfe sie, um ein sicheres Konto gewährleisten zu können", "unverified_sessions_toast_reject": "Später", "unverified_sessions_toast_title": "Du hast nicht verifizierte Sitzungen", - "verification_description": "Verifiziere diese Anmeldung, um auf verschlüsselte Nachrichten zuzugreifen und dich anderen gegenüber zu identifizieren.", "verification_dialog_title_device": "Anderes Gerät verifizieren", "verification_dialog_title_user": "Verifizierungsanfrage", "verification_skip_warning": "Ohne dich zu verifizieren wirst du keinen Zugriff auf alle deine Nachrichten haben und könntest für andere als nicht vertrauenswürdig erscheinen.", @@ -1020,6 +1059,10 @@ "error_app_open_in_another_tab": "Wechsle zu einem anderen Tab um mit %(brand)s zu verbinden. Dieser Tab kann jetzt geschlossen werden.", "error_app_open_in_another_tab_title": "%(brand)s läuft bereits in einem anderen Tab.", "error_app_opened_in_another_window": "%(brand)s läuft bereit in einem anderen Fenster. Klicke \"%(label)s um %(brand)s hier zu nutzen und beende das andere Fenster.", + "error_database_closed_description": { + "for_desktop": "Deine Festplatte scheint voll zu sein. Mache Speicherplatz frei und lade erneut.", + "for_web": "Diese Meldung wird erwartet, wenn Sie die Browserdaten gelöscht haben. %(brand)s ist möglicherweise auch in einem anderen Tab geöffnet oder Ihre Festplatte ist voll. Bitte machen Sie etwas Speicherplatz frei und laden Sie die Seite neu." + }, "error_database_closed_title": "%(brand)s funktioniert nicht mehr", "error_dialog": { "copy_room_link_failed": { @@ -1060,7 +1103,15 @@ "you": "Du reagiertest mit %(reaction)s auf %(message)s" }, "m.sticker": "%(senderName)s: %(stickerName)s", - "m.text": "%(senderName)s: %(message)s" + "m.text": "%(senderName)s: %(message)s", + "prefix": { + "audio": "Audio", + "file": "Datei", + "image": "Bild", + "poll": "Umfrage", + "video": "Video" + }, + "preview": "%(prefix)s%(preview)s" }, "export_chat": { "cancelled": "Exportieren abgebrochen", @@ -1183,7 +1234,17 @@ "other": "In %(spaceName)s und %(count)s weiteren Spaces." }, "incompatible_browser": { - "title": "Nicht unterstützter Browser" + "continue": "Trotzdem fortfahren", + "description": "%(brand)s verwendet einige Browser-Funktionen, die von deinem aktuellen Browser nicht unterstützt werden. %(detail)s", + "learn_more": "Mehr erfahren", + "linux": "Linux", + "macos": "Mac", + "supported_browsers": "Verwenden Sie Chrome, Firefox, Edge oder Safari, um das beste Erlebnis zu erzielen.", + "title": "Nicht unterstützter Browser", + "use_desktop_heading": "Verwende stattdessen %(brand)s Desktop", + "use_mobile_heading": "Stattdessen %(brand)s am Smartphone benutzen", + "use_mobile_heading_after_desktop": "Oder verwende die mobile App", + "windows": "Windows (%(bits)s-bit)" }, "info_tooltip_title": "Information", "integration_manager": { @@ -1307,12 +1368,14 @@ "navigate_next_message_edit": "Nächste Nachricht bearbeiten", "navigate_prev_history": "Vorheriger kürzlich besuchter Raum oder Space", "navigate_prev_message_edit": "Vorherige Nachricht bearbeiten", + "next_landmark": "Zur nächsten Landmark springen", "next_room": "Nächste Unterhaltung", "next_unread_room": "Nächste ungelesene Nachricht", "number": "[Nummer]", "open_user_settings": "Benutzereinstellungen öffnen", "page_down": "Bild runter", "page_up": "Bild hoch", + "prev_landmark": "Zur vorherigen Landmark springen", "prev_room": "Vorherige Unterhaltung", "prev_unread_room": "Vorherige ungelesene Nachricht", "room_list_collapse_section": "Raumliste einklappen", @@ -1357,8 +1420,11 @@ "dynamic_room_predecessors": "Veränderbare Raumvorgänger", "dynamic_room_predecessors_description": "MSC3946 aktivieren (zur Verknüpfung von Raumarchiven nach der Raumerstellung)", "element_call_video_rooms": "Element Call-Videoräume", + "exclude_insecure_devices": "Unsichere Geräte ausschließen beim senden/empfangen von Nachrichten", + "exclude_insecure_devices_description": "Bei Aktivierung dieses Modus werden verschlüsselte Nachrichten nicht mehr mit unverifizierten Geräten geteilt und Nachrichten von unverifizierten Geräten werden als Fehler angezeigt. Beachte, dass wenn du den Modus aktivierst, eine Kommunikation mit Benutzern, die keine verifizierten Geräte haben, nicht möglich ist.", "experimental_description": "Experimentierfreudig? Probiere unsere neuesten, sich in Entwicklung befindlichen Ideen aus. Diese Funktionen sind nicht final; Sie könnten instabil sein, sich verändern oder sogar ganz entfernt werden. Erfahre mehr.", "experimental_section": "Frühe Vorschauen", + "extended_profiles_msc_support": "Erfordert die Unterstützung von MSC4133 durch den Server", "feature_disable_call_per_sender_encryption": "Verschlüsselung per-sender für Element Anruf abschalten", "feature_wysiwyg_composer_description": "Verwende Textverarbeitung (Rich-Text) statt Markdown im Eingabefeld.", "group_calls": "Neue Gruppenanruf-Erfahrung", @@ -1369,9 +1435,10 @@ "group_moderation": "Moderation", "group_profile": "Profil", "group_rooms": "Räume", - "group_spaces": "Räume", + "group_spaces": "Spaces", "group_themes": "Themen", "group_threads": "Themen", + "group_ui": "Benutzeroberfläche", "group_voip": "Anrufe", "group_widgets": "Widgets", "hidebold": "Benachrichtigungspunkt ausblenden (nur Zähler zeigen)", @@ -1400,7 +1467,6 @@ "sliding_sync": "Sliding-Sync-Modus", "sliding_sync_description": "In aktiver Entwicklung, kann nicht deaktiviert werden.", "sliding_sync_disabled_notice": "Zum Deaktivieren, melde dich ab und erneut an", - "sliding_sync_server_no_support": "Dein Server unterstützt dies nicht nativ", "under_active_development": "In aktiver Entwicklung.", "unrealiable_e2e": "Nicht zuverlässig in verschlüsselten Räumen", "video_rooms": "Videoräume", @@ -1518,6 +1584,7 @@ }, "member_list_back_action_label": "Raummitglieder", "message_edit_dialog_title": "Nachrichtenänderungen", + "migrating_crypto": "Bleib dran. Wir aktualisieren%(brand)s, um die Verschlüsselung schneller und zuverlässiger zu machen.", "mobile_guide": { "toast_accept": "App verwenden", "toast_description": "%(brand)s ist in mobilen Browsern experimentell. Für eine bessere Erfahrung nutze unsere App.", @@ -1543,8 +1610,10 @@ "keyword": "Schlüsselwort", "keyword_new": "Neues Schlüsselwort", "level_activity": "Aktivität", + "level_highlight": "Hervorhebung", "level_muted": "Stumm", "level_none": "Nichts", + "level_notification": "Benachrichtigung", "level_unsent": "Nicht gesendet", "mark_all_read": "Alle als gelesen markieren", "mentions_and_keywords": "@Erwähnungen und Schlüsselwörter", @@ -1572,8 +1641,8 @@ "download_brand_desktop": "%(brand)s Desktop herunterladen", "download_f_droid": "In F-Droid erhältlich", "download_google_play": "In Google Play erhältlich", - "enable_notifications": "Benachrichtigungen einschalten", - "enable_notifications_action": "Benachrichtigungen aktivieren", + "enable_notifications": "Desktopbenachrichtigungen einschalten", + "enable_notifications_action": "Einstellungen öffnen", "enable_notifications_description": "Verpasse keine Antworten oder wichtigen Nachrichten", "explore_rooms": "Öffentliche Räume erkunden", "find_community_members": "Finde deine Community-Mitglieder und lade sie ein", @@ -1711,7 +1780,7 @@ "disagree": "Ablehnen", "error_create_room_moderation_bot": "Erstellen des Raums mit Moderations-Bot nicht möglich", "hide_messages_from_user": "Prüfe, ob du alle aktuellen und zukünftigen Nachrichten dieses Nutzers verstecken willst.", - "ignore_user": "Nutzer ignorieren", + "ignore_user": "Benutzer blockieren", "illegal_content": "Illegale Inhalte", "missing_reason": "Bitte gib an, weshalb du einen Fehler meldest.", "nature": "Bitte wähle eine Kategorie aus und beschreibe, was die Nachricht missbräuchlich macht.", @@ -1752,14 +1821,32 @@ "restore_failed_error": "Konnte Schlüsselsicherung nicht wiederherstellen" }, "right_panel": { - "add_integrations": "Widgets, Brücken und Bots hinzufügen", + "add_integrations": "Erweiterungen hinzufügen", + "add_topic": "Thema hinzufügen", + "extensions_button": "Erweiterungen", + "extensions_empty_description": "Wählen Sie \"%(addIntegrations)s\" um Erweiterungen zu suchen und diesem Raum hinzuzufügen", "files_button": "Dateien", "pinned_messages": { + "empty_title": "Hefte wichtige Nachrichten an, damit sie leicht gefunden werden können.", + "header": { + "one": "1 angeheftete Nachricht", + "other": "%(count)s angeheftete Nachrichten" + }, "limits": { "other": "Du kannst nur %(count)s Widgets anheften" - } + }, + "menu": "Menü öffnen", + "release_announcement": { + "close": "Ok", + "description": "Alle angehefteten Nachrichten findest du hier. Bewege den Mauszeiger über eine beliebige Nachricht und wählen anheften, um sie hinzuzufügen." + }, + "reply_thread": "Auf Nachricht im Thread antworten", + "unpin_all": { + "button": "Alle Nachrichten lösen", + "title": "Alle Nachrichten lösen?" + }, + "view": "Im Nachrichtenverlauf ansehen" }, - "pinned_messages_button": "Angeheftet", "poll": { "active_heading": "Aktive Umfragen", "empty_active": "In diesem Raum gibt es keine aktiven Umfragen", @@ -1780,11 +1867,11 @@ }, "load_more": "Weitere Umfragen laden", "loading": "Lade Umfragen", - "past_heading": "Vergangene Umfragen", + "past_heading": "Abgeschlossene Umfragen", "view_in_timeline": "Umfrage im Verlauf anzeigen", "view_poll": "Umfrage ansehen" }, - "polls_button": "Umfrageverlauf", + "polls_button": "Umfragen", "room_summary_card": { "title": "Raum-Info" }, @@ -1813,6 +1900,7 @@ "forget": "Raum vergessen", "low_priority": "Niedrige Priorität", "mark_read": "Als gelesen markieren", + "mark_unread": "Als ungelesen markieren", "notifications_default": "Standardeinstellung verwenden", "notifications_mute": "Raum stumm stellen", "title": "Raumoptionen", @@ -1861,6 +1949,8 @@ }, "room_is_public": "Dieser Raum ist öffentlich" }, + "header_avatar_open_settings_label": "Raumeinstellungen öffnen", + "header_face_pile_tooltip": "Personen", "header_untrusted_label": "Nicht vertrauenswürdig", "inaccessible": "Dieser Raum oder Space ist im Moment nicht zugänglich.", "inaccessible_name": "Auf %(roomName)s kann momentan nicht zugegriffen werden.", @@ -1884,10 +1974,10 @@ "you_created": "Du hast diesen Raum erstellt." }, "invite_email_mismatch_suggestion": "Teile diese E-Mail-Adresse in den Einstellungen, um Einladungen direkt in %(brand)s zu erhalten.", - "invite_reject_ignore": "Ablehnen und Nutzer blockieren", + "invite_reject_ignore": "Ablehnen und Benutzer blockieren", "invite_sent_to_email": "Einladung an %(email)s gesendet", "invite_sent_to_email_room": "Diese Einladung zu %(roomName)s wurde an %(email)s gesendet", - "invite_subtitle": " hat dich eingeladen", + "invite_subtitle": "Eingeladen von ", "invite_this_room": "In diesen Raum einladen", "invite_title": "Möchtest du %(roomName)s betreten?", "inviter_unknown": "Unbekannt", @@ -1930,11 +2020,24 @@ "not_found_title": "Dieser Raum oder Space existiert nicht.", "not_found_title_name": "%(roomName)s existiert nicht.", "peek_join_prompt": "Du erkundest den Raum %(roomName)s. Willst du ihn betreten?", + "pinned_message_badge": "Fixierte Nachrichten", + "pinned_message_banner": { + "button_close_list": "Liste schließen", + "button_view_all": "Alle anzeigen", + "description": "Dieser Raum hat fixierte Nachrichten. Klicke zum Anzeigen.", + "go_to_message": "Fixierte Nachrichten im Nachrichtenverlauf anzeigen.", + "title": "%(index)s of %(length)s Angeheftete Nachrichten" + }, "read_topic": "Klicke, um das Thema zu lesen", "rejecting": "Lehne Einladung ab …", "rejoin_button": "Erneut betreten", "search": { "all_rooms_button": "Alle Räume durchsuchen", + "placeholder": "Nachrichten durchsuchen...", + "summary": { + "one": "1 Ergebnis für \"\" gefunden", + "other": "%(count)s Ergebnisse für \"\" gefunden" + }, "this_room_button": "Diesen Raum durchsuchen" }, "status_bar": { @@ -2070,6 +2173,8 @@ "error_deleting_alias_description": "Beim Entfernen dieser Adresse ist ein Fehler aufgetreten. Vielleicht existiert sie nicht mehr oder es kam zu einem temporären Fehler.", "error_deleting_alias_description_forbidden": "Du hast nicht die Berechtigung, die Adresse zu löschen.", "error_deleting_alias_title": "Fehler beim Löschen der Adresse", + "error_publishing": "Raum konnte nicht auf öffentlich gestellt werden", + "error_publishing_detail": "Bei der Freigabe des Raumes hat es einen Fehler gegeben", "error_save_space_settings": "Spaceeinstellungen konnten nicht gespeichert werden.", "error_updating_alias_description": "Es gab einen Fehler beim Ändern des Raumalias. Entweder erlaubt es der Server nicht oder es gab ein temporäres Problem.", "error_updating_canonical_alias_description": "Es gab ein Problem beim Aktualisieren der Raum-Hauptadresse. Es kann sein, dass der Server dies verbietet oder ein temporäres Problem aufgetreten ist.", @@ -2140,7 +2245,7 @@ "m.room.history_visibility": "Sichtbarkeit des Verlaufs ändern", "m.room.name": "Raumname ändern", "m.room.name_space": "Name des Space ändern", - "m.room.pinned_events": "Angeheftete Ereignisse verwalten", + "m.room.pinned_events": "Angeheftete Nachrichten verwalten", "m.room.power_levels": "Berechtigungen ändern", "m.room.redaction": "Vom mir gesendete Nachrichten löschen", "m.room.server_acl": "Server-ACLs bearbeiten", @@ -2297,25 +2402,37 @@ "brand_version": "Version von %(brand)s:", "clear_cache_reload": "Zwischenspeicher löschen und neu laden", "crypto_version": "Krypto-Version:", + "dialog_title": "Einstellungen: Hilfe & Info", "help_link": "Um Hilfe zur Benutzung von %(brand)s zu erhalten, klicke hier.", "homeserver": "Heim-Server ist %(homeserverUrl)s", "identity_server": "Identitäts-Server ist %(identityServerUrl)s", - "title": "Hilfe und Info", + "title": "Hilfe & Info", "versions": "Versionen" } }, "settings": { + "account": { + "dialog_title": "Einstellunge: Konto", + "title": "Konto" + }, "all_rooms_home": "Alle Räume auf Startseite anzeigen", "all_rooms_home_description": "Alle Räume, denen du beigetreten bist, werden auf der Startseite erscheinen.", "always_show_message_timestamps": "Nachrichtenzeitstempel immer anzeigen", "appearance": { + "compact_layout": "Kompakten Text und Nachrichten anzeigen", + "compact_layout_description": "Um diese Funktion nutzen zu können, muss das moderne Nachrichtenlayout ausgewählt sein.", "custom_font": "Systemschriftart verwenden", "custom_font_description": "Setze den Schriftnamen auf eine in deinem System installierte Schriftart und %(brand)s wird versuchen, sie zu verwenden.", "custom_font_name": "Systemschriftart", "custom_font_size": "Andere Schriftgröße verwenden", - "custom_theme_error_downloading": "Fehler beim herunterladen des Themas.", + "custom_theme_add": "Benutzerdefiniertes Design hinzufügen", + "custom_theme_downloading": "Benutzerdefiniertes Design wird heruntergeladen...", + "custom_theme_help": "Gib die URL des einzustellenden benutzerdefinierten Designs ein.", "custom_theme_invalid": "Ungültiges Designschema.", + "dialog_title": "Einstellungen: Erscheinungsbild", "font_size": "Schriftgröße", + "font_size_default": "%(fontSize)s (Standard)", + "high_contrast": "Hochkontrast", "image_size_default": "Standard", "image_size_large": "Groß", "layout_bubbles": "Nachrichtenblasen", @@ -2330,6 +2447,9 @@ "code_block_expand_default": "Quelltextblöcke standardmäßig erweitern", "code_block_line_numbers": "Zeilennummern in Quelltextblöcken", "disable_historical_profile": "Aktuelle Profilbilder und Anzeigenamen im Verlauf anzeigen", + "discovery": { + "title": "Wie man Sie findet" + }, "emoji_autocomplete": "Emoji-Vorschläge während Eingabe", "enable_markdown": "Markdown aktivieren", "enable_markdown_description": "Beginne Nachrichten mit /plain, um sie ohne Markdown zu senden.", @@ -2345,6 +2465,13 @@ "add_msisdn_dialog_title": "Telefonnummer hinzufügen", "add_msisdn_instructions": "Gib den per SMS an +%(msisdn)s gesendeten Bestätigungscode ein.", "add_msisdn_misconfigured": "Das MSISDN-Verknüpfungsverfahren ist falsch konfiguriert", + "allow_spellcheck": "Rechtschreibprüfung zulassen", + "application_language_reload_hint": "Nach Änderung der Sprache wird die App neu gestartet", + "avatar_remove_progress": "Bild wird entfernt...", + "avatar_save_progress": "Bild wird hochgeladen...", + "avatar_upload_error_text": "Das Dateiformat wird nicht unterstützt oder das Bild ist größer als %(size)s.", + "avatar_upload_error_text_generic": "Das Dateiformat wird möglicherweise nicht unterstützt.", + "avatar_upload_error_title": "Profilbild konnte nicht hochgeladen werden", "confirm_adding_email_body": "Klicke unten auf den Knopf, um die hinzugefügte E-Mail-Adresse zu bestätigen.", "confirm_adding_email_title": "Hinzugefügte E-Mail-Addresse bestätigen", "deactivate_confirm_body": "Willst du dein Konto wirklich deaktivieren? Du kannst dies nicht rückgängig machen.", @@ -2352,7 +2479,6 @@ "deactivate_confirm_content": "Bestätige, dass du dein Konto deaktivieren möchtest. Wenn du fortfährst, tritt folgendes ein:", "deactivate_confirm_content_1": "Du wirst dein Konto nicht reaktivieren können", "deactivate_confirm_content_2": "Du wirst dich nicht mehr anmelden können", - "deactivate_confirm_content_3": "Niemand wird in der Lage sein deinen Benutzernamen (MXID) wiederzuverwenden, dich eingeschlossen: Der Benutzername wird nicht verfügbar bleiben", "deactivate_confirm_content_4": "Du wirst alle Unterhaltungen verlassen, in denen du dich befindest", "deactivate_confirm_content_5": "Du wirst vom Identitäts-Server entfernt: Deine Freunde werden nicht mehr in der Lage sein, dich über deine E-Mail-Adresse oder Telefonnummer zu finden", "deactivate_confirm_content_6": "Deine alten Nachrichten werden weiterhin für Personen sichtbar bleiben, die sie erhalten haben, so wie es bei E-Mails der Fall ist. Möchtest du deine Nachrichten vor Personen verbergen, die Räume in der Zukunft betreten?", @@ -2360,10 +2486,12 @@ "deactivate_confirm_erase_label": "Meine Nachrichten vor neuen Teilnehmern verstecken", "deactivate_section": "Benutzerkonto deaktivieren", "deactivate_warning": "Die Deaktivierung deines Kontos ist unwiderruflich — sei vorsichtig!", - "discovery_email_empty": "Entdeckungsoptionen werden angezeigt, sobald du eine E-Mail-Adresse hinzugefügt hast.", + "discovery_email_empty": "Optionen zum Entdecken anderer Benutzer werden angezeigt, sobald Sie Ihre E-Mail Adresse zu Ihrem Konto hinzugefügt haben,", "discovery_email_verification_instructions": "Verifiziere den Link in deinem Posteingang", - "discovery_msisdn_empty": "Entdeckungsoptionen werden angezeigt, sobald du eine Telefonnummer hinzugefügt hast.", + "discovery_msisdn_empty": "Optionen zum Entdecken anderer Benutzer werden angezeigt, sobald Sie Ihre Telefonnummer zu Ihrem Konto hinzugefügt haben", "discovery_needs_terms": "Stimme den Nutzungsbedingungen des Identitäts-Servers %(serverName)s zu, um per E-Mail-Adresse oder Telefonnummer auffindbar zu werden.", + "display_name": "Anzeigename", + "display_name_error": "Anzeigename konnte nicht gesetzt werden", "email_address_in_use": "Diese E-Mail-Adresse wird bereits verwendet", "email_address_label": "E-Mail-Adresse", "email_not_verified": "Deine E-Mail-Adresse wurde noch nicht verifiziert", @@ -2388,18 +2516,23 @@ "error_share_msisdn_discovery": "Teilen der Telefonnummer nicht möglich", "identity_server_no_token": "Kein Identitäts-Zugangs-Token gefunden", "identity_server_not_set": "Kein Identitäts-Server festgelegt", - "language_section": "Sprache und Region", + "language_section": "Sprache", "msisdn_in_use": "Diese Telefonnummer wird bereits verwendet", "msisdn_label": "Telefonnummer", "msisdn_verification_field_label": "Bestätigungscode", "msisdn_verification_instructions": "Gib den Bestätigungscode ein, den du empfangen hast.", "msisdns_heading": "Telefonnummern", "oidc_manage_button": "Konto verwalten", - "password_change_section": "Setze neues Kontopasswort …", + "password_change_section": "Passwort des Nutzerkontos ändern...", "password_change_success": "Dein Passwort wurde erfolgreich geändert.", + "personal_info": "Persönliche Daten", + "profile_subtitle": "So sehen dich andere in der App.", + "profile_subtitle_oidc": "Dein Konto wird separat durch einen Identitätsanbieter verwaltet. Einige persönliche Daten können hier nicht geändert werden.", "remove_email_prompt": "%(email)s entfernen?", "remove_msisdn_prompt": "%(phone)s entfernen?", - "spell_check_locale_placeholder": "Wähle ein Gebietsschema" + "spell_check_locale_placeholder": "Wähle ein Gebietsschema", + "unable_to_load_emails": "E-Mail Adresse konnte nicht geladen werden", + "username": "Benutzername" }, "image_thumbnails": "Vorschauen für Bilder", "inline_url_previews_default": "URL-Vorschau standardmäßig aktivieren", @@ -2455,12 +2588,17 @@ "phrase_strong_enough": "Super! Diese Passphrase wirkt stark genug" }, "keyboard": { + "dialog_title": "Einstellungen: Tastatur", "title": "Tastatur" }, + "labs_mjolnir": { + "dialog_title": "Einstellungen: Blockierte Benutzer" + }, "notifications": { "default_setting_description": "Diese Einstellung wird standardmäßig für all deine Räume übernommen.", "default_setting_section": "Ich möchte benachrichtigt werden für (Standardeinstellung)", "desktop_notification_message_preview": "Nachrichtenvorschau in der Desktopbenachrichtigung anzeigen", + "dialog_title": "Einstellungen: Benachrichtigungen", "email_description": "E-Mail-Zusammenfassung für verpasste Benachrichtigungen erhalten", "email_section": "E-Mail-Zusammenfassung", "email_select": "Wähle, an welche E-Mail-Adresse die Zusammenfassungen gesendet werden. Verwalte deine E-Mail-Adressen unter .", @@ -2519,12 +2657,15 @@ "code_blocks_heading": "Quelltextblöcke", "compact_modern": "Modernes kompaktes Layout verwenden", "composer_heading": "Nachrichteneingabe", + "default_timezone": "Browser-Standard (%(timezone)s )", + "dialog_title": "Einstellungen: Präferenzen", "enable_hardware_acceleration": "Aktiviere die Hardwarebeschleunigung", "enable_tray_icon": "Fenster beim Schließen in die Symbolleiste minimieren", "keyboard_heading": "Tastenkombinationen", "keyboard_view_shortcuts_button": "Um alle Tastenkombinationen anzuzeigen, klicke hier.", "media_heading": "Mediendateien", "presence_description": "Teile anderen deine Aktivität und deinen Status mit.", + "publish_timezone": "Zeitzone auf öffentlichem Profil anzeigen lassen", "rm_lifetime": "Gültigkeitsdauer der Gelesen-Markierung (ms)", "rm_lifetime_offscreen": "Gültigkeitsdauer der Gelesen-Markierung außerhalb des Bildschirms (ms)", "room_directory_heading": "Raumverzeichnis", @@ -2533,7 +2674,8 @@ "show_checklist_shortcuts": "Verknüpfung zu ersten Schritten (Willkommen) anzeigen", "show_polls_button": "Zeige Pol button", "surround_text": "Sonderzeichen automatisch vor und hinter Textauswahl setzen", - "time_heading": "Zeitanzeige" + "time_heading": "Zeitanzeige", + "user_timezone": "Zeitzone festlegen" }, "prompt_invite": "Warnen, bevor du Einladungen zu ungültigen Matrix-IDs sendest", "replace_plain_emoji": "Klartext-Emoji automatisch ersetzen", @@ -2564,14 +2706,16 @@ "cross_signing_self_signing_private_key": "Selbst signierter privater Schlüssel:", "cross_signing_user_signing_private_key": "Privater Benutzerschlüssel:", "cryptography_section": "Verschlüsselung", + "dehydrated_device_description": "Die Offline-Gerätefunktion ermöglicht es Ihnen, verschlüsselte Nachrichten zu empfangen, auch wenn Sie an keinem Gerät angemeldet sind", + "dehydrated_device_enabled": "Offline-Gerät aktiviert", "delete_backup": "Lösche Sicherung", "delete_backup_confirm_description": "Bist du sicher? Du wirst alle deine verschlüsselten Nachrichten verlieren, wenn deine Schlüssel nicht gut gesichert sind.", + "dialog_title": "Einstellungen Sicherheit & Datenschutz", "e2ee_default_disabled_warning": "Deine Server-Administration hat die Ende-zu-Ende-Verschlüsselung für private Räume und Direktnachrichten standardmäßig deaktiviert.", "enable_message_search": "Nachrichtensuche in verschlüsselten Räumen aktivieren", "encryption_section": "Verschlüsselung", "error_loading_key_backup_status": "Konnte Status der Schlüsselsicherung nicht laden", "export_megolm_keys": "E2E-Raumschlüssel exportieren", - "ignore_users_empty": "Du ignorierst keine Benutzer.", "ignore_users_section": "Blockierte Benutzer", "import_megolm_keys": "E2E-Raumschlüssel importieren", "key_backup_active": "Diese Sitzung sichert deine Schlüssel.", @@ -2682,9 +2826,9 @@ "security_recommendations_description": "Verbessere deine Kontosicherheit, indem du diese Empfehlungen beherzigst.", "session_id": "Sitzungs-ID", "show_details": "Details anzeigen", - "sign_in_with_qr": "Mit QR-Code anmelden", + "sign_in_with_qr": "Neues Gerät verknüpfen", "sign_in_with_qr_button": "QR-Code anzeigen", - "sign_in_with_qr_description": "Du kannst dieses Gerät verwenden, um ein neues Gerät per QR-Code anzumelden. Dazu musst du den auf diesem Gerät angezeigten QR-Code mit deinem nicht angemeldeten Gerät einlesen.", + "sign_in_with_qr_unsupported": "Nicht unterstützt von deinem Kontoanbieter", "sign_out": "Von dieser Sitzung abmelden", "sign_out_all_other_sessions": "Von allen anderen Sitzungen abmelden (%(otherSessionsCount)s)", "sign_out_confirm_description": { @@ -2724,7 +2868,9 @@ "show_redaction_placeholder": "Platzhalter für gelöschte Nachrichten", "show_stickers_button": "Sticker-Schaltfläche", "show_typing_notifications": "Tippbenachrichtigungen anzeigen", + "showbold": "Alle Aktivitäten in Raumliste anzeigen (Punkt oder Anzahl ungelesener Nachrichten)", "sidebar": { + "dialog_title": "Einstellungen: Seitenleiste", "metaspaces_favourites_description": "Gruppiere all deine favorisierten Unterhaltungen an einem Ort.", "metaspaces_home_all_rooms": "Alle Räume anzeigen", "metaspaces_home_all_rooms_description": "Alle Räume auf der Startseite anzeigen, auch wenn sie Teil eines Space sind.", @@ -2733,10 +2879,12 @@ "metaspaces_orphans_description": "Gruppiere all deine Räume, die nicht Teil eines Spaces sind, an einem Ort.", "metaspaces_people_description": "Gruppiere all deine Direktnachrichten an einem Ort.", "metaspaces_subsection": "Anzuzeigende Spaces", - "spaces_explainer": "Räume sind Möglichkeiten, Personen zu gruppieren. Neben den Räumen, in denen du dich befindest, kannst du auch einige vorgefertigte verwenden.", + "metaspaces_video_rooms": "Videoräume und -konferenzen", + "metaspaces_video_rooms_description": "Gruppiere alle privaten Videoräume und -konferenzen.", "title": "Seitenleiste" }, "start_automatically": "Nach Systemstart automatisch starten", + "tac_only_notifications": "Benachrichtigungen nur im Thread Aktivitätszentrum anzeigen", "use_12_hour_format": "Uhrzeiten im 12-Stundenformat (z. B. 2:30 p. m.)", "use_command_enter_send_message": "Benutze Betriebssystemtaste + Eingabe um eine Nachricht zu senden", "use_command_f_search": "Nutze Command + F um den Verlauf zu durchsuchen", @@ -2750,6 +2898,7 @@ "audio_output_empty": "Keine Audioausgabe erkannt", "auto_gain_control": "Automatische Lautstärkeregelung", "connection_section": "Verbindung", + "dialog_title": "Einstellungen: Anrufe", "echo_cancellation": "Echounterdrückung", "enable_fallback_ice_server": "Ersatz-Anrufassistenz-Server erlauben (%(server)s)", "enable_fallback_ice_server_description": "Dieser wird nur verwendet, sollte dein Heim-Server keinen bieten. Deine IP-Adresse würde während eines Anrufs geteilt werden.", @@ -2771,6 +2920,9 @@ "link_title": "Link zum Raum", "permalink_message": "Link zur ausgewählten Nachricht", "permalink_most_recent": "Link zur aktuellsten Nachricht", + "share_call": "Konferenzeinladungslink", + "share_call_subtitle": "Link für externe Benutzer, um dem Anruf ohne Matrixkonto beizutreten:", + "title_link": "Link teilen", "title_message": "Raumnachricht teilen", "title_room": "Raum teilen", "title_user": "Teile Benutzer" @@ -2803,7 +2955,7 @@ "help_dialog_title": "Befehl Hilfe", "holdcall": "Den aktuellen Anruf halten", "html": "Sendet eine Nachricht als HTML, ohne sie als Markdown darzustellen", - "ignore": "Nutzer blockieren und dessen Nachrichten ausblenden", + "ignore": "Benutzer blockieren und dessen Nachrichten ausblenden", "ignore_dialog_description": "%(userId)s ist jetzt blockiert", "ignore_dialog_title": "Benutzer blockiert", "invite": "Lädt den Benutzer mit der angegebenen ID in den aktuellen Raum ein", @@ -2846,7 +2998,7 @@ "unban": "Entbannt den Benutzer mit der angegebenen ID", "unflip": "Stellt ┬──┬ ノ( ゜-゜ノ) einer Klartextnachricht voran", "unholdcall": "Beendet das Halten des Anrufs", - "unignore": "Benutzer nicht mehr ignorieren und neue Nachrichten wieder anzeigen", + "unignore": "Benutzer freigeben und ihre neuen Nachrichten wieder anzeigen", "unignore_dialog_description": "%(userId)s wird nicht mehr blockiert", "unignore_dialog_title": "Benutzer nicht mehr blockiert", "unknown_command": "Unbekannter Befehl", @@ -2971,7 +3123,7 @@ "heading_with_query": "Nutze \"%(query)s\" zum Suchen", "heading_without_query": "Suche nach", "join_button_text": "%(roomAddress)s betreten", - "keyboard_scroll_hint": "Benutze zum scrollen", + "keyboard_scroll_hint": "Benutze zum Scrollen", "message_search_section_title": "Andere Suchen", "other_rooms_in_space": "Andere Räume in %(spaceName)s", "public_rooms_label": "Öffentliche Räume", @@ -3019,12 +3171,22 @@ "one": "%(count)s Antwort", "other": "%(count)s Antworten" }, + "empty_description": "Verwende \"%(replyInThread)s\" beim Hovern über eine Nachricht.", + "empty_title": "Threads helfen dir, deine Unterhaltungen zu einem Thema leichter zu verfolgen.", "error_start_thread_existing_relation": "Du kannst keinen Thread in einem Thread starten", + "mark_all_read": "Alle als gelesen markieren", "my_threads": "Meine Threads", "my_threads_description": "Zeigt alle Threads, an denen du teilgenommen hast", "open_thread": "Thread anzeigen", "show_thread_filter": "Zeige:" }, + "threads_activity_centre": { + "header": "Thread-Aktivität", + "no_rooms_with_threads_notifs": "Sie haben noch keine Räume mit Thread-Benachrichtigungen.", + "no_rooms_with_unread_threads": "Sie haben noch keine Räume mit ungelesenen Threads.", + "release_announcement_description": "Die Thread-Benachrichtigungen wurden verschoben. Sie finden sie ab sofort hier.", + "release_announcement_header": "Thread Aktivitätszentrum" + }, "time": { "about_day_ago": "vor etwa einem Tag", "about_hour_ago": "vor etwa einer Stunde", @@ -3066,9 +3228,19 @@ }, "creation_summary_dm": "%(creator)s hat diese Direktnachricht erstellt.", "creation_summary_room": "%(creator)s hat den Raum erstellt und konfiguriert.", + "decryption_failure": { + "historical_event_no_key_backup": "Der historische Nachrichtenverlauf ist auf diesem Gerät nicht verfügbar.", + "historical_event_unverified_device": "Du musst dieses Gerät verifizieren, um auf den Nachrichtenverlauf zugreifen zu können", + "historical_event_user_not_joined": "Du hast keinen Zugriff auf diese Nachricht", + "sender_identity_previously_verified": "Die verifizierte Identität des Absenders hat sich geändert", + "sender_unsigned_device": "Von einem unsicheren Gerät verschickt.", + "unable_to_decrypt": "Entschlüsselung der Nachricht nicht möglich" + }, "disambiguated_profile": "%(displayName)s (%(matrixId)s)", "download_action_decrypting": "Entschlüsseln", "download_action_downloading": "Herunterladen", + "download_failed": "Herunterladen fehlgeschlagen", + "download_failed_description": "Beim Herunterladen dieser Datei ist ein Fehler aufgetreten", "e2e_state": "Status der Ende zu Ende Verschlüssellung", "edits": { "tooltip_label": "Am %(date)s geändert. Klicke, um Änderungen anzuzeigen.", @@ -3127,7 +3299,7 @@ }, "m.file": { "error_decrypting": "Fehler beim Entschlüsseln des Anhangs", - "error_invalid": "Ungültige Datei%(extra)s" + "error_invalid": "Ungültige Datei" }, "m.image": { "error": "Kann Bild aufgrund eines Fehlers nicht anzeigen", @@ -3242,12 +3414,12 @@ "set": "%(senderDisplayName)s hat den Raumnamen geändert zu %(roomName)s." }, "m.room.pinned_events": { - "changed": "%(senderName)s hat die angehefteten Nachrichten für diesen Raum geändert.", - "changed_link": "%(senderName)s hat die angehefteten Nachrichten geändert.", - "pinned": "%(senderName)s hat eine Nachricht angeheftet. Alle angehefteten Nachrichten anzeigen.", - "pinned_link": "%(senderName)s hat eine Nachricht angeheftet. Alle angehefteten Nachrichten anzeigen.", - "unpinned": "%(senderName)s hat eine Nachricht losgelöst. Alle angepinnten Nachrichten anzeigen.", - "unpinned_link": "%(senderName)s hat eine Nachricht losgeheftet. Alle angehefteten Nachrichten anzeigen." + "changed": "%(senderName)s hat die fixierte Nachrichten für diesen Chatroom geändert.", + "changed_link": "%(senderName)s hat die fixierten Nachrichten geändert.", + "pinned": "%(senderName)s hat eine Nachricht fixiert. Alle fixierten Nachrichten anzeigen.", + "pinned_link": "%(senderName)s hat eine Nachricht fixiert. Alle fixierten Nachrichten anzeigen.", + "unpinned": "%(senderName)s hat eine Nachricht losgelöst. Alle fixierten Nachrichten anzeigen.", + "unpinned_link": "%(senderName)s hat eine Nachricht gelöst. Alle fixierten Nachrichten anzeigen." }, "m.room.power_levels": { "changed": "%(senderName)s hat das Berechtigungslevel von %(powerLevelDiffText)s geändert.", @@ -3314,7 +3486,8 @@ "reactions": { "add_reaction_prompt": "Reaktion hinzufügen", "custom_reaction_fallback_label": "Benutzerdefinierte Reaktion", - "label": "%(reactors)s hat mit %(content)s reagiert" + "label": "%(reactors)s hat mit %(content)s reagiert", + "tooltip_caption": "hat reagiert mit %(shortName)s" }, "read_receipt_title": { "one": "Von %(count)s Person gesehen", @@ -3499,6 +3672,9 @@ "truncated_list_n_more": { "other": "Und %(count)s weitere …" }, + "unsupported_browser": { + "description": "Wenn Sie fortfahren, funktionieren einige Funktionen möglicherweise nicht mehr und es besteht das Risiko, dass Sie in Zukunft Daten verlieren. Aktualisieren Sie Ihren Browser, um die Nutzung von %(brand)s fortzusetzen." + }, "unsupported_server_description": "Dieser Server nutzt eine ältere Matrix-Version. Aktualisiere auf Matrix %(version)s, um %(brand)s fehlerfrei nutzen zu können.", "unsupported_server_title": "Dein Server wird nicht unterstützt", "update": { @@ -3516,6 +3692,10 @@ "toast_title": "Aktualisiere %(brand)s", "unavailable": "Nicht verfügbar" }, + "update_room_access_modal": { + "no_change": "Ich möchte die Zugriffsebene nicht ändern.", + "title": "Ändern Sie die Zugriffsebene des Raums" + }, "upload_failed_generic": "Die Datei „%(fileName)s“ konnte nicht hochgeladen werden.", "upload_failed_size": "Die Datei „%(fileName)s“ überschreitet das Hochladelimit deines Heim-Servers", "upload_failed_title": "Hochladen fehlgeschlagen", @@ -3525,6 +3705,7 @@ "error_files_too_large": "Die Datei ist zu groß, um hochgeladen zu werden. Die maximale Dateigröße ist %(limit)s.", "error_some_files_too_large": "Einige Dateien sind zu groß, um hochgeladen zu werden. Die maximale Dateigröße ist %(limit)s.", "error_title": "Fehler beim Hochladen", + "not_image": "Die von dir ausgewählte Datei ist keine gültige Bilddatei.", "title": "Dateien hochladen", "title_progress": "Dateien hochladen (%(current)s von %(total)s)", "upload_all_button": "Alle hochladen", @@ -3551,6 +3732,7 @@ "deactivate_confirm_action": "Konto deaktivieren", "deactivate_confirm_description": "Beim Deaktivieren wirst du abgemeldet und ein erneutes Anmelden verhindert. Zusätzlich wirst du aus allen Räumen entfernt. Diese Aktion kann nicht rückgängig gemacht werden. Bist du sicher, dass du dieses Konto deaktivieren willst?", "deactivate_confirm_title": "Konto deaktivieren?", + "dehydrated_device_enabled": "Offline-Gerät aktiviert", "demote_button": "Zurückstufen", "demote_self_confirm_description_space": "Das Entfernen von Rechten kann nicht rückgängig gemacht werden. Falls sie dir niemand anderer zurückgeben kann, kannst du sie nie wieder erhalten.", "demote_self_confirm_room": "Du wirst nicht in der Lage sein, die Änderung zurückzusetzen, da du dich degradierst. Wenn du der letze Nutzer mit Berechtigungen bist, wird es unmöglich sein die Privilegien zurückzubekommen.", @@ -3567,6 +3749,7 @@ "error_revoke_3pid_invite_title": "Einladung konnte nicht zurückgezogen werden", "hide_sessions": "Sitzungen ausblenden", "hide_verified_sessions": "Verifizierte Sitzungen ausblenden", + "ignore_button": "Blockieren", "ignore_confirm_description": "Alle Nachrichten und Einladungen der Person werden verborgen. Bist du sicher, dass du sie ignorieren möchtest?", "ignore_confirm_title": "%(user)s ignorieren", "invited_by": "%(sender)s eingeladen", @@ -3594,23 +3777,26 @@ "no_recent_messages_description": "Versuche nach oben zu scrollen, um zu sehen ob sich dort frühere Nachrichten befinden.", "no_recent_messages_title": "Keine neuen Nachrichten von %(user)s gefunden" }, - "redact_button": "Kürzlich gesendete Nachrichten entfernen", + "redact_button": "Nachrichten entfernen", "revoke_invite": "Einladung zurückziehen", "room_encrypted": "Nachrichten in diesem Raum sind Ende-zu-Ende verschlüsselt.", "room_encrypted_detail": "Diese Nachricht ist verschlüsselt. Nur Sie und der Empfänger haben den Schlüssel, um die Nachricht zu entschlüsseln.", "room_unencrypted": "Nachrichten in diesem Raum sind nicht Ende-zu-Ende verschlüsselt.", "room_unencrypted_detail": "Nachrichten in verschlüsselten Räumen können nur von dir und vom Empfänger gelesen werden.", - "share_button": "Link zu Benutzer teilen", + "send_message": "Nachricht senden", + "share_button": "Profil teilen", "unban_button_room": "Entbannen", "unban_button_space": "Entbannen", "unban_room_confirm_title": "Von %(roomName)s entbannen", "unban_space_everything": "Überall wo ich die Rechte dazu habe, entbannen", "unban_space_specific": "In ausgewählten Räumen und Spaces entbannen", "unban_space_warning": "Die Person wird keinen Zutritt zu Bereichen haben, in denen du nicht administrierst.", + "unignore_button": "Nicht mehr ignorieren", "verify_button": "Nutzer verifizieren", "verify_explainer": "Für zusätzliche Sicherheit, verifiziere diesen Nutzer, durch Vergleichen eines Einmal-Codes auf euren beiden Geräten." }, "user_menu": { + "link_new_device": "Neues Gerät verknüpfen", "settings": "Alle Einstellungen", "switch_theme_dark": "Zum dunklen Thema wechseln", "switch_theme_light": "Zum hellen Thema wechseln" @@ -3667,9 +3853,9 @@ "camera_enabled": "Deine Kamera ist noch aktiv", "cannot_call_yourself_description": "Du kannst keinen Anruf mit dir selbst starten.", "change_input_device": "Eingabegerät wechseln", + "close_lobby": "Lobby schließen", "connecting": "Verbinden", "connection_lost": "Verbindung zum Server unterbrochen", - "connection_lost_description": "Sie können keine Anrufe starten ohne Verbindung zum Server.", "consulting": "%(transferTarget)s wird angefragt. Übertragung zu %(transferee)s", "default_device": "Standardgerät", "dial": "Wählen", @@ -3680,17 +3866,26 @@ "disabled_no_perms_start_video_call": "Dir fehlt die Berechtigung, um Videoanrufe zu beginnen", "disabled_no_perms_start_voice_call": "Dir fehlt die Berechtigung, um Audioanrufe zu beginnen", "disabled_ongoing_call": "laufender Anruf", + "element_call": "Element Anruf", "enable_camera": "Kamera aktivieren", "enable_microphone": "Mikrofon aktivieren", "expand": "Zurück zum Anruf", "failed_call_live_broadcast_description": "Du kannst keinen Anruf beginnen, da du im Moment eine Sprachübertragung aufzeichnest. Bitte beende deine Sprachübertragung, um ein Gespräch zu beginnen.", "failed_call_live_broadcast_title": "Kann keinen Anruf beginnen", + "get_call_link": "Anruflink teilen", "hangup": "Auflegen", "hide_sidebar_button": "Seitenleiste verbergen", "input_devices": "Eingabegeräte", + "jitsi_call": "Jitsi-Konferenz", "join_button_tooltip_call_full": "Entschuldigung — dieser Anruf ist aktuell besetzt", "join_button_tooltip_connecting": "Verbinden", + "legacy_call": "Legacy-Anruf", "maximise": "Bildschirm füllen", + "maximise_call": "Anruf maximieren", + "metaspace_video_rooms": { + "conference_room_section": "Konferenzen" + }, + "minimise_call": "Anruf minimieren", "misconfigured_server": "Anruf aufgrund eines falsch konfigurierten Servers fehlgeschlagen", "misconfigured_server_description": "Bitte frage die Administration deines Heim-Servers (%(homeserverDomain)s) darum, einen TURN-Server einzurichten, damit Anrufe zuverlässig funktionieren.", "misconfigured_server_fallback": "Alternativ kannst du versuchen, den öffentlichen Server unter zu verwenden. Dieser wird nicht so zuverlässig sein und deine IP-Adresse wird mit ihm geteilt. Du kannst dies auch in den Einstellungen konfigurieren.", @@ -3732,12 +3927,13 @@ "unknown_person": "unbekannte Person", "unsilence": "Ton an", "unsupported": "Anrufe werden nicht unterstützt", - "unsupported_browser": "Sie können in diesem Browser keien Anrufe durchführen.", + "unsupported_browser": "Du kannst in diesem Browser keine Anrufe tätigen.", "user_busy": "Person beschäftigt", "user_busy_description": "Die angerufene Person ist momentan beschäftigt.", "user_is_presenting": "%(sharerName)s präsentiert", "video_call": "Videoanruf", "video_call_started": "Videoanruf hat begonnen", + "video_call_using": "Videoanruf mit:", "voice_call": "Sprachanruf", "you_are_presenting": "Du präsentierst" }, @@ -3846,7 +4042,7 @@ "title": "Erlaube diesem Widget deine Identität zu überprüfen" }, "popout": "Widget in eigenem Fenster öffnen", - "set_room_layout": "Dein Raumlayout für alle setzen", + "set_room_layout": "Layout für alle festlegen", "shared_data_avatar": "Deine Profilbild-URL", "shared_data_device_id": "Deine Geräte-ID", "shared_data_lang": "Deine Sprache", @@ -3872,7 +4068,6 @@ "l33t": "Vorhersagbare Ersetzungen wie „@“ anstelle von „a“ helfen nicht besonders", "longerKeyboardPattern": "Nutze ein längeres Tastaturmuster mit mehr Abwechslung", "noNeed": "Kein Bedarf an Symbolen, Zahlen oder Großbuchstaben", - "pwned": "Wenn Sie dieses Passwort woanders verwenden, sollten Sie es ändern.", "recentYears": "Vermeide die letzten Jahre", "repeated": "Vermeide wiederholte Worte und Zeichen", "reverseWords": "Umgedrehte Worte sind nicht schwerer zu erraten", From b87437d439b6132fc5fe08a1248ade0c3921a98c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 2 Dec 2024 09:49:52 +0000 Subject: [PATCH 19/27] Improve performance of RoomContext in RoomHeader (#28574) * Improve performance of RoomContext in RoomHeader This allows a component to subscribe to only part of the RoomContext so they do not need to re-render on every single change Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Prettier Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Add comment Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/FilePanel.tsx | 22 ++---- .../structures/NotificationPanel.tsx | 13 ++-- src/components/structures/RoomSearchView.tsx | 4 +- src/components/structures/RoomView.tsx | 42 +++++----- src/components/structures/ThreadPanel.tsx | 19 +++-- src/components/structures/ThreadView.tsx | 17 ++-- .../WaitingForThirdPartyRoomView.tsx | 4 +- src/components/views/avatars/BaseAvatar.tsx | 6 +- .../views/context_menus/WidgetContextMenu.tsx | 4 +- .../views/elements/MiniAvatarUploader.tsx | 4 +- .../views/messages/RoomPredecessorTile.tsx | 6 +- .../views/right_panel/PinnedMessagesCard.tsx | 16 ++-- .../views/right_panel/RoomSummaryCard.tsx | 5 +- .../views/right_panel/TimelineCard.tsx | 15 ++-- src/components/views/rooms/HistoryTile.tsx | 6 +- .../views/rooms/MessageComposerButtons.tsx | 6 +- src/components/views/rooms/NewRoomIntro.tsx | 4 +- src/components/views/rooms/RoomHeader.tsx | 6 +- src/components/views/rooms/ThreadSummary.tsx | 4 +- .../wysiwyg_composer/components/Emoji.tsx | 4 +- .../components/WysiwygAutocomplete.tsx | 4 +- .../components/WysiwygComposer.tsx | 4 +- .../wysiwyg_composer/hooks/useEditing.ts | 4 +- .../hooks/useInitialContent.ts | 10 +-- .../hooks/useInputEventProcessor.ts | 8 +- .../hooks/usePlainTextListeners.ts | 4 +- .../hooks/useWysiwygEditActionHandler.ts | 5 +- .../hooks/useWysiwygSendActionHandler.ts | 5 +- .../rooms/wysiwyg_composer/hooks/utils.ts | 4 +- .../rooms/wysiwyg_composer/utils/editing.ts | 2 +- .../rooms/wysiwyg_composer/utils/event.ts | 4 +- .../rooms/wysiwyg_composer/utils/message.ts | 4 +- src/contexts/RoomContext.ts | 5 +- src/contexts/ScopedRoomContext.tsx | 78 +++++++++++++++++++ src/hooks/room/useRoomMemberProfile.ts | 7 +- .../structures/MessagePanel-test.tsx | 5 +- .../structures/ThreadPanel-test.tsx | 12 +-- .../components/structures/ThreadView-test.tsx | 8 +- .../audio_messages/RecordingPlayback-test.tsx | 7 +- .../views/avatars/MemberAvatar-test.tsx | 6 +- .../context_menus/MessageContextMenu-test.tsx | 7 +- .../views/messages/MFileBody-test.tsx | 7 +- .../views/messages/MessageActionBar-test.tsx | 5 +- .../messages/RoomPredecessorTile-test.tsx | 6 +- .../right_panel/RoomSummaryCard-test.tsx | 11 +-- .../views/rooms/EditMessageComposer-test.tsx | 4 +- .../components/views/rooms/EventTile-test.tsx | 7 +- .../views/rooms/MessageComposer-test.tsx | 6 +- .../rooms/MessageComposerButtons-test.tsx | 4 +- .../views/rooms/NewRoomIntro-test.tsx | 6 +- .../views/rooms/SendMessageComposer-test.tsx | 7 +- .../EditWysiwygComposer-test.tsx | 14 ++-- .../SendWysiwygComposer-test.tsx | 6 +- .../components/PlainTextComposer-test.tsx | 6 +- .../components/WysiwygAutocomplete-test.tsx | 6 +- .../components/WysiwygComposer-test.tsx | 10 +-- 56 files changed, 289 insertions(+), 216 deletions(-) create mode 100644 src/contexts/ScopedRoomContext.tsx diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx index 2a82352447..4c580cb9fe 100644 --- a/src/components/structures/FilePanel.tsx +++ b/src/components/structures/FilePanel.tsx @@ -34,6 +34,7 @@ import { Layout } from "../../settings/enums/Layout"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import Measured from "../views/elements/Measured"; import EmptyState from "../views/right_panel/EmptyState"; +import { ScopedRoomContextProvider } from "../../contexts/ScopedRoomContext.tsx"; interface IProps { roomId: string; @@ -273,12 +274,10 @@ class FilePanel extends React.Component { if (this.state.timelineSet) { return ( - { layout={Layout.Group} /> - + ); } else { return ( - + { > - + ); } } diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx index d0460412db..236da25409 100644 --- a/src/components/structures/NotificationPanel.tsx +++ b/src/components/structures/NotificationPanel.tsx @@ -19,6 +19,7 @@ import { Layout } from "../../settings/enums/Layout"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import Measured from "../views/elements/Measured"; import EmptyState from "../views/right_panel/EmptyState"; +import { ScopedRoomContextProvider } from "../../contexts/ScopedRoomContext.tsx"; interface IProps { onClose(): void; @@ -79,12 +80,10 @@ export default class NotificationPanel extends React.PureComponent } {content} - + ); } } diff --git a/src/components/structures/RoomSearchView.tsx b/src/components/structures/RoomSearchView.tsx index de2d9d2198..82146bcc5e 100644 --- a/src/components/structures/RoomSearchView.tsx +++ b/src/components/structures/RoomSearchView.tsx @@ -26,7 +26,7 @@ import ErrorDialog from "../views/dialogs/ErrorDialog"; import ResizeNotifier from "../../utils/ResizeNotifier"; import MatrixClientContext from "../../contexts/MatrixClientContext"; import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; -import RoomContext from "../../contexts/RoomContext"; +import { useScopedRoomContext } from "../../contexts/ScopedRoomContext.tsx"; const DEBUG = false; let debuglog = function (msg: string): void {}; @@ -53,7 +53,7 @@ interface Props { export const RoomSearchView = forwardRef( ({ term, scope, promise, abortController, resizeNotifier, className, onUpdate, inProgress }: Props, ref) => { const client = useContext(MatrixClientContext); - const roomContext = useContext(RoomContext); + const roomContext = useScopedRoomContext("showHiddenEvents"); const [highlights, setHighlights] = useState(null); const [results, setResults] = useState(null); const aborted = useRef(false); diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index c87580490e..86f2acb154 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -9,16 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { - ChangeEvent, - ComponentProps, - createRef, - ReactElement, - ReactNode, - RefObject, - useContext, - JSX, -} from "react"; +import React, { ChangeEvent, ComponentProps, createRef, ReactElement, ReactNode, RefObject, JSX } from "react"; import classNames from "classnames"; import { IRecommendedVersion, @@ -64,7 +55,7 @@ import WidgetEchoStore from "../../stores/WidgetEchoStore"; import SettingsStore from "../../settings/SettingsStore"; import { Layout } from "../../settings/enums/Layout"; import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton"; -import RoomContext, { TimelineRenderingType, MainSplitContentType } from "../../contexts/RoomContext"; +import { TimelineRenderingType, MainSplitContentType } from "../../contexts/RoomContext"; import { E2EStatus, shieldStatusForRoom } from "../../utils/ShieldUtils"; import { Action } from "../../dispatcher/actions"; import { IMatrixClientCreds } from "../../MatrixClientPeg"; @@ -136,6 +127,7 @@ import RightPanelStore from "../../stores/right-panel/RightPanelStore"; import { onView3pidInvite } from "../../stores/right-panel/action-handlers"; import RoomSearchAuxPanel from "../views/rooms/RoomSearchAuxPanel"; import { PinnedMessageBanner } from "../views/rooms/PinnedMessageBanner"; +import { ScopedRoomContextProvider, useScopedRoomContext } from "../../contexts/ScopedRoomContext"; const DEBUG = false; const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000; @@ -261,6 +253,7 @@ interface LocalRoomViewProps { permalinkCreator: RoomPermalinkCreator; roomView: RefObject; onFileDrop: (dataTransfer: DataTransfer) => Promise; + mainSplitContentType: MainSplitContentType; } /** @@ -270,7 +263,7 @@ interface LocalRoomViewProps { * @returns {ReactElement} */ function LocalRoomView(props: LocalRoomViewProps): ReactElement { - const context = useContext(RoomContext); + const context = useScopedRoomContext("room"); const room = context.room as LocalRoom; const encryptionEvent = props.localRoom.currentState.getStateEvents(EventType.RoomEncryption)[0]; let encryptionTile: ReactNode; @@ -338,6 +331,7 @@ interface ILocalRoomCreateLoaderProps { localRoom: LocalRoom; names: string; resizeNotifier: ResizeNotifier; + mainSplitContentType: MainSplitContentType; } /** @@ -2007,35 +2001,41 @@ export class RoomView extends React.Component { if (!this.state.room || !this.context?.client) return null; const names = this.state.room.getDefaultRoomName(this.context.client.getSafeUserId()); return ( - - - + + + ); } private renderLocalRoomView(localRoom: LocalRoom): ReactNode { return ( - + - + ); } private renderWaitingForThirdPartyRoomView(inviteEvent: MatrixEvent): ReactNode { return ( - + - + ); } @@ -2573,7 +2573,7 @@ export class RoomView extends React.Component { } return ( - +
{showChatEffects && this.roomView.current && ( @@ -2600,7 +2600,7 @@ export class RoomView extends React.Component {
-
+ ); } } diff --git a/src/components/structures/ThreadPanel.tsx b/src/components/structures/ThreadPanel.tsx index eb20b05fdc..0e82baa28b 100644 --- a/src/components/structures/ThreadPanel.tsx +++ b/src/components/structures/ThreadPanel.tsx @@ -20,7 +20,7 @@ import MatrixClientContext, { useMatrixClientContext } from "../../contexts/Matr import { _t } from "../../languageHandler"; import { ContextMenuButton } from "../../accessibility/context_menu/ContextMenuButton"; import ContextMenu, { ChevronFace, MenuItemRadio, useContextMenu } from "./ContextMenu"; -import RoomContext, { TimelineRenderingType, useRoomContext } from "../../contexts/RoomContext"; +import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import TimelinePanel from "./TimelinePanel"; import { Layout } from "../../settings/enums/Layout"; import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; @@ -30,6 +30,7 @@ import { ButtonEvent } from "../views/elements/AccessibleButton"; import Spinner from "../views/elements/Spinner"; import { clearRoomNotification } from "../../utils/notifications"; import EmptyState from "../views/right_panel/EmptyState"; +import { ScopedRoomContextProvider, useScopedRoomContext } from "../../contexts/ScopedRoomContext.tsx"; interface IProps { roomId: string; @@ -68,7 +69,7 @@ export const ThreadPanelHeader: React.FC<{ setFilterOption: (filterOption: ThreadFilterType) => void; }> = ({ filterOption, setFilterOption }) => { const mxClient = useMatrixClientContext(); - const roomContext = useRoomContext(); + const roomContext = useScopedRoomContext("room"); const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); const options: readonly ThreadPanelHeaderOption[] = [ { @@ -184,13 +185,11 @@ const ThreadPanel: React.FC = ({ roomId, onClose, permalinkCreator }) => }, [timelineSet, timelinePanel]); return ( - = ({ roomId, onClose, permalinkCreator }) => )} - + ); }; export default ThreadPanel; diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index a4dbd2bfe7..bc1e26d087 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -51,6 +51,7 @@ import { ComposerInsertPayload, ComposerType } from "../../dispatcher/payloads/C import Heading from "../views/typography/Heading"; import { SdkContextClass } from "../../contexts/SDKContext"; import { ThreadPayload } from "../../dispatcher/payloads/ThreadPayload"; +import { ScopedRoomContextProvider } from "../../contexts/ScopedRoomContext.tsx"; interface IProps { room: Room; @@ -422,14 +423,12 @@ export default class ThreadView extends React.Component { } return ( - { /> )} - + ); } } diff --git a/src/components/structures/WaitingForThirdPartyRoomView.tsx b/src/components/structures/WaitingForThirdPartyRoomView.tsx index fd9afc50f2..cabe92a53d 100644 --- a/src/components/structures/WaitingForThirdPartyRoomView.tsx +++ b/src/components/structures/WaitingForThirdPartyRoomView.tsx @@ -9,7 +9,6 @@ Please see LICENSE files in the repository root for full details. import React, { RefObject } from "react"; import { MatrixEvent } from "matrix-js-sdk/src/matrix"; -import { useRoomContext } from "../../contexts/RoomContext"; import ResizeNotifier from "../../utils/ResizeNotifier"; import ErrorBoundary from "../views/elements/ErrorBoundary"; import RoomHeader from "../views/rooms/RoomHeader"; @@ -19,6 +18,7 @@ import NewRoomIntro from "../views/rooms/NewRoomIntro"; import { UnwrappedEventTile } from "../views/rooms/EventTile"; import { _t } from "../../languageHandler"; import SdkConfig from "../../SdkConfig"; +import { useScopedRoomContext } from "../../contexts/ScopedRoomContext.tsx"; interface Props { roomView: RefObject; @@ -32,7 +32,7 @@ interface Props { * To avoid UTDs, users are shown a waiting room until the others have joined. */ export const WaitingForThirdPartyRoomView: React.FC = ({ roomView, resizeNotifier, inviteEvent }) => { - const context = useRoomContext(); + const context = useScopedRoomContext("room"); const brand = SdkConfig.get().brand; return ( diff --git a/src/components/views/avatars/BaseAvatar.tsx b/src/components/views/avatars/BaseAvatar.tsx index db74d7b95e..766eb561ec 100644 --- a/src/components/views/avatars/BaseAvatar.tsx +++ b/src/components/views/avatars/BaseAvatar.tsx @@ -16,10 +16,10 @@ import { Avatar } from "@vector-im/compound-web"; import SettingsStore from "../../../settings/SettingsStore"; import { ButtonEvent } from "../elements/AccessibleButton"; -import RoomContext from "../../../contexts/RoomContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { useTypedEventEmitter } from "../../../hooks/useEventEmitter"; import { _t } from "../../../languageHandler"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; interface IProps { name?: React.ComponentProps["name"]; // The name (first initial used as default) @@ -57,8 +57,8 @@ const calculateUrls = (url?: string | null, urls?: string[], lowBandwidth = fals const useImageUrl = ({ url, urls }: { url?: string | null; urls?: string[] }): [string, () => void] => { // Since this is a hot code path and the settings store can be slow, we // use the cached lowBandwidth value from the room context if it exists - const roomContext = useContext(RoomContext); - const lowBandwidth = roomContext ? roomContext.lowBandwidth : SettingsStore.getValue("lowBandwidth"); + const roomContext = useScopedRoomContext("lowBandwidth"); + const lowBandwidth = roomContext?.lowBandwidth ?? SettingsStore.getValue("lowBandwidth"); const [imageUrls, setUrls] = useState(calculateUrls(url, urls, lowBandwidth)); const [urlsIndex, setIndex] = useState(0); diff --git a/src/components/views/context_menus/WidgetContextMenu.tsx b/src/components/views/context_menus/WidgetContextMenu.tsx index 21239d0fee..46052de9ff 100644 --- a/src/components/views/context_menus/WidgetContextMenu.tsx +++ b/src/components/views/context_menus/WidgetContextMenu.tsx @@ -18,7 +18,6 @@ import { _t } from "../../../languageHandler"; import { isAppWidget } from "../../../stores/WidgetStore"; import WidgetUtils from "../../../utils/WidgetUtils"; import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore"; -import RoomContext from "../../../contexts/RoomContext"; import dis from "../../../dispatcher/dispatcher"; import SettingsStore from "../../../settings/SettingsStore"; import Modal from "../../../Modal"; @@ -30,6 +29,7 @@ import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayo import { getConfigLivestreamUrl, startJitsiAudioLivestream } from "../../../Livestream"; import { ModuleRunner } from "../../../modules/ModuleRunner"; import { ElementWidget } from "../../../stores/widgets/StopGapWidget"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; interface IProps extends Omit, "children"> { app: IWidget; @@ -114,7 +114,7 @@ export const WidgetContextMenu: React.FC = ({ ...props }) => { const cli = useContext(MatrixClientContext); - const { room, roomId } = useContext(RoomContext); + const { room, roomId } = useScopedRoomContext("room", "roomId"); const widgetMessaging = WidgetMessagingStore.instance.getMessagingForUid(WidgetUtils.getWidgetUid(app)); const canModify = userWidget || WidgetUtils.canUserModifyWidgets(cli, roomId); diff --git a/src/components/views/elements/MiniAvatarUploader.tsx b/src/components/views/elements/MiniAvatarUploader.tsx index cf5a239814..452b206bef 100644 --- a/src/components/views/elements/MiniAvatarUploader.tsx +++ b/src/components/views/elements/MiniAvatarUploader.tsx @@ -12,12 +12,12 @@ import React, { useContext, useRef, useState, MouseEvent, ReactNode } from "reac import { Tooltip } from "@vector-im/compound-web"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import RoomContext from "../../../contexts/RoomContext"; import { useTimeout } from "../../../hooks/useTimeout"; import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; import AccessibleButton from "./AccessibleButton"; import Spinner from "./Spinner"; import { getFileChanged } from "../settings/AvatarSetting.tsx"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; export const AVATAR_SIZE = "52px"; @@ -56,7 +56,7 @@ const MiniAvatarUploader: React.FC = ({ const label = hasAvatar || busy ? hasAvatarLabel : noAvatarLabel; - const { room } = useContext(RoomContext); + const { room } = useScopedRoomContext("room"); const canSetAvatar = isUserAvatar || room?.currentState?.maySendStateEvent(EventType.RoomAvatar, cli.getSafeUserId()); if (!canSetAvatar) return {children}; diff --git a/src/components/views/messages/RoomPredecessorTile.tsx b/src/components/views/messages/RoomPredecessorTile.tsx index 2e8633febd..afc8142234 100644 --- a/src/components/views/messages/RoomPredecessorTile.tsx +++ b/src/components/views/messages/RoomPredecessorTile.tsx @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { useCallback, useContext } from "react"; +import React, { useCallback } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import { MatrixEvent, Room, RoomState } from "matrix-js-sdk/src/matrix"; @@ -18,10 +18,10 @@ import { _t } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import EventTileBubble from "./EventTileBubble"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; -import RoomContext from "../../../contexts/RoomContext"; import { useRoomState } from "../../../hooks/useRoomState"; import SettingsStore from "../../../settings/SettingsStore"; import MatrixToPermalinkConstructor from "../../../utils/permalinks/MatrixToPermalinkConstructor"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; interface IProps { /** The m.room.create MatrixEvent that this tile represents */ @@ -40,7 +40,7 @@ export const RoomPredecessorTile: React.FC = ({ mxEvent, timestamp }) => // the information inside mxEvent. This allows us the flexibility later to // use a different predecessor (e.g. through MSC3946) and still display it // in the timeline location of the create event. - const roomContext = useContext(RoomContext); + const roomContext = useScopedRoomContext("room"); const predecessor = useRoomState( roomContext.room, useCallback( diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx index af7106f9c5..d6161e9434 100644 --- a/src/components/views/right_panel/PinnedMessagesCard.tsx +++ b/src/components/views/right_panel/PinnedMessagesCard.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { useCallback, useEffect, JSX } from "react"; +import React, { useCallback, useEffect, JSX, useContext } from "react"; import { Room, MatrixEvent } from "matrix-js-sdk/src/matrix"; import { Button, Separator } from "@vector-im/compound-web"; import classNames from "classnames"; @@ -18,7 +18,7 @@ import Spinner from "../elements/Spinner"; import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; import { PinnedEventTile } from "../rooms/PinnedEventTile"; import { useRoomState } from "../../../hooks/useRoomState"; -import RoomContext, { TimelineRenderingType, useRoomContext } from "../../../contexts/RoomContext"; +import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext"; import { ReadPinsEventId } from "./types"; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import { filterBoolean } from "../../../utils/arrays"; @@ -27,6 +27,7 @@ import { UnpinAllDialog } from "../dialogs/UnpinAllDialog"; import EmptyState from "./EmptyState"; import { usePinnedEvents, useReadPinnedEvents, useSortedFetchedPinnedEvents } from "../../../hooks/usePinnedEvents"; import PinningUtils from "../../../utils/PinningUtils.ts"; +import { ScopedRoomContextProvider } from "../../../contexts/ScopedRoomContext.tsx"; /** * List the pinned messages in a room inside a Card. @@ -48,7 +49,7 @@ interface PinnedMessagesCardProps { export function PinnedMessagesCard({ room, onClose, permalinkCreator }: PinnedMessagesCardProps): JSX.Element { const cli = useMatrixClientContext(); - const roomContext = useRoomContext(); + const roomContext = useContext(RoomContext); const pinnedEventIds = usePinnedEvents(room); const readPinnedEvents = useReadPinnedEvents(room); const pinnedEvents = useSortedFetchedPinnedEvents(room, pinnedEventIds); @@ -89,14 +90,9 @@ export function PinnedMessagesCard({ room, onClose, permalinkCreator }: PinnedMe className="mx_PinnedMessagesCard" onClose={onClose} > - + {content} - + ); } diff --git a/src/components/views/right_panel/RoomSummaryCard.tsx b/src/components/views/right_panel/RoomSummaryCard.tsx index 664977bbe2..396b5aa591 100644 --- a/src/components/views/right_panel/RoomSummaryCard.tsx +++ b/src/components/views/right_panel/RoomSummaryCard.tsx @@ -51,7 +51,7 @@ import ShareDialog from "../dialogs/ShareDialog"; import { useEventEmitterState } from "../../../hooks/useEventEmitter"; import { E2EStatus } from "../../../utils/ShieldUtils"; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; -import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext"; +import { TimelineRenderingType } from "../../../contexts/RoomContext"; import RoomName from "../elements/RoomName"; import ExportDialog from "../dialogs/ExportDialog"; import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; @@ -76,6 +76,7 @@ import { useTransition } from "../../../hooks/useTransition"; import { isVideoRoom as calcIsVideoRoom } from "../../../utils/video-rooms"; import { usePinnedEvents } from "../../../hooks/usePinnedEvents"; import { ReleaseAnnouncement } from "../../structures/ReleaseAnnouncement.tsx"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; interface IProps { room: Room; @@ -232,7 +233,7 @@ const RoomSummaryCard: React.FC = ({ }; const isRoomEncrypted = useIsEncrypted(cli, room); - const roomContext = useContext(RoomContext); + const roomContext = useScopedRoomContext("e2eStatus", "timelineRenderingType"); const e2eStatus = roomContext.e2eStatus; const isVideoRoom = calcIsVideoRoom(room); diff --git a/src/components/views/right_panel/TimelineCard.tsx b/src/components/views/right_panel/TimelineCard.tsx index e745f7a227..f62319f3cd 100644 --- a/src/components/views/right_panel/TimelineCard.tsx +++ b/src/components/views/right_panel/TimelineCard.tsx @@ -38,6 +38,7 @@ import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import Measured from "../elements/Measured"; import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import { SdkContextClass } from "../../../contexts/SDKContext"; +import { ScopedRoomContextProvider } from "../../../contexts/ScopedRoomContext.tsx"; interface IProps { room: Room; @@ -199,13 +200,11 @@ export default class TimelineCard extends React.Component { const showComposer = myMembership === KnownMembership.Join; return ( - { /> )} - + ); } } diff --git a/src/components/views/rooms/HistoryTile.tsx b/src/components/views/rooms/HistoryTile.tsx index c52ab044a7..3aa74b8b0c 100644 --- a/src/components/views/rooms/HistoryTile.tsx +++ b/src/components/views/rooms/HistoryTile.tsx @@ -6,15 +6,15 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { useContext } from "react"; +import React from "react"; import { EventTimeline } from "matrix-js-sdk/src/matrix"; import EventTileBubble from "../messages/EventTileBubble"; -import RoomContext from "../../../contexts/RoomContext"; import { _t } from "../../../languageHandler"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; const HistoryTile: React.FC = () => { - const { room } = useContext(RoomContext); + const { room } = useScopedRoomContext("room"); const oldState = room?.getLiveTimeline().getState(EventTimeline.BACKWARDS); const historyState = oldState?.getStateEvents("m.room.history_visibility")[0]?.getContent().history_visibility; diff --git a/src/components/views/rooms/MessageComposerButtons.tsx b/src/components/views/rooms/MessageComposerButtons.tsx index 65e53148f4..370bc0861c 100644 --- a/src/components/views/rooms/MessageComposerButtons.tsx +++ b/src/components/views/rooms/MessageComposerButtons.tsx @@ -21,7 +21,6 @@ import PollCreateDialog from "../elements/PollCreateDialog"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import ContentMessages from "../../../ContentMessages"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import RoomContext from "../../../contexts/RoomContext"; import { useDispatcher } from "../../../hooks/useDispatcher"; import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; import IconizedContextMenu, { IconizedContextMenuOptionList } from "../context_menus/IconizedContextMenu"; @@ -29,6 +28,7 @@ import { EmojiButton } from "./EmojiButton"; import { filterBoolean } from "../../../utils/arrays"; import { useSettingValue } from "../../../hooks/useSettings"; import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; interface IProps { addEmoji: (emoji: string) => boolean; @@ -54,7 +54,7 @@ export const OverflowMenuContext = createContext(null const MessageComposerButtons: React.FC = (props: IProps) => { const matrixClient = useContext(MatrixClientContext); - const { room, narrow } = useContext(RoomContext); + const { room, narrow } = useScopedRoomContext("room", "narrow"); const isWysiwygLabEnabled = useSettingValue("feature_wysiwyg_composer"); @@ -168,7 +168,7 @@ interface IUploadButtonProps { // We put the file input outside the UploadButton component so that it doesn't get killed when the context menu closes. const UploadButtonContextProvider: React.FC = ({ roomId, relation, children }) => { const cli = useContext(MatrixClientContext); - const roomContext = useContext(RoomContext); + const roomContext = useScopedRoomContext("timelineRenderingType"); const uploadInput = useRef(null); const onUploadClick = (): void => { diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx index 05912c482e..a5577ee372 100644 --- a/src/components/views/rooms/NewRoomIntro.tsx +++ b/src/components/views/rooms/NewRoomIntro.tsx @@ -11,7 +11,6 @@ import { EventType, Room, User, MatrixClient } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import RoomContext from "../../../contexts/RoomContext"; import DMRoomMap from "../../../utils/DMRoomMap"; import { _t, _td, TranslationKey } from "../../../languageHandler"; import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; @@ -30,6 +29,7 @@ import { UIComponent } from "../../../settings/UIFeature"; import { privateShouldBeEncrypted } from "../../../utils/rooms"; import { LocalRoom } from "../../../models/LocalRoom"; import { shouldEncryptRoomWithSingle3rdPartyInvite } from "../../../utils/room/shouldEncryptRoomWithSingle3rdPartyInvite"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; function hasExpectedEncryptionSettings(matrixClient: MatrixClient, room: Room): boolean { const isEncrypted: boolean = matrixClient.isRoomEncrypted(room.roomId); @@ -51,7 +51,7 @@ const determineIntroMessage = (room: Room, encryptedSingle3rdPartyInvite: boolea const NewRoomIntro: React.FC = () => { const cli = useContext(MatrixClientContext); - const { room, roomId } = useContext(RoomContext); + const { room, roomId } = useScopedRoomContext("room", "roomId"); if (!room || !roomId) { throw new Error("Unable to create a NewRoomIntro without room and roomId"); diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index c2642ea733..b0040c45fd 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { useCallback, useContext, useMemo, useState } from "react"; +import React, { useCallback, useMemo, useState } from "react"; import { Body as BodyText, Button, IconButton, Menu, MenuItem, Tooltip } from "@vector-im/compound-web"; import VideoCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/video-call-solid"; import VoiceCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/voice-call"; @@ -48,10 +48,10 @@ import { CallGuestLinkButton } from "./RoomHeader/CallGuestLinkButton"; import { ButtonEvent } from "../elements/AccessibleButton"; import WithPresenceIndicator, { useDmMember } from "../avatars/WithPresenceIndicator"; import { IOOBData } from "../../../stores/ThreepidInviteStore"; -import RoomContext from "../../../contexts/RoomContext"; import { MainSplitContentType } from "../../structures/RoomView"; import defaultDispatcher from "../../../dispatcher/dispatcher.ts"; import { RoomSettingsTab } from "../dialogs/RoomSettingsDialog.tsx"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; export default function RoomHeader({ room, @@ -229,7 +229,7 @@ export default function RoomHeader({ voiceCallButton = undefined; } - const roomContext = useContext(RoomContext); + const roomContext = useScopedRoomContext("mainSplitContentType"); const isVideoRoom = calcIsVideoRoom(room); const showChatButton = isVideoRoom || diff --git a/src/components/views/rooms/ThreadSummary.tsx b/src/components/views/rooms/ThreadSummary.tsx index 4a3032d641..ac23331f66 100644 --- a/src/components/views/rooms/ThreadSummary.tsx +++ b/src/components/views/rooms/ThreadSummary.tsx @@ -16,7 +16,6 @@ import { CardContext } from "../right_panel/context"; import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; import PosthogTrackers from "../../../PosthogTrackers"; import { useTypedEventEmitterState } from "../../../hooks/useEventEmitter"; -import RoomContext from "../../../contexts/RoomContext"; import MemberAvatar from "../avatars/MemberAvatar"; import { Action } from "../../../dispatcher/actions"; import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload"; @@ -24,6 +23,7 @@ import defaultDispatcher from "../../../dispatcher/dispatcher"; import { useUnreadNotifications } from "../../../hooks/useUnreadNotifications"; import { notificationLevelToIndicator } from "../../../utils/notifications"; import { EventPreviewTile, useEventPreview } from "./EventPreview.tsx"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; interface IProps { mxEvent: MatrixEvent; @@ -31,7 +31,7 @@ interface IProps { } const ThreadSummary: React.FC = ({ mxEvent, thread, ...props }) => { - const roomContext = useContext(RoomContext); + const roomContext = useScopedRoomContext("narrow"); const cardContext = useContext(CardContext); const count = useTypedEventEmitterState(thread, ThreadEvent.Update, () => thread.length); const { level } = useUnreadNotifications(thread.room, thread.id); diff --git a/src/components/views/rooms/wysiwyg_composer/components/Emoji.tsx b/src/components/views/rooms/wysiwyg_composer/components/Emoji.tsx index b7a6d65e23..9ab3d210ab 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/Emoji.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/Emoji.tsx @@ -13,14 +13,14 @@ import { EmojiButton } from "../../EmojiButton"; import dis from "../../../../../dispatcher/dispatcher"; import { ComposerInsertPayload } from "../../../../../dispatcher/payloads/ComposerInsertPayload"; import { Action } from "../../../../../dispatcher/actions"; -import { useRoomContext } from "../../../../../contexts/RoomContext"; +import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx"; interface EmojiProps { menuPosition: MenuProps; } export function Emoji({ menuPosition }: EmojiProps): JSX.Element { - const roomContext = useRoomContext(); + const roomContext = useScopedRoomContext("timelineRenderingType"); return ( , ): JSX.Element | null => { - const { room } = useRoomContext(); + const { room } = useScopedRoomContext("room"); const client = useMatrixClientContext(); function handleConfirm(completion: ICompletion): void { diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx index 5b6361a58e..f1e42ce091 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx @@ -19,12 +19,12 @@ import { Editor } from "./Editor"; import { useInputEventProcessor } from "../hooks/useInputEventProcessor"; import { useSetCursorPosition } from "../hooks/useSetCursorPosition"; import { useIsFocused } from "../hooks/useIsFocused"; -import { useRoomContext } from "../../../../../contexts/RoomContext"; import defaultDispatcher from "../../../../../dispatcher/dispatcher"; import { Action } from "../../../../../dispatcher/actions"; import { parsePermalink } from "../../../../../utils/permalinks/Permalinks"; import { isNotNull } from "../../../../../Typeguards"; import { useSettingValue } from "../../../../../hooks/useSettings"; +import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx"; interface WysiwygComposerProps { disabled?: boolean; @@ -56,7 +56,7 @@ export const WysiwygComposer = memo(function WysiwygComposer({ children, eventRelation, }: WysiwygComposerProps) { - const { room } = useRoomContext(); + const { room } = useScopedRoomContext("room"); const autocompleteRef = useRef(null); const inputEventProcessor = useInputEventProcessor(onSend, autocompleteRef, initialContent, eventRelation); diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useEditing.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useEditing.ts index 5d1c3b867e..20f394e8a3 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useEditing.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useEditing.ts @@ -10,10 +10,10 @@ import { ISendEventResponse } from "matrix-js-sdk/src/matrix"; import { useCallback, useState } from "react"; import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext"; -import { useRoomContext } from "../../../../../contexts/RoomContext"; import EditorStateTransfer from "../../../../../utils/EditorStateTransfer"; import { endEditing } from "../utils/editing"; import { editMessage } from "../utils/message"; +import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx"; export function useEditing( editorStateTransfer: EditorStateTransfer, @@ -24,7 +24,7 @@ export function useEditing( editMessage(): Promise; endEditing(): void; } { - const roomContext = useRoomContext(); + const roomContext = useScopedRoomContext("timelineRenderingType"); const mxClient = useMatrixClientContext(); const [isSaveDisabled, setIsSaveDisabled] = useState(true); diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useInitialContent.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useInitialContent.ts index 2e0ddd3ccd..3a3799496b 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useInitialContent.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useInitialContent.ts @@ -10,11 +10,11 @@ import { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; import { useMemo } from "react"; import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext"; -import { useRoomContext } from "../../../../../contexts/RoomContext"; import { parseEvent } from "../../../../../editor/deserialize"; import { CommandPartCreator, Part } from "../../../../../editor/parts"; import SettingsStore from "../../../../../settings/SettingsStore"; import EditorStateTransfer from "../../../../../utils/EditorStateTransfer"; +import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx"; function getFormattedContent(editorStateTransfer: EditorStateTransfer): string { return ( @@ -60,12 +60,12 @@ export function parseEditorStateTransfer( } export function useInitialContent(editorStateTransfer: EditorStateTransfer): string | undefined { - const roomContext = useRoomContext(); + const { room } = useScopedRoomContext("room"); const mxClient = useMatrixClientContext(); return useMemo(() => { - if (editorStateTransfer && roomContext.room && mxClient) { - return parseEditorStateTransfer(editorStateTransfer, roomContext.room, mxClient); + if (editorStateTransfer && room && mxClient) { + return parseEditorStateTransfer(editorStateTransfer, room, mxClient); } - }, [editorStateTransfer, roomContext, mxClient]); + }, [editorStateTransfer, room, mxClient]); } diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts index 8eac63eb36..cab3bdefb8 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts @@ -16,7 +16,6 @@ import { KeyBindingAction } from "../../../../../accessibility/KeyboardShortcuts import { findEditableEvent } from "../../../../../utils/EventUtils"; import dis from "../../../../../dispatcher/dispatcher"; import { Action } from "../../../../../dispatcher/actions"; -import { useRoomContext } from "../../../../../contexts/RoomContext"; import { IRoomState } from "../../../../structures/RoomView"; import { ComposerContextState, useComposerContext } from "../ComposerContext"; import EditorStateTransfer from "../../../../../utils/EditorStateTransfer"; @@ -26,6 +25,7 @@ import { getEventsFromEditorStateTransfer, getEventsFromRoom } from "../utils/ev import { endEditing } from "../utils/editing"; import Autocomplete from "../../Autocomplete"; import { handleClipboardEvent, handleEventWithAutocomplete, isEventToHandleAsClipboardEvent } from "./utils"; +import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx"; export function useInputEventProcessor( onSend: () => void, @@ -33,7 +33,7 @@ export function useInputEventProcessor( initialContent?: string, eventRelation?: IEventRelation, ): (event: WysiwygEvent, composer: Wysiwyg, editor: HTMLElement) => WysiwygEvent | null { - const roomContext = useRoomContext(); + const roomContext = useScopedRoomContext("liveTimeline", "room", "replyToEvent", "timelineRenderingType"); const composerContext = useComposerContext(); const mxClient = useMatrixClientContext(); const isCtrlEnterToSend = useSettingValue("MessageComposerInput.ctrlEnterToSend"); @@ -94,7 +94,7 @@ function handleKeyboardEvent( initialContent: string | undefined, composer: Wysiwyg, editor: HTMLElement, - roomContext: IRoomState, + roomContext: Pick, composerContext: ComposerContextState, mxClient: MatrixClient | undefined, autocompleteRef: React.RefObject, @@ -175,7 +175,7 @@ function dispatchEditEvent( isForward: boolean, editorStateTransfer: EditorStateTransfer | undefined, composerContext: ComposerContextState, - roomContext: IRoomState, + roomContext: Pick, mxClient: MatrixClient, ): boolean { const foundEvents = editorStateTransfer diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts index 742d24fe34..1dc23cc274 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts @@ -16,8 +16,8 @@ import Autocomplete from "../../Autocomplete"; import { handleClipboardEvent, handleEventWithAutocomplete, isEventToHandleAsClipboardEvent } from "./utils"; import { useSuggestion } from "./useSuggestion"; import { isNotNull, isNotUndefined } from "../../../../../Typeguards"; -import { useRoomContext } from "../../../../../contexts/RoomContext"; import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext"; +import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx"; function isDivElement(target: EventTarget): target is HTMLDivElement { return target instanceof HTMLDivElement; @@ -63,7 +63,7 @@ export function usePlainTextListeners( onSelect: (event: SyntheticEvent) => void; suggestion: MappedSuggestion | null; } { - const roomContext = useRoomContext(); + const roomContext = useScopedRoomContext("room", "timelineRenderingType", "replyToEvent"); const mxClient = useMatrixClientContext(); const ref = useRef(null); diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts index e1e34623c8..eb76d77af5 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts @@ -11,20 +11,21 @@ import { RefObject, useCallback, useRef } from "react"; import defaultDispatcher from "../../../../../dispatcher/dispatcher"; import { Action } from "../../../../../dispatcher/actions"; import { ActionPayload } from "../../../../../dispatcher/payloads"; -import { TimelineRenderingType, useRoomContext } from "../../../../../contexts/RoomContext"; +import { TimelineRenderingType } from "../../../../../contexts/RoomContext"; import { useDispatcher } from "../../../../../hooks/useDispatcher"; import { focusComposer } from "./utils"; import { ComposerType } from "../../../../../dispatcher/payloads/ComposerInsertPayload"; import { ComposerFunctions } from "../types"; import { setSelection } from "../utils/selection"; import { useComposerContext } from "../ComposerContext"; +import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx"; export function useWysiwygEditActionHandler( disabled: boolean, composerElement: RefObject, composerFunctions: ComposerFunctions, ): void { - const roomContext = useRoomContext(); + const roomContext = useScopedRoomContext("timelineRenderingType"); const composerContext = useComposerContext(); const timeoutId = useRef(null); diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts index 16e1e608ec..d11f3498fd 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts @@ -11,20 +11,21 @@ import { MutableRefObject, useCallback, useRef } from "react"; import defaultDispatcher from "../../../../../dispatcher/dispatcher"; import { Action } from "../../../../../dispatcher/actions"; import { ActionPayload } from "../../../../../dispatcher/payloads"; -import { TimelineRenderingType, useRoomContext } from "../../../../../contexts/RoomContext"; +import { TimelineRenderingType } from "../../../../../contexts/RoomContext"; import { useDispatcher } from "../../../../../hooks/useDispatcher"; import { focusComposer } from "./utils"; import { ComposerFunctions } from "../types"; import { ComposerType } from "../../../../../dispatcher/payloads/ComposerInsertPayload"; import { useComposerContext } from "../ComposerContext"; import { setSelection } from "../utils/selection"; +import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx"; export function useWysiwygSendActionHandler( disabled: boolean, composerElement: MutableRefObject, composerFunctions: ComposerFunctions, ): void { - const roomContext = useRoomContext(); + const roomContext = useScopedRoomContext("timelineRenderingType"); const composerContext = useComposerContext(); const timeoutId = useRef(null); diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts b/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts index 39317ea88c..3345c9f474 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts @@ -22,7 +22,7 @@ import { isNotNull } from "../../../../../Typeguards"; export function focusComposer( composerElement: MutableRefObject, renderingType: TimelineRenderingType, - roomContext: IRoomState, + roomContext: Pick, timeoutId: MutableRefObject, ): void { if (renderingType === roomContext.timelineRenderingType) { @@ -123,7 +123,7 @@ export function handleEventWithAutocomplete( export function handleClipboardEvent( event: ClipboardEvent | InputEvent, data: DataTransfer | null, - roomContext: IRoomState, + roomContext: Pick, mxClient: MatrixClient, eventRelation?: IEventRelation, ): boolean { diff --git a/src/components/views/rooms/wysiwyg_composer/utils/editing.ts b/src/components/views/rooms/wysiwyg_composer/utils/editing.ts index 58a9e24492..462763b8f4 100644 --- a/src/components/views/rooms/wysiwyg_composer/utils/editing.ts +++ b/src/components/views/rooms/wysiwyg_composer/utils/editing.ts @@ -13,7 +13,7 @@ import dis from "../../../../../dispatcher/dispatcher"; import { Action } from "../../../../../dispatcher/actions"; import EditorStateTransfer from "../../../../../utils/EditorStateTransfer"; -export function endEditing(roomContext: IRoomState): void { +export function endEditing(roomContext: Pick): void { // todo local storage // localStorage.removeItem(this.editorRoomKey); // localStorage.removeItem(this.editorStateKey); diff --git a/src/components/views/rooms/wysiwyg_composer/utils/event.ts b/src/components/views/rooms/wysiwyg_composer/utils/event.ts index 5fd37b3665..45c6b1cac3 100644 --- a/src/components/views/rooms/wysiwyg_composer/utils/event.ts +++ b/src/components/views/rooms/wysiwyg_composer/utils/event.ts @@ -15,7 +15,7 @@ import { ComposerContextState } from "../ComposerContext"; // From EditMessageComposer private get events(): MatrixEvent[] export function getEventsFromEditorStateTransfer( editorStateTransfer: EditorStateTransfer, - roomContext: IRoomState, + roomContext: Pick, mxClient: MatrixClient, ): MatrixEvent[] | undefined { const liveTimelineEvents = roomContext.liveTimeline?.getEvents(); @@ -41,7 +41,7 @@ export function getEventsFromEditorStateTransfer( // From SendMessageComposer private onKeyDown = (event: KeyboardEvent): void export function getEventsFromRoom( composerContext: ComposerContextState, - roomContext: IRoomState, + roomContext: Pick, ): MatrixEvent[] | undefined { const isReplyingToThread = composerContext.eventRelation?.key === THREAD_RELATION_TYPE.name; return roomContext.liveTimeline diff --git a/src/components/views/rooms/wysiwyg_composer/utils/message.ts b/src/components/views/rooms/wysiwyg_composer/utils/message.ts index 44e10e3cc5..b7fca8ecb4 100644 --- a/src/components/views/rooms/wysiwyg_composer/utils/message.ts +++ b/src/components/views/rooms/wysiwyg_composer/utils/message.ts @@ -39,7 +39,7 @@ export interface SendMessageParams { mxClient: MatrixClient; relation?: IEventRelation; replyToEvent?: MatrixEvent; - roomContext: IRoomState; + roomContext: Pick; } export async function sendMessage( @@ -177,7 +177,7 @@ export async function sendMessage( interface EditMessageParams { mxClient: MatrixClient; - roomContext: IRoomState; + roomContext: Pick; editorStateTransfer: EditorStateTransfer; } diff --git a/src/contexts/RoomContext.ts b/src/contexts/RoomContext.ts index 0ec179eca4..4303c46a34 100644 --- a/src/contexts/RoomContext.ts +++ b/src/contexts/RoomContext.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { createContext, useContext } from "react"; +import { createContext } from "react"; import { IRoomState } from "../components/structures/RoomView"; import { Layout } from "../settings/enums/Layout"; @@ -79,6 +79,3 @@ const RoomContext = createContext< }); RoomContext.displayName = "RoomContext"; export default RoomContext; -export function useRoomContext(): IRoomState { - return useContext(RoomContext); -} diff --git a/src/contexts/ScopedRoomContext.tsx b/src/contexts/ScopedRoomContext.tsx new file mode 100644 index 0000000000..1222443d29 --- /dev/null +++ b/src/contexts/ScopedRoomContext.tsx @@ -0,0 +1,78 @@ +/* +Copyright 2024 New Vector Ltd. +Copyright 2019 The Matrix.org Foundation C.I.C. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import { TypedEventEmitter } from "matrix-js-sdk/src/matrix"; +import React, { ContextType, createContext, memo, ReactNode, useContext, useEffect, useRef, useState } from "react"; + +import { objectKeyChanges } from "../utils/objects.ts"; +import { useTypedEventEmitter } from "../hooks/useEventEmitter.ts"; +import RoomContext from "./RoomContext.ts"; + +// React Contexts with frequently changing values (like State where the object reference is changed on every update) +// cause performance issues by triggering a re-render on every component subscribed to that context. +// With ScopedRoomContext we're effectively setting up virtual contexts which are a subset of the overall context object +// and subscribers specify which fields they care about, and they will only be awoken on updates to those specific fields. + +type ContextValue = ContextType; + +export enum NotificationStateEvents { + Update = "update", +} + +type EventHandlerMap> = { + [NotificationStateEvents.Update]: (keys: Array) => void; +}; + +class EfficientContext> extends TypedEventEmitter< + NotificationStateEvents, + EventHandlerMap +> { + public constructor(public state: C) { + super(); + } + + public setState(state: C): void { + const changedKeys = objectKeyChanges(this.state ?? ({} as C), state); + this.state = state; + this.emit(NotificationStateEvents.Update, changedKeys); + } +} + +const ScopedRoomContext = createContext | undefined>(undefined); + +// Uses react memo and leverages splatting the value to ensure that the context is only updated when the state changes (shallow compare) +export const ScopedRoomContextProvider = memo( + ({ children, ...state }: { children: ReactNode } & ContextValue): JSX.Element => { + const contextRef = useRef(new EfficientContext(state)); + useEffect(() => { + contextRef.current.setState(state); + }, [state]); + + // Includes the legacy RoomContext provider for backwards compatibility with class components + return ( + + {children} + + ); + }, +); + +type ScopedRoomContext> = { [key in K[number]]: ContextValue[key] }; + +export function useScopedRoomContext>(...keys: K): ScopedRoomContext { + const context = useContext(ScopedRoomContext); + const [state, setState] = useState>(context?.state ?? ({} as ScopedRoomContext)); + + useTypedEventEmitter(context, NotificationStateEvents.Update, (updatedKeys: K): void => { + if (context?.state && updatedKeys.some((updatedKey) => keys.includes(updatedKey))) { + setState(context.state); + } + }); + + return state; +} diff --git a/src/hooks/room/useRoomMemberProfile.ts b/src/hooks/room/useRoomMemberProfile.ts index 57f72a722e..b8bb44c50d 100644 --- a/src/hooks/room/useRoomMemberProfile.ts +++ b/src/hooks/room/useRoomMemberProfile.ts @@ -7,10 +7,11 @@ Please see LICENSE files in the repository root for full details. */ import { RoomMember } from "matrix-js-sdk/src/matrix"; -import { useContext, useMemo } from "react"; +import { useMemo } from "react"; -import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; +import { TimelineRenderingType } from "../../contexts/RoomContext"; import { useSettingValue } from "../useSettings"; +import { useScopedRoomContext } from "../../contexts/ScopedRoomContext.tsx"; export function useRoomMemberProfile({ userId = "", @@ -21,7 +22,7 @@ export function useRoomMemberProfile({ member?: RoomMember | null; forceHistorical?: boolean; }): RoomMember | undefined | null { - const context = useContext(RoomContext); + const context = useScopedRoomContext("room", "timelineRenderingType"); const useOnlyCurrentProfiles = useSettingValue("useOnlyCurrentProfiles"); const member = useMemo(() => { diff --git a/test/unit-tests/components/structures/MessagePanel-test.tsx b/test/unit-tests/components/structures/MessagePanel-test.tsx index cf44716ba9..dbb83da312 100644 --- a/test/unit-tests/components/structures/MessagePanel-test.tsx +++ b/test/unit-tests/components/structures/MessagePanel-test.tsx @@ -30,6 +30,7 @@ import { import ResizeNotifier from "../../../../src/utils/ResizeNotifier"; import { IRoomState } from "../../../../src/components/structures/RoomView"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; +import { ScopedRoomContextProvider } from "../../../../src/contexts/ScopedRoomContext.tsx"; jest.mock("../../../../src/utils/beacon", () => ({ useBeacon: jest.fn(), @@ -91,9 +92,9 @@ describe("MessagePanel", function () { const getComponent = (props = {}, roomContext: Partial = {}) => ( - + - + ); diff --git a/test/unit-tests/components/structures/ThreadPanel-test.tsx b/test/unit-tests/components/structures/ThreadPanel-test.tsx index 909e972638..20fc708103 100644 --- a/test/unit-tests/components/structures/ThreadPanel-test.tsx +++ b/test/unit-tests/components/structures/ThreadPanel-test.tsx @@ -20,7 +20,6 @@ import { import ThreadPanel, { ThreadFilterType, ThreadPanelHeader } from "../../../../src/components/structures/ThreadPanel"; import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; -import RoomContext from "../../../../src/contexts/RoomContext"; import { _t } from "../../../../src/languageHandler"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks"; @@ -28,6 +27,7 @@ import ResizeNotifier from "../../../../src/utils/ResizeNotifier"; import { createTestClient, getRoomContext, mkRoom, mockPlatformPeg, stubClient } from "../../../test-utils"; import { mkThread } from "../../../test-utils/threads"; import { IRoomState } from "../../../../src/components/structures/RoomView"; +import { ScopedRoomContextProvider } from "../../../../src/contexts/ScopedRoomContext.tsx"; jest.mock("../../../../src/utils/Feedback"); @@ -81,11 +81,11 @@ describe("ThreadPanel", () => { room: mockRoom, } as unknown as IRoomState; const { container } = render( - + undefined} /> - , + , ); fireEvent.click(getByRole(container, "button", { name: "Mark all as read" })); await waitFor(() => @@ -114,8 +114,8 @@ describe("ThreadPanel", () => { const TestThreadPanel = () => ( - @@ -125,7 +125,7 @@ describe("ThreadPanel", () => { resizeNotifier={new ResizeNotifier()} permalinkCreator={new RoomPermalinkCreator(room)} /> - + ); diff --git a/test/unit-tests/components/structures/ThreadView-test.tsx b/test/unit-tests/components/structures/ThreadView-test.tsx index 697fd25181..ee4afff525 100644 --- a/test/unit-tests/components/structures/ThreadView-test.tsx +++ b/test/unit-tests/components/structures/ThreadView-test.tsx @@ -23,7 +23,6 @@ import React, { useState } from "react"; import ThreadView from "../../../../src/components/structures/ThreadView"; import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; -import RoomContext from "../../../../src/contexts/RoomContext"; import { SdkContextClass } from "../../../../src/contexts/SDKContext"; import { Action } from "../../../../src/dispatcher/actions"; import dispatcher from "../../../../src/dispatcher/dispatcher"; @@ -34,6 +33,7 @@ import { mockPlatformPeg } from "../../../test-utils/platform"; import { getRoomContext } from "../../../test-utils/room"; import { mkMessage, stubClient } from "../../../test-utils/test-utils"; import { mkThread } from "../../../test-utils/threads"; +import { ScopedRoomContextProvider } from "../../../../src/contexts/ScopedRoomContext.tsx"; describe("ThreadView", () => { const ROOM_ID = "!roomId:example.org"; @@ -51,8 +51,8 @@ describe("ThreadView", () => { return ( - @@ -63,7 +63,7 @@ describe("ThreadView", () => { initialEvent={initialEvent} resizeNotifier={new ResizeNotifier()} /> - + , ); diff --git a/test/unit-tests/components/views/audio_messages/RecordingPlayback-test.tsx b/test/unit-tests/components/views/audio_messages/RecordingPlayback-test.tsx index 1f193d6883..419357e5c8 100644 --- a/test/unit-tests/components/views/audio_messages/RecordingPlayback-test.tsx +++ b/test/unit-tests/components/views/audio_messages/RecordingPlayback-test.tsx @@ -15,10 +15,11 @@ import RecordingPlayback, { PlaybackLayout, } from "../../../../../src/components/views/audio_messages/RecordingPlayback"; import { Playback } from "../../../../../src/audio/Playback"; -import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext"; +import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext"; import { createAudioContext } from "../../../../../src/audio/compat"; import { flushPromises } from "../../../../test-utils"; import { IRoomState } from "../../../../../src/components/structures/RoomView"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; jest.mock("../../../../../src/WorkerManager", () => ({ WorkerManager: jest.fn(() => ({ @@ -56,9 +57,9 @@ describe("", () => { const defaultRoom = { roomId: "!room:server.org", timelineRenderingType: TimelineRenderingType.File } as IRoomState; const getComponent = (props: React.ComponentProps, room = defaultRoom) => render( - + - , + , ); beforeEach(() => { diff --git a/test/unit-tests/components/views/avatars/MemberAvatar-test.tsx b/test/unit-tests/components/views/avatars/MemberAvatar-test.tsx index eaa3a308d5..1c5bf3a44f 100644 --- a/test/unit-tests/components/views/avatars/MemberAvatar-test.tsx +++ b/test/unit-tests/components/views/avatars/MemberAvatar-test.tsx @@ -12,11 +12,11 @@ import { MatrixClient, PendingEventOrdering, Room, RoomMember } from "matrix-js- import React, { ComponentProps } from "react"; import MemberAvatar from "../../../../../src/components/views/avatars/MemberAvatar"; -import RoomContext from "../../../../../src/contexts/RoomContext"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; import SettingsStore from "../../../../../src/settings/SettingsStore"; import { getRoomContext } from "../../../../test-utils/room"; import { stubClient } from "../../../../test-utils/test-utils"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; describe("MemberAvatar", () => { const ROOM_ID = "roomId"; @@ -27,9 +27,9 @@ describe("MemberAvatar", () => { function getComponent(props: Partial>) { return ( - + - + ); } diff --git a/test/unit-tests/components/views/context_menus/MessageContextMenu-test.tsx b/test/unit-tests/components/views/context_menus/MessageContextMenu-test.tsx index 54e8fa434b..892ca6dbed 100644 --- a/test/unit-tests/components/views/context_menus/MessageContextMenu-test.tsx +++ b/test/unit-tests/components/views/context_menus/MessageContextMenu-test.tsx @@ -27,7 +27,7 @@ import { mocked } from "jest-mock"; import userEvent from "@testing-library/user-event"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; -import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext"; +import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext"; import { IRoomState } from "../../../../../src/components/structures/RoomView"; import { canEditContent } from "../../../../../src/utils/EventUtils"; import { copyPlaintext, getSelectedText } from "../../../../../src/utils/strings"; @@ -40,6 +40,7 @@ import { Action } from "../../../../../src/dispatcher/actions"; import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils"; import { VoiceBroadcastInfoState } from "../../../../../src/voice-broadcast"; import { createMessageEventContent } from "../../../../test-utils/events"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; jest.mock("../../../../../src/utils/strings", () => ({ copyPlaintext: jest.fn(), @@ -546,8 +547,8 @@ function createMenu( client.getRoom = jest.fn().mockReturnValue(room); return render( - + - , + , ); } diff --git a/test/unit-tests/components/views/messages/MFileBody-test.tsx b/test/unit-tests/components/views/messages/MFileBody-test.tsx index 60795babde..e124e3a2ff 100644 --- a/test/unit-tests/components/views/messages/MFileBody-test.tsx +++ b/test/unit-tests/components/views/messages/MFileBody-test.tsx @@ -20,7 +20,8 @@ import { import { MediaEventHelper } from "../../../../../src/utils/MediaEventHelper"; import SettingsStore from "../../../../../src/settings/SettingsStore"; import MFileBody from "../../../../../src/components/views/messages/MFileBody.tsx"; -import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext.ts"; +import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext.ts"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; jest.mock("matrix-encrypt-attachment", () => ({ decryptAttachment: jest.fn(), @@ -72,14 +73,14 @@ describe("", () => { it("should show a download button in file rendering type", async () => { const { container, getByRole } = render( - + - , + , ); expect(getByRole("link", { name: "Download" })).toBeInTheDocument(); diff --git a/test/unit-tests/components/views/messages/MessageActionBar-test.tsx b/test/unit-tests/components/views/messages/MessageActionBar-test.tsx index 8b639eb94e..dda5b348a9 100644 --- a/test/unit-tests/components/views/messages/MessageActionBar-test.tsx +++ b/test/unit-tests/components/views/messages/MessageActionBar-test.tsx @@ -35,6 +35,7 @@ import dispatcher from "../../../../../src/dispatcher/dispatcher"; import SettingsStore from "../../../../../src/settings/SettingsStore"; import { Action } from "../../../../../src/dispatcher/actions"; import PinningUtils from "../../../../../src/utils/PinningUtils"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; jest.mock("../../../../../src/dispatcher/dispatcher"); @@ -117,9 +118,9 @@ describe("", () => { } as unknown as IRoomState; const getComponent = (props = {}, roomContext: Partial = {}) => render( - + - , + , ); beforeEach(() => { diff --git a/test/unit-tests/components/views/messages/RoomPredecessorTile-test.tsx b/test/unit-tests/components/views/messages/RoomPredecessorTile-test.tsx index f666b13b33..3ed7d7eb2b 100644 --- a/test/unit-tests/components/views/messages/RoomPredecessorTile-test.tsx +++ b/test/unit-tests/components/views/messages/RoomPredecessorTile-test.tsx @@ -20,9 +20,9 @@ import { } from "../../../../../src/components/views/messages/RoomPredecessorTile"; import { stubClient, upsertRoomStateEvents } from "../../../../test-utils/test-utils"; import { Action } from "../../../../../src/dispatcher/actions"; -import RoomContext from "../../../../../src/contexts/RoomContext"; import { filterConsole, getRoomContext } from "../../../../test-utils"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; jest.mock("../../../../../src/dispatcher/dispatcher"); @@ -99,9 +99,9 @@ describe("", () => { expect(createEvent).toBeTruthy(); return render( - + - , + , ); } diff --git a/test/unit-tests/components/views/right_panel/RoomSummaryCard-test.tsx b/test/unit-tests/components/views/right_panel/RoomSummaryCard-test.tsx index 4026149f98..a2178466de 100644 --- a/test/unit-tests/components/views/right_panel/RoomSummaryCard-test.tsx +++ b/test/unit-tests/components/views/right_panel/RoomSummaryCard-test.tsx @@ -30,7 +30,8 @@ import { _t } from "../../../../../src/languageHandler"; import { tagRoom } from "../../../../../src/utils/room/tagRoom"; import { DefaultTagID } from "../../../../../src/stores/room-list/models"; import { Action } from "../../../../../src/dispatcher/actions"; -import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext"; +import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; jest.mock("../../../../../src/utils/room/tagRoom"); @@ -172,14 +173,14 @@ describe("", () => { const onSearchChange = jest.fn(); const { rerender } = render( - + - + , ); @@ -188,13 +189,13 @@ describe("", () => { rerender( - + - + , ); expect(screen.getByPlaceholderText("Search messages…")).toHaveValue(""); diff --git a/test/unit-tests/components/views/rooms/EditMessageComposer-test.tsx b/test/unit-tests/components/views/rooms/EditMessageComposer-test.tsx index a34e2e895b..0ef3dd76e5 100644 --- a/test/unit-tests/components/views/rooms/EditMessageComposer-test.tsx +++ b/test/unit-tests/components/views/rooms/EditMessageComposer-test.tsx @@ -27,12 +27,12 @@ import { import DocumentOffset from "../../../../../src/editor/offset"; import SettingsStore from "../../../../../src/settings/SettingsStore"; import EditorStateTransfer from "../../../../../src/utils/EditorStateTransfer"; -import RoomContext from "../../../../../src/contexts/RoomContext"; import { IRoomState } from "../../../../../src/components/structures/RoomView"; import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; import Autocompleter, { IProviderCompletions } from "../../../../../src/autocomplete/Autocompleter"; import NotifProvider from "../../../../../src/autocomplete/NotifProvider"; import DMRoomMap from "../../../../../src/utils/DMRoomMap"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; describe("", () => { const userId = "@alice:server.org"; @@ -79,7 +79,7 @@ describe("", () => { render(, { wrapper: ({ children }) => ( - {children} + {children} ), }); diff --git a/test/unit-tests/components/views/rooms/EventTile-test.tsx b/test/unit-tests/components/views/rooms/EventTile-test.tsx index e57514904a..d41bc9b6f0 100644 --- a/test/unit-tests/components/views/rooms/EventTile-test.tsx +++ b/test/unit-tests/components/views/rooms/EventTile-test.tsx @@ -30,7 +30,7 @@ import { mkEncryptedMatrixEvent } from "matrix-js-sdk/src/testing"; import EventTile, { EventTileProps } from "../../../../../src/components/views/rooms/EventTile"; import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; -import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext"; +import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; import { filterConsole, flushPromises, getRoomContext, mkEvent, mkMessage, stubClient } from "../../../../test-utils"; import { mkThread } from "../../../../test-utils/threads"; @@ -40,6 +40,7 @@ import { Action } from "../../../../../src/dispatcher/actions"; import { IRoomState } from "../../../../../src/components/structures/RoomView"; import PinningUtils from "../../../../../src/utils/PinningUtils"; import { Layout } from "../../../../../src/settings/enums/Layout"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; describe("EventTile", () => { const ROOM_ID = "!roomId:example.org"; @@ -56,13 +57,13 @@ describe("EventTile", () => { }) { return ( - + - + ); } diff --git a/test/unit-tests/components/views/rooms/MessageComposer-test.tsx b/test/unit-tests/components/views/rooms/MessageComposer-test.tsx index 3bd9a6cf62..4ef533091a 100644 --- a/test/unit-tests/components/views/rooms/MessageComposer-test.tsx +++ b/test/unit-tests/components/views/rooms/MessageComposer-test.tsx @@ -24,7 +24,6 @@ import { import MessageComposer from "../../../../../src/components/views/rooms/MessageComposer"; import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; -import RoomContext from "../../../../../src/contexts/RoomContext"; import { IRoomState } from "../../../../../src/components/structures/RoomView"; import ResizeNotifier from "../../../../../src/utils/ResizeNotifier"; import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks"; @@ -40,6 +39,7 @@ import { Action } from "../../../../../src/dispatcher/actions"; import { VoiceBroadcastInfoState, VoiceBroadcastRecording } from "../../../../../src/voice-broadcast"; import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils"; import { SdkContextClass } from "../../../../../src/contexts/SDKContext"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; const openStickerPicker = async (): Promise => { await userEvent.click(screen.getByLabelText("More options")); @@ -512,9 +512,9 @@ function wrapAndRender( const getRawComponent = (props = {}, context = roomContext, client = mockClient) => ( - + - + ); return { diff --git a/test/unit-tests/components/views/rooms/MessageComposerButtons-test.tsx b/test/unit-tests/components/views/rooms/MessageComposerButtons-test.tsx index a66dc0d925..08204350ca 100644 --- a/test/unit-tests/components/views/rooms/MessageComposerButtons-test.tsx +++ b/test/unit-tests/components/views/rooms/MessageComposerButtons-test.tsx @@ -10,11 +10,11 @@ import React from "react"; import { render, screen, waitFor } from "jest-matrix-react"; import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; -import RoomContext from "../../../../../src/contexts/RoomContext"; import { createTestClient, getRoomContext, mkStubRoom } from "../../../../test-utils"; import { IRoomState } from "../../../../../src/components/structures/RoomView"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; import MessageComposerButtons from "../../../../../src/components/views/rooms/MessageComposerButtons"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; describe("MessageComposerButtons", () => { // @ts-ignore - we're deliberately not implementing the whole interface here, but @@ -54,7 +54,7 @@ describe("MessageComposerButtons", () => { return render( - {component} + {component} , ); } diff --git a/test/unit-tests/components/views/rooms/NewRoomIntro-test.tsx b/test/unit-tests/components/views/rooms/NewRoomIntro-test.tsx index 291d28c967..a1cd452610 100644 --- a/test/unit-tests/components/views/rooms/NewRoomIntro-test.tsx +++ b/test/unit-tests/components/views/rooms/NewRoomIntro-test.tsx @@ -13,19 +13,19 @@ import { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; import { LocalRoom } from "../../../../../src/models/LocalRoom"; import { filterConsole, mkRoomMemberJoinEvent, mkThirdPartyInviteEvent, stubClient } from "../../../../test-utils"; -import RoomContext from "../../../../../src/contexts/RoomContext"; import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; import NewRoomIntro from "../../../../../src/components/views/rooms/NewRoomIntro"; import { IRoomState } from "../../../../../src/components/structures/RoomView"; import DMRoomMap from "../../../../../src/utils/DMRoomMap"; import { DirectoryMember } from "../../../../../src/utils/direct-messages"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; const renderNewRoomIntro = (client: MatrixClient, room: Room | LocalRoom) => { render( - + - + , ); }; diff --git a/test/unit-tests/components/views/rooms/SendMessageComposer-test.tsx b/test/unit-tests/components/views/rooms/SendMessageComposer-test.tsx index 9853ddb1ba..c372819ce2 100644 --- a/test/unit-tests/components/views/rooms/SendMessageComposer-test.tsx +++ b/test/unit-tests/components/views/rooms/SendMessageComposer-test.tsx @@ -18,7 +18,7 @@ import SendMessageComposer, { isQuickReaction, } from "../../../../../src/components/views/rooms/SendMessageComposer"; import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; -import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext"; +import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext"; import EditorModel from "../../../../../src/editor/model"; import { createPartCreator } from "../../../editor/mock"; import { createTestClient, mkEvent, mkStubRoom, stubClient } from "../../../../test-utils"; @@ -30,6 +30,7 @@ import { IRoomState, MainSplitContentType } from "../../../../../src/components/ import { mockPlatformPeg } from "../../../../test-utils/platform"; import { doMaybeLocalRoomAction } from "../../../../../src/utils/local-room"; import { addTextToComposer } from "../../../../test-utils/composer"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; jest.mock("../../../../../src/utils/local-room", () => ({ doMaybeLocalRoomAction: jest.fn(), @@ -365,9 +366,9 @@ describe("", () => { }; const getRawComponent = (props = {}, roomContext = defaultRoomContext, client = mockClient) => ( - + - + ); const getComponent = (props = {}, roomContext = defaultRoomContext, client = mockClient) => { diff --git a/test/unit-tests/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx b/test/unit-tests/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx index 5d3c455288..167618e452 100644 --- a/test/unit-tests/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx +++ b/test/unit-tests/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx @@ -11,7 +11,6 @@ import React from "react"; import { fireEvent, render, screen, waitFor } from "jest-matrix-react"; import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext"; -import RoomContext from "../../../../../../src/contexts/RoomContext"; import defaultDispatcher from "../../../../../../src/dispatcher/dispatcher"; import { Action } from "../../../../../../src/dispatcher/actions"; import { flushPromises, mkEvent } from "../../../../../test-utils"; @@ -23,6 +22,7 @@ import { ComposerInsertPayload, ComposerType } from "../../../../../../src/dispa import { ActionPayload } from "../../../../../../src/dispatcher/payloads"; import * as EmojiButton from "../../../../../../src/components/views/rooms/EmojiButton"; import { createMocks } from "./utils"; +import { ScopedRoomContextProvider } from "../../../../../../src/contexts/ScopedRoomContext.tsx"; describe("EditWysiwygComposer", () => { afterEach(() => { @@ -39,9 +39,9 @@ describe("EditWysiwygComposer", () => { ) => { return render( - + - + , ); }; @@ -64,9 +64,9 @@ describe("EditWysiwygComposer", () => { rerender( - + - + , ); @@ -275,10 +275,10 @@ describe("EditWysiwygComposer", () => { ); render( - + - + , ); // Same behavior as in RoomView.tsx diff --git a/test/unit-tests/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx b/test/unit-tests/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx index 27110149f5..89415448b7 100644 --- a/test/unit-tests/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx +++ b/test/unit-tests/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx @@ -11,7 +11,6 @@ import React from "react"; import { act, fireEvent, render, screen, waitFor } from "jest-matrix-react"; import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext"; -import RoomContext from "../../../../../../src/contexts/RoomContext"; import defaultDispatcher from "../../../../../../src/dispatcher/dispatcher"; import { Action } from "../../../../../../src/dispatcher/actions"; import { flushPromises } from "../../../../../test-utils"; @@ -20,6 +19,7 @@ import { aboveLeftOf } from "../../../../../../src/components/structures/Context import { ComposerInsertPayload, ComposerType } from "../../../../../../src/dispatcher/payloads/ComposerInsertPayload"; import { setSelection } from "../../../../../../src/components/views/rooms/wysiwyg_composer/utils/selection"; import { createMocks } from "./utils"; +import { ScopedRoomContextProvider } from "../../../../../../src/contexts/ScopedRoomContext.tsx"; jest.mock("../../../../../../src/components/views/rooms/EmojiButton", () => ({ EmojiButton: ({ addEmoji }: { addEmoji: (emoji: string) => void }) => { @@ -66,7 +66,7 @@ describe("SendWysiwygComposer", () => { ) => { return render( - + { menuPosition={aboveLeftOf({ top: 0, bottom: 0, right: 0 })} placeholder={placeholder} /> - + , ); }; diff --git a/test/unit-tests/components/views/rooms/wysiwyg_composer/components/PlainTextComposer-test.tsx b/test/unit-tests/components/views/rooms/wysiwyg_composer/components/PlainTextComposer-test.tsx index 1ca6e9736c..ae37afe860 100644 --- a/test/unit-tests/components/views/rooms/wysiwyg_composer/components/PlainTextComposer-test.tsx +++ b/test/unit-tests/components/views/rooms/wysiwyg_composer/components/PlainTextComposer-test.tsx @@ -14,7 +14,7 @@ import { PlainTextComposer } from "../../../../../../../src/components/views/roo import * as mockUseSettingsHook from "../../../../../../../src/hooks/useSettings"; import * as mockKeyboard from "../../../../../../../src/Keyboard"; import { createMocks } from "../utils"; -import RoomContext from "../../../../../../../src/contexts/RoomContext"; +import { ScopedRoomContextProvider } from "../../../../../../../src/contexts/ScopedRoomContext.tsx"; describe("PlainTextComposer", () => { const customRender = ( @@ -275,9 +275,9 @@ describe("PlainTextComposer", () => { const { defaultRoomContext } = createMocks(); render( - + - , + , ); expect(screen.getByTestId("autocomplete-wrapper")).toBeInTheDocument(); diff --git a/test/unit-tests/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx b/test/unit-tests/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx index 68ce88ce0c..b0f4bbb5bd 100644 --- a/test/unit-tests/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx +++ b/test/unit-tests/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx @@ -11,12 +11,12 @@ import React, { createRef } from "react"; import { render, screen, waitFor } from "jest-matrix-react"; import MatrixClientContext from "../../../../../../../src/contexts/MatrixClientContext"; -import RoomContext from "../../../../../../../src/contexts/RoomContext"; import { WysiwygAutocomplete } from "../../../../../../../src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete"; import { getRoomContext, mkStubRoom, stubClient } from "../../../../../../test-utils"; import Autocomplete from "../../../../../../../src/components/views/rooms/Autocomplete"; import Autocompleter, { ICompletion } from "../../../../../../../src/autocomplete/Autocompleter"; import AutocompleteProvider from "../../../../../../../src/autocomplete/AutocompleteProvider"; +import { ScopedRoomContextProvider } from "../../../../../../../src/contexts/ScopedRoomContext.tsx"; const mockCompletion: ICompletion[] = [ { @@ -71,7 +71,7 @@ describe("WysiwygAutocomplete", () => { return render( - + { handleAtRoomMention={mockHandleAtRoomMention} {...props} /> - + , ); }; diff --git a/test/unit-tests/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx b/test/unit-tests/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx index adfd1412a7..c3c480eb82 100644 --- a/test/unit-tests/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx +++ b/test/unit-tests/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx @@ -18,7 +18,6 @@ import defaultDispatcher from "../../../../../../../src/dispatcher/dispatcher"; import * as EventUtils from "../../../../../../../src/utils/EventUtils"; import { Action } from "../../../../../../../src/dispatcher/actions"; import MatrixClientContext from "../../../../../../../src/contexts/MatrixClientContext"; -import RoomContext from "../../../../../../../src/contexts/RoomContext"; import { ComposerContext, getDefaultContextValue, @@ -32,20 +31,21 @@ import Autocompleter, { ICompletion } from "../../../../../../../src/autocomplet import AutocompleteProvider from "../../../../../../../src/autocomplete/AutocompleteProvider"; import * as Permalinks from "../../../../../../../src/utils/permalinks/Permalinks"; import { PermalinkParts } from "../../../../../../../src/utils/permalinks/PermalinkConstructor"; +import { ScopedRoomContextProvider } from "../../../../../../../src/contexts/ScopedRoomContext.tsx"; describe("WysiwygComposer", () => { const customRender = (onChange = jest.fn(), onSend = jest.fn(), disabled = false, initialContent?: string) => { const { mockClient, defaultRoomContext } = createMocks(); return render( - + - + , ); }; @@ -523,7 +523,7 @@ describe("WysiwygComposer", () => { ) => { return render( - + @@ -537,7 +537,7 @@ describe("WysiwygComposer", () => { } /> - + , ); }; From 6d8cbf39f5cd403ea14759f0396addf0005906e7 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 2 Dec 2024 11:20:13 +0100 Subject: [PATCH 20/27] Replace `MatrixClient.isRoomEncrypted` by `MatrixClient.CryptoApi.isEncryptionEnabledInRoom` in `EventTile.tsx` (#28510) * Replace `MatrixClient.isRoomEncrypted` by `MatrixClient.CryptoApi.isEncryptionEnabledInRoom` in `EventTile.tsx` * Use `roomContext.isRoomEncrypted` --- src/components/views/rooms/EventTile.tsx | 2 +- test/test-utils/test-utils.ts | 1 + .../views/dialogs/ForwardDialog-test.tsx | 1 - .../components/views/rooms/EventTile-test.tsx | 30 +++++++++++++++++-- 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index d66d90e0e2..8c755f00bd 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -775,7 +775,7 @@ export class UnwrappedEventTile extends React.Component } } - if (MatrixClientPeg.safeGet().isRoomEncrypted(ev.getRoomId()!)) { + if (this.context.isRoomEncrypted) { // else if room is encrypted // and event is being encrypted or is not_sent (Unknown Devices/Network Error) if (ev.status === EventStatus.ENCRYPTING) { diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index 6eae737dff..f9aee512a3 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -135,6 +135,7 @@ export function createTestClient(): MatrixClient { loadSessionBackupPrivateKeyFromSecretStorage: jest.fn(), storeSessionBackupPrivateKey: jest.fn(), getKeyBackupInfo: jest.fn().mockResolvedValue(null), + getEncryptionInfoForEvent: jest.fn().mockResolvedValue(null), }), getPushActionsForEvent: jest.fn(), diff --git a/test/unit-tests/components/views/dialogs/ForwardDialog-test.tsx b/test/unit-tests/components/views/dialogs/ForwardDialog-test.tsx index 6942f76cda..7307417b07 100644 --- a/test/unit-tests/components/views/dialogs/ForwardDialog-test.tsx +++ b/test/unit-tests/components/views/dialogs/ForwardDialog-test.tsx @@ -67,7 +67,6 @@ describe("ForwardDialog", () => { getAccountData: jest.fn().mockReturnValue(accountDataEvent), getPushActionsForEvent: jest.fn(), mxcUrlToHttp: jest.fn().mockReturnValue(""), - isRoomEncrypted: jest.fn().mockReturnValue(false), getProfileInfo: jest.fn().mockResolvedValue({ displayname: "Alice", }), diff --git a/test/unit-tests/components/views/rooms/EventTile-test.tsx b/test/unit-tests/components/views/rooms/EventTile-test.tsx index d41bc9b6f0..93445efbe3 100644 --- a/test/unit-tests/components/views/rooms/EventTile-test.tsx +++ b/test/unit-tests/components/views/rooms/EventTile-test.tsx @@ -71,9 +71,11 @@ describe("EventTile", () => { function getComponent( overrides: Partial = {}, renderingType: TimelineRenderingType = TimelineRenderingType.Room, + roomContext: Partial = {}, ) { const context = getRoomContext(room, { timelineRenderingType: renderingType, + ...roomContext, }); return render(); } @@ -437,8 +439,6 @@ describe("EventTile", () => { }); it("should update the warning when the event is replaced with an unencrypted one", async () => { - jest.spyOn(client, "isRoomEncrypted").mockReturnValue(true); - // we start out with an event from the trusted device mxEvent = await mkEncryptedMatrixEvent({ plainContent: { msgtype: "m.text", body: "msg1" }, @@ -452,7 +452,7 @@ describe("EventTile", () => { shieldReason: null, } as EventEncryptionInfo); - const roomContext = getRoomContext(room, {}); + const roomContext = getRoomContext(room, { isRoomEncrypted: true }); const { container, rerender } = render(); await flushPromises(); @@ -581,4 +581,28 @@ describe("EventTile", () => { }); }); }); + + it("should display the not encrypted status for an unencrypted event when the room becomes encrypted", async () => { + jest.spyOn(client.getCrypto()!, "getEncryptionInfoForEvent").mockResolvedValue({ + shieldColour: EventShieldColour.NONE, + shieldReason: null, + }); + + const { rerender } = getComponent(); + await flushPromises(); + // The room and the event are unencrypted, the tile should not show the not encrypted status + expect(screen.queryByText("Not encrypted")).toBeNull(); + + // The room is now encrypted + rerender( + , + ); + + // The event tile should now show the not encrypted status + await waitFor(() => expect(screen.getByText("Not encrypted")).toBeInTheDocument()); + }); }); From 2099aaa663ce0ac48d17934383000804966fc109 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 2 Dec 2024 10:29:04 +0000 Subject: [PATCH 21/27] Improve coverage Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../components/structures/RoomView-test.tsx | 16 +++ .../structures/SpaceRoomView-test.tsx | 117 ++++++++++++++++++ .../__snapshots__/RoomView-test.tsx.snap | 12 +- 3 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 test/unit-tests/components/structures/SpaceRoomView-test.tsx diff --git a/test/unit-tests/components/structures/RoomView-test.tsx b/test/unit-tests/components/structures/RoomView-test.tsx index cdb1d17532..385204c01b 100644 --- a/test/unit-tests/components/structures/RoomView-test.tsx +++ b/test/unit-tests/components/structures/RoomView-test.tsx @@ -75,6 +75,7 @@ import { ViewRoomErrorPayload } from "../../../../src/dispatcher/payloads/ViewRo import { SearchScope } from "../../../../src/Searching"; import { MEGOLM_ENCRYPTION_ALGORITHM } from "../../../../src/utils/crypto"; import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; +import { ViewUserPayload } from "../../../../src/dispatcher/payloads/ViewUserPayload.ts"; describe("RoomView", () => { let cli: MockedObject; @@ -202,6 +203,21 @@ describe("RoomView", () => { return ref.current!; }; + it("should show member list right panel phase on Action.ViewUser without `payload.member`", async () => { + const spy = jest.spyOn(stores.rightPanelStore, "showOrHidePhase"); + await renderRoomView(false); + + defaultDispatcher.dispatch( + { + action: Action.ViewUser, + member: undefined, + }, + true, + ); + + expect(spy).toHaveBeenCalledWith(RightPanelPhases.MemberList); + }); + it("when there is no room predecessor, getHiddenHighlightCount should return 0", async () => { const instance = await getRoomViewInstance(); expect(instance.getHiddenHighlightCount()).toBe(0); diff --git a/test/unit-tests/components/structures/SpaceRoomView-test.tsx b/test/unit-tests/components/structures/SpaceRoomView-test.tsx new file mode 100644 index 0000000000..fb24603283 --- /dev/null +++ b/test/unit-tests/components/structures/SpaceRoomView-test.tsx @@ -0,0 +1,117 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import React from "react"; +import { mocked, MockedObject } from "jest-mock"; +import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; +import { render, cleanup, screen, fireEvent } from "jest-matrix-react"; + +import { stubClient, mockPlatformPeg, unmockPlatformPeg, withClientContextRenderOptions } from "../../../test-utils"; +import { RightPanelPhases } from "../../../../src/stores/right-panel/RightPanelStorePhases"; +import SpaceRoomView from "../../../../src/components/structures/SpaceRoomView.tsx"; +import ResizeNotifier from "../../../../src/utils/ResizeNotifier.ts"; +import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks.ts"; +import RightPanelStore from "../../../../src/stores/right-panel/RightPanelStore.ts"; +import DMRoomMap from "../../../../src/utils/DMRoomMap.ts"; + +describe("SpaceRoomView", () => { + let cli: MockedObject; + let space: Room; + + beforeEach(() => { + mockPlatformPeg({ reload: () => {} }); + cli = mocked(stubClient()); + + space = new Room(`!space:example.org`, cli, cli.getSafeUserId()); + space.currentState.setStateEvents([ + new MatrixEvent({ + type: "m.room.create", + room_id: space.roomId, + sender: cli.getSafeUserId(), + state_key: "", + content: { + creator: cli.getSafeUserId(), + type: "m.space", + }, + }), + new MatrixEvent({ + type: "m.room.member", + room_id: space.roomId, + sender: cli.getSafeUserId(), + state_key: cli.getSafeUserId(), + content: { + membership: "join", + }, + }), + new MatrixEvent({ + type: "m.room.member", + room_id: space.roomId, + sender: "@userA:server", + state_key: "@userA:server", + content: { + membership: "join", + }, + }), + new MatrixEvent({ + type: "m.room.member", + room_id: space.roomId, + sender: "@userB:server", + state_key: "@userB:server", + content: { + membership: "join", + }, + }), + new MatrixEvent({ + type: "m.room.member", + room_id: space.roomId, + sender: "@userC:server", + state_key: "@userC:server", + content: { + membership: "join", + }, + }), + ]); + space.updateMyMembership("join"); + + DMRoomMap.makeShared(cli); + }); + + afterEach(() => { + unmockPlatformPeg(); + jest.clearAllMocks(); + cleanup(); + }); + + const renderSpaceRoomView = async (): Promise> => { + const resizeNotifier = new ResizeNotifier(); + const permalinkCreator = new RoomPermalinkCreator(space); + + const spaceRoomView = render( + , + withClientContextRenderOptions(cli), + ); + return spaceRoomView; + }; + + describe("SpaceLanding", () => { + it("should show member list right panel phase on members click on landing", async () => { + const spy = jest.spyOn(RightPanelStore.instance, "setCard"); + const { container } = await renderSpaceRoomView(); + + await expect(screen.findByText("Welcome to")).resolves.toBeVisible(); + fireEvent.click(container.querySelector(".mx_FacePile")!); + + expect(spy).toHaveBeenCalledWith({ phase: RightPanelPhases.MemberList }); + }); + }); +}); diff --git a/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap b/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap index eefca12067..1e0ed2248b 100644 --- a/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap +++ b/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap @@ -1302,7 +1302,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo aria-label="Open room settings" aria-live="off" class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61" - data-color="1" + data-color="2" data-testid="avatar-img" data-type="round" role="button" @@ -1329,7 +1329,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo - !5:example.org + !6:example.org @@ -1513,7 +1513,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo aria-label="Open room settings" aria-live="off" class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61" - data-color="1" + data-color="2" data-testid="avatar-img" data-type="round" role="button" @@ -1540,7 +1540,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo - !5:example.org + !6:example.org @@ -1897,7 +1897,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = ` aria-label="Open room settings" aria-live="off" class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61" - data-color="5" + data-color="6" data-testid="avatar-img" data-type="round" role="button" @@ -1924,7 +1924,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = ` - !12:example.org + !13:example.org From 5d72735b1f81fcf39f6a6f0683fa1952c0858d22 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 2 Dec 2024 10:47:38 +0000 Subject: [PATCH 22/27] Fix release-checks to not use reserved name GITHUB_TOKEN Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .github/workflows/release_prepare.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release_prepare.yml b/.github/workflows/release_prepare.yml index 2d04852258..031221041a 100644 --- a/.github/workflows/release_prepare.yml +++ b/.github/workflows/release_prepare.yml @@ -29,7 +29,7 @@ jobs: - element-hq/element-desktop uses: matrix-org/matrix-js-sdk/.github/workflows/release-checks.yml@develop secrets: - GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} with: repository: ${{ matrix.repo }} From d8ebc68aa893f7b546a298dc72c489e616b28bda Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 2 Dec 2024 10:53:27 +0000 Subject: [PATCH 23/27] Remove abandoned Voice Broadcasts labs flag (#28548) * Remove abandoned Voice Broadcasts labs flag Any existing voice broadcasts will be shown as a series of voice messages which will sequence play as normal Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Remove dead code Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update snapshots Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- docs/config.md | 1 - res/css/_components.pcss | 6 - res/css/structures/_UserMenu.pcss | 14 - res/css/views/rooms/_MessageComposer.pcss | 4 - res/css/voice-broadcast/atoms/_LiveBadge.pcss | 23 - .../atoms/_VoiceBroadcastControl.pcss | 28 - .../atoms/_VoiceBroadcastHeader.pcss | 60 -- ...oiceBroadcastRecordingConnectionError.pcss | 18 - .../atoms/_VoiceBroadcastRoomSubtitle.pcss | 14 - .../molecules/_VoiceBroadcastBody.pcss | 75 -- res/themes/dark/css/_dark.pcss | 5 - res/themes/legacy-dark/css/_legacy-dark.pcss | 5 - .../legacy-light/css/_legacy-light.pcss | 5 - res/themes/light/css/_light.pcss | 5 - src/@types/matrix-js-sdk.d.ts | 6 - src/IConfigOptions.ts | 7 - src/LegacyCallHandler.tsx | 11 - src/Notifier.ts | 15 - src/SdkConfig.ts | 4 - src/TextForEvent.tsx | 2 - src/components/structures/MatrixChat.tsx | 12 +- src/components/structures/PipContainer.tsx | 83 +- src/components/structures/UserMenu.tsx | 25 - .../structures/grouper/CreationGrouper.tsx | 6 - .../audio_messages/DevicesContextMenu.tsx | 45 - .../CantStartVoiceMessageBroadcastDialog.tsx | 21 - .../views/dialogs/ConfirmRedactDialog.tsx | 16 +- .../views/dialogs/DevtoolsDialog.tsx | 2 - .../views/messages/MessageActionBar.tsx | 4 +- .../views/messages/MessageEvent.tsx | 5 - .../views/rooms/MessageComposer.tsx | 34 +- .../views/rooms/MessageComposerButtons.tsx | 16 - src/components/views/rooms/RoomTile.tsx | 24 +- .../views/rooms/RoomTileSubtitle.tsx | 14 +- .../tabs/room/RolesRoomSettingsTab.tsx | 3 - src/contexts/SDKContext.ts | 29 - src/events/EventTileFactory.tsx | 22 - src/events/forward/getForwardableEvent.ts | 3 - src/hooks/useAudioDeviceSelection.ts | 76 -- src/i18n/strings/en_EN.json | 47 - src/settings/Settings.tsx | 15 - src/stores/RoomViewStore.tsx | 63 -- src/stores/room-list/MessagePreviewStore.ts | 6 - .../room-list/previews/MessageEventPreview.ts | 4 - .../previews/VoiceBroadcastPreview.ts | 23 - src/stores/widgets/StopGapWidget.ts | 4 - src/utils/EventRenderingUtils.ts | 7 +- src/utils/EventUtils.ts | 5 +- .../audio/VoiceBroadcastRecorder.ts | 181 ---- .../components/VoiceBroadcastBody.tsx | 58 -- .../components/atoms/LiveBadge.tsx | 30 - .../components/atoms/SeekButton.tsx | 25 - .../atoms/VoiceBroadcastControl.tsx | 31 - .../components/atoms/VoiceBroadcastError.tsx | 23 - .../components/atoms/VoiceBroadcastHeader.tsx | 139 --- .../atoms/VoiceBroadcastPlaybackControl.tsx | 51 - ...VoiceBroadcastRecordingConnectionError.tsx | 21 - .../atoms/VoiceBroadcastRoomSubtitle.tsx | 21 - .../ConfirmListenBroadcastStopCurrent.tsx | 38 - .../molecules/VoiceBroadcastPlaybackBody.tsx | 102 -- .../VoiceBroadcastPreRecordingPip.tsx | 82 -- .../molecules/VoiceBroadcastRecordingBody.tsx | 31 - .../molecules/VoiceBroadcastRecordingPip.tsx | 116 --- .../VoiceBroadcastSmallPlaybackBody.tsx | 44 - .../hooks/useCurrentVoiceBroadcastPlayback.ts | 32 - .../useCurrentVoiceBroadcastPreRecording.ts | 29 - .../useCurrentVoiceBroadcastRecording.ts | 28 - .../hooks/useHasRoomLiveVoiceBroadcast.ts | 39 - .../hooks/useVoiceBroadcastPlayback.ts | 90 -- .../hooks/useVoiceBroadcastRecording.tsx | 96 -- src/voice-broadcast/index.ts | 57 -- .../models/VoiceBroadcastPlayback.ts | 651 ------------- .../models/VoiceBroadcastPreRecording.ts | 48 - .../models/VoiceBroadcastRecording.ts | 441 --------- .../stores/VoiceBroadcastPlaybacksStore.ts | 113 --- .../stores/VoiceBroadcastPreRecordingStore.ts | 63 -- .../stores/VoiceBroadcastRecordingsStore.ts | 89 -- src/voice-broadcast/types.ts | 32 - .../utils/VoiceBroadcastChunkEvents.ts | 147 --- .../utils/VoiceBroadcastResumer.ts | 90 -- .../checkVoiceBroadcastPreConditions.tsx | 86 -- .../utils/cleanUpBroadcasts.ts | 20 - .../utils/determineVoiceBroadcastLiveness.ts | 20 - ...rCurrentVoiceBroadcastPlaybackIfStopped.ts | 18 - ...doMaybeSetCurrentVoiceBroadcastPlayback.ts | 56 -- ...RoomLiveVoiceBroadcastFromUserAndDevice.ts | 29 - src/voice-broadcast/utils/getChunkLength.ts | 23 - .../utils/getMaxBroadcastLength.ts | 19 - .../utils/hasRoomLiveVoiceBroadcast.ts | 57 -- .../utils/isRelatedToVoiceBroadcast.ts | 21 - .../utils/isVoiceBroadcastStartedEvent.ts | 17 - .../pauseNonLiveBroadcastFromOtherRoom.ts | 29 - .../utils/retrieveStartedInfoEvent.ts | 37 - .../utils/setUpVoiceBroadcastPreRecording.ts | 43 - ...uldDisplayAsVoiceBroadcastRecordingTile.ts | 25 - ...houldDisplayAsVoiceBroadcastStoppedText.ts | 16 - .../shouldDisplayAsVoiceBroadcastTile.ts | 15 - .../utils/showCantStartACallDialog.tsx | 21 - .../utils/startNewVoiceBroadcastRecording.ts | 92 -- .../textForVoiceBroadcastStoppedEvent.tsx | 41 - ...orVoiceBroadcastStoppedEventWithoutLink.ts | 23 - test/unit-tests/LegacyCallHandler-test.ts | 54 -- test/unit-tests/Notifier-test.ts | 43 +- test/unit-tests/SdkConfig-test.ts | 6 - test/unit-tests/TestSdkContext.ts | 8 - .../LegacyCallHandler-test.ts.snap | 24 - .../components/structures/MatrixChat-test.tsx | 12 - .../structures/PipContainer-test.tsx | 201 +--- .../components/structures/UserMenu-test.tsx | 75 +- .../__snapshots__/UserMenu-test.tsx.snap | 33 - .../context_menus/MessageContextMenu-test.tsx | 13 - .../dialogs/ConfirmRedactDialog-test.tsx | 54 +- .../DevtoolsDialog-test.tsx.snap | 27 - .../views/messages/MessageEvent-test.tsx | 26 - .../views/rooms/MessageComposer-test.tsx | 51 +- .../rooms/MessageComposerButtons-test.tsx | 23 - .../components/views/rooms/RoomTile-test.tsx | 69 +- .../tabs/room/RolesRoomSettingsTab-test.tsx | 29 - test/unit-tests/contexts/SdkContext-test.ts | 9 - .../events/EventTileFactory-test.ts | 70 +- test/unit-tests/stores/RoomViewStore-test.ts | 90 -- .../__snapshots__/RoomViewStore-test.ts.snap | 23 - .../previews/MessageEventPreview-test.ts | 13 - .../previews/VoiceBroadcastPreview-test.ts | 54 -- .../stores/widgets/StopGapWidget-test.ts | 38 - .../utils/EventRenderingUtils-test.ts | 46 - test/unit-tests/utils/EventUtils-test.ts | 18 - .../audio/VoiceBroadcastRecorder-test.ts | 251 ----- .../components/VoiceBroadcastBody-test.tsx | 171 ---- .../components/atoms/LiveBadge-test.tsx | 24 - .../atoms/VoiceBroadcastControl-test.tsx | 44 - .../atoms/VoiceBroadcastHeader-test.tsx | 89 -- .../VoiceBroadcastPlaybackControl-test.tsx | 51 - .../__snapshots__/LiveBadge-test.tsx.snap | 27 - .../VoiceBroadcastControl-test.tsx.snap | 16 - .../VoiceBroadcastHeader-test.tsx.snap | 277 ------ ...oiceBroadcastPlaybackControl-test.tsx.snap | 97 -- .../VoiceBroadcastPlaybackBody-test.tsx | 249 ----- .../VoiceBroadcastPreRecordingPip-test.tsx | 163 ---- .../VoiceBroadcastRecordingBody-test.tsx | 79 -- .../VoiceBroadcastRecordingPip-test.tsx | 217 ----- .../VoiceBroadcastSmallPlaybackBody-test.tsx | 129 --- .../VoiceBroadcastPlaybackBody-test.tsx.snap | 914 ------------------ ...oiceBroadcastPreRecordingPip-test.tsx.snap | 98 -- .../VoiceBroadcastRecordingBody-test.tsx.snap | 131 --- .../VoiceBroadcastRecordingPip-test.tsx.snap | 238 ----- ...ceBroadcastSmallPlaybackBody-test.tsx.snap | 558 ----------- .../models/VoiceBroadcastPlayback-test.tsx | 747 -------------- .../models/VoiceBroadcastPreRecording-test.ts | 68 -- .../models/VoiceBroadcastRecording-test.ts | 660 ------------- .../VoiceBroadcastPlaybacksStore-test.ts | 162 ---- .../VoiceBroadcastPreRecordingStore-test.ts | 130 --- .../VoiceBroadcastRecordingsStore-test.ts | 167 ---- .../utils/VoiceBroadcastChunkEvents-test.ts | 142 --- .../utils/VoiceBroadcastResumer-test.ts | 179 ---- ...tUpVoiceBroadcastPreRecording-test.ts.snap | 24 - ...artNewVoiceBroadcastRecording-test.ts.snap | 116 --- ...orVoiceBroadcastStoppedEvent-test.tsx.snap | 42 - .../utils/cleanUpBroadcasts-test.ts | 51 - .../determineVoiceBroadcastLiveness-test.ts | 33 - ...iveVoiceBroadcastFromUserAndDevice-test.ts | 116 --- .../utils/getChunkLength-test.ts | 63 -- .../utils/getMaxBroadcastLength-test.ts | 43 - .../utils/hasRoomLiveVoiceBroadcast-test.ts | 195 ---- .../utils/isRelatedToVoiceBroadcast-test.ts | 109 --- ...pauseNonLiveBroadcastFromOtherRoom-test.ts | 98 -- .../utils/retrieveStartedInfoEvent-test.ts | 81 -- .../setUpVoiceBroadcastPreRecording-test.ts | 123 --- ...splayAsVoiceBroadcastRecordingTile-test.ts | 72 -- .../shouldDisplayAsVoiceBroadcastTile-test.ts | 138 --- .../startNewVoiceBroadcastRecording-test.ts | 234 ----- .../voice-broadcast/utils/test-utils.ts | 131 --- ...textForVoiceBroadcastStoppedEvent-test.tsx | 90 -- ...ceBroadcastStoppedEventWithoutLink-test.ts | 47 - 174 files changed, 29 insertions(+), 13632 deletions(-) delete mode 100644 res/css/voice-broadcast/atoms/_LiveBadge.pcss delete mode 100644 res/css/voice-broadcast/atoms/_VoiceBroadcastControl.pcss delete mode 100644 res/css/voice-broadcast/atoms/_VoiceBroadcastHeader.pcss delete mode 100644 res/css/voice-broadcast/atoms/_VoiceBroadcastRecordingConnectionError.pcss delete mode 100644 res/css/voice-broadcast/atoms/_VoiceBroadcastRoomSubtitle.pcss delete mode 100644 res/css/voice-broadcast/molecules/_VoiceBroadcastBody.pcss delete mode 100644 src/components/views/audio_messages/DevicesContextMenu.tsx delete mode 100644 src/components/views/dialogs/CantStartVoiceMessageBroadcastDialog.tsx delete mode 100644 src/hooks/useAudioDeviceSelection.ts delete mode 100644 src/stores/room-list/previews/VoiceBroadcastPreview.ts delete mode 100644 src/voice-broadcast/audio/VoiceBroadcastRecorder.ts delete mode 100644 src/voice-broadcast/components/VoiceBroadcastBody.tsx delete mode 100644 src/voice-broadcast/components/atoms/LiveBadge.tsx delete mode 100644 src/voice-broadcast/components/atoms/SeekButton.tsx delete mode 100644 src/voice-broadcast/components/atoms/VoiceBroadcastControl.tsx delete mode 100644 src/voice-broadcast/components/atoms/VoiceBroadcastError.tsx delete mode 100644 src/voice-broadcast/components/atoms/VoiceBroadcastHeader.tsx delete mode 100644 src/voice-broadcast/components/atoms/VoiceBroadcastPlaybackControl.tsx delete mode 100644 src/voice-broadcast/components/atoms/VoiceBroadcastRecordingConnectionError.tsx delete mode 100644 src/voice-broadcast/components/atoms/VoiceBroadcastRoomSubtitle.tsx delete mode 100644 src/voice-broadcast/components/molecules/ConfirmListenBroadcastStopCurrent.tsx delete mode 100644 src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx delete mode 100644 src/voice-broadcast/components/molecules/VoiceBroadcastPreRecordingPip.tsx delete mode 100644 src/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody.tsx delete mode 100644 src/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip.tsx delete mode 100644 src/voice-broadcast/components/molecules/VoiceBroadcastSmallPlaybackBody.tsx delete mode 100644 src/voice-broadcast/hooks/useCurrentVoiceBroadcastPlayback.ts delete mode 100644 src/voice-broadcast/hooks/useCurrentVoiceBroadcastPreRecording.ts delete mode 100644 src/voice-broadcast/hooks/useCurrentVoiceBroadcastRecording.ts delete mode 100644 src/voice-broadcast/hooks/useHasRoomLiveVoiceBroadcast.ts delete mode 100644 src/voice-broadcast/hooks/useVoiceBroadcastPlayback.ts delete mode 100644 src/voice-broadcast/hooks/useVoiceBroadcastRecording.tsx delete mode 100644 src/voice-broadcast/index.ts delete mode 100644 src/voice-broadcast/models/VoiceBroadcastPlayback.ts delete mode 100644 src/voice-broadcast/models/VoiceBroadcastPreRecording.ts delete mode 100644 src/voice-broadcast/models/VoiceBroadcastRecording.ts delete mode 100644 src/voice-broadcast/stores/VoiceBroadcastPlaybacksStore.ts delete mode 100644 src/voice-broadcast/stores/VoiceBroadcastPreRecordingStore.ts delete mode 100644 src/voice-broadcast/stores/VoiceBroadcastRecordingsStore.ts delete mode 100644 src/voice-broadcast/types.ts delete mode 100644 src/voice-broadcast/utils/VoiceBroadcastChunkEvents.ts delete mode 100644 src/voice-broadcast/utils/VoiceBroadcastResumer.ts delete mode 100644 src/voice-broadcast/utils/checkVoiceBroadcastPreConditions.tsx delete mode 100644 src/voice-broadcast/utils/cleanUpBroadcasts.ts delete mode 100644 src/voice-broadcast/utils/determineVoiceBroadcastLiveness.ts delete mode 100644 src/voice-broadcast/utils/doClearCurrentVoiceBroadcastPlaybackIfStopped.ts delete mode 100644 src/voice-broadcast/utils/doMaybeSetCurrentVoiceBroadcastPlayback.ts delete mode 100644 src/voice-broadcast/utils/findRoomLiveVoiceBroadcastFromUserAndDevice.ts delete mode 100644 src/voice-broadcast/utils/getChunkLength.ts delete mode 100644 src/voice-broadcast/utils/getMaxBroadcastLength.ts delete mode 100644 src/voice-broadcast/utils/hasRoomLiveVoiceBroadcast.ts delete mode 100644 src/voice-broadcast/utils/isRelatedToVoiceBroadcast.ts delete mode 100644 src/voice-broadcast/utils/isVoiceBroadcastStartedEvent.ts delete mode 100644 src/voice-broadcast/utils/pauseNonLiveBroadcastFromOtherRoom.ts delete mode 100644 src/voice-broadcast/utils/retrieveStartedInfoEvent.ts delete mode 100644 src/voice-broadcast/utils/setUpVoiceBroadcastPreRecording.ts delete mode 100644 src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastRecordingTile.ts delete mode 100644 src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastStoppedText.ts delete mode 100644 src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastTile.ts delete mode 100644 src/voice-broadcast/utils/showCantStartACallDialog.tsx delete mode 100644 src/voice-broadcast/utils/startNewVoiceBroadcastRecording.ts delete mode 100644 src/voice-broadcast/utils/textForVoiceBroadcastStoppedEvent.tsx delete mode 100644 src/voice-broadcast/utils/textForVoiceBroadcastStoppedEventWithoutLink.ts delete mode 100644 test/unit-tests/__snapshots__/LegacyCallHandler-test.ts.snap delete mode 100644 test/unit-tests/components/structures/__snapshots__/UserMenu-test.tsx.snap delete mode 100644 test/unit-tests/stores/room-list/previews/VoiceBroadcastPreview-test.ts delete mode 100644 test/unit-tests/utils/EventRenderingUtils-test.ts delete mode 100644 test/unit-tests/voice-broadcast/audio/VoiceBroadcastRecorder-test.ts delete mode 100644 test/unit-tests/voice-broadcast/components/VoiceBroadcastBody-test.tsx delete mode 100644 test/unit-tests/voice-broadcast/components/atoms/LiveBadge-test.tsx delete mode 100644 test/unit-tests/voice-broadcast/components/atoms/VoiceBroadcastControl-test.tsx delete mode 100644 test/unit-tests/voice-broadcast/components/atoms/VoiceBroadcastHeader-test.tsx delete mode 100644 test/unit-tests/voice-broadcast/components/atoms/VoiceBroadcastPlaybackControl-test.tsx delete mode 100644 test/unit-tests/voice-broadcast/components/atoms/__snapshots__/LiveBadge-test.tsx.snap delete mode 100644 test/unit-tests/voice-broadcast/components/atoms/__snapshots__/VoiceBroadcastControl-test.tsx.snap delete mode 100644 test/unit-tests/voice-broadcast/components/atoms/__snapshots__/VoiceBroadcastHeader-test.tsx.snap delete mode 100644 test/unit-tests/voice-broadcast/components/atoms/__snapshots__/VoiceBroadcastPlaybackControl-test.tsx.snap delete mode 100644 test/unit-tests/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody-test.tsx delete mode 100644 test/unit-tests/voice-broadcast/components/molecules/VoiceBroadcastPreRecordingPip-test.tsx delete mode 100644 test/unit-tests/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody-test.tsx delete mode 100644 test/unit-tests/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip-test.tsx delete mode 100644 test/unit-tests/voice-broadcast/components/molecules/VoiceBroadcastSmallPlaybackBody-test.tsx delete mode 100644 test/unit-tests/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastPlaybackBody-test.tsx.snap delete mode 100644 test/unit-tests/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastPreRecordingPip-test.tsx.snap delete mode 100644 test/unit-tests/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastRecordingBody-test.tsx.snap delete mode 100644 test/unit-tests/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastRecordingPip-test.tsx.snap delete mode 100644 test/unit-tests/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastSmallPlaybackBody-test.tsx.snap delete mode 100644 test/unit-tests/voice-broadcast/models/VoiceBroadcastPlayback-test.tsx delete mode 100644 test/unit-tests/voice-broadcast/models/VoiceBroadcastPreRecording-test.ts delete mode 100644 test/unit-tests/voice-broadcast/models/VoiceBroadcastRecording-test.ts delete mode 100644 test/unit-tests/voice-broadcast/stores/VoiceBroadcastPlaybacksStore-test.ts delete mode 100644 test/unit-tests/voice-broadcast/stores/VoiceBroadcastPreRecordingStore-test.ts delete mode 100644 test/unit-tests/voice-broadcast/stores/VoiceBroadcastRecordingsStore-test.ts delete mode 100644 test/unit-tests/voice-broadcast/utils/VoiceBroadcastChunkEvents-test.ts delete mode 100644 test/unit-tests/voice-broadcast/utils/VoiceBroadcastResumer-test.ts delete mode 100644 test/unit-tests/voice-broadcast/utils/__snapshots__/setUpVoiceBroadcastPreRecording-test.ts.snap delete mode 100644 test/unit-tests/voice-broadcast/utils/__snapshots__/startNewVoiceBroadcastRecording-test.ts.snap delete mode 100644 test/unit-tests/voice-broadcast/utils/__snapshots__/textForVoiceBroadcastStoppedEvent-test.tsx.snap delete mode 100644 test/unit-tests/voice-broadcast/utils/cleanUpBroadcasts-test.ts delete mode 100644 test/unit-tests/voice-broadcast/utils/determineVoiceBroadcastLiveness-test.ts delete mode 100644 test/unit-tests/voice-broadcast/utils/findRoomLiveVoiceBroadcastFromUserAndDevice-test.ts delete mode 100644 test/unit-tests/voice-broadcast/utils/getChunkLength-test.ts delete mode 100644 test/unit-tests/voice-broadcast/utils/getMaxBroadcastLength-test.ts delete mode 100644 test/unit-tests/voice-broadcast/utils/hasRoomLiveVoiceBroadcast-test.ts delete mode 100644 test/unit-tests/voice-broadcast/utils/isRelatedToVoiceBroadcast-test.ts delete mode 100644 test/unit-tests/voice-broadcast/utils/pauseNonLiveBroadcastFromOtherRoom-test.ts delete mode 100644 test/unit-tests/voice-broadcast/utils/retrieveStartedInfoEvent-test.ts delete mode 100644 test/unit-tests/voice-broadcast/utils/setUpVoiceBroadcastPreRecording-test.ts delete mode 100644 test/unit-tests/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastRecordingTile-test.ts delete mode 100644 test/unit-tests/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastTile-test.ts delete mode 100644 test/unit-tests/voice-broadcast/utils/startNewVoiceBroadcastRecording-test.ts delete mode 100644 test/unit-tests/voice-broadcast/utils/test-utils.ts delete mode 100644 test/unit-tests/voice-broadcast/utils/textForVoiceBroadcastStoppedEvent-test.tsx delete mode 100644 test/unit-tests/voice-broadcast/utils/textForVoiceBroadcastStoppedEventWithoutLink-test.ts diff --git a/docs/config.md b/docs/config.md index 7a5445f303..8ca4ba4eb8 100644 --- a/docs/config.md +++ b/docs/config.md @@ -592,4 +592,3 @@ The following are undocumented or intended for developer use only. 2. `sync_timeline_limit` 3. `dangerously_allow_unsafe_and_insecure_passwords` 4. `latex_maths_delims`: An optional setting to override the default delimiters used for maths parsing. See https://github.com/matrix-org/matrix-react-sdk/pull/5939 for details. Only used when `feature_latex_maths` is enabled. -5. `voice_broadcast.chunk_length`: Target chunk length in seconds for the Voice Broadcast feature currently under development. diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 0fcdf6dee6..e9a53cd43c 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -393,9 +393,3 @@ @import "./views/voip/_LegacyCallViewHeader.pcss"; @import "./views/voip/_LegacyCallViewSidebar.pcss"; @import "./views/voip/_VideoFeed.pcss"; -@import "./voice-broadcast/atoms/_LiveBadge.pcss"; -@import "./voice-broadcast/atoms/_VoiceBroadcastControl.pcss"; -@import "./voice-broadcast/atoms/_VoiceBroadcastHeader.pcss"; -@import "./voice-broadcast/atoms/_VoiceBroadcastRecordingConnectionError.pcss"; -@import "./voice-broadcast/atoms/_VoiceBroadcastRoomSubtitle.pcss"; -@import "./voice-broadcast/molecules/_VoiceBroadcastBody.pcss"; diff --git a/res/css/structures/_UserMenu.pcss b/res/css/structures/_UserMenu.pcss index 741a4e90dc..d24a6e4ac7 100644 --- a/res/css/structures/_UserMenu.pcss +++ b/res/css/structures/_UserMenu.pcss @@ -22,20 +22,6 @@ Please see LICENSE files in the repository root for full details. pointer-events: none; /* makes the avatar non-draggable */ } } - - .mx_UserMenu_userAvatarLive { - align-items: center; - background-color: $alert; - border-radius: 6px; - color: $live-badge-color; - display: flex; - height: 12px; - justify-content: center; - left: 25px; - position: absolute; - top: 20px; - width: 12px; - } } .mx_UserMenu_contextMenuButton { diff --git a/res/css/views/rooms/_MessageComposer.pcss b/res/css/views/rooms/_MessageComposer.pcss index 3f11e9fa6c..73ac15c9c9 100644 --- a/res/css/views/rooms/_MessageComposer.pcss +++ b/res/css/views/rooms/_MessageComposer.pcss @@ -256,10 +256,6 @@ Please see LICENSE files in the repository root for full details. mask-image: url("@vector-im/compound-design-tokens/icons/mic-on-solid.svg"); } -.mx_MessageComposer_voiceBroadcast::before { - mask-image: url("$(res)/img/element-icons/live.svg"); -} - .mx_MessageComposer_plain_text::before { mask-image: url("$(res)/img/element-icons/room/composer/plain_text.svg"); } diff --git a/res/css/voice-broadcast/atoms/_LiveBadge.pcss b/res/css/voice-broadcast/atoms/_LiveBadge.pcss deleted file mode 100644 index 7d5f23819b..0000000000 --- a/res/css/voice-broadcast/atoms/_LiveBadge.pcss +++ /dev/null @@ -1,23 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -.mx_LiveBadge { - align-items: center; - background-color: $alert; - border-radius: 2px; - color: $live-badge-color; - display: inline-flex; - font-size: $font-12px; - font-weight: var(--cpd-font-weight-semibold); - gap: $spacing-4; - padding: 2px 4px; -} - -.mx_LiveBadge--grey { - background-color: $quaternary-content; -} diff --git a/res/css/voice-broadcast/atoms/_VoiceBroadcastControl.pcss b/res/css/voice-broadcast/atoms/_VoiceBroadcastControl.pcss deleted file mode 100644 index 5bd7bfe098..0000000000 --- a/res/css/voice-broadcast/atoms/_VoiceBroadcastControl.pcss +++ /dev/null @@ -1,28 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -.mx_VoiceBroadcastControl { - align-items: center; - background-color: $background; - border-radius: 50%; - color: $secondary-content; - display: flex; - flex: 0 0 32px; - height: 32px; - justify-content: center; - width: 32px; -} - -.mx_VoiceBroadcastControl-recording { - color: $alert; -} - -.mx_VoiceBroadcastControl-play .mx_Icon { - left: 1px; - position: relative; -} diff --git a/res/css/voice-broadcast/atoms/_VoiceBroadcastHeader.pcss b/res/css/voice-broadcast/atoms/_VoiceBroadcastHeader.pcss deleted file mode 100644 index c5e21233b7..0000000000 --- a/res/css/voice-broadcast/atoms/_VoiceBroadcastHeader.pcss +++ /dev/null @@ -1,60 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -.mx_VoiceBroadcastHeader { - align-items: flex-start; - display: flex; - gap: $spacing-8; - line-height: 20px; - margin-bottom: $spacing-16; - min-width: 0; -} - -.mx_VoiceBroadcastHeader_content { - flex-grow: 1; - min-width: 0; -} - -.mx_VoiceBroadcastHeader_room_wrapper { - align-items: center; - display: flex; - gap: 4px; - justify-content: flex-start; -} - -.mx_VoiceBroadcastHeader_room { - font-size: $font-12px; - font-weight: var(--cpd-font-weight-semibold); - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.mx_VoiceBroadcastHeader_line { - align-items: center; - color: $secondary-content; - font-size: $font-12px; - display: flex; - gap: $spacing-4; - - .mx_Spinner { - flex: 0 0 14px; - padding: 1px; - } - - span { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } -} - -.mx_VoiceBroadcastHeader_mic--clickable { - cursor: pointer; -} diff --git a/res/css/voice-broadcast/atoms/_VoiceBroadcastRecordingConnectionError.pcss b/res/css/voice-broadcast/atoms/_VoiceBroadcastRecordingConnectionError.pcss deleted file mode 100644 index f21c0bb733..0000000000 --- a/res/css/voice-broadcast/atoms/_VoiceBroadcastRecordingConnectionError.pcss +++ /dev/null @@ -1,18 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -.mx_VoiceBroadcastRecordingConnectionError { - align-items: center; - color: $alert; - display: flex; - gap: $spacing-12; - - svg path { - fill: $alert; - } -} diff --git a/res/css/voice-broadcast/atoms/_VoiceBroadcastRoomSubtitle.pcss b/res/css/voice-broadcast/atoms/_VoiceBroadcastRoomSubtitle.pcss deleted file mode 100644 index e0748e7626..0000000000 --- a/res/css/voice-broadcast/atoms/_VoiceBroadcastRoomSubtitle.pcss +++ /dev/null @@ -1,14 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -.mx_RoomTile .mx_RoomTile_titleContainer .mx_RoomTile_subtitle.mx_RoomTile_subtitle--voice-broadcast { - align-items: center; - color: $alert; - display: flex; - gap: $spacing-4; -} diff --git a/res/css/voice-broadcast/molecules/_VoiceBroadcastBody.pcss b/res/css/voice-broadcast/molecules/_VoiceBroadcastBody.pcss deleted file mode 100644 index 45ed0e98f9..0000000000 --- a/res/css/voice-broadcast/molecules/_VoiceBroadcastBody.pcss +++ /dev/null @@ -1,75 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -.mx_VoiceBroadcastBody { - background-color: $quinary-content; - border-radius: 8px; - color: $secondary-content; - display: inline-block; - font-size: $font-12px; - padding: $spacing-12; - width: 271px; - - .mx_Clock { - line-height: 1; - } -} - -.mx_VoiceBroadcastBody--pip { - background-color: $system; - box-shadow: 0 2px 8px 0 #0000004a; -} - -.mx_VoiceBroadcastBody--small { - display: flex; - gap: $spacing-8; - width: 192px; - - .mx_VoiceBroadcastHeader { - margin-bottom: 0; - } - - .mx_VoiceBroadcastControl { - align-self: center; - } - - .mx_LiveBadge { - margin-top: 4px; - } -} - -.mx_VoiceBroadcastBody_divider { - background-color: $quinary-content; - border: 0; - height: 1px; - margin: $spacing-12 0; -} - -.mx_VoiceBroadcastBody_controls { - align-items: center; - display: flex; - gap: $spacing-32; - justify-content: center; - margin-bottom: $spacing-8; -} - -.mx_VoiceBroadcastBody_timerow { - display: flex; - justify-content: space-between; -} - -.mx_AccessibleButton.mx_VoiceBroadcastBody_blockButton { - display: flex; - gap: $spacing-8; -} - -.mx_VoiceBroadcastBody__small-close { - right: 8px; - position: absolute; - top: 8px; -} diff --git a/res/themes/dark/css/_dark.pcss b/res/themes/dark/css/_dark.pcss index 8b0673f692..2d3ea2e4f4 100644 --- a/res/themes/dark/css/_dark.pcss +++ b/res/themes/dark/css/_dark.pcss @@ -240,11 +240,6 @@ $location-live-secondary-color: #deddfd; } /* ******************** */ -/* Voice Broadcast */ -/* ******************** */ -$live-badge-color: #ffffff; -/* ******************** */ - /* One-off colors */ /* ******************** */ $progressbar-bg-color: var(--cpd-color-gray-200); diff --git a/res/themes/legacy-dark/css/_legacy-dark.pcss b/res/themes/legacy-dark/css/_legacy-dark.pcss index 45bb1870f1..ea5228b6c7 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.pcss +++ b/res/themes/legacy-dark/css/_legacy-dark.pcss @@ -226,11 +226,6 @@ $location-live-color: #5c56f5; $location-live-secondary-color: #deddfd; /* ******************** */ -/* Voice Broadcast */ -/* ******************** */ -$live-badge-color: #ffffff; -/* ******************** */ - body { color-scheme: dark; } diff --git a/res/themes/legacy-light/css/_legacy-light.pcss b/res/themes/legacy-light/css/_legacy-light.pcss index 76e0eec588..32ca7d3d1a 100644 --- a/res/themes/legacy-light/css/_legacy-light.pcss +++ b/res/themes/legacy-light/css/_legacy-light.pcss @@ -325,11 +325,6 @@ $location-live-color: #5c56f5; $location-live-secondary-color: #deddfd; /* ******************** */ -/* Voice Broadcast */ -/* ******************** */ -$live-badge-color: #ffffff; -/* ******************** */ - body { color-scheme: light; } diff --git a/res/themes/light/css/_light.pcss b/res/themes/light/css/_light.pcss index 32629a55f7..1a1705a9c1 100644 --- a/res/themes/light/css/_light.pcss +++ b/res/themes/light/css/_light.pcss @@ -355,11 +355,6 @@ $location-live-color: var(--cpd-color-purple-900); $location-live-secondary-color: var(--cpd-color-purple-600); /* ******************** */ -/* Voice Broadcast */ -/* ******************** */ -$live-badge-color: var(--cpd-color-icon-on-solid-primary); -/* ******************** */ - body { color-scheme: light; } diff --git a/src/@types/matrix-js-sdk.d.ts b/src/@types/matrix-js-sdk.d.ts index 73366f2fee..41ccfcbb3b 100644 --- a/src/@types/matrix-js-sdk.d.ts +++ b/src/@types/matrix-js-sdk.d.ts @@ -10,7 +10,6 @@ import type { IWidget } from "matrix-widget-api"; import type { BLURHASH_FIELD } from "../utils/image-media"; import type { JitsiCallMemberEventType, JitsiCallMemberContent } from "../call-types"; import type { ILayoutStateEvent, WIDGET_LAYOUT_EVENT_TYPE } from "../stores/widgets/types"; -import type { VoiceBroadcastInfoEventContent, VoiceBroadcastInfoEventType } from "../voice-broadcast/types"; import type { EncryptedFile } from "matrix-js-sdk/src/types"; // Extend Matrix JS SDK types via Typescript declaration merging to support unspecced event fields and types @@ -37,9 +36,6 @@ declare module "matrix-js-sdk/src/types" { "im.vector.modular.widgets": IWidget | {}; [WIDGET_LAYOUT_EVENT_TYPE]: ILayoutStateEvent; - // Unstable voice broadcast state events - [VoiceBroadcastInfoEventType]: VoiceBroadcastInfoEventContent; - // Element custom state events "im.vector.web.settings": Record; "org.matrix.room.preview_urls": { disable: boolean }; @@ -78,7 +74,5 @@ declare module "matrix-js-sdk/src/types" { waveform?: number[]; }; "org.matrix.msc3245.voice"?: {}; - - "io.element.voice_broadcast_chunk"?: { sequence: number }; } } diff --git a/src/IConfigOptions.ts b/src/IConfigOptions.ts index 72bee5d0ab..5dd500402d 100644 --- a/src/IConfigOptions.ts +++ b/src/IConfigOptions.ts @@ -175,13 +175,6 @@ export interface IConfigOptions { sync_timeline_limit?: number; dangerously_allow_unsafe_and_insecure_passwords?: boolean; // developer option - voice_broadcast?: { - // length per voice chunk in seconds - chunk_length?: number; - // max voice broadcast length in seconds - max_length?: number; - }; - user_notice?: { title: string; description: string; diff --git a/src/LegacyCallHandler.tsx b/src/LegacyCallHandler.tsx index a06480e9cd..b804ca0084 100644 --- a/src/LegacyCallHandler.tsx +++ b/src/LegacyCallHandler.tsx @@ -55,8 +55,6 @@ import { OpenInviteDialogPayload } from "./dispatcher/payloads/OpenInviteDialogP import { findDMForUser } from "./utils/dm/findDMForUser"; import { getJoinedNonFunctionalMembers } from "./utils/room/getJoinedNonFunctionalMembers"; import { localNotificationsAreSilenced } from "./utils/notifications"; -import { SdkContextClass } from "./contexts/SDKContext"; -import { showCantStartACallDialog } from "./voice-broadcast/utils/showCantStartACallDialog"; import { isNotNull } from "./Typeguards"; import { BackgroundAudio } from "./audio/BackgroundAudio"; import { Jitsi } from "./widgets/Jitsi.ts"; @@ -859,15 +857,6 @@ export default class LegacyCallHandler extends EventEmitter { return; } - // Pause current broadcast, if any - SdkContextClass.instance.voiceBroadcastPlaybacksStore.getCurrent()?.pause(); - - if (SdkContextClass.instance.voiceBroadcastRecordingsStore.getCurrent()) { - // Do not start a call, if recording a broadcast - showCantStartACallDialog(); - return; - } - // We might be using managed hybrid widgets if (isManagedHybridWidgetEnabled(room)) { await addManagedHybridWidget(room); diff --git a/src/Notifier.ts b/src/Notifier.ts index 961d2171a8..45e6a1195d 100644 --- a/src/Notifier.ts +++ b/src/Notifier.ts @@ -49,8 +49,6 @@ import { SdkContextClass } from "./contexts/SDKContext"; import { localNotificationsAreSilenced, createLocalNotificationSettingsIfNeeded } from "./utils/notifications"; import { getIncomingCallToastKey, IncomingCallToast } from "./toasts/IncomingCallToast"; import ToastStore from "./stores/ToastStore"; -import { VoiceBroadcastChunkEventType, VoiceBroadcastInfoEventType } from "./voice-broadcast"; -import { getSenderName } from "./utils/event/getSenderName"; import { stripPlainReply } from "./utils/Reply"; import { BackgroundAudio } from "./audio/BackgroundAudio"; @@ -81,17 +79,6 @@ const msgTypeHandlers: Record string | null> = { return TextForEvent.textForLocationEvent(event)(); }, [MsgType.Audio]: (event: MatrixEvent): string | null => { - if (event.getContent()?.[VoiceBroadcastChunkEventType]) { - if (event.getContent()?.[VoiceBroadcastChunkEventType]?.sequence === 1) { - // Show a notification for the first broadcast chunk. - // At this point a user received something to listen to. - return _t("notifier|io.element.voice_broadcast_chunk", { senderName: getSenderName(event) }); - } - - // Mute other broadcast chunks - return null; - } - return TextForEvent.textForEvent(event, MatrixClientPeg.safeGet()); }, }; @@ -460,8 +447,6 @@ class NotifierClass extends TypedEventEmitter = { logo: require("../res/img/element-desktop-logo.svg").default, url: "https://element.io/get-started", }, - voice_broadcast: { - chunk_length: 2 * 60, // two minutes - max_length: 4 * 60 * 60, // four hours - }, feedback: { existing_issues_url: diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx index 1ffae62aea..49d8b739b7 100644 --- a/src/TextForEvent.tsx +++ b/src/TextForEvent.tsx @@ -36,7 +36,6 @@ import AccessibleButton from "./components/views/elements/AccessibleButton"; import RightPanelStore from "./stores/right-panel/RightPanelStore"; import { highlightEvent, isLocationEvent } from "./utils/EventUtils"; import { ElementCall } from "./models/Call"; -import { textForVoiceBroadcastStoppedEvent, VoiceBroadcastInfoEventType } from "./voice-broadcast"; import { getSenderName } from "./utils/event/getSenderName"; import PosthogTrackers from "./PosthogTrackers.ts"; @@ -906,7 +905,6 @@ const stateHandlers: IHandlers = { // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) "im.vector.modular.widgets": textForWidgetEvent, [WIDGET_LAYOUT_EVENT_TYPE]: textForWidgetLayoutEvent, - [VoiceBroadcastInfoEventType]: textForVoiceBroadcastStoppedEvent, }; // Add all the Mjolnir stuff to the renderer diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 9f9e225352..548dbff983 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -119,7 +119,6 @@ import { ValidatedServerConfig } from "../../utils/ValidatedServerConfig"; import { isLocalRoom } from "../../utils/localRoom/isLocalRoom"; import { SDKContext, SdkContextClass } from "../../contexts/SDKContext"; import { viewUserDeviceSettings } from "../../actions/handlers/viewUserDeviceSettings"; -import { cleanUpBroadcasts, VoiceBroadcastResumer } from "../../voice-broadcast"; import GenericToast from "../views/toasts/GenericToast"; import RovingSpotlightDialog from "../views/dialogs/spotlight/SpotlightDialog"; import { findDMForUser } from "../../utils/dm/findDMForUser"; @@ -227,7 +226,6 @@ export default class MatrixChat extends React.PureComponent { private focusNext: FocusNextType; private subTitleStatus: string; private prevWindowWidth: number; - private voiceBroadcastResumer?: VoiceBroadcastResumer; private readonly loggedInView = createRef(); private dispatcherRef?: string; @@ -501,7 +499,6 @@ export default class MatrixChat extends React.PureComponent { window.removeEventListener("resize", this.onWindowResized); this.stores.accountPasswordStore.clearPassword(); - this.voiceBroadcastResumer?.destroy(); } private onWindowResized = (): void => { @@ -651,10 +648,9 @@ export default class MatrixChat extends React.PureComponent { break; case "logout": LegacyCallHandler.instance.hangupAllCalls(); - Promise.all([ - ...[...CallStore.instance.connectedCalls].map((call) => call.disconnect()), - cleanUpBroadcasts(this.stores), - ]).finally(() => Lifecycle.logout(this.stores.oidcClientStore)); + Promise.all([...[...CallStore.instance.connectedCalls].map((call) => call.disconnect())]).finally(() => + Lifecycle.logout(this.stores.oidcClientStore), + ); break; case "require_registration": startAnyRegistrationFlow(payload as any); @@ -1679,8 +1675,6 @@ export default class MatrixChat extends React.PureComponent { }); } }); - - this.voiceBroadcastResumer = new VoiceBroadcastResumer(cli); } /** diff --git a/src/components/structures/PipContainer.tsx b/src/components/structures/PipContainer.tsx index 731e720b12..c9fabfe0c9 100644 --- a/src/components/structures/PipContainer.tsx +++ b/src/components/structures/PipContainer.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { MutableRefObject, ReactNode, useContext, useRef } from "react"; +import React, { MutableRefObject, ReactNode, useRef } from "react"; import { CallEvent, CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call"; import { logger } from "matrix-js-sdk/src/logger"; import { Optional } from "matrix-events-sdk"; @@ -21,19 +21,7 @@ import { WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore"; import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../stores/ActiveWidgetStore"; import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; import { UPDATE_EVENT } from "../../stores/AsyncStore"; -import { SDKContext, SdkContextClass } from "../../contexts/SDKContext"; -import { - useCurrentVoiceBroadcastPreRecording, - useCurrentVoiceBroadcastRecording, - VoiceBroadcastPlayback, - VoiceBroadcastPlaybackBody, - VoiceBroadcastPreRecording, - VoiceBroadcastPreRecordingPip, - VoiceBroadcastRecording, - VoiceBroadcastRecordingPip, - VoiceBroadcastSmallPlaybackBody, -} from "../../voice-broadcast"; -import { useCurrentVoiceBroadcastPlayback } from "../../voice-broadcast/hooks/useCurrentVoiceBroadcastPlayback"; +import { SdkContextClass } from "../../contexts/SDKContext"; import { WidgetPip } from "../views/pips/WidgetPip"; const SHOW_CALL_IN_STATES = [ @@ -46,9 +34,6 @@ const SHOW_CALL_IN_STATES = [ ]; interface IProps { - voiceBroadcastRecording: Optional; - voiceBroadcastPreRecording: Optional; - voiceBroadcastPlayback: Optional; movePersistedElement: MutableRefObject<(() => void) | undefined>; } @@ -245,52 +230,9 @@ class PipContainerInner extends React.Component { this.setState({ showWidgetInPip, persistentWidgetId, persistentRoomId }); } - private createVoiceBroadcastPlaybackPipContent(voiceBroadcastPlayback: VoiceBroadcastPlayback): CreatePipChildren { - const content = - this.state.viewedRoomId === voiceBroadcastPlayback.infoEvent.getRoomId() ? ( - - ) : ( - - ); - - return ({ onStartMoving }) => ( -
- {content} -
- ); - } - - private createVoiceBroadcastPreRecordingPipContent( - voiceBroadcastPreRecording: VoiceBroadcastPreRecording, - ): CreatePipChildren { - return ({ onStartMoving }) => ( -
- -
- ); - } - - private createVoiceBroadcastRecordingPipContent( - voiceBroadcastRecording: VoiceBroadcastRecording, - ): CreatePipChildren { - return ({ onStartMoving }) => ( -
- -
- ); - } - public render(): ReactNode { const pipMode = true; - let pipContent: Array = []; - - if (this.props.voiceBroadcastRecording) { - pipContent = [this.createVoiceBroadcastRecordingPipContent(this.props.voiceBroadcastRecording)]; - } else if (this.props.voiceBroadcastPreRecording) { - pipContent = [this.createVoiceBroadcastPreRecordingPipContent(this.props.voiceBroadcastPreRecording)]; - } else if (this.props.voiceBroadcastPlayback) { - pipContent = [this.createVoiceBroadcastPlaybackPipContent(this.props.voiceBroadcastPlayback)]; - } + const pipContent: Array = []; if (this.state.primaryCall) { // get a ref to call inside the current scope @@ -338,24 +280,7 @@ class PipContainerInner extends React.Component { } export const PipContainer: React.FC = () => { - const sdkContext = useContext(SDKContext); - const voiceBroadcastPreRecordingStore = sdkContext.voiceBroadcastPreRecordingStore; - const { currentVoiceBroadcastPreRecording } = useCurrentVoiceBroadcastPreRecording(voiceBroadcastPreRecordingStore); - - const voiceBroadcastRecordingsStore = sdkContext.voiceBroadcastRecordingsStore; - const { currentVoiceBroadcastRecording } = useCurrentVoiceBroadcastRecording(voiceBroadcastRecordingsStore); - - const voiceBroadcastPlaybacksStore = sdkContext.voiceBroadcastPlaybacksStore; - const { currentVoiceBroadcastPlayback } = useCurrentVoiceBroadcastPlayback(voiceBroadcastPlaybacksStore); - const movePersistedElement = useRef<() => void>(); - return ( - - ); + return ; }; diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 5cd6ea7484..c5f8ef841d 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -40,8 +40,6 @@ import { UPDATE_SELECTED_SPACE } from "../../stores/spaces"; import UserIdentifierCustomisations from "../../customisations/UserIdentifier"; import PosthogTrackers from "../../PosthogTrackers"; import { ViewHomePagePayload } from "../../dispatcher/payloads/ViewHomePagePayload"; -import { Icon as LiveIcon } from "../../../res/img/compound/live-8px.svg"; -import { VoiceBroadcastRecording, VoiceBroadcastRecordingsStoreEvent } from "../../voice-broadcast"; import { SDKContext } from "../../contexts/SDKContext"; import { shouldShowFeedback } from "../../utils/Feedback"; import DarkLightModeSvg from "../../../res/img/element-icons/roomlist/dark-light-mode.svg"; @@ -58,7 +56,6 @@ interface IState { isDarkTheme: boolean; isHighContrast: boolean; selectedSpace?: Room | null; - showLiveAvatarAddon: boolean; } const toRightOf = (rect: PartialDOMRect): MenuProps => { @@ -94,7 +91,6 @@ export default class UserMenu extends React.Component { isDarkTheme: this.isUserOnDarkTheme(), isHighContrast: this.isUserOnHighContrastTheme(), selectedSpace: SpaceStore.instance.activeSpaceRoom, - showLiveAvatarAddon: this.context.voiceBroadcastRecordingsStore.hasCurrent(), }; } @@ -102,19 +98,9 @@ export default class UserMenu extends React.Component { return !!getHomePageUrl(SdkConfig.get(), this.context.client!); } - private onCurrentVoiceBroadcastRecordingChanged = (recording: VoiceBroadcastRecording | null): void => { - this.setState({ - showLiveAvatarAddon: recording !== null, - }); - }; - public componentDidMount(): void { OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate); SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate); - this.context.voiceBroadcastRecordingsStore.on( - VoiceBroadcastRecordingsStoreEvent.CurrentChanged, - this.onCurrentVoiceBroadcastRecordingChanged, - ); this.dispatcherRef = defaultDispatcher.register(this.onAction); this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged); } @@ -125,10 +111,6 @@ export default class UserMenu extends React.Component { defaultDispatcher.unregister(this.dispatcherRef); OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate); SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate); - this.context.voiceBroadcastRecordingsStore.off( - VoiceBroadcastRecordingsStoreEvent.CurrentChanged, - this.onCurrentVoiceBroadcastRecordingChanged, - ); } private isUserOnDarkTheme(): boolean { @@ -435,12 +417,6 @@ export default class UserMenu extends React.Component { name =
{displayName}
; } - const liveAvatarAddon = this.state.showLiveAvatarAddon ? ( -
- -
- ) : null; - return (
{ size={avatarSize + "px"} className="mx_UserMenu_userAvatar_BaseAvatar" /> - {liveAvatarAddon}
{name} {this.renderContextMenu()} diff --git a/src/components/structures/grouper/CreationGrouper.tsx b/src/components/structures/grouper/CreationGrouper.tsx index db4542e836..84982066c3 100644 --- a/src/components/structures/grouper/CreationGrouper.tsx +++ b/src/components/structures/grouper/CreationGrouper.tsx @@ -12,7 +12,6 @@ import { KnownMembership } from "matrix-js-sdk/src/types"; import { BaseGrouper } from "./BaseGrouper"; import MessagePanel, { WrappedEvent } from "../MessagePanel"; -import { VoiceBroadcastInfoEventType } from "../../../voice-broadcast"; import DMRoomMap from "../../../utils/DMRoomMap"; import { _t } from "../../../languageHandler"; import DateSeparator from "../../views/messages/DateSeparator"; @@ -53,11 +52,6 @@ export class CreationGrouper extends BaseGrouper { return false; } - if (VoiceBroadcastInfoEventType === eventType) { - // always show voice broadcast info events in timeline - return false; - } - if (event.isState() && event.getSender() === createEvent.getSender()) { return true; } diff --git a/src/components/views/audio_messages/DevicesContextMenu.tsx b/src/components/views/audio_messages/DevicesContextMenu.tsx deleted file mode 100644 index a88a280242..0000000000 --- a/src/components/views/audio_messages/DevicesContextMenu.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022, 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React, { MutableRefObject } from "react"; - -import { toLeftOrRightOf } from "../../structures/ContextMenu"; -import IconizedContextMenu, { - IconizedContextMenuOptionList, - IconizedContextMenuRadio, -} from "../context_menus/IconizedContextMenu"; - -interface Props { - containerRef: MutableRefObject; - currentDevice: MediaDeviceInfo | null; - devices: MediaDeviceInfo[]; - onDeviceSelect: (device: MediaDeviceInfo) => void; -} - -export const DevicesContextMenu: React.FC = ({ containerRef, currentDevice, devices, onDeviceSelect }) => { - const deviceOptions = devices.map((d: MediaDeviceInfo) => { - return ( - onDeviceSelect(d)} - label={d.label} - /> - ); - }); - - return ( - {}} - {...(containerRef.current ? toLeftOrRightOf(containerRef.current.getBoundingClientRect(), 0) : {})} - > - {deviceOptions} - - ); -}; diff --git a/src/components/views/dialogs/CantStartVoiceMessageBroadcastDialog.tsx b/src/components/views/dialogs/CantStartVoiceMessageBroadcastDialog.tsx deleted file mode 100644 index eb565c0072..0000000000 --- a/src/components/views/dialogs/CantStartVoiceMessageBroadcastDialog.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; - -import { _t } from "../../../languageHandler"; -import Modal from "../../../Modal"; -import InfoDialog from "./InfoDialog"; - -export const createCantStartVoiceMessageBroadcastDialog = (): void => { - Modal.createDialog(InfoDialog, { - title: _t("voice_message|cant_start_broadcast_title"), - description:

{_t("voice_message|cant_start_broadcast_description")}

, - hasCloseButton: true, - }); -}; diff --git a/src/components/views/dialogs/ConfirmRedactDialog.tsx b/src/components/views/dialogs/ConfirmRedactDialog.tsx index 27823ec478..f4258c9d6d 100644 --- a/src/components/views/dialogs/ConfirmRedactDialog.tsx +++ b/src/components/views/dialogs/ConfirmRedactDialog.tsx @@ -6,14 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { Feature, ServerSupport } from "matrix-js-sdk/src/feature"; -import { IRedactOpts, MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix"; +import { IRedactOpts, MatrixEvent } 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"; @@ -70,18 +68,6 @@ export function createRedactEventDialog({ const cli = MatrixClientPeg.safeGet(); const withRelTypes: Pick = {}; - // 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 - ) { - withRelTypes.with_rel_types = [RelationType.Reference]; - } - } - try { onCloseDialog?.(); await cli.redactEvent(roomId, eventId, undefined, { diff --git a/src/components/views/dialogs/DevtoolsDialog.tsx b/src/components/views/dialogs/DevtoolsDialog.tsx index 7319685014..7dc683469a 100644 --- a/src/components/views/dialogs/DevtoolsDialog.tsx +++ b/src/components/views/dialogs/DevtoolsDialog.tsx @@ -22,7 +22,6 @@ import { AccountDataExplorer, RoomAccountDataExplorer } from "./devtools/Account import SettingsFlag from "../elements/SettingsFlag"; import { SettingLevel } from "../../../settings/SettingLevel"; import ServerInfo from "./devtools/ServerInfo"; -import { Features } from "../../../settings/Settings"; import CopyableText from "../elements/CopyableText"; import RoomNotifications from "./devtools/RoomNotifications"; @@ -100,7 +99,6 @@ const DevtoolsDialog: React.FC = ({ roomId, threadRootId, onFinished }) - ); diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx index bf92c993c5..9d21b8fa45 100644 --- a/src/components/views/messages/MessageActionBar.tsx +++ b/src/components/views/messages/MessageActionBar.tsx @@ -58,7 +58,6 @@ import { ALTERNATE_KEY_NAME } from "../../../accessibility/KeyboardShortcuts"; import { Action } from "../../../dispatcher/actions"; import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload"; import { GetRelationsForEvent, IEventTileType } from "../rooms/EventTile"; -import { VoiceBroadcastInfoEventType } from "../../../voice-broadcast/types"; import { ButtonEvent } from "../elements/AccessibleButton"; import PinningUtils from "../../../utils/PinningUtils"; import PosthogTrackers from "../../../PosthogTrackers.ts"; @@ -354,8 +353,7 @@ export default class MessageActionBar extends React.PureComponent { @@ -276,10 +275,6 @@ export default class MessageEvent extends React.Component implements IMe if (M_LOCATION.matches(type) || (type === EventType.RoomMessage && msgtype === MsgType.Location)) { BodyType = MLocationBody; } - - if (type === VoiceBroadcastInfoEventType && content?.state === VoiceBroadcastInfoState.Started) { - BodyType = VoiceBroadcastBody; - } } if (SettingsStore.getValue("feature_mjolnir")) { diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 9dcd107fed..f5716d728b 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -48,14 +48,9 @@ import MessageComposerButtons from "./MessageComposerButtons"; import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom"; -import { Features } from "../../../settings/Settings"; import { VoiceMessageRecording } from "../../../audio/VoiceMessageRecording"; import { SendWysiwygComposer, sendMessage, getConversionFunctions } from "./wysiwyg_composer/"; import { MatrixClientProps, withMatrixClientHOC } from "../../../contexts/MatrixClientContext"; -import { setUpVoiceBroadcastPreRecording } from "../../../voice-broadcast/utils/setUpVoiceBroadcastPreRecording"; -import { SdkContextClass } from "../../../contexts/SDKContext"; -import { VoiceBroadcastInfoState } from "../../../voice-broadcast"; -import { createCantStartVoiceMessageBroadcastDialog } from "../dialogs/CantStartVoiceMessageBroadcastDialog"; import { UIFeature } from "../../../settings/UIFeature"; import { formatTimeLeft } from "../../../DateUtils"; import RoomReplacedSvg from "../../../../res/img/room_replaced.svg"; @@ -101,7 +96,6 @@ interface IState { isStickerPickerOpen: boolean; showStickersButton: boolean; showPollsButton: boolean; - showVoiceBroadcastButton: boolean; isWysiwygLabEnabled: boolean; isRichTextEnabled: boolean; initialComposerContent: string; @@ -127,7 +121,6 @@ export class MessageComposer extends React.Component { public static defaultProps = { compact: false, - showVoiceBroadcastButton: false, isRichTextEnabled: true, }; @@ -155,7 +148,6 @@ export class MessageComposer extends React.Component { isStickerPickerOpen: false, showStickersButton: SettingsStore.getValue("MessageComposerInput.showStickersButton"), showPollsButton: SettingsStore.getValue("MessageComposerInput.showPollsButton"), - showVoiceBroadcastButton: SettingsStore.getValue(Features.VoiceBroadcast), isWysiwygLabEnabled: isWysiwygLabEnabled, isRichTextEnabled: isRichTextEnabled, initialComposerContent: initialComposerContent, @@ -250,7 +242,6 @@ export class MessageComposer extends React.Component { SettingsStore.monitorSetting("MessageComposerInput.showStickersButton", null); SettingsStore.monitorSetting("MessageComposerInput.showPollsButton", null); - SettingsStore.monitorSetting(Features.VoiceBroadcast, null); SettingsStore.monitorSetting("feature_wysiwyg_composer", null); this.dispatcherRef = dis.register(this.onAction); @@ -301,12 +292,6 @@ export class MessageComposer extends React.Component { } break; } - case Features.VoiceBroadcast: { - if (this.state.showVoiceBroadcastButton !== settingUpdatedPayload.newValue) { - this.setState({ showVoiceBroadcastButton: !!settingUpdatedPayload.newValue }); - } - break; - } case "feature_wysiwyg_composer": { if (this.state.isWysiwygLabEnabled !== settingUpdatedPayload.newValue) { this.setState({ isWysiwygLabEnabled: Boolean(settingUpdatedPayload.newValue) }); @@ -533,13 +518,7 @@ export class MessageComposer extends React.Component { } private onRecordStartEndClick = (): void => { - const currentBroadcastRecording = SdkContextClass.instance.voiceBroadcastRecordingsStore.getCurrent(); - - if (currentBroadcastRecording && currentBroadcastRecording.getState() !== VoiceBroadcastInfoState.Stopped) { - createCantStartVoiceMessageBroadcastDialog(); - } else { - this.voiceRecordingButton.current?.onRecordStartEndClick(); - } + this.voiceRecordingButton.current?.onRecordStartEndClick(); if (this.context.narrow) { this.toggleButtonMenu(); @@ -698,17 +677,6 @@ export class MessageComposer extends React.Component { isRichTextEnabled={this.state.isRichTextEnabled} onComposerModeClick={this.onRichTextToggle} toggleButtonMenu={this.toggleButtonMenu} - showVoiceBroadcastButton={this.state.showVoiceBroadcastButton} - onStartVoiceBroadcastClick={() => { - setUpVoiceBroadcastPreRecording( - this.props.room, - MatrixClientPeg.safeGet(), - SdkContextClass.instance.voiceBroadcastPlaybacksStore, - SdkContextClass.instance.voiceBroadcastRecordingsStore, - SdkContextClass.instance.voiceBroadcastPreRecordingStore, - ); - this.toggleButtonMenu(); - }} /> )} {showSendButton && ( diff --git a/src/components/views/rooms/MessageComposerButtons.tsx b/src/components/views/rooms/MessageComposerButtons.tsx index 370bc0861c..19b86834dd 100644 --- a/src/components/views/rooms/MessageComposerButtons.tsx +++ b/src/components/views/rooms/MessageComposerButtons.tsx @@ -43,8 +43,6 @@ interface IProps { showPollsButton: boolean; showStickersButton: boolean; toggleButtonMenu: () => void; - showVoiceBroadcastButton: boolean; - onStartVoiceBroadcastClick: () => void; isRichTextEnabled: boolean; onComposerModeClick: () => void; } @@ -80,7 +78,6 @@ const MessageComposerButtons: React.FC = (props: IProps) => { uploadButton(), // props passed via UploadButtonContext showStickersButton(props), voiceRecordingButton(props, narrow), - startVoiceBroadcastButton(props), props.showPollsButton ? pollButton(room, props.relation) : null, showLocationButton(props, room, matrixClient), ]; @@ -100,7 +97,6 @@ const MessageComposerButtons: React.FC = (props: IProps) => { moreButtons = [ showStickersButton(props), voiceRecordingButton(props, narrow), - startVoiceBroadcastButton(props), props.showPollsButton ? pollButton(room, props.relation) : null, showLocationButton(props, room, matrixClient), ]; @@ -254,18 +250,6 @@ function showStickersButton(props: IProps): ReactElement | null { ) : null; } -const startVoiceBroadcastButton: React.FC = (props: IProps): ReactElement | null => { - return props.showVoiceBroadcastButton ? ( - - ) : null; -}; - function voiceRecordingButton(props: IProps, narrow: boolean): ReactElement | null { // XXX: recording UI does not work well in narrow mode, so hide for now return narrow ? null : ( diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index 8351c176ff..7953c5068d 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -39,7 +39,6 @@ import { getKeyBindingsManager } from "../../../KeyBindingsManager"; import { RoomGeneralContextMenu } from "../context_menus/RoomGeneralContextMenu"; import { CallStore, CallStoreEvent } from "../../../stores/CallStore"; import { SdkContextClass } from "../../../contexts/SDKContext"; -import { useHasRoomLiveVoiceBroadcast } from "../../../voice-broadcast"; import { RoomTileSubtitle } from "./RoomTileSubtitle"; import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; import { UIComponent } from "../../../settings/UIFeature"; @@ -53,10 +52,6 @@ interface Props { tag: TagID; } -interface ClassProps extends Props { - hasLiveVoiceBroadcast: boolean; -} - type PartialDOMRect = Pick; interface State { @@ -77,13 +72,13 @@ export const contextMenuBelow = (elementRect: PartialDOMRect): MenuProps => { return { left, top, chevronFace }; }; -export class RoomTile extends React.PureComponent { +class RoomTile extends React.PureComponent { private dispatcherRef?: string; private roomTileRef = createRef(); private notificationState: NotificationState; private roomProps: RoomEchoChamber; - public constructor(props: ClassProps) { + public constructor(props: Props) { super(props); this.state = { @@ -370,15 +365,10 @@ export class RoomTile extends React.PureComponent { /** * RoomTile has a subtile if one of the following applies: * - there is a call - * - there is a live voice broadcast * - message previews are enabled and there is a previewable message */ private get shouldRenderSubtitle(): boolean { - return ( - !!this.state.call || - this.props.hasLiveVoiceBroadcast || - (this.props.showMessagePreview && !!this.state.messagePreview) - ); + return !!this.state.call || (this.props.showMessagePreview && !!this.state.messagePreview); } public render(): React.ReactElement { @@ -409,7 +399,6 @@ export class RoomTile extends React.PureComponent { const subtitle = this.shouldRenderSubtitle ? ( { } } -const RoomTileHOC: React.FC = (props: Props) => { - const hasLiveVoiceBroadcast = useHasRoomLiveVoiceBroadcast(props.room); - return ; -}; - -export default RoomTileHOC; +export default RoomTile; diff --git a/src/components/views/rooms/RoomTileSubtitle.tsx b/src/components/views/rooms/RoomTileSubtitle.tsx index ea4a96d259..479b9c4f71 100644 --- a/src/components/views/rooms/RoomTileSubtitle.tsx +++ b/src/components/views/rooms/RoomTileSubtitle.tsx @@ -13,11 +13,9 @@ import { ThreadsIcon } from "@vector-im/compound-design-tokens/assets/web/icons" import { MessagePreview } from "../../../stores/room-list/MessagePreviewStore"; import { Call } from "../../../models/Call"; import { RoomTileCallSummary } from "./RoomTileCallSummary"; -import { VoiceBroadcastRoomSubtitle } from "../../../voice-broadcast"; interface Props { call: Call | null; - hasLiveVoiceBroadcast: boolean; messagePreview: MessagePreview | null; roomId: string; showMessagePreview: boolean; @@ -25,13 +23,7 @@ interface Props { const messagePreviewId = (roomId: string): string => `mx_RoomTile_messagePreview_${roomId}`; -export const RoomTileSubtitle: React.FC = ({ - call, - hasLiveVoiceBroadcast, - messagePreview, - roomId, - showMessagePreview, -}) => { +export const RoomTileSubtitle: React.FC = ({ call, messagePreview, roomId, showMessagePreview }) => { if (call) { return (
@@ -40,10 +32,6 @@ export const RoomTileSubtitle: React.FC = ({ ); } - if (hasLiveVoiceBroadcast) { - return ; - } - if (showMessagePreview && messagePreview) { const className = classNames("mx_RoomTile_subtitle", { "mx_RoomTile_subtitle--thread-reply": messagePreview.isThreadReply, diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx index 5fada0b6bc..baf4b41253 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx @@ -19,7 +19,6 @@ import ErrorDialog from "../../../dialogs/ErrorDialog"; import PowerSelector from "../../../elements/PowerSelector"; import SettingsFieldset from "../../SettingsFieldset"; import SettingsStore from "../../../../../settings/SettingsStore"; -import { VoiceBroadcastInfoEventType } from "../../../../../voice-broadcast"; import { ElementCall } from "../../../../../models/Call"; import SdkConfig, { DEFAULTS } from "../../../../../SdkConfig"; import { AddPrivilegedUsers } from "../../AddPrivilegedUsers"; @@ -62,7 +61,6 @@ const plEventsToShow: Record = { // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) "im.vector.modular.widgets": { isState: true, hideForSpace: true }, - [VoiceBroadcastInfoEventType]: { isState: true, hideForSpace: true }, }; // parse a string as an integer; if the input is undefined, or cannot be parsed @@ -289,7 +287,6 @@ export default class RolesRoomSettingsTab extends React.Component void, -): { - currentDevice: MediaDeviceInfo | null; - currentDeviceLabel: string; - devices: MediaDeviceInfo[]; - setDevice(device: MediaDeviceInfo): void; -} => { - const shouldRequestPermissionsRef = useRef(true); - const [state, setState] = useState({ - devices: [], - device: null, - }); - - if (shouldRequestPermissionsRef.current) { - shouldRequestPermissionsRef.current = false; - requestMediaPermissions(false).then((stream: MediaStream | undefined) => { - MediaDeviceHandler.getDevices().then((devices) => { - if (!devices) return; - const { audioinput } = devices; - MediaDeviceHandler.getDefaultDevice(audioinput); - const deviceFromSettings = MediaDeviceHandler.getAudioInput(); - const device = - audioinput.find((d) => { - return d.deviceId === deviceFromSettings; - }) || audioinput[0]; - setState({ - ...state, - devices: audioinput, - device, - }); - stream?.getTracks().forEach((t) => t.stop()); - }); - }); - } - - const setDevice = (device: MediaDeviceInfo): void => { - const shouldNotify = device.deviceId !== state.device?.deviceId; - MediaDeviceHandler.instance.setDevice(device.deviceId, MediaDeviceKindEnum.AudioInput); - - setState({ - ...state, - device, - }); - - if (shouldNotify) { - onDeviceChanged?.(device); - } - }; - - return { - currentDevice: state.device, - currentDeviceLabel: state.device?.label || _t("voip|default_device"), - devices: state.devices, - setDevice, - }; -}; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 50ca4ae1e4..31a6c71c1a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1087,10 +1087,6 @@ }, "error_user_not_logged_in": "User is not logged in", "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s ended a voice broadcast", - "you": "You ended a voice broadcast" - }, "m.call.answer": { "dm": "Call in progress", "user": "%(senderName)s joined the call", @@ -1491,8 +1487,6 @@ "video_rooms_faq2_answer": "Yes, the chat timeline is displayed alongside the video.", "video_rooms_faq2_question": "Can I use text chat alongside the video call?", "video_rooms_feedbackSubheading": "Thank you for trying the beta, please go into as much detail as you can so we can improve it.", - "voice_broadcast": "Voice broadcast", - "voice_broadcast_force_small_chunks": "Force 15s voice broadcast chunk length", "wysiwyg_composer": "Rich text editor" }, "labs_mjolnir": { @@ -1638,7 +1632,6 @@ "mute_description": "You won't get any notifications" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s started a voice broadcast", "m.key.verification.request": "%(name)s is requesting verification" }, "onboarding": { @@ -2253,7 +2246,6 @@ "error_unbanning": "Failed to unban", "events_default": "Send messages", "invite": "Invite users", - "io.element.voice_broadcast_info": "Voice broadcasts", "kick": "Remove users", "m.call": "Start %(brand)s calls", "m.call.member": "Join %(brand)s calls", @@ -3287,10 +3279,6 @@ "error_rendering_message": "Can't load this message", "historical_messages_unavailable": "You can't see earlier messages", "in_room_name": " in %(room)s", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s ended a voice broadcast", - "you": "You ended a voice broadcast" - }, "io.element.widgets.layout": "%(senderName)s has updated the room layout", "late_event_separator": "Originally sent %(dateTime)s", "load_error": { @@ -3840,38 +3828,6 @@ "switch_theme_dark": "Switch to dark mode", "switch_theme_light": "Switch to light mode" }, - "voice_broadcast": { - "30s_backward": "30s backward", - "30s_forward": "30s forward", - "action": "Voice broadcast", - "buffering": "Buffering…", - "confirm_listen_affirm": "Yes, end my recording", - "confirm_listen_description": "If you start listening to this live broadcast, your current live broadcast recording will be ended.", - "confirm_listen_title": "Listen to live broadcast?", - "confirm_stop_affirm": "Yes, stop broadcast", - "confirm_stop_description": "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.", - "confirm_stop_title": "Stop live broadcasting?", - "connection_error": "Connection error - Recording paused", - "failed_already_recording_description": "You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.", - "failed_already_recording_title": "Can't start a new voice broadcast", - "failed_decrypt": "Unable to decrypt voice broadcast", - "failed_generic": "Unable to play this voice broadcast", - "failed_insufficient_permission_description": "You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.", - "failed_insufficient_permission_title": "Can't start a new voice broadcast", - "failed_no_connection_description": "Unfortunately we're unable to start a recording right now. Please try again later.", - "failed_no_connection_title": "Connection error", - "failed_others_already_recording_description": "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.", - "failed_others_already_recording_title": "Can't start a new voice broadcast", - "go_live": "Go live", - "live": "Live", - "pause": "pause voice broadcast", - "play": "play voice broadcast", - "resume": "resume voice broadcast" - }, - "voice_message": { - "cant_start_broadcast_description": "You can't start a voice message as you are currently recording a live broadcast. Please end your live broadcast in order to start recording a voice message.", - "cant_start_broadcast_title": "Can't start voice message" - }, "voip": { "already_in_call": "Already in call", "already_in_call_person": "You're already in a call with this person.", @@ -3891,7 +3847,6 @@ "camera_disabled": "Your camera is turned off", "camera_enabled": "Your camera is still enabled", "cannot_call_yourself_description": "You cannot place a call with yourself.", - "change_input_device": "Change input device", "close_lobby": "Close lobby", "connecting": "Connecting", "connection_lost": "Connectivity to the server has been lost", @@ -3910,8 +3865,6 @@ "enable_camera": "Turn on camera", "enable_microphone": "Unmute microphone", "expand": "Return to call", - "failed_call_live_broadcast_description": "You can’t start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call.", - "failed_call_live_broadcast_title": "Can’t start a call", "get_call_link": "Share call link", "hangup": "Hangup", "hide_sidebar_button": "Hide sidebar", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 08cff0c1bb..6cd5b15a51 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -85,8 +85,6 @@ export enum LabGroup { } export enum Features { - VoiceBroadcast = "feature_voice_broadcast", - VoiceBroadcastForceSmallChunks = "feature_voice_broadcast_force_small_chunks", NotificationSettings2 = "feature_notification_settings2", OidcNativeFlow = "feature_oidc_native_flow", ReleaseAnnouncement = "feature_release_announcement", @@ -440,19 +438,6 @@ export const SETTINGS: { [setting: string]: ISetting } = { shouldWarn: true, default: false, }, - [Features.VoiceBroadcast]: { - isFeature: true, - labsGroup: LabGroup.Messaging, - supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG_PRIORITISED, - supportedLevelsAreOrdered: true, - displayName: _td("labs|voice_broadcast"), - default: false, - }, - [Features.VoiceBroadcastForceSmallChunks]: { - supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, - displayName: _td("labs|voice_broadcast_force_small_chunks"), - default: false, - }, [Features.OidcNativeFlow]: { isFeature: true, labsGroup: LabGroup.Developer, diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx index 53e25736f0..66644c06a1 100644 --- a/src/stores/RoomViewStore.tsx +++ b/src/stores/RoomViewStore.tsx @@ -42,15 +42,6 @@ import { UPDATE_EVENT } from "./AsyncStore"; import { SdkContextClass } from "../contexts/SDKContext"; import { CallStore } from "./CallStore"; import { ThreadPayload } from "../dispatcher/payloads/ThreadPayload"; -import { - doClearCurrentVoiceBroadcastPlaybackIfStopped, - doMaybeSetCurrentVoiceBroadcastPlayback, - VoiceBroadcastRecording, - VoiceBroadcastRecordingsStoreEvent, -} from "../voice-broadcast"; -import { IRoomStateEventsActionPayload } from "../actions/MatrixActionCreators"; -import { showCantStartACallDialog } from "../voice-broadcast/utils/showCantStartACallDialog"; -import { pauseNonLiveBroadcastFromOtherRoom } from "../voice-broadcast/utils/pauseNonLiveBroadcastFromOtherRoom"; import { ActionPayload } from "../dispatcher/payloads"; import { CancelAskToJoinPayload } from "../dispatcher/payloads/CancelAskToJoinPayload"; import { SubmitAskToJoinPayload } from "../dispatcher/payloads/SubmitAskToJoinPayload"; @@ -164,10 +155,6 @@ export class RoomViewStore extends EventEmitter { ) { super(); this.resetDispatcher(dis); - this.stores.voiceBroadcastRecordingsStore.addListener( - VoiceBroadcastRecordingsStoreEvent.CurrentChanged, - this.onCurrentBroadcastRecordingChanged, - ); } public addRoomListener(roomId: string, fn: Listener): void { @@ -182,16 +169,6 @@ export class RoomViewStore extends EventEmitter { this.emit(roomId, isActive); } - private onCurrentBroadcastRecordingChanged = (recording: VoiceBroadcastRecording | null): void => { - if (recording === null) { - const room = this.stores.client?.getRoom(this.state.roomId || undefined); - - if (room) { - this.doMaybeSetCurrentVoiceBroadcastPlayback(room); - } - } - }; - private setState(newState: Partial): void { // If values haven't changed, there's nothing to do. // This only tries a shallow comparison, so unchanged objects will slip @@ -207,16 +184,6 @@ export class RoomViewStore extends EventEmitter { return; } - if (newState.viewingCall) { - // Pause current broadcast, if any - this.stores.voiceBroadcastPlaybacksStore.getCurrent()?.pause(); - - if (this.stores.voiceBroadcastRecordingsStore.getCurrent()) { - showCantStartACallDialog(); - newState.viewingCall = false; - } - } - const lastRoomId = this.state.roomId; this.state = Object.assign(this.state, newState); if (lastRoomId !== this.state.roomId) { @@ -235,29 +202,6 @@ export class RoomViewStore extends EventEmitter { this.emit(UPDATE_EVENT); } - private doMaybeSetCurrentVoiceBroadcastPlayback(room: Room): void { - if (!this.stores.client) return; - doMaybeSetCurrentVoiceBroadcastPlayback( - room, - this.stores.client, - this.stores.voiceBroadcastPlaybacksStore, - this.stores.voiceBroadcastRecordingsStore, - ); - } - - private onRoomStateEvents(event: MatrixEvent): void { - const roomId = event.getRoomId?.(); - - // no room or not current room - if (!roomId || roomId !== this.state.roomId) return; - - const room = this.stores.client?.getRoom(roomId); - - if (room) { - this.doMaybeSetCurrentVoiceBroadcastPlayback(room); - } - } - private onDispatch(payload: ActionPayload): void { // eslint-disable-line @typescript-eslint/naming-convention switch (payload.action) { @@ -283,10 +227,6 @@ export class RoomViewStore extends EventEmitter { wasContextSwitch: false, viewingCall: false, }); - doClearCurrentVoiceBroadcastPlaybackIfStopped(this.stores.voiceBroadcastPlaybacksStore); - break; - case "MatrixActions.RoomState.events": - this.onRoomStateEvents((payload as IRoomStateEventsActionPayload).event); break; case Action.ViewRoomError: this.viewRoomError(payload as ViewRoomErrorPayload); @@ -489,9 +429,6 @@ export class RoomViewStore extends EventEmitter { } if (room) { - pauseNonLiveBroadcastFromOtherRoom(room, this.stores.voiceBroadcastPlaybacksStore); - this.doMaybeSetCurrentVoiceBroadcastPlayback(room); - await setMarkedUnreadState(room, MatrixClientPeg.safeGet(), false); } } else if (payload.room_alias) { diff --git a/src/stores/room-list/MessagePreviewStore.ts b/src/stores/room-list/MessagePreviewStore.ts index e0e06ec980..2577b2ba23 100644 --- a/src/stores/room-list/MessagePreviewStore.ts +++ b/src/stores/room-list/MessagePreviewStore.ts @@ -22,8 +22,6 @@ 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"; import shouldHideEvent from "../../shouldHideEvent"; // Emitted event for when a room's preview has changed. First argument will the room for which @@ -69,10 +67,6 @@ 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 2873320cf3..20631f1425 100644 --- a/src/stores/room-list/previews/MessageEventPreview.ts +++ b/src/stores/room-list/previews/MessageEventPreview.ts @@ -14,15 +14,11 @@ 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 | 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 deleted file mode 100644 index 94116692a6..0000000000 --- a/src/stores/room-list/previews/VoiceBroadcastPreview.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -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 { - public 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/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index 8362f1048a..0472b1664b 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -284,10 +284,6 @@ export class StopGapWidget extends EventEmitter { }); this.messaging.on("capabilitiesNotified", () => this.emit("capabilitiesNotified")); this.messaging.on(`action:${WidgetApiFromWidgetAction.OpenModalWidget}`, this.onOpenModal); - this.messaging.on(`action:${ElementWidgetActions.JoinCall}`, () => { - // pause voice broadcast recording when any widget sends a "join" - SdkContextClass.instance.voiceBroadcastRecordingsStore.getCurrent()?.pause(); - }); // Always attach a handler for ViewRoom, but permission check it internally this.messaging.on(`action:${ElementWidgetActions.ViewRoom}`, (ev: CustomEvent) => { diff --git a/src/utils/EventRenderingUtils.ts b/src/utils/EventRenderingUtils.ts index 099bf768d8..ed8d4af101 100644 --- a/src/utils/EventRenderingUtils.ts +++ b/src/utils/EventRenderingUtils.ts @@ -21,7 +21,6 @@ import SettingsStore from "../settings/SettingsStore"; import { haveRendererForEvent, JitsiEventFactory, JSONEventFactory, pickFactory } from "../events/EventTileFactory"; import { getMessageModerationState, isLocationEvent, MessageModerationState } from "./EventUtils"; import { ElementCall } from "../models/Call"; -import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../voice-broadcast"; const calcIsInfoMessage = ( eventType: EventType | string, @@ -38,8 +37,7 @@ const calcIsInfoMessage = ( eventType !== EventType.RoomCreate && !M_POLL_START.matches(eventType) && !M_POLL_END.matches(eventType) && - !M_BEACON_INFO.matches(eventType) && - !(eventType === VoiceBroadcastInfoEventType && content?.state === VoiceBroadcastInfoState.Started) + !M_BEACON_INFO.matches(eventType) ); }; @@ -91,8 +89,7 @@ export function getEventDisplayInfo( (eventType === EventType.RoomMessage && msgtype === MsgType.Emote) || M_POLL_START.matches(eventType) || M_BEACON_INFO.matches(eventType) || - isLocationEvent(mxEvent) || - eventType === VoiceBroadcastInfoEventType; + isLocationEvent(mxEvent); // If we're showing hidden events in the timeline, we should use the // source tile when there's no regular tile for an event and also for diff --git a/src/utils/EventUtils.ts b/src/utils/EventUtils.ts index 7c5b80697b..d57cefa1b5 100644 --- a/src/utils/EventUtils.ts +++ b/src/utils/EventUtils.ts @@ -30,7 +30,6 @@ 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,9 +55,7 @@ export function isContentActionable(mxEvent: MatrixEvent): boolean { mxEvent.getType() === "m.sticker" || M_POLL_START.matches(mxEvent.getType()) || M_POLL_END.matches(mxEvent.getType()) || - M_BEACON_INFO.matches(mxEvent.getType()) || - (mxEvent.getType() === VoiceBroadcastInfoEventType && - mxEvent.getContent()?.state === VoiceBroadcastInfoState.Started) + M_BEACON_INFO.matches(mxEvent.getType()) ) { return true; } diff --git a/src/voice-broadcast/audio/VoiceBroadcastRecorder.ts b/src/voice-broadcast/audio/VoiceBroadcastRecorder.ts deleted file mode 100644 index 8a6e17a1a5..0000000000 --- a/src/voice-broadcast/audio/VoiceBroadcastRecorder.ts +++ /dev/null @@ -1,181 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { isEqual } from "lodash"; -import { Optional } from "matrix-events-sdk"; -import { logger } from "matrix-js-sdk/src/logger"; -import { TypedEventEmitter } from "matrix-js-sdk/src/matrix"; - -import { getChunkLength } from ".."; -import { IRecordingUpdate, VoiceRecording } from "../../audio/VoiceRecording"; -import { concat } from "../../utils/arrays"; -import { IDestroyable } from "../../utils/IDestroyable"; -import { Singleflight } from "../../utils/Singleflight"; - -export enum VoiceBroadcastRecorderEvent { - ChunkRecorded = "chunk_recorded", - CurrentChunkLengthUpdated = "current_chunk_length_updated", -} - -interface EventMap { - [VoiceBroadcastRecorderEvent.ChunkRecorded]: (chunk: ChunkRecordedPayload) => void; - [VoiceBroadcastRecorderEvent.CurrentChunkLengthUpdated]: (length: number) => void; -} - -export interface ChunkRecordedPayload { - buffer: Uint8Array; - length: number; -} - -// char sequence of "OpusHead" -const OpusHead = [79, 112, 117, 115, 72, 101, 97, 100]; - -// char sequence of "OpusTags" -const OpusTags = [79, 112, 117, 115, 84, 97, 103, 115]; - -/** - * This class provides the function to seamlessly record fixed length chunks. - * Subscribe with on(VoiceBroadcastRecordingEvents.ChunkRecorded, (payload: ChunkRecordedPayload) => {}) - * to retrieve chunks while recording. - */ -export class VoiceBroadcastRecorder - extends TypedEventEmitter - implements IDestroyable -{ - private opusHead?: Uint8Array; - private opusTags?: Uint8Array; - private chunkBuffer = new Uint8Array(0); - // position of the previous chunk in seconds - private previousChunkEndTimePosition = 0; - // current chunk length in seconds - private currentChunkLength = 0; - - public constructor( - private voiceRecording: VoiceRecording, - public readonly targetChunkLength: number, - ) { - super(); - this.voiceRecording.onDataAvailable = this.onDataAvailable; - } - - public async start(): Promise { - await this.voiceRecording.start(); - this.voiceRecording.liveData.onUpdate((data: IRecordingUpdate) => { - this.setCurrentChunkLength(data.timeSeconds - this.previousChunkEndTimePosition); - }); - } - - /** - * Stops the recording and returns the remaining chunk (if any). - */ - public async stop(): Promise> { - try { - await this.voiceRecording.stop(); - } catch { - // Ignore if the recording raises any error. - } - - // forget about that call, so that we can stop it again later - Singleflight.forgetAllFor(this.voiceRecording); - const chunk = this.extractChunk(); - this.currentChunkLength = 0; - this.previousChunkEndTimePosition = 0; - return chunk; - } - - public get contentType(): string { - return this.voiceRecording.contentType; - } - - private setCurrentChunkLength(currentChunkLength: number): void { - if (this.currentChunkLength === currentChunkLength) return; - - this.currentChunkLength = currentChunkLength; - this.emit(VoiceBroadcastRecorderEvent.CurrentChunkLengthUpdated, currentChunkLength); - } - - public getCurrentChunkLength(): number { - return this.currentChunkLength; - } - - private onDataAvailable = (data: ArrayBuffer): void => { - const dataArray = new Uint8Array(data); - - // extract the part, that contains the header type info - const headerType = Array.from(dataArray.slice(28, 36)); - - if (isEqual(OpusHead, headerType)) { - // data seems to be an "OpusHead" header - this.opusHead = dataArray; - return; - } - - if (isEqual(OpusTags, headerType)) { - // data seems to be an "OpusTags" header - this.opusTags = dataArray; - return; - } - - this.setCurrentChunkLength(this.voiceRecording.recorderSeconds! - this.previousChunkEndTimePosition); - this.handleData(dataArray); - }; - - private handleData(data: Uint8Array): void { - this.chunkBuffer = concat(this.chunkBuffer, data); - this.emitChunkIfTargetLengthReached(); - } - - private emitChunkIfTargetLengthReached(): void { - if (this.getCurrentChunkLength() >= this.targetChunkLength) { - this.emitAndResetChunk(); - } - } - - /** - * Extracts the current chunk and resets the buffer. - */ - private extractChunk(): Optional { - if (this.chunkBuffer.length === 0) { - return null; - } - - if (!this.opusHead || !this.opusTags) { - logger.warn("Broadcast chunk cannot be extracted. OpusHead or OpusTags is missing."); - return null; - } - - const currentRecorderTime = this.voiceRecording.recorderSeconds!; - const payload: ChunkRecordedPayload = { - buffer: concat(this.opusHead!, this.opusTags!, this.chunkBuffer), - length: this.getCurrentChunkLength(), - }; - this.chunkBuffer = new Uint8Array(0); - this.setCurrentChunkLength(0); - this.previousChunkEndTimePosition = currentRecorderTime; - return payload; - } - - private emitAndResetChunk(): void { - if (this.chunkBuffer.length === 0) { - return; - } - - this.emit(VoiceBroadcastRecorderEvent.ChunkRecorded, this.extractChunk()!); - } - - public destroy(): void { - this.removeAllListeners(); - this.voiceRecording.destroy(); - } -} - -export const createVoiceBroadcastRecorder = (): VoiceBroadcastRecorder => { - const voiceRecording = new VoiceRecording(); - voiceRecording.disableMaxLength(); - return new VoiceBroadcastRecorder(voiceRecording, getChunkLength()); -}; diff --git a/src/voice-broadcast/components/VoiceBroadcastBody.tsx b/src/voice-broadcast/components/VoiceBroadcastBody.tsx deleted file mode 100644 index 916ee9f907..0000000000 --- a/src/voice-broadcast/components/VoiceBroadcastBody.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React, { useContext, useEffect, useState } from "react"; -import { MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix"; - -import { - VoiceBroadcastRecordingBody, - shouldDisplayAsVoiceBroadcastRecordingTile, - VoiceBroadcastInfoEventType, - VoiceBroadcastPlaybackBody, - VoiceBroadcastInfoState, -} from ".."; -import { IBodyProps } from "../../components/views/messages/IBodyProps"; -import { RelationsHelper, RelationsHelperEvent } from "../../events/RelationsHelper"; -import { SDKContext } from "../../contexts/SDKContext"; -import { useMatrixClientContext } from "../../contexts/MatrixClientContext"; - -export const VoiceBroadcastBody: React.FC = ({ mxEvent }) => { - const sdkContext = useContext(SDKContext); - const client = useMatrixClientContext(); - const [infoState, setInfoState] = useState(mxEvent.getContent()?.state || VoiceBroadcastInfoState.Stopped); - - useEffect(() => { - const onInfoEvent = (event: MatrixEvent): void => { - if (event.getContent()?.state === VoiceBroadcastInfoState.Stopped) { - // only a stopped event can change the tile state - setInfoState(VoiceBroadcastInfoState.Stopped); - } - }; - - const relationsHelper = new RelationsHelper( - mxEvent, - RelationType.Reference, - VoiceBroadcastInfoEventType, - client, - ); - relationsHelper.on(RelationsHelperEvent.Add, onInfoEvent); - relationsHelper.emitCurrent(); - - return () => { - relationsHelper.destroy(); - }; - }); - - if (shouldDisplayAsVoiceBroadcastRecordingTile(infoState, client, mxEvent)) { - const recording = sdkContext.voiceBroadcastRecordingsStore.getByInfoEvent(mxEvent, client); - return ; - } - - const playback = sdkContext.voiceBroadcastPlaybacksStore.getByInfoEvent(mxEvent, client); - return ; -}; diff --git a/src/voice-broadcast/components/atoms/LiveBadge.tsx b/src/voice-broadcast/components/atoms/LiveBadge.tsx deleted file mode 100644 index 2591fee435..0000000000 --- a/src/voice-broadcast/components/atoms/LiveBadge.tsx +++ /dev/null @@ -1,30 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import classNames from "classnames"; -import React from "react"; - -import { Icon as LiveIcon } from "../../../../res/img/compound/live-16px.svg"; -import { _t } from "../../../languageHandler"; - -interface Props { - grey?: boolean; -} - -export const LiveBadge: React.FC = ({ grey = false }) => { - const liveBadgeClasses = classNames("mx_LiveBadge", { - "mx_LiveBadge--grey": grey, - }); - - return ( -
- - {_t("voice_broadcast|live")} -
- ); -}; diff --git a/src/voice-broadcast/components/atoms/SeekButton.tsx b/src/voice-broadcast/components/atoms/SeekButton.tsx deleted file mode 100644 index 5ee0826488..0000000000 --- a/src/voice-broadcast/components/atoms/SeekButton.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; - -import AccessibleButton from "../../../components/views/elements/AccessibleButton"; - -interface Props { - icon: React.FC>; - label: string; - onClick: () => void; -} - -export const SeekButton: React.FC = ({ onClick, icon: Icon, label }) => { - return ( - - - - ); -}; diff --git a/src/voice-broadcast/components/atoms/VoiceBroadcastControl.tsx b/src/voice-broadcast/components/atoms/VoiceBroadcastControl.tsx deleted file mode 100644 index 177b8fd732..0000000000 --- a/src/voice-broadcast/components/atoms/VoiceBroadcastControl.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import classNames from "classnames"; -import React, { ReactElement } from "react"; - -import AccessibleButton from "../../../components/views/elements/AccessibleButton"; - -interface Props { - className?: string; - icon: ReactElement; - label: string; - onClick: () => void; -} - -export const VoiceBroadcastControl: React.FC = ({ className = "", icon, label, onClick }) => { - return ( - - {icon} - - ); -}; diff --git a/src/voice-broadcast/components/atoms/VoiceBroadcastError.tsx b/src/voice-broadcast/components/atoms/VoiceBroadcastError.tsx deleted file mode 100644 index d326853f4e..0000000000 --- a/src/voice-broadcast/components/atoms/VoiceBroadcastError.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { WarningIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; - -interface Props { - message: string; -} - -export const VoiceBroadcastError: React.FC = ({ message }) => { - return ( -
- - {message} -
- ); -}; diff --git a/src/voice-broadcast/components/atoms/VoiceBroadcastHeader.tsx b/src/voice-broadcast/components/atoms/VoiceBroadcastHeader.tsx deleted file mode 100644 index 52c0251c5e..0000000000 --- a/src/voice-broadcast/components/atoms/VoiceBroadcastHeader.tsx +++ /dev/null @@ -1,139 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { Room } from "matrix-js-sdk/src/matrix"; -import classNames from "classnames"; -import CloseIcon from "@vector-im/compound-design-tokens/assets/web/icons/close"; -import MicrophoneIcon from "@vector-im/compound-design-tokens/assets/web/icons/mic-on-solid"; - -import { LiveBadge, VoiceBroadcastLiveness } from "../.."; -import { Icon as LiveIcon } from "../../../../res/img/compound/live-16px.svg"; -import { Icon as TimerIcon } from "../../../../res/img/compound/timer-16px.svg"; -import { _t } from "../../../languageHandler"; -import RoomAvatar from "../../../components/views/avatars/RoomAvatar"; -import AccessibleButton, { ButtonEvent } from "../../../components/views/elements/AccessibleButton"; -import Clock from "../../../components/views/audio_messages/Clock"; -import { formatTimeLeft } from "../../../DateUtils"; -import Spinner from "../../../components/views/elements/Spinner"; -import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; -import { Action } from "../../../dispatcher/actions"; -import dis from "../../../dispatcher/dispatcher"; - -interface VoiceBroadcastHeaderProps { - linkToRoom?: boolean; - live?: VoiceBroadcastLiveness; - liveBadgePosition?: "middle" | "right"; - onCloseClick?: () => void; - onMicrophoneLineClick?: ((e: ButtonEvent) => void | Promise) | null; - room: Room; - microphoneLabel?: string; - showBroadcast?: boolean; - showBuffering?: boolean; - bufferingPosition?: "line" | "title"; - timeLeft?: number; - showClose?: boolean; -} - -export const VoiceBroadcastHeader: React.FC = ({ - linkToRoom = false, - live = "not-live", - liveBadgePosition = "right", - onCloseClick = (): void => {}, - onMicrophoneLineClick = null, - room, - microphoneLabel, - showBroadcast = false, - showBuffering = false, - bufferingPosition = "line", - showClose = false, - timeLeft, -}) => { - const broadcast = showBroadcast && ( -
- - {_t("voice_broadcast|action")} -
- ); - - const liveBadge = live !== "not-live" && ; - - const closeButton = showClose && ( - - - - ); - - const timeLeftLine = timeLeft && ( -
- - -
- ); - - const bufferingLine = showBuffering && bufferingPosition === "line" && ( -
- - {_t("voice_broadcast|buffering")} -
- ); - - const microphoneLineClasses = classNames({ - mx_VoiceBroadcastHeader_line: true, - ["mx_VoiceBroadcastHeader_mic--clickable"]: onMicrophoneLineClick, - }); - - const microphoneLine = microphoneLabel && ( - - - {microphoneLabel} - - ); - - const onRoomAvatarOrNameClick = (): void => { - dis.dispatch({ - action: Action.ViewRoom, - room_id: room.roomId, - metricsTrigger: undefined, // other - }); - }; - - let roomAvatar = ; - let roomName = ( -
-
{room.name}
- {showBuffering && bufferingPosition === "title" && } -
- ); - - if (linkToRoom) { - roomAvatar = {roomAvatar}; - - roomName = {roomName}; - } - - return ( -
- {roomAvatar} -
- {roomName} - {microphoneLine} - {timeLeftLine} - {broadcast} - {bufferingLine} - {liveBadgePosition === "middle" && liveBadge} -
- {liveBadgePosition === "right" && liveBadge} - {closeButton} -
- ); -}; diff --git a/src/voice-broadcast/components/atoms/VoiceBroadcastPlaybackControl.tsx b/src/voice-broadcast/components/atoms/VoiceBroadcastPlaybackControl.tsx deleted file mode 100644 index 08531b8afd..0000000000 --- a/src/voice-broadcast/components/atoms/VoiceBroadcastPlaybackControl.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022, 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React, { ReactElement } from "react"; -import PauseIcon from "@vector-im/compound-design-tokens/assets/web/icons/pause-solid"; -import PlayIcon from "@vector-im/compound-design-tokens/assets/web/icons/play-solid"; - -import { _t } from "../../../languageHandler"; -import { VoiceBroadcastControl, VoiceBroadcastPlaybackState } from "../.."; - -interface Props { - onClick: () => void; - state: VoiceBroadcastPlaybackState; -} - -export const VoiceBroadcastPlaybackControl: React.FC = ({ onClick, state }) => { - let controlIcon: ReactElement | null = null; - let controlLabel: string | null = null; - let className = ""; - - switch (state) { - case VoiceBroadcastPlaybackState.Stopped: - controlIcon = ; - className = "mx_VoiceBroadcastControl-play"; - controlLabel = _t("voice_broadcast|play"); - break; - case VoiceBroadcastPlaybackState.Paused: - controlIcon = ; - className = "mx_VoiceBroadcastControl-play"; - controlLabel = _t("voice_broadcast|resume"); - break; - case VoiceBroadcastPlaybackState.Buffering: - case VoiceBroadcastPlaybackState.Playing: - controlIcon = ; - controlLabel = _t("voice_broadcast|pause"); - break; - } - - if (controlIcon && controlLabel) { - return ( - - ); - } - - return null; -}; diff --git a/src/voice-broadcast/components/atoms/VoiceBroadcastRecordingConnectionError.tsx b/src/voice-broadcast/components/atoms/VoiceBroadcastRecordingConnectionError.tsx deleted file mode 100644 index 250d71f2f3..0000000000 --- a/src/voice-broadcast/components/atoms/VoiceBroadcastRecordingConnectionError.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { WarningIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; - -import { _t } from "../../../languageHandler"; - -export const VoiceBroadcastRecordingConnectionError: React.FC = () => { - return ( -
- - {_t("voice_broadcast|connection_error")} -
- ); -}; diff --git a/src/voice-broadcast/components/atoms/VoiceBroadcastRoomSubtitle.tsx b/src/voice-broadcast/components/atoms/VoiceBroadcastRoomSubtitle.tsx deleted file mode 100644 index 20b7379789..0000000000 --- a/src/voice-broadcast/components/atoms/VoiceBroadcastRoomSubtitle.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; - -import { Icon as LiveIcon } from "../../../../res/img/compound/live-16px.svg"; -import { _t } from "../../../languageHandler"; - -export const VoiceBroadcastRoomSubtitle: React.FC = () => { - return ( -
- - {_t("voice_broadcast|live")} -
- ); -}; diff --git a/src/voice-broadcast/components/molecules/ConfirmListenBroadcastStopCurrent.tsx b/src/voice-broadcast/components/molecules/ConfirmListenBroadcastStopCurrent.tsx deleted file mode 100644 index 3dadfeba60..0000000000 --- a/src/voice-broadcast/components/molecules/ConfirmListenBroadcastStopCurrent.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; - -import BaseDialog from "../../../components/views/dialogs/BaseDialog"; -import DialogButtons from "../../../components/views/elements/DialogButtons"; -import { _t } from "../../../languageHandler"; -import Modal from "../../../Modal"; - -interface Props { - onFinished: (confirmed?: boolean) => void; -} - -export const ConfirmListenBroadcastStopCurrentDialog: React.FC = ({ onFinished }) => { - return ( - -

{_t("voice_broadcast|confirm_listen_description")}

- onFinished(true)} - primaryButton={_t("voice_broadcast|confirm_listen_affirm")} - cancelButton={_t("action|no")} - onCancel={() => onFinished(false)} - /> -
- ); -}; - -export const showConfirmListenBroadcastStopCurrentDialog = async (): Promise => { - const { finished } = Modal.createDialog(ConfirmListenBroadcastStopCurrentDialog); - const [confirmed] = await finished; - return !!confirmed; -}; diff --git a/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx b/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx deleted file mode 100644 index 913b144960..0000000000 --- a/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx +++ /dev/null @@ -1,102 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React, { ReactElement } from "react"; -import classNames from "classnames"; - -import { - VoiceBroadcastError, - VoiceBroadcastHeader, - VoiceBroadcastPlayback, - VoiceBroadcastPlaybackControl, - VoiceBroadcastPlaybackState, -} from "../.."; -import { useVoiceBroadcastPlayback } from "../../hooks/useVoiceBroadcastPlayback"; -import { Icon as Back30sIcon } from "../../../../res/img/compound/back-30s-24px.svg"; -import { Icon as Forward30sIcon } from "../../../../res/img/compound/forward-30s-24px.svg"; -import { _t } from "../../../languageHandler"; -import Clock from "../../../components/views/audio_messages/Clock"; -import SeekBar from "../../../components/views/audio_messages/SeekBar"; -import { SeekButton } from "../atoms/SeekButton"; - -const SEEK_TIME = 30; - -interface VoiceBroadcastPlaybackBodyProps { - pip?: boolean; - playback: VoiceBroadcastPlayback; -} - -export const VoiceBroadcastPlaybackBody: React.FC = ({ pip = false, playback }) => { - const { times, liveness, playbackState, room, sender, toggle } = useVoiceBroadcastPlayback(playback); - - let seekBackwardButton: ReactElement | null = null; - let seekForwardButton: ReactElement | null = null; - - if (playbackState !== VoiceBroadcastPlaybackState.Stopped) { - const onSeekBackwardButtonClick = (): void => { - playback.skipTo(Math.max(0, times.position - SEEK_TIME)); - }; - - seekBackwardButton = ( - - ); - - const onSeekForwardButtonClick = (): void => { - playback.skipTo(Math.min(times.duration, times.position + SEEK_TIME)); - }; - - seekForwardButton = ( - - ); - } - - const classes = classNames({ - mx_VoiceBroadcastBody: true, - ["mx_VoiceBroadcastBody--pip"]: pip, - }); - - const content = - playbackState === VoiceBroadcastPlaybackState.Error ? ( - - ) : ( - <> -
- {seekBackwardButton} - - {seekForwardButton} -
- -
- - -
- - ); - - return ( -
- - {content} -
- ); -}; diff --git a/src/voice-broadcast/components/molecules/VoiceBroadcastPreRecordingPip.tsx b/src/voice-broadcast/components/molecules/VoiceBroadcastPreRecordingPip.tsx deleted file mode 100644 index ac742e0fd8..0000000000 --- a/src/voice-broadcast/components/molecules/VoiceBroadcastPreRecordingPip.tsx +++ /dev/null @@ -1,82 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React, { useRef, useState } from "react"; - -import { VoiceBroadcastHeader } from "../.."; -import AccessibleButton from "../../../components/views/elements/AccessibleButton"; -import { VoiceBroadcastPreRecording } from "../../models/VoiceBroadcastPreRecording"; -import { Icon as LiveIcon } from "../../../../res/img/compound/live-16px.svg"; -import { _t } from "../../../languageHandler"; -import { useAudioDeviceSelection } from "../../../hooks/useAudioDeviceSelection"; -import { DevicesContextMenu } from "../../../components/views/audio_messages/DevicesContextMenu"; - -interface Props { - voiceBroadcastPreRecording: VoiceBroadcastPreRecording; -} - -interface State { - showDeviceSelect: boolean; - disableStartButton: boolean; -} - -export const VoiceBroadcastPreRecordingPip: React.FC = ({ voiceBroadcastPreRecording }) => { - const pipRef = useRef(null); - const { currentDevice, currentDeviceLabel, devices, setDevice } = useAudioDeviceSelection(); - const [state, setState] = useState({ - showDeviceSelect: false, - disableStartButton: false, - }); - - const onDeviceSelect = (device: MediaDeviceInfo): void => { - setState((state) => ({ - ...state, - showDeviceSelect: false, - })); - setDevice(device); - }; - - const onStartBroadcastClick = (): void => { - setState((state) => ({ - ...state, - disableStartButton: true, - })); - - voiceBroadcastPreRecording.start(); - }; - - return ( -
- setState({ ...state, showDeviceSelect: true })} - room={voiceBroadcastPreRecording.room} - microphoneLabel={currentDeviceLabel} - showClose={true} - /> - - - {_t("voice_broadcast|go_live")} - - {state.showDeviceSelect && ( - - )} -
- ); -}; diff --git a/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody.tsx b/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody.tsx deleted file mode 100644 index 15547792db..0000000000 --- a/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; - -import { - useVoiceBroadcastRecording, - VoiceBroadcastHeader, - VoiceBroadcastRecording, - VoiceBroadcastRecordingConnectionError, -} from "../.."; - -interface VoiceBroadcastRecordingBodyProps { - recording: VoiceBroadcastRecording; -} - -export const VoiceBroadcastRecordingBody: React.FC = ({ recording }) => { - const { live, room, sender, recordingState } = useVoiceBroadcastRecording(recording); - - return ( -
- - {recordingState === "connection_error" && } -
- ); -}; diff --git a/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip.tsx b/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip.tsx deleted file mode 100644 index d04132b220..0000000000 --- a/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip.tsx +++ /dev/null @@ -1,116 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React, { useRef, useState } from "react"; -import PauseIcon from "@vector-im/compound-design-tokens/assets/web/icons/pause-solid"; -import MicrophoneIcon from "@vector-im/compound-design-tokens/assets/web/icons/mic-on-solid"; - -import { - VoiceBroadcastControl, - VoiceBroadcastInfoState, - VoiceBroadcastRecording, - VoiceBroadcastRecordingConnectionError, - VoiceBroadcastRecordingState, -} from "../.."; -import { useVoiceBroadcastRecording } from "../../hooks/useVoiceBroadcastRecording"; -import { VoiceBroadcastHeader } from "../atoms/VoiceBroadcastHeader"; -import { Icon as StopIcon } from "../../../../res/img/compound/stop-16.svg"; -import { Icon as RecordIcon } from "../../../../res/img/compound/record-10px.svg"; -import { _t } from "../../../languageHandler"; -import { useAudioDeviceSelection } from "../../../hooks/useAudioDeviceSelection"; -import { DevicesContextMenu } from "../../../components/views/audio_messages/DevicesContextMenu"; -import AccessibleButton from "../../../components/views/elements/AccessibleButton"; - -interface VoiceBroadcastRecordingPipProps { - recording: VoiceBroadcastRecording; -} - -export const VoiceBroadcastRecordingPip: React.FC = ({ recording }) => { - const pipRef = useRef(null); - const { live, timeLeft, recordingState, room, stopRecording, toggleRecording } = - useVoiceBroadcastRecording(recording); - const { currentDevice, devices, setDevice } = useAudioDeviceSelection(); - - const onDeviceSelect = async (device: MediaDeviceInfo): Promise => { - setShowDeviceSelect(false); - - if (currentDevice?.deviceId === device.deviceId) { - // device unchanged - return; - } - - setDevice(device); - - if ( - ( - [VoiceBroadcastInfoState.Paused, VoiceBroadcastInfoState.Stopped] as VoiceBroadcastRecordingState[] - ).includes(recordingState) - ) { - // Nothing to do in these cases. Resume will use the selected device. - return; - } - - // pause and resume to switch the input device - await recording.pause(); - await recording.resume(); - }; - - const [showDeviceSelect, setShowDeviceSelect] = useState(false); - - const toggleControl = - recordingState === VoiceBroadcastInfoState.Paused ? ( - } - label={_t("voice_broadcast|resume")} - /> - ) : ( - } - label={_t("voice_broadcast|pause")} - /> - ); - - const controls = - recordingState === "connection_error" ? ( - - ) : ( -
- {toggleControl} - setShowDeviceSelect(true)} - title={_t("voip|change_input_device")} - > - - - } - label="Stop Recording" - onClick={stopRecording} - /> -
- ); - - return ( -
- -
- {controls} - {showDeviceSelect && ( - - )} -
- ); -}; diff --git a/src/voice-broadcast/components/molecules/VoiceBroadcastSmallPlaybackBody.tsx b/src/voice-broadcast/components/molecules/VoiceBroadcastSmallPlaybackBody.tsx deleted file mode 100644 index a791ac75d7..0000000000 --- a/src/voice-broadcast/components/molecules/VoiceBroadcastSmallPlaybackBody.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import CloseIcon from "@vector-im/compound-design-tokens/assets/web/icons/close"; - -import { - VoiceBroadcastHeader, - VoiceBroadcastPlayback, - VoiceBroadcastPlaybackControl, - VoiceBroadcastPlaybackState, -} from "../.."; -import AccessibleButton from "../../../components/views/elements/AccessibleButton"; -import { useVoiceBroadcastPlayback } from "../../hooks/useVoiceBroadcastPlayback"; - -interface VoiceBroadcastSmallPlaybackBodyProps { - playback: VoiceBroadcastPlayback; -} - -export const VoiceBroadcastSmallPlaybackBody: React.FC = ({ playback }) => { - const { liveness, playbackState, room, sender, toggle } = useVoiceBroadcastPlayback(playback); - return ( -
- - - playback.stop()}> - - -
- ); -}; diff --git a/src/voice-broadcast/hooks/useCurrentVoiceBroadcastPlayback.ts b/src/voice-broadcast/hooks/useCurrentVoiceBroadcastPlayback.ts deleted file mode 100644 index 3ff4081a9f..0000000000 --- a/src/voice-broadcast/hooks/useCurrentVoiceBroadcastPlayback.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { useTypedEventEmitterState } from "../../hooks/useEventEmitter"; -import { VoiceBroadcastPlayback } from "../models/VoiceBroadcastPlayback"; -import { - VoiceBroadcastPlaybacksStore, - VoiceBroadcastPlaybacksStoreEvent, -} from "../stores/VoiceBroadcastPlaybacksStore"; - -export const useCurrentVoiceBroadcastPlayback = ( - voiceBroadcastPlaybackStore: VoiceBroadcastPlaybacksStore, -): { - currentVoiceBroadcastPlayback: VoiceBroadcastPlayback | null; -} => { - const currentVoiceBroadcastPlayback = useTypedEventEmitterState( - voiceBroadcastPlaybackStore, - VoiceBroadcastPlaybacksStoreEvent.CurrentChanged, - (playback?: VoiceBroadcastPlayback) => { - return playback ?? voiceBroadcastPlaybackStore.getCurrent(); - }, - ); - - return { - currentVoiceBroadcastPlayback, - }; -}; diff --git a/src/voice-broadcast/hooks/useCurrentVoiceBroadcastPreRecording.ts b/src/voice-broadcast/hooks/useCurrentVoiceBroadcastPreRecording.ts deleted file mode 100644 index bb14e38640..0000000000 --- a/src/voice-broadcast/hooks/useCurrentVoiceBroadcastPreRecording.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { useTypedEventEmitterState } from "../../hooks/useEventEmitter"; -import { VoiceBroadcastPreRecordingStore } from "../stores/VoiceBroadcastPreRecordingStore"; -import { VoiceBroadcastPreRecording } from "../models/VoiceBroadcastPreRecording"; - -export const useCurrentVoiceBroadcastPreRecording = ( - voiceBroadcastPreRecordingStore: VoiceBroadcastPreRecordingStore, -): { - currentVoiceBroadcastPreRecording: VoiceBroadcastPreRecording | null; -} => { - const currentVoiceBroadcastPreRecording = useTypedEventEmitterState( - voiceBroadcastPreRecordingStore, - "changed", - (preRecording?: VoiceBroadcastPreRecording) => { - return preRecording ?? voiceBroadcastPreRecordingStore.getCurrent(); - }, - ); - - return { - currentVoiceBroadcastPreRecording, - }; -}; diff --git a/src/voice-broadcast/hooks/useCurrentVoiceBroadcastRecording.ts b/src/voice-broadcast/hooks/useCurrentVoiceBroadcastRecording.ts deleted file mode 100644 index 1d4abe3f10..0000000000 --- a/src/voice-broadcast/hooks/useCurrentVoiceBroadcastRecording.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { VoiceBroadcastRecording, VoiceBroadcastRecordingsStore, VoiceBroadcastRecordingsStoreEvent } from ".."; -import { useTypedEventEmitterState } from "../../hooks/useEventEmitter"; - -export const useCurrentVoiceBroadcastRecording = ( - voiceBroadcastRecordingsStore: VoiceBroadcastRecordingsStore, -): { - currentVoiceBroadcastRecording: VoiceBroadcastRecording | null; -} => { - const currentVoiceBroadcastRecording = useTypedEventEmitterState( - voiceBroadcastRecordingsStore, - VoiceBroadcastRecordingsStoreEvent.CurrentChanged, - (recording?: VoiceBroadcastRecording) => { - return recording ?? voiceBroadcastRecordingsStore.getCurrent(); - }, - ); - - return { - currentVoiceBroadcastRecording, - }; -}; diff --git a/src/voice-broadcast/hooks/useHasRoomLiveVoiceBroadcast.ts b/src/voice-broadcast/hooks/useHasRoomLiveVoiceBroadcast.ts deleted file mode 100644 index a298f4dc83..0000000000 --- a/src/voice-broadcast/hooks/useHasRoomLiveVoiceBroadcast.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { useContext, useEffect, useMemo, useState } from "react"; -import { Room, RoomStateEvent } from "matrix-js-sdk/src/matrix"; - -import { hasRoomLiveVoiceBroadcast } from "../utils/hasRoomLiveVoiceBroadcast"; -import { useTypedEventEmitter } from "../../hooks/useEventEmitter"; -import { SDKContext } from "../../contexts/SDKContext"; - -export const useHasRoomLiveVoiceBroadcast = (room: Room): boolean => { - const sdkContext = useContext(SDKContext); - const [hasLiveVoiceBroadcast, setHasLiveVoiceBroadcast] = useState(false); - - const update = useMemo(() => { - return sdkContext?.client - ? () => { - hasRoomLiveVoiceBroadcast(sdkContext.client!, room).then( - ({ hasBroadcast }) => { - setHasLiveVoiceBroadcast(hasBroadcast); - }, - () => {}, // no update on error - ); - } - : () => {}; // noop without client - }, [room, sdkContext, setHasLiveVoiceBroadcast]); - - useEffect(() => { - update(); - }, [update]); - - useTypedEventEmitter(room.currentState, RoomStateEvent.Update, () => update()); - return hasLiveVoiceBroadcast; -}; diff --git a/src/voice-broadcast/hooks/useVoiceBroadcastPlayback.ts b/src/voice-broadcast/hooks/useVoiceBroadcastPlayback.ts deleted file mode 100644 index eb50b0de08..0000000000 --- a/src/voice-broadcast/hooks/useVoiceBroadcastPlayback.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022, 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { Room, RoomMember } from "matrix-js-sdk/src/matrix"; - -import { useTypedEventEmitterState } from "../../hooks/useEventEmitter"; -import { MatrixClientPeg } from "../../MatrixClientPeg"; -import { - VoiceBroadcastLiveness, - VoiceBroadcastPlayback, - VoiceBroadcastPlaybackEvent, - VoiceBroadcastPlaybackState, - VoiceBroadcastPlaybackTimes, -} from ".."; - -export const useVoiceBroadcastPlayback = ( - playback: VoiceBroadcastPlayback, -): { - times: { - duration: number; - position: number; - timeLeft: number; - }; - sender: RoomMember | null; - liveness: VoiceBroadcastLiveness; - playbackState: VoiceBroadcastPlaybackState; - toggle(): void; - room: Room; -} => { - const client = MatrixClientPeg.safeGet(); - const room = client.getRoom(playback.infoEvent.getRoomId()); - - if (!room) { - throw new Error(`Voice Broadcast room not found (event ${playback.infoEvent.getId()})`); - } - - const sender = playback.infoEvent.sender; - - if (!sender) { - throw new Error(`Voice Broadcast sender not found (event ${playback.infoEvent.getId()})`); - } - - const playbackToggle = (): void => { - playback.toggle(); - }; - - const playbackState = useTypedEventEmitterState( - playback, - VoiceBroadcastPlaybackEvent.StateChanged, - (state?: VoiceBroadcastPlaybackState) => { - return state ?? playback.getState(); - }, - ); - - const times = useTypedEventEmitterState( - playback, - VoiceBroadcastPlaybackEvent.TimesChanged, - (t?: VoiceBroadcastPlaybackTimes) => { - return ( - t ?? { - duration: playback.durationSeconds, - position: playback.timeSeconds, - timeLeft: playback.timeLeftSeconds, - } - ); - }, - ); - - const liveness = useTypedEventEmitterState( - playback, - VoiceBroadcastPlaybackEvent.LivenessChanged, - (l?: VoiceBroadcastLiveness) => { - return l ?? playback.getLiveness(); - }, - ); - - return { - times, - liveness: liveness, - playbackState, - room: room, - sender, - toggle: playbackToggle, - }; -}; diff --git a/src/voice-broadcast/hooks/useVoiceBroadcastRecording.tsx b/src/voice-broadcast/hooks/useVoiceBroadcastRecording.tsx deleted file mode 100644 index fa3c635bc9..0000000000 --- a/src/voice-broadcast/hooks/useVoiceBroadcastRecording.tsx +++ /dev/null @@ -1,96 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022, 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { Room, RoomMember } from "matrix-js-sdk/src/matrix"; -import React from "react"; - -import { - VoiceBroadcastInfoState, - VoiceBroadcastRecording, - VoiceBroadcastRecordingEvent, - VoiceBroadcastRecordingState, -} from ".."; -import QuestionDialog from "../../components/views/dialogs/QuestionDialog"; -import { useTypedEventEmitterState } from "../../hooks/useEventEmitter"; -import { _t } from "../../languageHandler"; -import { MatrixClientPeg } from "../../MatrixClientPeg"; -import Modal from "../../Modal"; - -const showStopBroadcastingDialog = async (): Promise => { - const { finished } = Modal.createDialog(QuestionDialog, { - title: _t("voice_broadcast|confirm_stop_title"), - description:

{_t("voice_broadcast|confirm_stop_description")}

, - button: _t("voice_broadcast|confirm_stop_affirm"), - }); - const [confirmed] = await finished; - return !!confirmed; -}; - -export const useVoiceBroadcastRecording = ( - recording: VoiceBroadcastRecording, -): { - live: boolean; - timeLeft: number; - recordingState: VoiceBroadcastRecordingState; - room: Room; - sender: RoomMember | null; - stopRecording(): void; - toggleRecording(): void; -} => { - const client = MatrixClientPeg.safeGet(); - const roomId = recording.infoEvent.getRoomId(); - const room = client.getRoom(roomId); - - if (!room) { - throw new Error("Unable to find voice broadcast room with Id: " + roomId); - } - - const sender = recording.infoEvent.sender; - - if (!sender) { - throw new Error(`Voice Broadcast sender not found (event ${recording.infoEvent.getId()})`); - } - - const stopRecording = async (): Promise => { - const confirmed = await showStopBroadcastingDialog(); - - if (confirmed) { - await recording.stop(); - } - }; - - const recordingState = useTypedEventEmitterState( - recording, - VoiceBroadcastRecordingEvent.StateChanged, - (state?: VoiceBroadcastRecordingState) => { - return state ?? recording.getState(); - }, - ); - - const timeLeft = useTypedEventEmitterState( - recording, - VoiceBroadcastRecordingEvent.TimeLeftChanged, - (t?: number) => { - return t ?? recording.getTimeLeft(); - }, - ); - - const live = ( - [VoiceBroadcastInfoState.Started, VoiceBroadcastInfoState.Resumed] as VoiceBroadcastRecordingState[] - ).includes(recordingState); - - return { - live, - timeLeft, - recordingState, - room, - sender, - stopRecording, - toggleRecording: recording.toggle, - }; -}; diff --git a/src/voice-broadcast/index.ts b/src/voice-broadcast/index.ts deleted file mode 100644 index 712c25fdc2..0000000000 --- a/src/voice-broadcast/index.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -/** - * Voice Broadcast module - * {@link https://github.com/vector-im/element-meta/discussions/632} - */ - -export * from "./types"; -export * from "./models/VoiceBroadcastPlayback"; -export * from "./models/VoiceBroadcastPreRecording"; -export * from "./models/VoiceBroadcastRecording"; -export * from "./audio/VoiceBroadcastRecorder"; -export * from "./components/VoiceBroadcastBody"; -export * from "./components/atoms/LiveBadge"; -export * from "./components/atoms/VoiceBroadcastControl"; -export * from "./components/atoms/VoiceBroadcastError"; -export * from "./components/atoms/VoiceBroadcastHeader"; -export * from "./components/atoms/VoiceBroadcastPlaybackControl"; -export * from "./components/atoms/VoiceBroadcastRecordingConnectionError"; -export * from "./components/atoms/VoiceBroadcastRoomSubtitle"; -export * from "./components/molecules/ConfirmListenBroadcastStopCurrent"; -export * from "./components/molecules/VoiceBroadcastPlaybackBody"; -export * from "./components/molecules/VoiceBroadcastSmallPlaybackBody"; -export * from "./components/molecules/VoiceBroadcastPreRecordingPip"; -export * from "./components/molecules/VoiceBroadcastRecordingBody"; -export * from "./components/molecules/VoiceBroadcastRecordingPip"; -export * from "./hooks/useCurrentVoiceBroadcastPreRecording"; -export * from "./hooks/useCurrentVoiceBroadcastRecording"; -export * from "./hooks/useHasRoomLiveVoiceBroadcast"; -export * from "./hooks/useVoiceBroadcastRecording"; -export * from "./stores/VoiceBroadcastPlaybacksStore"; -export * from "./stores/VoiceBroadcastPreRecordingStore"; -export * from "./stores/VoiceBroadcastRecordingsStore"; -export * from "./utils/checkVoiceBroadcastPreConditions"; -export * from "./utils/cleanUpBroadcasts"; -export * from "./utils/doClearCurrentVoiceBroadcastPlaybackIfStopped"; -export * from "./utils/doMaybeSetCurrentVoiceBroadcastPlayback"; -export * from "./utils/getChunkLength"; -export * from "./utils/getMaxBroadcastLength"; -export * from "./utils/hasRoomLiveVoiceBroadcast"; -export * from "./utils/isRelatedToVoiceBroadcast"; -export * from "./utils/isVoiceBroadcastStartedEvent"; -export * from "./utils/findRoomLiveVoiceBroadcastFromUserAndDevice"; -export * from "./utils/retrieveStartedInfoEvent"; -export * from "./utils/shouldDisplayAsVoiceBroadcastRecordingTile"; -export * from "./utils/shouldDisplayAsVoiceBroadcastTile"; -export * from "./utils/shouldDisplayAsVoiceBroadcastStoppedText"; -export * from "./utils/startNewVoiceBroadcastRecording"; -export * from "./utils/textForVoiceBroadcastStoppedEvent"; -export * from "./utils/textForVoiceBroadcastStoppedEventWithoutLink"; -export * from "./utils/VoiceBroadcastResumer"; diff --git a/src/voice-broadcast/models/VoiceBroadcastPlayback.ts b/src/voice-broadcast/models/VoiceBroadcastPlayback.ts deleted file mode 100644 index ce6215312f..0000000000 --- a/src/voice-broadcast/models/VoiceBroadcastPlayback.ts +++ /dev/null @@ -1,651 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { - EventType, - MatrixClient, - MatrixEvent, - MatrixEventEvent, - MsgType, - RelationType, - TypedEventEmitter, -} from "matrix-js-sdk/src/matrix"; -import { SimpleObservable } from "matrix-widget-api"; -import { logger } from "matrix-js-sdk/src/logger"; -import { defer, IDeferred } from "matrix-js-sdk/src/utils"; - -import { Playback, PlaybackInterface, PlaybackState } from "../../audio/Playback"; -import { PlaybackManager } from "../../audio/PlaybackManager"; -import { UPDATE_EVENT } from "../../stores/AsyncStore"; -import { MediaEventHelper } from "../../utils/MediaEventHelper"; -import { IDestroyable } from "../../utils/IDestroyable"; -import { - VoiceBroadcastLiveness, - VoiceBroadcastInfoEventType, - VoiceBroadcastInfoState, - VoiceBroadcastInfoEventContent, - VoiceBroadcastRecordingsStore, - showConfirmListenBroadcastStopCurrentDialog, -} from ".."; -import { RelationsHelper, RelationsHelperEvent } from "../../events/RelationsHelper"; -import { VoiceBroadcastChunkEvents } from "../utils/VoiceBroadcastChunkEvents"; -import { determineVoiceBroadcastLiveness } from "../utils/determineVoiceBroadcastLiveness"; -import { _t } from "../../languageHandler"; - -export enum VoiceBroadcastPlaybackState { - Paused = "pause", - Playing = "playing", - Stopped = "stopped", - Buffering = "buffering", - Error = "error", -} - -export enum VoiceBroadcastPlaybackEvent { - TimesChanged = "times_changed", - LivenessChanged = "liveness_changed", - StateChanged = "state_changed", - InfoStateChanged = "info_state_changed", -} - -export type VoiceBroadcastPlaybackTimes = { - duration: number; - position: number; - timeLeft: number; -}; - -interface EventMap { - [VoiceBroadcastPlaybackEvent.TimesChanged]: (times: VoiceBroadcastPlaybackTimes) => void; - [VoiceBroadcastPlaybackEvent.LivenessChanged]: (liveness: VoiceBroadcastLiveness) => void; - [VoiceBroadcastPlaybackEvent.StateChanged]: ( - state: VoiceBroadcastPlaybackState, - playback: VoiceBroadcastPlayback, - ) => void; - [VoiceBroadcastPlaybackEvent.InfoStateChanged]: (state: VoiceBroadcastInfoState) => void; -} - -export class VoiceBroadcastPlayback - extends TypedEventEmitter - implements IDestroyable, PlaybackInterface -{ - private state = VoiceBroadcastPlaybackState.Stopped; - private chunkEvents = new VoiceBroadcastChunkEvents(); - /** @var Map: event Id → undecryptable event */ - private utdChunkEvents: Map = new Map(); - private playbacks = new Map(); - private currentlyPlaying: MatrixEvent | null = null; - /** @var total duration of all chunks in milliseconds */ - private duration = 0; - /** @var current playback position in milliseconds */ - private position = 0; - public readonly liveData = new SimpleObservable(); - private liveness: VoiceBroadcastLiveness = "not-live"; - - // set via addInfoEvent() in constructor - private infoState!: VoiceBroadcastInfoState; - private lastInfoEvent!: MatrixEvent; - - // set via setUpRelationsHelper() in constructor - private chunkRelationHelper!: RelationsHelper; - private infoRelationHelper!: RelationsHelper; - - private skipToNext?: number; - private skipToDeferred?: IDeferred; - - public constructor( - public readonly infoEvent: MatrixEvent, - private client: MatrixClient, - private recordings: VoiceBroadcastRecordingsStore, - ) { - super(); - this.addInfoEvent(this.infoEvent); - this.infoEvent.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction); - this.setUpRelationsHelper(); - } - - private async setUpRelationsHelper(): Promise { - this.infoRelationHelper = new RelationsHelper( - this.infoEvent, - RelationType.Reference, - VoiceBroadcastInfoEventType, - this.client, - ); - this.infoRelationHelper.getCurrent().forEach(this.addInfoEvent); - - if (this.infoState !== VoiceBroadcastInfoState.Stopped) { - // Only required if not stopped. Stopped is the final state. - this.infoRelationHelper.on(RelationsHelperEvent.Add, this.addInfoEvent); - - try { - await this.infoRelationHelper.emitFetchCurrent(); - } catch (err) { - logger.warn("error fetching server side relation for voice broadcast info", err); - // fall back to local events - this.infoRelationHelper.emitCurrent(); - } - } - - this.chunkRelationHelper = new RelationsHelper( - this.infoEvent, - RelationType.Reference, - EventType.RoomMessage, - this.client, - ); - this.chunkRelationHelper.on(RelationsHelperEvent.Add, this.addChunkEvent); - - try { - // TODO Michael W: only fetch events if needed, blocked by PSF-1708 - await this.chunkRelationHelper.emitFetchCurrent(); - } catch (err) { - logger.warn("error fetching server side relation for voice broadcast chunks", err); - // fall back to local events - this.chunkRelationHelper.emitCurrent(); - } - } - - private addChunkEvent = async (event: MatrixEvent): Promise => { - if (!event.getId() && !event.getTxnId()) { - // skip events without id and txn id - return false; - } - - if (event.isDecryptionFailure()) { - this.onChunkEventDecryptionFailure(event); - return false; - } - - if (event.getContent()?.msgtype !== MsgType.Audio) { - // skip non-audio event - return false; - } - - this.chunkEvents.addEvent(event); - this.setDuration(this.chunkEvents.getLength()); - - if (this.getState() === VoiceBroadcastPlaybackState.Buffering) { - await this.startOrPlayNext(); - } - - return true; - }; - - private onChunkEventDecryptionFailure = (event: MatrixEvent): void => { - const eventId = event.getId(); - - if (!eventId) { - // This should not happen, as the existence of the Id is checked before the call. - // Log anyway and return. - logger.warn("Broadcast chunk decryption failure for event without Id", { - broadcast: this.infoEvent.getId(), - }); - return; - } - - if (!this.utdChunkEvents.has(eventId)) { - event.once(MatrixEventEvent.Decrypted, this.onChunkEventDecrypted); - } - - this.utdChunkEvents.set(eventId, event); - this.setError(); - }; - - private onChunkEventDecrypted = async (event: MatrixEvent): Promise => { - const eventId = event.getId(); - - if (!eventId) { - // This should not happen, as the existence of the Id is checked before the call. - // Log anyway and return. - logger.warn("Broadcast chunk decrypted for event without Id", { broadcast: this.infoEvent.getId() }); - return; - } - - this.utdChunkEvents.delete(eventId); - await this.addChunkEvent(event); - - if (this.utdChunkEvents.size === 0) { - // no more UTD events, recover from error to paused - this.setState(VoiceBroadcastPlaybackState.Paused); - } - }; - - private startOrPlayNext = async (): Promise => { - if (this.currentlyPlaying) { - return this.playNext(); - } - - return await this.start(); - }; - - private addInfoEvent = (event: MatrixEvent): void => { - if (this.lastInfoEvent && this.lastInfoEvent.getTs() >= event.getTs()) { - // Only handle newer events - return; - } - - const state = event.getContent()?.state; - - if (!Object.values(VoiceBroadcastInfoState).includes(state)) { - // Do not handle unknown voice broadcast states - return; - } - - this.lastInfoEvent = event; - this.setInfoState(state); - }; - - private onBeforeRedaction = (): void => { - if (this.getState() !== VoiceBroadcastPlaybackState.Stopped) { - this.stop(); - // destroy cleans up everything - this.destroy(); - } - }; - - private async tryLoadPlayback(chunkEvent: MatrixEvent): Promise { - try { - return await this.loadPlayback(chunkEvent); - } catch (err: any) { - logger.warn("Unable to load broadcast playback", { - message: err.message, - broadcastId: this.infoEvent.getId(), - chunkId: chunkEvent.getId(), - }); - this.setError(); - } - } - - private async loadPlayback(chunkEvent: MatrixEvent): Promise { - const eventId = chunkEvent.getId(); - - if (!eventId) { - throw new Error("Broadcast chunk event without Id occurred"); - } - - const helper = new MediaEventHelper(chunkEvent); - const blob = await helper.sourceBlob.value; - const buffer = await blob.arrayBuffer(); - const playback = PlaybackManager.instance.createPlaybackInstance(buffer); - await playback.prepare(); - playback.clockInfo.populatePlaceholdersFrom(chunkEvent); - this.playbacks.set(eventId, playback); - playback.on(UPDATE_EVENT, (state) => this.onPlaybackStateChange(chunkEvent, state)); - playback.clockInfo.liveData.onUpdate(([position]) => { - this.onPlaybackPositionUpdate(chunkEvent, position); - }); - } - - private unloadPlayback(event: MatrixEvent): void { - const playback = this.playbacks.get(event.getId()!); - if (!playback) return; - - playback.destroy(); - this.playbacks.delete(event.getId()!); - } - - private onPlaybackPositionUpdate = (event: MatrixEvent, position: number): void => { - if (event !== this.currentlyPlaying) return; - - const newPosition = this.chunkEvents.getLengthTo(event) + position * 1000; // observable sends seconds - - // do not jump backwards - this can happen when transiting from one to another chunk - if (newPosition < this.position) return; - - this.setPosition(newPosition); - }; - - private setDuration(duration: number): void { - if (this.duration === duration) return; - - this.duration = duration; - this.emitTimesChanged(); - this.liveData.update([this.timeSeconds, this.durationSeconds]); - } - - private setPosition(position: number): void { - if (this.position === position) return; - - this.position = position; - this.emitTimesChanged(); - this.liveData.update([this.timeSeconds, this.durationSeconds]); - } - - private emitTimesChanged(): void { - this.emit(VoiceBroadcastPlaybackEvent.TimesChanged, { - duration: this.durationSeconds, - position: this.timeSeconds, - timeLeft: this.timeLeftSeconds, - }); - } - - private onPlaybackStateChange = async (event: MatrixEvent, newState: PlaybackState): Promise => { - if (event !== this.currentlyPlaying) return; - if (newState !== PlaybackState.Stopped) return; - - await this.playNext(); - this.unloadPlayback(event); - }; - - private async playNext(): Promise { - if (!this.currentlyPlaying) return; - - const next = this.chunkEvents.getNext(this.currentlyPlaying); - - if (next) { - return this.playEvent(next); - } - - if ( - this.getInfoState() === VoiceBroadcastInfoState.Stopped && - this.chunkEvents.getSequenceForEvent(this.currentlyPlaying) === this.lastChunkSequence - ) { - this.stop(); - } else { - // No more chunks available, although the broadcast is not finished → enter buffering state. - this.setState(VoiceBroadcastPlaybackState.Buffering); - } - } - - /** - * @returns {number} The last chunk sequence from the latest info event. - * Falls back to the length of received chunks if the info event does not provide the number. - */ - private get lastChunkSequence(): number { - return ( - this.lastInfoEvent.getContent()?.last_chunk_sequence || - this.chunkEvents.getNumberOfEvents() - ); - } - - private async playEvent(event: MatrixEvent): Promise { - this.setState(VoiceBroadcastPlaybackState.Playing); - this.currentlyPlaying = event; - const playback = await this.tryGetOrLoadPlaybackForEvent(event); - playback?.play(); - } - - private async tryGetOrLoadPlaybackForEvent(event: MatrixEvent): Promise { - try { - return await this.getOrLoadPlaybackForEvent(event); - } catch (err: any) { - logger.warn("Unable to load broadcast playback", { - message: err.message, - broadcastId: this.infoEvent.getId(), - chunkId: event.getId(), - }); - this.setError(); - } - } - - private async getOrLoadPlaybackForEvent(event: MatrixEvent): Promise { - const eventId = event.getId(); - - if (!eventId) { - throw new Error("Broadcast chunk event without Id occurred"); - } - - if (!this.playbacks.has(eventId)) { - // set to buffering while loading the chunk data - const currentState = this.getState(); - this.setState(VoiceBroadcastPlaybackState.Buffering); - await this.loadPlayback(event); - this.setState(currentState); - } - - const playback = this.playbacks.get(eventId); - - if (!playback) { - throw new Error(`Unable to find playback for event ${event.getId()}`); - } - - // try to load the playback for the next event for a smooth(er) playback - const nextEvent = this.chunkEvents.getNext(event); - if (nextEvent) this.tryLoadPlayback(nextEvent); - - return playback; - } - - private getCurrentPlayback(): Playback | undefined { - if (!this.currentlyPlaying) return; - return this.playbacks.get(this.currentlyPlaying.getId()!); - } - - public getLiveness(): VoiceBroadcastLiveness { - return this.liveness; - } - - private setLiveness(liveness: VoiceBroadcastLiveness): void { - if (this.liveness === liveness) return; - - this.liveness = liveness; - this.emit(VoiceBroadcastPlaybackEvent.LivenessChanged, liveness); - } - - public get currentState(): PlaybackState { - return PlaybackState.Playing; - } - - public get timeSeconds(): number { - return this.position / 1000; - } - - public get durationSeconds(): number { - return this.duration / 1000; - } - - public get timeLeftSeconds(): number { - // Sometimes the meta data and the audio files are a little bit out of sync. - // Be sure it never returns a negative value. - return Math.max(0, Math.round(this.durationSeconds) - this.timeSeconds); - } - - public async skipTo(timeSeconds: number): Promise { - this.skipToNext = timeSeconds; - - if (this.skipToDeferred) { - // Skip to position is already in progress. Return the promise for that. - return this.skipToDeferred.promise; - } - - this.skipToDeferred = defer(); - - while (this.skipToNext !== undefined) { - // Skip to position until skipToNext is undefined. - // skipToNext can be set if skipTo is called while already skipping. - const skipToNext = this.skipToNext; - this.skipToNext = undefined; - await this.doSkipTo(skipToNext); - } - - this.skipToDeferred.resolve(); - this.skipToDeferred = undefined; - } - - private async doSkipTo(timeSeconds: number): Promise { - const time = timeSeconds * 1000; - const event = this.chunkEvents.findByTime(time); - - if (!event) { - logger.warn("voice broadcast chunk event to skip to not found"); - return; - } - - const currentPlayback = this.getCurrentPlayback(); - const skipToPlayback = await this.tryGetOrLoadPlaybackForEvent(event); - const currentPlaybackEvent = this.currentlyPlaying; - - if (!skipToPlayback) { - logger.warn("voice broadcast chunk to skip to not found", event); - return; - } - - this.currentlyPlaying = event; - - if (currentPlayback && currentPlaybackEvent && currentPlayback !== skipToPlayback) { - // only stop and unload the playback here without triggering other effects, e.g. play next - currentPlayback.off(UPDATE_EVENT, this.onPlaybackStateChange); - await currentPlayback.stop(); - currentPlayback.on(UPDATE_EVENT, this.onPlaybackStateChange); - this.unloadPlayback(currentPlaybackEvent); - } - - const offsetInChunk = time - this.chunkEvents.getLengthTo(event); - await skipToPlayback.skipTo(offsetInChunk / 1000); - - if (this.state === VoiceBroadcastPlaybackState.Playing && !skipToPlayback.isPlaying) { - await skipToPlayback.play(); - } - - this.setPosition(time); - } - - public async start(): Promise { - if (this.state === VoiceBroadcastPlaybackState.Playing) return; - - const currentRecording = this.recordings.getCurrent(); - - if (currentRecording && currentRecording.getState() !== VoiceBroadcastInfoState.Stopped) { - const shouldStopRecording = await showConfirmListenBroadcastStopCurrentDialog(); - - if (!shouldStopRecording) { - // keep recording - return; - } - - await this.recordings.getCurrent()?.stop(); - } - - const chunkEvents = this.chunkEvents.getEvents(); - - const toPlay = - this.getInfoState() === VoiceBroadcastInfoState.Stopped - ? chunkEvents[0] // start at the beginning for an ended voice broadcast - : chunkEvents[chunkEvents.length - 1]; // start at the current chunk for an ongoing voice broadcast - - if (toPlay) { - return this.playEvent(toPlay); - } - - this.setState(VoiceBroadcastPlaybackState.Buffering); - } - - public stop(): void { - // error is a final state - if (this.getState() === VoiceBroadcastPlaybackState.Error) return; - - this.setState(VoiceBroadcastPlaybackState.Stopped); - this.getCurrentPlayback()?.stop(); - this.currentlyPlaying = null; - this.setPosition(0); - } - - public pause(): void { - // error is a final state - if (this.getState() === VoiceBroadcastPlaybackState.Error) return; - - // stopped voice broadcasts cannot be paused - if (this.getState() === VoiceBroadcastPlaybackState.Stopped) return; - - this.setState(VoiceBroadcastPlaybackState.Paused); - this.getCurrentPlayback()?.pause(); - } - - public resume(): void { - // error is a final state - if (this.getState() === VoiceBroadcastPlaybackState.Error) return; - - if (!this.currentlyPlaying) { - // no playback to resume, start from the beginning - this.start(); - return; - } - - this.setState(VoiceBroadcastPlaybackState.Playing); - this.getCurrentPlayback()?.play(); - } - - /** - * Toggles the playback: - * stopped → playing - * playing → paused - * paused → playing - */ - public async toggle(): Promise { - // error is a final state - if (this.getState() === VoiceBroadcastPlaybackState.Error) return; - - if (this.state === VoiceBroadcastPlaybackState.Stopped) { - await this.start(); - return; - } - - if (this.state === VoiceBroadcastPlaybackState.Paused) { - this.resume(); - return; - } - - this.pause(); - } - - public getState(): VoiceBroadcastPlaybackState { - return this.state; - } - - private setState(state: VoiceBroadcastPlaybackState): void { - if (this.state === state) { - return; - } - - this.state = state; - this.emit(VoiceBroadcastPlaybackEvent.StateChanged, state, this); - } - - /** - * Set error state. Stop current playback, if any. - */ - private setError(): void { - this.setState(VoiceBroadcastPlaybackState.Error); - this.getCurrentPlayback()?.stop(); - this.currentlyPlaying = null; - this.setPosition(0); - } - - public getInfoState(): VoiceBroadcastInfoState { - return this.infoState; - } - - private setInfoState(state: VoiceBroadcastInfoState): void { - if (this.infoState === state) { - return; - } - - this.infoState = state; - this.emit(VoiceBroadcastPlaybackEvent.InfoStateChanged, state); - this.setLiveness(determineVoiceBroadcastLiveness(this.infoState)); - } - - public get errorMessage(): string { - if (this.getState() !== VoiceBroadcastPlaybackState.Error) return ""; - if (this.utdChunkEvents.size) return _t("voice_broadcast|failed_decrypt"); - return _t("voice_broadcast|failed_generic"); - } - - public destroy(): void { - for (const [, utdEvent] of this.utdChunkEvents) { - utdEvent.off(MatrixEventEvent.Decrypted, this.onChunkEventDecrypted); - } - - this.utdChunkEvents.clear(); - - this.chunkRelationHelper.destroy(); - this.infoRelationHelper.destroy(); - this.removeAllListeners(); - - this.chunkEvents = new VoiceBroadcastChunkEvents(); - this.playbacks.forEach((p) => p.destroy()); - this.playbacks = new Map(); - } -} diff --git a/src/voice-broadcast/models/VoiceBroadcastPreRecording.ts b/src/voice-broadcast/models/VoiceBroadcastPreRecording.ts deleted file mode 100644 index 0cf47c6f21..0000000000 --- a/src/voice-broadcast/models/VoiceBroadcastPreRecording.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixClient, Room, RoomMember, TypedEventEmitter } from "matrix-js-sdk/src/matrix"; - -import { IDestroyable } from "../../utils/IDestroyable"; -import { VoiceBroadcastPlaybacksStore } from "../stores/VoiceBroadcastPlaybacksStore"; -import { VoiceBroadcastRecordingsStore } from "../stores/VoiceBroadcastRecordingsStore"; -import { startNewVoiceBroadcastRecording } from "../utils/startNewVoiceBroadcastRecording"; - -type VoiceBroadcastPreRecordingEvent = "dismiss"; - -interface EventMap { - dismiss: (voiceBroadcastPreRecording: VoiceBroadcastPreRecording) => void; -} - -export class VoiceBroadcastPreRecording - extends TypedEventEmitter - implements IDestroyable -{ - public constructor( - public room: Room, - public sender: RoomMember, - private client: MatrixClient, - private playbacksStore: VoiceBroadcastPlaybacksStore, - private recordingsStore: VoiceBroadcastRecordingsStore, - ) { - super(); - } - - public start = async (): Promise => { - await startNewVoiceBroadcastRecording(this.room, this.client, this.playbacksStore, this.recordingsStore); - this.emit("dismiss", this); - }; - - public cancel = (): void => { - this.emit("dismiss", this); - }; - - public destroy(): void { - this.removeAllListeners(); - } -} diff --git a/src/voice-broadcast/models/VoiceBroadcastRecording.ts b/src/voice-broadcast/models/VoiceBroadcastRecording.ts deleted file mode 100644 index ebf8ee697f..0000000000 --- a/src/voice-broadcast/models/VoiceBroadcastRecording.ts +++ /dev/null @@ -1,441 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { logger } from "matrix-js-sdk/src/logger"; -import { - ClientEvent, - ClientEventHandlerMap, - EventType, - MatrixClient, - MatrixEvent, - MatrixEventEvent, - MsgType, - RelationType, - TypedEventEmitter, -} from "matrix-js-sdk/src/matrix"; -import { AudioContent, EncryptedFile } from "matrix-js-sdk/src/types"; - -import { - ChunkRecordedPayload, - createVoiceBroadcastRecorder, - getMaxBroadcastLength, - VoiceBroadcastInfoEventContent, - VoiceBroadcastInfoEventType, - VoiceBroadcastInfoState, - VoiceBroadcastRecorder, - VoiceBroadcastRecorderEvent, -} from ".."; -import { uploadFile } from "../../ContentMessages"; -import { createVoiceMessageContent } from "../../utils/createVoiceMessageContent"; -import { IDestroyable } from "../../utils/IDestroyable"; -import dis from "../../dispatcher/dispatcher"; -import { ActionPayload } from "../../dispatcher/payloads"; -import { VoiceBroadcastChunkEvents } from "../utils/VoiceBroadcastChunkEvents"; -import { RelationsHelper, RelationsHelperEvent } from "../../events/RelationsHelper"; -import { createReconnectedListener } from "../../utils/connection"; -import { localNotificationsAreSilenced } from "../../utils/notifications"; -import { BackgroundAudio } from "../../audio/BackgroundAudio"; - -export enum VoiceBroadcastRecordingEvent { - StateChanged = "liveness_changed", - TimeLeftChanged = "time_left_changed", -} - -export type VoiceBroadcastRecordingState = VoiceBroadcastInfoState | "connection_error"; - -interface EventMap { - [VoiceBroadcastRecordingEvent.StateChanged]: (state: VoiceBroadcastRecordingState) => void; - [VoiceBroadcastRecordingEvent.TimeLeftChanged]: (timeLeft: number) => void; -} - -export class VoiceBroadcastRecording - extends TypedEventEmitter - implements IDestroyable -{ - private state: VoiceBroadcastRecordingState; - private recorder: VoiceBroadcastRecorder | null = null; - private dispatcherRef: string; - private chunkEvents = new VoiceBroadcastChunkEvents(); - private chunkRelationHelper: RelationsHelper; - private maxLength: number; - private timeLeft: number; - private toRetry: Array<() => Promise> = []; - private reconnectedListener: ClientEventHandlerMap[ClientEvent.Sync]; - private roomId: string; - private infoEventId: string; - private backgroundAudio = new BackgroundAudio(); - - /** - * Broadcast chunks have a sequence number to bring them in the correct order and to know if a message is missing. - * This variable holds the last sequence number. - * Starts with 0 because there is no chunk at the beginning of a broadcast. - * Will be incremented when a chunk message is created. - */ - private sequence = 0; - - public constructor( - public readonly infoEvent: MatrixEvent, - private client: MatrixClient, - initialState?: VoiceBroadcastInfoState, - ) { - super(); - this.maxLength = getMaxBroadcastLength(); - this.timeLeft = this.maxLength; - this.infoEventId = this.determineEventIdFromInfoEvent(); - this.roomId = this.determineRoomIdFromInfoEvent(); - - if (initialState) { - this.state = initialState; - } else { - this.state = this.determineInitialStateFromInfoEvent(); - } - - // TODO Michael W: listen for state updates - - this.infoEvent.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction); - this.dispatcherRef = dis.register(this.onAction); - this.chunkRelationHelper = this.initialiseChunkEventRelation(); - this.reconnectedListener = createReconnectedListener(this.onReconnect); - this.client.on(ClientEvent.Sync, this.reconnectedListener); - } - - private initialiseChunkEventRelation(): RelationsHelper { - const relationsHelper = new RelationsHelper( - this.infoEvent, - RelationType.Reference, - EventType.RoomMessage, - this.client, - ); - relationsHelper.on(RelationsHelperEvent.Add, this.onChunkEvent); - - relationsHelper.emitFetchCurrent().catch((err) => { - logger.warn("error fetching server side relation for voice broadcast chunks", err); - // fall back to local events - relationsHelper.emitCurrent(); - }); - - return relationsHelper; - } - - private onChunkEvent = (event: MatrixEvent): void => { - if ( - (!event.getId() && !event.getTxnId()) || - event.getContent()?.msgtype !== MsgType.Audio // don't add non-audio event - ) { - return; - } - - this.chunkEvents.addEvent(event); - }; - - private determineEventIdFromInfoEvent(): string { - const infoEventId = this.infoEvent.getId(); - - if (!infoEventId) { - throw new Error("Cannot create broadcast for info event without Id."); - } - - return infoEventId; - } - - private determineRoomIdFromInfoEvent(): string { - const roomId = this.infoEvent.getRoomId(); - - if (!roomId) { - throw new Error(`Cannot create broadcast for unknown room (info event ${this.infoEventId})`); - } - - return roomId; - } - - /** - * Determines the initial broadcast state. - * Checks all related events. If one has the "stopped" state → stopped, else started. - */ - private determineInitialStateFromInfoEvent(): VoiceBroadcastRecordingState { - const room = this.client.getRoom(this.roomId); - const relations = room - ?.getUnfilteredTimelineSet() - ?.relations?.getChildEventsForEvent(this.infoEventId, RelationType.Reference, VoiceBroadcastInfoEventType); - const relatedEvents = relations?.getRelations(); - return !relatedEvents?.find((event: MatrixEvent) => { - return event.getContent()?.state === VoiceBroadcastInfoState.Stopped; - }) - ? VoiceBroadcastInfoState.Started - : VoiceBroadcastInfoState.Stopped; - } - - public getTimeLeft(): number { - return this.timeLeft; - } - - /** - * Retries failed actions on reconnect. - */ - private onReconnect = async (): Promise => { - // Do nothing if not in connection_error state. - if (this.state !== "connection_error") return; - - // Copy the array, so that it is possible to remove elements from it while iterating over the original. - const toRetryCopy = [...this.toRetry]; - - for (const retryFn of this.toRetry) { - try { - await retryFn(); - // Successfully retried. Remove from array copy. - toRetryCopy.splice(toRetryCopy.indexOf(retryFn), 1); - } catch { - // The current retry callback failed. Stop the loop. - break; - } - } - - this.toRetry = toRetryCopy; - - if (this.toRetry.length === 0) { - // Everything has been successfully retried. Recover from error state to paused. - await this.pause(); - } - }; - - private async setTimeLeft(timeLeft: number): Promise { - if (timeLeft <= 0) { - // time is up - stop the recording - return await this.stop(); - } - - // do never increase time left; no action if equals - if (timeLeft >= this.timeLeft) return; - - this.timeLeft = timeLeft; - this.emit(VoiceBroadcastRecordingEvent.TimeLeftChanged, timeLeft); - } - - public async start(): Promise { - return this.getRecorder().start(); - } - - public async stop(): Promise { - if (this.state === VoiceBroadcastInfoState.Stopped) return; - - this.setState(VoiceBroadcastInfoState.Stopped); - await this.stopRecorder(); - await this.sendInfoStateEvent(VoiceBroadcastInfoState.Stopped); - } - - public async pause(): Promise { - // stopped or already paused recordings cannot be paused - if ( - ( - [VoiceBroadcastInfoState.Stopped, VoiceBroadcastInfoState.Paused] as VoiceBroadcastRecordingState[] - ).includes(this.state) - ) - return; - - this.setState(VoiceBroadcastInfoState.Paused); - await this.stopRecorder(); - await this.sendInfoStateEvent(VoiceBroadcastInfoState.Paused); - } - - public async resume(): Promise { - if (this.state !== VoiceBroadcastInfoState.Paused) return; - - this.setState(VoiceBroadcastInfoState.Resumed); - await this.getRecorder().start(); - await this.sendInfoStateEvent(VoiceBroadcastInfoState.Resumed); - } - - public toggle = async (): Promise => { - if (this.getState() === VoiceBroadcastInfoState.Paused) return this.resume(); - - if ( - ( - [VoiceBroadcastInfoState.Started, VoiceBroadcastInfoState.Resumed] as VoiceBroadcastRecordingState[] - ).includes(this.getState()) - ) { - return this.pause(); - } - }; - - public getState(): VoiceBroadcastRecordingState { - return this.state; - } - - private getRecorder(): VoiceBroadcastRecorder { - if (!this.recorder) { - this.recorder = createVoiceBroadcastRecorder(); - this.recorder.on(VoiceBroadcastRecorderEvent.ChunkRecorded, this.onChunkRecorded); - this.recorder.on(VoiceBroadcastRecorderEvent.CurrentChunkLengthUpdated, this.onCurrentChunkLengthUpdated); - } - - return this.recorder; - } - - public async destroy(): Promise { - if (this.recorder) { - this.recorder.stop(); - this.recorder.destroy(); - } - - this.infoEvent.off(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction); - this.removeAllListeners(); - dis.unregister(this.dispatcherRef); - this.chunkEvents = new VoiceBroadcastChunkEvents(); - this.chunkRelationHelper.destroy(); - this.client.off(ClientEvent.Sync, this.reconnectedListener); - } - - private onBeforeRedaction = (): void => { - if (this.getState() !== VoiceBroadcastInfoState.Stopped) { - this.setState(VoiceBroadcastInfoState.Stopped); - // destroy cleans up everything - this.destroy(); - } - }; - - private onAction = (payload: ActionPayload): void => { - if (payload.action !== "call_state") return; - - // pause on any call action - this.pause(); - }; - - private setState(state: VoiceBroadcastRecordingState): void { - this.state = state; - this.emit(VoiceBroadcastRecordingEvent.StateChanged, this.state); - } - - private onCurrentChunkLengthUpdated = (currentChunkLength: number): void => { - this.setTimeLeft(this.maxLength - this.chunkEvents.getLengthSeconds() - currentChunkLength); - }; - - private onChunkRecorded = async (chunk: ChunkRecordedPayload): Promise => { - const uploadAndSendFn = async (): Promise => { - const { url, file } = await this.uploadFile(chunk); - await this.sendVoiceMessage(chunk, url, file); - }; - - await this.callWithRetry(uploadAndSendFn); - }; - - /** - * This function is called on connection errors. - * It sets the connection error state and stops the recorder. - */ - private async onConnectionError(): Promise { - this.playConnectionErrorAudioNotification().catch(() => { - // Error logged in playConnectionErrorAudioNotification(). - }); - await this.stopRecorder(false); - this.setState("connection_error"); - } - - private async playConnectionErrorAudioNotification(): Promise { - if (localNotificationsAreSilenced(this.client)) { - return; - } - - await this.backgroundAudio.pickFormatAndPlay("./media/error", ["mp3", "ogg"]); - } - - private async uploadFile(chunk: ChunkRecordedPayload): ReturnType { - return uploadFile( - this.client, - this.roomId, - new Blob([chunk.buffer], { - type: this.getRecorder().contentType, - }), - ); - } - - private async sendVoiceMessage(chunk: ChunkRecordedPayload, url?: string, file?: EncryptedFile): Promise { - /** - * Increment the last sequence number and use it for this message. - * Done outside of the sendMessageFn to get a scoped value. - * Also see {@link VoiceBroadcastRecording.sequence}. - */ - const sequence = ++this.sequence; - - const sendMessageFn = async (): Promise => { - const content = createVoiceMessageContent( - url, - this.getRecorder().contentType, - Math.round(chunk.length * 1000), - chunk.buffer.length, - file, - ); - content["m.relates_to"] = { - rel_type: RelationType.Reference, - event_id: this.infoEventId, - }; - (content)["io.element.voice_broadcast_chunk"] = { - sequence, - }; - - await this.client.sendMessage(this.roomId, content); - }; - - await this.callWithRetry(sendMessageFn); - } - - /** - * Sends an info state event with given state. - * On error stores a resend function and setState(state) in {@link toRetry} and - * sets the broadcast state to connection_error. - */ - private async sendInfoStateEvent(state: VoiceBroadcastInfoState): Promise { - const sendEventFn = async (): Promise => { - await this.client.sendStateEvent( - this.roomId, - VoiceBroadcastInfoEventType, - { - device_id: this.client.getDeviceId(), - state, - last_chunk_sequence: this.sequence, - ["m.relates_to"]: { - rel_type: RelationType.Reference, - event_id: this.infoEventId, - }, - } as VoiceBroadcastInfoEventContent, - this.client.getSafeUserId(), - ); - }; - - await this.callWithRetry(sendEventFn); - } - - /** - * Calls the function. - * On failure adds it to the retry list and triggers connection error. - * {@link toRetry} - * {@link onConnectionError} - */ - private async callWithRetry(retryAbleFn: () => Promise): Promise { - try { - await retryAbleFn(); - } catch { - this.toRetry.push(retryAbleFn); - this.onConnectionError(); - } - } - - private async stopRecorder(emit = true): Promise { - if (!this.recorder) { - return; - } - - try { - const lastChunk = await this.recorder.stop(); - if (lastChunk && emit) { - await this.onChunkRecorded(lastChunk); - } - } catch (err) { - logger.warn("error stopping voice broadcast recorder", err); - } - } -} diff --git a/src/voice-broadcast/stores/VoiceBroadcastPlaybacksStore.ts b/src/voice-broadcast/stores/VoiceBroadcastPlaybacksStore.ts deleted file mode 100644 index 69b8c21d90..0000000000 --- a/src/voice-broadcast/stores/VoiceBroadcastPlaybacksStore.ts +++ /dev/null @@ -1,113 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixClient, MatrixEvent, TypedEventEmitter } from "matrix-js-sdk/src/matrix"; - -import { - VoiceBroadcastPlayback, - VoiceBroadcastPlaybackEvent, - VoiceBroadcastPlaybackState, - VoiceBroadcastRecordingsStore, -} from ".."; -import { IDestroyable } from "../../utils/IDestroyable"; - -export enum VoiceBroadcastPlaybacksStoreEvent { - CurrentChanged = "current_changed", -} - -interface EventMap { - [VoiceBroadcastPlaybacksStoreEvent.CurrentChanged]: (recording: VoiceBroadcastPlayback | null) => void; -} - -/** - * This store manages VoiceBroadcastPlaybacks: - * - access the currently playing voice broadcast - * - ensures that only once broadcast is playing at a time - */ -export class VoiceBroadcastPlaybacksStore - extends TypedEventEmitter - implements IDestroyable -{ - private current: VoiceBroadcastPlayback | null = null; - - /** Playbacks indexed by their info event id. */ - private playbacks = new Map(); - - public constructor(private recordings: VoiceBroadcastRecordingsStore) { - super(); - } - - public setCurrent(current: VoiceBroadcastPlayback): void { - if (this.current === current) return; - - this.current = current; - this.addPlayback(current); - this.emit(VoiceBroadcastPlaybacksStoreEvent.CurrentChanged, current); - } - - public clearCurrent(): void { - if (this.current === null) return; - - this.current = null; - this.emit(VoiceBroadcastPlaybacksStoreEvent.CurrentChanged, null); - } - - public getCurrent(): VoiceBroadcastPlayback | null { - return this.current; - } - - public getByInfoEvent(infoEvent: MatrixEvent, client: MatrixClient): VoiceBroadcastPlayback { - const infoEventId = infoEvent.getId()!; - - if (!this.playbacks.has(infoEventId)) { - this.addPlayback(new VoiceBroadcastPlayback(infoEvent, client, this.recordings)); - } - - return this.playbacks.get(infoEventId)!; - } - - private addPlayback(playback: VoiceBroadcastPlayback): void { - const infoEventId = playback.infoEvent.getId()!; - - if (this.playbacks.has(infoEventId)) return; - - this.playbacks.set(infoEventId, playback); - playback.on(VoiceBroadcastPlaybackEvent.StateChanged, this.onPlaybackStateChanged); - } - - private onPlaybackStateChanged = (state: VoiceBroadcastPlaybackState, playback: VoiceBroadcastPlayback): void => { - switch (state) { - case VoiceBroadcastPlaybackState.Buffering: - case VoiceBroadcastPlaybackState.Playing: - this.pauseExcept(playback); - this.setCurrent(playback); - break; - case VoiceBroadcastPlaybackState.Stopped: - this.clearCurrent(); - break; - } - }; - - private pauseExcept(playbackNotToPause: VoiceBroadcastPlayback): void { - for (const playback of this.playbacks.values()) { - if (playback !== playbackNotToPause) { - playback.pause(); - } - } - } - - public destroy(): void { - this.removeAllListeners(); - - for (const playback of this.playbacks.values()) { - playback.off(VoiceBroadcastPlaybackEvent.StateChanged, this.onPlaybackStateChanged); - } - - this.playbacks = new Map(); - } -} diff --git a/src/voice-broadcast/stores/VoiceBroadcastPreRecordingStore.ts b/src/voice-broadcast/stores/VoiceBroadcastPreRecordingStore.ts deleted file mode 100644 index 3552930687..0000000000 --- a/src/voice-broadcast/stores/VoiceBroadcastPreRecordingStore.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { TypedEventEmitter } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastPreRecording } from ".."; -import { IDestroyable } from "../../utils/IDestroyable"; - -export type VoiceBroadcastPreRecordingEvent = "changed"; - -interface EventMap { - changed: (preRecording: VoiceBroadcastPreRecording | null) => void; -} - -export class VoiceBroadcastPreRecordingStore - extends TypedEventEmitter - implements IDestroyable -{ - private current: VoiceBroadcastPreRecording | null = null; - - public setCurrent(current: VoiceBroadcastPreRecording): void { - if (this.current === current) return; - - if (this.current) { - this.current.off("dismiss", this.onCancel); - } - - this.current = current; - current.on("dismiss", this.onCancel); - this.emit("changed", current); - } - - public clearCurrent(): void { - if (this.current === null) return; - - this.current.off("dismiss", this.onCancel); - this.current = null; - this.emit("changed", null); - } - - public getCurrent(): VoiceBroadcastPreRecording | null { - return this.current; - } - - public destroy(): void { - this.removeAllListeners(); - - if (this.current) { - this.current.off("dismiss", this.onCancel); - } - } - - private onCancel = (voiceBroadcastPreRecording: VoiceBroadcastPreRecording): void => { - if (this.current === voiceBroadcastPreRecording) { - this.clearCurrent(); - } - }; -} diff --git a/src/voice-broadcast/stores/VoiceBroadcastRecordingsStore.ts b/src/voice-broadcast/stores/VoiceBroadcastRecordingsStore.ts deleted file mode 100644 index ff0f67b910..0000000000 --- a/src/voice-broadcast/stores/VoiceBroadcastRecordingsStore.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022, 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixClient, MatrixEvent, TypedEventEmitter } from "matrix-js-sdk/src/matrix"; - -import { - VoiceBroadcastInfoState, - VoiceBroadcastRecording, - VoiceBroadcastRecordingEvent, - VoiceBroadcastRecordingState, -} from ".."; - -export enum VoiceBroadcastRecordingsStoreEvent { - CurrentChanged = "current_changed", -} - -interface EventMap { - [VoiceBroadcastRecordingsStoreEvent.CurrentChanged]: (recording: VoiceBroadcastRecording | null) => void; -} - -/** - * This store provides access to the current and specific Voice Broadcast recordings. - */ -export class VoiceBroadcastRecordingsStore extends TypedEventEmitter { - private current: VoiceBroadcastRecording | null = null; - private recordings = new Map(); - - public constructor() { - super(); - } - - public setCurrent(current: VoiceBroadcastRecording): void { - if (this.current === current) return; - - const infoEventId = current.infoEvent.getId(); - - if (!infoEventId) { - throw new Error("Got broadcast info event without Id"); - } - - if (this.current) { - this.current.off(VoiceBroadcastRecordingEvent.StateChanged, this.onCurrentStateChanged); - } - - this.current = current; - this.current.on(VoiceBroadcastRecordingEvent.StateChanged, this.onCurrentStateChanged); - this.recordings.set(infoEventId, current); - this.emit(VoiceBroadcastRecordingsStoreEvent.CurrentChanged, current); - } - - public getCurrent(): VoiceBroadcastRecording | null { - return this.current; - } - - public hasCurrent(): boolean { - return this.current !== null; - } - - public clearCurrent(): void { - if (!this.current) return; - - this.current.off(VoiceBroadcastRecordingEvent.StateChanged, this.onCurrentStateChanged); - this.current = null; - this.emit(VoiceBroadcastRecordingsStoreEvent.CurrentChanged, null); - } - - public getByInfoEvent(infoEvent: MatrixEvent, client: MatrixClient): VoiceBroadcastRecording { - const infoEventId = infoEvent.getId(); - - if (!infoEventId) { - throw new Error("Got broadcast info event without Id"); - } - - const recording = this.recordings.get(infoEventId) || new VoiceBroadcastRecording(infoEvent, client); - this.recordings.set(infoEventId, recording); - return recording; - } - - private onCurrentStateChanged = (state: VoiceBroadcastRecordingState): void => { - if (state === VoiceBroadcastInfoState.Stopped) { - this.clearCurrent(); - } - }; -} diff --git a/src/voice-broadcast/types.ts b/src/voice-broadcast/types.ts deleted file mode 100644 index 8191a0be16..0000000000 --- a/src/voice-broadcast/types.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -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/VoiceBroadcastChunkEvents.ts b/src/voice-broadcast/utils/VoiceBroadcastChunkEvents.ts deleted file mode 100644 index 039749cf8d..0000000000 --- a/src/voice-broadcast/utils/VoiceBroadcastChunkEvents.ts +++ /dev/null @@ -1,147 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixEvent } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastChunkEventType } from ".."; - -/** - * Voice broadcast chunk collection. - * Orders chunks by sequence (if available) or timestamp. - */ -export class VoiceBroadcastChunkEvents { - private events: MatrixEvent[] = []; - - public getEvents(): MatrixEvent[] { - return [...this.events]; - } - - public getNext(event: MatrixEvent): MatrixEvent | undefined { - return this.events[this.events.indexOf(event) + 1]; - } - - public addEvent(event: MatrixEvent): void { - if (this.addOrReplaceEvent(event)) { - this.sort(); - } - } - - public addEvents(events: MatrixEvent[]): void { - const atLeastOneNew = events.reduce((newSoFar: boolean, event: MatrixEvent): boolean => { - return this.addOrReplaceEvent(event) || newSoFar; - }, false); - - if (atLeastOneNew) { - this.sort(); - } - } - - public includes(event: MatrixEvent): boolean { - return !!this.events.find((e) => this.equalByTxnIdOrId(event, e)); - } - - /** - * @returns {number} Length in milliseconds - */ - public getLength(): number { - return this.events.reduce((length: number, event: MatrixEvent) => { - return length + this.calculateChunkLength(event); - }, 0); - } - - public getLengthSeconds(): number { - return this.getLength() / 1000; - } - - /** - * Returns the accumulated length to (excl.) a chunk event. - */ - public getLengthTo(event: MatrixEvent): number { - let length = 0; - - for (let i = 0; i < this.events.indexOf(event); i++) { - length += this.calculateChunkLength(this.events[i]); - } - - return length; - } - - public findByTime(time: number): MatrixEvent | null { - let lengthSoFar = 0; - - for (let i = 0; i < this.events.length; i++) { - lengthSoFar += this.calculateChunkLength(this.events[i]); - - if (lengthSoFar >= time) { - return this.events[i]; - } - } - - return null; - } - - public isLast(event: MatrixEvent): boolean { - return this.events.indexOf(event) >= this.events.length - 1; - } - - public getSequenceForEvent(event: MatrixEvent): number | null { - const sequence = parseInt(event.getContent()?.[VoiceBroadcastChunkEventType]?.sequence, 10); - if (!isNaN(sequence)) return sequence; - - if (this.events.includes(event)) return this.events.indexOf(event) + 1; - - return null; - } - - public getNumberOfEvents(): number { - return this.events.length; - } - - private calculateChunkLength(event: MatrixEvent): number { - return event.getContent()?.["org.matrix.msc1767.audio"]?.duration || event.getContent()?.info?.duration || 0; - } - - private addOrReplaceEvent = (event: MatrixEvent): boolean => { - this.events = this.events.filter((e) => !this.equalByTxnIdOrId(event, e)); - this.events.push(event); - return true; - }; - - private equalByTxnIdOrId(eventA: MatrixEvent, eventB: MatrixEvent): boolean { - return ( - (eventA.getTxnId() && eventB.getTxnId() && eventA.getTxnId() === eventB.getTxnId()) || - eventA.getId() === eventB.getId() - ); - } - - /** - * Sort by sequence, if available for all events. - * Else fall back to timestamp. - */ - private sort(): void { - const compareFn = this.allHaveSequence() ? this.compareBySequence : this.compareByTimestamp; - this.events.sort(compareFn); - } - - private compareBySequence = (a: MatrixEvent, b: MatrixEvent): number => { - const aSequence = a.getContent()?.[VoiceBroadcastChunkEventType]?.sequence || 0; - const bSequence = b.getContent()?.[VoiceBroadcastChunkEventType]?.sequence || 0; - return aSequence - bSequence; - }; - - private compareByTimestamp = (a: MatrixEvent, b: MatrixEvent): number => { - return a.getTs() - b.getTs(); - }; - - private allHaveSequence(): boolean { - return !this.events.some((event: MatrixEvent) => { - const sequence = event.getContent()?.[VoiceBroadcastChunkEventType]?.sequence; - return parseInt(sequence, 10) !== sequence; - }); - } -} diff --git a/src/voice-broadcast/utils/VoiceBroadcastResumer.ts b/src/voice-broadcast/utils/VoiceBroadcastResumer.ts deleted file mode 100644 index 963b6ef3a6..0000000000 --- a/src/voice-broadcast/utils/VoiceBroadcastResumer.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { ClientEvent, MatrixClient, MatrixEvent, RelationType, Room, SyncState } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastInfoEventContent, VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from ".."; -import { IDestroyable } from "../../utils/IDestroyable"; -import { findRoomLiveVoiceBroadcastFromUserAndDevice } from "./findRoomLiveVoiceBroadcastFromUserAndDevice"; - -/** - * Handles voice broadcasts on app resume (after logging in, reload, crash…). - */ -export class VoiceBroadcastResumer implements IDestroyable { - public constructor(private client: MatrixClient) { - if (client.isInitialSyncComplete()) { - this.resume(); - } else { - // wait for initial sync - client.on(ClientEvent.Sync, this.onClientSync); - } - } - - private onClientSync = (): void => { - if (this.client.getSyncState() === SyncState.Syncing) { - this.client.off(ClientEvent.Sync, this.onClientSync); - this.resume(); - } - }; - - private resume(): void { - const userId = this.client.getUserId(); - const deviceId = this.client.getDeviceId(); - - if (!userId || !deviceId) { - // Resuming a voice broadcast only makes sense if there is a user. - return; - } - - this.client.getRooms().forEach((room: Room) => { - const infoEvent = findRoomLiveVoiceBroadcastFromUserAndDevice(room, userId, deviceId); - - if (infoEvent) { - // Found a live broadcast event from current device; stop it. - // Stopping it is a temporary solution (see PSF-1669). - this.sendStopVoiceBroadcastStateEvent(infoEvent); - return false; - } - }); - } - - private sendStopVoiceBroadcastStateEvent(infoEvent: MatrixEvent): void { - const userId = this.client.getUserId(); - const deviceId = this.client.getDeviceId(); - const roomId = infoEvent.getRoomId(); - - if (!userId || !deviceId || !roomId) { - // We can only send a state event if we know all the IDs. - return; - } - - const content: VoiceBroadcastInfoEventContent = { - device_id: deviceId, - state: VoiceBroadcastInfoState.Stopped, - }; - - // all events should reference the started event - const referencedEventId = - infoEvent.getContent()?.state === VoiceBroadcastInfoState.Started - ? infoEvent.getId() - : infoEvent.getContent()?.["m.relates_to"]?.event_id; - - if (referencedEventId) { - content["m.relates_to"] = { - rel_type: RelationType.Reference, - event_id: referencedEventId, - }; - } - - this.client.sendStateEvent(roomId, VoiceBroadcastInfoEventType, content, userId); - } - - public destroy(): void { - this.client.off(ClientEvent.Sync, this.onClientSync); - } -} diff --git a/src/voice-broadcast/utils/checkVoiceBroadcastPreConditions.tsx b/src/voice-broadcast/utils/checkVoiceBroadcastPreConditions.tsx deleted file mode 100644 index ae96bc0b14..0000000000 --- a/src/voice-broadcast/utils/checkVoiceBroadcastPreConditions.tsx +++ /dev/null @@ -1,86 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { MatrixClient, Room, SyncState } from "matrix-js-sdk/src/matrix"; - -import { hasRoomLiveVoiceBroadcast, VoiceBroadcastInfoEventType, VoiceBroadcastRecordingsStore } from ".."; -import InfoDialog from "../../components/views/dialogs/InfoDialog"; -import { _t } from "../../languageHandler"; -import Modal from "../../Modal"; - -const showAlreadyRecordingDialog = (): void => { - Modal.createDialog(InfoDialog, { - title: _t("voice_broadcast|failed_already_recording_title"), - description:

{_t("voice_broadcast|failed_already_recording_description")}

, - hasCloseButton: true, - }); -}; - -const showInsufficientPermissionsDialog = (): void => { - Modal.createDialog(InfoDialog, { - title: _t("voice_broadcast|failed_insufficient_permission_title"), - description:

{_t("voice_broadcast|failed_insufficient_permission_description")}

, - hasCloseButton: true, - }); -}; - -const showOthersAlreadyRecordingDialog = (): void => { - Modal.createDialog(InfoDialog, { - title: _t("voice_broadcast|failed_others_already_recording_title"), - description:

{_t("voice_broadcast|failed_others_already_recording_description")}

, - hasCloseButton: true, - }); -}; - -const showNoConnectionDialog = (): void => { - Modal.createDialog(InfoDialog, { - title: _t("voice_broadcast|failed_no_connection_title"), - description:

{_t("voice_broadcast|failed_no_connection_description")}

, - hasCloseButton: true, - }); -}; - -export const checkVoiceBroadcastPreConditions = async ( - room: Room, - client: MatrixClient, - recordingsStore: VoiceBroadcastRecordingsStore, -): Promise => { - if (recordingsStore.getCurrent()) { - showAlreadyRecordingDialog(); - return false; - } - - const currentUserId = client.getUserId(); - - if (!currentUserId) return false; - - if (!room.currentState.maySendStateEvent(VoiceBroadcastInfoEventType, currentUserId)) { - showInsufficientPermissionsDialog(); - return false; - } - - if (client.getSyncState() === SyncState.Error) { - showNoConnectionDialog(); - return false; - } - - const { hasBroadcast, startedByUser } = await hasRoomLiveVoiceBroadcast(client, room, currentUserId); - - if (hasBroadcast && startedByUser) { - showAlreadyRecordingDialog(); - return false; - } - - if (hasBroadcast) { - showOthersAlreadyRecordingDialog(); - return false; - } - - return true; -}; diff --git a/src/voice-broadcast/utils/cleanUpBroadcasts.ts b/src/voice-broadcast/utils/cleanUpBroadcasts.ts deleted file mode 100644 index 50133274b0..0000000000 --- a/src/voice-broadcast/utils/cleanUpBroadcasts.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { SdkContextClass } from "../../contexts/SDKContext"; - -export const cleanUpBroadcasts = async (stores: SdkContextClass): Promise => { - stores.voiceBroadcastPlaybacksStore.getCurrent()?.stop(); - stores.voiceBroadcastPlaybacksStore.clearCurrent(); - - await stores.voiceBroadcastRecordingsStore.getCurrent()?.stop(); - stores.voiceBroadcastRecordingsStore.clearCurrent(); - - stores.voiceBroadcastPreRecordingStore.getCurrent()?.cancel(); - stores.voiceBroadcastPreRecordingStore.clearCurrent(); -}; diff --git a/src/voice-broadcast/utils/determineVoiceBroadcastLiveness.ts b/src/voice-broadcast/utils/determineVoiceBroadcastLiveness.ts deleted file mode 100644 index 8d9660c572..0000000000 --- a/src/voice-broadcast/utils/determineVoiceBroadcastLiveness.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { VoiceBroadcastInfoState, VoiceBroadcastLiveness } from ".."; - -const stateLivenessMap: Map = new Map([ - ["started", "live"], - ["resumed", "live"], - ["paused", "grey"], - ["stopped", "not-live"], -] as Array<[VoiceBroadcastInfoState, VoiceBroadcastLiveness]>); - -export const determineVoiceBroadcastLiveness = (infoState: VoiceBroadcastInfoState): VoiceBroadcastLiveness => { - return stateLivenessMap.get(infoState) ?? "not-live"; -}; diff --git a/src/voice-broadcast/utils/doClearCurrentVoiceBroadcastPlaybackIfStopped.ts b/src/voice-broadcast/utils/doClearCurrentVoiceBroadcastPlaybackIfStopped.ts deleted file mode 100644 index ef0e1e7aed..0000000000 --- a/src/voice-broadcast/utils/doClearCurrentVoiceBroadcastPlaybackIfStopped.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { VoiceBroadcastPlaybacksStore, VoiceBroadcastPlaybackState } from ".."; - -export const doClearCurrentVoiceBroadcastPlaybackIfStopped = ( - voiceBroadcastPlaybacksStore: VoiceBroadcastPlaybacksStore, -): void => { - if (voiceBroadcastPlaybacksStore.getCurrent()?.getState() === VoiceBroadcastPlaybackState.Stopped) { - // clear current if stopped - return; - } -}; diff --git a/src/voice-broadcast/utils/doMaybeSetCurrentVoiceBroadcastPlayback.ts b/src/voice-broadcast/utils/doMaybeSetCurrentVoiceBroadcastPlayback.ts deleted file mode 100644 index 2ec4ab185d..0000000000 --- a/src/voice-broadcast/utils/doMaybeSetCurrentVoiceBroadcastPlayback.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; - -import { - hasRoomLiveVoiceBroadcast, - VoiceBroadcastPlaybacksStore, - VoiceBroadcastPlaybackState, - VoiceBroadcastRecordingsStore, -} from ".."; - -/** - * When a live voice broadcast is in the room and - * another voice broadcast is not currently being listened to or recorded - * the live broadcast in the room is set as the current broadcast to listen to. - * When there is no live broadcast in the room: clear current broadcast. - * - * @param {Room} room The room to check for a live voice broadcast - * @param {MatrixClient} client - * @param {VoiceBroadcastPlaybacksStore} voiceBroadcastPlaybacksStore - * @param {VoiceBroadcastRecordingsStore} voiceBroadcastRecordingsStore - */ -export const doMaybeSetCurrentVoiceBroadcastPlayback = async ( - room: Room, - client: MatrixClient, - voiceBroadcastPlaybacksStore: VoiceBroadcastPlaybacksStore, - voiceBroadcastRecordingsStore: VoiceBroadcastRecordingsStore, -): Promise => { - // do not disturb the current recording - if (voiceBroadcastRecordingsStore.hasCurrent()) return; - - const currentPlayback = voiceBroadcastPlaybacksStore.getCurrent(); - - if (currentPlayback && currentPlayback.getState() !== VoiceBroadcastPlaybackState.Stopped) { - // do not disturb the current playback - return; - } - - const { infoEvent } = await hasRoomLiveVoiceBroadcast(client, room); - - if (infoEvent) { - // live broadcast in the room + no recording + not listening yet: set the current broadcast - const voiceBroadcastPlayback = voiceBroadcastPlaybacksStore.getByInfoEvent(infoEvent, client); - voiceBroadcastPlaybacksStore.setCurrent(voiceBroadcastPlayback); - return; - } - - // no broadcast; not listening: clear current - voiceBroadcastPlaybacksStore.clearCurrent(); -}; diff --git a/src/voice-broadcast/utils/findRoomLiveVoiceBroadcastFromUserAndDevice.ts b/src/voice-broadcast/utils/findRoomLiveVoiceBroadcastFromUserAndDevice.ts deleted file mode 100644 index fbd02d44fb..0000000000 --- a/src/voice-broadcast/utils/findRoomLiveVoiceBroadcastFromUserAndDevice.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from ".."; - -export const findRoomLiveVoiceBroadcastFromUserAndDevice = ( - room: Room, - userId: string, - deviceId: string, -): MatrixEvent | null => { - const stateEvent = room.currentState.getStateEvents(VoiceBroadcastInfoEventType, userId); - - // no broadcast from that user - if (!stateEvent) return null; - - const content = stateEvent.getContent() || {}; - - // stopped broadcast - if (content.state === VoiceBroadcastInfoState.Stopped) return null; - - return content.device_id === deviceId ? stateEvent : null; -}; diff --git a/src/voice-broadcast/utils/getChunkLength.ts b/src/voice-broadcast/utils/getChunkLength.ts deleted file mode 100644 index b3fe2f557d..0000000000 --- a/src/voice-broadcast/utils/getChunkLength.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import SdkConfig, { DEFAULTS } from "../../SdkConfig"; -import { Features } from "../../settings/Settings"; -import SettingsStore from "../../settings/SettingsStore"; - -/** - * Returns the target chunk length for voice broadcasts: - * - If {@see Features.VoiceBroadcastForceSmallChunks} is enabled uses 15s chunk length - * - Otherwise to get the value from the voice_broadcast.chunk_length config - * - If that fails from DEFAULTS - * - If that fails fall back to 120 (two minutes) - */ -export const getChunkLength = (): number => { - if (SettingsStore.getValue(Features.VoiceBroadcastForceSmallChunks)) return 15; - return SdkConfig.get("voice_broadcast")?.chunk_length || DEFAULTS.voice_broadcast?.chunk_length || 120; -}; diff --git a/src/voice-broadcast/utils/getMaxBroadcastLength.ts b/src/voice-broadcast/utils/getMaxBroadcastLength.ts deleted file mode 100644 index e5df83ef05..0000000000 --- a/src/voice-broadcast/utils/getMaxBroadcastLength.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import SdkConfig, { DEFAULTS } from "../../SdkConfig"; - -/** - * Returns the max length for voice broadcasts: - * - Tries to get the value from the voice_broadcast.max_length config - * - If that fails from DEFAULTS - * - If that fails fall back to four hours - */ -export const getMaxBroadcastLength = (): number => { - return SdkConfig.get("voice_broadcast")?.max_length || DEFAULTS.voice_broadcast?.max_length || 4 * 60 * 60; -}; diff --git a/src/voice-broadcast/utils/hasRoomLiveVoiceBroadcast.ts b/src/voice-broadcast/utils/hasRoomLiveVoiceBroadcast.ts deleted file mode 100644 index 939eb1a0c2..0000000000 --- a/src/voice-broadcast/utils/hasRoomLiveVoiceBroadcast.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; - -import { retrieveStartedInfoEvent, VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from ".."; -import { asyncEvery } from "../../utils/arrays"; - -interface Result { - // whether there is a live broadcast in the room - hasBroadcast: boolean; - // info event of any live broadcast in the room - infoEvent: MatrixEvent | null; - // whether the broadcast was started by the user - startedByUser: boolean; -} - -export const hasRoomLiveVoiceBroadcast = async (client: MatrixClient, room: Room, userId?: string): Promise => { - let hasBroadcast = false; - let startedByUser = false; - let infoEvent: MatrixEvent | null = null; - - const stateEvents = room.currentState.getStateEvents(VoiceBroadcastInfoEventType); - await asyncEvery(stateEvents, async (event: MatrixEvent) => { - const state = event.getContent()?.state; - - if (state && state !== VoiceBroadcastInfoState.Stopped) { - const startEvent = await retrieveStartedInfoEvent(event, client); - - // skip if started voice broadcast event is redacted - if (startEvent?.isRedacted()) return true; - - hasBroadcast = true; - infoEvent = startEvent; - - // state key = sender's MXID - if (event.getStateKey() === userId) { - startedByUser = true; - // break here, because more than true / true is not possible - return false; - } - } - - return true; - }); - - return { - hasBroadcast, - infoEvent, - startedByUser, - }; -}; diff --git a/src/voice-broadcast/utils/isRelatedToVoiceBroadcast.ts b/src/voice-broadcast/utils/isRelatedToVoiceBroadcast.ts deleted file mode 100644 index eca8f890e0..0000000000 --- a/src/voice-broadcast/utils/isRelatedToVoiceBroadcast.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixClient, MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastInfoEventType } from "../types"; - -export const isRelatedToVoiceBroadcast = (event: MatrixEvent, client: MatrixClient): boolean => { - const relation = event.getRelation(); - - return ( - relation?.rel_type === RelationType.Reference && - !!relation.event_id && - client.getRoom(event.getRoomId())?.findEventById(relation.event_id)?.getType() === VoiceBroadcastInfoEventType - ); -}; diff --git a/src/voice-broadcast/utils/isVoiceBroadcastStartedEvent.ts b/src/voice-broadcast/utils/isVoiceBroadcastStartedEvent.ts deleted file mode 100644 index fffe45850e..0000000000 --- a/src/voice-broadcast/utils/isVoiceBroadcastStartedEvent.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -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/src/voice-broadcast/utils/pauseNonLiveBroadcastFromOtherRoom.ts b/src/voice-broadcast/utils/pauseNonLiveBroadcastFromOtherRoom.ts deleted file mode 100644 index e854ba9bac..0000000000 --- a/src/voice-broadcast/utils/pauseNonLiveBroadcastFromOtherRoom.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { Room } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastPlaybacksStore } from ".."; - -export const pauseNonLiveBroadcastFromOtherRoom = ( - room: Room, - voiceBroadcastPlaybacksStore: VoiceBroadcastPlaybacksStore, -): void => { - const playingBroadcast = voiceBroadcastPlaybacksStore.getCurrent(); - - if ( - !playingBroadcast || - playingBroadcast?.getLiveness() === "live" || - playingBroadcast?.infoEvent.getRoomId() === room.roomId - ) { - return; - } - - voiceBroadcastPlaybacksStore.clearCurrent(); - playingBroadcast.pause(); -}; diff --git a/src/voice-broadcast/utils/retrieveStartedInfoEvent.ts b/src/voice-broadcast/utils/retrieveStartedInfoEvent.ts deleted file mode 100644 index cc5be144c9..0000000000 --- a/src/voice-broadcast/utils/retrieveStartedInfoEvent.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastInfoState } from ".."; - -export const retrieveStartedInfoEvent = async ( - event: MatrixEvent, - client: MatrixClient, -): Promise => { - // started event passed as argument - if (event.getContent()?.state === VoiceBroadcastInfoState.Started) return event; - - const relatedEventId = event.getRelation()?.event_id; - - // no related event - if (!relatedEventId) return null; - - const roomId = event.getRoomId() || ""; - const relatedEventFromRoom = client.getRoom(roomId)?.findEventById(relatedEventId); - - // event found - if (relatedEventFromRoom) return relatedEventFromRoom; - - try { - const relatedEventData = await client.fetchRoomEvent(roomId, relatedEventId); - return new MatrixEvent(relatedEventData); - } catch {} - - return null; -}; diff --git a/src/voice-broadcast/utils/setUpVoiceBroadcastPreRecording.ts b/src/voice-broadcast/utils/setUpVoiceBroadcastPreRecording.ts deleted file mode 100644 index c50607c58f..0000000000 --- a/src/voice-broadcast/utils/setUpVoiceBroadcastPreRecording.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; - -import { - checkVoiceBroadcastPreConditions, - VoiceBroadcastPlaybacksStore, - VoiceBroadcastPreRecording, - VoiceBroadcastPreRecordingStore, - VoiceBroadcastRecordingsStore, -} from ".."; - -export const setUpVoiceBroadcastPreRecording = async ( - room: Room, - client: MatrixClient, - playbacksStore: VoiceBroadcastPlaybacksStore, - recordingsStore: VoiceBroadcastRecordingsStore, - preRecordingStore: VoiceBroadcastPreRecordingStore, -): Promise => { - if (!(await checkVoiceBroadcastPreConditions(room, client, recordingsStore))) { - return null; - } - - const userId = client.getUserId(); - if (!userId) return null; - - const sender = room.getMember(userId); - if (!sender) return null; - - // pause and clear current playback (if any) - playbacksStore.getCurrent()?.pause(); - playbacksStore.clearCurrent(); - - const preRecording = new VoiceBroadcastPreRecording(room, sender, client, playbacksStore, recordingsStore); - preRecordingStore.setCurrent(preRecording); - return preRecording; -}; diff --git a/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastRecordingTile.ts b/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastRecordingTile.ts deleted file mode 100644 index d729d9e1ca..0000000000 --- a/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastRecordingTile.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastInfoEventContent, VoiceBroadcastInfoState } from ".."; - -export const shouldDisplayAsVoiceBroadcastRecordingTile = ( - state: VoiceBroadcastInfoState, - client: MatrixClient, - event: MatrixEvent, -): boolean => { - const userId = client.getUserId(); - return ( - !!userId && - userId === event.getSender() && - client.getDeviceId() === event.getContent()?.device_id && - state !== VoiceBroadcastInfoState.Stopped - ); -}; diff --git a/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastStoppedText.ts b/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastStoppedText.ts deleted file mode 100644 index 2179aff3b7..0000000000 --- a/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastStoppedText.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixEvent } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from ".."; - -export const shouldDisplayAsVoiceBroadcastStoppedText = (event: MatrixEvent): boolean => - event.getType() === VoiceBroadcastInfoEventType && - event.getContent()?.state === VoiceBroadcastInfoState.Stopped && - !event.isRedacted(); diff --git a/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastTile.ts b/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastTile.ts deleted file mode 100644 index 9a51b33c9a..0000000000 --- a/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastTile.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixEvent } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from ".."; - -export const shouldDisplayAsVoiceBroadcastTile = (event: MatrixEvent): boolean => - event.getType?.() === VoiceBroadcastInfoEventType && - (event.getContent?.()?.state === VoiceBroadcastInfoState.Started || event.isRedacted()); diff --git a/src/voice-broadcast/utils/showCantStartACallDialog.tsx b/src/voice-broadcast/utils/showCantStartACallDialog.tsx deleted file mode 100644 index eeeb86ee07..0000000000 --- a/src/voice-broadcast/utils/showCantStartACallDialog.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; - -import InfoDialog from "../../components/views/dialogs/InfoDialog"; -import { _t } from "../../languageHandler"; -import Modal from "../../Modal"; - -export const showCantStartACallDialog = (): void => { - Modal.createDialog(InfoDialog, { - title: _t("voip|failed_call_live_broadcast_title"), - description:

{_t("voip|failed_call_live_broadcast_description")}

, - hasCloseButton: true, - }); -}; diff --git a/src/voice-broadcast/utils/startNewVoiceBroadcastRecording.ts b/src/voice-broadcast/utils/startNewVoiceBroadcastRecording.ts deleted file mode 100644 index f0c5a91932..0000000000 --- a/src/voice-broadcast/utils/startNewVoiceBroadcastRecording.ts +++ /dev/null @@ -1,92 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { ISendEventResponse, MatrixClient, Room, RoomStateEvent } from "matrix-js-sdk/src/matrix"; -import { defer } from "matrix-js-sdk/src/utils"; - -import { - VoiceBroadcastInfoEventContent, - VoiceBroadcastInfoEventType, - VoiceBroadcastInfoState, - VoiceBroadcastRecordingsStore, - VoiceBroadcastRecording, - getChunkLength, - VoiceBroadcastPlaybacksStore, -} from ".."; -import { checkVoiceBroadcastPreConditions } from "./checkVoiceBroadcastPreConditions"; - -const startBroadcast = async ( - room: Room, - client: MatrixClient, - recordingsStore: VoiceBroadcastRecordingsStore, -): Promise => { - const { promise, resolve, reject } = defer(); - - const userId = client.getUserId(); - - if (!userId) { - reject("unable to start voice broadcast if current user is unknown"); - return promise; - } - - let result: ISendEventResponse | null = null; - - const onRoomStateEvents = (): void => { - if (!result) return; - - const voiceBroadcastEvent = room.currentState.getStateEvents(VoiceBroadcastInfoEventType, userId); - - if (voiceBroadcastEvent?.getId() === result.event_id) { - room.off(RoomStateEvent.Events, onRoomStateEvents); - const recording = new VoiceBroadcastRecording(voiceBroadcastEvent, client); - recordingsStore.setCurrent(recording); - recording.start(); - resolve(recording); - } - }; - - room.on(RoomStateEvent.Events, onRoomStateEvents); - - // XXX Michael W: refactor to live event - result = await client.sendStateEvent( - room.roomId, - VoiceBroadcastInfoEventType, - { - device_id: client.getDeviceId(), - state: VoiceBroadcastInfoState.Started, - chunk_length: getChunkLength(), - } as VoiceBroadcastInfoEventContent, - userId, - ); - - return promise; -}; - -/** - * Starts a new Voice Broadcast Recording, if - * - the user has the permissions to do so in the room - * - the user is not already recording a voice broadcast - * - there is no other broadcast being recorded in the room, yet - * Sends a voice_broadcast_info state event and waits for the event to actually appear in the room state. - */ -export const startNewVoiceBroadcastRecording = async ( - room: Room, - client: MatrixClient, - playbacksStore: VoiceBroadcastPlaybacksStore, - recordingsStore: VoiceBroadcastRecordingsStore, -): Promise => { - if (!(await checkVoiceBroadcastPreConditions(room, client, recordingsStore))) { - return null; - } - - // pause and clear current playback (if any) - playbacksStore.getCurrent()?.pause(); - playbacksStore.clearCurrent(); - - return startBroadcast(room, client, recordingsStore); -}; diff --git a/src/voice-broadcast/utils/textForVoiceBroadcastStoppedEvent.tsx b/src/voice-broadcast/utils/textForVoiceBroadcastStoppedEvent.tsx deleted file mode 100644 index bc2aa412a5..0000000000 --- a/src/voice-broadcast/utils/textForVoiceBroadcastStoppedEvent.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React, { ReactNode } from "react"; -import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; - -import { MatrixClientPeg } from "../../MatrixClientPeg"; -import AccessibleButton from "../../components/views/elements/AccessibleButton"; -import { highlightEvent } from "../../utils/EventUtils"; -import { _t } from "../../languageHandler"; -import { getSenderName } from "../../utils/event/getSenderName"; - -export const textForVoiceBroadcastStoppedEvent = (event: MatrixEvent, client: MatrixClient): (() => ReactNode) => { - return (): ReactNode => { - const ownUserId = MatrixClientPeg.get()?.getUserId(); - const startEventId = event.getRelation()?.event_id; - const roomId = event.getRoomId(); - - const templateTags = { - a: (text: string) => - startEventId && roomId ? ( - highlightEvent(roomId, startEventId)}> - {text} - - ) : ( - text - ), - }; - - if (ownUserId && ownUserId === event.getSender()) { - return _t("timeline|io.element.voice_broadcast_info|you", {}, templateTags); - } - - return _t("timeline|io.element.voice_broadcast_info|user", { senderName: getSenderName(event) }, templateTags); - }; -}; diff --git a/src/voice-broadcast/utils/textForVoiceBroadcastStoppedEventWithoutLink.ts b/src/voice-broadcast/utils/textForVoiceBroadcastStoppedEventWithoutLink.ts deleted file mode 100644 index 13d7f47c48..0000000000 --- a/src/voice-broadcast/utils/textForVoiceBroadcastStoppedEventWithoutLink.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -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("event_preview|io.element.voice_broadcast_info|you", {}); - } - - return _t("event_preview|io.element.voice_broadcast_info|user", { senderName: getSenderName(event) }); -}; diff --git a/test/unit-tests/LegacyCallHandler-test.ts b/test/unit-tests/LegacyCallHandler-test.ts index c3e64dcf94..476d89a1f0 100644 --- a/test/unit-tests/LegacyCallHandler-test.ts +++ b/test/unit-tests/LegacyCallHandler-test.ts @@ -39,10 +39,6 @@ import { Action } from "../../src/dispatcher/actions"; import { getFunctionalMembers } from "../../src/utils/room/getFunctionalMembers"; import SettingsStore from "../../src/settings/SettingsStore"; import { UIFeature } from "../../src/settings/UIFeature"; -import { VoiceBroadcastInfoState, VoiceBroadcastPlayback, VoiceBroadcastRecording } from "../../src/voice-broadcast"; -import { mkVoiceBroadcastInfoStateEvent } from "./voice-broadcast/utils/test-utils"; -import { SdkContextClass } from "../../src/contexts/SDKContext"; -import Modal from "../../src/Modal"; import { createAudioContext } from "../../src/audio/compat"; import * as ManagedHybrid from "../../src/widgets/ManagedHybrid"; @@ -403,53 +399,6 @@ describe("LegacyCallHandler", () => { await callHandler.placeCall(NATIVE_ROOM_ALICE, CallType.Voice); expect(spy).toHaveBeenCalledWith(MatrixClientPeg.safeGet().getRoom(NATIVE_ROOM_ALICE)); }); - - describe("when listening to a voice broadcast", () => { - let voiceBroadcastPlayback: VoiceBroadcastPlayback; - - beforeEach(() => { - voiceBroadcastPlayback = new VoiceBroadcastPlayback( - mkVoiceBroadcastInfoStateEvent( - "!room:example.com", - VoiceBroadcastInfoState.Started, - MatrixClientPeg.safeGet().getSafeUserId(), - "d42", - ), - MatrixClientPeg.safeGet(), - SdkContextClass.instance.voiceBroadcastRecordingsStore, - ); - SdkContextClass.instance.voiceBroadcastPlaybacksStore.setCurrent(voiceBroadcastPlayback); - jest.spyOn(voiceBroadcastPlayback, "pause").mockImplementation(); - }); - - it("and placing a call should pause the broadcast", async () => { - callHandler.placeCall(NATIVE_ROOM_ALICE, CallType.Voice); - await untilCallHandlerEvent(callHandler, LegacyCallHandlerEvent.CallState); - - expect(voiceBroadcastPlayback.pause).toHaveBeenCalled(); - }); - }); - - describe("when recording a voice broadcast", () => { - beforeEach(() => { - SdkContextClass.instance.voiceBroadcastRecordingsStore.setCurrent( - new VoiceBroadcastRecording( - mkVoiceBroadcastInfoStateEvent( - "!room:example.com", - VoiceBroadcastInfoState.Started, - MatrixClientPeg.safeGet().getSafeUserId(), - "d42", - ), - MatrixClientPeg.safeGet(), - ), - ); - }); - - it("and placing a call should show the info dialog", async () => { - callHandler.placeCall(NATIVE_ROOM_ALICE, CallType.Voice); - expect(Modal.createDialog).toMatchSnapshot(); - }); - }); }); describe("LegacyCallHandler without third party protocols", () => { @@ -528,9 +477,6 @@ describe("LegacyCallHandler without third party protocols", () => { audioElement.id = "remoteAudio"; document.body.appendChild(audioElement); - SdkContextClass.instance.voiceBroadcastPlaybacksStore.clearCurrent(); - SdkContextClass.instance.voiceBroadcastRecordingsStore.clearCurrent(); - fetchMock.get( "/media/ring.mp3", { body: new Blob(["1", "2", "3", "4"], { type: "audio/mpeg" }) }, diff --git a/test/unit-tests/Notifier-test.ts b/test/unit-tests/Notifier-test.ts index 0996c625b5..f94f50724d 100644 --- a/test/unit-tests/Notifier-test.ts +++ b/test/unit-tests/Notifier-test.ts @@ -43,8 +43,6 @@ import { mkThread } from "../test-utils/threads"; import dis from "../../src/dispatcher/dispatcher"; import { ThreadPayload } from "../../src/dispatcher/payloads/ThreadPayload"; import { Action } from "../../src/dispatcher/actions"; -import { VoiceBroadcastChunkEventType, VoiceBroadcastInfoState } from "../../src/voice-broadcast"; -import { mkVoiceBroadcastInfoStateEvent } from "./voice-broadcast/utils/test-utils"; import { addReplyToMessageContent } from "../../src/utils/Reply"; jest.mock("../../src/utils/notifications", () => ({ @@ -85,16 +83,13 @@ describe("Notifier", () => { }); }; - const mkAudioEvent = (broadcastChunkContent?: object): MatrixEvent => { - const chunkContent = broadcastChunkContent ? { [VoiceBroadcastChunkEventType]: broadcastChunkContent } : {}; - + const mkAudioEvent = (): MatrixEvent => { return mkEvent({ event: true, type: EventType.RoomMessage, user: "@user:example.com", room: "!room:example.com", content: { - ...chunkContent, msgtype: MsgType.Audio, body: "test audio message", }, @@ -320,24 +315,6 @@ describe("Notifier", () => { ); }); - it("should display the expected notification for a broadcast chunk with sequence = 1", () => { - const audioEvent = mkAudioEvent({ sequence: 1 }); - Notifier.displayPopupNotification(audioEvent, testRoom); - expect(MockPlatform.displayNotification).toHaveBeenCalledWith( - "@user:example.com (!room1:server)", - "@user:example.com started a voice broadcast", - "data:image/png;base64,00", - testRoom, - audioEvent, - ); - }); - - it("should display the expected notification for a broadcast chunk with sequence = 2", () => { - const audioEvent = mkAudioEvent({ sequence: 2 }); - Notifier.displayPopupNotification(audioEvent, testRoom); - expect(MockPlatform.displayNotification).not.toHaveBeenCalled(); - }); - it("should strip reply fallback", () => { const event = mkMessage({ msg: "Test", @@ -581,24 +558,6 @@ describe("Notifier", () => { Notifier.evaluateEvent(mkAudioEvent()); expect(Notifier.displayPopupNotification).toHaveBeenCalledTimes(1); }); - - it("should not show a notification for broadcast info events in any case", () => { - // Let client decide to show a notification - mockClient.getPushActionsForEvent.mockReturnValue({ - notify: true, - tweaks: {}, - }); - - const broadcastStartedEvent = mkVoiceBroadcastInfoStateEvent( - "!other:example.org", - VoiceBroadcastInfoState.Started, - "@user:example.com", - "ABC123", - ); - - Notifier.evaluateEvent(broadcastStartedEvent); - expect(Notifier.displayPopupNotification).not.toHaveBeenCalled(); - }); }); describe("setPromptHidden", () => { diff --git a/test/unit-tests/SdkConfig-test.ts b/test/unit-tests/SdkConfig-test.ts index 4204a698fa..19d0eec9c3 100644 --- a/test/unit-tests/SdkConfig-test.ts +++ b/test/unit-tests/SdkConfig-test.ts @@ -18,10 +18,6 @@ describe("SdkConfig", () => { describe("with custom values", () => { beforeEach(() => { SdkConfig.put({ - voice_broadcast: { - chunk_length: 42, - max_length: 1337, - }, feedback: { existing_issues_url: "https://existing", } as any, @@ -30,8 +26,6 @@ describe("SdkConfig", () => { it("should return the custom config", () => { const customConfig = JSON.parse(JSON.stringify(DEFAULTS)); - customConfig.voice_broadcast.chunk_length = 42; - customConfig.voice_broadcast.max_length = 1337; customConfig.feedback.existing_issues_url = "https://existing"; expect(SdkConfig.get()).toEqual(customConfig); }); diff --git a/test/unit-tests/TestSdkContext.ts b/test/unit-tests/TestSdkContext.ts index 0d7e806514..f60b083bef 100644 --- a/test/unit-tests/TestSdkContext.ts +++ b/test/unit-tests/TestSdkContext.ts @@ -16,11 +16,6 @@ import { SpaceStoreClass } from "../../src/stores/spaces/SpaceStore"; import { WidgetLayoutStore } from "../../src/stores/widgets/WidgetLayoutStore"; import { WidgetPermissionStore } from "../../src/stores/widgets/WidgetPermissionStore"; import WidgetStore from "../../src/stores/WidgetStore"; -import { - VoiceBroadcastPlaybacksStore, - VoiceBroadcastPreRecordingStore, - VoiceBroadcastRecordingsStore, -} from "../../src/voice-broadcast"; /** * A class which provides the same API as SdkContextClass but adds additional unsafe setters which can @@ -36,9 +31,6 @@ export class TestSdkContext extends SdkContextClass { declare public _PosthogAnalytics?: PosthogAnalytics; declare public _SlidingSyncManager?: SlidingSyncManager; declare public _SpaceStore?: SpaceStoreClass; - declare public _VoiceBroadcastRecordingsStore?: VoiceBroadcastRecordingsStore; - declare public _VoiceBroadcastPreRecordingStore?: VoiceBroadcastPreRecordingStore; - declare public _VoiceBroadcastPlaybacksStore?: VoiceBroadcastPlaybacksStore; constructor() { super(); diff --git a/test/unit-tests/__snapshots__/LegacyCallHandler-test.ts.snap b/test/unit-tests/__snapshots__/LegacyCallHandler-test.ts.snap deleted file mode 100644 index aaf4d78758..0000000000 --- a/test/unit-tests/__snapshots__/LegacyCallHandler-test.ts.snap +++ /dev/null @@ -1,24 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LegacyCallHandler when recording a voice broadcast and placing a call should show the info dialog 1`] = ` -[MockFunction] { - "calls": [ - [ - [Function], - { - "description":

- You can’t start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call. -

, - "hasCloseButton": true, - "title": "Can’t start a call", - }, - ], - ], - "results": [ - { - "type": "return", - "value": undefined, - }, - ], -} -`; diff --git a/test/unit-tests/components/structures/MatrixChat-test.tsx b/test/unit-tests/components/structures/MatrixChat-test.tsx index 28bf99fa97..fd17ccf583 100644 --- a/test/unit-tests/components/structures/MatrixChat-test.tsx +++ b/test/unit-tests/components/structures/MatrixChat-test.tsx @@ -44,7 +44,6 @@ import { } from "../../../test-utils"; import * as leaveRoomUtils from "../../../../src/utils/leave-behaviour"; import { OidcClientError } from "../../../../src/utils/oidc/error"; -import * as voiceBroadcastUtils from "../../../../src/voice-broadcast/utils/cleanUpBroadcasts"; import LegacyCallHandler from "../../../../src/LegacyCallHandler"; import { CallStore } from "../../../../src/stores/CallStore"; import { Call } from "../../../../src/models/Call"; @@ -811,7 +810,6 @@ describe("", () => { jest.spyOn(LegacyCallHandler.instance, "hangupAllCalls") .mockClear() .mockImplementation(() => {}); - jest.spyOn(voiceBroadcastUtils, "cleanUpBroadcasts").mockImplementation(async () => {}); jest.spyOn(PosthogAnalytics.instance, "logout").mockImplementation(() => {}); jest.spyOn(EventIndexPeg, "deleteEventIndex").mockImplementation(async () => {}); @@ -831,22 +829,12 @@ describe("", () => { jest.spyOn(logger, "warn").mockClear(); }); - afterAll(() => { - jest.spyOn(voiceBroadcastUtils, "cleanUpBroadcasts").mockRestore(); - }); - it("should hangup all legacy calls", async () => { await getComponentAndWaitForReady(); await dispatchLogoutAndWait(); expect(LegacyCallHandler.instance.hangupAllCalls).toHaveBeenCalled(); }); - it("should cleanup broadcasts", async () => { - await getComponentAndWaitForReady(); - await dispatchLogoutAndWait(); - expect(voiceBroadcastUtils.cleanUpBroadcasts).toHaveBeenCalled(); - }); - it("should disconnect all calls", async () => { await getComponentAndWaitForReady(); await dispatchLogoutAndWait(); diff --git a/test/unit-tests/components/structures/PipContainer-test.tsx b/test/unit-tests/components/structures/PipContainer-test.tsx index 446727c74e..f573b0a0cd 100644 --- a/test/unit-tests/components/structures/PipContainer-test.tsx +++ b/test/unit-tests/components/structures/PipContainer-test.tsx @@ -10,7 +10,7 @@ import React from "react"; import { mocked, Mocked } from "jest-mock"; import { screen, render, act, cleanup } from "jest-matrix-react"; import userEvent from "@testing-library/user-event"; -import { MatrixClient, PendingEventOrdering, Room, MatrixEvent, RoomStateEvent } from "matrix-js-sdk/src/matrix"; +import { MatrixClient, PendingEventOrdering, Room, RoomStateEvent } from "matrix-js-sdk/src/matrix"; import { Widget, ClientWidgetApi } from "matrix-widget-api"; import { UserEvent } from "@testing-library/user-event/dist/types/setup/setup"; @@ -26,7 +26,6 @@ import { wrapInSdkContext, mkRoomCreateEvent, mockPlatformPeg, - flushPromises, useMockMediaDevices, } from "../../../test-utils"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; @@ -39,17 +38,7 @@ import defaultDispatcher from "../../../../src/dispatcher/dispatcher"; import { Action } from "../../../../src/dispatcher/actions"; import { ViewRoomPayload } from "../../../../src/dispatcher/payloads/ViewRoomPayload"; import { TestSdkContext } from "../../TestSdkContext"; -import { - VoiceBroadcastInfoState, - VoiceBroadcastPlaybacksStore, - VoiceBroadcastPreRecording, - VoiceBroadcastPreRecordingStore, - VoiceBroadcastRecording, - VoiceBroadcastRecordingsStore, -} from "../../../../src/voice-broadcast"; -import { mkVoiceBroadcastInfoStateEvent } from "../../voice-broadcast/utils/test-utils"; import { RoomViewStore } from "../../../../src/stores/RoomViewStore"; -import { IRoomStateEventsActionPayload } from "../../../../src/actions/MatrixActionCreators"; import { Container, WidgetLayoutStore } from "../../../../src/stores/widgets/WidgetLayoutStore"; import WidgetStore from "../../../../src/stores/WidgetStore"; import { WidgetType } from "../../../../src/widgets/WidgetType"; @@ -76,13 +65,6 @@ describe("PipContainer", () => { let room: Room; let room2: Room; let alice: RoomMember; - let voiceBroadcastRecordingsStore: VoiceBroadcastRecordingsStore; - let voiceBroadcastPreRecordingStore: VoiceBroadcastPreRecordingStore; - let voiceBroadcastPlaybacksStore: VoiceBroadcastPlaybacksStore; - - const actFlushPromises = async () => { - await flushPromises(); - }; beforeEach(async () => { useMockMediaDevices(); @@ -125,13 +107,7 @@ describe("PipContainer", () => { sdkContext = new TestSdkContext(); // @ts-ignore PipContainer uses SDKContext in the constructor SdkContextClass.instance = sdkContext; - voiceBroadcastRecordingsStore = new VoiceBroadcastRecordingsStore(); - voiceBroadcastPreRecordingStore = new VoiceBroadcastPreRecordingStore(); - voiceBroadcastPlaybacksStore = new VoiceBroadcastPlaybacksStore(voiceBroadcastRecordingsStore); sdkContext.client = client; - sdkContext._VoiceBroadcastRecordingsStore = voiceBroadcastRecordingsStore; - sdkContext._VoiceBroadcastPreRecordingStore = voiceBroadcastPreRecordingStore; - sdkContext._VoiceBroadcastPlaybacksStore = voiceBroadcastPlaybacksStore; }); afterEach(async () => { @@ -190,51 +166,10 @@ describe("PipContainer", () => { ActiveWidgetStore.instance.destroyPersistentWidget("1", room.roomId); }; - const makeVoiceBroadcastInfoStateEvent = (): MatrixEvent => { - return mkVoiceBroadcastInfoStateEvent( - room.roomId, - VoiceBroadcastInfoState.Started, - alice.userId, - client.getDeviceId() || "", - ); - }; - - const setUpVoiceBroadcastRecording = () => { - const infoEvent = makeVoiceBroadcastInfoStateEvent(); - const voiceBroadcastRecording = new VoiceBroadcastRecording(infoEvent, client); - voiceBroadcastRecordingsStore.setCurrent(voiceBroadcastRecording); - }; - - const setUpVoiceBroadcastPreRecording = () => { - const voiceBroadcastPreRecording = new VoiceBroadcastPreRecording( - room, - alice, - client, - voiceBroadcastPlaybacksStore, - voiceBroadcastRecordingsStore, - ); - voiceBroadcastPreRecordingStore.setCurrent(voiceBroadcastPreRecording); - }; - const setUpRoomViewStore = () => { sdkContext._RoomViewStore = new RoomViewStore(defaultDispatcher, sdkContext); }; - const mkVoiceBroadcast = (room: Room): MatrixEvent => { - const infoEvent = makeVoiceBroadcastInfoStateEvent(); - room.currentState.setStateEvents([infoEvent]); - defaultDispatcher.dispatch( - { - action: "MatrixActions.RoomState.events", - event: infoEvent, - state: room.currentState, - lastStateEvent: null, - }, - true, - ); - return infoEvent; - }; - it("hides if there's no content", () => { renderPip(); expect(screen.queryByRole("complementary")).toBeNull(); @@ -339,138 +274,4 @@ describe("PipContainer", () => { WidgetStore.instance.removeVirtualWidget("1", room.roomId); }); - - describe("when there is a voice broadcast recording and pre-recording", () => { - beforeEach(async () => { - setUpVoiceBroadcastPreRecording(); - setUpVoiceBroadcastRecording(); - renderPip(); - await actFlushPromises(); - }); - - it("should render the voice broadcast recording PiP", () => { - // check for the „Live“ badge to be present - expect(screen.queryByText("Live")).toBeInTheDocument(); - }); - - it("and a call it should show both, the call and the recording", async () => { - await withCall(async () => { - // Broadcast: Check for the „Live“ badge to be present - expect(screen.queryByText("Live")).toBeInTheDocument(); - // Call: Check for the „Leave“ button to be present - screen.getByRole("button", { name: "Leave" }); - }); - }); - }); - - describe("when there is a voice broadcast playback and pre-recording", () => { - beforeEach(async () => { - mkVoiceBroadcast(room); - setUpVoiceBroadcastPreRecording(); - renderPip(); - await actFlushPromises(); - }); - - it("should render the voice broadcast pre-recording PiP", () => { - // check for the „Go live“ button - expect(screen.queryByText("Go live")).toBeInTheDocument(); - }); - }); - - describe("when there is a voice broadcast pre-recording", () => { - beforeEach(async () => { - setUpVoiceBroadcastPreRecording(); - renderPip(); - await actFlushPromises(); - }); - - it("should render the voice broadcast pre-recording PiP", () => { - // check for the „Go live“ button - expect(screen.queryByText("Go live")).toBeInTheDocument(); - }); - }); - - describe("when listening to a voice broadcast in a room and then switching to another room", () => { - beforeEach(async () => { - setUpRoomViewStore(); - viewRoom(room.roomId); - mkVoiceBroadcast(room); - await actFlushPromises(); - - expect(voiceBroadcastPlaybacksStore.getCurrent()).toBeTruthy(); - - await voiceBroadcastPlaybacksStore.getCurrent()?.start(); - viewRoom(room2.roomId); - renderPip(); - }); - - it("should render the small voice broadcast playback PiP", () => { - // check for the „pause voice broadcast“ button - expect(screen.getByLabelText("pause voice broadcast")).toBeInTheDocument(); - // check for the absence of the „30s forward“ button - expect(screen.queryByLabelText("30s forward")).not.toBeInTheDocument(); - }); - }); - - describe("when viewing a room with a live voice broadcast", () => { - let startEvent!: MatrixEvent; - - beforeEach(async () => { - setUpRoomViewStore(); - viewRoom(room.roomId); - startEvent = mkVoiceBroadcast(room); - renderPip(); - await actFlushPromises(); - }); - - it("should render the voice broadcast playback pip", () => { - // check for the „resume voice broadcast“ button - expect(screen.queryByLabelText("play voice broadcast")).toBeInTheDocument(); - }); - - describe("and the broadcast stops", () => { - beforeEach(async () => { - const stopEvent = mkVoiceBroadcastInfoStateEvent( - room.roomId, - VoiceBroadcastInfoState.Stopped, - alice.userId, - client.getDeviceId() || "", - startEvent, - ); - - await act(async () => { - room.currentState.setStateEvents([stopEvent]); - defaultDispatcher.dispatch( - { - action: "MatrixActions.RoomState.events", - event: stopEvent, - state: room.currentState, - lastStateEvent: stopEvent, - }, - true, - ); - await flushPromises(); - }); - }); - - it("should not render the voice broadcast playback pip", () => { - // check for the „resume voice broadcast“ button - expect(screen.queryByLabelText("play voice broadcast")).not.toBeInTheDocument(); - }); - }); - - describe("and leaving the room", () => { - beforeEach(async () => { - await act(async () => { - viewRoom(room2.roomId); - await flushPromises(); - }); - }); - - it("should not render the voice broadcast playback pip", () => { - // check for the „resume voice broadcast“ button - expect(screen.queryByLabelText("play voice broadcast")).not.toBeInTheDocument(); - }); - }); - }); }); diff --git a/test/unit-tests/components/structures/UserMenu-test.tsx b/test/unit-tests/components/structures/UserMenu-test.tsx index ac76aba2ad..907bf664b7 100644 --- a/test/unit-tests/components/structures/UserMenu-test.tsx +++ b/test/unit-tests/components/structures/UserMenu-test.tsx @@ -7,20 +7,14 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; -import { act, render, RenderResult, screen, waitFor } from "jest-matrix-react"; -import { DEVICE_CODE_SCOPE, MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; +import { render, screen, waitFor } from "jest-matrix-react"; +import { DEVICE_CODE_SCOPE, MatrixClient, Room } from "matrix-js-sdk/src/matrix"; import { CryptoApi } from "matrix-js-sdk/src/crypto-api"; import { mocked } from "jest-mock"; import fetchMock from "fetch-mock-jest"; import UnwrappedUserMenu from "../../../../src/components/structures/UserMenu"; import { stubClient, wrapInSdkContext } from "../../../test-utils"; -import { - VoiceBroadcastInfoState, - VoiceBroadcastRecording, - VoiceBroadcastRecordingsStore, -} from "../../../../src/voice-broadcast"; -import { mkVoiceBroadcastInfoStateEvent } from "../../voice-broadcast/utils/test-utils"; import { TestSdkContext } from "../../TestSdkContext"; import defaultDispatcher from "../../../../src/dispatcher/dispatcher"; import LogoutDialog from "../../../../src/components/views/dialogs/LogoutDialog"; @@ -34,71 +28,12 @@ import { UserTab } from "../../../../src/components/views/dialogs/UserTab"; describe("", () => { let client: MatrixClient; - let renderResult: RenderResult; let sdkContext: TestSdkContext; beforeEach(() => { sdkContext = new TestSdkContext(); }); - describe(" when video broadcast", () => { - let voiceBroadcastInfoEvent: MatrixEvent; - let voiceBroadcastRecording: VoiceBroadcastRecording; - let voiceBroadcastRecordingsStore: VoiceBroadcastRecordingsStore; - - beforeAll(() => { - client = stubClient(); - voiceBroadcastInfoEvent = mkVoiceBroadcastInfoStateEvent( - "!room:example.com", - VoiceBroadcastInfoState.Started, - client.getUserId() || "", - client.getDeviceId() || "", - ); - }); - - beforeEach(() => { - voiceBroadcastRecordingsStore = new VoiceBroadcastRecordingsStore(); - sdkContext._VoiceBroadcastRecordingsStore = voiceBroadcastRecordingsStore; - - voiceBroadcastRecording = new VoiceBroadcastRecording(voiceBroadcastInfoEvent, client); - }); - - describe("when rendered", () => { - beforeEach(() => { - const UserMenu = wrapInSdkContext(UnwrappedUserMenu, sdkContext); - renderResult = render(); - }); - - it("should render as expected", () => { - expect(renderResult.container).toMatchSnapshot(); - }); - - describe("and a live voice broadcast starts", () => { - beforeEach(() => { - act(() => { - voiceBroadcastRecordingsStore.setCurrent(voiceBroadcastRecording); - }); - }); - - it("should render the live voice broadcast avatar addon", () => { - expect(renderResult.queryByTestId("user-menu-live-vb")).toBeInTheDocument(); - }); - - describe("and the broadcast ends", () => { - beforeEach(() => { - act(() => { - voiceBroadcastRecordingsStore.clearCurrent(); - }); - }); - - it("should not render the live voice broadcast avatar addon", () => { - expect(renderResult.queryByTestId("user-menu-live-vb")).not.toBeInTheDocument(); - }); - }); - }); - }); - }); - describe(" logout", () => { beforeEach(() => { client = stubClient(); @@ -106,7 +41,7 @@ describe("", () => { it("should logout directly if no crypto", async () => { const UserMenu = wrapInSdkContext(UnwrappedUserMenu, sdkContext); - renderResult = render(); + render(); mocked(client.getRooms).mockReturnValue([ { @@ -128,7 +63,7 @@ describe("", () => { it("should logout directly if no encrypted rooms", async () => { const UserMenu = wrapInSdkContext(UnwrappedUserMenu, sdkContext); - renderResult = render(); + render(); mocked(client.getRooms).mockReturnValue([ { @@ -152,7 +87,7 @@ describe("", () => { it("should show dialog if some encrypted rooms", async () => { const UserMenu = wrapInSdkContext(UnwrappedUserMenu, sdkContext); - renderResult = render(); + render(); mocked(client.getRooms).mockReturnValue([ { diff --git a/test/unit-tests/components/structures/__snapshots__/UserMenu-test.tsx.snap b/test/unit-tests/components/structures/__snapshots__/UserMenu-test.tsx.snap deleted file mode 100644 index 029db9bfd4..0000000000 --- a/test/unit-tests/components/structures/__snapshots__/UserMenu-test.tsx.snap +++ /dev/null @@ -1,33 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` when video broadcast when rendered should render as expected 1`] = ` -
-
- -
-
-`; diff --git a/test/unit-tests/components/views/context_menus/MessageContextMenu-test.tsx b/test/unit-tests/components/views/context_menus/MessageContextMenu-test.tsx index 892ca6dbed..142840fd5a 100644 --- a/test/unit-tests/components/views/context_menus/MessageContextMenu-test.tsx +++ b/test/unit-tests/components/views/context_menus/MessageContextMenu-test.tsx @@ -37,8 +37,6 @@ 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"; import { createMessageEventContent } from "../../../../test-utils/events"; import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; @@ -234,17 +232,6 @@ describe("MessageContextMenu", () => { expect(document.querySelector('li[aria-label="Forward"]')).toBeFalsy(); }); - it("should not allow forwarding a voice broadcast", () => { - const broadcastStartEvent = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Started, - "@user:example.com", - "ABC123", - ); - createMenu(broadcastStartEvent); - expect(document.querySelector('li[aria-label="Forward"]')).toBeFalsy(); - }); - describe("forwarding beacons", () => { const aliceId = "@alice:server.org"; diff --git a/test/unit-tests/components/views/dialogs/ConfirmRedactDialog-test.tsx b/test/unit-tests/components/views/dialogs/ConfirmRedactDialog-test.tsx index c648f416f9..ed5b545ed5 100644 --- a/test/unit-tests/components/views/dialogs/ConfirmRedactDialog-test.tsx +++ b/test/unit-tests/components/views/dialogs/ConfirmRedactDialog-test.tsx @@ -6,14 +6,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { Feature, ServerSupport } from "matrix-js-sdk/src/feature"; -import { MatrixClient, MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix"; +import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; import { screen, act } from "jest-matrix-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", () => { @@ -21,15 +18,6 @@ describe("ConfirmRedactDialog", () => { let client: MatrixClient; let mxEvent: MatrixEvent; - const setUpVoiceBroadcastStartedEvent = () => { - mxEvent = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Started, - client.getUserId()!, - client.deviceId!, - ); - }; - const confirmDeleteVoiceBroadcastStartedEvent = async () => { act(() => createRedactEventDialog({ mxEvent })); // double-flush promises required for the dialog to show up @@ -68,44 +56,4 @@ describe("ConfirmRedactDialog", () => { `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_rel_types`", () => { - 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_rel_types`", () => { - expect(client.redactEvent).toHaveBeenCalledWith(roomId, mxEvent.getId(), undefined, { - with_rel_types: [RelationType.Reference], - }); - }); - }); - }); - }); }); diff --git a/test/unit-tests/components/views/dialogs/__snapshots__/DevtoolsDialog-test.tsx.snap b/test/unit-tests/components/views/dialogs/__snapshots__/DevtoolsDialog-test.tsx.snap index 910569c112..622ed32065 100644 --- a/test/unit-tests/components/views/dialogs/__snapshots__/DevtoolsDialog-test.tsx.snap +++ b/test/unit-tests/components/views/dialogs/__snapshots__/DevtoolsDialog-test.tsx.snap @@ -185,33 +185,6 @@ exports[`DevtoolsDialog renders the devtools dialog 1`] = ` />
-
- -
-
-
-
({ default: () =>
, })); -jest.mock("../../../../../src/voice-broadcast/components/VoiceBroadcastBody", () => ({ - VoiceBroadcastBody: () =>
, -})); - jest.mock("../../../../../src/components/views/messages/MImageBody", () => ({ __esModule: true, default: () =>
, @@ -81,27 +76,6 @@ describe("MessageEvent", () => { jest.spyOn(SettingsStore, "unwatchSetting").mockImplementation(jest.fn()); }); - describe("when a voice broadcast start event occurs", () => { - let result: RenderResult; - - beforeEach(() => { - event = mkEvent({ - event: true, - type: VoiceBroadcastInfoEventType, - user: client.getUserId()!, - room: room.roomId, - content: { - state: VoiceBroadcastInfoState.Started, - }, - }); - result = renderMessageEvent(); - }); - - it("should render a VoiceBroadcast component", () => { - result.getByTestId("voice-broadcast-body"); - }); - }); - describe("when an image with a caption is sent", () => { let result: RenderResult; diff --git a/test/unit-tests/components/views/rooms/MessageComposer-test.tsx b/test/unit-tests/components/views/rooms/MessageComposer-test.tsx index 4ef533091a..6849612fee 100644 --- a/test/unit-tests/components/views/rooms/MessageComposer-test.tsx +++ b/test/unit-tests/components/views/rooms/MessageComposer-test.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import * as React from "react"; -import { EventType, MatrixEvent, Room, RoomMember, THREAD_RELATION_TYPE } from "matrix-js-sdk/src/matrix"; +import { EventType, MatrixEvent, RoomMember, THREAD_RELATION_TYPE } from "matrix-js-sdk/src/matrix"; import { act, fireEvent, render, screen, waitFor } from "jest-matrix-react"; import userEvent from "@testing-library/user-event"; @@ -19,7 +19,6 @@ import { mkStubRoom, mockPlatformPeg, stubClient, - waitEnoughCyclesForModal, } from "../../../../test-utils"; import MessageComposer from "../../../../../src/components/views/rooms/MessageComposer"; import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; @@ -28,7 +27,6 @@ import { IRoomState } from "../../../../../src/components/structures/RoomView"; import ResizeNotifier from "../../../../../src/utils/ResizeNotifier"; import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks"; import { LocalRoom } from "../../../../../src/models/LocalRoom"; -import { Features } from "../../../../../src/settings/Settings"; import SettingsStore from "../../../../../src/settings/SettingsStore"; import { SettingLevel } from "../../../../../src/settings/SettingLevel"; import dis from "../../../../../src/dispatcher/dispatcher"; @@ -36,9 +34,6 @@ import { E2EStatus } from "../../../../../src/utils/ShieldUtils"; import { addTextToComposerRTL } from "../../../../test-utils/composer"; import UIStore, { UI_EVENTS } from "../../../../../src/stores/UIStore"; import { Action } from "../../../../../src/dispatcher/actions"; -import { VoiceBroadcastInfoState, VoiceBroadcastRecording } from "../../../../../src/voice-broadcast"; -import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils"; -import { SdkContextClass } from "../../../../../src/contexts/SDKContext"; import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; const openStickerPicker = async (): Promise => { @@ -51,15 +46,6 @@ const startVoiceMessage = async (): Promise => { await userEvent.click(screen.getByLabelText("Voice Message")); }; -const setCurrentBroadcastRecording = (room: Room, state: VoiceBroadcastInfoState): void => { - const recording = new VoiceBroadcastRecording( - mkVoiceBroadcastInfoStateEvent(room.roomId, state, "@user:example.com", "ABC123"), - MatrixClientPeg.safeGet(), - state, - ); - act(() => SdkContextClass.instance.voiceBroadcastRecordingsStore.setCurrent(recording)); -}; - const expectVoiceMessageRecordingTriggered = (): void => { // Checking for the voice message dialog text, if no mic can be found. // By this we know at least that starting a voice message was triggered. @@ -78,14 +64,11 @@ describe("MessageComposer", () => { await clearAllModals(); jest.useRealTimers(); - SdkContextClass.instance.voiceBroadcastRecordingsStore.clearCurrent(); - // restore settings act(() => { [ "MessageComposerInput.showStickersButton", "MessageComposerInput.showPollsButton", - Features.VoiceBroadcast, "feature_wysiwyg_composer", ].forEach((setting: string): void => { SettingsStore.setValue(setting, null, SettingLevel.DEVICE, SettingsStore.getDefaultValue(setting)); @@ -212,10 +195,6 @@ describe("MessageComposer", () => { setting: "MessageComposerInput.showPollsButton", buttonLabel: "Poll", }, - { - setting: Features.VoiceBroadcast, - buttonLabel: "Voice broadcast", - }, ].forEach(({ setting, buttonLabel }) => { [true, false].forEach((value: boolean) => { describe(`when ${setting} = ${value}`, () => { @@ -437,34 +416,6 @@ describe("MessageComposer", () => { expectVoiceMessageRecordingTriggered(); }); }); - - describe("when recording a voice broadcast and trying to start a voice message", () => { - beforeEach(async () => { - setCurrentBroadcastRecording(room, VoiceBroadcastInfoState.Started); - wrapAndRender({ room }); - await startVoiceMessage(); - await waitEnoughCyclesForModal(); - }); - - it("should not start a voice message and display the info dialog", async () => { - expect(screen.queryByLabelText("Stop recording")).not.toBeInTheDocument(); - expect(screen.getByText("Can't start voice message")).toBeInTheDocument(); - }); - }); - - describe("when there is a stopped voice broadcast recording and trying to start a voice message", () => { - beforeEach(async () => { - setCurrentBroadcastRecording(room, VoiceBroadcastInfoState.Stopped); - wrapAndRender({ room }); - await startVoiceMessage(); - await waitEnoughCyclesForModal(); - }); - - it("should try to start a voice message and should not display the info dialog", async () => { - expect(screen.queryByText("Can't start voice message")).not.toBeInTheDocument(); - expectVoiceMessageRecordingTriggered(); - }); - }); }); describe("for a LocalRoom", () => { diff --git a/test/unit-tests/components/views/rooms/MessageComposerButtons-test.tsx b/test/unit-tests/components/views/rooms/MessageComposerButtons-test.tsx index 08204350ca..c2f56dc968 100644 --- a/test/unit-tests/components/views/rooms/MessageComposerButtons-test.tsx +++ b/test/unit-tests/components/views/rooms/MessageComposerButtons-test.tsx @@ -168,27 +168,4 @@ describe("MessageComposerButtons", () => { ]); }); }); - - describe("with showVoiceBroadcastButton = true", () => { - it("should render the »Voice broadcast« button", () => { - wrapAndRender( - , - false, - ); - - expect(getButtonLabels()).toEqual([ - "Emoji", - "Attachment", - "More options", - ["Sticker", "Voice Message", "Voice broadcast", "Poll", "Location"], - ]); - }); - }); }); diff --git a/test/unit-tests/components/views/rooms/RoomTile-test.tsx b/test/unit-tests/components/views/rooms/RoomTile-test.tsx index 7aa9ad8462..c14c699699 100644 --- a/test/unit-tests/components/views/rooms/RoomTile-test.tsx +++ b/test/unit-tests/components/views/rooms/RoomTile-test.tsx @@ -9,14 +9,7 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import { render, screen, act, RenderResult } from "jest-matrix-react"; import { mocked, Mocked } from "jest-mock"; -import { - MatrixClient, - PendingEventOrdering, - Room, - MatrixEvent, - RoomStateEvent, - Thread, -} from "matrix-js-sdk/src/matrix"; +import { MatrixClient, PendingEventOrdering, Room, RoomStateEvent, Thread } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; import { Widget } from "matrix-widget-api"; @@ -40,8 +33,6 @@ import DMRoomMap from "../../../../../src/utils/DMRoomMap"; import PlatformPeg from "../../../../../src/PlatformPeg"; import BasePlatform from "../../../../../src/BasePlatform"; import { WidgetMessagingStore } from "../../../../../src/stores/widgets/WidgetMessagingStore"; -import { VoiceBroadcastInfoState } from "../../../../../src/voice-broadcast"; -import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils"; import { TestSdkContext } from "../../../TestSdkContext"; import { SDKContext } from "../../../../../src/contexts/SDKContext"; import { shouldShowComponent } from "../../../../../src/customisations/helpers/UIComponents"; @@ -61,20 +52,6 @@ describe("RoomTile", () => { } as unknown as BasePlatform); useMockedCalls(); - const setUpVoiceBroadcast = async (state: VoiceBroadcastInfoState): Promise => { - voiceBroadcastInfoEvent = mkVoiceBroadcastInfoStateEvent( - room.roomId, - state, - client.getSafeUserId(), - client.getDeviceId()!, - ); - - await act(async () => { - room.currentState.setStateEvents([voiceBroadcastInfoEvent]); - await flushPromises(); - }); - }; - const renderRoomTile = (): RenderResult => { return render( @@ -89,7 +66,6 @@ describe("RoomTile", () => { }; let client: Mocked; - let voiceBroadcastInfoEvent: MatrixEvent; let room: Room; let sdkContext: TestSdkContext; let showMessagePreview = false; @@ -303,49 +279,6 @@ describe("RoomTile", () => { }); expect(screen.queryByLabelText(/participant/)).toBe(null); }); - - describe("and a live broadcast starts", () => { - beforeEach(async () => { - renderRoomTile(); - await setUpVoiceBroadcast(VoiceBroadcastInfoState.Started); - }); - - it("should still render the call subtitle", () => { - expect(screen.queryByText("Video")).toBeInTheDocument(); - expect(screen.queryByText("Live")).not.toBeInTheDocument(); - }); - }); - }); - - describe("when a live voice broadcast starts", () => { - beforeEach(async () => { - renderRoomTile(); - await setUpVoiceBroadcast(VoiceBroadcastInfoState.Started); - }); - - it("should render the »Live« subtitle", () => { - expect(screen.queryByText("Live")).toBeInTheDocument(); - }); - - describe("and the broadcast stops", () => { - beforeEach(async () => { - const stopEvent = mkVoiceBroadcastInfoStateEvent( - room.roomId, - VoiceBroadcastInfoState.Stopped, - client.getSafeUserId(), - client.getDeviceId()!, - voiceBroadcastInfoEvent, - ); - await act(async () => { - room.currentState.setStateEvents([stopEvent]); - await flushPromises(); - }); - }); - - it("should not render the »Live« subtitle", () => { - expect(screen.queryByText("Live")).not.toBeInTheDocument(); - }); - }); }); }); diff --git a/test/unit-tests/components/views/settings/tabs/room/RolesRoomSettingsTab-test.tsx b/test/unit-tests/components/views/settings/tabs/room/RolesRoomSettingsTab-test.tsx index 000c38c771..e1a451c9d5 100644 --- a/test/unit-tests/components/views/settings/tabs/room/RolesRoomSettingsTab-test.tsx +++ b/test/unit-tests/components/views/settings/tabs/room/RolesRoomSettingsTab-test.tsx @@ -17,7 +17,6 @@ import userEvent from "@testing-library/user-event"; import RolesRoomSettingsTab from "../../../../../../../src/components/views/settings/tabs/room/RolesRoomSettingsTab"; import { mkStubRoom, withClientContextRenderOptions, stubClient } from "../../../../../../test-utils"; import { MatrixClientPeg } from "../../../../../../../src/MatrixClientPeg"; -import { VoiceBroadcastInfoEventType } from "../../../../../../../src/voice-broadcast"; import SettingsStore from "../../../../../../../src/settings/SettingsStore"; import { ElementCall } from "../../../../../../../src/models/Call"; @@ -34,14 +33,6 @@ describe("RolesRoomSettingsTab", () => { return renderResult; }; - const getVoiceBroadcastsSelect = async (): Promise => { - return (await renderTab()).container.querySelector("select[label='Voice broadcasts']")!; - }; - - const getVoiceBroadcastsSelectedOption = async (): Promise => { - return (await renderTab()).container.querySelector("select[label='Voice broadcasts'] option:checked")!; - }; - beforeEach(() => { stubClient(); cli = MatrixClientPeg.safeGet(); @@ -76,26 +67,6 @@ describe("RolesRoomSettingsTab", () => { expect(container.querySelector(`[placeholder="@admin:server"]`)).toBeDisabled(); }); - it("should initially show »Moderator« permission for »Voice broadcasts«", async () => { - expect((await getVoiceBroadcastsSelectedOption()).textContent).toBe("Moderator"); - }); - - describe("when setting »Default« permission for »Voice broadcasts«", () => { - beforeEach(async () => { - fireEvent.change(await getVoiceBroadcastsSelect(), { - target: { value: 0 }, - }); - }); - - it("should update the power levels", () => { - expect(cli.sendStateEvent).toHaveBeenCalledWith(roomId, EventType.RoomPowerLevels, { - events: { - [VoiceBroadcastInfoEventType]: 0, - }, - }); - }); - }); - describe("Element Call", () => { const setGroupCallsEnabled = (val: boolean): void => { jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => { diff --git a/test/unit-tests/contexts/SdkContext-test.ts b/test/unit-tests/contexts/SdkContext-test.ts index 21f356ed94..340fabdd3d 100644 --- a/test/unit-tests/contexts/SdkContext-test.ts +++ b/test/unit-tests/contexts/SdkContext-test.ts @@ -11,11 +11,8 @@ import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { SdkContextClass } from "../../../src/contexts/SDKContext"; import { OidcClientStore } from "../../../src/stores/oidc/OidcClientStore"; import { UserProfilesStore } from "../../../src/stores/UserProfilesStore"; -import { VoiceBroadcastPreRecordingStore } from "../../../src/voice-broadcast"; import { createTestClient } from "../../test-utils"; -jest.mock("../../../src/voice-broadcast/stores/VoiceBroadcastPreRecordingStore"); - describe("SdkContextClass", () => { let sdkContext = SdkContextClass.instance; let client: MatrixClient; @@ -33,12 +30,6 @@ describe("SdkContextClass", () => { expect(SdkContextClass.instance).toBe(globalInstance); }); - it("voiceBroadcastPreRecordingStore should always return the same VoiceBroadcastPreRecordingStore", () => { - const first = sdkContext.voiceBroadcastPreRecordingStore; - expect(first).toBeInstanceOf(VoiceBroadcastPreRecordingStore); - expect(sdkContext.voiceBroadcastPreRecordingStore).toBe(first); - }); - it("userProfilesStore should raise an error without a client", () => { expect(() => sdkContext.userProfilesStore).toThrow("Unable to create UserProfilesStore without a client"); }); diff --git a/test/unit-tests/events/EventTileFactory-test.ts b/test/unit-tests/events/EventTileFactory-test.ts index 7044a883d0..8a7d09c434 100644 --- a/test/unit-tests/events/EventTileFactory-test.ts +++ b/test/unit-tests/events/EventTileFactory-test.ts @@ -7,19 +7,16 @@ Please see LICENSE files in the repository root for full details. */ import { mocked } from "jest-mock"; -import { EventType, MatrixClient, MatrixEvent, MsgType, RelationType, Room } from "matrix-js-sdk/src/matrix"; +import { EventType, MatrixClient, MatrixEvent, MsgType, Room } from "matrix-js-sdk/src/matrix"; import { JSONEventFactory, MessageEventFactory, pickFactory, RoomCreateEventFactory, - TextualEventFactory, } from "../../../src/events/EventTileFactory"; import SettingsStore from "../../../src/settings/SettingsStore"; -import { VoiceBroadcastChunkEventType, VoiceBroadcastInfoState } from "../../../src/voice-broadcast"; import { createTestClient, mkEvent } from "../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "../voice-broadcast/utils/test-utils"; const roomId = "!room:example.com"; @@ -31,11 +28,7 @@ describe("pickFactory", () => { let createEventWithoutPredecessor: MatrixEvent; let dynamicPredecessorEvent: MatrixEvent; - let voiceBroadcastStartedEvent: MatrixEvent; - let voiceBroadcastStoppedEvent: MatrixEvent; - let voiceBroadcastChunkEvent: MatrixEvent; let utdEvent: MatrixEvent; - let utdBroadcastChunkEvent: MatrixEvent; let audioMessageEvent: MatrixEvent; beforeAll(() => { @@ -82,29 +75,6 @@ describe("pickFactory", () => { last_known_event_id: null, }, }); - voiceBroadcastStartedEvent = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Started, - client.getUserId()!, - client.deviceId!, - ); - room.addLiveEvents([voiceBroadcastStartedEvent], { addToState: true }); - voiceBroadcastStoppedEvent = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Stopped, - client.getUserId()!, - client.deviceId!, - ); - voiceBroadcastChunkEvent = mkEvent({ - event: true, - type: EventType.RoomMessage, - user: client.getUserId()!, - room: roomId, - content: { - msgtype: MsgType.Audio, - [VoiceBroadcastChunkEventType]: {}, - }, - }); audioMessageEvent = mkEvent({ event: true, type: EventType.RoomMessage, @@ -123,20 +93,6 @@ describe("pickFactory", () => { msgtype: "m.bad.encrypted", }, }); - utdBroadcastChunkEvent = mkEvent({ - event: true, - type: EventType.RoomMessage, - user: client.getUserId()!, - room: roomId, - content: { - "msgtype": "m.bad.encrypted", - "m.relates_to": { - rel_type: RelationType.Reference, - event_id: voiceBroadcastStartedEvent.getId(), - }, - }, - }); - jest.spyOn(utdBroadcastChunkEvent, "isDecryptionFailure").mockReturnValue(true); }); it("should return JSONEventFactory for a no-op m.room.power_levels event", () => { @@ -151,10 +107,6 @@ describe("pickFactory", () => { }); describe("when showing hidden events", () => { - it("should return a JSONEventFactory for a voice broadcast event", () => { - expect(pickFactory(voiceBroadcastChunkEvent, client, true)).toBe(JSONEventFactory); - }); - it("should return a JSONEventFactory for a room create event without predecessor", () => { room.currentState.events.set( EventType.RoomCreate, @@ -164,17 +116,9 @@ describe("pickFactory", () => { expect(pickFactory(createEventWithoutPredecessor, client, true)).toBe(JSONEventFactory); }); - it("should return a TextualEventFactory for a voice broadcast stopped event", () => { - expect(pickFactory(voiceBroadcastStoppedEvent, client, true)).toBe(TextualEventFactory); - }); - it("should return a MessageEventFactory for an audio message event", () => { expect(pickFactory(audioMessageEvent, client, true)).toBe(MessageEventFactory); }); - - it("should return a MessageEventFactory for a UTD broadcast chunk event", () => { - expect(pickFactory(utdBroadcastChunkEvent, client, true)).toBe(MessageEventFactory); - }); }); describe("when not showing hidden events", () => { @@ -252,14 +196,6 @@ describe("pickFactory", () => { }); }); - it("should return undefined for a voice broadcast event", () => { - expect(pickFactory(voiceBroadcastChunkEvent, client, false)).toBeUndefined(); - }); - - it("should return a TextualEventFactory for a voice broadcast stopped event", () => { - expect(pickFactory(voiceBroadcastStoppedEvent, client, false)).toBe(TextualEventFactory); - }); - it("should return a MessageEventFactory for an audio message event", () => { expect(pickFactory(audioMessageEvent, client, false)).toBe(MessageEventFactory); }); @@ -267,9 +203,5 @@ describe("pickFactory", () => { it("should return a MessageEventFactory for a UTD event", () => { expect(pickFactory(utdEvent, client, false)).toBe(MessageEventFactory); }); - - it("should return undefined for a UTD broadcast chunk event", () => { - expect(pickFactory(utdBroadcastChunkEvent, client, false)).toBeUndefined(); - }); }); }); diff --git a/test/unit-tests/stores/RoomViewStore-test.ts b/test/unit-tests/stores/RoomViewStore-test.ts index 01f07e89ac..782bb79738 100644 --- a/test/unit-tests/stores/RoomViewStore-test.ts +++ b/test/unit-tests/stores/RoomViewStore-test.ts @@ -24,13 +24,6 @@ import { ActiveRoomChangedPayload } from "../../../src/dispatcher/payloads/Activ import { SpaceStoreClass } from "../../../src/stores/spaces/SpaceStore"; import { TestSdkContext } from "../TestSdkContext"; import { ViewRoomPayload } from "../../../src/dispatcher/payloads/ViewRoomPayload"; -import { - VoiceBroadcastInfoState, - VoiceBroadcastPlayback, - VoiceBroadcastPlaybacksStore, - VoiceBroadcastRecording, -} from "../../../src/voice-broadcast"; -import { mkVoiceBroadcastInfoStateEvent } from "../voice-broadcast/utils/test-utils"; import Modal from "../../../src/Modal"; import ErrorDialog from "../../../src/components/views/dialogs/ErrorDialog"; import { CancelAskToJoinPayload } from "../../../src/dispatcher/payloads/CancelAskToJoinPayload"; @@ -160,7 +153,6 @@ describe("RoomViewStore", function () { stores._SlidingSyncManager = slidingSyncManager; stores._PosthogAnalytics = new MockPosthogAnalytics(); stores._SpaceStore = new MockSpaceStore(); - stores._VoiceBroadcastPlaybacksStore = new VoiceBroadcastPlaybacksStore(stores.voiceBroadcastRecordingsStore); roomViewStore = new RoomViewStore(dis, stores); stores._RoomViewStore = roomViewStore; }); @@ -343,88 +335,6 @@ describe("RoomViewStore", function () { }); }); - describe("when listening to a voice broadcast", () => { - let voiceBroadcastPlayback: VoiceBroadcastPlayback; - - beforeEach(() => { - voiceBroadcastPlayback = new VoiceBroadcastPlayback( - mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Started, - mockClient.getSafeUserId(), - "d42", - ), - mockClient, - stores.voiceBroadcastRecordingsStore, - ); - stores.voiceBroadcastPlaybacksStore.setCurrent(voiceBroadcastPlayback); - jest.spyOn(voiceBroadcastPlayback, "pause").mockImplementation(); - }); - - it("and viewing a call it should pause the current broadcast", async () => { - await viewCall(); - expect(voiceBroadcastPlayback.pause).toHaveBeenCalled(); - expect(roomViewStore.isViewingCall()).toBe(true); - }); - }); - - describe("when recording a voice broadcast", () => { - beforeEach(() => { - stores.voiceBroadcastRecordingsStore.setCurrent( - new VoiceBroadcastRecording( - mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Started, - mockClient.getSafeUserId(), - "d42", - ), - mockClient, - ), - ); - }); - - it("and trying to view a call, it should not actually view it and show the info dialog", async () => { - await viewCall(); - expect(Modal.createDialog).toMatchSnapshot(); - expect(roomViewStore.isViewingCall()).toBe(false); - }); - - describe("and viewing a room with a broadcast", () => { - beforeEach(async () => { - const broadcastEvent = mkVoiceBroadcastInfoStateEvent( - roomId2, - VoiceBroadcastInfoState.Started, - mockClient.getSafeUserId(), - "ABC123", - ); - room2.addLiveEvents([broadcastEvent], { addToState: true }); - - stores.voiceBroadcastPlaybacksStore.getByInfoEvent(broadcastEvent, mockClient); - dis.dispatch({ action: Action.ViewRoom, room_id: roomId2 }); - await untilDispatch(Action.ActiveRoomChanged, dis); - }); - - it("should continue recording", () => { - expect(stores.voiceBroadcastPlaybacksStore.getCurrent()).toBeNull(); - expect(stores.voiceBroadcastRecordingsStore.getCurrent()?.getState()).toBe( - VoiceBroadcastInfoState.Started, - ); - }); - - describe("and stopping the recording", () => { - beforeEach(async () => { - await stores.voiceBroadcastRecordingsStore.getCurrent()?.stop(); - // check test precondition - expect(stores.voiceBroadcastRecordingsStore.getCurrent()).toBeNull(); - }); - - it("should view the broadcast", () => { - expect(stores.voiceBroadcastPlaybacksStore.getCurrent()?.infoEvent.getRoomId()).toBe(roomId2); - }); - }); - }); - }); - describe("Sliding Sync", function () { beforeEach(() => { jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName, roomId, value) => { diff --git a/test/unit-tests/stores/__snapshots__/RoomViewStore-test.ts.snap b/test/unit-tests/stores/__snapshots__/RoomViewStore-test.ts.snap index a6b7953697..77ee50df5b 100644 --- a/test/unit-tests/stores/__snapshots__/RoomViewStore-test.ts.snap +++ b/test/unit-tests/stores/__snapshots__/RoomViewStore-test.ts.snap @@ -18,26 +18,3 @@ exports[`RoomViewStore should display the generic error message when the roomId "title": "Failed to join", } `; - -exports[`RoomViewStore when recording a voice broadcast and trying to view a call, it should not actually view it and show the info dialog 1`] = ` -[MockFunction] { - "calls": [ - [ - [Function], - { - "description":

- You can’t start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call. -

, - "hasCloseButton": true, - "title": "Can’t start a call", - }, - ], - ], - "results": [ - { - "type": "return", - "value": undefined, - }, - ], -} -`; diff --git a/test/unit-tests/stores/room-list/previews/MessageEventPreview-test.ts b/test/unit-tests/stores/room-list/previews/MessageEventPreview-test.ts index fd6626e6b6..ec97cdd20e 100644 --- a/test/unit-tests/stores/room-list/previews/MessageEventPreview-test.ts +++ b/test/unit-tests/stores/room-list/previews/MessageEventPreview-test.ts @@ -71,18 +71,5 @@ describe("MessageEventPreview", () => { }); 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/unit-tests/stores/room-list/previews/VoiceBroadcastPreview-test.ts b/test/unit-tests/stores/room-list/previews/VoiceBroadcastPreview-test.ts deleted file mode 100644 index a96f3c11bd..0000000000 --- a/test/unit-tests/stores/room-list/previews/VoiceBroadcastPreview-test.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { Room } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastPreview } from "../../../../../src/stores/room-list/previews/VoiceBroadcastPreview"; -import { VoiceBroadcastInfoState } from "../../../../../src/voice-broadcast"; -import { mkEvent, stubClient } 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" }), - new Room(roomId, stubClient(), userId), - ); - expect(preview.getTextFor(event)).toBeNull(); - }); -}); diff --git a/test/unit-tests/stores/widgets/StopGapWidget-test.ts b/test/unit-tests/stores/widgets/StopGapWidget-test.ts index 397c289d22..1416711017 100644 --- a/test/unit-tests/stores/widgets/StopGapWidget-test.ts +++ b/test/unit-tests/stores/widgets/StopGapWidget-test.ts @@ -22,9 +22,6 @@ import { waitFor } from "jest-matrix-react"; import { stubClient, mkRoom, mkEvent } from "../../../test-utils"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import { StopGapWidget } from "../../../../src/stores/widgets/StopGapWidget"; -import { ElementWidgetActions } from "../../../../src/stores/widgets/ElementWidgetActions"; -import { VoiceBroadcastInfoEventType, VoiceBroadcastRecording } from "../../../../src/voice-broadcast"; -import { SdkContextClass } from "../../../../src/contexts/SDKContext"; import ActiveWidgetStore from "../../../../src/stores/ActiveWidgetStore"; import SettingsStore from "../../../../src/settings/SettingsStore"; @@ -225,41 +222,6 @@ describe("StopGapWidget", () => { expect(messaging.feedEvent).toHaveBeenLastCalledWith(event.getEffectiveEvent(), "!1:example.org"); }); }); - - describe("when there is a voice broadcast recording", () => { - let voiceBroadcastInfoEvent: MatrixEvent; - let voiceBroadcastRecording: VoiceBroadcastRecording; - - beforeEach(() => { - voiceBroadcastInfoEvent = mkEvent({ - event: true, - room: client.getRoom("x")?.roomId, - user: client.getUserId()!, - type: VoiceBroadcastInfoEventType, - content: {}, - }); - voiceBroadcastRecording = new VoiceBroadcastRecording(voiceBroadcastInfoEvent, client); - jest.spyOn(voiceBroadcastRecording, "pause"); - jest.spyOn(SdkContextClass.instance.voiceBroadcastRecordingsStore, "getCurrent").mockReturnValue( - voiceBroadcastRecording, - ); - }); - - describe(`and receiving a action:${ElementWidgetActions.JoinCall} message`, () => { - beforeEach(async () => { - messaging.on.mock.calls.find(([event, listener]) => { - if (event === `action:${ElementWidgetActions.JoinCall}`) { - listener(); - return true; - } - }); - }); - - it("should pause the current voice broadcast recording", () => { - expect(voiceBroadcastRecording.pause).toHaveBeenCalled(); - }); - }); - }); }); describe("StopGapWidget with stickyPromise", () => { let client: MockedObject; diff --git a/test/unit-tests/utils/EventRenderingUtils-test.ts b/test/unit-tests/utils/EventRenderingUtils-test.ts deleted file mode 100644 index 8a3cfded4a..0000000000 --- a/test/unit-tests/utils/EventRenderingUtils-test.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { getEventDisplayInfo } from "../../../src/utils/EventRenderingUtils"; -import { VoiceBroadcastInfoState } from "../../../src/voice-broadcast"; -import { mkVoiceBroadcastInfoStateEvent } from "../voice-broadcast/utils/test-utils"; -import { createTestClient } from "../../test-utils"; - -describe("getEventDisplayInfo", () => { - const mkBroadcastInfoEvent = (state: VoiceBroadcastInfoState) => { - return mkVoiceBroadcastInfoStateEvent("!room:example.com", state, "@user:example.com", "ASD123"); - }; - - it("should return the expected value for a broadcast started event", () => { - expect(getEventDisplayInfo(createTestClient(), mkBroadcastInfoEvent(VoiceBroadcastInfoState.Started), false)) - .toMatchInlineSnapshot(` - { - "hasRenderer": true, - "isBubbleMessage": false, - "isInfoMessage": false, - "isLeftAlignedBubbleMessage": false, - "isSeeingThroughMessageHiddenForModeration": false, - "noBubbleEvent": true, - } - `); - }); - - it("should return the expected value for a broadcast stopped event", () => { - expect(getEventDisplayInfo(createTestClient(), mkBroadcastInfoEvent(VoiceBroadcastInfoState.Stopped), false)) - .toMatchInlineSnapshot(` - { - "hasRenderer": true, - "isBubbleMessage": false, - "isInfoMessage": true, - "isLeftAlignedBubbleMessage": false, - "isSeeingThroughMessageHiddenForModeration": false, - "noBubbleEvent": true, - } - `); - }); -}); diff --git a/test/unit-tests/utils/EventUtils-test.ts b/test/unit-tests/utils/EventUtils-test.ts index 2fc13b507c..c7828c3a2f 100644 --- a/test/unit-tests/utils/EventUtils-test.ts +++ b/test/unit-tests/utils/EventUtils-test.ts @@ -35,8 +35,6 @@ 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"); @@ -148,20 +146,6 @@ 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([ @@ -172,7 +156,6 @@ 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); }); @@ -183,7 +166,6 @@ 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/unit-tests/voice-broadcast/audio/VoiceBroadcastRecorder-test.ts b/test/unit-tests/voice-broadcast/audio/VoiceBroadcastRecorder-test.ts deleted file mode 100644 index 5718fc118c..0000000000 --- a/test/unit-tests/voice-broadcast/audio/VoiceBroadcastRecorder-test.ts +++ /dev/null @@ -1,251 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { mocked } from "jest-mock"; -import { Optional } from "matrix-events-sdk"; - -import { VoiceRecording } from "../../../../src/audio/VoiceRecording"; -import SdkConfig from "../../../../src/SdkConfig"; -import { concat } from "../../../../src/utils/arrays"; -import { - ChunkRecordedPayload, - createVoiceBroadcastRecorder, - VoiceBroadcastRecorder, - VoiceBroadcastRecorderEvent, -} from "../../../../src/voice-broadcast"; - -// mock VoiceRecording because it contains all the audio APIs -jest.mock("../../../../src/audio/VoiceRecording", () => ({ - VoiceRecording: jest.fn().mockReturnValue({ - disableMaxLength: jest.fn(), - emit: jest.fn(), - liveData: { - onUpdate: jest.fn(), - }, - start: jest.fn(), - stop: jest.fn(), - destroy: jest.fn(), - }), -})); - -jest.mock("../../../../src/settings/SettingsStore"); - -describe("VoiceBroadcastRecorder", () => { - describe("createVoiceBroadcastRecorder", () => { - beforeEach(() => { - jest.spyOn(SdkConfig, "get").mockImplementation((key: string) => { - if (key === "voice_broadcast") { - return { - chunk_length: 1337, - }; - } - }); - }); - - afterEach(() => { - mocked(SdkConfig.get).mockRestore(); - }); - - it("should return a VoiceBroadcastRecorder instance with targetChunkLength from config", () => { - const voiceBroadcastRecorder = createVoiceBroadcastRecorder(); - expect(voiceBroadcastRecorder).toBeInstanceOf(VoiceBroadcastRecorder); - expect(voiceBroadcastRecorder.targetChunkLength).toBe(1337); - }); - }); - - describe("instance", () => { - const chunkLength = 30; - // 0... OpusHead - const headers1 = new Uint8Array([...Array(28).fill(0), 79, 112, 117, 115, 72, 101, 97, 100]); - // 0... OpusTags - const headers2 = new Uint8Array([...Array(28).fill(0), 79, 112, 117, 115, 84, 97, 103, 115]); - const chunk1 = new Uint8Array([5, 6]); - const chunk2a = new Uint8Array([7, 8]); - const chunk2b = new Uint8Array([9, 10]); - const contentType = "test content type"; - - let voiceRecording: VoiceRecording; - let voiceBroadcastRecorder: VoiceBroadcastRecorder; - let onChunkRecorded: (chunk: ChunkRecordedPayload) => void; - - const simulateFirstChunk = (): void => { - // send headers in wrong order and multiple times to test robustness for that - voiceRecording.onDataAvailable!(headers2); - voiceRecording.onDataAvailable!(headers1); - voiceRecording.onDataAvailable!(headers1); - voiceRecording.onDataAvailable!(headers2); - // set recorder seconds to something greater than the test chunk length of 30 - // @ts-ignore - voiceRecording.recorderSeconds = 42; - voiceRecording.onDataAvailable!(chunk1); - voiceRecording.onDataAvailable!(headers1); - }; - - const expectOnFirstChunkRecorded = (): void => { - expect(onChunkRecorded).toHaveBeenNthCalledWith(1, { - buffer: concat(headers1, headers2, chunk1), - length: 42, - }); - }; - - const itShouldNotEmitAChunkRecordedEvent = (): void => { - it("should not emit a ChunkRecorded event", (): void => { - expect(voiceRecording.emit).not.toHaveBeenCalledWith( - VoiceBroadcastRecorderEvent.ChunkRecorded, - expect.anything(), - ); - }); - }; - - beforeEach(() => { - voiceRecording = new VoiceRecording(); - // @ts-ignore - voiceRecording.recorderSeconds = 23; - // @ts-ignore - voiceRecording.contentType = contentType; - - voiceBroadcastRecorder = new VoiceBroadcastRecorder(voiceRecording, chunkLength); - jest.spyOn(voiceBroadcastRecorder, "removeAllListeners"); - onChunkRecorded = jest.fn(); - voiceBroadcastRecorder.on(VoiceBroadcastRecorderEvent.ChunkRecorded, onChunkRecorded); - }); - - afterEach(() => { - voiceBroadcastRecorder.destroy(); - }); - - it("start should forward the call to VoiceRecording.start", async () => { - await voiceBroadcastRecorder.start(); - expect(voiceRecording.start).toHaveBeenCalled(); - }); - - describe("stop", () => { - beforeEach(async () => { - await voiceBroadcastRecorder.stop(); - }); - - it("should forward the call to VoiceRecording.stop", async () => { - expect(voiceRecording.stop).toHaveBeenCalled(); - }); - - itShouldNotEmitAChunkRecordedEvent(); - }); - - describe("when calling destroy", () => { - beforeEach(() => { - voiceBroadcastRecorder.destroy(); - }); - - it("should call VoiceRecording.destroy", () => { - expect(voiceRecording.destroy).toHaveBeenCalled(); - }); - - it("should remove all listeners", () => { - expect(voiceBroadcastRecorder.removeAllListeners).toHaveBeenCalled(); - }); - }); - - it("contentType should return the value from VoiceRecording", () => { - expect(voiceBroadcastRecorder.contentType).toBe(contentType); - }); - - describe("when the first header from recorder has been received", () => { - beforeEach(() => { - voiceRecording.onDataAvailable!(headers1); - }); - - itShouldNotEmitAChunkRecordedEvent(); - }); - - describe("when the second header from recorder has been received", () => { - beforeEach(() => { - voiceRecording.onDataAvailable!(headers1); - voiceRecording.onDataAvailable!(headers2); - }); - - itShouldNotEmitAChunkRecordedEvent(); - }); - - describe("when a third page from recorder has been received", () => { - beforeEach(() => { - voiceRecording.onDataAvailable!(headers1); - voiceRecording.onDataAvailable!(headers2); - voiceRecording.onDataAvailable!(chunk1); - }); - - itShouldNotEmitAChunkRecordedEvent(); - - describe("and calling stop", () => { - let stopPayload: Optional; - - beforeEach(async () => { - stopPayload = await voiceBroadcastRecorder.stop(); - }); - - it("should return the remaining chunk", () => { - expect(stopPayload).toEqual({ - buffer: concat(headers1, headers2, chunk1), - length: 23, - }); - }); - - describe("and calling start again and receiving some data", () => { - beforeEach(() => { - simulateFirstChunk(); - }); - - it("should emit the ChunkRecorded event for the first chunk", () => { - expectOnFirstChunkRecorded(); - }); - }); - }); - - describe("and calling stop() with recording.stop error)", () => { - let stopPayload: Optional; - - beforeEach(async () => { - mocked(voiceRecording.stop).mockRejectedValue("Error"); - stopPayload = await voiceBroadcastRecorder.stop(); - }); - - it("should return the remaining chunk", () => { - expect(stopPayload).toEqual({ - buffer: concat(headers1, headers2, chunk1), - length: 23, - }); - }); - }); - }); - - describe("when some chunks have been received", () => { - beforeEach(() => { - simulateFirstChunk(); - - // simulate a second chunk - voiceRecording.onDataAvailable!(chunk2a); - - // send headers again to test robustness for that - voiceRecording.onDataAvailable!(headers2); - - // add another 30 seconds for the next chunk - // @ts-ignore - voiceRecording.recorderSeconds = 72; - voiceRecording.onDataAvailable!(chunk2b); - }); - - it("should emit ChunkRecorded events", () => { - expectOnFirstChunkRecorded(); - - expect(onChunkRecorded).toHaveBeenNthCalledWith(2, { - buffer: concat(headers1, headers2, chunk2a, chunk2b), - length: 72 - 42, // 72 (position at second chunk) - 42 (position of first chunk) - }); - }); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/components/VoiceBroadcastBody-test.tsx b/test/unit-tests/voice-broadcast/components/VoiceBroadcastBody-test.tsx deleted file mode 100644 index fd416de8ce..0000000000 --- a/test/unit-tests/voice-broadcast/components/VoiceBroadcastBody-test.tsx +++ /dev/null @@ -1,171 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React, { ReactElement } from "react"; -import { act, render, screen } from "jest-matrix-react"; -import { mocked } from "jest-mock"; -import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; - -import { - VoiceBroadcastBody as UnwrappedVoiceBroadcastBody, - VoiceBroadcastInfoState, - VoiceBroadcastRecordingBody, - VoiceBroadcastRecording, - VoiceBroadcastPlaybackBody, - VoiceBroadcastPlayback, - VoiceBroadcastRecordingsStore, -} from "../../../../src/voice-broadcast"; -import { withClientContextRenderOptions, stubClient, wrapInSdkContext } from "../../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils"; -import { MediaEventHelper } from "../../../../src/utils/MediaEventHelper"; -import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks"; -import { SdkContextClass } from "../../../../src/contexts/SDKContext"; - -jest.mock("../../../../src/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody", () => ({ - VoiceBroadcastRecordingBody: jest.fn(), -})); - -jest.mock("../../../../src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody", () => ({ - VoiceBroadcastPlaybackBody: jest.fn(), -})); - -jest.mock("../../../../src/utils/permalinks/Permalinks"); -jest.mock("../../../../src/utils/MediaEventHelper"); -jest.mock("../../../../src/stores/WidgetStore"); -jest.mock("../../../../src/stores/widgets/WidgetLayoutStore"); - -describe("VoiceBroadcastBody", () => { - const roomId = "!room:example.com"; - let userId: string; - let deviceId: string; - let client: MatrixClient; - let room: Room; - let infoEvent: MatrixEvent; - let stoppedEvent: MatrixEvent; - let testRecording: VoiceBroadcastRecording; - let testPlayback: VoiceBroadcastPlayback; - - const renderVoiceBroadcast = () => { - const VoiceBroadcastBody = wrapInSdkContext(UnwrappedVoiceBroadcastBody, SdkContextClass.instance); - render( - {}} - onMessageAllowed={() => {}} - permalinkCreator={new RoomPermalinkCreator(room)} - />, - withClientContextRenderOptions(client), - ); - testRecording = SdkContextClass.instance.voiceBroadcastRecordingsStore.getByInfoEvent(infoEvent, client); - }; - - beforeEach(() => { - client = stubClient(); - userId = client.getUserId() || ""; - deviceId = client.getDeviceId() || ""; - mocked(client.relations).mockClear(); - mocked(client.relations).mockResolvedValue({ events: [] }); - room = new Room(roomId, client, userId); - mocked(client.getRoom).mockImplementation((getRoomId?: string) => { - if (getRoomId === roomId) return room; - return null; - }); - - infoEvent = mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Started, userId, deviceId); - stoppedEvent = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Stopped, - userId, - deviceId, - infoEvent, - ); - room.addEventsToTimeline([infoEvent], true, true, room.getLiveTimeline()); - testRecording = new VoiceBroadcastRecording(infoEvent, client); - testPlayback = new VoiceBroadcastPlayback(infoEvent, client, new VoiceBroadcastRecordingsStore()); - mocked(VoiceBroadcastRecordingBody).mockImplementation(({ recording }): ReactElement | null => { - if (testRecording === recording) { - return
; - } - - return null; - }); - - mocked(VoiceBroadcastPlaybackBody).mockImplementation(({ playback }): ReactElement | null => { - if (testPlayback === playback) { - return
; - } - - return null; - }); - - jest.spyOn(SdkContextClass.instance.voiceBroadcastRecordingsStore, "getByInfoEvent").mockImplementation( - (getEvent: MatrixEvent, getClient: MatrixClient): VoiceBroadcastRecording => { - if (getEvent === infoEvent && getClient === client) { - return testRecording; - } - - throw new Error("unexpected event"); - }, - ); - - jest.spyOn(SdkContextClass.instance.voiceBroadcastPlaybacksStore, "getByInfoEvent").mockImplementation( - (getEvent: MatrixEvent): VoiceBroadcastPlayback => { - if (getEvent === infoEvent) { - return testPlayback; - } - - throw new Error("unexpected event"); - }, - ); - }); - - describe("when there is a stopped voice broadcast", () => { - beforeEach(() => { - room.addEventsToTimeline([stoppedEvent], true, true, room.getLiveTimeline()); - renderVoiceBroadcast(); - }); - - it("should render a voice broadcast playback body", () => { - screen.getByTestId("voice-broadcast-playback-body"); - }); - }); - - describe("when there is a started voice broadcast from the current user", () => { - beforeEach(() => { - renderVoiceBroadcast(); - }); - - it("should render a voice broadcast recording body", () => { - screen.getByTestId("voice-broadcast-recording-body"); - }); - - describe("and the recordings ends", () => { - beforeEach(() => { - act(() => { - room.addEventsToTimeline([stoppedEvent], true, true, room.getLiveTimeline()); - }); - }); - - it("should render a voice broadcast playback body", () => { - screen.getByTestId("voice-broadcast-playback-body"); - }); - }); - }); - - describe("when displaying a voice broadcast playback", () => { - beforeEach(() => { - mocked(client).getUserId.mockReturnValue("@other:example.com"); - renderVoiceBroadcast(); - }); - - it("should render a voice broadcast playback body", () => { - screen.getByTestId("voice-broadcast-playback-body"); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/components/atoms/LiveBadge-test.tsx b/test/unit-tests/voice-broadcast/components/atoms/LiveBadge-test.tsx deleted file mode 100644 index 2d630fca9d..0000000000 --- a/test/unit-tests/voice-broadcast/components/atoms/LiveBadge-test.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { render } from "jest-matrix-react"; - -import { LiveBadge } from "../../../../../src/voice-broadcast"; - -describe("LiveBadge", () => { - it("should render as expected with default props", () => { - const { container } = render(); - expect(container).toMatchSnapshot(); - }); - - it("should render in grey as expected", () => { - const { container } = render(); - expect(container).toMatchSnapshot(); - }); -}); diff --git a/test/unit-tests/voice-broadcast/components/atoms/VoiceBroadcastControl-test.tsx b/test/unit-tests/voice-broadcast/components/atoms/VoiceBroadcastControl-test.tsx deleted file mode 100644 index 8c52062b5e..0000000000 --- a/test/unit-tests/voice-broadcast/components/atoms/VoiceBroadcastControl-test.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { render, RenderResult, screen } from "jest-matrix-react"; -import userEvent from "@testing-library/user-event"; - -import { VoiceBroadcastControl } from "../../../../../src/voice-broadcast"; -import { Icon as StopIcon } from "../../../../res/img/compound/stop-16.svg"; - -describe("VoiceBroadcastControl", () => { - let result: RenderResult; - let onClick: () => void; - - beforeEach(() => { - onClick = jest.fn(); - }); - - describe("when rendering it", () => { - beforeEach(() => { - const stopIcon = ; - result = render(); - }); - - it("should render as expected", () => { - expect(result.container).toMatchSnapshot(); - }); - - describe("when clicking it", () => { - beforeEach(async () => { - await userEvent.click(screen.getByLabelText("test label")); - }); - - it("should call onClick", () => { - expect(onClick).toHaveBeenCalled(); - }); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/components/atoms/VoiceBroadcastHeader-test.tsx b/test/unit-tests/voice-broadcast/components/atoms/VoiceBroadcastHeader-test.tsx deleted file mode 100644 index 6a0e7190d8..0000000000 --- a/test/unit-tests/voice-broadcast/components/atoms/VoiceBroadcastHeader-test.tsx +++ /dev/null @@ -1,89 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix"; -import { render, RenderResult } from "jest-matrix-react"; - -import { VoiceBroadcastHeader, VoiceBroadcastLiveness } from "../../../../../src/voice-broadcast"; -import { mkRoom, stubClient } from "../../../../test-utils"; - -// mock RoomAvatar, because it is doing too much fancy stuff -jest.mock("../../../../../src/components/views/avatars/RoomAvatar", () => ({ - __esModule: true, - default: jest.fn().mockImplementation(({ room }) => { - return
room avatar: {room.name}
; - }), -})); - -describe("VoiceBroadcastHeader", () => { - const userId = "@user:example.com"; - const roomId = "!room:example.com"; - let client: MatrixClient; - let room: Room; - const sender = new RoomMember(roomId, userId); - let container: RenderResult["container"]; - - const renderHeader = (live: VoiceBroadcastLiveness, showBroadcast?: boolean, buffering?: boolean): RenderResult => { - return render( - , - ); - }; - - beforeAll(() => { - client = stubClient(); - room = mkRoom(client, roomId); - sender.name = "test user"; - }); - - describe("when rendering a live broadcast header with broadcast info", () => { - beforeEach(() => { - container = renderHeader("live", true, true).container; - }); - - it("should render the header with a red live badge", () => { - expect(container).toMatchSnapshot(); - }); - }); - - describe("when rendering a buffering live broadcast header with broadcast info", () => { - beforeEach(() => { - container = renderHeader("live", true).container; - }); - - it("should render the header with a red live badge", () => { - expect(container).toMatchSnapshot(); - }); - }); - - describe("when rendering a live (grey) broadcast header with broadcast info", () => { - beforeEach(() => { - container = renderHeader("grey", true).container; - }); - - it("should render the header with a grey live badge", () => { - expect(container).toMatchSnapshot(); - }); - }); - - describe("when rendering a non-live broadcast header", () => { - beforeEach(() => { - container = renderHeader("not-live").container; - }); - - it("should render the header without a live badge", () => { - expect(container).toMatchSnapshot(); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/components/atoms/VoiceBroadcastPlaybackControl-test.tsx b/test/unit-tests/voice-broadcast/components/atoms/VoiceBroadcastPlaybackControl-test.tsx deleted file mode 100644 index 9eef863eb4..0000000000 --- a/test/unit-tests/voice-broadcast/components/atoms/VoiceBroadcastPlaybackControl-test.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022, 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { render, RenderResult, screen } from "jest-matrix-react"; -import userEvent from "@testing-library/user-event"; - -import { VoiceBroadcastPlaybackControl, VoiceBroadcastPlaybackState } from "../../../../../src/voice-broadcast"; - -describe("", () => { - const renderControl = (state: VoiceBroadcastPlaybackState): { result: RenderResult; onClick: () => void } => { - const onClick = jest.fn(); - return { - onClick, - result: render(), - }; - }; - - it.each([ - VoiceBroadcastPlaybackState.Stopped, - VoiceBroadcastPlaybackState.Paused, - VoiceBroadcastPlaybackState.Buffering, - VoiceBroadcastPlaybackState.Playing, - ])("should render state %s as expected", (state: VoiceBroadcastPlaybackState) => { - expect(renderControl(state).result.container).toMatchSnapshot(); - }); - - it("should not render for error state", () => { - expect(renderControl(VoiceBroadcastPlaybackState.Error).result.asFragment()).toMatchInlineSnapshot( - ``, - ); - }); - - describe("when clicking the control", () => { - let onClick: () => void; - - beforeEach(async () => { - onClick = renderControl(VoiceBroadcastPlaybackState.Playing).onClick; - await userEvent.click(screen.getByLabelText("pause voice broadcast")); - }); - - it("should invoke the onClick callback", () => { - expect(onClick).toHaveBeenCalled(); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/components/atoms/__snapshots__/LiveBadge-test.tsx.snap b/test/unit-tests/voice-broadcast/components/atoms/__snapshots__/LiveBadge-test.tsx.snap deleted file mode 100644 index bd4b8d2bcc..0000000000 --- a/test/unit-tests/voice-broadcast/components/atoms/__snapshots__/LiveBadge-test.tsx.snap +++ /dev/null @@ -1,27 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LiveBadge should render as expected with default props 1`] = ` -
-
-
- Live -
-
-`; - -exports[`LiveBadge should render in grey as expected 1`] = ` -
-
-
- Live -
-
-`; diff --git a/test/unit-tests/voice-broadcast/components/atoms/__snapshots__/VoiceBroadcastControl-test.tsx.snap b/test/unit-tests/voice-broadcast/components/atoms/__snapshots__/VoiceBroadcastControl-test.tsx.snap deleted file mode 100644 index a28e105b4c..0000000000 --- a/test/unit-tests/voice-broadcast/components/atoms/__snapshots__/VoiceBroadcastControl-test.tsx.snap +++ /dev/null @@ -1,16 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`VoiceBroadcastControl when rendering it should render as expected 1`] = ` -
-
-
-
-
-`; diff --git a/test/unit-tests/voice-broadcast/components/atoms/__snapshots__/VoiceBroadcastHeader-test.tsx.snap b/test/unit-tests/voice-broadcast/components/atoms/__snapshots__/VoiceBroadcastHeader-test.tsx.snap deleted file mode 100644 index 0c1c966f73..0000000000 --- a/test/unit-tests/voice-broadcast/components/atoms/__snapshots__/VoiceBroadcastHeader-test.tsx.snap +++ /dev/null @@ -1,277 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`VoiceBroadcastHeader when rendering a buffering live broadcast header with broadcast info should render the header with a red live badge 1`] = ` -
-
-
- room avatar: - !room:example.com -
-
-
-
- !room:example.com -
-
-
- - - - - - test user - -
-
-
- Voice broadcast -
-
-
-
- Live -
-
-
-`; - -exports[`VoiceBroadcastHeader when rendering a live (grey) broadcast header with broadcast info should render the header with a grey live badge 1`] = ` -
-
-
- room avatar: - !room:example.com -
-
-
-
- !room:example.com -
-
-
- - - - - - test user - -
-
-
- Voice broadcast -
-
-
-
- Live -
-
-
-`; - -exports[`VoiceBroadcastHeader when rendering a live broadcast header with broadcast info should render the header with a red live badge 1`] = ` -
-
-
- room avatar: - !room:example.com -
-
-
-
- !room:example.com -
-
-
- - - - - - test user - -
-
-
- Voice broadcast -
-
-
-
-
- Buffering… -
-
-
-
- Live -
-
-
-`; - -exports[`VoiceBroadcastHeader when rendering a non-live broadcast header should render the header without a live badge 1`] = ` -
-
-
- room avatar: - !room:example.com -
-
-
-
- !room:example.com -
-
-
- - - - - - test user - -
-
-
-
-`; diff --git a/test/unit-tests/voice-broadcast/components/atoms/__snapshots__/VoiceBroadcastPlaybackControl-test.tsx.snap b/test/unit-tests/voice-broadcast/components/atoms/__snapshots__/VoiceBroadcastPlaybackControl-test.tsx.snap deleted file mode 100644 index 1ce23ca8b5..0000000000 --- a/test/unit-tests/voice-broadcast/components/atoms/__snapshots__/VoiceBroadcastPlaybackControl-test.tsx.snap +++ /dev/null @@ -1,97 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` should render state buffering as expected 1`] = ` -
-
- - - -
-
-`; - -exports[` should render state pause as expected 1`] = ` -
-
- - - -
-
-`; - -exports[` should render state playing as expected 1`] = ` -
-
- - - -
-
-`; - -exports[` should render state stopped as expected 1`] = ` -
-
- - - -
-
-`; diff --git a/test/unit-tests/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody-test.tsx b/test/unit-tests/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody-test.tsx deleted file mode 100644 index a6dd10e5c1..0000000000 --- a/test/unit-tests/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody-test.tsx +++ /dev/null @@ -1,249 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022, 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; -import { act, render, RenderResult, screen } from "jest-matrix-react"; -import userEvent from "@testing-library/user-event"; -import { mocked } from "jest-mock"; - -import { - VoiceBroadcastInfoState, - VoiceBroadcastLiveness, - VoiceBroadcastPlayback, - VoiceBroadcastPlaybackBody, - VoiceBroadcastPlaybackEvent, - VoiceBroadcastPlaybackState, -} from "../../../../../src/voice-broadcast"; -import { filterConsole, stubClient } from "../../../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "../../utils/test-utils"; -import dis from "../../../../../src/dispatcher/dispatcher"; -import { Action } from "../../../../../src/dispatcher/actions"; -import { SdkContextClass } from "../../../../../src/contexts/SDKContext"; - -jest.mock("../../../../../src/dispatcher/dispatcher"); - -// mock RoomAvatar, because it is doing too much fancy stuff -jest.mock("../../../../../src/components/views/avatars/RoomAvatar", () => ({ - __esModule: true, - default: jest.fn().mockImplementation(({ room }) => { - return
room avatar: {room.name}
; - }), -})); - -describe("VoiceBroadcastPlaybackBody", () => { - const userId = "@user:example.com"; - const roomId = "!room:example.com"; - const duration = 23 * 60 + 42; // 23:42 - let client: MatrixClient; - let infoEvent: MatrixEvent; - let playback: VoiceBroadcastPlayback; - let renderResult: RenderResult; - - filterConsole( - // expected for some tests - "voice broadcast chunk event to skip to not found", - ); - - beforeAll(() => { - client = stubClient(); - mocked(client.relations).mockClear(); - mocked(client.relations).mockResolvedValue({ events: [] }); - - infoEvent = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Stopped, - userId, - client.getDeviceId(), - ); - }); - - beforeEach(() => { - playback = new VoiceBroadcastPlayback( - infoEvent, - client, - SdkContextClass.instance.voiceBroadcastRecordingsStore, - ); - jest.spyOn(playback, "toggle").mockImplementation(() => Promise.resolve()); - jest.spyOn(playback, "getLiveness"); - jest.spyOn(playback, "getState"); - jest.spyOn(playback, "skipTo"); - jest.spyOn(playback, "durationSeconds", "get").mockReturnValue(duration); - }); - - describe("when rendering a buffering voice broadcast", () => { - beforeEach(() => { - mocked(playback.getState).mockReturnValue(VoiceBroadcastPlaybackState.Buffering); - mocked(playback.getLiveness).mockReturnValue("live"); - renderResult = render(); - }); - - it("should render as expected", () => { - expect(renderResult.container).toMatchSnapshot(); - }); - }); - - describe("when rendering a playing broadcast", () => { - beforeEach(() => { - mocked(playback.getState).mockReturnValue(VoiceBroadcastPlaybackState.Playing); - mocked(playback.getLiveness).mockReturnValue("not-live"); - renderResult = render(); - }); - - it("should render as expected", () => { - expect(renderResult.container).toMatchSnapshot(); - }); - - describe("and being in the middle of the playback", () => { - beforeEach(() => { - act(() => { - playback.emit(VoiceBroadcastPlaybackEvent.TimesChanged, { - duration, - position: 10 * 60, - timeLeft: duration - 10 * 60, - }); - }); - }); - - describe("and clicking 30s backward", () => { - beforeEach(async () => { - await act(async () => { - await userEvent.click(screen.getByLabelText("30s backward")); - }); - }); - - it("should seek 30s backward", () => { - expect(playback.skipTo).toHaveBeenCalledWith(9 * 60 + 30); - }); - }); - - describe("and clicking 30s forward", () => { - beforeEach(async () => { - await act(async () => { - await userEvent.click(screen.getByLabelText("30s forward")); - }); - }); - - it("should seek 30s forward", () => { - expect(playback.skipTo).toHaveBeenCalledWith(10 * 60 + 30); - }); - }); - }); - - describe("and clicking the room name", () => { - beforeEach(async () => { - await userEvent.click(screen.getByText("My room")); - }); - - it("should not view the room", () => { - expect(dis.dispatch).not.toHaveBeenCalled(); - }); - }); - }); - - describe("when rendering a playing broadcast in pip mode", () => { - beforeEach(() => { - mocked(playback.getState).mockReturnValue(VoiceBroadcastPlaybackState.Playing); - mocked(playback.getLiveness).mockReturnValue("not-live"); - renderResult = render(); - }); - - it("should render as expected", () => { - expect(renderResult.container).toMatchSnapshot(); - }); - - describe("and clicking the room name", () => { - beforeEach(async () => { - await userEvent.click(screen.getByText("My room")); - }); - - it("should view the room", () => { - expect(dis.dispatch).toHaveBeenCalledWith({ - action: Action.ViewRoom, - room_id: roomId, - metricsTrigger: undefined, - }); - }); - }); - }); - - describe(`when rendering a stopped broadcast`, () => { - beforeEach(() => { - mocked(playback.getState).mockReturnValue(VoiceBroadcastPlaybackState.Stopped); - mocked(playback.getLiveness).mockReturnValue("not-live"); - renderResult = render(); - }); - - it("should render as expected", () => { - expect(renderResult.container).toMatchSnapshot(); - }); - - describe("and clicking the play button", () => { - beforeEach(async () => { - await userEvent.click(renderResult.getByLabelText("play voice broadcast")); - }); - - it("should toggle the recording", () => { - expect(playback.toggle).toHaveBeenCalled(); - }); - }); - - describe("and the times update", () => { - beforeEach(() => { - act(() => { - playback.emit(VoiceBroadcastPlaybackEvent.TimesChanged, { - duration, - position: 5 * 60 + 13, - timeLeft: 7 * 60 + 5, - }); - }); - }); - - it("should render the times", async () => { - expect(await screen.findByText("05:13")).toBeInTheDocument(); - expect(await screen.findByText("-07:05")).toBeInTheDocument(); - }); - }); - }); - - describe("when rendering an error broadcast", () => { - beforeEach(() => { - mocked(playback.getState).mockReturnValue(VoiceBroadcastPlaybackState.Error); - renderResult = render(); - }); - - it("should render as expected", () => { - expect(renderResult.container).toMatchSnapshot(); - }); - }); - - describe.each([ - [VoiceBroadcastPlaybackState.Paused, "not-live"], - [VoiceBroadcastPlaybackState.Playing, "live"], - ] satisfies [VoiceBroadcastPlaybackState, VoiceBroadcastLiveness][])( - "when rendering a %s/%s broadcast", - (state: VoiceBroadcastPlaybackState, liveness: VoiceBroadcastLiveness) => { - beforeEach(() => { - mocked(playback.getState).mockReturnValue(state); - mocked(playback.getLiveness).mockReturnValue(liveness); - renderResult = render(); - }); - - it("should render as expected", () => { - expect(renderResult.container).toMatchSnapshot(); - }); - }, - ); - - it("when there is a broadcast without sender, it should raise an error", () => { - infoEvent.sender = null; - expect(() => { - render(); - }).toThrow(`Voice Broadcast sender not found (event ${playback.infoEvent.getId()})`); - }); -}); diff --git a/test/unit-tests/voice-broadcast/components/molecules/VoiceBroadcastPreRecordingPip-test.tsx b/test/unit-tests/voice-broadcast/components/molecules/VoiceBroadcastPreRecordingPip-test.tsx deleted file mode 100644 index c21cc8d7e4..0000000000 --- a/test/unit-tests/voice-broadcast/components/molecules/VoiceBroadcastPreRecordingPip-test.tsx +++ /dev/null @@ -1,163 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { mocked } from "jest-mock"; -import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix"; -import { act, render, RenderResult, screen } from "jest-matrix-react"; -import userEvent from "@testing-library/user-event"; - -import { - VoiceBroadcastPlaybacksStore, - VoiceBroadcastPreRecording, - VoiceBroadcastPreRecordingPip, - VoiceBroadcastRecordingsStore, -} from "../../../../../src/voice-broadcast"; -import { flushPromises, stubClient } from "../../../../test-utils"; -import { requestMediaPermissions } from "../../../../../src/utils/media/requestMediaPermissions"; -import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../../../src/MediaDeviceHandler"; -import dis from "../../../../../src/dispatcher/dispatcher"; -import { Action } from "../../../../../src/dispatcher/actions"; - -jest.mock("../../../../../src/dispatcher/dispatcher"); -jest.mock("../../../../../src/utils/media/requestMediaPermissions"); - -// mock RoomAvatar, because it is doing too much fancy stuff -jest.mock("../../../../../src/components/views/avatars/RoomAvatar", () => ({ - __esModule: true, - default: jest.fn().mockImplementation(({ room }) => { - return
room avatar: {room.name}
; - }), -})); - -describe("VoiceBroadcastPreRecordingPip", () => { - let renderResult: RenderResult; - let preRecording: VoiceBroadcastPreRecording; - let playbacksStore: VoiceBroadcastPlaybacksStore; - let recordingsStore: VoiceBroadcastRecordingsStore; - let client: MatrixClient; - let room: Room; - let sender: RoomMember; - - const itShouldShowTheBroadcastRoom = () => { - it("should show the broadcast room", () => { - expect(dis.dispatch).toHaveBeenCalledWith({ - action: Action.ViewRoom, - room_id: room.roomId, - metricsTrigger: undefined, - }); - }); - }; - - beforeEach(() => { - client = stubClient(); - room = new Room("!room@example.com", client, client.getUserId() || ""); - sender = new RoomMember(room.roomId, client.getUserId() || ""); - recordingsStore = new VoiceBroadcastRecordingsStore(); - playbacksStore = new VoiceBroadcastPlaybacksStore(recordingsStore); - mocked(requestMediaPermissions).mockResolvedValue({ - getTracks: (): Array => [], - } as unknown as MediaStream); - jest.spyOn(MediaDeviceHandler, "getDevices").mockResolvedValue({ - [MediaDeviceKindEnum.AudioInput]: [ - { - deviceId: "d1", - label: "Device 1", - } as MediaDeviceInfo, - { - deviceId: "d2", - label: "Device 2", - } as MediaDeviceInfo, - ], - [MediaDeviceKindEnum.AudioOutput]: [], - [MediaDeviceKindEnum.VideoInput]: [], - }); - jest.spyOn(MediaDeviceHandler.instance, "setDevice").mockImplementation(); - preRecording = new VoiceBroadcastPreRecording(room, sender, client, playbacksStore, recordingsStore); - jest.spyOn(preRecording, "start").mockResolvedValue(); - }); - - afterAll(() => { - jest.resetAllMocks(); - }); - - describe("when rendered", () => { - beforeEach(async () => { - renderResult = render(); - - await flushPromises(); - }); - - it("should match the snapshot", () => { - expect(renderResult.container).toMatchSnapshot(); - }); - - describe("and double clicking »Go live«", () => { - beforeEach(async () => { - await userEvent.click(screen.getByText("Go live")); - await userEvent.click(screen.getByText("Go live")); - }); - - it("should call start once", () => { - expect(preRecording.start).toHaveBeenCalledTimes(1); - }); - }); - - describe("and clicking the room name", () => { - beforeEach(async () => { - await userEvent.click(screen.getByText(room.name)); - }); - - itShouldShowTheBroadcastRoom(); - }); - - describe("and clicking the room avatar", () => { - beforeEach(async () => { - await userEvent.click(screen.getByText(`room avatar: ${room.name}`)); - }); - - itShouldShowTheBroadcastRoom(); - }); - - describe("and clicking the device label", () => { - beforeEach(async () => { - await act(async () => { - await userEvent.click(screen.getByText("Default Device")); - }); - }); - - it("should display the device selection", () => { - expect(screen.queryAllByText("Default Device").length).toBe(2); - expect(screen.queryByText("Device 1")).toBeInTheDocument(); - expect(screen.queryByText("Device 2")).toBeInTheDocument(); - }); - - describe("and selecting a device", () => { - beforeEach(async () => { - await act(async () => { - await userEvent.click(screen.getByText("Device 1")); - }); - }); - - it("should set it as current device", () => { - expect(MediaDeviceHandler.instance.setDevice).toHaveBeenCalledWith( - "d1", - MediaDeviceKindEnum.AudioInput, - ); - }); - - it("should not show the device selection", () => { - expect(screen.queryByText("Default Device")).not.toBeInTheDocument(); - // expected to be one in the document, displayed in the pip directly - expect(screen.queryByText("Device 1")).toBeInTheDocument(); - expect(screen.queryByText("Device 2")).not.toBeInTheDocument(); - }); - }); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody-test.tsx b/test/unit-tests/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody-test.tsx deleted file mode 100644 index 6c4d035373..0000000000 --- a/test/unit-tests/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody-test.tsx +++ /dev/null @@ -1,79 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022, 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { render, RenderResult } from "jest-matrix-react"; -import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; - -import { - VoiceBroadcastInfoEventType, - VoiceBroadcastInfoState, - VoiceBroadcastRecording, - VoiceBroadcastRecordingBody, -} from "../../../../../src/voice-broadcast"; -import { mkEvent, stubClient } from "../../../../test-utils"; - -// mock RoomAvatar, because it is doing too much fancy stuff -jest.mock("../../../../../src/components/views/avatars/RoomAvatar", () => ({ - __esModule: true, - default: jest.fn().mockImplementation(({ room }) => { - return
room avatar: {room.name}
; - }), -})); - -describe("VoiceBroadcastRecordingBody", () => { - const userId = "@user:example.com"; - const roomId = "!room:example.com"; - let client: MatrixClient; - let infoEvent: MatrixEvent; - let recording: VoiceBroadcastRecording; - - beforeAll(() => { - client = stubClient(); - infoEvent = mkEvent({ - event: true, - type: VoiceBroadcastInfoEventType, - content: {}, - room: roomId, - user: userId, - }); - recording = new VoiceBroadcastRecording(infoEvent, client, VoiceBroadcastInfoState.Resumed); - }); - - describe("when rendering a live broadcast", () => { - let renderResult: RenderResult; - - beforeEach(() => { - renderResult = render(); - }); - - it("should render with a red live badge", () => { - expect(renderResult.container).toMatchSnapshot(); - }); - }); - - describe("when rendering a paused broadcast", () => { - let renderResult: RenderResult; - - beforeEach(async () => { - await recording.pause(); - renderResult = render(); - }); - - it("should render with a grey live badge", () => { - expect(renderResult.container).toMatchSnapshot(); - }); - }); - - it("when there is a broadcast without sender, it should raise an error", () => { - infoEvent.sender = null; - expect(() => { - render(); - }).toThrow(`Voice Broadcast sender not found (event ${recording.infoEvent.getId()})`); - }); -}); diff --git a/test/unit-tests/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip-test.tsx b/test/unit-tests/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip-test.tsx deleted file mode 100644 index eafa0d0af6..0000000000 --- a/test/unit-tests/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip-test.tsx +++ /dev/null @@ -1,217 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ -// - -import React from "react"; -import { render, RenderResult, screen } from "jest-matrix-react"; -import userEvent from "@testing-library/user-event"; -import { ClientEvent, MatrixClient, MatrixEvent, SyncState } from "matrix-js-sdk/src/matrix"; -import { sleep } from "matrix-js-sdk/src/utils"; -import { mocked } from "jest-mock"; - -import { - VoiceBroadcastInfoState, - VoiceBroadcastRecording, - VoiceBroadcastRecordingPip, -} from "../../../../../src/voice-broadcast"; -import { flushPromises, stubClient } from "../../../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "../../utils/test-utils"; -import { requestMediaPermissions } from "../../../../../src/utils/media/requestMediaPermissions"; -import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../../../src/MediaDeviceHandler"; -import dis from "../../../../../src/dispatcher/dispatcher"; -import { Action } from "../../../../../src/dispatcher/actions"; - -jest.mock("../../../../../src/dispatcher/dispatcher"); -jest.mock("../../../../../src/utils/media/requestMediaPermissions"); - -// mock RoomAvatar, because it is doing too much fancy stuff -jest.mock("../../../../../src/components/views/avatars/RoomAvatar", () => ({ - __esModule: true, - default: jest.fn().mockImplementation(({ room }) => { - return
room avatar: {room.name}
; - }), -})); - -// mock VoiceRecording because it contains all the audio APIs -jest.mock("../../../../../src/audio/VoiceRecording", () => ({ - VoiceRecording: jest.fn().mockReturnValue({ - disableMaxLength: jest.fn(), - liveData: { - onUpdate: jest.fn(), - }, - start: jest.fn(), - }), -})); - -describe("VoiceBroadcastRecordingPip", () => { - const roomId = "!room:example.com"; - let client: MatrixClient; - let infoEvent: MatrixEvent; - let recording: VoiceBroadcastRecording; - let renderResult: RenderResult; - - const renderPip = async (state: VoiceBroadcastInfoState) => { - infoEvent = mkVoiceBroadcastInfoStateEvent(roomId, state, client.getUserId() || "", client.getDeviceId() || ""); - recording = new VoiceBroadcastRecording(infoEvent, client, state); - jest.spyOn(recording, "pause"); - jest.spyOn(recording, "resume"); - renderResult = render(); - await flushPromises(); - }; - - const itShouldShowTheBroadcastRoom = () => { - it("should show the broadcast room", () => { - expect(dis.dispatch).toHaveBeenCalledWith({ - action: Action.ViewRoom, - room_id: roomId, - metricsTrigger: undefined, - }); - }); - }; - - beforeAll(() => { - client = stubClient(); - mocked(requestMediaPermissions).mockResolvedValue({ - getTracks: (): Array => [], - } as unknown as MediaStream); - jest.spyOn(MediaDeviceHandler, "getDevices").mockResolvedValue({ - [MediaDeviceKindEnum.AudioInput]: [ - { - deviceId: "d1", - label: "Device 1", - } as MediaDeviceInfo, - { - deviceId: "d2", - label: "Device 2", - } as MediaDeviceInfo, - ], - [MediaDeviceKindEnum.AudioOutput]: [], - [MediaDeviceKindEnum.VideoInput]: [], - }); - jest.spyOn(MediaDeviceHandler.instance, "setDevice").mockImplementation(); - }); - - describe("when rendering a started recording", () => { - beforeEach(async () => { - await renderPip(VoiceBroadcastInfoState.Started); - }); - - it("should render as expected", () => { - expect(renderResult.container).toMatchSnapshot(); - }); - - describe("and selecting another input device", () => { - beforeEach(async () => { - await userEvent.click(screen.getByLabelText("Change input device")); - await userEvent.click(screen.getByText("Device 1")); - }); - - it("should select the device and pause and resume the broadcast", () => { - expect(MediaDeviceHandler.instance.setDevice).toHaveBeenCalledWith( - "d1", - MediaDeviceKindEnum.AudioInput, - ); - expect(recording.pause).toHaveBeenCalled(); - expect(recording.resume).toHaveBeenCalled(); - }); - }); - - describe("and clicking the room name", () => { - beforeEach(async () => { - await userEvent.click(screen.getByText("My room")); - }); - - itShouldShowTheBroadcastRoom(); - }); - - describe("and clicking the room avatar", () => { - beforeEach(async () => { - await userEvent.click(screen.getByText("room avatar: My room")); - }); - - itShouldShowTheBroadcastRoom(); - }); - - describe("and clicking the pause button", () => { - beforeEach(async () => { - await userEvent.click(screen.getByLabelText("pause voice broadcast")); - }); - - it("should pause the recording", () => { - expect(recording.getState()).toBe(VoiceBroadcastInfoState.Paused); - }); - }); - - describe("and clicking the stop button", () => { - beforeEach(async () => { - await userEvent.click(screen.getByLabelText("Stop Recording")); - await screen.findByText("Stop live broadcasting?"); - // modal rendering has some weird sleeps - await sleep(200); - }); - - it("should display the confirm end dialog", () => { - screen.getByText("Stop live broadcasting?"); - }); - - describe("and confirming the dialog", () => { - beforeEach(async () => { - await userEvent.click(screen.getByText("Yes, stop broadcast")); - }); - - it("should end the recording", () => { - expect(recording.getState()).toBe(VoiceBroadcastInfoState.Stopped); - }); - }); - }); - - describe("and there is no connection and clicking the pause button", () => { - beforeEach(async () => { - mocked(client.sendStateEvent).mockImplementation(() => { - throw new Error(); - }); - await userEvent.click(screen.getByLabelText("pause voice broadcast")); - }); - - it("should show a connection error info", () => { - expect(screen.getByText("Connection error - Recording paused")).toBeInTheDocument(); - }); - - describe("and the connection is back", () => { - beforeEach(() => { - mocked(client.sendStateEvent).mockResolvedValue({ event_id: "e1" }); - client.emit(ClientEvent.Sync, SyncState.Catchup, SyncState.Error); - }); - - it("should render a paused recording", async () => { - await expect(screen.findByLabelText("resume voice broadcast")).resolves.toBeInTheDocument(); - }); - }); - }); - }); - - describe("when rendering a paused recording", () => { - beforeEach(async () => { - await renderPip(VoiceBroadcastInfoState.Paused); - }); - - it("should render as expected", () => { - expect(renderResult.container).toMatchSnapshot(); - }); - - describe("and clicking the resume button", () => { - beforeEach(async () => { - await userEvent.click(screen.getByLabelText("resume voice broadcast")); - }); - - it("should resume the recording", () => { - expect(recording.getState()).toBe(VoiceBroadcastInfoState.Resumed); - }); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/components/molecules/VoiceBroadcastSmallPlaybackBody-test.tsx b/test/unit-tests/voice-broadcast/components/molecules/VoiceBroadcastSmallPlaybackBody-test.tsx deleted file mode 100644 index b65bbcf583..0000000000 --- a/test/unit-tests/voice-broadcast/components/molecules/VoiceBroadcastSmallPlaybackBody-test.tsx +++ /dev/null @@ -1,129 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; -import { render, RenderResult } from "jest-matrix-react"; -import userEvent from "@testing-library/user-event"; -import { mocked } from "jest-mock"; - -import { - VoiceBroadcastInfoState, - VoiceBroadcastLiveness, - VoiceBroadcastPlayback, - VoiceBroadcastSmallPlaybackBody, - VoiceBroadcastPlaybackState, -} from "../../../../../src/voice-broadcast"; -import { stubClient } from "../../../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "../../utils/test-utils"; -import { SdkContextClass } from "../../../../../src/contexts/SDKContext"; - -// mock RoomAvatar, because it is doing too much fancy stuff -jest.mock("../../../../../src/components/views/avatars/RoomAvatar", () => ({ - __esModule: true, - default: jest.fn().mockImplementation(({ room }) => { - return
room avatar: {room.name}
; - }), -})); - -describe("", () => { - const userId = "@user:example.com"; - const roomId = "!room:example.com"; - let client: MatrixClient; - let infoEvent: MatrixEvent; - let playback: VoiceBroadcastPlayback; - let renderResult: RenderResult; - - beforeAll(() => { - client = stubClient(); - mocked(client.relations).mockClear(); - mocked(client.relations).mockResolvedValue({ events: [] }); - - infoEvent = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Stopped, - userId, - client.getDeviceId()!, - ); - }); - - beforeEach(() => { - playback = new VoiceBroadcastPlayback( - infoEvent, - client, - SdkContextClass.instance.voiceBroadcastRecordingsStore, - ); - jest.spyOn(playback, "toggle").mockImplementation(() => Promise.resolve()); - jest.spyOn(playback, "getLiveness"); - jest.spyOn(playback, "getState"); - }); - - describe("when rendering a buffering broadcast", () => { - beforeEach(() => { - mocked(playback.getState).mockReturnValue(VoiceBroadcastPlaybackState.Buffering); - mocked(playback.getLiveness).mockReturnValue("live"); - renderResult = render(); - }); - - it("should render as expected", () => { - expect(renderResult.container).toMatchSnapshot(); - }); - }); - - describe("when rendering a playing broadcast", () => { - beforeEach(() => { - mocked(playback.getState).mockReturnValue(VoiceBroadcastPlaybackState.Playing); - mocked(playback.getLiveness).mockReturnValue("not-live"); - renderResult = render(); - }); - - it("should render as expected", () => { - expect(renderResult.container).toMatchSnapshot(); - }); - }); - - describe(`when rendering a stopped broadcast`, () => { - beforeEach(() => { - mocked(playback.getState).mockReturnValue(VoiceBroadcastPlaybackState.Stopped); - mocked(playback.getLiveness).mockReturnValue("not-live"); - renderResult = render(); - }); - - it("should render as expected", () => { - expect(renderResult.container).toMatchSnapshot(); - }); - - describe("and clicking the play button", () => { - beforeEach(async () => { - await userEvent.click(renderResult.getByLabelText("play voice broadcast")); - }); - - it("should toggle the playback", () => { - expect(playback.toggle).toHaveBeenCalled(); - }); - }); - }); - - describe.each([ - { state: VoiceBroadcastPlaybackState.Paused, liveness: "not-live" }, - { state: VoiceBroadcastPlaybackState.Playing, liveness: "live" }, - ] as Array<{ state: VoiceBroadcastPlaybackState; liveness: VoiceBroadcastLiveness }>)( - "when rendering a %s/%s broadcast", - ({ state, liveness }) => { - beforeEach(() => { - mocked(playback.getState).mockReturnValue(state); - mocked(playback.getLiveness).mockReturnValue(liveness); - renderResult = render(); - }); - - it("should render as expected", () => { - expect(renderResult.container).toMatchSnapshot(); - }); - }, - ); -}); diff --git a/test/unit-tests/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastPlaybackBody-test.tsx.snap b/test/unit-tests/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastPlaybackBody-test.tsx.snap deleted file mode 100644 index cb063c395c..0000000000 --- a/test/unit-tests/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastPlaybackBody-test.tsx.snap +++ /dev/null @@ -1,914 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`VoiceBroadcastPlaybackBody when rendering a buffering voice broadcast should render as expected 1`] = ` -
-
-
-
- room avatar: - My room -
-
-
-
- My room -
-
-
- - - - - - @user:example.com - -
-
-
-
-
- Buffering… -
-
-
-
- Live -
-
-
-
-
-
-
- - - -
-
-
-
-
- -
- - -
-
-
-`; - -exports[`VoiceBroadcastPlaybackBody when rendering a pause/not-live broadcast should render as expected 1`] = ` -
-
-
-
- room avatar: - My room -
-
-
-
- My room -
-
-
- - - - - - @user:example.com - -
-
-
- Voice broadcast -
-
-
-
-
-
-
-
- - - -
-
-
-
-
- -
- - -
-
-
-`; - -exports[`VoiceBroadcastPlaybackBody when rendering a playing broadcast in pip mode should render as expected 1`] = ` -
-
-
-
-
- room avatar: - My room -
-
-
-
-
-
- My room -
-
-
-
- - - - - - @user:example.com - -
-
-
- Voice broadcast -
-
-
-
-
-
-
-
- - - -
-
-
-
-
- -
- - -
-
-
-`; - -exports[`VoiceBroadcastPlaybackBody when rendering a playing broadcast should render as expected 1`] = ` -
-
-
-
- room avatar: - My room -
-
-
-
- My room -
-
-
- - - - - - @user:example.com - -
-
-
- Voice broadcast -
-
-
-
-
-
-
-
- - - -
-
-
-
-
- -
- - -
-
-
-`; - -exports[`VoiceBroadcastPlaybackBody when rendering a playing/live broadcast should render as expected 1`] = ` -
-
-
-
- room avatar: - My room -
-
-
-
- My room -
-
-
- - - - - - @user:example.com - -
-
-
- Voice broadcast -
-
-
-
- Live -
-
-
-
-
-
-
- - - -
-
-
-
-
- -
- - -
-
-
-`; - -exports[`VoiceBroadcastPlaybackBody when rendering a stopped broadcast should render as expected 1`] = ` -
-
-
-
- room avatar: - My room -
-
-
-
- My room -
-
-
- - - - - - @user:example.com - -
-
-
- Voice broadcast -
-
-
-
-
- - - -
-
- -
- - -
-
-
-`; - -exports[`VoiceBroadcastPlaybackBody when rendering an error broadcast should render as expected 1`] = ` -
-
-
-
- room avatar: - My room -
-
-
-
- My room -
-
-
- - - - - - @user:example.com - -
-
-
- Voice broadcast -
-
-
-
- - - - - Unable to play this voice broadcast -
-
-
-`; diff --git a/test/unit-tests/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastPreRecordingPip-test.tsx.snap b/test/unit-tests/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastPreRecordingPip-test.tsx.snap deleted file mode 100644 index f50cdc3be4..0000000000 --- a/test/unit-tests/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastPreRecordingPip-test.tsx.snap +++ /dev/null @@ -1,98 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`VoiceBroadcastPreRecordingPip when rendered should match the snapshot 1`] = ` -
-
-
-
-
- room avatar: - !room@example.com -
-
-
-
-
-
- !room@example.com -
-
-
-
- - - - - - Default Device - -
-
-
- - - -
-
-
-
- Go live -
-
-
-`; diff --git a/test/unit-tests/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastRecordingBody-test.tsx.snap b/test/unit-tests/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastRecordingBody-test.tsx.snap deleted file mode 100644 index c2e6fdcd54..0000000000 --- a/test/unit-tests/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastRecordingBody-test.tsx.snap +++ /dev/null @@ -1,131 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`VoiceBroadcastRecordingBody when rendering a live broadcast should render with a red live badge 1`] = ` -
-
-
-
- room avatar: - My room -
-
-
-
- My room -
-
-
- - - - - - @user:example.com - -
-
-
-
- Live -
-
-
-
-`; - -exports[`VoiceBroadcastRecordingBody when rendering a paused broadcast should render with a grey live badge 1`] = ` -
-
-
-
- room avatar: - My room -
-
-
-
- My room -
-
-
- - - - - - @user:example.com - -
-
-
-
- Live -
-
-
-
-`; diff --git a/test/unit-tests/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastRecordingPip-test.tsx.snap b/test/unit-tests/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastRecordingPip-test.tsx.snap deleted file mode 100644 index 2fc2334575..0000000000 --- a/test/unit-tests/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastRecordingPip-test.tsx.snap +++ /dev/null @@ -1,238 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`VoiceBroadcastRecordingPip when rendering a paused recording should render as expected 1`] = ` -
-
-
-
-
- room avatar: - My room -
-
-
-
-
-
- My room -
-
-
-
-
- -
-
-
-
- Live -
-
-
-
-
-
-
-
- - - - -
-
-
-
-
-
-
-`; - -exports[`VoiceBroadcastRecordingPip when rendering a started recording should render as expected 1`] = ` -
-
-
-
-
- room avatar: - My room -
-
-
-
-
-
- My room -
-
-
-
-
- -
-
-
-
- Live -
-
-
-
-
- - - -
-
- - - - -
-
-
-
-
-
-
-`; diff --git a/test/unit-tests/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastSmallPlaybackBody-test.tsx.snap b/test/unit-tests/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastSmallPlaybackBody-test.tsx.snap deleted file mode 100644 index 088151158b..0000000000 --- a/test/unit-tests/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastSmallPlaybackBody-test.tsx.snap +++ /dev/null @@ -1,558 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` when rendering a { state: 'pause', liveness: 'not-live' }/%s broadcast should render as expected 1`] = ` -
-
-
-
-
- room avatar: - My room -
-
-
-
-
-
- My room -
-
-
-
- - - - - - @user:example.com - -
-
-
-
- - - -
-
- - - -
-
-
-`; - -exports[` when rendering a { state: 'playing', liveness: 'live' }/%s broadcast should render as expected 1`] = ` -
-
-
-
-
- room avatar: - My room -
-
-
-
-
-
- My room -
-
-
-
- - - - - - @user:example.com - -
-
-
- Live -
-
-
-
- - - -
-
- - - -
-
-
-`; - -exports[` when rendering a buffering broadcast should render as expected 1`] = ` -
-
-
-
-
- room avatar: - My room -
-
-
-
-
-
- My room -
-
-
-
-
-
-
- - - - - - @user:example.com - -
-
-
- Live -
-
-
-
- - - -
-
- - - -
-
-
-`; - -exports[` when rendering a playing broadcast should render as expected 1`] = ` -
-
-
-
-
- room avatar: - My room -
-
-
-
-
-
- My room -
-
-
-
- - - - - - @user:example.com - -
-
-
-
- - - -
-
- - - -
-
-
-`; - -exports[` when rendering a stopped broadcast should render as expected 1`] = ` -
-
-
-
-
- room avatar: - My room -
-
-
-
-
-
- My room -
-
-
-
- - - - - - @user:example.com - -
-
-
-
- - - -
-
- - - -
-
-
-`; diff --git a/test/unit-tests/voice-broadcast/models/VoiceBroadcastPlayback-test.tsx b/test/unit-tests/voice-broadcast/models/VoiceBroadcastPlayback-test.tsx deleted file mode 100644 index 9ce24e5921..0000000000 --- a/test/unit-tests/voice-broadcast/models/VoiceBroadcastPlayback-test.tsx +++ /dev/null @@ -1,747 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { mocked } from "jest-mock"; -import { screen } from "jest-matrix-react"; -import userEvent from "@testing-library/user-event"; -import { MatrixClient, MatrixEvent, MatrixEventEvent, Room } from "matrix-js-sdk/src/matrix"; -import { defer } from "matrix-js-sdk/src/utils"; - -import { Playback, PlaybackState } from "../../../../src/audio/Playback"; -import { PlaybackManager } from "../../../../src/audio/PlaybackManager"; -import { SdkContextClass } from "../../../../src/contexts/SDKContext"; -import { MediaEventHelper } from "../../../../src/utils/MediaEventHelper"; -import { - VoiceBroadcastInfoState, - VoiceBroadcastLiveness, - VoiceBroadcastPlayback, - VoiceBroadcastPlaybackEvent, - VoiceBroadcastPlaybackState, - VoiceBroadcastRecording, -} from "../../../../src/voice-broadcast"; -import { - filterConsole, - flushPromises, - flushPromisesWithFakeTimers, - stubClient, - waitEnoughCyclesForModal, -} from "../../../test-utils"; -import { createTestPlayback } from "../../../test-utils/audio"; -import { mkVoiceBroadcastChunkEvent, mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils"; -import { LazyValue } from "../../../../src/utils/LazyValue"; - -jest.mock("../../../../src/utils/MediaEventHelper", () => ({ - MediaEventHelper: jest.fn(), -})); - -describe("VoiceBroadcastPlayback", () => { - const userId = "@user:example.com"; - let deviceId: string; - const roomId = "!room:example.com"; - let room: Room; - let client: MatrixClient; - let infoEvent: MatrixEvent; - let playback: VoiceBroadcastPlayback; - let onStateChanged: (state: VoiceBroadcastPlaybackState) => void; - let chunk1Event: MatrixEvent; - let deplayedChunk1Event: MatrixEvent; - let chunk2Event: MatrixEvent; - let chunk2BEvent: MatrixEvent; - let chunk3Event: MatrixEvent; - const chunk1Length = 2300; - const chunk2Length = 4200; - const chunk3Length = 6900; - const chunk1Data = new ArrayBuffer(2); - const chunk2Data = new ArrayBuffer(3); - const chunk3Data = new ArrayBuffer(3); - let delayedChunk1Helper: MediaEventHelper; - let chunk1Helper: MediaEventHelper; - let chunk2Helper: MediaEventHelper; - let chunk3Helper: MediaEventHelper; - let chunk1Playback: Playback; - let chunk2Playback: Playback; - let chunk3Playback: Playback; - let middleOfSecondChunk!: number; - let middleOfThirdChunk!: number; - - const queryConfirmListeningDialog = () => { - return screen.queryByText( - "If you start listening to this live broadcast, your current live broadcast recording will be ended.", - ); - }; - - const itShouldSetTheStateTo = (state: VoiceBroadcastPlaybackState) => { - it(`should set the state to ${state}`, () => { - expect(playback.getState()).toBe(state); - }); - }; - - const itShouldEmitAStateChangedEvent = (state: VoiceBroadcastPlaybackState) => { - it(`should emit a ${state} state changed event`, () => { - expect(mocked(onStateChanged)).toHaveBeenCalledWith(state, playback); - }); - }; - - const itShouldHaveLiveness = (liveness: VoiceBroadcastLiveness): void => { - it(`should have liveness ${liveness}`, () => { - expect(playback.getLiveness()).toBe(liveness); - }); - }; - - const startPlayback = () => { - beforeEach(() => { - playback.start(); - }); - }; - - const pausePlayback = () => { - beforeEach(() => { - playback.pause(); - }); - }; - - const stopPlayback = () => { - beforeEach(() => { - playback.stop(); - }); - }; - - const mkChunkHelper = (data: ArrayBuffer): MediaEventHelper => { - return { - sourceBlob: { - cachedValue: new Blob(), - done: false, - value: { - // @ts-ignore - arrayBuffer: jest.fn().mockResolvedValue(data), - }, - }, - }; - }; - - const mkDeplayedChunkHelper = (data: ArrayBuffer): MediaEventHelper => { - const deferred = defer>(); - - setTimeout(() => { - deferred.resolve({ - // @ts-ignore - arrayBuffer: jest.fn().mockResolvedValue(data), - }); - }, 7500); - - return { - sourceBlob: { - cachedValue: new Blob(), - done: false, - // @ts-ignore - value: deferred.promise, - }, - }; - }; - - const simulateFirstChunkArrived = async (): Promise => { - jest.advanceTimersByTime(10000); - await flushPromisesWithFakeTimers(); - }; - - const mkInfoEvent = (state: VoiceBroadcastInfoState) => { - return mkVoiceBroadcastInfoStateEvent(roomId, state, userId, deviceId); - }; - - const mkPlayback = async (fakeTimers = false): Promise => { - const playback = new VoiceBroadcastPlayback( - infoEvent, - client, - SdkContextClass.instance.voiceBroadcastRecordingsStore, - ); - jest.spyOn(playback, "removeAllListeners"); - jest.spyOn(playback, "destroy"); - playback.on(VoiceBroadcastPlaybackEvent.StateChanged, onStateChanged); - if (fakeTimers) { - await flushPromisesWithFakeTimers(); - } else { - await flushPromises(); - } - return playback; - }; - - const setUpChunkEvents = (chunkEvents: MatrixEvent[]) => { - mocked(client.relations).mockResolvedValueOnce({ - events: chunkEvents, - }); - }; - - const createChunkEvents = () => { - chunk1Event = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk1Length, 1); - deplayedChunk1Event = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk1Length, 1); - chunk2Event = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk2Length, 2); - chunk2Event.setTxnId("tx-id-1"); - chunk2BEvent = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk2Length, 2); - chunk2BEvent.setTxnId("tx-id-1"); - chunk3Event = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk3Length, 3); - - chunk1Helper = mkChunkHelper(chunk1Data); - delayedChunk1Helper = mkDeplayedChunkHelper(chunk1Data); - chunk2Helper = mkChunkHelper(chunk2Data); - chunk3Helper = mkChunkHelper(chunk3Data); - - chunk1Playback = createTestPlayback(); - chunk2Playback = createTestPlayback(); - chunk3Playback = createTestPlayback(); - - middleOfSecondChunk = (chunk1Length + chunk2Length / 2) / 1000; - middleOfThirdChunk = (chunk1Length + chunk2Length + chunk3Length / 2) / 1000; - - jest.spyOn(PlaybackManager.instance, "createPlaybackInstance").mockImplementation( - (buffer: ArrayBuffer, _waveForm?: number[]) => { - if (buffer === chunk1Data) return chunk1Playback; - if (buffer === chunk2Data) return chunk2Playback; - if (buffer === chunk3Data) return chunk3Playback; - - throw new Error("unexpected buffer"); - }, - ); - - mocked(MediaEventHelper).mockImplementation((event: MatrixEvent): any => { - if (event === chunk1Event) return chunk1Helper; - if (event === deplayedChunk1Event) return delayedChunk1Helper; - if (event === chunk2Event) return chunk2Helper; - if (event === chunk3Event) return chunk3Helper; - }); - }; - - filterConsole( - // expected for some tests - "Unable to load broadcast playback", - ); - - beforeEach(() => { - client = stubClient(); - deviceId = client.getDeviceId() || ""; - room = new Room(roomId, client, client.getSafeUserId()); - mocked(client.getRoom).mockImplementation((roomId: string): Room | null => { - if (roomId === room.roomId) return room; - return null; - }); - onStateChanged = jest.fn(); - }); - - afterEach(async () => { - SdkContextClass.instance.voiceBroadcastPlaybacksStore.getCurrent()?.stop(); - SdkContextClass.instance.voiceBroadcastPlaybacksStore.clearCurrent(); - await SdkContextClass.instance.voiceBroadcastRecordingsStore.getCurrent()?.stop(); - SdkContextClass.instance.voiceBroadcastRecordingsStore.clearCurrent(); - playback.destroy(); - }); - - describe(`when there is a ${VoiceBroadcastInfoState.Resumed} broadcast without chunks yet`, () => { - beforeEach(async () => { - infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Resumed); - createChunkEvents(); - room.addLiveEvents([infoEvent], { addToState: true }); - playback = await mkPlayback(); - }); - - describe("and calling start", () => { - startPlayback(); - - itShouldHaveLiveness("live"); - - it("should be in buffering state", () => { - expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Buffering); - }); - - it("should have duration 0", () => { - expect(playback.durationSeconds).toBe(0); - }); - - it("should be at time 0", () => { - expect(playback.timeSeconds).toBe(0); - }); - - describe("and calling stop", () => { - stopPlayback(); - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped); - - describe("and calling pause", () => { - pausePlayback(); - // stopped voice broadcasts cannot be paused - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped); - }); - }); - - describe("and calling pause", () => { - pausePlayback(); - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Paused); - }); - - describe("and receiving the first chunk", () => { - beforeEach(() => { - room.relations.aggregateChildEvent(chunk1Event); - }); - - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing); - itShouldHaveLiveness("live"); - - it("should update the duration", () => { - expect(playback.durationSeconds).toBe(2.3); - }); - - it("should play the first chunk", () => { - expect(chunk1Playback.play).toHaveBeenCalled(); - }); - }); - - describe("and receiving the first undecryptable chunk", () => { - beforeEach(() => { - jest.spyOn(chunk1Event, "isDecryptionFailure").mockReturnValue(true); - room.relations.aggregateChildEvent(chunk1Event); - }); - - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Error); - - it("should not update the duration", () => { - expect(playback.durationSeconds).toBe(0); - }); - - describe("and the chunk is decrypted", () => { - beforeEach(() => { - mocked(chunk1Event.isDecryptionFailure).mockReturnValue(false); - chunk1Event.emit(MatrixEventEvent.Decrypted, chunk1Event); - }); - - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Paused); - - it("should not update the duration", () => { - expect(playback.durationSeconds).toBe(2.3); - }); - }); - }); - }); - }); - - describe(`when there is a ${VoiceBroadcastInfoState.Resumed} voice broadcast with some chunks`, () => { - beforeEach(async () => { - mocked(client.relations).mockResolvedValueOnce({ events: [] }); - infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Resumed); - createChunkEvents(); - setUpChunkEvents([chunk2Event, chunk1Event]); - room.addLiveEvents([infoEvent, chunk1Event, chunk2Event], { addToState: true }); - room.relations.aggregateChildEvent(chunk2Event); - room.relations.aggregateChildEvent(chunk1Event); - playback = await mkPlayback(); - }); - - it("durationSeconds should have the length of the known chunks", () => { - expect(playback.durationSeconds).toEqual(6.5); - }); - - describe("and starting a playback with a broken chunk", () => { - beforeEach(async () => { - mocked(chunk2Playback.prepare).mockRejectedValue("Error decoding chunk"); - await playback.start(); - }); - - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Error); - - it("start() should keep it in the error state)", async () => { - await playback.start(); - expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Error); - }); - - it("stop() should keep it in the error state)", () => { - playback.stop(); - expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Error); - }); - - it("toggle() should keep it in the error state)", async () => { - await playback.toggle(); - expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Error); - }); - - it("pause() should keep it in the error state)", () => { - playback.pause(); - expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Error); - }); - }); - - describe("and an event with the same transaction Id occurs", () => { - beforeEach(() => { - room.addLiveEvents([chunk2BEvent], { addToState: true }); - room.relations.aggregateChildEvent(chunk2BEvent); - }); - - it("durationSeconds should not change", () => { - expect(playback.durationSeconds).toEqual(6.5); - }); - }); - - describe("and calling start", () => { - startPlayback(); - - it("should play the last chunk", () => { - expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Playing); - // assert that the last chunk is played first - expect(chunk2Playback.play).toHaveBeenCalled(); - expect(chunk1Playback.play).not.toHaveBeenCalled(); - }); - - describe( - "and receiving a stop info event with last_chunk_sequence = 2 and " + - "the playback of the last available chunk ends", - () => { - beforeEach(() => { - const stoppedEvent = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Stopped, - client.getSafeUserId(), - client.deviceId!, - infoEvent, - 2, - ); - room.addLiveEvents([stoppedEvent], { addToState: true }); - room.relations.aggregateChildEvent(stoppedEvent); - chunk2Playback.emit(PlaybackState.Stopped); - }); - - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped); - }, - ); - - describe( - "and receiving a stop info event with last_chunk_sequence = 3 and " + - "the playback of the last available chunk ends", - () => { - beforeEach(() => { - const stoppedEvent = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Stopped, - client.getSafeUserId(), - client.deviceId!, - infoEvent, - 3, - ); - room.addLiveEvents([stoppedEvent], { addToState: true }); - room.relations.aggregateChildEvent(stoppedEvent); - chunk2Playback.emit(PlaybackState.Stopped); - }); - - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Buffering); - - describe("and the next chunk arrives", () => { - beforeEach(() => { - room.addLiveEvents([chunk3Event], { addToState: true }); - room.relations.aggregateChildEvent(chunk3Event); - }); - - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing); - - it("should play the next chunk", () => { - expect(chunk3Playback.play).toHaveBeenCalled(); - }); - }); - }, - ); - - describe("and the info event is deleted", () => { - beforeEach(() => { - infoEvent.makeRedacted(new MatrixEvent({}), room); - }); - - it("should stop and destroy the playback", () => { - expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Stopped); - expect(playback.destroy).toHaveBeenCalled(); - }); - }); - }); - - describe("and currently recording a broadcast", () => { - let recording: VoiceBroadcastRecording; - - beforeEach(async () => { - recording = new VoiceBroadcastRecording( - mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Started, - client.getSafeUserId(), - client.deviceId, - ), - client, - ); - jest.spyOn(recording, "stop"); - SdkContextClass.instance.voiceBroadcastRecordingsStore.setCurrent(recording); - playback.start(); - await waitEnoughCyclesForModal(); - }); - - it("should display a confirm modal", () => { - expect(queryConfirmListeningDialog()).toBeInTheDocument(); - }); - - describe("when confirming the dialog", () => { - beforeEach(async () => { - await userEvent.click(screen.getByText("Yes, end my recording")); - }); - - it("should stop the recording", () => { - expect(recording.stop).toHaveBeenCalled(); - expect(SdkContextClass.instance.voiceBroadcastRecordingsStore.getCurrent()).toBeNull(); - }); - - it("should not start the playback", () => { - expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Playing); - }); - }); - - describe("when not confirming the dialog", () => { - beforeEach(async () => { - await userEvent.click(screen.getByText("No")); - }); - - it("should not stop the recording", () => { - expect(recording.stop).not.toHaveBeenCalled(); - }); - - it("should start the playback", () => { - expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Stopped); - }); - }); - }); - }); - - describe("when there is a stopped voice broadcast", () => { - beforeEach(async () => { - jest.useFakeTimers(); - infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Stopped); - createChunkEvents(); - // use delayed first chunk here to simulate loading time - setUpChunkEvents([chunk2Event, deplayedChunk1Event, chunk3Event]); - room.addLiveEvents([infoEvent, deplayedChunk1Event, chunk2Event, chunk3Event], { addToState: true }); - playback = await mkPlayback(true); - }); - - afterEach(() => { - jest.useRealTimers(); - }); - - it("should expose the info event", () => { - expect(playback.infoEvent).toBe(infoEvent); - }); - - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped); - - describe("and calling start", () => { - startPlayback(); - - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Buffering); - - describe("and the first chunk data has been loaded", () => { - beforeEach(async () => { - await simulateFirstChunkArrived(); - }); - - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing); - - it("should play the chunks beginning with the first one", () => { - // assert that the first chunk is being played - expect(chunk1Playback.play).toHaveBeenCalled(); - expect(chunk2Playback.play).not.toHaveBeenCalled(); - }); - - describe("and calling start again", () => { - it("should not play the first chunk a second time", () => { - expect(chunk1Playback.play).toHaveBeenCalledTimes(1); - }); - }); - - describe("and the chunk playback progresses", () => { - beforeEach(() => { - chunk1Playback.clockInfo.liveData.update([11]); - }); - - it("should update the time", () => { - expect(playback.timeSeconds).toBe(11); - }); - }); - - describe("and the chunk playback progresses across the actual time", () => { - // This can be the case if the meta data is out of sync with the actual audio data. - - beforeEach(() => { - chunk1Playback.clockInfo.liveData.update([15]); - }); - - it("should update the time", () => { - expect(playback.timeSeconds).toBe(15); - expect(playback.timeLeftSeconds).toBe(0); - }); - }); - - describe("and skipping to the middle of the second chunk", () => { - const middleOfSecondChunk = (chunk1Length + chunk2Length / 2) / 1000; - - beforeEach(async () => { - await playback.skipTo(middleOfSecondChunk); - }); - - it("should play the second chunk", () => { - expect(chunk1Playback.stop).toHaveBeenCalled(); - expect(chunk1Playback.destroy).toHaveBeenCalled(); - expect(chunk2Playback.play).toHaveBeenCalled(); - }); - - it("should update the time", () => { - expect(playback.timeSeconds).toBe(middleOfSecondChunk); - }); - - describe("and skipping to the start", () => { - beforeEach(async () => { - await playback.skipTo(0); - }); - - it("should play the first chunk", () => { - expect(chunk2Playback.stop).toHaveBeenCalled(); - expect(chunk2Playback.destroy).toHaveBeenCalled(); - expect(chunk1Playback.play).toHaveBeenCalled(); - }); - - it("should update the time", () => { - expect(playback.timeSeconds).toBe(0); - }); - }); - }); - - describe("and skipping multiple times", () => { - beforeEach(async () => { - return Promise.all([ - playback.skipTo(middleOfSecondChunk), - playback.skipTo(middleOfThirdChunk), - playback.skipTo(0), - ]); - }); - - it("should only skip to the first and last position", () => { - expect(chunk1Playback.stop).toHaveBeenCalled(); - expect(chunk1Playback.destroy).toHaveBeenCalled(); - expect(chunk2Playback.play).toHaveBeenCalled(); - - expect(chunk3Playback.play).not.toHaveBeenCalled(); - - expect(chunk2Playback.stop).toHaveBeenCalled(); - expect(chunk2Playback.destroy).toHaveBeenCalled(); - expect(chunk1Playback.play).toHaveBeenCalled(); - }); - }); - - describe("and the first chunk ends", () => { - beforeEach(() => { - chunk1Playback.emit(PlaybackState.Stopped); - }); - - it("should play until the end", () => { - // assert first chunk was unloaded - expect(chunk1Playback.destroy).toHaveBeenCalled(); - - // assert that the second chunk is being played - expect(chunk2Playback.play).toHaveBeenCalled(); - - // simulate end of second and third chunk - chunk2Playback.emit(PlaybackState.Stopped); - chunk3Playback.emit(PlaybackState.Stopped); - - // assert that the entire playback is now in stopped state - expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Stopped); - }); - }); - - describe("and calling pause", () => { - pausePlayback(); - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Paused); - itShouldEmitAStateChangedEvent(VoiceBroadcastPlaybackState.Paused); - }); - - describe("and calling stop", () => { - stopPlayback(); - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped); - - it("should stop the playback", () => { - expect(chunk1Playback.stop).toHaveBeenCalled(); - }); - - describe("and skipping to somewhere in the middle of the first chunk", () => { - beforeEach(async () => { - mocked(chunk1Playback.play).mockClear(); - await playback.skipTo(1); - }); - - it("should not start the playback", () => { - expect(chunk1Playback.play).not.toHaveBeenCalled(); - }); - }); - }); - - describe("and calling destroy", () => { - beforeEach(() => { - playback.destroy(); - }); - - it("should call removeAllListeners", () => { - expect(playback.removeAllListeners).toHaveBeenCalled(); - }); - - it("should call destroy on the playbacks", () => { - expect(chunk1Playback.destroy).toHaveBeenCalled(); - expect(chunk2Playback.destroy).toHaveBeenCalled(); - }); - }); - }); - }); - - describe("and calling toggle for the first time", () => { - beforeEach(async () => { - playback.toggle(); - await simulateFirstChunkArrived(); - }); - - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing); - - describe("and calling toggle a second time", () => { - beforeEach(async () => { - await playback.toggle(); - }); - - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Paused); - - describe("and calling toggle a third time", () => { - beforeEach(async () => { - await playback.toggle(); - }); - - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing); - }); - }); - }); - - describe("and calling stop", () => { - stopPlayback(); - - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped); - - describe("and calling toggle", () => { - beforeEach(async () => { - mocked(onStateChanged).mockReset(); - playback.toggle(); - await simulateFirstChunkArrived(); - }); - - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing); - itShouldEmitAStateChangedEvent(VoiceBroadcastPlaybackState.Playing); - }); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/models/VoiceBroadcastPreRecording-test.ts b/test/unit-tests/voice-broadcast/models/VoiceBroadcastPreRecording-test.ts deleted file mode 100644 index f8b9e89d3e..0000000000 --- a/test/unit-tests/voice-broadcast/models/VoiceBroadcastPreRecording-test.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix"; - -import { - startNewVoiceBroadcastRecording, - VoiceBroadcastPlaybacksStore, - VoiceBroadcastPreRecording, - VoiceBroadcastRecordingsStore, -} from "../../../../src/voice-broadcast"; -import { stubClient } from "../../../test-utils"; - -jest.mock("../../../../src/voice-broadcast/utils/startNewVoiceBroadcastRecording"); - -describe("VoiceBroadcastPreRecording", () => { - const roomId = "!room:example.com"; - let client: MatrixClient; - let room: Room; - let sender: RoomMember; - let playbacksStore: VoiceBroadcastPlaybacksStore; - let recordingsStore: VoiceBroadcastRecordingsStore; - let preRecording: VoiceBroadcastPreRecording; - let onDismiss: (voiceBroadcastPreRecording: VoiceBroadcastPreRecording) => void; - - beforeAll(() => { - client = stubClient(); - room = new Room(roomId, client, client.getUserId() || ""); - sender = new RoomMember(roomId, client.getUserId() || ""); - recordingsStore = new VoiceBroadcastRecordingsStore(); - playbacksStore = new VoiceBroadcastPlaybacksStore(recordingsStore); - }); - - beforeEach(() => { - onDismiss = jest.fn(); - preRecording = new VoiceBroadcastPreRecording(room, sender, client, playbacksStore, recordingsStore); - preRecording.on("dismiss", onDismiss); - }); - - describe("start", () => { - beforeEach(() => { - preRecording.start(); - }); - - it("should start a new voice broadcast recording", () => { - expect(startNewVoiceBroadcastRecording).toHaveBeenCalledWith(room, client, playbacksStore, recordingsStore); - }); - - it("should emit a dismiss event", () => { - expect(onDismiss).toHaveBeenCalledWith(preRecording); - }); - }); - - describe("cancel", () => { - beforeEach(() => { - preRecording.cancel(); - }); - - it("should emit a dismiss event", () => { - expect(onDismiss).toHaveBeenCalledWith(preRecording); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/models/VoiceBroadcastRecording-test.ts b/test/unit-tests/voice-broadcast/models/VoiceBroadcastRecording-test.ts deleted file mode 100644 index 4f6bd8f47b..0000000000 --- a/test/unit-tests/voice-broadcast/models/VoiceBroadcastRecording-test.ts +++ /dev/null @@ -1,660 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { mocked } from "jest-mock"; -import { - ClientEvent, - EventTimelineSet, - EventType, - LOCAL_NOTIFICATION_SETTINGS_PREFIX, - MatrixClient, - MatrixEvent, - MatrixEventEvent, - MsgType, - RelationType, - Room, - Relations, - SyncState, -} from "matrix-js-sdk/src/matrix"; -import { EncryptedFile } from "matrix-js-sdk/src/types"; -import fetchMock from "fetch-mock-jest"; - -import { uploadFile } from "../../../../src/ContentMessages"; -import { createVoiceMessageContent } from "../../../../src/utils/createVoiceMessageContent"; -import { - createVoiceBroadcastRecorder, - getChunkLength, - getMaxBroadcastLength, - VoiceBroadcastInfoEventContent, - VoiceBroadcastInfoEventType, - VoiceBroadcastInfoState, - VoiceBroadcastRecorder, - VoiceBroadcastRecorderEvent, - VoiceBroadcastRecording, - VoiceBroadcastRecordingEvent, - VoiceBroadcastRecordingState, -} from "../../../../src/voice-broadcast"; -import { mkEvent, mkStubRoom, stubClient } from "../../../test-utils"; -import dis from "../../../../src/dispatcher/dispatcher"; -import { VoiceRecording } from "../../../../src/audio/VoiceRecording"; -import { createAudioContext } from "../../../../src/audio/compat"; - -jest.mock("../../../../src/voice-broadcast/audio/VoiceBroadcastRecorder", () => ({ - ...(jest.requireActual("../../../../src/voice-broadcast/audio/VoiceBroadcastRecorder") as object), - createVoiceBroadcastRecorder: jest.fn(), -})); - -// mock VoiceRecording because it contains all the audio APIs -jest.mock("../../../../src/audio/VoiceRecording", () => ({ - VoiceRecording: jest.fn().mockReturnValue({ - disableMaxLength: jest.fn(), - liveData: { - onUpdate: jest.fn(), - }, - off: jest.fn(), - on: jest.fn(), - start: jest.fn(), - stop: jest.fn(), - destroy: jest.fn(), - contentType: "audio/ogg", - }), -})); - -jest.mock("../../../../src/ContentMessages", () => ({ - uploadFile: jest.fn(), -})); - -jest.mock("../../../../src/utils/createVoiceMessageContent", () => ({ - createVoiceMessageContent: jest.fn(), -})); - -jest.mock("../../../../src/audio/compat", () => ({ - ...jest.requireActual("../../../../src/audio/compat"), - createAudioContext: jest.fn(), -})); - -describe("VoiceBroadcastRecording", () => { - const roomId = "!room:example.com"; - const uploadedUrl = "mxc://example.com/vb"; - const uploadedFile = { file: true } as unknown as EncryptedFile; - const maxLength = getMaxBroadcastLength(); - let room: Room; - let client: MatrixClient; - let infoEvent: MatrixEvent; - let voiceBroadcastRecording: VoiceBroadcastRecording; - let onStateChanged: (state: VoiceBroadcastRecordingState) => void; - let voiceBroadcastRecorder: VoiceBroadcastRecorder; - let audioElement: HTMLAudioElement; - - const mkVoiceBroadcastInfoEvent = (content: VoiceBroadcastInfoEventContent) => { - return mkEvent({ - event: true, - type: VoiceBroadcastInfoEventType, - user: client.getSafeUserId(), - room: roomId, - content, - }); - }; - - const setUpVoiceBroadcastRecording = () => { - voiceBroadcastRecording = new VoiceBroadcastRecording(infoEvent, client); - voiceBroadcastRecording.on(VoiceBroadcastRecordingEvent.StateChanged, onStateChanged); - jest.spyOn(voiceBroadcastRecording, "destroy"); - jest.spyOn(voiceBroadcastRecording, "emit"); - jest.spyOn(voiceBroadcastRecording, "removeAllListeners"); - }; - - const itShouldBeInState = (state: VoiceBroadcastRecordingState) => { - it(`should be in state stopped ${state}`, () => { - expect(voiceBroadcastRecording.getState()).toBe(state); - }); - }; - - const emitFirsChunkRecorded = () => { - voiceBroadcastRecorder.emit(VoiceBroadcastRecorderEvent.ChunkRecorded, { - buffer: new Uint8Array([1, 2, 3]), - length: 23, - }); - }; - - const itShouldSendAnInfoEvent = (state: VoiceBroadcastInfoState, lastChunkSequence: number) => { - it(`should send a ${state} info event`, () => { - expect(client.sendStateEvent).toHaveBeenCalledWith( - roomId, - VoiceBroadcastInfoEventType, - { - device_id: client.getDeviceId(), - state, - last_chunk_sequence: lastChunkSequence, - ["m.relates_to"]: { - rel_type: RelationType.Reference, - event_id: infoEvent.getId(), - }, - } as VoiceBroadcastInfoEventContent, - client.getUserId()!, - ); - }); - }; - - const itShouldSendAVoiceMessage = (data: number[], size: number, duration: number, sequence: number) => { - // events contain milliseconds - duration *= 1000; - - it("should send a voice message", () => { - expect(uploadFile).toHaveBeenCalledWith( - client, - roomId, - new Blob([new Uint8Array(data)], { type: voiceBroadcastRecorder.contentType }), - ); - - expect(mocked(client.sendMessage)).toHaveBeenCalledWith(roomId, { - body: "Voice message", - file: { - file: true, - }, - info: { - duration, - mimetype: "audio/ogg", - size, - }, - ["m.relates_to"]: { - event_id: infoEvent.getId(), - rel_type: "m.reference", - }, - msgtype: "m.audio", - ["org.matrix.msc1767.audio"]: { - duration, - waveform: undefined, - }, - ["org.matrix.msc1767.file"]: { - file: { - file: true, - }, - mimetype: "audio/ogg", - name: "Voice message.ogg", - size, - url: "mxc://example.com/vb", - }, - ["org.matrix.msc1767.text"]: "Voice message", - ["org.matrix.msc3245.voice"]: {}, - url: "mxc://example.com/vb", - ["io.element.voice_broadcast_chunk"]: { - sequence, - }, - }); - }); - }; - - const setUpUploadFileMock = () => { - mocked(uploadFile).mockResolvedValue({ - url: uploadedUrl, - file: uploadedFile, - }); - }; - - const mockAudioBufferSourceNode = { - addEventListener: jest.fn(), - connect: jest.fn(), - start: jest.fn(), - }; - const mockAudioContext = { - decodeAudioData: jest.fn(), - suspend: jest.fn(), - resume: jest.fn(), - createBufferSource: jest.fn().mockReturnValue(mockAudioBufferSourceNode), - currentTime: 1337, - }; - - beforeEach(() => { - client = stubClient(); - room = mkStubRoom(roomId, "Test Room", client); - mocked(client.getRoom).mockImplementation((getRoomId: string | undefined): Room | null => { - if (getRoomId === roomId) { - return room; - } - - return null; - }); - onStateChanged = jest.fn(); - voiceBroadcastRecorder = new VoiceBroadcastRecorder(new VoiceRecording(), getChunkLength()); - jest.spyOn(voiceBroadcastRecorder, "start"); - jest.spyOn(voiceBroadcastRecorder, "stop"); - jest.spyOn(voiceBroadcastRecorder, "destroy"); - mocked(createVoiceBroadcastRecorder).mockReturnValue(voiceBroadcastRecorder); - - setUpUploadFileMock(); - - mocked(createVoiceMessageContent).mockImplementation( - ( - mxc: string | undefined, - mimetype: string, - duration: number, - size: number, - file?: EncryptedFile, - waveform?: number[], - ) => { - return { - body: "Voice message", - msgtype: MsgType.Audio, - url: mxc, - file, - info: { - duration, - mimetype, - size, - }, - ["org.matrix.msc1767.text"]: "Voice message", - ["org.matrix.msc1767.file"]: { - url: mxc, - file, - name: "Voice message.ogg", - mimetype, - size, - }, - ["org.matrix.msc1767.audio"]: { - duration, - // https://github.com/matrix-org/matrix-doc/pull/3246 - waveform, - }, - ["org.matrix.msc3245.voice"]: {}, // No content, this is a rendering hint - }; - }, - ); - - audioElement = { - play: jest.fn(), - } as any as HTMLAudioElement; - - jest.spyOn(document, "querySelector").mockImplementation((selector: string) => { - if (selector === "audio#errorAudio") { - return audioElement; - } - - return null; - }); - - mocked(createAudioContext).mockReturnValue(mockAudioContext as unknown as AudioContext); - }); - - afterEach(() => { - voiceBroadcastRecording?.off(VoiceBroadcastRecordingEvent.StateChanged, onStateChanged); - }); - - describe("when there is an info event without id", () => { - beforeEach(() => { - infoEvent = mkVoiceBroadcastInfoEvent({ - device_id: client.getDeviceId()!, - state: VoiceBroadcastInfoState.Started, - }); - jest.spyOn(infoEvent, "getId").mockReturnValue(undefined); - }); - - it("should raise an error when creating a broadcast", () => { - expect(() => { - setUpVoiceBroadcastRecording(); - }).toThrow("Cannot create broadcast for info event without Id."); - }); - }); - - describe("when there is an info event without room", () => { - beforeEach(() => { - infoEvent = mkVoiceBroadcastInfoEvent({ - device_id: client.getDeviceId()!, - state: VoiceBroadcastInfoState.Started, - }); - jest.spyOn(infoEvent, "getRoomId").mockReturnValue(undefined); - }); - - it("should raise an error when creating a broadcast", () => { - expect(() => { - setUpVoiceBroadcastRecording(); - }).toThrow(`Cannot create broadcast for unknown room (info event ${infoEvent.getId()})`); - }); - }); - - describe("when created for a Voice Broadcast Info without relations", () => { - beforeEach(() => { - infoEvent = mkVoiceBroadcastInfoEvent({ - device_id: client.getDeviceId()!, - state: VoiceBroadcastInfoState.Started, - }); - setUpVoiceBroadcastRecording(); - }); - - it("should be in Started state", () => { - expect(voiceBroadcastRecording.getState()).toBe(VoiceBroadcastInfoState.Started); - }); - - describe("and calling stop", () => { - beforeEach(() => { - voiceBroadcastRecording.stop(); - }); - - itShouldSendAnInfoEvent(VoiceBroadcastInfoState.Stopped, 0); - itShouldBeInState(VoiceBroadcastInfoState.Stopped); - - it("should emit a stopped state changed event", () => { - expect(onStateChanged).toHaveBeenCalledWith(VoiceBroadcastInfoState.Stopped); - }); - }); - - describe("and calling start", () => { - beforeEach(async () => { - await voiceBroadcastRecording.start(); - }); - - it("should start the recorder", () => { - expect(voiceBroadcastRecorder.start).toHaveBeenCalled(); - }); - - describe("and the info event is redacted", () => { - beforeEach(() => { - infoEvent.emit( - MatrixEventEvent.BeforeRedaction, - infoEvent, - mkEvent({ - event: true, - type: EventType.RoomRedaction, - user: client.getSafeUserId(), - content: {}, - }), - ); - }); - - itShouldBeInState(VoiceBroadcastInfoState.Stopped); - - it("should destroy the recording", () => { - expect(voiceBroadcastRecording.destroy).toHaveBeenCalled(); - }); - }); - - describe("and receiving a call action", () => { - beforeEach(() => { - dis.dispatch( - { - action: "call_state", - }, - true, - ); - }); - - itShouldBeInState(VoiceBroadcastInfoState.Paused); - }); - - describe("and a chunk time update occurs", () => { - beforeEach(() => { - voiceBroadcastRecorder.emit(VoiceBroadcastRecorderEvent.CurrentChunkLengthUpdated, 10); - }); - - it("should update time left", () => { - expect(voiceBroadcastRecording.getTimeLeft()).toBe(maxLength - 10); - expect(voiceBroadcastRecording.emit).toHaveBeenCalledWith( - VoiceBroadcastRecordingEvent.TimeLeftChanged, - maxLength - 10, - ); - }); - - describe("and a chunk time update occurs, that would increase time left", () => { - beforeEach(() => { - mocked(voiceBroadcastRecording.emit).mockClear(); - voiceBroadcastRecorder.emit(VoiceBroadcastRecorderEvent.CurrentChunkLengthUpdated, 5); - }); - - it("should not change time left", () => { - expect(voiceBroadcastRecording.getTimeLeft()).toBe(maxLength - 10); - expect(voiceBroadcastRecording.emit).not.toHaveBeenCalled(); - }); - }); - }); - - describe("and a chunk has been recorded", () => { - beforeEach(async () => { - emitFirsChunkRecorded(); - }); - - itShouldSendAVoiceMessage([1, 2, 3], 3, 23, 1); - - describe("and another chunk has been recorded, that exceeds the max time", () => { - beforeEach(() => { - mocked(voiceBroadcastRecorder.stop).mockResolvedValue({ - buffer: new Uint8Array([23, 24, 25]), - length: getMaxBroadcastLength(), - }); - voiceBroadcastRecorder.emit( - VoiceBroadcastRecorderEvent.CurrentChunkLengthUpdated, - getMaxBroadcastLength(), - ); - }); - - itShouldBeInState(VoiceBroadcastInfoState.Stopped); - itShouldSendAVoiceMessage([23, 24, 25], 3, getMaxBroadcastLength(), 2); - itShouldSendAnInfoEvent(VoiceBroadcastInfoState.Stopped, 2); - }); - }); - - describe("and calling stop", () => { - beforeEach(async () => { - mocked(voiceBroadcastRecorder.stop).mockResolvedValue({ - buffer: new Uint8Array([4, 5, 6]), - length: 42, - }); - await voiceBroadcastRecording.stop(); - }); - - itShouldSendAVoiceMessage([4, 5, 6], 3, 42, 1); - itShouldSendAnInfoEvent(VoiceBroadcastInfoState.Stopped, 1); - }); - - describe.each([ - ["pause", async () => voiceBroadcastRecording.pause()], - ["toggle", async () => voiceBroadcastRecording.toggle()], - ])("and calling %s", (_case: string, action: () => Promise) => { - beforeEach(async () => { - await action(); - }); - - itShouldBeInState(VoiceBroadcastInfoState.Paused); - itShouldSendAnInfoEvent(VoiceBroadcastInfoState.Paused, 0); - - it("should stop the recorder", () => { - expect(mocked(voiceBroadcastRecorder.stop)).toHaveBeenCalled(); - }); - - it("should emit a paused state changed event", () => { - expect(onStateChanged).toHaveBeenCalledWith(VoiceBroadcastInfoState.Paused); - }); - }); - - describe("and there is no connection", () => { - beforeEach(() => { - mocked(client.sendStateEvent).mockImplementation(() => { - throw new Error(); - }); - }); - - describe.each([ - ["pause", async () => voiceBroadcastRecording.pause()], - ["toggle", async () => voiceBroadcastRecording.toggle()], - ])("and calling %s", (_case: string, action: () => Promise) => { - beforeEach(async () => { - await action(); - }); - - itShouldBeInState("connection_error"); - - describe("and the connection is back", () => { - beforeEach(() => { - mocked(client.sendStateEvent).mockResolvedValue({ event_id: "e1" }); - client.emit(ClientEvent.Sync, SyncState.Catchup, SyncState.Error); - }); - - itShouldBeInState(VoiceBroadcastInfoState.Paused); - }); - }); - }); - - describe("and calling destroy", () => { - beforeEach(() => { - voiceBroadcastRecording.destroy(); - }); - - it("should stop the recorder and remove all listeners", () => { - expect(mocked(voiceBroadcastRecorder.stop)).toHaveBeenCalled(); - expect(mocked(voiceBroadcastRecorder.destroy)).toHaveBeenCalled(); - expect(mocked(voiceBroadcastRecording.removeAllListeners)).toHaveBeenCalled(); - }); - }); - - describe("and a chunk has been recorded and the upload fails", () => { - beforeEach(() => { - mocked(uploadFile).mockRejectedValue("Error"); - emitFirsChunkRecorded(); - }); - - itShouldBeInState("connection_error"); - - describe("and the connection is back", () => { - beforeEach(() => { - setUpUploadFileMock(); - client.emit(ClientEvent.Sync, SyncState.Catchup, SyncState.Error); - }); - - itShouldBeInState(VoiceBroadcastInfoState.Paused); - itShouldSendAVoiceMessage([1, 2, 3], 3, 23, 1); - }); - }); - - describe("and audible notifications are disabled", () => { - beforeEach(() => { - const notificationSettings = mkEvent({ - event: true, - type: `${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${client.getDeviceId()}`, - user: client.getSafeUserId(), - content: { - is_silenced: true, - }, - }); - mocked(client.getAccountData).mockReturnValue(notificationSettings); - }); - - describe("and a chunk has been recorded and sending the voice message fails", () => { - beforeEach(() => { - mocked(client.sendMessage).mockRejectedValue("Error"); - emitFirsChunkRecorded(); - }); - - itShouldBeInState("connection_error"); - - it("should not play a notification", () => { - expect(audioElement.play).not.toHaveBeenCalled(); - }); - }); - }); - - describe("and a chunk has been recorded and sending the voice message fails", () => { - beforeEach(() => { - mocked(client.sendMessage).mockRejectedValue("Error"); - emitFirsChunkRecorded(); - fetchMock.get("media/error.mp3", 200); - }); - - itShouldBeInState("connection_error"); - - it("should play a notification", () => { - expect(mockAudioBufferSourceNode.start).toHaveBeenCalled(); - }); - - describe("and the connection is back", () => { - beforeEach(() => { - mocked(client.sendMessage).mockClear(); - mocked(client.sendMessage).mockResolvedValue({ event_id: "e23" }); - client.emit(ClientEvent.Sync, SyncState.Catchup, SyncState.Error); - }); - - itShouldBeInState(VoiceBroadcastInfoState.Paused); - itShouldSendAVoiceMessage([1, 2, 3], 3, 23, 1); - }); - }); - }); - - describe("and it is in paused state", () => { - beforeEach(async () => { - await voiceBroadcastRecording.pause(); - }); - - describe.each([ - ["resume", async () => voiceBroadcastRecording.resume()], - ["toggle", async () => voiceBroadcastRecording.toggle()], - ])("and calling %s", (_case: string, action: () => Promise) => { - beforeEach(async () => { - await action(); - }); - - itShouldBeInState(VoiceBroadcastInfoState.Resumed); - itShouldSendAnInfoEvent(VoiceBroadcastInfoState.Resumed, 0); - - it("should start the recorder", () => { - expect(mocked(voiceBroadcastRecorder.start)).toHaveBeenCalled(); - }); - - it(`should emit a ${VoiceBroadcastInfoState.Resumed} state changed event`, () => { - expect(onStateChanged).toHaveBeenCalledWith(VoiceBroadcastInfoState.Resumed); - }); - }); - }); - }); - - describe("when created for a Voice Broadcast Info with a Stopped relation", () => { - beforeEach(() => { - infoEvent = mkVoiceBroadcastInfoEvent({ - device_id: client.getDeviceId()!, - state: VoiceBroadcastInfoState.Started, - chunk_length: 120, - }); - - const relationsContainer = { - getRelations: jest.fn(), - } as unknown as Relations; - mocked(relationsContainer.getRelations).mockReturnValue([ - mkVoiceBroadcastInfoEvent({ - device_id: client.getDeviceId()!, - state: VoiceBroadcastInfoState.Stopped, - ["m.relates_to"]: { - rel_type: RelationType.Reference, - event_id: infoEvent.getId()!, - }, - }), - ]); - - const timelineSet = { - relations: { - getChildEventsForEvent: jest - .fn() - .mockImplementation( - (eventId: string, relationType: RelationType | string, eventType: EventType | string) => { - if ( - eventId === infoEvent.getId() && - relationType === RelationType.Reference && - eventType === VoiceBroadcastInfoEventType - ) { - return relationsContainer; - } - }, - ), - }, - } as unknown as EventTimelineSet; - mocked(room.getUnfilteredTimelineSet).mockReturnValue(timelineSet); - - setUpVoiceBroadcastRecording(); - }); - - it("should be in Stopped state", () => { - expect(voiceBroadcastRecording.getState()).toBe(VoiceBroadcastInfoState.Stopped); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/stores/VoiceBroadcastPlaybacksStore-test.ts b/test/unit-tests/voice-broadcast/stores/VoiceBroadcastPlaybacksStore-test.ts deleted file mode 100644 index 29026345cf..0000000000 --- a/test/unit-tests/voice-broadcast/stores/VoiceBroadcastPlaybacksStore-test.ts +++ /dev/null @@ -1,162 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { mocked } from "jest-mock"; -import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; - -import { - VoiceBroadcastInfoState, - VoiceBroadcastPlayback, - VoiceBroadcastPlaybackEvent, - VoiceBroadcastPlaybacksStore, - VoiceBroadcastPlaybacksStoreEvent, - VoiceBroadcastPlaybackState, - VoiceBroadcastRecordingsStore, -} from "../../../../src/voice-broadcast"; -import { mkStubRoom, stubClient } from "../../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils"; - -describe("VoiceBroadcastPlaybacksStore", () => { - const roomId = "!room:example.com"; - let client: MatrixClient; - let userId: string; - let deviceId: string; - let room: Room; - let infoEvent1: MatrixEvent; - let infoEvent2: MatrixEvent; - let playback1: VoiceBroadcastPlayback; - let playback2: VoiceBroadcastPlayback; - let playbacks: VoiceBroadcastPlaybacksStore; - let onCurrentChanged: (playback: VoiceBroadcastPlayback | null) => void; - - beforeEach(() => { - client = stubClient(); - userId = client.getUserId() || ""; - deviceId = client.getDeviceId() || ""; - mocked(client.relations).mockClear(); - mocked(client.relations).mockResolvedValue({ events: [] }); - - room = mkStubRoom(roomId, "test room", client); - mocked(client.getRoom).mockImplementation((roomId: string): Room | null => { - if (roomId === room.roomId) { - return room; - } - - return null; - }); - - infoEvent1 = mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Started, userId, deviceId); - infoEvent2 = mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Started, userId, deviceId); - const recordings = new VoiceBroadcastRecordingsStore(); - playback1 = new VoiceBroadcastPlayback(infoEvent1, client, recordings); - jest.spyOn(playback1, "off"); - playback2 = new VoiceBroadcastPlayback(infoEvent2, client, recordings); - jest.spyOn(playback2, "off"); - - playbacks = new VoiceBroadcastPlaybacksStore(recordings); - jest.spyOn(playbacks, "removeAllListeners"); - onCurrentChanged = jest.fn(); - playbacks.on(VoiceBroadcastPlaybacksStoreEvent.CurrentChanged, onCurrentChanged); - }); - - afterEach(() => { - playbacks.off(VoiceBroadcastPlaybacksStoreEvent.CurrentChanged, onCurrentChanged); - }); - - describe("when setting a current Voice Broadcast playback", () => { - beforeEach(() => { - playbacks.setCurrent(playback1); - }); - - it("should return it as current", () => { - expect(playbacks.getCurrent()).toBe(playback1); - }); - - it("should return it by id", () => { - expect(playbacks.getByInfoEvent(infoEvent1, client)).toBe(playback1); - }); - - it("should emit a CurrentChanged event", () => { - expect(onCurrentChanged).toHaveBeenCalledWith(playback1); - }); - - describe("and setting the same again", () => { - beforeEach(() => { - mocked(onCurrentChanged).mockClear(); - playbacks.setCurrent(playback1); - }); - - it("should not emit a CurrentChanged event", () => { - expect(onCurrentChanged).not.toHaveBeenCalled(); - }); - }); - - describe("and setting another playback and start both", () => { - beforeEach(() => { - playbacks.setCurrent(playback2); - playback1.start(); - playback2.start(); - }); - - it("should set playback1 to paused", () => { - expect(playback1.getState()).toBe(VoiceBroadcastPlaybackState.Paused); - }); - - it("should set playback2 to buffering", () => { - // buffering because there are no chunks, yet - expect(playback2.getState()).toBe(VoiceBroadcastPlaybackState.Buffering); - }); - - describe("and calling destroy", () => { - beforeEach(() => { - playbacks.destroy(); - }); - - it("should remove all listeners", () => { - expect(playbacks.removeAllListeners).toHaveBeenCalled(); - }); - - it("should deregister the listeners on the playbacks", () => { - expect(playback1.off).toHaveBeenCalledWith( - VoiceBroadcastPlaybackEvent.StateChanged, - expect.any(Function), - ); - expect(playback2.off).toHaveBeenCalledWith( - VoiceBroadcastPlaybackEvent.StateChanged, - expect.any(Function), - ); - }); - }); - }); - }); - - describe("getByInfoEventId", () => { - let returnedPlayback: VoiceBroadcastPlayback; - - describe("when retrieving a known playback", () => { - beforeEach(() => { - playbacks.setCurrent(playback1); - returnedPlayback = playbacks.getByInfoEvent(infoEvent1, client); - }); - - it("should return the playback", () => { - expect(returnedPlayback).toBe(playback1); - }); - }); - - describe("when retrieving an unknown playback", () => { - beforeEach(() => { - returnedPlayback = playbacks.getByInfoEvent(infoEvent1, client); - }); - - it("should return the playback", () => { - expect(returnedPlayback.infoEvent).toBe(infoEvent1); - }); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/stores/VoiceBroadcastPreRecordingStore-test.ts b/test/unit-tests/voice-broadcast/stores/VoiceBroadcastPreRecordingStore-test.ts deleted file mode 100644 index 53d3a4c0f2..0000000000 --- a/test/unit-tests/voice-broadcast/stores/VoiceBroadcastPreRecordingStore-test.ts +++ /dev/null @@ -1,130 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { mocked } from "jest-mock"; -import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix"; - -import { - VoiceBroadcastPlaybacksStore, - VoiceBroadcastPreRecording, - VoiceBroadcastPreRecordingStore, - VoiceBroadcastRecordingsStore, -} from "../../../../src/voice-broadcast"; -import { stubClient } from "../../../test-utils"; - -describe("VoiceBroadcastPreRecordingStore", () => { - const roomId = "!room:example.com"; - let client: MatrixClient; - let room: Room; - let sender: RoomMember; - let playbacksStore: VoiceBroadcastPlaybacksStore; - let recordingsStore: VoiceBroadcastRecordingsStore; - let store: VoiceBroadcastPreRecordingStore; - let preRecording1: VoiceBroadcastPreRecording; - - beforeAll(() => { - client = stubClient(); - room = new Room(roomId, client, client.getUserId() || ""); - sender = new RoomMember(roomId, client.getUserId() || ""); - recordingsStore = new VoiceBroadcastRecordingsStore(); - playbacksStore = new VoiceBroadcastPlaybacksStore(recordingsStore); - }); - - beforeEach(() => { - store = new VoiceBroadcastPreRecordingStore(); - jest.spyOn(store, "emit"); - jest.spyOn(store, "removeAllListeners"); - preRecording1 = new VoiceBroadcastPreRecording(room, sender, client, playbacksStore, recordingsStore); - jest.spyOn(preRecording1, "off"); - }); - - it("getCurrent() should return null", () => { - expect(store.getCurrent()).toBeNull(); - }); - - it("clearCurrent() should work", () => { - store.clearCurrent(); - expect(store.getCurrent()).toBeNull(); - }); - - describe("when setting a current recording", () => { - beforeEach(() => { - store.setCurrent(preRecording1); - }); - - it("getCurrent() should return the recording", () => { - expect(store.getCurrent()).toBe(preRecording1); - }); - - it("should emit a changed event with the recording", () => { - expect(store.emit).toHaveBeenCalledWith("changed", preRecording1); - }); - - describe("and calling destroy()", () => { - beforeEach(() => { - store.destroy(); - }); - - it("should remove all listeners", () => { - expect(store.removeAllListeners).toHaveBeenCalled(); - }); - - it("should deregister from the pre-recordings", () => { - expect(preRecording1.off).toHaveBeenCalledWith("dismiss", expect.any(Function)); - }); - }); - - describe("and cancelling the pre-recording", () => { - beforeEach(() => { - preRecording1.cancel(); - }); - - it("should clear the current recording", () => { - expect(store.getCurrent()).toBeNull(); - }); - - it("should emit a changed event with null", () => { - expect(store.emit).toHaveBeenCalledWith("changed", null); - }); - }); - - describe("and setting the same pre-recording again", () => { - beforeEach(() => { - mocked(store.emit).mockClear(); - store.setCurrent(preRecording1); - }); - - it("should not emit a changed event", () => { - expect(store.emit).not.toHaveBeenCalled(); - }); - }); - - describe("and setting another pre-recording", () => { - let preRecording2: VoiceBroadcastPreRecording; - - beforeEach(() => { - mocked(store.emit).mockClear(); - mocked(preRecording1.off).mockClear(); - preRecording2 = new VoiceBroadcastPreRecording(room, sender, client, playbacksStore, recordingsStore); - store.setCurrent(preRecording2); - }); - - it("should deregister from the current pre-recording", () => { - expect(preRecording1.off).toHaveBeenCalledWith("dismiss", expect.any(Function)); - }); - - it("getCurrent() should return the new recording", () => { - expect(store.getCurrent()).toBe(preRecording2); - }); - - it("should emit a changed event with the new recording", () => { - expect(store.emit).toHaveBeenCalledWith("changed", preRecording2); - }); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/stores/VoiceBroadcastRecordingsStore-test.ts b/test/unit-tests/voice-broadcast/stores/VoiceBroadcastRecordingsStore-test.ts deleted file mode 100644 index 97b756fce1..0000000000 --- a/test/unit-tests/voice-broadcast/stores/VoiceBroadcastRecordingsStore-test.ts +++ /dev/null @@ -1,167 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { mocked } from "jest-mock"; -import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; - -import { - VoiceBroadcastRecordingsStore, - VoiceBroadcastRecordingsStoreEvent, - VoiceBroadcastRecording, - VoiceBroadcastInfoState, -} from "../../../../src/voice-broadcast"; -import { mkStubRoom, stubClient } from "../../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils"; - -describe("VoiceBroadcastRecordingsStore", () => { - const roomId = "!room:example.com"; - let client: MatrixClient; - let room: Room; - let infoEvent: MatrixEvent; - let otherInfoEvent: MatrixEvent; - let recording: VoiceBroadcastRecording; - let otherRecording: VoiceBroadcastRecording; - let recordings: VoiceBroadcastRecordingsStore; - let onCurrentChanged: (recording: VoiceBroadcastRecording | null) => void; - - beforeEach(() => { - client = stubClient(); - room = mkStubRoom(roomId, "test room", client); - mocked(client.getRoom).mockImplementation((roomId: string) => { - if (roomId === room.roomId) { - return room; - } - return null; - }); - infoEvent = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Started, - client.getUserId()!, - client.getDeviceId()!, - ); - otherInfoEvent = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Started, - client.getUserId()!, - client.getDeviceId()!, - ); - recording = new VoiceBroadcastRecording(infoEvent, client); - otherRecording = new VoiceBroadcastRecording(otherInfoEvent, client); - recordings = new VoiceBroadcastRecordingsStore(); - onCurrentChanged = jest.fn(); - recordings.on(VoiceBroadcastRecordingsStoreEvent.CurrentChanged, onCurrentChanged); - }); - - afterEach(() => { - recording.destroy(); - recordings.off(VoiceBroadcastRecordingsStoreEvent.CurrentChanged, onCurrentChanged); - }); - - it("when setting a recording without info event Id, it should raise an error", () => { - infoEvent.event.event_id = undefined; - expect(() => { - recordings.setCurrent(recording); - }).toThrow("Got broadcast info event without Id"); - }); - - describe("when setting a current Voice Broadcast recording", () => { - beforeEach(() => { - recordings.setCurrent(recording); - }); - - it("should return it as current", () => { - expect(recordings.hasCurrent()).toBe(true); - expect(recordings.getCurrent()).toBe(recording); - }); - - it("should return it by id", () => { - expect(recordings.getByInfoEvent(infoEvent, client)).toBe(recording); - }); - - it("should emit a CurrentChanged event", () => { - expect(onCurrentChanged).toHaveBeenCalledWith(recording); - }); - - describe("and setting the same again", () => { - beforeEach(() => { - mocked(onCurrentChanged).mockClear(); - recordings.setCurrent(recording); - }); - - it("should not emit a CurrentChanged event", () => { - expect(onCurrentChanged).not.toHaveBeenCalled(); - }); - }); - - describe("and calling clearCurrent()", () => { - beforeEach(() => { - recordings.clearCurrent(); - }); - - it("should clear the current recording", () => { - expect(recordings.hasCurrent()).toBe(false); - expect(recordings.getCurrent()).toBeNull(); - }); - - it("should emit a current changed event", () => { - expect(onCurrentChanged).toHaveBeenCalledWith(null); - }); - - it("and calling it again should work", () => { - recordings.clearCurrent(); - expect(recordings.getCurrent()).toBeNull(); - }); - }); - - describe("and setting another recording and stopping the previous recording", () => { - beforeEach(() => { - recordings.setCurrent(otherRecording); - recording.stop(); - }); - - it("should keep the current recording", () => { - expect(recordings.getCurrent()).toBe(otherRecording); - }); - }); - - describe("and the recording stops", () => { - beforeEach(() => { - recording.stop(); - }); - - it("should clear the current recording", () => { - expect(recordings.getCurrent()).toBeNull(); - }); - }); - }); - - describe("getByInfoEventId", () => { - let returnedRecording: VoiceBroadcastRecording; - - describe("when retrieving a known recording", () => { - beforeEach(() => { - recordings.setCurrent(recording); - returnedRecording = recordings.getByInfoEvent(infoEvent, client); - }); - - it("should return the recording", () => { - expect(returnedRecording).toBe(recording); - }); - }); - - describe("when retrieving an unknown recording", () => { - beforeEach(() => { - returnedRecording = recordings.getByInfoEvent(infoEvent, client); - }); - - it("should return the recording", () => { - expect(returnedRecording.infoEvent).toBe(infoEvent); - }); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/utils/VoiceBroadcastChunkEvents-test.ts b/test/unit-tests/voice-broadcast/utils/VoiceBroadcastChunkEvents-test.ts deleted file mode 100644 index 817538ef17..0000000000 --- a/test/unit-tests/voice-broadcast/utils/VoiceBroadcastChunkEvents-test.ts +++ /dev/null @@ -1,142 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixEvent } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastChunkEvents } from "../../../../src/voice-broadcast/utils/VoiceBroadcastChunkEvents"; -import { mkVoiceBroadcastChunkEvent } from "./test-utils"; - -describe("VoiceBroadcastChunkEvents", () => { - const userId = "@user:example.com"; - const roomId = "!room:example.com"; - const txnId = "txn-id"; - let eventSeq1Time1: MatrixEvent; - let eventSeq2Time4: MatrixEvent; - let eventSeq3Time2: MatrixEvent; - let eventSeq3Time2T: MatrixEvent; - let eventSeq4Time1: MatrixEvent; - let eventSeqUTime3: MatrixEvent; - let eventSeq2Time4Dup: MatrixEvent; - let chunkEvents: VoiceBroadcastChunkEvents; - - beforeEach(() => { - eventSeq1Time1 = mkVoiceBroadcastChunkEvent("info1", userId, roomId, 7, 1, 1); - eventSeq2Time4 = mkVoiceBroadcastChunkEvent("info1", userId, roomId, 23, 2, 4); - eventSeq2Time4Dup = mkVoiceBroadcastChunkEvent("info1", userId, roomId, 3141, 2, 4); - jest.spyOn(eventSeq2Time4Dup, "getId").mockReturnValue(eventSeq2Time4.getId()); - eventSeq3Time2 = mkVoiceBroadcastChunkEvent("info1", userId, roomId, 42, 3, 2); - eventSeq3Time2.setTxnId(txnId); - eventSeq3Time2T = mkVoiceBroadcastChunkEvent("info1", userId, roomId, 42, 3, 2); - eventSeq3Time2T.setTxnId(txnId); - eventSeq4Time1 = mkVoiceBroadcastChunkEvent("info1", userId, roomId, 69, 4, 1); - eventSeqUTime3 = mkVoiceBroadcastChunkEvent("info1", userId, roomId, 314, undefined, 3); - chunkEvents = new VoiceBroadcastChunkEvents(); - }); - - describe("when adding events that all have a sequence", () => { - beforeEach(() => { - chunkEvents.addEvent(eventSeq2Time4); - chunkEvents.addEvent(eventSeq1Time1); - chunkEvents.addEvents([eventSeq4Time1, eventSeq2Time4Dup, eventSeq3Time2]); - }); - - it("should provide the events sort by sequence", () => { - expect(chunkEvents.getEvents()).toEqual([ - eventSeq1Time1, - eventSeq2Time4Dup, - eventSeq3Time2, - eventSeq4Time1, - ]); - }); - - it("getNumberOfEvents should return 4", () => { - expect(chunkEvents.getNumberOfEvents()).toBe(4); - }); - - it("getLength should return the total length of all chunks", () => { - expect(chunkEvents.getLength()).toBe(3259); - }); - - it("getLengthTo(first event) should return 0", () => { - expect(chunkEvents.getLengthTo(eventSeq1Time1)).toBe(0); - }); - - it("getLengthTo(some event) should return the time excl. that event", () => { - expect(chunkEvents.getLengthTo(eventSeq3Time2)).toBe(7 + 3141); - }); - - it("getLengthTo(last event) should return the time excl. that event", () => { - expect(chunkEvents.getLengthTo(eventSeq4Time1)).toBe(7 + 3141 + 42); - }); - - it("should return the expected next chunk", () => { - expect(chunkEvents.getNext(eventSeq2Time4Dup)).toBe(eventSeq3Time2); - }); - - it("should return undefined for next last chunk", () => { - expect(chunkEvents.getNext(eventSeq4Time1)).toBeUndefined(); - }); - - it("findByTime(0) should return the first chunk", () => { - expect(chunkEvents.findByTime(0)).toBe(eventSeq1Time1); - }); - - it("findByTime(some time) should return the chunk with this time", () => { - expect(chunkEvents.findByTime(7 + 3141 + 21)).toBe(eventSeq3Time2); - }); - - it("findByTime(entire duration) should return the last chunk", () => { - expect(chunkEvents.findByTime(7 + 3141 + 42 + 69)).toBe(eventSeq4Time1); - }); - - describe("and adding an event with a known transaction Id", () => { - beforeEach(() => { - chunkEvents.addEvent(eventSeq3Time2T); - }); - - it("should replace the previous event", () => { - expect(chunkEvents.getEvents()).toEqual([ - eventSeq1Time1, - eventSeq2Time4Dup, - eventSeq3Time2T, - eventSeq4Time1, - ]); - expect(chunkEvents.getNumberOfEvents()).toBe(4); - }); - }); - }); - - describe("when adding events where at least one does not have a sequence", () => { - beforeEach(() => { - chunkEvents.addEvent(eventSeq2Time4); - chunkEvents.addEvent(eventSeq1Time1); - chunkEvents.addEvents([eventSeq4Time1, eventSeqUTime3, eventSeq2Time4Dup, eventSeq3Time2]); - }); - - it("should provide the events sort by timestamp without duplicates", () => { - expect(chunkEvents.getEvents()).toEqual([ - eventSeq1Time1, - eventSeq4Time1, - eventSeq3Time2, - eventSeqUTime3, - eventSeq2Time4Dup, - ]); - expect(chunkEvents.getNumberOfEvents()).toBe(5); - }); - - describe("getSequenceForEvent", () => { - it("should return the sequence if provided by the event", () => { - expect(chunkEvents.getSequenceForEvent(eventSeq3Time2)).toBe(3); - }); - - it("should return the index if no sequence provided by event", () => { - expect(chunkEvents.getSequenceForEvent(eventSeqUTime3)).toBe(4); - }); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/utils/VoiceBroadcastResumer-test.ts b/test/unit-tests/voice-broadcast/utils/VoiceBroadcastResumer-test.ts deleted file mode 100644 index 81bcce5f3f..0000000000 --- a/test/unit-tests/voice-broadcast/utils/VoiceBroadcastResumer-test.ts +++ /dev/null @@ -1,179 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { mocked } from "jest-mock"; -import { ClientEvent, MatrixClient, MatrixEvent, RelationType, Room, SyncState } from "matrix-js-sdk/src/matrix"; - -import { - VoiceBroadcastInfoEventContent, - VoiceBroadcastInfoEventType, - VoiceBroadcastInfoState, - VoiceBroadcastResumer, -} from "../../../../src/voice-broadcast"; -import { stubClient } from "../../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "./test-utils"; - -describe("VoiceBroadcastResumer", () => { - const roomId = "!room:example.com"; - let client: MatrixClient; - let room: Room; - let resumer: VoiceBroadcastResumer; - let startedInfoEvent: MatrixEvent; - let pausedInfoEvent: MatrixEvent; - - const itShouldNotSendAStateEvent = (): void => { - it("should not send a state event", () => { - expect(client.sendStateEvent).not.toHaveBeenCalled(); - }); - }; - - const itShouldSendAStoppedStateEvent = (): void => { - it("should send a stopped state event", () => { - expect(client.sendStateEvent).toHaveBeenCalledWith( - startedInfoEvent.getRoomId(), - VoiceBroadcastInfoEventType, - { - "device_id": client.getDeviceId(), - "state": VoiceBroadcastInfoState.Stopped, - "m.relates_to": { - rel_type: RelationType.Reference, - event_id: startedInfoEvent.getId(), - }, - } as VoiceBroadcastInfoEventContent, - client.getUserId()!, - ); - }); - }; - - const itShouldDeregisterFromTheClient = () => { - it("should deregister from the client", () => { - expect(client.off).toHaveBeenCalledWith(ClientEvent.Sync, expect.any(Function)); - }); - }; - - beforeEach(() => { - client = stubClient(); - jest.spyOn(client, "off"); - room = new Room(roomId, client, client.getUserId()!); - mocked(client.getRoom).mockImplementation((getRoomId: string | undefined) => { - if (getRoomId === roomId) return room; - - return null; - }); - mocked(client.getRooms).mockReturnValue([room]); - startedInfoEvent = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Started, - client.getUserId()!, - client.getDeviceId()!, - ); - pausedInfoEvent = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Paused, - client.getUserId()!, - client.getDeviceId()!, - startedInfoEvent, - ); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe("when the initial sync is completed", () => { - beforeEach(() => { - mocked(client.isInitialSyncComplete).mockReturnValue(true); - }); - - describe("and there is no info event", () => { - beforeEach(() => { - resumer = new VoiceBroadcastResumer(client); - }); - - itShouldNotSendAStateEvent(); - - describe("and calling destroy", () => { - beforeEach(() => { - resumer.destroy(); - }); - - itShouldDeregisterFromTheClient(); - }); - }); - - describe("and there is a started info event", () => { - beforeEach(() => { - room.currentState.setStateEvents([startedInfoEvent]); - }); - - describe("and the client knows about the user and device", () => { - beforeEach(() => { - resumer = new VoiceBroadcastResumer(client); - }); - - itShouldSendAStoppedStateEvent(); - }); - - describe("and the client doesn't know about the user", () => { - beforeEach(() => { - mocked(client.getUserId).mockReturnValue(null); - resumer = new VoiceBroadcastResumer(client); - }); - - itShouldNotSendAStateEvent(); - }); - - describe("and the client doesn't know about the device", () => { - beforeEach(() => { - mocked(client.getDeviceId).mockReturnValue(null); - resumer = new VoiceBroadcastResumer(client); - }); - - itShouldNotSendAStateEvent(); - }); - }); - - describe("and there is a paused info event", () => { - beforeEach(() => { - room.currentState.setStateEvents([pausedInfoEvent]); - resumer = new VoiceBroadcastResumer(client); - }); - - itShouldSendAStoppedStateEvent(); - }); - }); - - describe("when the initial sync is not completed", () => { - beforeEach(() => { - room.currentState.setStateEvents([pausedInfoEvent]); - mocked(client.isInitialSyncComplete).mockReturnValue(false); - mocked(client.getSyncState).mockReturnValue(SyncState.Prepared); - resumer = new VoiceBroadcastResumer(client); - }); - - itShouldNotSendAStateEvent(); - - describe("and a sync event appears", () => { - beforeEach(() => { - client.emit(ClientEvent.Sync, SyncState.Prepared, SyncState.Stopped); - }); - - itShouldNotSendAStateEvent(); - - describe("and the initial sync completed and a sync event appears", () => { - beforeEach(() => { - mocked(client.getSyncState).mockReturnValue(SyncState.Syncing); - client.emit(ClientEvent.Sync, SyncState.Syncing, SyncState.Prepared); - }); - - itShouldSendAStoppedStateEvent(); - itShouldDeregisterFromTheClient(); - }); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/utils/__snapshots__/setUpVoiceBroadcastPreRecording-test.ts.snap b/test/unit-tests/voice-broadcast/utils/__snapshots__/setUpVoiceBroadcastPreRecording-test.ts.snap deleted file mode 100644 index 2d6ba0a409..0000000000 --- a/test/unit-tests/voice-broadcast/utils/__snapshots__/setUpVoiceBroadcastPreRecording-test.ts.snap +++ /dev/null @@ -1,24 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`setUpVoiceBroadcastPreRecording when trying to start a broadcast if there is no connection should show an info dialog and not set up a pre-recording 1`] = ` -[MockFunction] { - "calls": [ - [ - [Function], - { - "description":

- Unfortunately we're unable to start a recording right now. Please try again later. -

, - "hasCloseButton": true, - "title": "Connection error", - }, - ], - ], - "results": [ - { - "type": "return", - "value": undefined, - }, - ], -} -`; diff --git a/test/unit-tests/voice-broadcast/utils/__snapshots__/startNewVoiceBroadcastRecording-test.ts.snap b/test/unit-tests/voice-broadcast/utils/__snapshots__/startNewVoiceBroadcastRecording-test.ts.snap deleted file mode 100644 index dd5aa15305..0000000000 --- a/test/unit-tests/voice-broadcast/utils/__snapshots__/startNewVoiceBroadcastRecording-test.ts.snap +++ /dev/null @@ -1,116 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`startNewVoiceBroadcastRecording when the current user is allowed to send voice broadcast info state events when there already is a live broadcast of another user should show an info dialog 1`] = ` -[MockFunction] { - "calls": [ - [ - [Function], - { - "description":

- Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one. -

, - "hasCloseButton": true, - "title": "Can't start a new voice broadcast", - }, - ], - ], - "results": [ - { - "type": "return", - "value": undefined, - }, - ], -} -`; - -exports[`startNewVoiceBroadcastRecording when the current user is allowed to send voice broadcast info state events when there already is a live broadcast of the current user in the room should show an info dialog 1`] = ` -[MockFunction] { - "calls": [ - [ - [Function], - { - "description":

- You are already recording a voice broadcast. Please end your current voice broadcast to start a new one. -

, - "hasCloseButton": true, - "title": "Can't start a new voice broadcast", - }, - ], - ], - "results": [ - { - "type": "return", - "value": undefined, - }, - ], -} -`; - -exports[`startNewVoiceBroadcastRecording when the current user is allowed to send voice broadcast info state events when there is already a current voice broadcast should show an info dialog 1`] = ` -[MockFunction] { - "calls": [ - [ - [Function], - { - "description":

- You are already recording a voice broadcast. Please end your current voice broadcast to start a new one. -

, - "hasCloseButton": true, - "title": "Can't start a new voice broadcast", - }, - ], - ], - "results": [ - { - "type": "return", - "value": undefined, - }, - ], -} -`; - -exports[`startNewVoiceBroadcastRecording when the current user is not allowed to send voice broadcast info state events should show an info dialog 1`] = ` -[MockFunction] { - "calls": [ - [ - [Function], - { - "description":

- You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions. -

, - "hasCloseButton": true, - "title": "Can't start a new voice broadcast", - }, - ], - ], - "results": [ - { - "type": "return", - "value": undefined, - }, - ], -} -`; - -exports[`startNewVoiceBroadcastRecording when trying to start a broadcast if there is no connection should show an info dialog and not start a recording 1`] = ` -[MockFunction] { - "calls": [ - [ - [Function], - { - "description":

- Unfortunately we're unable to start a recording right now. Please try again later. -

, - "hasCloseButton": true, - "title": "Connection error", - }, - ], - ], - "results": [ - { - "type": "return", - "value": undefined, - }, - ], -} -`; diff --git a/test/unit-tests/voice-broadcast/utils/__snapshots__/textForVoiceBroadcastStoppedEvent-test.tsx.snap b/test/unit-tests/voice-broadcast/utils/__snapshots__/textForVoiceBroadcastStoppedEvent-test.tsx.snap deleted file mode 100644 index cf1e93db13..0000000000 --- a/test/unit-tests/voice-broadcast/utils/__snapshots__/textForVoiceBroadcastStoppedEvent-test.tsx.snap +++ /dev/null @@ -1,42 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`textForVoiceBroadcastStoppedEvent should render other users broadcast as expected 1`] = ` -
-
- @other:example.com ended a voice broadcast -
-
-`; - -exports[`textForVoiceBroadcastStoppedEvent should render own broadcast as expected 1`] = ` -
-
- You ended a voice broadcast -
-
-`; - -exports[`textForVoiceBroadcastStoppedEvent should render without login as expected 1`] = ` -
-
- @other:example.com ended a voice broadcast -
-
-`; - -exports[`textForVoiceBroadcastStoppedEvent when rendering an event with relation to the start event should render events with relation to the start event 1`] = ` -
-
- - You ended a - - -
-
-`; diff --git a/test/unit-tests/voice-broadcast/utils/cleanUpBroadcasts-test.ts b/test/unit-tests/voice-broadcast/utils/cleanUpBroadcasts-test.ts deleted file mode 100644 index ebf0de390c..0000000000 --- a/test/unit-tests/voice-broadcast/utils/cleanUpBroadcasts-test.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { - cleanUpBroadcasts, - VoiceBroadcastPlayback, - VoiceBroadcastPreRecording, - VoiceBroadcastRecording, -} from "../../../../src/voice-broadcast"; -import { stubClient } from "../../../test-utils"; -import { TestSdkContext } from "../../TestSdkContext"; -import { mkVoiceBroadcastPlayback, mkVoiceBroadcastPreRecording, mkVoiceBroadcastRecording } from "./test-utils"; - -describe("cleanUpBroadcasts", () => { - let playback: VoiceBroadcastPlayback; - let recording: VoiceBroadcastRecording; - let preRecording: VoiceBroadcastPreRecording; - let stores: TestSdkContext; - - beforeEach(() => { - stores = new TestSdkContext(); - stores.client = stubClient(); - - playback = mkVoiceBroadcastPlayback(stores); - jest.spyOn(playback, "stop").mockReturnValue(); - stores.voiceBroadcastPlaybacksStore.setCurrent(playback); - - recording = mkVoiceBroadcastRecording(stores); - jest.spyOn(recording, "stop").mockResolvedValue(); - stores.voiceBroadcastRecordingsStore.setCurrent(recording); - - preRecording = mkVoiceBroadcastPreRecording(stores); - jest.spyOn(preRecording, "cancel").mockReturnValue(); - stores.voiceBroadcastPreRecordingStore.setCurrent(preRecording); - }); - - it("should stop and clear all broadcast related stuff", async () => { - await cleanUpBroadcasts(stores); - expect(playback.stop).toHaveBeenCalled(); - expect(stores.voiceBroadcastPlaybacksStore.getCurrent()).toBeNull(); - expect(recording.stop).toHaveBeenCalled(); - expect(stores.voiceBroadcastRecordingsStore.getCurrent()).toBeNull(); - expect(preRecording.cancel).toHaveBeenCalled(); - expect(stores.voiceBroadcastPreRecordingStore.getCurrent()).toBeNull(); - }); -}); diff --git a/test/unit-tests/voice-broadcast/utils/determineVoiceBroadcastLiveness-test.ts b/test/unit-tests/voice-broadcast/utils/determineVoiceBroadcastLiveness-test.ts deleted file mode 100644 index c1a9b2db13..0000000000 --- a/test/unit-tests/voice-broadcast/utils/determineVoiceBroadcastLiveness-test.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { VoiceBroadcastInfoState, VoiceBroadcastLiveness } from "../../../../src/voice-broadcast"; -import { determineVoiceBroadcastLiveness } from "../../../../src/voice-broadcast/utils/determineVoiceBroadcastLiveness"; - -const testData: Array<{ state: VoiceBroadcastInfoState; expected: VoiceBroadcastLiveness }> = [ - { state: VoiceBroadcastInfoState.Started, expected: "live" }, - { state: VoiceBroadcastInfoState.Resumed, expected: "live" }, - { state: VoiceBroadcastInfoState.Paused, expected: "grey" }, - { state: VoiceBroadcastInfoState.Stopped, expected: "not-live" }, -]; - -describe("determineVoiceBroadcastLiveness", () => { - it.each(testData)("should return the expected value for a %s broadcast", ({ state, expected }) => { - expect(determineVoiceBroadcastLiveness(state)).toBe(expected); - }); - - it("should return »non-live« for an undefined state", () => { - // @ts-ignore - expect(determineVoiceBroadcastLiveness(undefined)).toBe("not-live"); - }); - - it("should return »non-live« for an unknown state", () => { - // @ts-ignore - expect(determineVoiceBroadcastLiveness("unknown test state")).toBe("not-live"); - }); -}); diff --git a/test/unit-tests/voice-broadcast/utils/findRoomLiveVoiceBroadcastFromUserAndDevice-test.ts b/test/unit-tests/voice-broadcast/utils/findRoomLiveVoiceBroadcastFromUserAndDevice-test.ts deleted file mode 100644 index 3120e2b517..0000000000 --- a/test/unit-tests/voice-broadcast/utils/findRoomLiveVoiceBroadcastFromUserAndDevice-test.ts +++ /dev/null @@ -1,116 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { mocked } from "jest-mock"; -import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; - -import { - findRoomLiveVoiceBroadcastFromUserAndDevice, - VoiceBroadcastInfoEventType, - VoiceBroadcastInfoState, -} from "../../../../src/voice-broadcast"; -import { mkEvent, stubClient } from "../../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "./test-utils"; - -describe("findRoomLiveVoiceBroadcastFromUserAndDevice", () => { - const roomId = "!room:example.com"; - let client: MatrixClient; - let room: Room; - - const itShouldReturnNull = () => { - it("should return null", () => { - expect( - findRoomLiveVoiceBroadcastFromUserAndDevice(room, client.getUserId()!, client.getDeviceId()!), - ).toBeNull(); - }); - }; - - beforeAll(() => { - client = stubClient(); - room = new Room(roomId, client, client.getUserId()!); - jest.spyOn(room.currentState, "getStateEvents"); - mocked(client.getRoom).mockImplementation((getRoomId: string) => { - if (getRoomId === roomId) return room; - return null; - }); - }); - - describe("when there is no info event", () => { - itShouldReturnNull(); - }); - - describe("when there is an info event without content", () => { - beforeEach(() => { - room.currentState.setStateEvents([ - mkEvent({ - event: true, - type: VoiceBroadcastInfoEventType, - room: roomId, - user: client.getUserId()!, - content: {}, - }), - ]); - }); - - itShouldReturnNull(); - }); - - describe("when there is a stopped info event", () => { - beforeEach(() => { - room.currentState.setStateEvents([ - mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Stopped, - client.getUserId()!, - client.getDeviceId(), - ), - ]); - }); - - itShouldReturnNull(); - }); - - describe("when there is a started info event from another device", () => { - beforeEach(() => { - const event = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Stopped, - client.getUserId()!, - "JKL123", - ); - room.currentState.setStateEvents([event]); - }); - - itShouldReturnNull(); - }); - - describe("when there is a started info event", () => { - let event: MatrixEvent; - - beforeEach(() => { - event = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Started, - client.getUserId()!, - client.getDeviceId(), - ); - room.currentState.setStateEvents([event]); - }); - - it("should return this event", () => { - expect(room.currentState.getStateEvents).toHaveBeenCalledWith( - VoiceBroadcastInfoEventType, - client.getUserId()!, - ); - - expect(findRoomLiveVoiceBroadcastFromUserAndDevice(room, client.getUserId()!, client.getDeviceId()!)).toBe( - event, - ); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/utils/getChunkLength-test.ts b/test/unit-tests/voice-broadcast/utils/getChunkLength-test.ts deleted file mode 100644 index b2b41ae577..0000000000 --- a/test/unit-tests/voice-broadcast/utils/getChunkLength-test.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import SdkConfig from "../../../../src/SdkConfig"; -import { SettingLevel } from "../../../../src/settings/SettingLevel"; -import { Features } from "../../../../src/settings/Settings"; -import SettingsStore from "../../../../src/settings/SettingsStore"; -import { getChunkLength } from "../../../../src/voice-broadcast/utils/getChunkLength"; - -describe("getChunkLength", () => { - afterEach(() => { - SdkConfig.reset(); - }); - - describe("when there is a value provided by Sdk config", () => { - beforeEach(() => { - SdkConfig.add({ - voice_broadcast: { - chunk_length: 42, - }, - }); - }); - - it("should return this value", () => { - expect(getChunkLength()).toBe(42); - }); - }); - - describe("when Sdk config does not provide a value", () => { - beforeEach(() => { - SdkConfig.add({ - voice_broadcast: { - chunk_length: 23, - }, - }); - }); - - it("should return this value", () => { - expect(getChunkLength()).toBe(23); - }); - }); - - describe("when there are no defaults", () => { - it("should return the fallback value", () => { - expect(getChunkLength()).toBe(120); - }); - }); - - describe("when the Features.VoiceBroadcastForceSmallChunks is enabled", () => { - beforeEach(async () => { - await SettingsStore.setValue(Features.VoiceBroadcastForceSmallChunks, null, SettingLevel.DEVICE, true); - }); - - it("should return a chunk length of 15 seconds", () => { - expect(getChunkLength()).toBe(15); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/utils/getMaxBroadcastLength-test.ts b/test/unit-tests/voice-broadcast/utils/getMaxBroadcastLength-test.ts deleted file mode 100644 index 6bc38bee98..0000000000 --- a/test/unit-tests/voice-broadcast/utils/getMaxBroadcastLength-test.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import SdkConfig, { DEFAULTS } from "../../../../src/SdkConfig"; -import { getMaxBroadcastLength } from "../../../../src/voice-broadcast"; - -describe("getMaxBroadcastLength", () => { - afterEach(() => { - SdkConfig.reset(); - }); - - describe("when there is a value provided by Sdk config", () => { - beforeEach(() => { - SdkConfig.put({ - voice_broadcast: { - max_length: 42, - }, - }); - }); - - it("should return this value", () => { - expect(getMaxBroadcastLength()).toBe(42); - }); - }); - - describe("when Sdk config does not provide a value", () => { - it("should return this value", () => { - expect(getMaxBroadcastLength()).toBe(DEFAULTS.voice_broadcast!.max_length); - }); - }); - - describe("if there are no defaults", () => { - it("should return the fallback value", () => { - expect(DEFAULTS.voice_broadcast!.max_length).toBe(4 * 60 * 60); - expect(getMaxBroadcastLength()).toBe(4 * 60 * 60); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/utils/hasRoomLiveVoiceBroadcast-test.ts b/test/unit-tests/voice-broadcast/utils/hasRoomLiveVoiceBroadcast-test.ts deleted file mode 100644 index 37fbf0c277..0000000000 --- a/test/unit-tests/voice-broadcast/utils/hasRoomLiveVoiceBroadcast-test.ts +++ /dev/null @@ -1,195 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { mocked } from "jest-mock"; -import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; - -import { - hasRoomLiveVoiceBroadcast, - VoiceBroadcastInfoEventType, - VoiceBroadcastInfoState, -} from "../../../../src/voice-broadcast"; -import { mkEvent, stubClient } from "../../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "./test-utils"; - -describe("hasRoomLiveVoiceBroadcast", () => { - const otherUserId = "@other:example.com"; - const otherDeviceId = "ASD123"; - const roomId = "!room:example.com"; - let client: MatrixClient; - let room: Room; - let expectedEvent: MatrixEvent | null = null; - - const addVoiceBroadcastInfoEvent = ( - state: VoiceBroadcastInfoState, - userId: string, - deviceId: string, - startedEvent?: MatrixEvent, - ): MatrixEvent => { - const infoEvent = mkVoiceBroadcastInfoStateEvent(room.roomId, state, userId, deviceId, startedEvent); - room.addLiveEvents([infoEvent], { addToState: true }); - room.currentState.setStateEvents([infoEvent]); - room.relations.aggregateChildEvent(infoEvent); - return infoEvent; - }; - - const itShouldReturnTrueTrue = () => { - it("should return true/true", async () => { - expect(await hasRoomLiveVoiceBroadcast(client, room, client.getSafeUserId())).toEqual({ - hasBroadcast: true, - infoEvent: expectedEvent, - startedByUser: true, - }); - }); - }; - - const itShouldReturnTrueFalse = () => { - it("should return true/false", async () => { - expect(await hasRoomLiveVoiceBroadcast(client, room, client.getSafeUserId())).toEqual({ - hasBroadcast: true, - infoEvent: expectedEvent, - startedByUser: false, - }); - }); - }; - - const itShouldReturnFalseFalse = () => { - it("should return false/false", async () => { - expect(await hasRoomLiveVoiceBroadcast(client, room, client.getSafeUserId())).toEqual({ - hasBroadcast: false, - infoEvent: null, - startedByUser: false, - }); - }); - }; - - beforeEach(() => { - client = stubClient(); - room = new Room(roomId, client, client.getSafeUserId()); - mocked(client.getRoom).mockImplementation((roomId: string): Room | null => { - return roomId === room.roomId ? room : null; - }); - expectedEvent = null; - }); - - describe("when there is no voice broadcast info at all", () => { - itShouldReturnFalseFalse(); - }); - - describe("when the »state« prop is missing", () => { - beforeEach(() => { - room.currentState.setStateEvents([ - mkEvent({ - event: true, - room: room.roomId, - user: client.getSafeUserId(), - type: VoiceBroadcastInfoEventType, - skey: client.getSafeUserId(), - content: {}, - }), - ]); - }); - itShouldReturnFalseFalse(); - }); - - describe("when there is a live broadcast from the current and another user", () => { - beforeEach(() => { - expectedEvent = addVoiceBroadcastInfoEvent( - VoiceBroadcastInfoState.Started, - client.getSafeUserId(), - client.getDeviceId()!, - ); - addVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Started, otherUserId, otherDeviceId); - }); - - itShouldReturnTrueTrue(); - }); - - describe("when there are only stopped info events", () => { - beforeEach(() => { - addVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Stopped, client.getSafeUserId(), client.getDeviceId()!); - addVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Stopped, otherUserId, otherDeviceId); - }); - - itShouldReturnFalseFalse(); - }); - - describe("when there is a live, started broadcast from the current user", () => { - beforeEach(() => { - expectedEvent = addVoiceBroadcastInfoEvent( - VoiceBroadcastInfoState.Started, - client.getSafeUserId(), - client.getDeviceId()!, - ); - }); - - itShouldReturnTrueTrue(); - }); - - describe("when there is a live, paused broadcast from the current user", () => { - beforeEach(() => { - expectedEvent = addVoiceBroadcastInfoEvent( - VoiceBroadcastInfoState.Started, - client.getSafeUserId(), - client.getDeviceId()!, - ); - addVoiceBroadcastInfoEvent( - VoiceBroadcastInfoState.Paused, - client.getSafeUserId(), - client.getDeviceId()!, - expectedEvent, - ); - }); - - itShouldReturnTrueTrue(); - }); - - describe("when there is a live, resumed broadcast from the current user", () => { - beforeEach(() => { - expectedEvent = addVoiceBroadcastInfoEvent( - VoiceBroadcastInfoState.Started, - client.getSafeUserId(), - client.getDeviceId()!, - ); - addVoiceBroadcastInfoEvent( - VoiceBroadcastInfoState.Resumed, - client.getSafeUserId(), - client.getDeviceId()!, - expectedEvent, - ); - }); - - itShouldReturnTrueTrue(); - }); - - describe("when there was a live broadcast, that has been stopped", () => { - beforeEach(() => { - const startedEvent = addVoiceBroadcastInfoEvent( - VoiceBroadcastInfoState.Started, - client.getSafeUserId(), - client.getDeviceId()!, - ); - addVoiceBroadcastInfoEvent( - VoiceBroadcastInfoState.Stopped, - client.getSafeUserId(), - client.getDeviceId()!, - startedEvent, - ); - }); - - itShouldReturnFalseFalse(); - }); - - describe("when there is a live broadcast from another user", () => { - beforeEach(() => { - expectedEvent = addVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Started, otherUserId, otherDeviceId); - }); - - itShouldReturnTrueFalse(); - }); -}); diff --git a/test/unit-tests/voice-broadcast/utils/isRelatedToVoiceBroadcast-test.ts b/test/unit-tests/voice-broadcast/utils/isRelatedToVoiceBroadcast-test.ts deleted file mode 100644 index eae78df109..0000000000 --- a/test/unit-tests/voice-broadcast/utils/isRelatedToVoiceBroadcast-test.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { EventType, MatrixEvent, RelationType, Room, MatrixClient } from "matrix-js-sdk/src/matrix"; -import { mocked } from "jest-mock"; - -import { isRelatedToVoiceBroadcast, VoiceBroadcastInfoState } from "../../../../src/voice-broadcast"; -import { mkEvent, stubClient } from "../../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "./test-utils"; - -const mkRelatedEvent = ( - room: Room, - relationType: RelationType, - relatesTo: MatrixEvent | undefined, - client: MatrixClient, -): MatrixEvent => { - const event = mkEvent({ - event: true, - type: EventType.RoomMessage, - room: room.roomId, - content: { - "m.relates_to": { - rel_type: relationType, - event_id: relatesTo?.getId(), - }, - }, - user: client.getSafeUserId(), - }); - room.addLiveEvents([event], { addToState: true }); - return event; -}; - -describe("isRelatedToVoiceBroadcast", () => { - const roomId = "!room:example.com"; - let client: MatrixClient; - let room: Room; - let broadcastEvent: MatrixEvent; - let nonBroadcastEvent: MatrixEvent; - - beforeAll(() => { - client = stubClient(); - room = new Room(roomId, client, client.getSafeUserId()); - - mocked(client.getRoom).mockImplementation((getRoomId: string): Room | null => { - if (getRoomId === roomId) return room; - return null; - }); - - broadcastEvent = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Started, - client.getSafeUserId(), - "ABC123", - ); - nonBroadcastEvent = mkEvent({ - event: true, - type: EventType.RoomMessage, - room: roomId, - content: {}, - user: client.getSafeUserId(), - }); - - room.addLiveEvents([broadcastEvent, nonBroadcastEvent], { addToState: true }); - }); - - it("should return true if related (reference) to a broadcast event", () => { - expect( - isRelatedToVoiceBroadcast(mkRelatedEvent(room, RelationType.Reference, broadcastEvent, client), client), - ).toBe(true); - }); - - it("should return false if related (reference) is undefeind", () => { - expect(isRelatedToVoiceBroadcast(mkRelatedEvent(room, RelationType.Reference, undefined, client), client)).toBe( - false, - ); - }); - - it("should return false if related (referenireplace) to a broadcast event", () => { - expect( - isRelatedToVoiceBroadcast(mkRelatedEvent(room, RelationType.Replace, broadcastEvent, client), client), - ).toBe(false); - }); - - it("should return false if the event has no relation", () => { - const noRelationEvent = mkEvent({ - event: true, - type: EventType.RoomMessage, - room: room.roomId, - content: {}, - user: client.getSafeUserId(), - }); - expect(isRelatedToVoiceBroadcast(noRelationEvent, client)).toBe(false); - }); - - it("should return false for an unknown room", () => { - const otherRoom = new Room("!other:example.com", client, client.getSafeUserId()); - expect( - isRelatedToVoiceBroadcast( - mkRelatedEvent(otherRoom, RelationType.Reference, broadcastEvent, client), - client, - ), - ).toBe(false); - }); -}); diff --git a/test/unit-tests/voice-broadcast/utils/pauseNonLiveBroadcastFromOtherRoom-test.ts b/test/unit-tests/voice-broadcast/utils/pauseNonLiveBroadcastFromOtherRoom-test.ts deleted file mode 100644 index 565ec77053..0000000000 --- a/test/unit-tests/voice-broadcast/utils/pauseNonLiveBroadcastFromOtherRoom-test.ts +++ /dev/null @@ -1,98 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; - -import { - VoiceBroadcastInfoState, - VoiceBroadcastPlayback, - VoiceBroadcastPlaybacksStore, - VoiceBroadcastRecordingsStore, -} from "../../../../src/voice-broadcast"; -import { pauseNonLiveBroadcastFromOtherRoom } from "../../../../src/voice-broadcast/utils/pauseNonLiveBroadcastFromOtherRoom"; -import { stubClient } from "../../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "./test-utils"; - -describe("pauseNonLiveBroadcastFromOtherRoom", () => { - const roomId = "!room:example.com"; - const roomId2 = "!room2@example.com"; - let room: Room; - let client: MatrixClient; - let playback: VoiceBroadcastPlayback; - let playbacks: VoiceBroadcastPlaybacksStore; - let recordings: VoiceBroadcastRecordingsStore; - - const mkPlayback = (infoState: VoiceBroadcastInfoState, roomId: string): VoiceBroadcastPlayback => { - const infoEvent = mkVoiceBroadcastInfoStateEvent( - roomId, - infoState, - client.getSafeUserId(), - client.getDeviceId()!, - ); - const playback = new VoiceBroadcastPlayback(infoEvent, client, recordings); - jest.spyOn(playback, "pause"); - playbacks.setCurrent(playback); - return playback; - }; - - beforeEach(() => { - client = stubClient(); - room = new Room(roomId, client, client.getSafeUserId()); - recordings = new VoiceBroadcastRecordingsStore(); - playbacks = new VoiceBroadcastPlaybacksStore(recordings); - jest.spyOn(playbacks, "clearCurrent"); - }); - - afterEach(() => { - playback?.destroy(); - playbacks.destroy(); - }); - - describe("when there is no current playback", () => { - it("should not clear the current playback", () => { - pauseNonLiveBroadcastFromOtherRoom(room, playbacks); - expect(playbacks.clearCurrent).not.toHaveBeenCalled(); - }); - }); - - describe("when listening to a live broadcast in another room", () => { - beforeEach(() => { - playback = mkPlayback(VoiceBroadcastInfoState.Started, roomId2); - }); - - it("should not clear current / pause the playback", () => { - pauseNonLiveBroadcastFromOtherRoom(room, playbacks); - expect(playbacks.clearCurrent).not.toHaveBeenCalled(); - expect(playback.pause).not.toHaveBeenCalled(); - }); - }); - - describe("when listening to a non-live broadcast in the same room", () => { - beforeEach(() => { - playback = mkPlayback(VoiceBroadcastInfoState.Stopped, roomId); - }); - - it("should not clear current / pause the playback", () => { - pauseNonLiveBroadcastFromOtherRoom(room, playbacks); - expect(playbacks.clearCurrent).not.toHaveBeenCalled(); - expect(playback.pause).not.toHaveBeenCalled(); - }); - }); - - describe("when listening to a non-live broadcast in another room", () => { - beforeEach(() => { - playback = mkPlayback(VoiceBroadcastInfoState.Stopped, roomId2); - }); - - it("should clear current and pause the playback", () => { - pauseNonLiveBroadcastFromOtherRoom(room, playbacks); - expect(playbacks.getCurrent()).toBeNull(); - expect(playback.pause).toHaveBeenCalled(); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/utils/retrieveStartedInfoEvent-test.ts b/test/unit-tests/voice-broadcast/utils/retrieveStartedInfoEvent-test.ts deleted file mode 100644 index 70316f3b29..0000000000 --- a/test/unit-tests/voice-broadcast/utils/retrieveStartedInfoEvent-test.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { mocked } from "jest-mock"; -import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; - -import { - retrieveStartedInfoEvent, - VoiceBroadcastInfoEventType, - VoiceBroadcastInfoState, -} from "../../../../src/voice-broadcast"; -import { mkEvent, stubClient } from "../../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "./test-utils"; - -describe("retrieveStartedInfoEvent", () => { - let client: MatrixClient; - let room: Room; - - const mkStartEvent = () => { - return mkVoiceBroadcastInfoStateEvent( - room.roomId, - VoiceBroadcastInfoState.Started, - client.getUserId()!, - client.deviceId!, - ); - }; - - const mkStopEvent = (startEvent: MatrixEvent) => { - return mkVoiceBroadcastInfoStateEvent( - room.roomId, - VoiceBroadcastInfoState.Stopped, - client.getUserId()!, - client.deviceId!, - startEvent, - ); - }; - - beforeEach(() => { - client = stubClient(); - room = new Room("!room:example.com", client, client.getUserId()!); - mocked(client.getRoom).mockImplementation((roomId: string): Room | null => { - if (roomId === room.roomId) return room; - return null; - }); - }); - - it("when passing a started event, it should return the event", async () => { - const event = mkStartEvent(); - expect(await retrieveStartedInfoEvent(event, client)).toBe(event); - }); - - it("when passing an event without relation, it should return null", async () => { - const event = mkEvent({ - event: true, - type: VoiceBroadcastInfoEventType, - user: client.getUserId()!, - content: {}, - }); - expect(await retrieveStartedInfoEvent(event, client)).toBeNull(); - }); - - it("when the room contains the event, it should return it", async () => { - const startEvent = mkStartEvent(); - const stopEvent = mkStopEvent(startEvent); - room.addLiveEvents([startEvent], { addToState: true }); - expect(await retrieveStartedInfoEvent(stopEvent, client)).toBe(startEvent); - }); - - it("when the room not contains the event, it should fetch it", async () => { - const startEvent = mkStartEvent(); - const stopEvent = mkStopEvent(startEvent); - mocked(client.fetchRoomEvent).mockResolvedValue(startEvent.event); - expect((await retrieveStartedInfoEvent(stopEvent, client))?.getId()).toBe(startEvent.getId()); - expect(client.fetchRoomEvent).toHaveBeenCalledWith(room.roomId, startEvent.getId()); - }); -}); diff --git a/test/unit-tests/voice-broadcast/utils/setUpVoiceBroadcastPreRecording-test.ts b/test/unit-tests/voice-broadcast/utils/setUpVoiceBroadcastPreRecording-test.ts deleted file mode 100644 index 40c94aa371..0000000000 --- a/test/unit-tests/voice-broadcast/utils/setUpVoiceBroadcastPreRecording-test.ts +++ /dev/null @@ -1,123 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { mocked } from "jest-mock"; -import { MatrixClient, MatrixEvent, Room, SyncState } from "matrix-js-sdk/src/matrix"; - -import Modal from "../../../../src/Modal"; -import { - VoiceBroadcastInfoState, - VoiceBroadcastPlayback, - VoiceBroadcastPlaybacksStore, - VoiceBroadcastPreRecording, - VoiceBroadcastPreRecordingStore, - VoiceBroadcastRecordingsStore, -} from "../../../../src/voice-broadcast"; -import { setUpVoiceBroadcastPreRecording } from "../../../../src/voice-broadcast/utils/setUpVoiceBroadcastPreRecording"; -import { mkRoomMemberJoinEvent, stubClient } from "../../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "./test-utils"; - -jest.mock("../../../../src/Modal"); - -describe("setUpVoiceBroadcastPreRecording", () => { - const roomId = "!room:example.com"; - let client: MatrixClient; - let userId: string; - let room: Room; - let preRecordingStore: VoiceBroadcastPreRecordingStore; - let infoEvent: MatrixEvent; - let playback: VoiceBroadcastPlayback; - let playbacksStore: VoiceBroadcastPlaybacksStore; - let recordingsStore: VoiceBroadcastRecordingsStore; - let preRecording: VoiceBroadcastPreRecording | null; - - const itShouldNotCreateAPreRecording = () => { - it("should return null", () => { - expect(preRecording).toBeNull(); - }); - - it("should not create a broadcast pre recording", () => { - expect(preRecordingStore.getCurrent()).toBeNull(); - }); - }; - - const setUpPreRecording = async () => { - preRecording = await setUpVoiceBroadcastPreRecording( - room, - client, - playbacksStore, - recordingsStore, - preRecordingStore, - ); - }; - - beforeEach(() => { - client = stubClient(); - userId = client.getSafeUserId(); - room = new Room(roomId, client, userId); - infoEvent = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Started, - client.getUserId()!, - client.getDeviceId()!, - ); - preRecording = null; - preRecordingStore = new VoiceBroadcastPreRecordingStore(); - recordingsStore = new VoiceBroadcastRecordingsStore(); - playback = new VoiceBroadcastPlayback(infoEvent, client, recordingsStore); - jest.spyOn(playback, "pause"); - playbacksStore = new VoiceBroadcastPlaybacksStore(recordingsStore); - }); - - describe("when trying to start a broadcast if there is no connection", () => { - beforeEach(async () => { - mocked(client.getSyncState).mockReturnValue(SyncState.Error); - await setUpPreRecording(); - }); - - it("should show an info dialog and not set up a pre-recording", () => { - expect(preRecordingStore.getCurrent()).toBeNull(); - expect(Modal.createDialog).toMatchSnapshot(); - }); - }); - - describe("when setting up a pre-recording", () => { - describe("and there is no user id", () => { - beforeEach(async () => { - mocked(client.getUserId).mockReturnValue(null); - await setUpPreRecording(); - }); - - itShouldNotCreateAPreRecording(); - }); - - describe("and there is no room member", () => { - beforeEach(async () => { - // check test precondition - expect(room.getMember(userId)).toBeNull(); - await setUpPreRecording(); - }); - - itShouldNotCreateAPreRecording(); - }); - - describe("and there is a room member and listening to another broadcast", () => { - beforeEach(async () => { - playbacksStore.setCurrent(playback); - room.currentState.setStateEvents([mkRoomMemberJoinEvent(userId, roomId)]); - await setUpPreRecording(); - }); - - it("should pause the current playback and create a voice broadcast pre-recording", () => { - expect(playback.pause).toHaveBeenCalled(); - expect(playbacksStore.getCurrent()).toBeNull(); - expect(preRecording).toBeInstanceOf(VoiceBroadcastPreRecording); - }); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastRecordingTile-test.ts b/test/unit-tests/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastRecordingTile-test.ts deleted file mode 100644 index 6448e3d2e7..0000000000 --- a/test/unit-tests/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastRecordingTile-test.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { mocked } from "jest-mock"; -import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; - -import { shouldDisplayAsVoiceBroadcastRecordingTile, VoiceBroadcastInfoState } from "../../../../src/voice-broadcast"; -import { createTestClient } from "../../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "./test-utils"; - -type TestTuple = [string | null, string, string, string, VoiceBroadcastInfoState, boolean]; - -const testCases: TestTuple[] = [ - [ - "@user1:example.com", // own MXID - "@user1:example.com", // sender MXID - "ABC123", // own device ID - "ABC123", // sender device ID - VoiceBroadcastInfoState.Started, - true, // expected return value - ], - ["@user1:example.com", "@user1:example.com", "ABC123", "ABC123", VoiceBroadcastInfoState.Paused, true], - ["@user1:example.com", "@user1:example.com", "ABC123", "ABC123", VoiceBroadcastInfoState.Resumed, true], - ["@user1:example.com", "@user1:example.com", "ABC123", "ABC123", VoiceBroadcastInfoState.Stopped, false], - ["@user2:example.com", "@user1:example.com", "ABC123", "ABC123", VoiceBroadcastInfoState.Started, false], - [null, "@user1:example.com", "ABC123", "ABC123", VoiceBroadcastInfoState.Started, false], - // other device - ["@user1:example.com", "@user1:example.com", "ABC123", "JKL123", VoiceBroadcastInfoState.Started, false], - ["@user1:example.com", "@user1:example.com", "ABC123", "JKL123", VoiceBroadcastInfoState.Paused, false], - ["@user1:example.com", "@user1:example.com", "ABC123", "JKL123", VoiceBroadcastInfoState.Resumed, false], -]; - -describe("shouldDisplayAsVoiceBroadcastRecordingTile", () => { - let event: MatrixEvent; - let client: MatrixClient; - - beforeAll(() => { - client = createTestClient(); - }); - - describe.each(testCases)( - "when called with user »%s«, sender »%s«, device »%s«, sender device »%s« state »%s«", - (userId, senderId, deviceId, senderDeviceId, state, expected) => { - beforeEach(() => { - event = mkVoiceBroadcastInfoStateEvent("!room:example.com", state, senderId, senderDeviceId); - mocked(client.getUserId).mockReturnValue(userId); - mocked(client.getDeviceId).mockReturnValue(deviceId); - }); - - it(`should return ${expected}`, () => { - expect(shouldDisplayAsVoiceBroadcastRecordingTile(state, client, event)).toBe(expected); - }); - }, - ); - - it("should return false, when all params are null", () => { - event = mkVoiceBroadcastInfoStateEvent("!room:example.com", null, null, null); - // @ts-ignore Simulate null state received for any reason. - expect(shouldDisplayAsVoiceBroadcastRecordingTile(null, client, event)).toBe(false); - }); - - it("should return false, when all params are undefined", () => { - event = mkVoiceBroadcastInfoStateEvent("!room:example.com", undefined, undefined, undefined); - // @ts-ignore Simulate undefined state received for any reason. - expect(shouldDisplayAsVoiceBroadcastRecordingTile(undefined, client, event)).toBe(false); - }); -}); diff --git a/test/unit-tests/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastTile-test.ts b/test/unit-tests/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastTile-test.ts deleted file mode 100644 index fd3d1f91c4..0000000000 --- a/test/unit-tests/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastTile-test.ts +++ /dev/null @@ -1,138 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { EventType, IEvent, MatrixEvent } from "matrix-js-sdk/src/matrix"; - -import { - shouldDisplayAsVoiceBroadcastTile, - VoiceBroadcastInfoEventType, - VoiceBroadcastInfoState, -} from "../../../../src/voice-broadcast"; -import { mkEvent } from "../../../test-utils"; - -describe("shouldDisplayAsVoiceBroadcastTile", () => { - let event: MatrixEvent; - const roomId = "!room:example.com"; - const senderId = "@user:example.com"; - - const itShouldReturnFalse = () => { - it("should return false", () => { - expect(shouldDisplayAsVoiceBroadcastTile(event)).toBe(false); - }); - }; - - const itShouldReturnTrue = () => { - it("should return true", () => { - expect(shouldDisplayAsVoiceBroadcastTile(event)).toBe(true); - }); - }; - - describe("when a broken event occurs", () => { - beforeEach(() => { - event = 23 as unknown as MatrixEvent; - }); - - itShouldReturnFalse(); - }); - - describe("when a non-voice broadcast info event occurs", () => { - beforeEach(() => { - event = mkEvent({ - event: true, - type: EventType.RoomMessage, - room: roomId, - user: senderId, - content: {}, - }); - }); - - itShouldReturnFalse(); - }); - - describe("when a voice broadcast info event with empty content occurs", () => { - beforeEach(() => { - event = mkEvent({ - event: true, - type: VoiceBroadcastInfoEventType, - room: roomId, - user: senderId, - content: {}, - }); - }); - - itShouldReturnFalse(); - }); - - describe("when a voice broadcast info event with undefined content occurs", () => { - beforeEach(() => { - event = mkEvent({ - event: true, - type: VoiceBroadcastInfoEventType, - room: roomId, - user: senderId, - content: {}, - }); - event.getContent = () => ({}) as any; - }); - - itShouldReturnFalse(); - }); - - describe("when a voice broadcast info event in state started occurs", () => { - beforeEach(() => { - event = mkEvent({ - event: true, - type: VoiceBroadcastInfoEventType, - room: roomId, - user: senderId, - content: { - state: VoiceBroadcastInfoState.Started, - }, - }); - }); - - itShouldReturnTrue(); - }); - - describe("when a redacted event occurs", () => { - beforeEach(() => { - event = mkEvent({ - event: true, - type: VoiceBroadcastInfoEventType, - room: roomId, - user: senderId, - content: {}, - unsigned: { - redacted_because: {} as unknown as IEvent, - }, - }); - event.getContent = () => ({}) as any; - }); - - itShouldReturnTrue(); - }); - - describe.each([VoiceBroadcastInfoState.Paused, VoiceBroadcastInfoState.Resumed, VoiceBroadcastInfoState.Stopped])( - "when a voice broadcast info event in state %s occurs", - (state: VoiceBroadcastInfoState) => { - beforeEach(() => { - event = mkEvent({ - event: true, - type: VoiceBroadcastInfoEventType, - room: roomId, - user: senderId, - content: { - state, - }, - }); - }); - - itShouldReturnFalse(); - }, - ); -}); diff --git a/test/unit-tests/voice-broadcast/utils/startNewVoiceBroadcastRecording-test.ts b/test/unit-tests/voice-broadcast/utils/startNewVoiceBroadcastRecording-test.ts deleted file mode 100644 index c22920c174..0000000000 --- a/test/unit-tests/voice-broadcast/utils/startNewVoiceBroadcastRecording-test.ts +++ /dev/null @@ -1,234 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { mocked } from "jest-mock"; -import { EventType, ISendEventResponse, MatrixClient, MatrixEvent, Room, SyncState } from "matrix-js-sdk/src/matrix"; - -import Modal from "../../../../src/Modal"; -import { - startNewVoiceBroadcastRecording, - VoiceBroadcastInfoEventType, - VoiceBroadcastInfoState, - VoiceBroadcastRecordingsStore, - VoiceBroadcastRecording, - VoiceBroadcastPlaybacksStore, - VoiceBroadcastPlayback, -} from "../../../../src/voice-broadcast"; -import { mkEvent, stubClient } from "../../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "./test-utils"; - -jest.mock("../../../../src/voice-broadcast/models/VoiceBroadcastRecording", () => ({ - VoiceBroadcastRecording: jest.fn(), -})); - -jest.mock("../../../../src/Modal"); - -describe("startNewVoiceBroadcastRecording", () => { - const roomId = "!room:example.com"; - const otherUserId = "@other:example.com"; - let client: MatrixClient; - let playbacksStore: VoiceBroadcastPlaybacksStore; - let recordingsStore: VoiceBroadcastRecordingsStore; - let room: Room; - let infoEvent: MatrixEvent; - let otherEvent: MatrixEvent; - let result: VoiceBroadcastRecording | null; - - beforeEach(() => { - client = stubClient(); - room = new Room(roomId, client, client.getUserId()!); - jest.spyOn(room.currentState, "maySendStateEvent"); - - mocked(client.getRoom).mockImplementation((getRoomId: string) => { - if (getRoomId === roomId) { - return room; - } - - return null; - }); - mocked(client.sendStateEvent).mockImplementation( - (sendRoomId: string, eventType: string, content: any, stateKey: string): Promise => { - if (sendRoomId === roomId && eventType === VoiceBroadcastInfoEventType) { - return Promise.resolve({ event_id: infoEvent.getId()! }); - } - - throw new Error("Unexpected sendStateEvent call"); - }, - ); - - infoEvent = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Started, - client.getUserId()!, - client.getDeviceId()!, - ); - otherEvent = mkEvent({ - event: true, - type: EventType.RoomMember, - content: {}, - user: client.getUserId()!, - room: roomId, - skey: "", - }); - - playbacksStore = new VoiceBroadcastPlaybacksStore(recordingsStore); - recordingsStore = { - setCurrent: jest.fn(), - getCurrent: jest.fn(), - } as unknown as VoiceBroadcastRecordingsStore; - - mocked(VoiceBroadcastRecording).mockImplementation((infoEvent: MatrixEvent, client: MatrixClient): any => { - return { - infoEvent, - client, - start: jest.fn(), - } as unknown as VoiceBroadcastRecording; - }); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe("when trying to start a broadcast if there is no connection", () => { - beforeEach(async () => { - mocked(client.getSyncState).mockReturnValue(SyncState.Error); - result = await startNewVoiceBroadcastRecording(room, client, playbacksStore, recordingsStore); - }); - - it("should show an info dialog and not start a recording", () => { - expect(result).toBeNull(); - expect(Modal.createDialog).toMatchSnapshot(); - }); - }); - - describe("when the current user is allowed to send voice broadcast info state events", () => { - beforeEach(() => { - mocked(room.currentState.maySendStateEvent).mockReturnValue(true); - }); - - describe("when currently listening to a broadcast and there is no recording", () => { - let playback: VoiceBroadcastPlayback; - - beforeEach(() => { - playback = new VoiceBroadcastPlayback(infoEvent, client, recordingsStore); - jest.spyOn(playback, "pause"); - playbacksStore.setCurrent(playback); - }); - - it("should stop listen to the current broadcast and create a new recording", async () => { - mocked(client.sendStateEvent).mockImplementation( - async ( - _roomId: string, - _eventType: string, - _content: any, - _stateKey = "", - ): Promise => { - window.setTimeout(() => { - // emit state events after resolving the promise - room.currentState.setStateEvents([otherEvent]); - room.currentState.setStateEvents([infoEvent]); - }, 0); - return { event_id: infoEvent.getId()! }; - }, - ); - const recording = await startNewVoiceBroadcastRecording(room, client, playbacksStore, recordingsStore); - expect(recording).not.toBeNull(); - - // expect to stop and clear the current playback - expect(playback.pause).toHaveBeenCalled(); - expect(playbacksStore.getCurrent()).toBeNull(); - - expect(client.sendStateEvent).toHaveBeenCalledWith( - roomId, - VoiceBroadcastInfoEventType, - { - chunk_length: 120, - device_id: client.getDeviceId(), - state: VoiceBroadcastInfoState.Started, - }, - client.getUserId()!, - ); - expect(recording!.infoEvent).toBe(infoEvent); - expect(recording!.start).toHaveBeenCalled(); - }); - }); - - describe("when there is already a current voice broadcast", () => { - beforeEach(async () => { - mocked(recordingsStore.getCurrent).mockReturnValue(new VoiceBroadcastRecording(infoEvent, client)); - - result = await startNewVoiceBroadcastRecording(room, client, playbacksStore, recordingsStore); - }); - - it("should not start a voice broadcast", () => { - expect(result).toBeNull(); - }); - - it("should show an info dialog", () => { - expect(Modal.createDialog).toMatchSnapshot(); - }); - }); - - describe("when there already is a live broadcast of the current user in the room", () => { - beforeEach(async () => { - room.currentState.setStateEvents([ - mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Resumed, - client.getUserId()!, - client.getDeviceId()!, - ), - ]); - - result = await startNewVoiceBroadcastRecording(room, client, playbacksStore, recordingsStore); - }); - - it("should not start a voice broadcast", () => { - expect(result).toBeNull(); - }); - - it("should show an info dialog", () => { - expect(Modal.createDialog).toMatchSnapshot(); - }); - }); - - describe("when there already is a live broadcast of another user", () => { - beforeEach(async () => { - room.currentState.setStateEvents([ - mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Resumed, otherUserId, "ASD123"), - ]); - - result = await startNewVoiceBroadcastRecording(room, client, playbacksStore, recordingsStore); - }); - - it("should not start a voice broadcast", () => { - expect(result).toBeNull(); - }); - - it("should show an info dialog", () => { - expect(Modal.createDialog).toMatchSnapshot(); - }); - }); - }); - - describe("when the current user is not allowed to send voice broadcast info state events", () => { - beforeEach(async () => { - mocked(room.currentState.maySendStateEvent).mockReturnValue(false); - result = await startNewVoiceBroadcastRecording(room, client, playbacksStore, recordingsStore); - }); - - it("should not start a voice broadcast", () => { - expect(result).toBeNull(); - }); - - it("should show an info dialog", () => { - expect(Modal.createDialog).toMatchSnapshot(); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/utils/test-utils.ts b/test/unit-tests/voice-broadcast/utils/test-utils.ts deleted file mode 100644 index 4d465e84dc..0000000000 --- a/test/unit-tests/voice-broadcast/utils/test-utils.ts +++ /dev/null @@ -1,131 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { Optional } from "matrix-events-sdk"; -import { EventType, IContent, MatrixEvent, MsgType, RelationType, Room, RoomMember } from "matrix-js-sdk/src/matrix"; - -import { SdkContextClass } from "../../../../src/contexts/SDKContext"; -import { - VoiceBroadcastPlayback, - VoiceBroadcastPreRecording, - VoiceBroadcastRecording, -} from "../../../../src/voice-broadcast"; -import { - VoiceBroadcastChunkEventType, - VoiceBroadcastInfoEventType, - VoiceBroadcastInfoState, -} from "../../../../src/voice-broadcast/types"; -import { mkEvent } from "../../../test-utils"; - -// timestamp incremented on each call to prevent duplicate timestamp -let timestamp = new Date().getTime(); - -export const mkVoiceBroadcastInfoStateEvent = ( - roomId: Optional, - state: Optional, - senderId: Optional, - senderDeviceId: Optional, - startedInfoEvent?: MatrixEvent, - lastChunkSequence?: number, -): MatrixEvent => { - const relationContent: IContent = {}; - - if (startedInfoEvent) { - relationContent["m.relates_to"] = { - event_id: startedInfoEvent.getId(), - rel_type: "m.reference", - }; - } - - const lastChunkSequenceContent = lastChunkSequence ? { last_chunk_sequence: lastChunkSequence } : {}; - - return mkEvent({ - event: true, - // @ts-ignore allow everything here for edge test cases - room: roomId, - // @ts-ignore allow everything here for edge test cases - user: senderId, - type: VoiceBroadcastInfoEventType, - // @ts-ignore allow everything here for edge test cases - skey: senderId, - content: { - state, - device_id: senderDeviceId, - ...relationContent, - ...lastChunkSequenceContent, - }, - ts: timestamp++, - }); -}; - -export const mkVoiceBroadcastChunkEvent = ( - infoEventId: string, - userId: string, - roomId: string, - duration: number, - sequence?: number, - timestamp?: number, -): MatrixEvent => { - return mkEvent({ - event: true, - user: userId, - room: roomId, - type: EventType.RoomMessage, - content: { - msgtype: MsgType.Audio, - ["org.matrix.msc1767.audio"]: { - duration, - }, - info: { - duration, - }, - [VoiceBroadcastChunkEventType]: { - ...(sequence ? { sequence } : {}), - }, - ["m.relates_to"]: { - rel_type: RelationType.Reference, - event_id: infoEventId, - }, - }, - ts: timestamp, - }); -}; - -export const mkVoiceBroadcastPlayback = (stores: SdkContextClass): VoiceBroadcastPlayback => { - const infoEvent = mkVoiceBroadcastInfoStateEvent( - "!room:example.com", - VoiceBroadcastInfoState.Started, - "@user:example.com", - "ASD123", - ); - return new VoiceBroadcastPlayback(infoEvent, stores.client!, stores.voiceBroadcastRecordingsStore); -}; - -export const mkVoiceBroadcastRecording = (stores: SdkContextClass): VoiceBroadcastRecording => { - const infoEvent = mkVoiceBroadcastInfoStateEvent( - "!room:example.com", - VoiceBroadcastInfoState.Started, - "@user:example.com", - "ASD123", - ); - return new VoiceBroadcastRecording(infoEvent, stores.client!); -}; - -export const mkVoiceBroadcastPreRecording = (stores: SdkContextClass): VoiceBroadcastPreRecording => { - const roomId = "!room:example.com"; - const userId = "@user:example.com"; - const room = new Room(roomId, stores.client!, userId); - const roomMember = new RoomMember(roomId, userId); - return new VoiceBroadcastPreRecording( - room, - roomMember, - stores.client!, - stores.voiceBroadcastPlaybacksStore, - stores.voiceBroadcastRecordingsStore, - ); -}; diff --git a/test/unit-tests/voice-broadcast/utils/textForVoiceBroadcastStoppedEvent-test.tsx b/test/unit-tests/voice-broadcast/utils/textForVoiceBroadcastStoppedEvent-test.tsx deleted file mode 100644 index 37871993d0..0000000000 --- a/test/unit-tests/voice-broadcast/utils/textForVoiceBroadcastStoppedEvent-test.tsx +++ /dev/null @@ -1,90 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { render, RenderResult, screen } from "jest-matrix-react"; -import userEvent from "@testing-library/user-event"; -import { mocked } from "jest-mock"; -import { MatrixClient, RelationType } from "matrix-js-sdk/src/matrix"; - -import { textForVoiceBroadcastStoppedEvent, VoiceBroadcastInfoState } from "../../../../src/voice-broadcast"; -import { stubClient } from "../../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "./test-utils"; -import dis from "../../../../src/dispatcher/dispatcher"; -import { Action } from "../../../../src/dispatcher/actions"; - -jest.mock("../../../../src/dispatcher/dispatcher"); - -describe("textForVoiceBroadcastStoppedEvent", () => { - const otherUserId = "@other:example.com"; - const roomId = "!room:example.com"; - let client: MatrixClient; - - const renderText = (senderId: string, startEventId?: string) => { - const event = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Stopped, - senderId, - client.deviceId!, - ); - - if (startEventId) { - event.getContent()["m.relates_to"] = { - rel_type: RelationType.Reference, - event_id: startEventId, - }; - } - - return render(
{textForVoiceBroadcastStoppedEvent(event, client)()}
); - }; - - beforeEach(() => { - client = stubClient(); - }); - - it("should render own broadcast as expected", () => { - expect(renderText(client.getUserId()!).container).toMatchSnapshot(); - }); - - it("should render other users broadcast as expected", () => { - expect(renderText(otherUserId).container).toMatchSnapshot(); - }); - - it("should render without login as expected", () => { - mocked(client.getUserId).mockReturnValue(null); - expect(renderText(otherUserId).container).toMatchSnapshot(); - }); - - describe("when rendering an event with relation to the start event", () => { - let result: RenderResult; - - beforeEach(() => { - result = renderText(client.getUserId()!, "$start-id"); - }); - - it("should render events with relation to the start event", () => { - expect(result.container).toMatchSnapshot(); - }); - - describe("and clicking the link", () => { - beforeEach(async () => { - await userEvent.click(screen.getByRole("button")); - }); - - it("should dispatch an action to highlight the event", () => { - expect(dis.dispatch).toHaveBeenCalledWith({ - action: Action.ViewRoom, - event_id: "$start-id", - highlighted: true, - room_id: roomId, - metricsTrigger: undefined, // room doesn't change - }); - }); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/utils/textForVoiceBroadcastStoppedEventWithoutLink-test.ts b/test/unit-tests/voice-broadcast/utils/textForVoiceBroadcastStoppedEventWithoutLink-test.ts deleted file mode 100644 index a010a6ec7c..0000000000 --- a/test/unit-tests/voice-broadcast/utils/textForVoiceBroadcastStoppedEventWithoutLink-test.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { mocked } from "jest-mock"; -import { MatrixClient } from "matrix-js-sdk/src/matrix"; - -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`); - }); -}); From 84709df3c91eff8faf69b99b1a44c12688d4662e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 2 Dec 2024 13:13:12 +0000 Subject: [PATCH 24/27] Remove remaining reply fallbacks code (#28610) * Remove remaining reply fallbacks code as MSC2781 has been merged Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/rooms/EditMessageComposer.tsx | 33 ++----------------- .../views/rooms/SendMessageComposer.tsx | 1 - .../utils/createMessageContent.ts | 30 ++--------------- .../views/rooms/EditMessageComposer-test.tsx | 16 ++++----- .../EditWysiwygComposer-test.tsx | 4 +-- .../utils/createMessageContent-test.ts | 4 +-- .../wysiwyg_composer/utils/message-test.ts | 4 +-- 7 files changed, 18 insertions(+), 74 deletions(-) diff --git a/src/components/views/rooms/EditMessageComposer.tsx b/src/components/views/rooms/EditMessageComposer.tsx index c6321ad53a..f4b5c3698e 100644 --- a/src/components/views/rooms/EditMessageComposer.tsx +++ b/src/components/views/rooms/EditMessageComposer.tsx @@ -43,25 +43,6 @@ import { attachMentions, attachRelation } from "./SendMessageComposer"; import { filterBoolean } from "../../../utils/arrays"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -function getHtmlReplyFallback(mxEvent: MatrixEvent): string { - const html = mxEvent.getContent().formatted_body; - if (!html) { - return ""; - } - const rootNode = new DOMParser().parseFromString(html, "text/html").body; - const mxReply = rootNode.querySelector("mx-reply"); - return (mxReply && mxReply.outerHTML) || ""; -} - -function getTextReplyFallback(mxEvent: MatrixEvent): string { - const body: string = mxEvent.getContent().body; - const lines = body.split("\n").map((l) => l.trim()); - if (lines.length > 2 && lines[0].startsWith("> ") && lines[1].length === 0) { - return `${lines[0]}\n\n`; - } - return ""; -} - // exported for tests export function createEditContent( model: EditorModel, @@ -72,15 +53,6 @@ export function createEditContent( if (isEmote) { model = stripEmoteCommand(model); } - const isReply = !!editedEvent.replyEventId; - let plainPrefix = ""; - let htmlPrefix = ""; - - if (isReply) { - plainPrefix = getTextReplyFallback(editedEvent); - htmlPrefix = getHtmlReplyFallback(editedEvent); - } - const body = textSerialize(model); const newContent: RoomMessageEventContent = { @@ -89,19 +61,18 @@ export function createEditContent( }; const contentBody: RoomMessageTextEventContent & Omit, "m.relates_to"> = { "msgtype": newContent.msgtype, - "body": `${plainPrefix} * ${body}`, + "body": `* ${body}`, "m.new_content": newContent, }; const formattedBody = htmlSerializeIfNeeded(model, { - forceHTML: isReply, useMarkdown: SettingsStore.getValue("MessageComposerInput.useMarkdown"), }); if (formattedBody) { newContent.format = "org.matrix.custom.html"; newContent.formatted_body = formattedBody; contentBody.format = newContent.format; - contentBody.formatted_body = `${htmlPrefix} * ${formattedBody}`; + contentBody.formatted_body = `* ${formattedBody}`; } // Build the mentions properties for both the content and new_content. diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 6533415a83..b3767cbd2a 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -193,7 +193,6 @@ export function createMessageContent( body: body, }; const formattedBody = htmlSerializeIfNeeded(model, { - forceHTML: !!replyToEvent, useMarkdown: SettingsStore.getValue("MessageComposerInput.useMarkdown"), }); if (formattedBody) { diff --git a/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts b/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts index 7f42ed2327..58d09b3d12 100644 --- a/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts +++ b/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts @@ -27,28 +27,6 @@ function attachRelation(content: IContent, relation?: IEventRelation): void { } } -function getHtmlReplyFallback(mxEvent: MatrixEvent): string { - const html = mxEvent.getContent().formatted_body; - if (!html) { - return ""; - } - const rootNode = new DOMParser().parseFromString(html, "text/html").body; - const mxReply = rootNode.querySelector("mx-reply"); - return (mxReply && mxReply.outerHTML) || ""; -} - -function getTextReplyFallback(mxEvent: MatrixEvent): string { - const body = mxEvent.getContent().body; - if (typeof body !== "string") { - return ""; - } - const lines = body.split("\n").map((l) => l.trim()); - if (lines.length > 2 && lines[0].startsWith("> ") && lines[1].length === 0) { - return `${lines[0]}\n\n`; - } - return ""; -} - interface CreateMessageContentParams { relation?: IEventRelation; replyToEvent?: MatrixEvent; @@ -63,8 +41,6 @@ export async function createMessageContent( { relation, replyToEvent, editedEvent }: CreateMessageContentParams, ): Promise { const isEditing = isMatrixEvent(editedEvent); - const isReply = isEditing ? Boolean(editedEvent.replyEventId) : isMatrixEvent(replyToEvent); - const isReplyAndEditing = isEditing && isReply; const isEmote = message.startsWith(EMOTE_PREFIX); if (isEmote) { @@ -82,12 +58,10 @@ export async function createMessageContent( // if we're editing rich text, the message content is pure html // BUT if we're not, the message content will be plain text where we need to convert the mentions const body = isHTML ? await richToPlain(message, false) : convertPlainTextToBody(message); - const bodyPrefix = (isReplyAndEditing && getTextReplyFallback(editedEvent)) || ""; - const formattedBodyPrefix = (isReplyAndEditing && getHtmlReplyFallback(editedEvent)) || ""; const content = { msgtype: isEmote ? MsgType.Emote : MsgType.Text, - body: isEditing ? `${bodyPrefix} * ${body}` : body, + body: isEditing ? `* ${body}` : body, } as RoomMessageTextEventContent & ReplacementEvent; // TODO markdown support @@ -97,7 +71,7 @@ export async function createMessageContent( if (formattedBody) { content.format = "org.matrix.custom.html"; - content.formatted_body = isEditing ? `${formattedBodyPrefix} * ${formattedBody}` : formattedBody; + content.formatted_body = isEditing ? `* ${formattedBody}` : formattedBody; } if (isEditing) { diff --git a/test/unit-tests/components/views/rooms/EditMessageComposer-test.tsx b/test/unit-tests/components/views/rooms/EditMessageComposer-test.tsx index 0ef3dd76e5..beac0c8e1d 100644 --- a/test/unit-tests/components/views/rooms/EditMessageComposer-test.tsx +++ b/test/unit-tests/components/views/rooms/EditMessageComposer-test.tsx @@ -128,7 +128,7 @@ describe("", () => { const expectedBody = { ...editedEvent.getContent(), - "body": " * original message + edit", + "body": "* original message + edit", "m.new_content": { "body": "original message + edit", "msgtype": "m.text", @@ -160,7 +160,7 @@ describe("", () => { const content = createEditContent(model, editedEvent); expect(content).toEqual({ - "body": " * hello world", + "body": "* hello world", "msgtype": "m.text", "m.new_content": { "body": "hello world", @@ -183,10 +183,10 @@ describe("", () => { const content = createEditContent(model, editedEvent); expect(content).toEqual({ - "body": " * hello *world*", + "body": "* hello *world*", "msgtype": "m.text", "format": "org.matrix.custom.html", - "formatted_body": " * hello world", + "formatted_body": "* hello world", "m.new_content": { "body": "hello *world*", "msgtype": "m.text", @@ -210,10 +210,10 @@ describe("", () => { const content = createEditContent(model, editedEvent); expect(content).toEqual({ - "body": " * blinks __quickly__", + "body": "* blinks __quickly__", "msgtype": "m.emote", "format": "org.matrix.custom.html", - "formatted_body": " * blinks quickly", + "formatted_body": "* blinks quickly", "m.new_content": { "body": "blinks __quickly__", "msgtype": "m.emote", @@ -238,7 +238,7 @@ describe("", () => { const content = createEditContent(model, editedEvent); expect(content).toEqual({ - "body": " * ✨sparkles✨", + "body": "* ✨sparkles✨", "msgtype": "m.emote", "m.new_content": { "body": "✨sparkles✨", @@ -264,7 +264,7 @@ describe("", () => { // TODO Edits do not properly strip the double slash used to skip // command processing. expect(content).toEqual({ - "body": " * //dev/null is my favourite place", + "body": "* //dev/null is my favourite place", "msgtype": "m.text", "m.new_content": { "body": "//dev/null is my favourite place", diff --git a/test/unit-tests/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx b/test/unit-tests/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx index 167618e452..a76c7a8b90 100644 --- a/test/unit-tests/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx +++ b/test/unit-tests/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx @@ -196,9 +196,9 @@ describe("EditWysiwygComposer", () => { // Then screen.getByText("Save").click(); const expectedContent = { - "body": ` * foo bar`, + "body": `* foo bar`, "format": "org.matrix.custom.html", - "formatted_body": ` * foo bar`, + "formatted_body": `* foo bar`, "m.new_content": { body: "foo bar", format: "org.matrix.custom.html", diff --git a/test/unit-tests/components/views/rooms/wysiwyg_composer/utils/createMessageContent-test.ts b/test/unit-tests/components/views/rooms/wysiwyg_composer/utils/createMessageContent-test.ts index f0d9ed73c1..74852e9c6d 100644 --- a/test/unit-tests/components/views/rooms/wysiwyg_composer/utils/createMessageContent-test.ts +++ b/test/unit-tests/components/views/rooms/wysiwyg_composer/utils/createMessageContent-test.ts @@ -88,9 +88,9 @@ describe("createMessageContent", () => { // Then expect(content).toEqual({ - "body": " * *__hello__ world*", + "body": "* *__hello__ world*", "format": "org.matrix.custom.html", - "formatted_body": ` * ${message}`, + "formatted_body": `* ${message}`, "msgtype": "m.text", "m.new_content": { body: "*__hello__ world*", diff --git a/test/unit-tests/components/views/rooms/wysiwyg_composer/utils/message-test.ts b/test/unit-tests/components/views/rooms/wysiwyg_composer/utils/message-test.ts index c339978f92..43320b0444 100644 --- a/test/unit-tests/components/views/rooms/wysiwyg_composer/utils/message-test.ts +++ b/test/unit-tests/components/views/rooms/wysiwyg_composer/utils/message-test.ts @@ -418,8 +418,8 @@ describe("message", () => { // Then const { msgtype, format } = mockEvent.getContent(); const expectedContent = { - "body": ` * ${newMessage}`, - "formatted_body": ` * ${newMessage}`, + "body": `* ${newMessage}`, + "formatted_body": `* ${newMessage}`, "m.new_content": { body: "Replying to this new content", format: "org.matrix.custom.html", From 2c3e01a31c648ea28a12d6356d38f1a891eb5836 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 2 Dec 2024 13:13:18 +0000 Subject: [PATCH 25/27] Provide a way to activate GIFs via the keyboard for a11y (#28611) * Provide a way to activate GIFs via the keyboard for a11y Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Remove dead code Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/messages/MImageBody.tsx | 58 ++++++++++++-------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index f77451108b..c3aeee1a54 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -51,6 +51,7 @@ interface IState { naturalHeight: number; }; hover: boolean; + focus: boolean; showImage: boolean; placeholder: Placeholder; } @@ -71,6 +72,7 @@ export default class MImageBody extends React.Component { imgError: false, imgLoaded: false, hover: false, + focus: false, showImage: SettingsStore.getValue("showImages"), placeholder: Placeholder.NoImage, }; @@ -120,30 +122,29 @@ export default class MImageBody extends React.Component { } }; - protected onImageEnter = (e: React.MouseEvent): void => { - this.setState({ hover: true }); - - if ( + private get shouldAutoplay(): boolean { + return !( !this.state.contentUrl || !this.state.showImage || !this.state.isAnimated || SettingsStore.getValue("autoplayGifs") - ) { - return; - } - const imgElement = e.currentTarget; - imgElement.src = this.state.contentUrl; + ); + } + + protected onImageEnter = (): void => { + this.setState({ hover: true }); }; - protected onImageLeave = (e: React.MouseEvent): void => { + protected onImageLeave = (): void => { this.setState({ hover: false }); + }; - const url = this.state.thumbUrl ?? this.state.contentUrl; - if (!url || !this.state.showImage || !this.state.isAnimated || SettingsStore.getValue("autoplayGifs")) { - return; - } - const imgElement = e.currentTarget; - imgElement.src = url; + private onFocus = (): void => { + this.setState({ focus: true }); + }; + + private onBlur = (): void => { + this.setState({ focus: false }); }; private reconnectedListener = createReconnectedListener((): void => { @@ -470,14 +471,20 @@ export default class MImageBody extends React.Component { let showPlaceholder = Boolean(placeholder); + const hoverOrFocus = this.state.hover || this.state.focus; if (thumbUrl && !this.state.imgError) { + let url = thumbUrl; + if (hoverOrFocus && this.shouldAutoplay) { + url = this.state.contentUrl!; + } + // Restrict the width of the thumbnail here, otherwise it will fill the container // which has the same width as the timeline // mx_MImageBody_thumbnail resizes img to exactly container size img = ( {content.body} { showPlaceholder = false; // because we're hiding the image, so don't show the placeholder. } - if (this.state.isAnimated && !SettingsStore.getValue("autoplayGifs") && !this.state.hover) { + if (this.state.isAnimated && !SettingsStore.getValue("autoplayGifs") && !hoverOrFocus) { // XXX: Arguably we may want a different label when the animated image is WEBP and not GIF gifLabel =

GIF

; } let banner: ReactNode | undefined; - if (this.state.showImage && this.state.hover) { + if (this.state.showImage && hoverOrFocus) { banner = this.getBanner(content); } @@ -568,7 +575,13 @@ export default class MImageBody extends React.Component { protected wrapImage(contentUrl: string | null | undefined, children: JSX.Element): ReactNode { if (contentUrl) { return ( - + {children} ); @@ -657,17 +670,14 @@ export default class MImageBody extends React.Component { } interface PlaceholderIProps { - hover?: boolean; maxWidth?: number; } export class HiddenImagePlaceholder extends React.PureComponent { public render(): React.ReactNode { const maxWidth = this.props.maxWidth ? this.props.maxWidth + "px" : null; - let className = "mx_HiddenImagePlaceholder"; - if (this.props.hover) className += " mx_HiddenImagePlaceholder_hover"; return ( -
+
{_t("timeline|m.image|show_image")} From e75ff818d371372e74d96899550706b85da5f282 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 2 Dec 2024 14:03:14 +0000 Subject: [PATCH 26/27] Fix code block highlighting not working reliably with many code blocks (#28613) Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/messages/TextualBody.tsx | 25 +++++++------------ src/utils/react.tsx | 23 ++++++++++++++--- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 1a87d43a45..ae99754cba 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -52,8 +52,6 @@ export default class TextualBody extends React.Component { private tooltips = new ReactRootManager(); private reactRoots = new ReactRootManager(); - private ref = createRef(); - public static contextType = RoomContext; declare public context: React.ContextType; @@ -86,7 +84,7 @@ export default class TextualBody extends React.Component { if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") { // Handle expansion and add buttons - const pres = this.ref.current?.getElementsByTagName("pre"); + const pres = [...content.getElementsByTagName("pre")]; if (pres && pres.length > 0) { for (let i = 0; i < pres.length; i++) { // If there already is a div wrapping the codeblock we want to skip this. @@ -115,13 +113,14 @@ export default class TextualBody extends React.Component { root.className = "mx_EventTile_pre_container"; // Insert containing div in place of
 block
-        pre.parentNode?.replaceChild(root, pre);
+        pre.replaceWith(root);
 
         this.reactRoots.render(
             
                 {pre}
             ,
             root,
+            pre,
         );
     }
 
@@ -196,10 +195,9 @@ export default class TextualBody extends React.Component {
                     
                 );
 
-                this.reactRoots.render(spoiler, spoilerContainer);
-
-                node.parentNode?.replaceChild(spoilerContainer, node);
+                this.reactRoots.render(spoiler, spoilerContainer, node);
 
+                node.replaceWith(spoilerContainer);
                 node = spoilerContainer;
             }
 
@@ -479,12 +477,7 @@ export default class TextualBody extends React.Component {
 
         if (isEmote) {
             return (
-                
+
{mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()} @@ -497,7 +490,7 @@ export default class TextualBody extends React.Component { } if (isNotice) { return ( -
+
{body} {widgets}
@@ -505,14 +498,14 @@ export default class TextualBody extends React.Component { } if (isCaption) { return ( -
+
{body} {widgets}
); } return ( -
+
{body} {widgets}
diff --git a/src/utils/react.tsx b/src/utils/react.tsx index 164d704d91..b78f574fa9 100644 --- a/src/utils/react.tsx +++ b/src/utils/react.tsx @@ -15,23 +15,38 @@ import { createRoot, Root } from "react-dom/client"; export class ReactRootManager { private roots: Root[] = []; private rootElements: Element[] = []; + private revertElements: Array = []; public get elements(): Element[] { return this.rootElements; } - public render(children: ReactNode, element: Element): void { - const root = createRoot(element); + /** + * Render a React component into a new root based on the given root element + * @param children the React component to render + * @param rootElement the root element to render the component into + * @param revertElement the element to replace the root element with when unmounting + */ + public render(children: ReactNode, rootElement: Element, revertElement?: Element): void { + const root = createRoot(rootElement); this.roots.push(root); - this.rootElements.push(element); + this.rootElements.push(rootElement); + this.revertElements.push(revertElement ?? null); root.render(children); } + /** + * Unmount all roots and revert the elements they were rendered into + */ public unmount(): void { while (this.roots.length) { const root = this.roots.pop()!; - this.rootElements.pop(); + const rootElement = this.rootElements.pop(); + const revertElement = this.revertElements.pop(); root.unmount(); + if (revertElement) { + rootElement?.replaceWith(revertElement); + } } } } From 06fa3481dfca51834c204614095f5cd093c9de70 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 2 Dec 2024 14:05:44 +0000 Subject: [PATCH 27/27] Remove unused scripts (#28612) Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- scripts/copy-i18n.py | 47 -------------------- scripts/fetch-develop.deps.sh | 84 ----------------------------------- scripts/genflags.sh | 64 -------------------------- 3 files changed, 195 deletions(-) delete mode 100755 scripts/copy-i18n.py delete mode 100755 scripts/fetch-develop.deps.sh delete mode 100755 scripts/genflags.sh diff --git a/scripts/copy-i18n.py b/scripts/copy-i18n.py deleted file mode 100755 index 07b1271239..0000000000 --- a/scripts/copy-i18n.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python - -import json -import sys -import os - -if len(sys.argv) < 3: - print "Usage: %s " % (sys.argv[0],) - print "eg. %s pt_BR.json pt.json" % (sys.argv[0],) - print - print "Adds any translations to that exist in but not " - sys.exit(1) - -srcpath = sys.argv[1] -dstpath = sys.argv[2] -tmppath = dstpath + ".tmp" - -with open(srcpath) as f: - src = json.load(f) - -with open(dstpath) as f: - dst = json.load(f) - -toAdd = {} -for k,v in src.iteritems(): - if k not in dst: - print "Adding %s" % (k,) - toAdd[k] = v - -# don't just json.dumps as we'll probably re-order all the keys (and they're -# not in any given order so we can't just sort_keys). Append them to the end. -with open(dstpath) as ifp: - with open(tmppath, 'w') as ofp: - for line in ifp: - strippedline = line.strip() - if strippedline in ('{', '}'): - ofp.write(line) - elif strippedline.endswith(','): - ofp.write(line) - else: - ofp.write(' '+strippedline+',') - toAddStr = json.dumps(toAdd, indent=4, separators=(',', ': '), ensure_ascii=False, encoding="utf8").strip("{}\n") - ofp.write("\n") - ofp.write(toAddStr.encode('utf8')) - ofp.write("\n") - -os.rename(tmppath, dstpath) diff --git a/scripts/fetch-develop.deps.sh b/scripts/fetch-develop.deps.sh deleted file mode 100755 index 5814b43ff7..0000000000 --- a/scripts/fetch-develop.deps.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env bash - -# Fetches the js-sdk dependency for development or testing purposes -# If there exists a branch of that dependency with the same name as -# the branch the current checkout is on, use that branch. Otherwise, -# use develop. - -set -x - -GIT_CLONE_ARGS=("$@") -[ -z "$defbranch" ] && defbranch="develop" - -# clone a specific branch of a github repo -function clone() { - org=$1 - repo=$2 - branch=$3 - - # Chop 'origin' off the start as jenkins ends up using - # branches on the origin, but this doesn't work if we - # specify the branch when cloning. - branch=${branch#origin/} - - if [ -n "$branch" ] - then - echo "Trying to use $org/$repo#$branch" - # Disable auth prompts: https://serverfault.com/a/665959 - GIT_TERMINAL_PROMPT=0 git clone https://github.com/$org/$repo.git $repo --branch $branch \ - "${GIT_CLONE_ARGS[@]}" - return $? - fi - return 1 -} - -function dodep() { - deforg=$1 - defrepo=$2 - rm -rf $defrepo - - # Try the PR author's branch in case it exists on the deps as well. - # Try the target branch of the push or PR. - # Use the default branch as the last resort. - if [[ "$BUILDKITE" == true ]]; then - # If BUILDKITE_BRANCH is set, it will contain either: - # * "branch" when the author's branch and target branch are in the same repo - # * "author:branch" when the author's branch is in their fork - # We can split on `:` into an array to check. - BUILDKITE_BRANCH_ARRAY=(${BUILDKITE_BRANCH//:/ }) - if [[ "${#BUILDKITE_BRANCH_ARRAY[@]}" == "2" ]]; then - prAuthor=${BUILDKITE_BRANCH_ARRAY[0]} - prBranch=${BUILDKITE_BRANCH_ARRAY[1]} - else - prAuthor=$deforg - prBranch=$BUILDKITE_BRANCH - fi - clone $prAuthor $defrepo $prBranch || - clone $deforg $defrepo $BUILDKITE_PULL_REQUEST_BASE_BRANCH || - clone $deforg $defrepo $defbranch || - return $? - else - clone $deforg $defrepo $ghprbSourceBranch || - clone $deforg $defrepo $GIT_BRANCH || - clone $deforg $defrepo `git rev-parse --abbrev-ref HEAD` || - clone $deforg $defrepo $defbranch || - return $? - fi - - echo "$defrepo set to branch "`git -C "$defrepo" rev-parse --abbrev-ref HEAD` -} - -############################## - -echo 'Setting up matrix-js-sdk' - -dodep matrix-org matrix-js-sdk - -pushd matrix-js-sdk -yarn link -yarn install --frozen-lockfile -popd - -yarn link matrix-js-sdk - -############################## diff --git a/scripts/genflags.sh b/scripts/genflags.sh deleted file mode 100755 index aa882a99b4..0000000000 --- a/scripts/genflags.sh +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright 2017-2024 New Vector Ltd. - -# SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -# Please see LICENSE in the repository root for full details. - - -# genflags.sh - Generates pngs for use with CountryDropdown.js -# -# Dependencies: -# - imagemagick --with-rsvg (because default imagemagick SVG -# renderer does not produce accurate results) -# -# on macOS, this is most easily done with: -# brew install imagemagick --with-librsvg -# -# This will clone the googlei18n flag repo before converting -# all phonenumber.js-supported country flags (as SVGs) into -# PNGs that can be used by CountryDropdown.js. - -set -e - -# Allow CTRL+C to terminate the script -trap "echo Exited!; exit;" SIGINT SIGTERM - -# git clone the google repo to get flag SVGs -git clone git@github.com:googlei18n/region-flags -for f in region-flags/svg/*.svg; do - # Skip state flags - if [[ $f =~ [A-Z]{2}-[A-Z]{2,3}.svg ]] ; then - echo "Skipping state flag "$f - continue - fi - - # Skip countries not included in phonenumber.js - if [[ $f =~ (AC|CP|DG|EA|EU|IC|TA|UM|UN|XK).svg ]] ; then - echo "Skipping non-phonenumber supported flag "$f - continue - fi - - # Run imagemagick convert - # -background none : transparent background - # -resize 50x30 : resize the flag to have a height of 15px (2x) - # By default, aspect ratio is respected so the width will - # be correct and not necessarily 25px. - # -filter Lanczos : use sharper resampling to avoid muddiness - # -gravity Center : keep the image central when adding an -extent - # -border 1 : add a 1px border around the flag - # -bordercolor : set the border colour - # -extent 54x54 : surround the image with padding so that it - # has the dimensions 27x27px (2x). - convert $f -background none -filter Lanczos -resize 50x30 \ - -gravity Center -border 1 -bordercolor \#e0e0e0 \ - -extent 54x54 $f.png - - # $f.png will be region-flags/svg/XX.svg.png at this point - - # Extract filename from path $f - newname=${f##*/} - # Replace .svg with .png - newname=${newname%.svg}.png - # Move the file to flags directory - mv $f.png ../res/flags/$newname - echo "Generated res/flags/"$newname -done