{
);
}
}
+
+const RoomTileHOC: React.FC = (props: Props) => {
+ const hasLiveVoiceBroadcast = useHasRoomLiveVoiceBroadcast(props.room);
+ return ;
+};
+
+export default RoomTileHOC;
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 4c8e4d33c3..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)",
@@ -3496,6 +3497,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 +3506,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/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/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/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/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/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({});
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/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/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/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);
};
}
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) => {
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);
+ });
+ });
+});
diff --git a/yarn.lock b/yarn.lock
index bd158677a6..89aa9ee0ab 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"
@@ -6357,11 +6352,10 @@ matrix-events-sdk@0.0.1:
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/1606274c36008b6a976a5e4b47cdd13a1e4e5997"
+ version "22.0.0"
+ 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"
@@ -6372,6 +6366,7 @@ matrix-events-sdk@0.0.1:
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"
@@ -8486,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"