{ 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 } },
+ ) }
+
{
);
}
}
+
+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`] = `
+