From fd509c1fcf72b567be8953f0a6686ece6f7b68b0 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 29 Nov 2022 15:39:38 +0000 Subject: [PATCH 01/19] Upgrade matrix-js-sdk to 22.0.0-rc.1 --- package.json | 2 +- yarn.lock | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index c3ab6219a7..b2234466fe 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "maplibre-gl": "^1.15.2", "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "0.0.1", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "22.0.0-rc.1", "matrix-widget-api": "^1.1.1", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/yarn.lock b/yarn.lock index 4cbb91c6ad..13f0be9c65 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7167,9 +7167,10 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": - version "21.2.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/b318a77ecef179a6fd288cdf32d3ff9c5e8ea989" +matrix-js-sdk@22.0.0-rc.1: + version "22.0.0-rc.1" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-22.0.0-rc.1.tgz#e590b204b39179cd1c48ea6d577f3ac96989a5b7" + integrity sha512-6BLXHle0QIpgccpFE7EQq2IhTnsbhomCx0NZJ9URIY08M2aznvVxM2XfTi+LGjRKhv7yu8TueJaW7sGlsqZ79w== dependencies: "@babel/runtime" "^7.12.5" "@types/sdp-transform" "^2.4.5" From de8e8a33de4bfee763b536e462e3facf4221feb3 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 29 Nov 2022 15:41:18 +0000 Subject: [PATCH 02/19] Prepare changelog for v3.62.0-rc.1 --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca24b8e447..d2e2dce388 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,29 @@ +Changes in [3.62.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.62.0-rc.1) (2022-11-29) +=============================================================================================================== + +## ✨ Features + * Further improve replies ([\#6396](https://github.com/matrix-org/matrix-react-sdk/pull/6396)). Fixes vector-im/element-web#19074, vector-im/element-web#18194 vector-im/element-web#18027 and vector-im/element-web#19179. + * Enable users to join group calls from multiple devices ([\#9625](https://github.com/matrix-org/matrix-react-sdk/pull/9625)). + * fix(visual): make cursor a pointer for summaries ([\#9419](https://github.com/matrix-org/matrix-react-sdk/pull/9419)). Contributed by @r00ster91. + * Add placeholder for rich text editor ([\#9613](https://github.com/matrix-org/matrix-react-sdk/pull/9613)). + * Consolidate public room search experience ([\#9605](https://github.com/matrix-org/matrix-react-sdk/pull/9605)). Fixes vector-im/element-web#22846. + * New password reset flow ([\#9581](https://github.com/matrix-org/matrix-react-sdk/pull/9581)). Fixes vector-im/element-web#23131. + * Device manager - add tooltip to device details toggle ([\#9594](https://github.com/matrix-org/matrix-react-sdk/pull/9594)). + * sliding sync: add lazy-loading member support ([\#9530](https://github.com/matrix-org/matrix-react-sdk/pull/9530)). + * Limit formatting bar offset to top of composer ([\#9365](https://github.com/matrix-org/matrix-react-sdk/pull/9365)). Fixes vector-im/element-web#12359. Contributed by @owi92. + +## 🐛 Bug Fixes + * Fix issues around up arrow event edit shortcut ([\#9645](https://github.com/matrix-org/matrix-react-sdk/pull/9645)). Fixes vector-im/element-web#18497 and vector-im/element-web#18964. + * Fix search not being cleared when clicking on a result ([\#9635](https://github.com/matrix-org/matrix-react-sdk/pull/9635)). Fixes vector-im/element-web#23845. + * Fix screensharing in 1:1 calls ([\#9612](https://github.com/matrix-org/matrix-react-sdk/pull/9612)). Fixes vector-im/element-web#23808. + * Fix the background color flashing when joining a call ([\#9640](https://github.com/matrix-org/matrix-react-sdk/pull/9640)). + * Fix the size of the 'Private space' icon ([\#9638](https://github.com/matrix-org/matrix-react-sdk/pull/9638)). + * Fix reply editing in rich text editor (https ([\#9615](https://github.com/matrix-org/matrix-react-sdk/pull/9615)). + * Fix thread list jumping back down while scrolling ([\#9606](https://github.com/matrix-org/matrix-react-sdk/pull/9606)). Fixes vector-im/element-web#23727. + * Fix regression with TimelinePanel props updates not taking effect ([\#9608](https://github.com/matrix-org/matrix-react-sdk/pull/9608)). Fixes vector-im/element-web#23794. + * Fix form tooltip positioning ([\#9598](https://github.com/matrix-org/matrix-react-sdk/pull/9598)). Fixes vector-im/element-web#22861. + * Extract Search handling from RoomView into its own Component ([\#9574](https://github.com/matrix-org/matrix-react-sdk/pull/9574)). Fixes vector-im/element-web#498. + Changes in [3.61.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.61.0) (2022-11-22) ===================================================================================================== From 1216580bafef8665f6bb65e45c747bb4b3407bae Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 29 Nov 2022 15:41:19 +0000 Subject: [PATCH 03/19] v3.62.0-rc.1 --- package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index b2234466fe..55302f0183 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.61.0", + "version": "3.62.0-rc.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -23,7 +23,7 @@ "package.json", ".stylelintrc.js" ], - "main": "./src/index.ts", + "main": "./lib/index.ts", "matrix_src_main": "./src/index.ts", "matrix_lib_main": "./lib/index.ts", "matrix_lib_typings": "./lib/index.d.ts", @@ -257,5 +257,6 @@ "outputDirectory": "coverage", "outputName": "jest-sonar-report.xml", "relativePaths": true - } + }, + "typings": "./lib/index.d.ts" } From f4b6719a2825211cf1e337809714884bd440abcd Mon Sep 17 00:00:00 2001 From: ElementRobot Date: Thu, 1 Dec 2022 20:34:45 +0000 Subject: [PATCH 04/19] [Backport staging] Start voice broadcast recording while listening (#9659) Co-authored-by: Michael Weimann --- .../views/rooms/MessageComposer.tsx | 1 + src/components/views/voip/PipView.tsx | 8 +- .../models/VoiceBroadcastPreRecording.ts | 3 + .../utils/setUpVoiceBroadcastPreRecording.ts | 8 +- .../utils/startNewVoiceBroadcastRecording.ts | 7 ++ test/components/views/voip/PipView-test.tsx | 14 ++++ .../VoiceBroadcastPreRecordingPip-test.tsx | 4 + .../models/VoiceBroadcastPreRecording-test.ts | 6 +- .../VoiceBroadcastPreRecordingStore-test.ts | 7 +- .../setUpVoiceBroadcastPreRecording-test.ts | 42 +++++++++-- .../startNewVoiceBroadcastRecording-test.ts | 75 ++++++++++++------- 11 files changed, 135 insertions(+), 40 deletions(-) diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 152c592a02..6fe5923a29 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -584,6 +584,7 @@ export class MessageComposer extends React.Component { setUpVoiceBroadcastPreRecording( this.props.room, MatrixClientPeg.get(), + SdkContextClass.instance.voiceBroadcastPlaybacksStore, VoiceBroadcastRecordingsStore.instance(), SdkContextClass.instance.voiceBroadcastPreRecordingStore, ); diff --git a/src/components/views/voip/PipView.tsx b/src/components/views/voip/PipView.tsx index 27f7798f11..40a59710d4 100644 --- a/src/components/views/voip/PipView.tsx +++ b/src/components/views/voip/PipView.tsx @@ -367,14 +367,14 @@ class PipView extends React.Component { const pipMode = true; let pipContent: CreatePipChildren | null = null; - if (this.props.voiceBroadcastPreRecording) { - pipContent = this.createVoiceBroadcastPreRecordingPipContent(this.props.voiceBroadcastPreRecording); - } - if (this.props.voiceBroadcastPlayback) { pipContent = this.createVoiceBroadcastPlaybackPipContent(this.props.voiceBroadcastPlayback); } + if (this.props.voiceBroadcastPreRecording) { + pipContent = this.createVoiceBroadcastPreRecordingPipContent(this.props.voiceBroadcastPreRecording); + } + if (this.props.voiceBroadcastRecording) { pipContent = this.createVoiceBroadcastRecordingPipContent(this.props.voiceBroadcastRecording); } diff --git a/src/voice-broadcast/models/VoiceBroadcastPreRecording.ts b/src/voice-broadcast/models/VoiceBroadcastPreRecording.ts index f1e956c600..10995e5d49 100644 --- a/src/voice-broadcast/models/VoiceBroadcastPreRecording.ts +++ b/src/voice-broadcast/models/VoiceBroadcastPreRecording.ts @@ -18,6 +18,7 @@ import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix"; import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter"; import { IDestroyable } from "../../utils/IDestroyable"; +import { VoiceBroadcastPlaybacksStore } from "../stores/VoiceBroadcastPlaybacksStore"; import { VoiceBroadcastRecordingsStore } from "../stores/VoiceBroadcastRecordingsStore"; import { startNewVoiceBroadcastRecording } from "../utils/startNewVoiceBroadcastRecording"; @@ -34,6 +35,7 @@ export class VoiceBroadcastPreRecording public room: Room, public sender: RoomMember, private client: MatrixClient, + private playbacksStore: VoiceBroadcastPlaybacksStore, private recordingsStore: VoiceBroadcastRecordingsStore, ) { super(); @@ -43,6 +45,7 @@ export class VoiceBroadcastPreRecording await startNewVoiceBroadcastRecording( this.room, this.client, + this.playbacksStore, this.recordingsStore, ); this.emit("dismiss", this); diff --git a/src/voice-broadcast/utils/setUpVoiceBroadcastPreRecording.ts b/src/voice-broadcast/utils/setUpVoiceBroadcastPreRecording.ts index 8bd211f612..9d5d410aa2 100644 --- a/src/voice-broadcast/utils/setUpVoiceBroadcastPreRecording.ts +++ b/src/voice-broadcast/utils/setUpVoiceBroadcastPreRecording.ts @@ -18,6 +18,7 @@ import { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; import { checkVoiceBroadcastPreConditions, + VoiceBroadcastPlaybacksStore, VoiceBroadcastPreRecording, VoiceBroadcastPreRecordingStore, VoiceBroadcastRecordingsStore, @@ -26,6 +27,7 @@ import { export const setUpVoiceBroadcastPreRecording = ( room: Room, client: MatrixClient, + playbacksStore: VoiceBroadcastPlaybacksStore, recordingsStore: VoiceBroadcastRecordingsStore, preRecordingStore: VoiceBroadcastPreRecordingStore, ): VoiceBroadcastPreRecording | null => { @@ -39,7 +41,11 @@ export const setUpVoiceBroadcastPreRecording = ( const sender = room.getMember(userId); if (!sender) return null; - const preRecording = new VoiceBroadcastPreRecording(room, sender, client, recordingsStore); + // 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/startNewVoiceBroadcastRecording.ts b/src/voice-broadcast/utils/startNewVoiceBroadcastRecording.ts index ae4e40c4a3..5306a9d605 100644 --- a/src/voice-broadcast/utils/startNewVoiceBroadcastRecording.ts +++ b/src/voice-broadcast/utils/startNewVoiceBroadcastRecording.ts @@ -24,6 +24,7 @@ import { VoiceBroadcastRecordingsStore, VoiceBroadcastRecording, getChunkLength, + VoiceBroadcastPlaybacksStore, } from ".."; import { checkVoiceBroadcastPreConditions } from "./checkVoiceBroadcastPreConditions"; @@ -80,17 +81,23 @@ const startBroadcast = async ( /** * 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 (!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/test/components/views/voip/PipView-test.tsx b/test/components/views/voip/PipView-test.tsx index 1dcc617e64..6a9105a413 100644 --- a/test/components/views/voip/PipView-test.tsx +++ b/test/components/views/voip/PipView-test.tsx @@ -184,6 +184,7 @@ describe("PipView", () => { room, alice, client, + voiceBroadcastPlaybacksStore, voiceBroadcastRecordingsStore, ); voiceBroadcastPreRecordingStore.setCurrent(voiceBroadcastPreRecording); @@ -271,6 +272,19 @@ describe("PipView", () => { }); }); + describe("when there is a voice broadcast playback and pre-recording", () => { + beforeEach(() => { + startVoiceBroadcastPlayback(room); + setUpVoiceBroadcastPreRecording(); + renderPip(); + }); + + 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(() => { setUpVoiceBroadcastPreRecording(); diff --git a/test/voice-broadcast/components/molecules/VoiceBroadcastPreRecordingPip-test.tsx b/test/voice-broadcast/components/molecules/VoiceBroadcastPreRecordingPip-test.tsx index 91658f26ed..61636ce000 100644 --- a/test/voice-broadcast/components/molecules/VoiceBroadcastPreRecordingPip-test.tsx +++ b/test/voice-broadcast/components/molecules/VoiceBroadcastPreRecordingPip-test.tsx @@ -21,6 +21,7 @@ import { act, render, RenderResult, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { + VoiceBroadcastPlaybacksStore, VoiceBroadcastPreRecording, VoiceBroadcastPreRecordingPip, VoiceBroadcastRecordingsStore, @@ -42,6 +43,7 @@ jest.mock("../../../../src/components/views/avatars/RoomAvatar", () => ({ describe("VoiceBroadcastPreRecordingPip", () => { let renderResult: RenderResult; let preRecording: VoiceBroadcastPreRecording; + let playbacksStore: VoiceBroadcastPlaybacksStore; let recordingsStore: VoiceBroadcastRecordingsStore; let client: MatrixClient; let room: Room; @@ -51,6 +53,7 @@ describe("VoiceBroadcastPreRecordingPip", () => { client = stubClient(); room = new Room("!room@example.com", client, client.getUserId() || ""); sender = new RoomMember(room.roomId, client.getUserId() || ""); + playbacksStore = new VoiceBroadcastPlaybacksStore(); recordingsStore = new VoiceBroadcastRecordingsStore(); mocked(requestMediaPermissions).mockReturnValue(new Promise((r) => { r({ @@ -76,6 +79,7 @@ describe("VoiceBroadcastPreRecordingPip", () => { room, sender, client, + playbacksStore, recordingsStore, ); }); diff --git a/test/voice-broadcast/models/VoiceBroadcastPreRecording-test.ts b/test/voice-broadcast/models/VoiceBroadcastPreRecording-test.ts index 3a9fc11065..2c2db30b38 100644 --- a/test/voice-broadcast/models/VoiceBroadcastPreRecording-test.ts +++ b/test/voice-broadcast/models/VoiceBroadcastPreRecording-test.ts @@ -18,6 +18,7 @@ import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix"; import { startNewVoiceBroadcastRecording, + VoiceBroadcastPlaybacksStore, VoiceBroadcastPreRecording, VoiceBroadcastRecordingsStore, } from "../../../src/voice-broadcast"; @@ -30,6 +31,7 @@ describe("VoiceBroadcastPreRecording", () => { let client: MatrixClient; let room: Room; let sender: RoomMember; + let playbacksStore: VoiceBroadcastPlaybacksStore; let recordingsStore: VoiceBroadcastRecordingsStore; let preRecording: VoiceBroadcastPreRecording; let onDismiss: (voiceBroadcastPreRecording: VoiceBroadcastPreRecording) => void; @@ -38,12 +40,13 @@ describe("VoiceBroadcastPreRecording", () => { client = stubClient(); room = new Room(roomId, client, client.getUserId() || ""); sender = new RoomMember(roomId, client.getUserId() || ""); + playbacksStore = new VoiceBroadcastPlaybacksStore(); recordingsStore = new VoiceBroadcastRecordingsStore(); }); beforeEach(() => { onDismiss = jest.fn(); - preRecording = new VoiceBroadcastPreRecording(room, sender, client, recordingsStore); + preRecording = new VoiceBroadcastPreRecording(room, sender, client, playbacksStore, recordingsStore); preRecording.on("dismiss", onDismiss); }); @@ -56,6 +59,7 @@ describe("VoiceBroadcastPreRecording", () => { expect(startNewVoiceBroadcastRecording).toHaveBeenCalledWith( room, client, + playbacksStore, recordingsStore, ); }); diff --git a/test/voice-broadcast/stores/VoiceBroadcastPreRecordingStore-test.ts b/test/voice-broadcast/stores/VoiceBroadcastPreRecordingStore-test.ts index 36983ae601..97e944b564 100644 --- a/test/voice-broadcast/stores/VoiceBroadcastPreRecordingStore-test.ts +++ b/test/voice-broadcast/stores/VoiceBroadcastPreRecordingStore-test.ts @@ -18,6 +18,7 @@ import { mocked } from "jest-mock"; import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix"; import { + VoiceBroadcastPlaybacksStore, VoiceBroadcastPreRecording, VoiceBroadcastPreRecordingStore, VoiceBroadcastRecordingsStore, @@ -31,6 +32,7 @@ describe("VoiceBroadcastPreRecordingStore", () => { let client: MatrixClient; let room: Room; let sender: RoomMember; + let playbacksStore: VoiceBroadcastPlaybacksStore; let recordingsStore: VoiceBroadcastRecordingsStore; let store: VoiceBroadcastPreRecordingStore; let preRecording1: VoiceBroadcastPreRecording; @@ -39,6 +41,7 @@ describe("VoiceBroadcastPreRecordingStore", () => { client = stubClient(); room = new Room(roomId, client, client.getUserId() || ""); sender = new RoomMember(roomId, client.getUserId() || ""); + playbacksStore = new VoiceBroadcastPlaybacksStore(); recordingsStore = new VoiceBroadcastRecordingsStore(); }); @@ -46,7 +49,7 @@ describe("VoiceBroadcastPreRecordingStore", () => { store = new VoiceBroadcastPreRecordingStore(); jest.spyOn(store, "emit"); jest.spyOn(store, "removeAllListeners"); - preRecording1 = new VoiceBroadcastPreRecording(room, sender, client, recordingsStore); + preRecording1 = new VoiceBroadcastPreRecording(room, sender, client, playbacksStore, recordingsStore); jest.spyOn(preRecording1, "off"); }); @@ -117,7 +120,7 @@ describe("VoiceBroadcastPreRecordingStore", () => { beforeEach(() => { mocked(store.emit).mockClear(); mocked(preRecording1.off).mockClear(); - preRecording2 = new VoiceBroadcastPreRecording(room, sender, client, recordingsStore); + preRecording2 = new VoiceBroadcastPreRecording(room, sender, client, playbacksStore, recordingsStore); store.setCurrent(preRecording2); }); diff --git a/test/voice-broadcast/utils/setUpVoiceBroadcastPreRecording-test.ts b/test/voice-broadcast/utils/setUpVoiceBroadcastPreRecording-test.ts index 0b05d26912..4779813165 100644 --- a/test/voice-broadcast/utils/setUpVoiceBroadcastPreRecording-test.ts +++ b/test/voice-broadcast/utils/setUpVoiceBroadcastPreRecording-test.ts @@ -15,16 +15,20 @@ limitations under the License. */ import { mocked } from "jest-mock"; -import { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; +import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; import { checkVoiceBroadcastPreConditions, + 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/voice-broadcast/utils/checkVoiceBroadcastPreConditions"); @@ -34,11 +38,20 @@ describe("setUpVoiceBroadcastPreRecording", () => { let userId: string; let room: Room; let preRecordingStore: VoiceBroadcastPreRecordingStore; + let infoEvent: MatrixEvent; + let playback: VoiceBroadcastPlayback; + let playbacksStore: VoiceBroadcastPlaybacksStore; let recordingsStore: VoiceBroadcastRecordingsStore; const itShouldReturnNull = () => { it("should return null", () => { - expect(setUpVoiceBroadcastPreRecording(room, client, recordingsStore, preRecordingStore)).toBeNull(); + expect(setUpVoiceBroadcastPreRecording( + room, + client, + playbacksStore, + recordingsStore, + preRecordingStore, + )).toBeNull(); expect(checkVoiceBroadcastPreConditions).toHaveBeenCalledWith(room, client, recordingsStore); }); }; @@ -51,7 +64,16 @@ describe("setUpVoiceBroadcastPreRecording", () => { userId = clientUserId; room = new Room(roomId, client, userId); + infoEvent = mkVoiceBroadcastInfoStateEvent( + roomId, + VoiceBroadcastInfoState.Started, + client.getUserId()!, + client.getDeviceId()!, + ); preRecordingStore = new VoiceBroadcastPreRecordingStore(); + playback = new VoiceBroadcastPlayback(infoEvent, client); + jest.spyOn(playback, "pause"); + playbacksStore = new VoiceBroadcastPlaybacksStore(); recordingsStore = new VoiceBroadcastRecordingsStore(); }); @@ -85,15 +107,25 @@ describe("setUpVoiceBroadcastPreRecording", () => { itShouldReturnNull(); }); - describe("and there is a room member", () => { + describe("and there is a room member and listening to another broadcast", () => { beforeEach(() => { + playbacksStore.setCurrent(playback); room.currentState.setStateEvents([ mkRoomMemberJoinEvent(userId, roomId), ]); }); - it("should create a voice broadcast pre-recording", () => { - const result = setUpVoiceBroadcastPreRecording(room, client, recordingsStore, preRecordingStore); + it("should pause the current playback and create a voice broadcast pre-recording", () => { + const result = setUpVoiceBroadcastPreRecording( + room, + client, + playbacksStore, + recordingsStore, + preRecordingStore, + ); + expect(playback.pause).toHaveBeenCalled(); + expect(playbacksStore.getCurrent()).toBeNull(); + expect(checkVoiceBroadcastPreConditions).toHaveBeenCalledWith(room, client, recordingsStore); expect(result).toBeInstanceOf(VoiceBroadcastPreRecording); }); diff --git a/test/voice-broadcast/utils/startNewVoiceBroadcastRecording-test.ts b/test/voice-broadcast/utils/startNewVoiceBroadcastRecording-test.ts index 1873a4b513..448a18a746 100644 --- a/test/voice-broadcast/utils/startNewVoiceBroadcastRecording-test.ts +++ b/test/voice-broadcast/utils/startNewVoiceBroadcastRecording-test.ts @@ -15,7 +15,7 @@ limitations under the License. */ import { mocked } from "jest-mock"; -import { EventType, MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; +import { EventType, ISendEventResponse, MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; import Modal from "../../../src/Modal"; import { @@ -24,6 +24,8 @@ import { VoiceBroadcastInfoState, VoiceBroadcastRecordingsStore, VoiceBroadcastRecording, + VoiceBroadcastPlaybacksStore, + VoiceBroadcastPlayback, } from "../../../src/voice-broadcast"; import { mkEvent, stubClient } from "../../test-utils"; import { mkVoiceBroadcastInfoStateEvent } from "./test-utils"; @@ -38,6 +40,7 @@ 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; @@ -46,45 +49,50 @@ describe("startNewVoiceBroadcastRecording", () => { beforeEach(() => { client = stubClient(); - room = new Room(roomId, client, client.getUserId()); + 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, - ) => { + content: any, + stateKey: string, + ): Promise => { if (sendRoomId === roomId && eventType === VoiceBroadcastInfoEventType) { - return Promise.resolve({ event_id: infoEvent.getId() }); + return Promise.resolve({ event_id: infoEvent.getId()! }); } - }); - recordingsStore = { - setCurrent: jest.fn(), - getCurrent: jest.fn(), - } as unknown as VoiceBroadcastRecordingsStore; + throw new Error("Unexpected sendStateEvent call"); + }); infoEvent = mkVoiceBroadcastInfoStateEvent( roomId, VoiceBroadcastInfoState.Started, - client.getUserId(), - client.getDeviceId(), + client.getUserId()!, + client.getDeviceId()!, ); otherEvent = mkEvent({ event: true, type: EventType.RoomMember, content: {}, - user: client.getUserId(), + user: client.getUserId()!, room: roomId, skey: "", }); + playbacksStore = new VoiceBroadcastPlaybacksStore(); + recordingsStore = { + setCurrent: jest.fn(), + getCurrent: jest.fn(), + } as unknown as VoiceBroadcastRecordingsStore; + mocked(VoiceBroadcastRecording).mockImplementation(( infoEvent: MatrixEvent, client: MatrixClient, @@ -106,22 +114,35 @@ describe("startNewVoiceBroadcastRecording", () => { mocked(room.currentState.maySendStateEvent).mockReturnValue(true); }); - describe("when there currently is no other broadcast", () => { - it("should create a new Voice Broadcast", async () => { + describe("when currently listening to a broadcast and there is no recording", () => { + let playback: VoiceBroadcastPlayback; + + beforeEach(() => { + playback = new VoiceBroadcastPlayback(infoEvent, client); + 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 => { setTimeout(() => { // emit state events after resolving the promise room.currentState.setStateEvents([otherEvent]); room.currentState.setStateEvents([infoEvent]); }, 0); - return { event_id: infoEvent.getId() }; + return { event_id: infoEvent.getId()! }; }); - const recording = await startNewVoiceBroadcastRecording(room, client, recordingsStore); + 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, @@ -133,8 +154,8 @@ describe("startNewVoiceBroadcastRecording", () => { }, client.getUserId(), ); - expect(recording.infoEvent).toBe(infoEvent); - expect(recording.start).toHaveBeenCalled(); + expect(recording!.infoEvent).toBe(infoEvent); + expect(recording!.start).toHaveBeenCalled(); }); }); @@ -144,7 +165,7 @@ describe("startNewVoiceBroadcastRecording", () => { new VoiceBroadcastRecording(infoEvent, client), ); - result = await startNewVoiceBroadcastRecording(room, client, recordingsStore); + result = await startNewVoiceBroadcastRecording(room, client, playbacksStore, recordingsStore); }); it("should not start a voice broadcast", () => { @@ -162,12 +183,12 @@ describe("startNewVoiceBroadcastRecording", () => { mkVoiceBroadcastInfoStateEvent( roomId, VoiceBroadcastInfoState.Resumed, - client.getUserId(), - client.getDeviceId(), + client.getUserId()!, + client.getDeviceId()!, ), ]); - result = await startNewVoiceBroadcastRecording(room, client, recordingsStore); + result = await startNewVoiceBroadcastRecording(room, client, playbacksStore, recordingsStore); }); it("should not start a voice broadcast", () => { @@ -190,7 +211,7 @@ describe("startNewVoiceBroadcastRecording", () => { ), ]); - result = await startNewVoiceBroadcastRecording(room, client, recordingsStore); + result = await startNewVoiceBroadcastRecording(room, client, playbacksStore, recordingsStore); }); it("should not start a voice broadcast", () => { @@ -206,7 +227,7 @@ describe("startNewVoiceBroadcastRecording", () => { 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, recordingsStore); + result = await startNewVoiceBroadcastRecording(room, client, playbacksStore, recordingsStore); }); it("should not start a voice broadcast", () => { From fcc49d09592c7023255bc8d52b8c68f4d489cf62 Mon Sep 17 00:00:00 2001 From: ElementRobot Date: Thu, 1 Dec 2022 20:34:57 +0000 Subject: [PATCH 05/19] [Backport staging] Update Voice Broadcast buffering style (#9660) Co-authored-by: Michael Weimann --- .../atoms/_VoiceBroadcastHeader.pcss | 5 +- src/i18n/strings/en_EN.json | 1 + .../components/atoms/VoiceBroadcastHeader.tsx | 41 +++++++----- .../molecules/VoiceBroadcastPlaybackBody.tsx | 61 ++++++++--------- .../hooks/useVoiceBroadcastPlayback.ts | 7 ++ .../atoms/VoiceBroadcastHeader-test.tsx | 17 ++++- .../VoiceBroadcastHeader-test.tsx.snap | 66 +++++++++++++++++++ .../VoiceBroadcastPlaybackBody-test.tsx.snap | 25 ++++--- 8 files changed, 163 insertions(+), 60 deletions(-) diff --git a/res/css/voice-broadcast/atoms/_VoiceBroadcastHeader.pcss b/res/css/voice-broadcast/atoms/_VoiceBroadcastHeader.pcss index 1ff29bd985..90092a35ac 100644 --- a/res/css/voice-broadcast/atoms/_VoiceBroadcastHeader.pcss +++ b/res/css/voice-broadcast/atoms/_VoiceBroadcastHeader.pcss @@ -40,8 +40,9 @@ limitations under the License. display: flex; gap: $spacing-4; - i { - flex-shrink: 0; + .mx_Spinner { + flex: 0 0 14px; + padding: 1px; } span { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 376133905d..9922fa0413 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -657,6 +657,7 @@ "Change input device": "Change input device", "Live": "Live", "Voice broadcast": "Voice broadcast", + "Buffering…": "Buffering…", "Cannot reach homeserver": "Cannot reach homeserver", "Ensure you have a stable internet connection, or get in touch with the server admin": "Ensure you have a stable internet connection, or get in touch with the server admin", "Your %(brand)s is misconfigured": "Your %(brand)s is misconfigured", diff --git a/src/voice-broadcast/components/atoms/VoiceBroadcastHeader.tsx b/src/voice-broadcast/components/atoms/VoiceBroadcastHeader.tsx index be31cd4efe..64640ca793 100644 --- a/src/voice-broadcast/components/atoms/VoiceBroadcastHeader.tsx +++ b/src/voice-broadcast/components/atoms/VoiceBroadcastHeader.tsx @@ -25,6 +25,7 @@ import AccessibleButton from "../../../components/views/elements/AccessibleButto import { Icon as XIcon } from "../../../../res/img/element-icons/cancel-rounded.svg"; import Clock from "../../../components/views/audio_messages/Clock"; import { formatTimeLeft } from "../../../DateUtils"; +import Spinner from "../../../components/views/elements/Spinner"; interface VoiceBroadcastHeaderProps { live?: VoiceBroadcastLiveness; @@ -33,6 +34,7 @@ interface VoiceBroadcastHeaderProps { room: Room; microphoneLabel?: string; showBroadcast?: boolean; + showBuffering?: boolean; timeLeft?: number; showClose?: boolean; } @@ -44,47 +46,55 @@ export const VoiceBroadcastHeader: React.FC = ({ room, microphoneLabel, showBroadcast = false, + showBuffering = false, showClose = false, timeLeft, }) => { - const broadcast = showBroadcast - ?
+ const broadcast = showBroadcast && ( +
{ _t("Voice broadcast") }
- : null; + ); - const liveBadge = live === "not-live" - ? null - : ; + const liveBadge = live !== "not-live" && ( + + ); - const closeButton = showClose - ? + const closeButton = showClose && ( + - : null; + ); - const timeLeftLine = timeLeft - ?
+ const timeLeftLine = timeLeft && ( +
- : null; + ); + + const buffering = showBuffering && ( +
+ + { _t("Buffering…") } +
+ ); const microphoneLineClasses = classNames({ mx_VoiceBroadcastHeader_line: true, ["mx_VoiceBroadcastHeader_mic--clickable"]: onMicrophoneLineClick, }); - const microphoneLine = microphoneLabel - ?
{ microphoneLabel }
- : null; + ); return
@@ -95,6 +105,7 @@ export const VoiceBroadcastHeader: React.FC = ({ { microphoneLine } { timeLeftLine } { broadcast } + { buffering }
{ liveBadge } { closeButton } diff --git a/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx b/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx index 6c16223388..1601fbb363 100644 --- a/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx +++ b/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx @@ -23,7 +23,6 @@ import { VoiceBroadcastPlayback, VoiceBroadcastPlaybackState, } from "../.."; -import Spinner from "../../../components/views/elements/Spinner"; import { useVoiceBroadcastPlayback } from "../../hooks/useVoiceBroadcastPlayback"; import { Icon as PlayIcon } from "../../../../res/img/element-icons/play.svg"; import { Icon as PauseIcon } from "../../../../res/img/element-icons/pause.svg"; @@ -55,40 +54,35 @@ export const VoiceBroadcastPlaybackBody: React.FC>; + let controlLabel: string; + let className = ""; - if (playbackState === VoiceBroadcastPlaybackState.Buffering) { - control = ; - } else { - let controlIcon: React.FC>; - let controlLabel: string; - let className = ""; - - switch (playbackState) { - case VoiceBroadcastPlaybackState.Stopped: - controlIcon = PlayIcon; - className = "mx_VoiceBroadcastControl-play"; - controlLabel = _t("play voice broadcast"); - break; - case VoiceBroadcastPlaybackState.Paused: - controlIcon = PlayIcon; - className = "mx_VoiceBroadcastControl-play"; - controlLabel = _t("resume voice broadcast"); - break; - case VoiceBroadcastPlaybackState.Playing: - controlIcon = PauseIcon; - controlLabel = _t("pause voice broadcast"); - break; - } - - control = ; + switch (playbackState) { + case VoiceBroadcastPlaybackState.Stopped: + controlIcon = PlayIcon; + className = "mx_VoiceBroadcastControl-play"; + controlLabel = _t("play voice broadcast"); + break; + case VoiceBroadcastPlaybackState.Paused: + controlIcon = PlayIcon; + className = "mx_VoiceBroadcastControl-play"; + controlLabel = _t("resume voice broadcast"); + break; + case VoiceBroadcastPlaybackState.Buffering: + case VoiceBroadcastPlaybackState.Playing: + controlIcon = PauseIcon; + controlLabel = _t("pause voice broadcast"); + break; } + const control = ; + let seekBackwardButton: ReactElement | null = null; let seekForwardButton: ReactElement | null = null; @@ -125,7 +119,8 @@ export const VoiceBroadcastPlaybackBody: React.FC
{ seekBackwardButton } diff --git a/src/voice-broadcast/hooks/useVoiceBroadcastPlayback.ts b/src/voice-broadcast/hooks/useVoiceBroadcastPlayback.ts index 1828b31d01..fb27abfab9 100644 --- a/src/voice-broadcast/hooks/useVoiceBroadcastPlayback.ts +++ b/src/voice-broadcast/hooks/useVoiceBroadcastPlayback.ts @@ -27,6 +27,13 @@ import { export const useVoiceBroadcastPlayback = (playback: VoiceBroadcastPlayback) => { const client = MatrixClientPeg.get(); const room = client.getRoom(playback.infoEvent.getRoomId()); + + if (!room) { + throw new Error( + `Voice Broadcast room not found (event ${playback.infoEvent.getId()})`, + ); + } + const playbackToggle = () => { playback.toggle(); }; diff --git a/test/voice-broadcast/components/atoms/VoiceBroadcastHeader-test.tsx b/test/voice-broadcast/components/atoms/VoiceBroadcastHeader-test.tsx index f056137813..e090841c82 100644 --- a/test/voice-broadcast/components/atoms/VoiceBroadcastHeader-test.tsx +++ b/test/voice-broadcast/components/atoms/VoiceBroadcastHeader-test.tsx @@ -35,12 +35,17 @@ describe("VoiceBroadcastHeader", () => { const sender = new RoomMember(roomId, userId); let container: Container; - const renderHeader = (live: VoiceBroadcastLiveness, showBroadcast: boolean = undefined): RenderResult => { + const renderHeader = ( + live: VoiceBroadcastLiveness, + showBroadcast?: boolean, + buffering?: boolean, + ): RenderResult => { return render(); }; @@ -51,6 +56,16 @@ describe("VoiceBroadcastHeader", () => { }); 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; }); diff --git a/test/voice-broadcast/components/atoms/__snapshots__/VoiceBroadcastHeader-test.tsx.snap b/test/voice-broadcast/components/atoms/__snapshots__/VoiceBroadcastHeader-test.tsx.snap index 1f4b657a22..c00d81e37d 100644 --- a/test/voice-broadcast/components/atoms/__snapshots__/VoiceBroadcastHeader-test.tsx.snap +++ b/test/voice-broadcast/components/atoms/__snapshots__/VoiceBroadcastHeader-test.tsx.snap @@ -1,5 +1,55 @@ // 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`] = `
Voice broadcast
+
+
+
+
+ Buffering… +
- Voice broadcast + class="mx_Spinner" + > +
+
+ Buffering…
Date: Fri, 2 Dec 2022 10:35:04 +0000 Subject: [PATCH 06/19] [Backport staging] Fix call splitbrains when switching between rooms (#9701) * Fix call splitbrains when switching between rooms Mounting CallView causes the user's call membership room state to be cleaned up. However, because the GroupCall object always thought the local device was disconnected from the call, it would remove the local device from room state when the clean method is called, causing a splitbrain. This uses GroupCall's new enteredViaAnotherSession field to fix that, and also simplify participant tracking. * Remove clean tests that have been moved to matrix-js-sdk (cherry picked from commit 62a740d318980c98475aa564ef54bfc27e6d3880) Co-authored-by: Robin --- src/models/Call.ts | 27 +---------- test/models/Call-test.ts | 90 ----------------------------------- test/test-utils/test-utils.ts | 1 + 3 files changed, 3 insertions(+), 115 deletions(-) diff --git a/src/models/Call.ts b/src/models/Call.ts index 0e20c331fb..383f3557ac 100644 --- a/src/models/Call.ts +++ b/src/models/Call.ts @@ -647,7 +647,6 @@ export class ElementCall extends Call { client, ); - this.on(CallEvent.ConnectionState, this.onConnectionState); this.on(CallEvent.Participants, this.onParticipants); groupCall.on(GroupCallEvent.ParticipantsChanged, this.onGroupCallParticipants); groupCall.on(GroupCallEvent.GroupCallStateChanged, this.onGroupCallState); @@ -704,6 +703,7 @@ export class ElementCall extends Call { throw new Error(`Failed to join call in room ${this.roomId}: ${e}`); } + this.groupCall.enteredViaAnotherSession = true; this.messaging!.on(`action:${ElementWidgetActions.HangupCall}`, this.onHangup); this.messaging!.on(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout); this.messaging!.on(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout); @@ -724,11 +724,11 @@ export class ElementCall extends Call { this.messaging!.off(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout); this.messaging!.off(`action:${ElementWidgetActions.ScreenshareRequest}`, this.onScreenshareRequest); super.setDisconnected(); + this.groupCall.enteredViaAnotherSession = false; } public destroy() { WidgetStore.instance.removeVirtualWidget(this.widget.id, this.groupCall.room.roomId); - this.off(CallEvent.ConnectionState, this.onConnectionState); this.off(CallEvent.Participants, this.onParticipants); this.groupCall.off(GroupCallEvent.ParticipantsChanged, this.onGroupCallParticipants); this.groupCall.off(GroupCallEvent.GroupCallStateChanged, this.onGroupCallState); @@ -760,20 +760,6 @@ export class ElementCall extends Call { participants.set(member, new Set(deviceMap.keys())); } - // We never enter group calls natively, so the GroupCall will think it's - // disconnected regardless of what our call member state says. Thus we - // have to insert our own device manually when connected via the widget. - if (this.connected) { - const localMember = this.room.getMember(this.client.getUserId()!)!; - let devices = participants.get(localMember); - if (devices === undefined) { - devices = new Set(); - participants.set(localMember, devices); - } - - devices.add(this.client.getDeviceId()!); - } - this.participants = participants; } @@ -782,15 +768,6 @@ export class ElementCall extends Call { && this.room.currentState.mayClientSendStateEvent(ElementCall.CALL_EVENT_TYPE.name, this.client); } - private onConnectionState = (state: ConnectionState, prevState: ConnectionState) => { - if ( - (state === ConnectionState.Connected && !isConnected(prevState)) - || (state === ConnectionState.Disconnected && isConnected(prevState)) - ) { - this.updateParticipants(); // Local echo - } - }; - private onParticipants = async ( participants: Map>, prevParticipants: Map>, diff --git a/test/models/Call-test.ts b/test/models/Call-test.ts index aa22db2718..785b9eea58 100644 --- a/test/models/Call-test.ts +++ b/test/models/Call-test.ts @@ -939,96 +939,6 @@ describe("ElementCall", () => { call.off(CallEvent.Destroy, onDestroy); }); - - describe("clean", () => { - const aliceWeb: IMyDevice = { - device_id: "aliceweb", - last_seen_ts: 0, - }; - const aliceDesktop: IMyDevice = { - device_id: "alicedesktop", - last_seen_ts: 0, - }; - const aliceDesktopOffline: IMyDevice = { - device_id: "alicedesktopoffline", - last_seen_ts: 1000 * 60 * 60 * -2, // 2 hours ago - }; - const aliceDesktopNeverOnline: IMyDevice = { - device_id: "alicedesktopneveronline", - }; - - const mkContent = (devices: IMyDevice[]) => ({ - "m.calls": [{ - "m.call_id": call.groupCall.groupCallId, - "m.devices": devices.map(d => ({ - device_id: d.device_id, session_id: "1", feeds: [], expires_ts: 1000 * 60 * 10, - })), - }], - }); - const expectDevices = (devices: IMyDevice[]) => expect( - room.currentState.getStateEvents(ElementCall.MEMBER_EVENT_TYPE.name, alice.userId)?.getContent(), - ).toEqual({ - "m.calls": [{ - "m.call_id": call.groupCall.groupCallId, - "m.devices": devices.map(d => ({ - device_id: d.device_id, session_id: "1", feeds: [], expires_ts: expect.any(Number), - })), - }], - }); - - beforeEach(() => { - client.getDeviceId.mockReturnValue(aliceWeb.device_id); - client.getDevices.mockResolvedValue({ - devices: [ - aliceWeb, - aliceDesktop, - aliceDesktopOffline, - aliceDesktopNeverOnline, - ], - }); - }); - - it("doesn't clean up valid devices", async () => { - await client.sendStateEvent( - room.roomId, - ElementCall.MEMBER_EVENT_TYPE.name, - mkContent([aliceDesktop]), - alice.userId, - ); - - await call.clean(); - expectDevices([aliceDesktop]); - }); - - it("cleans up our own device if we're disconnected", async () => { - await client.sendStateEvent( - room.roomId, - ElementCall.MEMBER_EVENT_TYPE.name, - mkContent([aliceWeb, aliceDesktop]), - alice.userId, - ); - - await call.clean(); - expectDevices([aliceDesktop]); - }); - - it("cleans up devices that have never been online", async () => { - await client.sendStateEvent( - room.roomId, - ElementCall.MEMBER_EVENT_TYPE.name, - mkContent([aliceDesktop, aliceDesktopNeverOnline]), - alice.userId, - ); - - await call.clean(); - expectDevices([aliceDesktop]); - }); - - it("no-ops if there are no state events", async () => { - await call.clean(); - expect(room.currentState.getStateEvents(JitsiCall.MEMBER_EVENT_TYPE, alice.userId)).toBe(null); - }); - }); }); describe("instance in a video room", () => { diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index e218629547..69b626dfd5 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -91,6 +91,7 @@ export function createTestClient(): MatrixClient { getDeviceId: jest.fn().mockReturnValue("ABCDEFGHI"), deviceId: "ABCDEFGHI", getDevices: jest.fn().mockResolvedValue({ devices: [{ device_id: "ABCDEFGHI" }] }), + getSessionId: jest.fn().mockReturnValue("iaszphgvfku"), credentials: { userId: "@userId:matrix.org" }, store: { From dae04a777e447972c7ef8d0a6a7dba752ce74663 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Fri, 2 Dec 2022 16:30:15 +0000 Subject: [PATCH 07/19] Upgrade matrix-js-sdk to 22.0.0-rc.2 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 55302f0183..d816663024 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "maplibre-gl": "^1.15.2", "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "0.0.1", - "matrix-js-sdk": "22.0.0-rc.1", + "matrix-js-sdk": "22.0.0-rc.2", "matrix-widget-api": "^1.1.1", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/yarn.lock b/yarn.lock index 13f0be9c65..e44f797940 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7167,10 +7167,10 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -matrix-js-sdk@22.0.0-rc.1: - version "22.0.0-rc.1" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-22.0.0-rc.1.tgz#e590b204b39179cd1c48ea6d577f3ac96989a5b7" - integrity sha512-6BLXHle0QIpgccpFE7EQq2IhTnsbhomCx0NZJ9URIY08M2aznvVxM2XfTi+LGjRKhv7yu8TueJaW7sGlsqZ79w== +matrix-js-sdk@22.0.0-rc.2: + version "22.0.0-rc.2" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-22.0.0-rc.2.tgz#101a3bf54b67d8c96b17dbc9bfdbed8457e3b673" + integrity sha512-yvfIfrlemxE+fhJHlmZqvwa/NVkV0zH0H+0ktxidd5WiXExMJL07uDZd9WyCOZb3vTkcxBiSDR9UWBfLJvQdjg== dependencies: "@babel/runtime" "^7.12.5" "@types/sdp-transform" "^2.4.5" From cc2e40e5cfe689359da865a1fd9ca539058a7857 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Fri, 2 Dec 2022 16:32:11 +0000 Subject: [PATCH 08/19] Prepare changelog for v3.62.0-rc.2 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2e2dce388..428d1d1024 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +Changes in [3.62.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.62.0-rc.2) (2022-12-02) +=============================================================================================================== + +## 🐛 Bug Fixes + * Fix call splitbrains when switching between rooms ([\#9692](https://github.com/matrix-org/matrix-react-sdk/pull/9692)). + Changes in [3.62.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.62.0-rc.1) (2022-11-29) =============================================================================================================== From cb2e1e92fd47afd3d2d20cdb39ca8aa574c167f6 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Fri, 2 Dec 2022 16:32:12 +0000 Subject: [PATCH 09/19] v3.62.0-rc.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d816663024..9c4972981b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.62.0-rc.1", + "version": "3.62.0-rc.2", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From f3432e99c8ee2ce2428444a09e04cbcf5b6d130b Mon Sep 17 00:00:00 2001 From: ElementRobot Date: Mon, 5 Dec 2022 13:14:58 +0000 Subject: [PATCH 10/19] [Backport staging] Fix replies to emotes not showing as inline (#9708) Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/views/rooms/_ReplyTile.pcss | 17 +++++++++++++++-- src/components/views/rooms/ReplyTile.tsx | 1 + 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/res/css/views/rooms/_ReplyTile.pcss b/res/css/views/rooms/_ReplyTile.pcss index fe6235eb1e..616f1b181f 100644 --- a/res/css/views/rooms/_ReplyTile.pcss +++ b/res/css/views/rooms/_ReplyTile.pcss @@ -28,8 +28,11 @@ limitations under the License. } > a { - display: flex; - flex-direction: column; + display: grid; + grid-template: + "sender" auto + "message" auto + / auto; text-decoration: none; color: $secondary-content; transition: color ease 0.15s; @@ -58,6 +61,7 @@ limitations under the License. /* We do reply size limiting with CSS to avoid duplicating the TextualBody component. */ .mx_EventTile_content { + grid-area: message; $reply-lines: 2; $line-height: $font-18px; @@ -102,7 +106,16 @@ limitations under the License. padding-top: 0; } + &.mx_ReplyTile_inline > a { + /* Render replies to emotes inline with the sender avatar */ + grid-template: + "sender message" auto + / max-content auto; + gap: 4px; // increase spacing + } + .mx_ReplyTile_sender { + grid-area: sender; display: flex; align-items: center; gap: 4px; diff --git a/src/components/views/rooms/ReplyTile.tsx b/src/components/views/rooms/ReplyTile.tsx index cdfbce1a88..515c8975e7 100644 --- a/src/components/views/rooms/ReplyTile.tsx +++ b/src/components/views/rooms/ReplyTile.tsx @@ -123,6 +123,7 @@ export default class ReplyTile extends React.PureComponent { } const classes = classNames("mx_ReplyTile", { + mx_ReplyTile_inline: msgType === MsgType.Emote, mx_ReplyTile_info: isInfoMessage && !mxEvent.isRedacted(), mx_ReplyTile_audio: msgType === MsgType.Audio, mx_ReplyTile_video: msgType === MsgType.Video, From 82ad8d5aa2faf03402c37db34594fdf14a4a7325 Mon Sep 17 00:00:00 2001 From: Kerry Date: Tue, 6 Dec 2022 19:18:03 +1300 Subject: [PATCH 11/19] Snooze the bulk unverified sessions reminder on dismiss (#9706) * test bulk unverified sessions toast behaviour * unverified sessions toast text tweak * only show bulk unverified sessions toast when current device is verified * add Setting for BulkUnverifiedSessionsReminder * add build config for BulkUnverifiedSessionsReminder * add more assertions for show/hide toast, fix strict errors * fix strict error * add util methods for snoozing in local storage * rename nag to reminder * set and read snooze for toast * test snooze * remove debug * strict fix * remove unused code --- src/DeviceListener.ts | 4 + src/toasts/BulkUnverifiedSessionsToast.ts | 2 + .../snoozeBulkUnverifiedDeviceReminder.ts | 40 ++++++++ test/DeviceListener-test.ts | 23 +++++ ...snoozeBulkUnverifiedDeviceReminder-test.ts | 98 +++++++++++++++++++ 5 files changed, 167 insertions(+) create mode 100644 src/utils/device/snoozeBulkUnverifiedDeviceReminder.ts create mode 100644 test/utils/device/snoozeBulkUnverifiedDeviceReminder-test.ts diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index ce1a0a26f0..f4d3d6ba7c 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -48,6 +48,7 @@ import { } from "./utils/device/clientInformation"; import SettingsStore, { CallbackFn } from "./settings/SettingsStore"; import { UIFeature } from "./settings/UIFeature"; +import { isBulkUnverifiedDeviceReminderSnoozed } from "./utils/device/snoozeBulkUnverifiedDeviceReminder"; const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000; @@ -335,12 +336,15 @@ export default class DeviceListener { logger.debug("New unverified sessions: " + Array.from(newUnverifiedDeviceIds).join(',')); logger.debug("Currently showing toasts for: " + Array.from(this.displayingToastsForDeviceIds).join(',')); + const isBulkUnverifiedSessionsReminderSnoozed = isBulkUnverifiedDeviceReminderSnoozed(); + // Display or hide the batch toast for old unverified sessions // don't show the toast if the current device is unverified if ( oldUnverifiedDeviceIds.size > 0 && isCurrentDeviceTrusted && this.enableBulkUnverifiedSessionsReminder + && !isBulkUnverifiedSessionsReminderSnoozed ) { showBulkUnverifiedSessionsToast(oldUnverifiedDeviceIds); } else { diff --git a/src/toasts/BulkUnverifiedSessionsToast.ts b/src/toasts/BulkUnverifiedSessionsToast.ts index ae512df7ed..439d781126 100644 --- a/src/toasts/BulkUnverifiedSessionsToast.ts +++ b/src/toasts/BulkUnverifiedSessionsToast.ts @@ -20,6 +20,7 @@ import DeviceListener from '../DeviceListener'; import GenericToast from "../components/views/toasts/GenericToast"; import ToastStore from "../stores/ToastStore"; import { Action } from "../dispatcher/actions"; +import { snoozeBulkUnverifiedDeviceReminder } from '../utils/device/snoozeBulkUnverifiedDeviceReminder'; const TOAST_KEY = "reviewsessions"; @@ -34,6 +35,7 @@ export const showToast = (deviceIds: Set) => { const onReject = () => { DeviceListener.sharedInstance().dismissUnverifiedSessions(deviceIds); + snoozeBulkUnverifiedDeviceReminder(); }; ToastStore.sharedInstance().addOrReplaceToast({ diff --git a/src/utils/device/snoozeBulkUnverifiedDeviceReminder.ts b/src/utils/device/snoozeBulkUnverifiedDeviceReminder.ts new file mode 100644 index 0000000000..80f107b18a --- /dev/null +++ b/src/utils/device/snoozeBulkUnverifiedDeviceReminder.ts @@ -0,0 +1,40 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { logger } from "matrix-js-sdk/src/logger"; + +const SNOOZE_KEY = 'mx_snooze_bulk_unverified_device_nag'; +// one week +const snoozePeriod = 1000 * 60 * 60 * 24 * 7; +export const snoozeBulkUnverifiedDeviceReminder = () => { + try { + localStorage.setItem(SNOOZE_KEY, String(Date.now())); + } catch (error) { + logger.error('Failed to persist bulk unverified device nag snooze', error); + } +}; + +export const isBulkUnverifiedDeviceReminderSnoozed = () => { + try { + const snoozedTimestamp = localStorage.getItem(SNOOZE_KEY); + + const parsedTimestamp = Number.parseInt(snoozedTimestamp || '', 10); + + return Number.isInteger(parsedTimestamp) && (parsedTimestamp + snoozePeriod) > Date.now(); + } catch (error) { + return false; + } +}; diff --git a/test/DeviceListener-test.ts b/test/DeviceListener-test.ts index 03ad29956e..20adbfd45d 100644 --- a/test/DeviceListener-test.ts +++ b/test/DeviceListener-test.ts @@ -35,6 +35,7 @@ import SettingsStore from "../src/settings/SettingsStore"; import { SettingLevel } from "../src/settings/SettingLevel"; import { getMockClientWithEventEmitter, mockPlatformPeg } from "./test-utils"; import { UIFeature } from "../src/settings/UIFeature"; +import { isBulkUnverifiedDeviceReminderSnoozed } from "../src/utils/device/snoozeBulkUnverifiedDeviceReminder"; // don't litter test console with logs jest.mock("matrix-js-sdk/src/logger"); @@ -48,6 +49,10 @@ jest.mock("../src/SecurityManager", () => ({ isSecretStorageBeingAccessed: jest.fn(), accessSecretStorage: jest.fn(), })); +jest.mock("../src/utils/device/snoozeBulkUnverifiedDeviceReminder", () => ({ + isBulkUnverifiedDeviceReminderSnoozed: jest.fn(), +})); + const userId = '@user:server'; const deviceId = 'my-device-id'; const mockDispatcher = mocked(dis); @@ -95,6 +100,7 @@ describe('DeviceListener', () => { }); jest.spyOn(MatrixClientPeg, 'get').mockReturnValue(mockClient); jest.spyOn(SettingsStore, 'getValue').mockReturnValue(false); + mocked(isBulkUnverifiedDeviceReminderSnoozed).mockClear().mockReturnValue(false); }); const createAndStart = async (): Promise => { @@ -451,6 +457,23 @@ describe('DeviceListener', () => { expect(BulkUnverifiedSessionsToast.showToast).not.toHaveBeenCalled(); }); + it('hides toast when reminder is snoozed', async () => { + mocked(isBulkUnverifiedDeviceReminderSnoozed).mockReturnValue(true); + // currentDevice, device2 are verified, device3 is unverified + mockClient!.checkDeviceTrust.mockImplementation((_userId, deviceId) => { + switch (deviceId) { + case currentDevice.deviceId: + case device2.deviceId: + return deviceTrustVerified; + default: + return deviceTrustUnverified; + } + }); + await createAndStart(); + expect(BulkUnverifiedSessionsToast.showToast).not.toHaveBeenCalled(); + expect(BulkUnverifiedSessionsToast.hideToast).toHaveBeenCalled(); + }); + it('shows toast with unverified devices at app start', async () => { // currentDevice, device2 are verified, device3 is unverified mockClient!.checkDeviceTrust.mockImplementation((_userId, deviceId) => { diff --git a/test/utils/device/snoozeBulkUnverifiedDeviceReminder-test.ts b/test/utils/device/snoozeBulkUnverifiedDeviceReminder-test.ts new file mode 100644 index 0000000000..e7abf4b56a --- /dev/null +++ b/test/utils/device/snoozeBulkUnverifiedDeviceReminder-test.ts @@ -0,0 +1,98 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { logger } from "matrix-js-sdk/src/logger"; + +import { + isBulkUnverifiedDeviceReminderSnoozed, + snoozeBulkUnverifiedDeviceReminder, +} from "../../../src/utils/device/snoozeBulkUnverifiedDeviceReminder"; + +const SNOOZE_KEY = 'mx_snooze_bulk_unverified_device_nag'; + +describe('snooze bulk unverified device nag', () => { + const localStorageSetSpy = jest.spyOn(localStorage.__proto__, 'setItem'); + const localStorageGetSpy = jest.spyOn(localStorage.__proto__, 'getItem'); + const localStorageRemoveSpy = jest.spyOn(localStorage.__proto__, 'removeItem'); + + // 14.03.2022 16:15 + const now = 1647270879403; + + beforeEach(() => { + localStorageSetSpy.mockClear().mockImplementation(() => {}); + localStorageGetSpy.mockClear().mockReturnValue(null); + localStorageRemoveSpy.mockClear().mockImplementation(() => {}); + + jest.spyOn(Date, 'now').mockReturnValue(now); + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + describe('snoozeBulkUnverifiedDeviceReminder()', () => { + it('sets the current time in local storage', () => { + snoozeBulkUnverifiedDeviceReminder(); + + expect(localStorageSetSpy).toHaveBeenCalledWith(SNOOZE_KEY, now.toString()); + }); + + it('catches an error from localstorage', () => { + const loggerErrorSpy = jest.spyOn(logger, 'error'); + localStorageSetSpy.mockImplementation(() => { throw new Error('oups'); }); + snoozeBulkUnverifiedDeviceReminder(); + expect(loggerErrorSpy).toHaveBeenCalled(); + }); + }); + + describe('isBulkUnverifiedDeviceReminderSnoozed()', () => { + it('returns false when there is no snooze in storage', () => { + const result = isBulkUnverifiedDeviceReminderSnoozed(); + expect(localStorageGetSpy).toHaveBeenCalledWith(SNOOZE_KEY); + expect(result).toBe(false); + }); + + it('catches an error from localstorage and returns false', () => { + const loggerErrorSpy = jest.spyOn(logger, 'error'); + localStorageGetSpy.mockImplementation(() => { throw new Error('oups'); }); + const result = isBulkUnverifiedDeviceReminderSnoozed(); + expect(result).toBe(false); + expect(loggerErrorSpy).toHaveBeenCalled(); + }); + + it('returns false when snooze timestamp in storage is not a number', () => { + localStorageGetSpy.mockReturnValue('test'); + const result = isBulkUnverifiedDeviceReminderSnoozed(); + expect(result).toBe(false); + }); + + it('returns false when snooze timestamp in storage is over a week ago', () => { + const msDay = 1000 * 60 * 60 * 24; + // snoozed 8 days ago + localStorageGetSpy.mockReturnValue(now - (msDay * 8)); + const result = isBulkUnverifiedDeviceReminderSnoozed(); + expect(result).toBe(false); + }); + + it('returns true when snooze timestamp in storage is less than a week ago', () => { + const msDay = 1000 * 60 * 60 * 24; + // snoozed 8 days ago + localStorageGetSpy.mockReturnValue(now - (msDay * 6)); + const result = isBulkUnverifiedDeviceReminderSnoozed(); + expect(result).toBe(true); + }); + }); +}); From 89439d4f1058b941bff8476526b58510363a13c3 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Tue, 6 Dec 2022 10:01:25 +0100 Subject: [PATCH 12/19] Further password reset flow enhancements (#9662) --- res/css/views/auth/_AuthBody.pcss | 55 +++++++++--- res/css/views/dialogs/_VerifyEMailDialog.pcss | 13 ++- src/Modal.tsx | 12 ++- src/PasswordReset.ts | 20 ----- .../structures/auth/ForgotPassword.tsx | 38 ++++++-- .../auth/forgot-password/CheckEmail.tsx | 42 ++++++--- .../auth/forgot-password/EnterEmail.tsx | 12 +++ .../auth/forgot-password/VerifyEmailModal.tsx | 24 ++++- src/i18n/strings/en_EN.json | 3 + .../structures/auth/ForgotPassword-test.tsx | 89 +++++++++++++++++-- 10 files changed, 241 insertions(+), 67 deletions(-) diff --git a/res/css/views/auth/_AuthBody.pcss b/res/css/views/auth/_AuthBody.pcss index 824f6411df..387c019928 100644 --- a/res/css/views/auth/_AuthBody.pcss +++ b/res/css/views/auth/_AuthBody.pcss @@ -137,15 +137,50 @@ limitations under the License. } /* specialisation for password reset views */ -.mx_AuthBody_forgot-password { +.mx_AuthBody.mx_AuthBody_forgot-password { font-size: $font-14px; color: $primary-content; padding: 50px 32px; min-height: 600px; h1 { - margin-bottom: $spacing-20; - margin-top: $spacing-24; + margin: $spacing-24 0; + } + + .mx_AuthBody_button-container { + display: flex; + justify-content: center; + } + + .mx_Login_submit { + font-weight: $font-semi-bold; + margin: 0 0 $spacing-16; + } + + .mx_AuthBody_text { + margin-bottom: $spacing-32; + + p { + margin: 0 0 $spacing-8; + } + } + + .mx_AuthBody_sign-in-instead-button { + font-weight: $font-semi-bold; + padding: $spacing-4; + } + + .mx_AuthBody_fieldRow { + margin-bottom: $spacing-24; + } + + .mx_AccessibleButton.mx_AccessibleButton_hasKind { + background: none; + + &:disabled { + cursor: default; + opacity: .4; + } } } @@ -154,12 +189,6 @@ limitations under the License. color: $secondary-content; display: flex; gap: $spacing-8; - margin-bottom: 10px; - margin-top: $spacing-24; -} - -.mx_AuthBody_did-not-receive--centered { - justify-content: center; } .mx_AuthBody_resend-button { @@ -168,7 +197,7 @@ limitations under the License. color: $accent; display: flex; gap: $spacing-4; - padding: 4px; + padding: $spacing-4; &:hover { background-color: $system; @@ -209,7 +238,7 @@ limitations under the License. text-align: center; .mx_AuthBody_paddedFooter_title { - margin-top: 16px; + margin-top: $spacing-16; font-size: $font-15px; line-height: $font-24px; @@ -220,7 +249,7 @@ limitations under the License. } .mx_AuthBody_paddedFooter_subtitle { - margin-top: 8px; + margin-top: $spacing-8; font-size: $font-10px; line-height: $font-14px; } @@ -236,7 +265,7 @@ limitations under the License. } .mx_SSOButtons + .mx_AuthBody_changeFlow { - margin-top: 24px; + margin-top: $spacing-24; } .mx_AuthBody_spinner { diff --git a/res/css/views/dialogs/_VerifyEMailDialog.pcss b/res/css/views/dialogs/_VerifyEMailDialog.pcss index fa36f0e114..47541dc452 100644 --- a/res/css/views/dialogs/_VerifyEMailDialog.pcss +++ b/res/css/views/dialogs/_VerifyEMailDialog.pcss @@ -20,8 +20,8 @@ limitations under the License. .mx_Dialog { color: $primary-content; - font-size: 14px; - padding: 16px; + font-size: $font-14px; + padding: $spacing-24 $spacing-24 $spacing-16; text-align: center; width: 485px; @@ -34,5 +34,14 @@ limitations under the License. color: $secondary-content; line-height: 20px; } + + .mx_AuthBody_did-not-receive { + justify-content: center; + margin-bottom: $spacing-8; + } + + .mx_Dialog_cancelButton { + right: 10px; + } } } diff --git a/src/Modal.tsx b/src/Modal.tsx index ee24b15d54..53e47cc01a 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -347,7 +347,11 @@ export class ModalManager extends TypedEventEmitter { this.staticModal.elem }
-
+
); @@ -368,7 +372,11 @@ export class ModalManager extends TypedEventEmitter { modal.elem }
-
+
); diff --git a/src/PasswordReset.ts b/src/PasswordReset.ts index 1f2c541270..7bcb6ac78e 100644 --- a/src/PasswordReset.ts +++ b/src/PasswordReset.ts @@ -19,8 +19,6 @@ import { createClient, IRequestTokenResponse, MatrixClient } from 'matrix-js-sdk import { _t } from './languageHandler'; -const CHECK_EMAIL_VERIFIED_POLL_INTERVAL = 2000; - /** * Allows a user to reset their password on a homeserver. * @@ -108,24 +106,6 @@ export default class PasswordReset { await this.checkEmailLinkClicked(); } - public async retrySetNewPassword(password: string): Promise { - this.password = password; - return new Promise((resolve) => { - this.tryCheckEmailLinkClicked(resolve); - }); - } - - private tryCheckEmailLinkClicked(resolve: Function): void { - this.checkEmailLinkClicked() - .then(() => resolve()) - .catch(() => { - window.setTimeout( - () => this.tryCheckEmailLinkClicked(resolve), - CHECK_EMAIL_VERIFIED_POLL_INTERVAL, - ); - }); - } - /** * Checks if the email link has been clicked by attempting to change the password * for the mxid linked to the email. diff --git a/src/components/structures/auth/ForgotPassword.tsx b/src/components/structures/auth/ForgotPassword.tsx index fe246aabf7..4698b99ae8 100644 --- a/src/components/structures/auth/ForgotPassword.tsx +++ b/src/components/structures/auth/ForgotPassword.tsx @@ -19,6 +19,7 @@ limitations under the License. import React, { ReactNode } from 'react'; import { logger } from 'matrix-js-sdk/src/logger'; import { createClient } from "matrix-js-sdk/src/matrix"; +import { sleep } from 'matrix-js-sdk/src/utils'; import { _t, _td } from '../../../languageHandler'; import Modal from "../../../Modal"; @@ -43,6 +44,8 @@ import Spinner from '../../views/elements/Spinner'; import { formatSeconds } from '../../../DateUtils'; import AutoDiscoveryUtils from '../../../utils/AutoDiscoveryUtils'; +const emailCheckInterval = 2000; + enum Phase { // Show email input EnterEmail = 1, @@ -60,7 +63,7 @@ enum Phase { interface Props { serverConfig: ValidatedServerConfig; - onLoginClick?: () => void; + onLoginClick: () => void; onComplete: () => void; } @@ -277,22 +280,43 @@ export default class ForgotPassword extends React.Component { { email: this.state.email, errorText: this.state.errorText, + onCloseClick: () => { + modal.close(); + this.setState({ phase: Phase.PasswordInput }); + }, + onReEnterEmailClick: () => { + modal.close(); + this.setState({ phase: Phase.EnterEmail }); + }, onResendClick: this.sendVerificationMail, }, "mx_VerifyEMailDialog", false, false, { - // this modal cannot be dismissed except reset is done or forced onBeforeClose: async (reason?: string) => { - return this.state.phase === Phase.Done || reason === "force"; + if (reason === "backgroundClick") { + // Modal dismissed by clicking the background. + // Go one phase back. + this.setState({ phase: Phase.PasswordInput }); + } + + return true; }, }, ); - await this.reset.retrySetNewPassword(this.state.password); - this.phase = Phase.Done; - modal.close(); + // Don't retry if the phase changed. For example when going back to email input. + while (this.state.phase === Phase.ResettingPassword) { + try { + await this.reset.setNewPassword(this.state.password); + this.setState({ phase: Phase.Done }); + modal.close(); + } catch (e) { + // Email not confirmed, yet. Retry after a while. + await sleep(emailCheckInterval); + } + } } private onSubmitForm = async (ev: React.FormEvent): Promise => { @@ -339,6 +363,7 @@ export default class ForgotPassword extends React.Component { homeserver={this.props.serverConfig.hsName} loading={this.state.phase === Phase.SendingEmail} onInputChanged={this.onInputChanged} + onLoginClick={this.props.onLoginClick!} // set by default props onSubmitForm={this.onSubmitForm} />; } @@ -374,6 +399,7 @@ export default class ForgotPassword extends React.Component { return this.setState({ phase: Phase.EnterEmail })} onResendClick={this.sendVerificationMail} onSubmitForm={this.onSubmitForm} />; diff --git a/src/components/structures/auth/forgot-password/CheckEmail.tsx b/src/components/structures/auth/forgot-password/CheckEmail.tsx index 27fa82f25e..b1faba936e 100644 --- a/src/components/structures/auth/forgot-password/CheckEmail.tsx +++ b/src/components/structures/auth/forgot-password/CheckEmail.tsx @@ -27,6 +27,7 @@ import { ErrorMessage } from "../../ErrorMessage"; interface CheckEmailProps { email: string; errorText: string | ReactNode | null; + onReEnterEmailClick: () => void; onResendClick: () => Promise; onSubmitForm: (ev: React.FormEvent) => void; } @@ -37,6 +38,7 @@ interface CheckEmailProps { export const CheckEmail: React.FC = ({ email, errorText, + onReEnterEmailClick, onSubmitForm, onResendClick, }) => { @@ -50,13 +52,32 @@ export const CheckEmail: React.FC = ({ return <>

{ _t("Check your email to continue") }

-

- { _t( - "Follow the instructions sent to %(email)s", - { email: email }, - { b: t => { t } }, - ) } -

+
+

+ { _t( + "Follow the instructions sent to %(email)s", + { email: email }, + { b: t => { t } }, + ) } +

+
+ { _t("Wrong email address?") } + + { _t("Re-enter email address") } + +
+
+ { errorText && } +
{ _t("Did not receive it?") } = ({ />
- { errorText && } - ; }; diff --git a/src/components/structures/auth/forgot-password/EnterEmail.tsx b/src/components/structures/auth/forgot-password/EnterEmail.tsx index a630291ae2..3201349b3d 100644 --- a/src/components/structures/auth/forgot-password/EnterEmail.tsx +++ b/src/components/structures/auth/forgot-password/EnterEmail.tsx @@ -22,6 +22,7 @@ import EmailField from "../../../views/auth/EmailField"; import { ErrorMessage } from "../../ErrorMessage"; import Spinner from "../../../views/elements/Spinner"; import Field from "../../../views/elements/Field"; +import AccessibleButton from "../../../views/elements/AccessibleButton"; interface EnterEmailProps { email: string; @@ -29,6 +30,7 @@ interface EnterEmailProps { homeserver: string; loading: boolean; onInputChanged: (stateKey: string, ev: React.FormEvent) => void; + onLoginClick: () => void; onSubmitForm: (ev: React.FormEvent) => void; } @@ -41,6 +43,7 @@ export const EnterEmail: React.FC = ({ homeserver, loading, onInputChanged, + onLoginClick, onSubmitForm, }) => { const submitButtonChild = loading @@ -92,6 +95,15 @@ export const EnterEmail: React.FC = ({ > { submitButtonChild } +
+ + { _t("Sign in instead") } + +
; diff --git a/src/components/structures/auth/forgot-password/VerifyEmailModal.tsx b/src/components/structures/auth/forgot-password/VerifyEmailModal.tsx index d63e4c97d7..41bdb7a051 100644 --- a/src/components/structures/auth/forgot-password/VerifyEmailModal.tsx +++ b/src/components/structures/auth/forgot-password/VerifyEmailModal.tsx @@ -27,12 +27,16 @@ import { ErrorMessage } from "../../ErrorMessage"; interface Props { email: string; errorText: string | null; + onCloseClick: () => void; + onReEnterEmailClick: () => void; onResendClick: () => Promise; } export const VerifyEmailModal: React.FC = ({ email, errorText, + onCloseClick, + onReEnterEmailClick, onResendClick, }) => { const { toggle: toggleTooltipVisible, value: tooltipVisible } = useTimeoutToggle(false, 2500); @@ -57,7 +61,8 @@ export const VerifyEmailModal: React.FC = ({ }, ) }

-
+ +
{ _t("Did not receive it?") } = ({ { errorText && }
+ +
+ { _t("Wrong email address?") } + + { _t("Re-enter email address") } + +
+ + ; }; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 4c8e4d33c3..e2daf50263 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -3496,6 +3496,8 @@ "Clear personal data": "Clear personal data", "Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.": "Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.", "Follow the instructions sent to %(email)s": "Follow the instructions sent to %(email)s", + "Wrong email address?": "Wrong email address?", + "Re-enter email address": "Re-enter email address", "Did not receive it?": "Did not receive it?", "Verification link email resent!": "Verification link email resent!", "Send email": "Send email", @@ -3503,6 +3505,7 @@ "%(homeserver)s will send you a verification link to let you reset your password.": "%(homeserver)s will send you a verification link to let you reset your password.", "The email address linked to your account must be entered.": "The email address linked to your account must be entered.", "The email address doesn't appear to be valid.": "The email address doesn't appear to be valid.", + "Sign in instead": "Sign in instead", "Verify your email to continue": "Verify your email to continue", "We need to know it’s you before resetting your password.\n Click the link in the email we just sent to %(email)s": "We need to know it’s you before resetting your password.\n Click the link in the email we just sent to %(email)s", "Commands": "Commands", diff --git a/test/components/structures/auth/ForgotPassword-test.tsx b/test/components/structures/auth/ForgotPassword-test.tsx index 9f4b192aa9..97e26a1150 100644 --- a/test/components/structures/auth/ForgotPassword-test.tsx +++ b/test/components/structures/auth/ForgotPassword-test.tsx @@ -38,6 +38,7 @@ describe("", () => { let client: MatrixClient; let serverConfig: ValidatedServerConfig; let onComplete: () => void; + let onLoginClick: () => void; let renderResult: RenderResult; let restoreConsole: () => void; @@ -49,9 +50,16 @@ describe("", () => { }); }; - const submitForm = async (submitLabel: string): Promise => { + const clickButton = async (label: string): Promise => { await act(async () => { - await userEvent.click(screen.getByText(submitLabel), { delay: null }); + await userEvent.click(screen.getByText(label), { delay: null }); + }); + }; + + const itShouldCloseTheDialogAndShowThePasswordInput = (): void => { + it("should close the dialog and show the password input", () => { + expect(screen.queryByText("Verify your email to continue")).not.toBeInTheDocument(); + expect(screen.getByText("Reset your password")).toBeInTheDocument(); }); }; @@ -70,6 +78,7 @@ describe("", () => { serverConfig.hsName = "example.com"; onComplete = jest.fn(); + onLoginClick = jest.fn(); jest.spyOn(AutoDiscoveryUtils, "validateServerConfigWithStaticUrls").mockResolvedValue(serverConfig); jest.spyOn(AutoDiscoveryUtils, "authComponentStateForError"); @@ -94,6 +103,7 @@ describe("", () => { renderResult = render(); }); @@ -108,6 +118,7 @@ describe("", () => { renderResult.rerender(); }); @@ -116,6 +127,16 @@ describe("", () => { }); }); + describe("when clicking »Sign in instead«", () => { + beforeEach(async () => { + await clickButton("Sign in instead"); + }); + + it("should call onLoginClick()", () => { + expect(onLoginClick).toHaveBeenCalled(); + }); + }); + describe("when entering a non-email value", () => { beforeEach(async () => { await typeIntoField("Email address", "not en email"); @@ -132,7 +153,7 @@ describe("", () => { mocked(client).requestPasswordEmailToken.mockRejectedValue({ errcode: "M_THREEPID_NOT_FOUND", }); - await submitForm("Send email"); + await clickButton("Send email"); }); it("should show an email not found message", () => { @@ -146,7 +167,7 @@ describe("", () => { mocked(client).requestPasswordEmailToken.mockRejectedValue({ name: "ConnectionError", }); - await submitForm("Send email"); + await clickButton("Send email"); }); it("should show an info about that", () => { @@ -166,7 +187,7 @@ describe("", () => { serverIsAlive: false, serverDeadError: "server down", }); - await submitForm("Send email"); + await clickButton("Send email"); }); it("should show the server error", () => { @@ -180,7 +201,7 @@ describe("", () => { mocked(client).requestPasswordEmailToken.mockResolvedValue({ sid: testSid, }); - await submitForm("Send email"); + await clickButton("Send email"); }); it("should send the mail and show the check email view", () => { @@ -193,6 +214,16 @@ describe("", () => { expect(screen.getByText(testEmail)).toBeInTheDocument(); }); + describe("when clicking re-enter email", () => { + beforeEach(async () => { + await clickButton("Re-enter email address"); + }); + + it("go back to the email input", () => { + expect(screen.queryByText("Enter your email to reset password")).toBeInTheDocument(); + }); + }); + describe("when clicking resend email", () => { beforeEach(async () => { await userEvent.click(screen.getByText("Resend"), { delay: null }); @@ -212,7 +243,7 @@ describe("", () => { describe("when clicking next", () => { beforeEach(async () => { - await submitForm("Next"); + await clickButton("Next"); }); it("should show the password input view", () => { @@ -246,7 +277,7 @@ describe("", () => { retry_after_ms: (13 * 60 + 37) * 1000, }, }); - await submitForm("Reset password"); + await clickButton("Reset password"); }); it("should show the rate limit error message", () => { @@ -258,7 +289,7 @@ describe("", () => { describe("and submitting it", () => { beforeEach(async () => { - await submitForm("Reset password"); + await clickButton("Reset password"); // double flush promises for the modal to appear await flushPromisesWithFakeTimers(); await flushPromisesWithFakeTimers(); @@ -284,6 +315,46 @@ describe("", () => { expect(screen.getByText(testEmail)).toBeInTheDocument(); }); + describe("and dismissing the dialog by clicking the background", () => { + beforeEach(async () => { + await act(async () => { + await userEvent.click(screen.getByTestId("dialog-background"), { delay: null }); + }); + // double flush promises for the modal to disappear + await flushPromisesWithFakeTimers(); + await flushPromisesWithFakeTimers(); + }); + + itShouldCloseTheDialogAndShowThePasswordInput(); + }); + + describe("and dismissing the dialog", () => { + beforeEach(async () => { + await act(async () => { + await userEvent.click(screen.getByLabelText("Close dialog"), { delay: null }); + }); + // double flush promises for the modal to disappear + await flushPromisesWithFakeTimers(); + await flushPromisesWithFakeTimers(); + }); + + itShouldCloseTheDialogAndShowThePasswordInput(); + }); + + describe("when clicking re-enter email", () => { + beforeEach(async () => { + await clickButton("Re-enter email address"); + // double flush promises for the modal to disappear + await flushPromisesWithFakeTimers(); + await flushPromisesWithFakeTimers(); + }); + + it("should close the dialog and go back to the email input", () => { + expect(screen.queryByText("Verify your email to continue")).not.toBeInTheDocument(); + expect(screen.queryByText("Enter your email to reset password")).toBeInTheDocument(); + }); + }); + describe("when validating the link from the mail", () => { beforeEach(async () => { mocked(client.setPassword).mockResolvedValue({}); From 474f464e48e93ad55d35017713424729f9a1b12f Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Tue, 6 Dec 2022 10:56:29 +0100 Subject: [PATCH 13/19] Voice broadcast indicator in room list (#9709) --- res/css/_components.pcss | 1 + .../atoms/_VoiceBroadcastRoomSubtitle.pcss | 22 ++++ src/components/views/rooms/RoomTile.tsx | 44 ++++--- .../atoms/VoiceBroadcastRoomSubtitle.tsx | 27 ++++ .../hooks/useHasRoomLiveVoiceBroadcast.ts | 35 ++++++ src/voice-broadcast/index.ts | 2 + test/components/views/rooms/RoomList-test.tsx | 2 +- test/components/views/rooms/RoomTile-test.tsx | 115 +++++++++++++++--- .../__snapshots__/RoomTile-test.tsx.snap | 81 ++++++++++++ test/test-utils/console.ts | 2 +- 10 files changed, 295 insertions(+), 36 deletions(-) create mode 100644 res/css/voice-broadcast/atoms/_VoiceBroadcastRoomSubtitle.pcss create mode 100644 src/voice-broadcast/components/atoms/VoiceBroadcastRoomSubtitle.tsx create mode 100644 src/voice-broadcast/hooks/useHasRoomLiveVoiceBroadcast.ts create mode 100644 test/components/views/rooms/__snapshots__/RoomTile-test.tsx.snap diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 7f21752d4a..2630ad1bc7 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -375,4 +375,5 @@ @import "./voice-broadcast/atoms/_PlaybackControlButton.pcss"; @import "./voice-broadcast/atoms/_VoiceBroadcastControl.pcss"; @import "./voice-broadcast/atoms/_VoiceBroadcastHeader.pcss"; +@import "./voice-broadcast/atoms/_VoiceBroadcastRoomSubtitle.pcss"; @import "./voice-broadcast/molecules/_VoiceBroadcastBody.pcss"; diff --git a/res/css/voice-broadcast/atoms/_VoiceBroadcastRoomSubtitle.pcss b/res/css/voice-broadcast/atoms/_VoiceBroadcastRoomSubtitle.pcss new file mode 100644 index 0000000000..570a30e6f6 --- /dev/null +++ b/res/css/voice-broadcast/atoms/_VoiceBroadcastRoomSubtitle.pcss @@ -0,0 +1,22 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.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/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index 68f4dfe4de..d19efb7d1f 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -48,20 +48,25 @@ import { RoomTileCallSummary } from "./RoomTileCallSummary"; import { RoomGeneralContextMenu } from "../context_menus/RoomGeneralContextMenu"; import { CallStore, CallStoreEvent } from "../../../stores/CallStore"; import { SdkContextClass } from "../../../contexts/SDKContext"; +import { useHasRoomLiveVoiceBroadcast, VoiceBroadcastRoomSubtitle } from "../../../voice-broadcast"; -interface IProps { +interface Props { room: Room; showMessagePreview: boolean; isMinimized: boolean; tag: TagID; } +interface ClassProps extends Props { + hasLiveVoiceBroadcast: boolean; +} + type PartialDOMRect = Pick; -interface IState { +interface State { selected: boolean; - notificationsMenuPosition: PartialDOMRect; - generalMenuPosition: PartialDOMRect; + notificationsMenuPosition: PartialDOMRect | null; + generalMenuPosition: PartialDOMRect | null; call: Call | null; messagePreview?: string; } @@ -76,13 +81,13 @@ export const contextMenuBelow = (elementRect: PartialDOMRect) => { return { left, top, chevronFace }; }; -export default class RoomTile extends React.PureComponent { - private dispatcherRef: string; +export class RoomTile extends React.PureComponent { + private dispatcherRef?: string; private roomTileRef = createRef(); private notificationState: NotificationState; private roomProps: RoomEchoChamber; - constructor(props: IProps) { + constructor(props: ClassProps) { super(props); this.state = { @@ -120,7 +125,7 @@ export default class RoomTile extends React.PureComponent { return !this.props.isMinimized && this.props.showMessagePreview; } - public componentDidUpdate(prevProps: Readonly, prevState: Readonly) { + public componentDidUpdate(prevProps: Readonly, prevState: Readonly) { const showMessageChanged = prevProps.showMessagePreview !== this.props.showMessagePreview; const minimizedChanged = prevProps.isMinimized !== this.props.isMinimized; if (showMessageChanged || minimizedChanged) { @@ -169,7 +174,7 @@ export default class RoomTile extends React.PureComponent { this.onRoomPreviewChanged, ); this.props.room.off(RoomEvent.Name, this.onRoomNameUpdate); - defaultDispatcher.unregister(this.dispatcherRef); + if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef); this.notificationState.off(NotificationStateEvents.Update, this.onNotificationUpdate); this.roomProps.off(PROPERTY_UPDATED, this.onRoomPropertyUpdate); CallStore.instance.off(CallStoreEvent.Call, this.onCallChanged); @@ -218,12 +223,14 @@ export default class RoomTile extends React.PureComponent { ev.stopPropagation(); const action = getKeyBindingsManager().getAccessibilityAction(ev); + const clearSearch = ([KeyBindingAction.Enter, KeyBindingAction.Space] as Array) + .includes(action); defaultDispatcher.dispatch({ action: Action.ViewRoom, show_room_tile: true, // make sure the room is visible in the list room_id: this.props.room.roomId, - clear_search: [KeyBindingAction.Enter, KeyBindingAction.Space].includes(action), + clear_search: clearSearch, metricsTrigger: "RoomList", metricsViaKeyboard: ev.type !== "click", }); @@ -233,7 +240,7 @@ export default class RoomTile extends React.PureComponent { this.setState({ selected: isActive }); }; - private onNotificationsMenuOpenClick = (ev: React.MouseEvent) => { + private onNotificationsMenuOpenClick = (ev: ButtonEvent) => { ev.preventDefault(); ev.stopPropagation(); const target = ev.target as HTMLButtonElement; @@ -246,7 +253,7 @@ export default class RoomTile extends React.PureComponent { this.setState({ notificationsMenuPosition: null }); }; - private onGeneralMenuOpenClick = (ev: React.MouseEvent) => { + private onGeneralMenuOpenClick = (ev: ButtonEvent) => { ev.preventDefault(); ev.stopPropagation(); const target = ev.target as HTMLButtonElement; @@ -271,7 +278,7 @@ export default class RoomTile extends React.PureComponent { this.setState({ generalMenuPosition: null }); }; - private renderNotificationsMenu(isActive: boolean): React.ReactElement { + private renderNotificationsMenu(isActive: boolean): React.ReactElement | null { if (MatrixClientPeg.get().isGuest() || this.props.tag === DefaultTagID.Archived || !this.showContextMenu || this.props.isMinimized ) { @@ -313,7 +320,7 @@ export default class RoomTile extends React.PureComponent { ); } - private renderGeneralMenu(): React.ReactElement { + private renderGeneralMenu(): React.ReactElement | null { if (!this.showContextMenu) return null; // no menu to show return ( @@ -379,6 +386,8 @@ export default class RoomTile extends React.PureComponent {
); + } else if (this.props.hasLiveVoiceBroadcast) { + subtitle = ; } else if (this.showMessagePreview && this.state.messagePreview) { subtitle = (
{ ); } } + +const RoomTileHOC: React.FC = (props: Props) => { + const hasLiveVoiceBroadcast = useHasRoomLiveVoiceBroadcast(props.room); + return ; +}; + +export default RoomTileHOC; diff --git a/src/voice-broadcast/components/atoms/VoiceBroadcastRoomSubtitle.tsx b/src/voice-broadcast/components/atoms/VoiceBroadcastRoomSubtitle.tsx new file mode 100644 index 0000000000..4c6356ba2b --- /dev/null +++ b/src/voice-broadcast/components/atoms/VoiceBroadcastRoomSubtitle.tsx @@ -0,0 +1,27 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; + +import { Icon as LiveIcon } from "../../../../res/img/element-icons/live.svg"; +import { _t } from "../../../languageHandler"; + +export const VoiceBroadcastRoomSubtitle = () => { + return
+ + { _t("Live") } +
; +}; diff --git a/src/voice-broadcast/hooks/useHasRoomLiveVoiceBroadcast.ts b/src/voice-broadcast/hooks/useHasRoomLiveVoiceBroadcast.ts new file mode 100644 index 0000000000..6db5ed789e --- /dev/null +++ b/src/voice-broadcast/hooks/useHasRoomLiveVoiceBroadcast.ts @@ -0,0 +1,35 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { useState } from "react"; +import { Room, RoomStateEvent } from "matrix-js-sdk/src/matrix"; + +import { hasRoomLiveVoiceBroadcast } from "../utils/hasRoomLiveVoiceBroadcast"; +import { useTypedEventEmitter } from "../../hooks/useEventEmitter"; + +export const useHasRoomLiveVoiceBroadcast = (room: Room) => { + const [hasLiveVoiceBroadcast, setHasLiveVoiceBroadcast] = useState(hasRoomLiveVoiceBroadcast(room).hasBroadcast); + + useTypedEventEmitter( + room.currentState, + RoomStateEvent.Update, + () => { + setHasLiveVoiceBroadcast(hasRoomLiveVoiceBroadcast(room).hasBroadcast); + }, + ); + + return hasLiveVoiceBroadcast; +}; diff --git a/src/voice-broadcast/index.ts b/src/voice-broadcast/index.ts index 21e1bdd4af..9bb2dfd4c0 100644 --- a/src/voice-broadcast/index.ts +++ b/src/voice-broadcast/index.ts @@ -29,12 +29,14 @@ export * from "./components/VoiceBroadcastBody"; export * from "./components/atoms/LiveBadge"; export * from "./components/atoms/VoiceBroadcastControl"; export * from "./components/atoms/VoiceBroadcastHeader"; +export * from "./components/atoms/VoiceBroadcastRoomSubtitle"; export * from "./components/molecules/VoiceBroadcastPlaybackBody"; 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"; diff --git a/test/components/views/rooms/RoomList-test.tsx b/test/components/views/rooms/RoomList-test.tsx index 6fa3fe22cf..cb5ddb1ffa 100644 --- a/test/components/views/rooms/RoomList-test.tsx +++ b/test/components/views/rooms/RoomList-test.tsx @@ -32,7 +32,7 @@ import RoomListStore, { RoomListStoreClass } from "../../../../src/stores/room-l import RoomListLayoutStore from "../../../../src/stores/room-list/RoomListLayoutStore"; import RoomList from "../../../../src/components/views/rooms/RoomList"; import RoomSublist from "../../../../src/components/views/rooms/RoomSublist"; -import RoomTile from "../../../../src/components/views/rooms/RoomTile"; +import { RoomTile } from "../../../../src/components/views/rooms/RoomTile"; import { getMockClientWithEventEmitter, mockClientMethodsUser } from '../../../test-utils'; import ResizeNotifier from '../../../../src/utils/ResizeNotifier'; diff --git a/test/components/views/rooms/RoomTile-test.tsx b/test/components/views/rooms/RoomTile-test.tsx index cf1ae59d09..4a3aa95937 100644 --- a/test/components/views/rooms/RoomTile-test.tsx +++ b/test/components/views/rooms/RoomTile-test.tsx @@ -15,12 +15,13 @@ limitations under the License. */ import React from "react"; -import { render, screen, act } from "@testing-library/react"; +import { render, screen, act, RenderResult } from "@testing-library/react"; import { mocked, Mocked } from "jest-mock"; import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client"; import { Room } from "matrix-js-sdk/src/models/room"; import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; import { Widget } from "matrix-widget-api"; +import { MatrixEvent } from "matrix-js-sdk/src/matrix"; import type { RoomMember } from "matrix-js-sdk/src/models/room-member"; import type { ClientWidgetApi } from "matrix-widget-api"; @@ -30,6 +31,7 @@ import { MockedCall, useMockedCalls, setupAsyncStoreWithClient, + filterConsole, } from "../../../test-utils"; import { CallStore } from "../../../../src/stores/CallStore"; import RoomTile from "../../../../src/components/views/rooms/RoomTile"; @@ -39,38 +41,79 @@ import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; 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"; describe("RoomTile", () => { jest.spyOn(PlatformPeg, "get") .mockReturnValue({ overrideBrowserShortcuts: () => false } as unknown as BasePlatform); useMockedCalls(); + const setUpVoiceBroadcast = (state: VoiceBroadcastInfoState): void => { + voiceBroadcastInfoEvent = mkVoiceBroadcastInfoStateEvent( + room.roomId, + state, + client.getUserId(), + client.getDeviceId(), + ); + + act(() => { + room.currentState.setStateEvents([voiceBroadcastInfoEvent]); + }); + }; + + const renderRoomTile = (): void => { + renderResult = render( + , + ); + }; + let client: Mocked; + let restoreConsole: () => void; + let voiceBroadcastInfoEvent: MatrixEvent; + let room: Room; + let renderResult: RenderResult; beforeEach(() => { + restoreConsole = filterConsole( + // irrelevant for this test + "Room !1:example.org does not have an m.room.create event", + ); + stubClient(); client = mocked(MatrixClientPeg.get()); DMRoomMap.makeShared(); + + room = new Room("!1:example.org", client, "@alice:example.org", { + pendingEventOrdering: PendingEventOrdering.Detached, + }); + + client.getRoom.mockImplementation(roomId => roomId === room.roomId ? room : null); + client.getRooms.mockReturnValue([room]); + client.reEmitter.reEmit(room, [RoomStateEvent.Events]); + + renderRoomTile(); }); afterEach(() => { + restoreConsole(); jest.clearAllMocks(); }); - describe("call subtitle", () => { - let room: Room; + it("should render the room", () => { + expect(renderResult.container).toMatchSnapshot(); + }); + + describe("when a call starts", () => { let call: MockedCall; let widget: Widget; beforeEach(() => { - room = new Room("!1:example.org", client, "@alice:example.org", { - pendingEventOrdering: PendingEventOrdering.Detached, - }); - - client.getRoom.mockImplementation(roomId => roomId === room.roomId ? room : null); - client.getRooms.mockReturnValue([room]); - client.reEmitter.reEmit(room, [RoomStateEvent.Events]); - setupAsyncStoreWithClient(CallStore.instance, client); setupAsyncStoreWithClient(WidgetMessagingStore.instance, client); @@ -83,18 +126,10 @@ describe("RoomTile", () => { WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, { stop: () => {}, } as unknown as ClientWidgetApi); - - render( - , - ); }); afterEach(() => { + renderResult.unmount(); call.destroy(); client.reEmitter.stopReEmitting(room, [RoomStateEvent.Events]); WidgetMessagingStore.instance.stopMessaging(widget, room.roomId); @@ -147,5 +182,45 @@ describe("RoomTile", () => { act(() => { call.participants = new Map(); }); expect(screen.queryByLabelText(/participant/)).toBe(null); }); + + describe("and a live broadcast starts", () => { + beforeEach(() => { + 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(() => { + setUpVoiceBroadcast(VoiceBroadcastInfoState.Started); + }); + + it("should render the »Live« subtitle", () => { + expect(screen.queryByText("Live")).toBeInTheDocument(); + }); + + describe("and the broadcast stops", () => { + beforeEach(() => { + const stopEvent = mkVoiceBroadcastInfoStateEvent( + room.roomId, + VoiceBroadcastInfoState.Stopped, + client.getUserId(), + client.getDeviceId(), + voiceBroadcastInfoEvent, + ); + act(() => { + room.currentState.setStateEvents([stopEvent]); + }); + }); + + it("should not render the »Live« subtitle", () => { + expect(screen.queryByText("Live")).not.toBeInTheDocument(); + }); + }); }); }); diff --git a/test/components/views/rooms/__snapshots__/RoomTile-test.tsx.snap b/test/components/views/rooms/__snapshots__/RoomTile-test.tsx.snap new file mode 100644 index 0000000000..b4114bcb53 --- /dev/null +++ b/test/components/views/rooms/__snapshots__/RoomTile-test.tsx.snap @@ -0,0 +1,81 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RoomTile should render the room 1`] = ` +
+
+
+ + + + +
+
+
+ + !1:​example.org + +
+
+ + +`; diff --git a/test/test-utils/console.ts b/test/test-utils/console.ts index ff1ea0be09..f73c42568a 100644 --- a/test/test-utils/console.ts +++ b/test/test-utils/console.ts @@ -39,7 +39,7 @@ export const filterConsole = (...ignoreList: string[]): () => void => { return; } - originalFunction(data); + originalFunction(...data); }; } From 3a501003e246dbf01f1f220ab6aa9f4df2885374 Mon Sep 17 00:00:00 2001 From: Germain Date: Tue, 6 Dec 2022 09:59:17 +0000 Subject: [PATCH 14/19] Add setting to hide bold notifications (#9705) --- .../StatelessNotificationBadge.tsx | 7 +++- src/i18n/strings/en_EN.json | 1 + src/settings/Settings.tsx | 9 ++++- .../notifications/ListNotificationState.ts | 2 +- src/stores/notifications/NotificationState.ts | 33 +++++++++++++++---- .../notifications/RoomNotificationState.ts | 4 +-- .../notifications/SpaceNotificationState.ts | 2 +- .../notifications/StaticNotificationState.ts | 2 +- .../NotificationBadge-test.tsx | 15 +++++++++ .../views/spaces/QuickThemeSwitcher-test.tsx | 1 + test/stores/TypingStore-test.ts | 1 + test/utils/MultiInviter-test.ts | 1 + 12 files changed, 65 insertions(+), 13 deletions(-) diff --git a/src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx b/src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx index e9e97475f7..ebefca56d5 100644 --- a/src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx @@ -20,6 +20,7 @@ import classNames from "classnames"; import { formatCount } from "../../../../utils/FormattingUtils"; import AccessibleButton from "../../elements/AccessibleButton"; import { NotificationColor } from "../../../../stores/notifications/NotificationColor"; +import { useSettingValue } from "../../../../hooks/useSettings"; interface Props { symbol: string | null; @@ -37,8 +38,12 @@ export function StatelessNotificationBadge({ count, color, ...props }: Props) { + const hideBold = useSettingValue("feature_hidebold"); + // Don't show a badge if we don't need to - if (color === NotificationColor.None) return null; + if (color === NotificationColor.None || (hideBold && color == NotificationColor.Bold)) { + return null; + } const hasUnreadCount = color >= NotificationColor.Grey && (!!count || !!symbol); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e2daf50263..b76586eabb 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -960,6 +960,7 @@ "Show stickers button": "Show stickers button", "Show polls button": "Show polls button", "Insert a trailing colon after user mentions at the start of a message": "Insert a trailing colon after user mentions at the start of a message", + "Hide notification dot (only display counters badges)": "Hide notification dot (only display counters badges)", "Use a more compact 'Modern' layout": "Use a more compact 'Modern' layout", "Show a placeholder for removed messages": "Show a placeholder for removed messages", "Show join/leave messages (invites/removes/bans unaffected)": "Show join/leave messages (invites/removes/bans unaffected)", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index c6472868b7..110a520f84 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -556,11 +556,18 @@ export const SETTINGS: {[setting: string]: ISetting} = { supportedLevels: LEVELS_ROOM_OR_ACCOUNT, default: false, }, + "feature_hidebold": { + isFeature: true, + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, + displayName: _td("Hide notification dot (only display counters badges)"), + labsGroup: LabGroup.Rooms, + default: false, + }, "useCompactLayout": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, displayName: _td("Use a more compact 'Modern' layout"), default: false, - controller: new IncompatibleController("layout", false, v => v !== Layout.Group), + controller: new IncompatibleController("layout", false, (v: Layout) => v !== Layout.Group), }, "showRedactions": { supportedLevels: LEVELS_ROOM_SETTINGS_WITH_ROOM, diff --git a/src/stores/notifications/ListNotificationState.ts b/src/stores/notifications/ListNotificationState.ts index 8ff1824bd6..37235b0dd6 100644 --- a/src/stores/notifications/ListNotificationState.ts +++ b/src/stores/notifications/ListNotificationState.ts @@ -31,7 +31,7 @@ export class ListNotificationState extends NotificationState { super(); } - public get symbol(): string { + public get symbol(): string | null { return this._color === NotificationColor.Unsent ? "!" : null; } diff --git a/src/stores/notifications/NotificationState.ts b/src/stores/notifications/NotificationState.ts index 60f50fad8c..c963d9c1a0 100644 --- a/src/stores/notifications/NotificationState.ts +++ b/src/stores/notifications/NotificationState.ts @@ -18,6 +18,7 @@ import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter" import { NotificationColor } from "./NotificationColor"; import { IDestroyable } from "../../utils/IDestroyable"; +import SettingsStore from "../../settings/SettingsStore"; export interface INotificationStateSnapshotParams { symbol: string | null; @@ -37,11 +38,22 @@ export abstract class NotificationState extends TypedEventEmitter implements INotificationStateSnapshotParams, IDestroyable { // - protected _symbol: string | null; - protected _count: number; - protected _color: NotificationColor; + protected _symbol: string | null = null; + protected _count = 0; + protected _color: NotificationColor = NotificationColor.None; - public get symbol(): string { + private watcherReferences: string[] = []; + + constructor() { + super(); + this.watcherReferences.push( + SettingsStore.watchSetting("feature_hidebold", null, () => { + this.emit(NotificationStateEvents.Update); + }), + ); + } + + public get symbol(): string | null { return this._symbol; } @@ -58,7 +70,12 @@ export abstract class NotificationState } public get isUnread(): boolean { - return this.color >= NotificationColor.Bold; + if (this.color > NotificationColor.Bold) { + return true; + } else { + const hideBold = SettingsStore.getValue("feature_hidebold"); + return this.color === NotificationColor.Bold && !hideBold; + } } public get hasUnreadCount(): boolean { @@ -81,11 +98,15 @@ export abstract class NotificationState public destroy(): void { this.removeAllListeners(NotificationStateEvents.Update); + for (const watcherReference of this.watcherReferences) { + SettingsStore.unwatchSetting(watcherReference); + } + this.watcherReferences = []; } } export class NotificationStateSnapshot { - private readonly symbol: string; + private readonly symbol: string | null; private readonly count: number; private readonly color: NotificationColor; diff --git a/src/stores/notifications/RoomNotificationState.ts b/src/stores/notifications/RoomNotificationState.ts index dca3e290e3..559ae55de1 100644 --- a/src/stores/notifications/RoomNotificationState.ts +++ b/src/stores/notifications/RoomNotificationState.ts @@ -98,8 +98,8 @@ export class RoomNotificationState extends NotificationState implements IDestroy this.updateNotificationState(); }; - private handleRoomEventUpdate = (event: MatrixEvent, room: Room | null) => { - if (room?.roomId !== this.room.roomId) return; // ignore - not for us or notifications timeline + private handleRoomEventUpdate = (event: MatrixEvent) => { + if (event?.getRoomId() !== this.room.roomId) return; // ignore - not for us or notifications timeline this.updateNotificationState(); }; diff --git a/src/stores/notifications/SpaceNotificationState.ts b/src/stores/notifications/SpaceNotificationState.ts index 241530f77f..0df920b566 100644 --- a/src/stores/notifications/SpaceNotificationState.ts +++ b/src/stores/notifications/SpaceNotificationState.ts @@ -32,7 +32,7 @@ export class SpaceNotificationState extends NotificationState { super(); } - public get symbol(): string { + public get symbol(): string | null { return this._color === NotificationColor.Unsent ? "!" : null; } diff --git a/src/stores/notifications/StaticNotificationState.ts b/src/stores/notifications/StaticNotificationState.ts index b18aa78e0f..fce8bee217 100644 --- a/src/stores/notifications/StaticNotificationState.ts +++ b/src/stores/notifications/StaticNotificationState.ts @@ -20,7 +20,7 @@ import { NotificationState } from "./NotificationState"; export class StaticNotificationState extends NotificationState { public static readonly RED_EXCLAMATION = StaticNotificationState.forSymbol("!", NotificationColor.Red); - constructor(symbol: string, count: number, color: NotificationColor) { + constructor(symbol: string | null, count: number, color: NotificationColor) { super(); this._symbol = symbol; this._count = count; diff --git a/test/components/views/rooms/NotificationBadge/NotificationBadge-test.tsx b/test/components/views/rooms/NotificationBadge/NotificationBadge-test.tsx index 95d598a704..e0c503d6c5 100644 --- a/test/components/views/rooms/NotificationBadge/NotificationBadge-test.tsx +++ b/test/components/views/rooms/NotificationBadge/NotificationBadge-test.tsx @@ -20,6 +20,7 @@ import React from "react"; import { StatelessNotificationBadge, } from "../../../../../src/components/views/rooms/NotificationBadge/StatelessNotificationBadge"; +import SettingsStore from "../../../../../src/settings/SettingsStore"; import { NotificationColor } from "../../../../../src/stores/notifications/NotificationColor"; describe("NotificationBadge", () => { @@ -45,5 +46,19 @@ describe("NotificationBadge", () => { fireEvent.mouseLeave(container.firstChild); expect(cb).toHaveBeenCalledTimes(3); }); + + it("hides the bold icon when the settings is set", () => { + jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => { + return name === "feature_hidebold"; + }); + + const { container } = render(); + + expect(container.firstChild).toBeNull(); + }); }); }); diff --git a/test/components/views/spaces/QuickThemeSwitcher-test.tsx b/test/components/views/spaces/QuickThemeSwitcher-test.tsx index 4efa1473b2..28a0e3e954 100644 --- a/test/components/views/spaces/QuickThemeSwitcher-test.tsx +++ b/test/components/views/spaces/QuickThemeSwitcher-test.tsx @@ -38,6 +38,7 @@ jest.mock('../../../../src/settings/SettingsStore', () => ({ setValue: jest.fn(), getValue: jest.fn(), monitorSetting: jest.fn(), + watchSetting: jest.fn(), })); jest.mock('../../../../src/dispatcher/dispatcher', () => ({ diff --git a/test/stores/TypingStore-test.ts b/test/stores/TypingStore-test.ts index a5b4437f14..b6b5c388f8 100644 --- a/test/stores/TypingStore-test.ts +++ b/test/stores/TypingStore-test.ts @@ -25,6 +25,7 @@ import { TestSdkContext } from "../TestSdkContext"; jest.mock("../../src/settings/SettingsStore", () => ({ getValue: jest.fn(), monitorSetting: jest.fn(), + watchSetting: jest.fn(), })); describe("TypingStore", () => { diff --git a/test/utils/MultiInviter-test.ts b/test/utils/MultiInviter-test.ts index 83b71232fc..49c2ebbeaf 100644 --- a/test/utils/MultiInviter-test.ts +++ b/test/utils/MultiInviter-test.ts @@ -42,6 +42,7 @@ jest.mock('../../src/Modal', () => ({ jest.mock('../../src/settings/SettingsStore', () => ({ getValue: jest.fn(), monitorSetting: jest.fn(), + watchSetting: jest.fn(), })); const mockPromptBeforeInviteUnknownUsers = (value: boolean) => { From 336b96acc3d2e6de3cf509099fef2d9d15e0b145 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 6 Dec 2022 12:40:19 +0000 Subject: [PATCH 15/19] Upgrade matrix-js-sdk to 22.0.0 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 9c4972981b..41d2e02e51 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "maplibre-gl": "^1.15.2", "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "0.0.1", - "matrix-js-sdk": "22.0.0-rc.2", + "matrix-js-sdk": "22.0.0", "matrix-widget-api": "^1.1.1", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/yarn.lock b/yarn.lock index e44f797940..07017a3b92 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7167,10 +7167,10 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -matrix-js-sdk@22.0.0-rc.2: - version "22.0.0-rc.2" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-22.0.0-rc.2.tgz#101a3bf54b67d8c96b17dbc9bfdbed8457e3b673" - integrity sha512-yvfIfrlemxE+fhJHlmZqvwa/NVkV0zH0H+0ktxidd5WiXExMJL07uDZd9WyCOZb3vTkcxBiSDR9UWBfLJvQdjg== +matrix-js-sdk@22.0.0: + version "22.0.0" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-22.0.0.tgz#8e396a1798d6d1515a92cf8f544b0010bd0c9e85" + integrity sha512-mpKqeD3nCobjGiUiATUyEoP44n+AzDW5cSeBTIBY5fPhj0AkzLJhblHt40vzSOJazj8tT0PhsSzhEIR9hGzYGA== dependencies: "@babel/runtime" "^7.12.5" "@types/sdp-transform" "^2.4.5" From 143a3b16b354b201a4e9798c269170f95d7be368 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 6 Dec 2022 12:47:42 +0000 Subject: [PATCH 16/19] Prepare changelog for v3.62.0 --- CHANGELOG.md | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 428d1d1024..6d46f19fb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,5 @@ -Changes in [3.62.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.62.0-rc.2) (2022-12-02) -=============================================================================================================== - -## 🐛 Bug Fixes - * Fix call splitbrains when switching between rooms ([\#9692](https://github.com/matrix-org/matrix-react-sdk/pull/9692)). - -Changes in [3.62.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.62.0-rc.1) (2022-11-29) -=============================================================================================================== +Changes in [3.62.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.62.0) (2022-12-06) +===================================================================================================== ## ✨ Features * Further improve replies ([\#6396](https://github.com/matrix-org/matrix-react-sdk/pull/6396)). Fixes vector-im/element-web#19074, vector-im/element-web#18194 vector-im/element-web#18027 and vector-im/element-web#19179. @@ -29,6 +23,8 @@ Changes in [3.62.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases * Fix regression with TimelinePanel props updates not taking effect ([\#9608](https://github.com/matrix-org/matrix-react-sdk/pull/9608)). Fixes vector-im/element-web#23794. * Fix form tooltip positioning ([\#9598](https://github.com/matrix-org/matrix-react-sdk/pull/9598)). Fixes vector-im/element-web#22861. * Extract Search handling from RoomView into its own Component ([\#9574](https://github.com/matrix-org/matrix-react-sdk/pull/9574)). Fixes vector-im/element-web#498. + * Fix call splitbrains when switching between rooms ([\#9692](https://github.com/matrix-org/matrix-react-sdk/pull/9692)). + * Fix replies to emotes not showing as inline ([\#9707](https://github.com/matrix-org/matrix-react-sdk/pull/9707)). Fixes vector-im/element-web#23903. Changes in [3.61.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.61.0) (2022-11-22) ===================================================================================================== From df0eba7eab8a64de9a715274c902f6f6fc718805 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 6 Dec 2022 12:47:43 +0000 Subject: [PATCH 17/19] v3.62.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 41d2e02e51..738268babf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.62.0-rc.2", + "version": "3.62.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From 5e5a5642d2a5cc2bf22ea423fb54b73829f8785f Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 6 Dec 2022 12:51:22 +0000 Subject: [PATCH 18/19] Resetting package fields for development --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 0b7486ab77..1017a9dc92 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "package.json", ".stylelintrc.js" ], - "main": "./lib/index.ts", + "main": "./src/index.ts", "matrix_src_main": "./src/index.ts", "matrix_lib_main": "./lib/index.ts", "matrix_lib_typings": "./lib/index.d.ts", @@ -256,6 +256,5 @@ "outputDirectory": "coverage", "outputName": "jest-sonar-report.xml", "relativePaths": true - }, - "typings": "./lib/index.d.ts" + } } From 9914b0bafd23d9aa25486079d1dc3407cbe33aed Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 6 Dec 2022 12:52:58 +0000 Subject: [PATCH 19/19] Reset matrix-js-sdk back to develop branch --- package.json | 2 +- yarn.lock | 17 ++++++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 1017a9dc92..70b92dbebf 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "maplibre-gl": "^1.15.2", "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "0.0.1", - "matrix-js-sdk": "22.0.0", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "matrix-widget-api": "^1.1.1", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/yarn.lock b/yarn.lock index 3e7eb42f91..67ac2305da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2267,11 +2267,6 @@ resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== -"@types/sdp-transform@^2.4.5": - version "2.4.5" - resolved "https://registry.yarnpkg.com/@types/sdp-transform/-/sdp-transform-2.4.5.tgz#3167961e0a1a5265545e278627aa37c606003f53" - integrity sha512-GVO0gnmbyO3Oxm2HdPsYUNcyihZE3GyCY8ysMYHuQGfLhGZq89Nm4lSzULWTzZoyHtg+VO/IdrnxZHPnPSGnAg== - "@types/semver@^7.3.12": version "7.3.13" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91" @@ -6356,13 +6351,11 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -matrix-js-sdk@22.0.0: +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": version "22.0.0" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-22.0.0.tgz#8e396a1798d6d1515a92cf8f544b0010bd0c9e85" - integrity sha512-mpKqeD3nCobjGiUiATUyEoP44n+AzDW5cSeBTIBY5fPhj0AkzLJhblHt40vzSOJazj8tT0PhsSzhEIR9hGzYGA== + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/ccab6985ad5567960fa9bc4cd95fc39241560b80" dependencies: "@babel/runtime" "^7.12.5" - "@types/sdp-transform" "^2.4.5" another-json "^0.2.0" bs58 "^5.0.0" content-type "^1.0.4" @@ -6373,6 +6366,7 @@ matrix-js-sdk@22.0.0: qs "^6.9.6" sdp-transform "^2.14.1" unhomoglyph "^1.0.6" + uuid "7" matrix-mock-request@^2.5.0: version "2.6.0" @@ -8487,6 +8481,11 @@ util-deprecate@^1.0.2, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== +uuid@7: + version "7.0.3" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b" + integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg== + uuid@8.3.2, uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"