From bf56a315c24c4bf483dd1add2c5dd31a8c09f020 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 4 Oct 2022 14:05:33 +0100 Subject: [PATCH 01/33] Upgrade matrix-js-sdk to 20.1.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 82b26a93f5..451145c140 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "maplibre-gl": "^1.15.2", "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "^0.0.1-beta.7", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "20.1.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 965d66f09c..a3bbb6b296 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6925,9 +6925,10 @@ matrix-events-sdk@^0.0.1-beta.7: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1-beta.7.tgz#5ffe45eba1f67cc8d7c2377736c728b322524934" integrity sha512-9jl4wtWanUFSy2sr2lCjErN/oC8KTAtaeaozJtrgot1JiQcEI4Rda9OLgQ7nLKaqb4Z/QUx/fR3XpDzm5Jy1JA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": - version "20.0.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/83fca5b57d8fe1b8c18444129a2e2318129753d5" +matrix-js-sdk@20.1.0-rc.1: + version "20.1.0-rc.1" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-20.1.0-rc.1.tgz#68018c6aca3eebcc648463c914b8b54c6967d797" + integrity sha512-l0a92PNWlCCz4ZMK7u/91kK/Cnxe7m0TusLvSbKK7gmzTxRDGYpdadAeuZ0/Vc8vgH904XUSilV0DoCjCIiCXA== dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" From ede9a07df17ec4bdd223327fa3a525f3af762a86 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 4 Oct 2022 14:08:13 +0100 Subject: [PATCH 02/33] Prepare changelog for v3.58.0-rc.1 --- CHANGELOG.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66cb4bd3b5..aaada45195 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,28 @@ +Changes in [3.58.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.58.0-rc.1) (2022-10-04) +=============================================================================================================== + +## ✨ Features + * Device manager - select all devices ([\#9330](https://github.com/matrix-org/matrix-react-sdk/pull/9330)). + * New group call experience: Call tiles ([\#9332](https://github.com/matrix-org/matrix-react-sdk/pull/9332)). + * Add Shift key to FormatQuote keyboard shortcut ([\#9298](https://github.com/matrix-org/matrix-react-sdk/pull/9298)). Contributed by @owi92. + * Device manager - sign out of multiple sessions ([\#9325](https://github.com/matrix-org/matrix-react-sdk/pull/9325)). + * Display push toggle for web sessions (MSC3890) ([\#9327](https://github.com/matrix-org/matrix-react-sdk/pull/9327)). + * Add device notifications enabled switch ([\#9324](https://github.com/matrix-org/matrix-react-sdk/pull/9324)). + * Implement push notification toggle in device detail ([\#9308](https://github.com/matrix-org/matrix-react-sdk/pull/9308)). + * New group call experience: Starting and ending calls ([\#9318](https://github.com/matrix-org/matrix-react-sdk/pull/9318)). + * New group call experience: Room header call buttons ([\#9311](https://github.com/matrix-org/matrix-react-sdk/pull/9311)). + * Make device ID copyable in device list ([\#9297](https://github.com/matrix-org/matrix-react-sdk/pull/9297)). + * Use display name instead of user ID when rendering power events ([\#9295](https://github.com/matrix-org/matrix-react-sdk/pull/9295)). + * Read receipts for threads ([\#9239](https://github.com/matrix-org/matrix-react-sdk/pull/9239)). Fixes vector-im/element-web#23191. + +## 🐛 Bug Fixes + * Fix device selection in pre-join screen for Element Call video rooms ([\#9321](https://github.com/matrix-org/matrix-react-sdk/pull/9321)). Fixes vector-im/element-web#23331. + * Don't render a 1px high room topic if the room topic is empty ([\#9317](https://github.com/matrix-org/matrix-react-sdk/pull/9317)). Contributed by @Arnei. + * Don't show feedback prompts when that UIFeature is disabled ([\#9305](https://github.com/matrix-org/matrix-react-sdk/pull/9305)). Fixes vector-im/element-web#23327. + * Fix soft crash around unknown room pills ([\#9301](https://github.com/matrix-org/matrix-react-sdk/pull/9301)). Fixes matrix-org/element-web-rageshakes#15465. + * Fix spaces feedback prompt wrongly showing when feedback is disabled ([\#9302](https://github.com/matrix-org/matrix-react-sdk/pull/9302)). Fixes vector-im/element-web#23314. + * Fix tile soft crash in ReplyInThreadButton ([\#9300](https://github.com/matrix-org/matrix-react-sdk/pull/9300)). Fixes matrix-org/element-web-rageshakes#15493. + Changes in [3.57.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.57.0) (2022-09-28) ===================================================================================================== From a64ab9d085442bb0d46aba413b6ba3cb16f015fa Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 4 Oct 2022 14:08:14 +0100 Subject: [PATCH 03/33] v3.58.0-rc.1 --- package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 451145c140..67b3b38e14 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.57.0", + "version": "3.58.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", @@ -251,5 +251,6 @@ "jestSonar": { "reportPath": "coverage", "sonar56x": true - } + }, + "typings": "./lib/index.d.ts" } From 5619de03c41b05be96d0bb6eb335fb502a979fc4 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 5 Oct 2022 13:27:37 +0100 Subject: [PATCH 04/33] Upgrade matrix-js-sdk to 20.1.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 67b3b38e14..644a8adf49 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "maplibre-gl": "^1.15.2", "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "^0.0.1-beta.7", - "matrix-js-sdk": "20.1.0-rc.1", + "matrix-js-sdk": "20.1.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 a3bbb6b296..3b6d0409ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6925,10 +6925,10 @@ matrix-events-sdk@^0.0.1-beta.7: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1-beta.7.tgz#5ffe45eba1f67cc8d7c2377736c728b322524934" integrity sha512-9jl4wtWanUFSy2sr2lCjErN/oC8KTAtaeaozJtrgot1JiQcEI4Rda9OLgQ7nLKaqb4Z/QUx/fR3XpDzm5Jy1JA== -matrix-js-sdk@20.1.0-rc.1: - version "20.1.0-rc.1" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-20.1.0-rc.1.tgz#68018c6aca3eebcc648463c914b8b54c6967d797" - integrity sha512-l0a92PNWlCCz4ZMK7u/91kK/Cnxe7m0TusLvSbKK7gmzTxRDGYpdadAeuZ0/Vc8vgH904XUSilV0DoCjCIiCXA== +matrix-js-sdk@20.1.0-rc.2: + version "20.1.0-rc.2" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-20.1.0-rc.2.tgz#f20ca324c68406734f7c80c41576f3c16cd4245b" + integrity sha512-X8/JBbw6ulmVcUQ9xwWkH7FR9O5YE54jADCkOhNShwvceCXFHRbXUL2eG/LeNQG+JPp2HgAnwCltDnwOc3ZwGA== dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" From af4382ba74b639409d33d31ebac67c354ceb5999 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 5 Oct 2022 13:32:20 +0100 Subject: [PATCH 05/33] Prepare changelog for v3.58.0-rc.2 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aaada45195..8521ce4c13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +Changes in [3.58.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.58.0-rc.2) (2022-10-05) +=============================================================================================================== + +## 🐛 Bug Fixes + * Use the correct sender key when checking shared secret ([\#2730](https://github.com/matrix-org/matrix-js-sdk/pull/2730)). Fixes vector-im/element-web#23374. + Changes in [3.58.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.58.0-rc.1) (2022-10-04) =============================================================================================================== From 630511de1c6d700197899cb6e3120cd0a734cc72 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 5 Oct 2022 13:32:21 +0100 Subject: [PATCH 06/33] v3.58.0-rc.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 644a8adf49..eb64a490fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.58.0-rc.1", + "version": "3.58.0-rc.2", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From 87d3fbd9961436426ff6ec7f59d3ec8cd8ef1e43 Mon Sep 17 00:00:00 2001 From: Kerry Date: Tue, 11 Oct 2022 11:10:55 +0200 Subject: [PATCH 07/33] Device manager - promote to beta (#9380) * promote new session manager to beta * hide old sessions section when new dm enabled * use correct logic * add new ViewUserDeviceSettings action * replace device management ctas with viewUserDeviceSettings * test SecurityUserSettingsTab * more complete mocks * more thorough mocks * more mocks * test LabsUserSettingsTab * lint * updated copy * update snaps for new copy --- .../handlers/viewUserDeviceSettings.ts | 30 +++ src/components/structures/MatrixChat.tsx | 5 + src/components/views/right_panel/UserInfo.tsx | 4 +- .../tabs/user/LabsUserSettingsTab.tsx | 11 +- .../tabs/user/SecurityUserSettingsTab.tsx | 20 +- src/dispatcher/actions.ts | 5 + src/i18n/strings/en_EN.json | 5 +- src/settings/Settings.tsx | 18 +- src/toasts/BulkUnverifiedSessionsToast.ts | 4 +- src/toasts/UnverifiedSessionToast.ts | 4 +- .../handlers/viewUserDeviceSettings-test.ts | 48 +++++ .../tabs/user/LabsUserSettingsTab-test.tsx | 73 +++++++ .../user/SecurityUserSettingsTab-test.tsx | 68 ++++++ .../LabsUserSettingsTab-test.tsx.snap | 196 ++++++++++++++++++ test/test-utils/client.ts | 32 ++- 15 files changed, 504 insertions(+), 19 deletions(-) create mode 100644 src/actions/handlers/viewUserDeviceSettings.ts create mode 100644 test/actions/handlers/viewUserDeviceSettings-test.ts create mode 100644 test/components/views/settings/tabs/user/LabsUserSettingsTab-test.tsx create mode 100644 test/components/views/settings/tabs/user/SecurityUserSettingsTab-test.tsx create mode 100644 test/components/views/settings/tabs/user/__snapshots__/LabsUserSettingsTab-test.tsx.snap diff --git a/src/actions/handlers/viewUserDeviceSettings.ts b/src/actions/handlers/viewUserDeviceSettings.ts new file mode 100644 index 0000000000..e1dc7b3f26 --- /dev/null +++ b/src/actions/handlers/viewUserDeviceSettings.ts @@ -0,0 +1,30 @@ +/* +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 { UserTab } from "../../components/views/dialogs/UserTab"; +import { Action } from "../../dispatcher/actions"; +import defaultDispatcher from "../../dispatcher/dispatcher"; + +/** + * Redirect to the correct device manager section + * Based on the labs setting + */ +export const viewUserDeviceSettings = (isNewDeviceManagerEnabled: boolean) => { + defaultDispatcher.dispatch({ + action: Action.ViewUserSettings, + initialTabId: isNewDeviceManagerEnabled ? UserTab.SessionManager : UserTab.Security, + }); +}; diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 6dd2820aa1..923f461092 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -137,6 +137,7 @@ import { TimelineRenderingType } from "../../contexts/RoomContext"; import { UseCaseSelection } from '../views/elements/UseCaseSelection'; import { ValidatedServerConfig } from '../../utils/ValidatedServerConfig'; import { isLocalRoom } from '../../utils/localRoom/isLocalRoom'; +import { viewUserDeviceSettings } from '../../actions/handlers/viewUserDeviceSettings'; // legacy export export { default as Views } from "../../Views"; @@ -677,6 +678,10 @@ export default class MatrixChat extends React.PureComponent { } break; } + case Action.ViewUserDeviceSettings: { + viewUserDeviceSettings(SettingsStore.getValue("feature_new_device_manager")); + break; + } case Action.ViewUserSettings: { const tabPayload = payload as OpenToTabPayload; Modal.createDialog(UserSettingsDialog, diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index 45489603ba..810ae48dd7 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -48,7 +48,6 @@ import EncryptionPanel from "./EncryptionPanel"; import { useAsyncMemo } from '../../../hooks/useAsyncMemo'; import { legacyVerifyUser, verifyDevice, verifyUser } from '../../../verification'; import { Action } from "../../../dispatcher/actions"; -import { UserTab } from "../dialogs/UserTab"; import { useIsEncrypted } from "../../../hooks/useIsEncrypted"; import BaseCard from "./BaseCard"; import { E2EStatus } from "../../../utils/ShieldUtils"; @@ -1331,8 +1330,7 @@ const BasicUserInfo: React.FC<{ className="mx_UserInfo_field" onClick={() => { dis.dispatch({ - action: Action.ViewUserSettings, - initialTabId: UserTab.Security, + action: Action.ViewUserDeviceSettings, }); }} > diff --git a/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx b/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx index 80e2ebb6cf..6057587626 100644 --- a/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx @@ -80,7 +80,10 @@ export default class LabsUserSettingsTab extends React.Component<{}, IState> { let betaSection; if (betas.length) { - betaSection =
+ betaSection =
{ betas.map(f => ) }
; } @@ -137,7 +140,11 @@ export default class LabsUserSettingsTab extends React.Component<{}, IState> { labsSections = <> { sortBy(Array.from(groups.entries()), "0").map(([group, flags]) => ( -
+
{ _t(labGroupNames[group]) } { flags }
diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx index 91b448eb3b..f4e4e55513 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx @@ -346,19 +346,29 @@ export default class SecurityUserSettingsTab extends React.Component - { warning } + const useNewSessionManager = SettingsStore.getValue("feature_new_device_manager"); + const devicesSection = useNewSessionManager + ? null + : <>
{ _t("Where you're signed in") }
-
+
{ _t( "Manage your signed-in devices below. " + - "A device's name is visible to people you communicate with.", + "A device's name is visible to people you communicate with.", ) }
+ ; + + return ( +
+ { warning } + { devicesSection }
{ _t("Encryption") }
{ secureBackup } diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index 4e161a7005..2b2e443e81 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -40,6 +40,11 @@ export enum Action { */ ViewUserSettings = "view_user_settings", + /** + * Open the user device settings. No additional payload information required. + */ + ViewUserDeviceSettings = "view_user_device_settings", + /** * Opens the room directory. No additional payload information required. */ diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 737f3e8879..5bd66ec9a3 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -922,7 +922,10 @@ "Live Location Sharing (temporary implementation: locations persist in room history)": "Live Location Sharing (temporary implementation: locations persist in room history)", "Favourite Messages (under active development)": "Favourite Messages (under active development)", "Voice broadcast (under active development)": "Voice broadcast (under active development)", - "Use new session manager (under active development)": "Use new session manager (under active development)", + "Use new session manager": "Use new session manager", + "New session manager": "New session manager", + "Have greater visibility and control over all your sessions.": "Have greater visibility and control over all your sessions.", + "Our new sessions manager provides better visibility of all your sessions, and greater control over them including the ability to remotely toggle push notifications.": "Our new sessions manager provides better visibility of all your sessions, and greater control over them including the ability to remotely toggle push notifications.", "Font size": "Font size", "Use custom size": "Use custom size", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index a4e55e6fcd..60fd06ef85 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -475,8 +475,24 @@ export const SETTINGS: {[setting: string]: ISetting} = { isFeature: true, labsGroup: LabGroup.Experimental, supportedLevels: LEVELS_FEATURE, - displayName: _td("Use new session manager (under active development)"), + displayName: _td("Use new session manager"), default: false, + betaInfo: { + title: _td('New session manager'), + caption: () => <> +

+ { _td('Have greater visibility and control over all your sessions.') } +

+

+ { _td( + 'Our new sessions manager provides better visibility of all your sessions, ' + + 'and greater control over them including the ability to remotely toggle push notifications.', + ) + } +

+ + , + }, }, "baseFontSize": { displayName: _td("Font size"), diff --git a/src/toasts/BulkUnverifiedSessionsToast.ts b/src/toasts/BulkUnverifiedSessionsToast.ts index 6ddb0d7db5..0113f2f030 100644 --- a/src/toasts/BulkUnverifiedSessionsToast.ts +++ b/src/toasts/BulkUnverifiedSessionsToast.ts @@ -20,7 +20,6 @@ import DeviceListener from '../DeviceListener'; import GenericToast from "../components/views/toasts/GenericToast"; import ToastStore from "../stores/ToastStore"; import { Action } from "../dispatcher/actions"; -import { UserTab } from "../components/views/dialogs/UserTab"; const TOAST_KEY = "reviewsessions"; @@ -29,8 +28,7 @@ export const showToast = (deviceIds: Set) => { DeviceListener.sharedInstance().dismissUnverifiedSessions(deviceIds); dis.dispatch({ - action: Action.ViewUserSettings, - initialTabId: UserTab.Security, + action: Action.ViewUserDeviceSettings, }); }; diff --git a/src/toasts/UnverifiedSessionToast.ts b/src/toasts/UnverifiedSessionToast.ts index d0db97cd08..f2d637ef0d 100644 --- a/src/toasts/UnverifiedSessionToast.ts +++ b/src/toasts/UnverifiedSessionToast.ts @@ -21,7 +21,6 @@ import DeviceListener from '../DeviceListener'; import ToastStore from "../stores/ToastStore"; import GenericToast from "../components/views/toasts/GenericToast"; import { Action } from "../dispatcher/actions"; -import { UserTab } from "../components/views/dialogs/UserTab"; function toastKey(deviceId: string) { return "unverified_session_" + deviceId; @@ -33,8 +32,7 @@ export const showToast = async (deviceId: string) => { const onAccept = () => { DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]); dis.dispatch({ - action: Action.ViewUserSettings, - initialTabId: UserTab.Security, + action: Action.ViewUserDeviceSettings, }); }; diff --git a/test/actions/handlers/viewUserDeviceSettings-test.ts b/test/actions/handlers/viewUserDeviceSettings-test.ts new file mode 100644 index 0000000000..72d1db430d --- /dev/null +++ b/test/actions/handlers/viewUserDeviceSettings-test.ts @@ -0,0 +1,48 @@ +/* +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 { viewUserDeviceSettings } from "../../../src/actions/handlers/viewUserDeviceSettings"; +import { UserTab } from "../../../src/components/views/dialogs/UserTab"; +import { Action } from "../../../src/dispatcher/actions"; +import defaultDispatcher from "../../../src/dispatcher/dispatcher"; + +describe('viewUserDeviceSettings()', () => { + const dispatchSpy = jest.spyOn(defaultDispatcher, 'dispatch'); + + beforeEach(() => { + dispatchSpy.mockClear(); + }); + + it('dispatches action to view new session manager when enabled', () => { + const isNewDeviceManagerEnabled = true; + viewUserDeviceSettings(isNewDeviceManagerEnabled); + + expect(dispatchSpy).toHaveBeenCalledWith({ + action: Action.ViewUserSettings, + initialTabId: UserTab.SessionManager, + }); + }); + + it('dispatches action to view old session manager when disabled', () => { + const isNewDeviceManagerEnabled = false; + viewUserDeviceSettings(isNewDeviceManagerEnabled); + + expect(dispatchSpy).toHaveBeenCalledWith({ + action: Action.ViewUserSettings, + initialTabId: UserTab.Security, + }); + }); +}); diff --git a/test/components/views/settings/tabs/user/LabsUserSettingsTab-test.tsx b/test/components/views/settings/tabs/user/LabsUserSettingsTab-test.tsx new file mode 100644 index 0000000000..614bb4062f --- /dev/null +++ b/test/components/views/settings/tabs/user/LabsUserSettingsTab-test.tsx @@ -0,0 +1,73 @@ +/* +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 { render } from '@testing-library/react'; + +import LabsUserSettingsTab from '../../../../../../src/components/views/settings/tabs/user/LabsUserSettingsTab'; +import SettingsStore from '../../../../../../src/settings/SettingsStore'; +import { + getMockClientWithEventEmitter, + mockClientMethodsServer, + mockClientMethodsUser, +} from '../../../../../test-utils'; +import SdkConfig from '../../../../../../src/SdkConfig'; + +describe('', () => { + const sdkConfigSpy = jest.spyOn(SdkConfig, 'get'); + + const defaultProps = { + closeSettingsFn: jest.fn(), + }; + const getComponent = () => ; + + const userId = '@alice:server.org'; + getMockClientWithEventEmitter({ + ...mockClientMethodsUser(userId), + ...mockClientMethodsServer(), + }); + + const settingsValueSpy = jest.spyOn(SettingsStore, 'getValue'); + + beforeEach(() => { + jest.clearAllMocks(); + settingsValueSpy.mockReturnValue(false); + sdkConfigSpy.mockReturnValue(false); + }); + + it('renders settings marked as beta as beta cards', () => { + const { getByTestId } = render(getComponent()); + expect(getByTestId("labs-beta-section")).toMatchSnapshot(); + }); + + it('does not render non-beta labs settings when disabled in config', () => { + const { container } = render(getComponent()); + expect(sdkConfigSpy).toHaveBeenCalledWith('show_labs_settings'); + + const labsSections = container.getElementsByClassName('mx_SettingsTab_section'); + // only section is beta section + expect(labsSections.length).toEqual(1); + }); + + it('renders non-beta labs settings when enabled in config', () => { + // enable labs + sdkConfigSpy.mockImplementation(configName => configName === 'show_labs_settings'); + const { container } = render(getComponent()); + + const labsSections = container.getElementsByClassName('mx_SettingsTab_section'); + expect(labsSections.length).toEqual(11); + }); +}); diff --git a/test/components/views/settings/tabs/user/SecurityUserSettingsTab-test.tsx b/test/components/views/settings/tabs/user/SecurityUserSettingsTab-test.tsx new file mode 100644 index 0000000000..bddb493463 --- /dev/null +++ b/test/components/views/settings/tabs/user/SecurityUserSettingsTab-test.tsx @@ -0,0 +1,68 @@ +/* +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 { render } from '@testing-library/react'; +import React from 'react'; + +import SecurityUserSettingsTab from "../../../../../../src/components/views/settings/tabs/user/SecurityUserSettingsTab"; +import SettingsStore from '../../../../../../src/settings/SettingsStore'; +import { + getMockClientWithEventEmitter, + mockClientMethodsServer, + mockClientMethodsUser, + mockClientMethodsCrypto, + mockClientMethodsDevice, + mockPlatformPeg, +} from '../../../../../test-utils'; + +describe('', () => { + const defaultProps = { + closeSettingsFn: jest.fn(), + }; + const getComponent = () => ; + + const userId = '@alice:server.org'; + const deviceId = 'alices-device'; + getMockClientWithEventEmitter({ + ...mockClientMethodsUser(userId), + ...mockClientMethodsServer(), + ...mockClientMethodsDevice(deviceId), + ...mockClientMethodsCrypto(), + getRooms: jest.fn().mockReturnValue([]), + getIgnoredUsers: jest.fn(), + }); + + const settingsValueSpy = jest.spyOn(SettingsStore, 'getValue'); + + beforeEach(() => { + mockPlatformPeg(); + jest.clearAllMocks(); + settingsValueSpy.mockReturnValue(false); + }); + + it('renders sessions section when new session manager is disabled', () => { + settingsValueSpy.mockReturnValue(false); + const { getByTestId } = render(getComponent()); + + expect(getByTestId('devices-section')).toBeTruthy(); + }); + + it('does not render sessions section when new session manager is enabled', () => { + settingsValueSpy.mockReturnValue(true); + const { queryByTestId } = render(getComponent()); + + expect(queryByTestId('devices-section')).toBeFalsy(); + }); +}); diff --git a/test/components/views/settings/tabs/user/__snapshots__/LabsUserSettingsTab-test.tsx.snap b/test/components/views/settings/tabs/user/__snapshots__/LabsUserSettingsTab-test.tsx.snap new file mode 100644 index 0000000000..b2ff84a823 --- /dev/null +++ b/test/components/views/settings/tabs/user/__snapshots__/LabsUserSettingsTab-test.tsx.snap @@ -0,0 +1,196 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders settings marked as beta as beta cards 1`] = ` +
+
+
+
+

+ + Video rooms + + + Beta + +

+
+

+ A new way to chat over voice and video in . +

+

+ Video rooms are always-on VoIP channels embedded within a room in . +

+
+
+
+ Join the beta +
+
+
+ Joining the beta will reload . +
+
+
+
+ +
+
+
+
+
+
+

+ + Threads + + + Beta + +

+
+

+ Keep discussions organised with threads. +

+

+ + Threads help keep conversations on-topic and easy to track. + + Learn more + + . + +

+
+
+
+ Join the beta +
+
+
+ Joining the beta will reload . +
+
+
+
+ +
+
+
+
+
+
+

+ + New session manager + + + Beta + +

+
+

+ Have greater visibility and control over all your sessions. +

+

+ Our new sessions manager provides better visibility of all your sessions, and greater control over them including the ability to remotely toggle push notifications. +

+
+
+
+ Join the beta +
+
+
+
+ +
+
+
+
+`; diff --git a/test/test-utils/client.ts b/test/test-utils/client.ts index 4a5b318491..d6ec3f2fd3 100644 --- a/test/test-utils/client.ts +++ b/test/test-utils/client.ts @@ -15,7 +15,7 @@ limitations under the License. */ import EventEmitter from "events"; -import { MethodKeysOf, mocked, MockedObject } from "jest-mock"; +import { MethodKeysOf, mocked, MockedObject, PropertyKeysOf } from "jest-mock"; import { MatrixClient, User } from "matrix-js-sdk/src/matrix"; import { MatrixClientPeg } from "../../src/MatrixClientPeg"; @@ -71,6 +71,7 @@ export const mockClientMethodsUser = (userId = '@alice:domain') => ({ credentials: { userId }, getThreePids: jest.fn().mockResolvedValue({ threepids: [] }), getAccessToken: jest.fn(), + getDeviceId: jest.fn(), }); /** @@ -94,6 +95,35 @@ export const mockClientMethodsServer = (): Partial, unknown>> => ({ + getDeviceId: jest.fn().mockReturnValue(deviceId), + getDeviceEd25519Key: jest.fn(), + getDevices: jest.fn().mockResolvedValue({ devices: [] }), +}); + +export const mockClientMethodsCrypto = (): Partial & PropertyKeysOf, unknown> +> => ({ + isCryptoEnabled: jest.fn(), + isSecretStorageReady: jest.fn(), + isCrossSigningReady: jest.fn(), + isKeyBackupKeyStored: jest.fn(), + getCrossSigningCacheCallbacks: jest.fn().mockReturnValue({ getCrossSigningKeyCache: jest.fn() }), + getStoredCrossSigningForUser: jest.fn(), + checkKeyBackup: jest.fn().mockReturnValue({}), + crypto: { + getSessionBackupPrivateKey: jest.fn(), + secretStorage: { hasKey: jest.fn() }, + crossSigningInfo: { + getId: jest.fn(), + isStoredInSecretStorage: jest.fn(), + }, + }, +}); + From 2d64c21c903977e8deaabe08d59fc0af1679fc9b Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 11 Oct 2022 13:51:54 +0100 Subject: [PATCH 08/33] Upgrade matrix-js-sdk to 20.1.0 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index eb64a490fc..d44ac86499 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "maplibre-gl": "^1.15.2", "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "^0.0.1-beta.7", - "matrix-js-sdk": "20.1.0-rc.2", + "matrix-js-sdk": "20.1.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 3b6d0409ff..a830108c18 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6925,10 +6925,10 @@ matrix-events-sdk@^0.0.1-beta.7: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1-beta.7.tgz#5ffe45eba1f67cc8d7c2377736c728b322524934" integrity sha512-9jl4wtWanUFSy2sr2lCjErN/oC8KTAtaeaozJtrgot1JiQcEI4Rda9OLgQ7nLKaqb4Z/QUx/fR3XpDzm5Jy1JA== -matrix-js-sdk@20.1.0-rc.2: - version "20.1.0-rc.2" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-20.1.0-rc.2.tgz#f20ca324c68406734f7c80c41576f3c16cd4245b" - integrity sha512-X8/JBbw6ulmVcUQ9xwWkH7FR9O5YE54jADCkOhNShwvceCXFHRbXUL2eG/LeNQG+JPp2HgAnwCltDnwOc3ZwGA== +matrix-js-sdk@20.1.0: + version "20.1.0" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-20.1.0.tgz#d235c9125a79901e7e0eb667178bbbdb26710266" + integrity sha512-AjuWVmerJ8aEAIgD6QfmSIg0RrvA8vzvOV+ycvSGg4DgiFlVZbFvBxkVZTRdZ5icJe1/XMaCEf5OZ9Ha2hXUUg== dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" From a2336f110f2110db3e7613417f95474541a8c9c4 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 11 Oct 2022 13:55:50 +0100 Subject: [PATCH 09/33] Prepare changelog for v3.58.0 --- CHANGELOG.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8521ce4c13..6e245f3646 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,9 @@ -Changes in [3.58.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.58.0-rc.2) (2022-10-05) +Changes in [3.58.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.58.0) (2022-10-11) =============================================================================================================== -## 🐛 Bug Fixes - * Use the correct sender key when checking shared secret ([\#2730](https://github.com/matrix-org/matrix-js-sdk/pull/2730)). Fixes vector-im/element-web#23374. +## Deprecations -Changes in [3.58.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.58.0-rc.1) (2022-10-04) -=============================================================================================================== + * Legacy Piwik config.json option `piwik.policy_url` is deprecated in favour of `privacy_policy_url`. Support will be removed in the next release. ## ✨ Features * Device manager - select all devices ([\#9330](https://github.com/matrix-org/matrix-react-sdk/pull/9330)). @@ -22,6 +20,7 @@ Changes in [3.58.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases * Read receipts for threads ([\#9239](https://github.com/matrix-org/matrix-react-sdk/pull/9239)). Fixes vector-im/element-web#23191. ## 🐛 Bug Fixes + * Use the correct sender key when checking shared secret ([\#2730](https://github.com/matrix-org/matrix-js-sdk/pull/2730)). Fixes vector-im/element-web#23374. * Fix device selection in pre-join screen for Element Call video rooms ([\#9321](https://github.com/matrix-org/matrix-react-sdk/pull/9321)). Fixes vector-im/element-web#23331. * Don't render a 1px high room topic if the room topic is empty ([\#9317](https://github.com/matrix-org/matrix-react-sdk/pull/9317)). Contributed by @Arnei. * Don't show feedback prompts when that UIFeature is disabled ([\#9305](https://github.com/matrix-org/matrix-react-sdk/pull/9305)). Fixes vector-im/element-web#23327. From 9dc82fb486cddd62a54eec96faabbfbe2ff6634e Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 11 Oct 2022 13:55:51 +0100 Subject: [PATCH 10/33] v3.58.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d44ac86499..5642124fb3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.58.0-rc.2", + "version": "3.58.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From f4dc1e0a7f8cb76b3b7fdcb090c6e34544125888 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 11 Oct 2022 13:56:13 +0100 Subject: [PATCH 11/33] Resetting package fields for development --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 10a8e93bc8..3ee14cdc96 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", @@ -254,6 +254,5 @@ "jestSonar": { "reportPath": "coverage", "sonar56x": true - }, - "typings": "./lib/index.d.ts" + } } From 7c1c49540a6adb454940ce0436e7a891656b0c7c Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 11 Oct 2022 13:56:23 +0100 Subject: [PATCH 12/33] Reset matrix-js-sdk back to develop branch --- package.json | 2 +- yarn.lock | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 3ee14cdc96..ffc54712bb 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-beta.7", - "matrix-js-sdk": "20.1.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 e653b40779..145de86179 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6935,10 +6935,9 @@ matrix-events-sdk@^0.0.1-beta.7: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1-beta.7.tgz#5ffe45eba1f67cc8d7c2377736c728b322524934" integrity sha512-9jl4wtWanUFSy2sr2lCjErN/oC8KTAtaeaozJtrgot1JiQcEI4Rda9OLgQ7nLKaqb4Z/QUx/fR3XpDzm5Jy1JA== -matrix-js-sdk@20.1.0: +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": version "20.1.0" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-20.1.0.tgz#d235c9125a79901e7e0eb667178bbbdb26710266" - integrity sha512-AjuWVmerJ8aEAIgD6QfmSIg0RrvA8vzvOV+ycvSGg4DgiFlVZbFvBxkVZTRdZ5icJe1/XMaCEf5OZ9Ha2hXUUg== + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/8eed354e17001cd25e3cafe81f74dab499a9882e" dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" From 9e055ee99ddcc45e8bc710abe391f6d694c842e9 Mon Sep 17 00:00:00 2001 From: Kerry Date: Tue, 11 Oct 2022 18:11:11 +0200 Subject: [PATCH 13/33] use correct default for notification silencing (#9388) --- src/utils/notifications.ts | 2 +- test/utils/notifications-test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/notifications.ts b/src/utils/notifications.ts index 7e8aff6d0b..0064eaf2bc 100644 --- a/src/utils/notifications.ts +++ b/src/utils/notifications.ts @@ -51,5 +51,5 @@ export async function createLocalNotificationSettingsIfNeeded(cli: MatrixClient) export function localNotificationsAreSilenced(cli: MatrixClient): boolean { const eventType = getLocalNotificationAccountDataEventType(cli.deviceId); const event = cli.getAccountData(eventType); - return event?.getContent()?.is_silenced ?? true; + return event?.getContent()?.is_silenced ?? false; } diff --git a/test/utils/notifications-test.ts b/test/utils/notifications-test.ts index ba134c1480..c44b496608 100644 --- a/test/utils/notifications-test.ts +++ b/test/utils/notifications-test.ts @@ -81,8 +81,8 @@ describe('notifications', () => { }); describe('localNotificationsAreSilenced', () => { - it('defaults to true when no setting exists', () => { - expect(localNotificationsAreSilenced(mockClient)).toBeTruthy(); + it('defaults to false when no setting exists', () => { + expect(localNotificationsAreSilenced(mockClient)).toBeFalsy(); }); it('checks the persisted value', () => { mockClient.setAccountData(accountDataEventKey, { is_silenced: true }); From 67cae5fe450a8cf89835ea278603928480470051 Mon Sep 17 00:00:00 2001 From: ElementRobot Date: Tue, 11 Oct 2022 17:27:36 +0100 Subject: [PATCH 14/33] [Backport staging] use correct default for notification silencing (#9389) Co-authored-by: Kerry --- src/utils/notifications.ts | 2 +- test/utils/notifications-test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/notifications.ts b/src/utils/notifications.ts index f41edd24bb..f38d58207d 100644 --- a/src/utils/notifications.ts +++ b/src/utils/notifications.ts @@ -25,5 +25,5 @@ export function getLocalNotificationAccountDataEventType(deviceId: string): stri export function localNotificationsAreSilenced(cli: MatrixClient): boolean { const eventType = getLocalNotificationAccountDataEventType(cli.deviceId); const event = cli.getAccountData(eventType); - return event?.getContent()?.is_silenced ?? true; + return event?.getContent()?.is_silenced ?? false; } diff --git a/test/utils/notifications-test.ts b/test/utils/notifications-test.ts index b27a660ebf..9e8a51ad3b 100644 --- a/test/utils/notifications-test.ts +++ b/test/utils/notifications-test.ts @@ -47,8 +47,8 @@ describe('notifications', () => { }); describe('localNotificationsAreSilenced', () => { - it('defaults to true when no setting exists', () => { - expect(localNotificationsAreSilenced(mockClient)).toBeTruthy(); + it('defaults to false when no setting exists', () => { + expect(localNotificationsAreSilenced(mockClient)).toBeFalsy(); }); it('checks the persisted value', () => { mockClient.setAccountData(accountDataEventKey, { is_silenced: true }); From 3c6fe5b6037c03cddcf26240fcfe731bfc4ce17e Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 11 Oct 2022 17:34:49 +0100 Subject: [PATCH 15/33] Prepare changelog for v3.58.1 --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e245f3646..edb088cd64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,13 @@ +Changes in [3.58.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.58.1) (2022-10-11) +===================================================================================================== + +## 🐛 Bug Fixes + * Use correct default for notification silencing ([\#9388](https://github.com/matrix-org/matrix-react-sdk/pull/9388)). Fixes vector-im/element-web#23456. + Changes in [3.58.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.58.0) (2022-10-11) =============================================================================================================== ## Deprecations - * Legacy Piwik config.json option `piwik.policy_url` is deprecated in favour of `privacy_policy_url`. Support will be removed in the next release. ## ✨ Features From f7159b859c9e64d8591c66ea72cf092c7eecb300 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 11 Oct 2022 17:34:49 +0100 Subject: [PATCH 16/33] v3.58.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5642124fb3..2c7fb8c005 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.58.0", + "version": "3.58.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From 03182d03be43af5a19c62f73de9ff038e7006984 Mon Sep 17 00:00:00 2001 From: Kerry Date: Tue, 11 Oct 2022 20:12:02 +0200 Subject: [PATCH 17/33] Device manager - add settings subsection heading component (#9387) * add settings subsection heading component * use named export * newline --- res/css/_components.pcss | 1 + .../settings/shared/_SettingsSubsection.pcss | 4 - .../shared/_SettingsSubsectionHeading.pcss | 27 ++++++ .../settings/shared/SettingsSubsection.tsx | 11 ++- .../shared/SettingsSubsectionHeading.tsx | 31 +++++++ .../CurrentDeviceSection-test.tsx.snap | 36 +++++--- .../SecurityRecommendations-test.tsx.snap | 36 +++++--- .../shared/SettingsSubsection-test.tsx | 11 +++ .../SettingsSubsection-test.tsx.snap | 82 ++++++++++++++++--- .../SessionManagerTab-test.tsx.snap | 24 ++++-- 10 files changed, 211 insertions(+), 52 deletions(-) create mode 100644 res/css/components/views/settings/shared/_SettingsSubsectionHeading.pcss create mode 100644 src/components/views/settings/shared/SettingsSubsectionHeading.tsx diff --git a/res/css/_components.pcss b/res/css/_components.pcss index b2a3752628..efb3e9e594 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -40,6 +40,7 @@ @import "./components/views/settings/devices/_SecurityRecommendations.pcss"; @import "./components/views/settings/devices/_SelectableDeviceTile.pcss"; @import "./components/views/settings/shared/_SettingsSubsection.pcss"; +@import "./components/views/settings/shared/_SettingsSubsectionHeading.pcss"; @import "./components/views/spaces/_QuickThemeSwitcher.pcss"; @import "./components/views/typography/_Caption.pcss"; @import "./structures/_AutoHideScrollbar.pcss"; diff --git a/res/css/components/views/settings/shared/_SettingsSubsection.pcss b/res/css/components/views/settings/shared/_SettingsSubsection.pcss index 9eb51696ba..2ba909aac1 100644 --- a/res/css/components/views/settings/shared/_SettingsSubsection.pcss +++ b/res/css/components/views/settings/shared/_SettingsSubsection.pcss @@ -19,10 +19,6 @@ limitations under the License. box-sizing: border-box; } -.mx_SettingsSubsection_heading { - padding-bottom: $spacing-8; -} - .mx_SettingsSubsection_description { width: 100%; box-sizing: inherit; diff --git a/res/css/components/views/settings/shared/_SettingsSubsectionHeading.pcss b/res/css/components/views/settings/shared/_SettingsSubsectionHeading.pcss new file mode 100644 index 0000000000..e6d4bf4be7 --- /dev/null +++ b/res/css/components/views/settings/shared/_SettingsSubsectionHeading.pcss @@ -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. +*/ + +.mx_SettingsSubsectionHeading { + display: flex; + flex-direction: row; + padding-bottom: $spacing-8; + + gap: $spacing-8; +} + +.mx_SettingsSubsectionHeading_heading { + flex: 1 1 100%; +} diff --git a/src/components/views/settings/shared/SettingsSubsection.tsx b/src/components/views/settings/shared/SettingsSubsection.tsx index 6d23a080ca..9ceff732cb 100644 --- a/src/components/views/settings/shared/SettingsSubsection.tsx +++ b/src/components/views/settings/shared/SettingsSubsection.tsx @@ -16,17 +16,22 @@ limitations under the License. import React, { HTMLAttributes } from "react"; -import Heading from "../../typography/Heading"; +import { SettingsSubsectionHeading } from "./SettingsSubsectionHeading"; export interface SettingsSubsectionProps extends HTMLAttributes { - heading: string; + heading: string | React.ReactNode; description?: string | React.ReactNode; children?: React.ReactNode; } const SettingsSubsection: React.FC = ({ heading, description, children, ...rest }) => (
- { heading } + { typeof heading === 'string' + ? + : <> + { heading } + + } { !!description &&
{ description }
}
{ children } diff --git a/src/components/views/settings/shared/SettingsSubsectionHeading.tsx b/src/components/views/settings/shared/SettingsSubsectionHeading.tsx new file mode 100644 index 0000000000..4a39ff7278 --- /dev/null +++ b/src/components/views/settings/shared/SettingsSubsectionHeading.tsx @@ -0,0 +1,31 @@ +/* +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, { HTMLAttributes } from "react"; + +import Heading from "../../typography/Heading"; + +export interface SettingsSubsectionHeadingProps extends HTMLAttributes { + heading: string; + children?: React.ReactNode; +} + +export const SettingsSubsectionHeading: React.FC = ({ heading, children, ...rest }) => ( +
+ { heading } + { children } +
+); diff --git a/test/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap index 73d7a10130..58356001f5 100644 --- a/test/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap +++ b/test/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap @@ -120,11 +120,15 @@ exports[` handles when device is falsy 1`] = ` class="mx_SettingsSubsection" data-testid="current-session-section" > -

- Current session -

+

+ Current session +

+
@@ -138,11 +142,15 @@ exports[` renders device and correct security card when class="mx_SettingsSubsection" data-testid="current-session-section" > -

- Current session -

+

+ Current session +

+
@@ -258,11 +266,15 @@ exports[` renders device and correct security card when class="mx_SettingsSubsection" data-testid="current-session-section" > -

- Current session -

+

+ Current session +

+
diff --git a/test/components/views/settings/devices/__snapshots__/SecurityRecommendations-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/SecurityRecommendations-test.tsx.snap index b122d91826..a854601344 100644 --- a/test/components/views/settings/devices/__snapshots__/SecurityRecommendations-test.tsx.snap +++ b/test/components/views/settings/devices/__snapshots__/SecurityRecommendations-test.tsx.snap @@ -6,11 +6,15 @@ exports[` renders both cards when user has both unver class="mx_SettingsSubsection" data-testid="security-recommendations-section" > -

- Security recommendations -

+

+ Security recommendations +

+
@@ -109,11 +113,15 @@ exports[` renders inactive devices section when user class="mx_SettingsSubsection" data-testid="security-recommendations-section" > -

- Security recommendations -

+

+ Security recommendations +

+
@@ -212,11 +220,15 @@ exports[` renders unverified devices section when use class="mx_SettingsSubsection" data-testid="security-recommendations-section" > -

- Security recommendations -

+

+ Security recommendations +

+
diff --git a/test/components/views/settings/shared/SettingsSubsection-test.tsx b/test/components/views/settings/shared/SettingsSubsection-test.tsx index acc2e6db95..cd833f90af 100644 --- a/test/components/views/settings/shared/SettingsSubsection-test.tsx +++ b/test/components/views/settings/shared/SettingsSubsection-test.tsx @@ -27,6 +27,17 @@ describe('', () => { const getComponent = (props = {}): React.ReactElement => (); + it('renders with plain text heading', () => { + const { container } = render(getComponent()); + expect(container).toMatchSnapshot(); + }); + + it('renders with react element heading', () => { + const heading =

This is the heading

; + const { container } = render(getComponent({ heading })); + expect(container).toMatchSnapshot(); + }); + it('renders without description', () => { const { container } = render(getComponent()); expect(container).toMatchSnapshot(); diff --git a/test/components/views/settings/shared/__snapshots__/SettingsSubsection-test.tsx.snap b/test/components/views/settings/shared/__snapshots__/SettingsSubsection-test.tsx.snap index 10309d2f67..130b825d65 100644 --- a/test/components/views/settings/shared/__snapshots__/SettingsSubsection-test.tsx.snap +++ b/test/components/views/settings/shared/__snapshots__/SettingsSubsection-test.tsx.snap @@ -5,11 +5,15 @@ exports[` renders with plain text description 1`] = `
-

- Test -

+

+ Test +

+
@@ -26,16 +30,45 @@ exports[` renders with plain text description 1`] = `
`; +exports[` renders with plain text heading 1`] = ` +
+
+
+

+ Test +

+
+
+
+ test settings content +
+
+
+
+`; + exports[` renders with react element description 1`] = `
-

- Test -

+

+ Test +

+
@@ -59,15 +92,13 @@ exports[` renders with react element description 1`] = `
`; -exports[` renders without description 1`] = ` +exports[` renders with react element heading 1`] = `
-

- Test +

+ This is the heading

renders without description 1`] = `
`; + +exports[` renders without description 1`] = ` +
+
+
+

+ Test +

+
+
+
+ test settings content +
+
+
+
+`; diff --git a/test/components/views/settings/tabs/user/__snapshots__/SessionManagerTab-test.tsx.snap b/test/components/views/settings/tabs/user/__snapshots__/SessionManagerTab-test.tsx.snap index 3cee723f28..723c9f18b5 100644 --- a/test/components/views/settings/tabs/user/__snapshots__/SessionManagerTab-test.tsx.snap +++ b/test/components/views/settings/tabs/user/__snapshots__/SessionManagerTab-test.tsx.snap @@ -81,11 +81,15 @@ exports[` renders current session section with a verified s class="mx_SettingsSubsection" data-testid="current-session-section" > -

- Current session -

+

+ Current session +

+
@@ -187,11 +191,15 @@ exports[` renders current session section with an unverifie class="mx_SettingsSubsection" data-testid="current-session-section" > -

- Current session -

+

+ Current session +

+
From bac6e12946200f9599480a05213f5b1587da29d1 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Wed, 12 Oct 2022 00:31:28 +0200 Subject: [PATCH 18/33] Implement Voice Broadcast recording (#9307) * Implement VoiceBroadcastRecording * Implement PR feedback * Add voice broadcast recording stores * Refactor startNewVoiceBroadcastRecording * Refactor VoiceBroadcastRecordingsStore to VoiceBroadcastRecording * Rename VoiceBroadcastRecording to VoiceBroadcastRecorder * Return remaining chunk on stop * Extract createVoiceMessageContent * Implement recording * Replace dev value with config * Fix clientInformation-test * Refactor VoiceBroadcastRecording * Fix VoiceBroadcastRecording types * Re-order getter * Mark voice_broadcast config as optional * Merge voice-broadcast modules * Remove underscore props * Add Optional types * Add return types everywhere * Remove test casts * Add magic comments * Trigger CI * Switch VoiceBroadcastRecorder to TypedEventEmitter * Trigger CI * Add voice broadcast chunk event content Co-authored-by: Travis Ralston --- src/IConfigOptions.ts | 5 + src/SdkConfig.ts | 3 + src/audio/VoiceRecording.ts | 11 +- .../audio/VoiceBroadcastRecorder.ts | 141 ++++++++++ .../components/VoiceBroadcastBody.tsx | 2 +- src/voice-broadcast/components/index.ts | 19 -- src/voice-broadcast/index.ts | 12 +- .../models/VoiceBroadcastRecording.ts | 117 ++++++++- src/voice-broadcast/models/index.ts | 17 -- .../stores/VoiceBroadcastRecordingsStore.ts | 16 +- src/voice-broadcast/stores/index.ts | 17 -- src/voice-broadcast/utils/index.ts | 18 -- .../utils/startNewVoiceBroadcastRecording.ts | 1 + test/SdkConfig-test.ts | 41 +++ .../audio/VoiceBroadcastRecorder-test.ts | 209 +++++++++++++++ .../components/VoiceBroadcastBody-test.tsx | 2 +- .../models/VoiceBroadcastRecording-test.ts | 242 +++++++++++++++++- .../VoiceBroadcastRecordingsStore-test.ts | 2 +- .../startNewVoiceBroadcastRecording-test.ts | 2 + 19 files changed, 773 insertions(+), 104 deletions(-) create mode 100644 src/voice-broadcast/audio/VoiceBroadcastRecorder.ts delete mode 100644 src/voice-broadcast/components/index.ts delete mode 100644 src/voice-broadcast/models/index.ts delete mode 100644 src/voice-broadcast/stores/index.ts delete mode 100644 src/voice-broadcast/utils/index.ts create mode 100644 test/SdkConfig-test.ts create mode 100644 test/voice-broadcast/audio/VoiceBroadcastRecorder-test.ts diff --git a/src/IConfigOptions.ts b/src/IConfigOptions.ts index 91391fc2a9..68d133ecd0 100644 --- a/src/IConfigOptions.ts +++ b/src/IConfigOptions.ts @@ -181,6 +181,11 @@ export interface IConfigOptions { sync_timeline_limit?: number; dangerously_allow_unsafe_and_insecure_passwords?: boolean; // developer option + + voice_broadcast?: { + // length per voice chunk in seconds + chunk_length?: number; + }; } export interface ISsoRedirectOptions { diff --git a/src/SdkConfig.ts b/src/SdkConfig.ts index 235ada7382..0d3400f4bb 100644 --- a/src/SdkConfig.ts +++ b/src/SdkConfig.ts @@ -46,6 +46,9 @@ export const DEFAULTS: IConfigOptions = { logo: require("../res/img/element-desktop-logo.svg").default, url: "https://element.io/get-started", }, + voice_broadcast: { + chunk_length: 60 * 1000, // one minute + }, }; export default class SdkConfig { diff --git a/src/audio/VoiceRecording.ts b/src/audio/VoiceRecording.ts index e98e85aba5..0e18756fe5 100644 --- a/src/audio/VoiceRecording.ts +++ b/src/audio/VoiceRecording.ts @@ -203,9 +203,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { // In testing, recorder time and worker time lag by about 400ms, which is roughly the // time needed to encode a sample/frame. // - // Ref for recorderSeconds: https://github.com/chris-rudmin/opus-recorder#instance-fields - const recorderSeconds = this.recorder.encodedSamplePosition / 48000; - const secondsLeft = TARGET_MAX_LENGTH - recorderSeconds; + const secondsLeft = TARGET_MAX_LENGTH - this.recorderSeconds; if (secondsLeft < 0) { // go over to make sure we definitely capture that last frame // noinspection JSIgnoredPromiseFromCall - we aren't concerned with it overlapping this.stop(); @@ -217,6 +215,13 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { } }; + /** + * {@link https://github.com/chris-rudmin/opus-recorder#instance-fields ref for recorderSeconds} + */ + public get recorderSeconds() { + return this.recorder.encodedSamplePosition / 48000; + } + public async start(): Promise { if (this.recording) { throw new Error("Recording already in progress"); diff --git a/src/voice-broadcast/audio/VoiceBroadcastRecorder.ts b/src/voice-broadcast/audio/VoiceBroadcastRecorder.ts new file mode 100644 index 0000000000..7f084f3f4a --- /dev/null +++ b/src/voice-broadcast/audio/VoiceBroadcastRecorder.ts @@ -0,0 +1,141 @@ +/* +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 { Optional } from "matrix-events-sdk"; +import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter"; + +import { VoiceRecording } from "../../audio/VoiceRecording"; +import SdkConfig, { DEFAULTS } from "../../SdkConfig"; +import { concat } from "../../utils/arrays"; +import { IDestroyable } from "../../utils/IDestroyable"; + +export enum VoiceBroadcastRecorderEvent { + ChunkRecorded = "chunk_recorded", +} + +interface EventMap { + [VoiceBroadcastRecorderEvent.ChunkRecorded]: (chunk: ChunkRecordedPayload) => void; +} + +export interface ChunkRecordedPayload { + buffer: Uint8Array; + length: number; +} + +/** + * This class provides the function to seamlessly record fixed length chunks. + * Subscribe with on(VoiceBroadcastRecordingEvents.ChunkRecorded, (payload: ChunkRecordedPayload) => {}) + * to retrieve chunks while recording. + */ +export class VoiceBroadcastRecorder + extends TypedEventEmitter + implements IDestroyable { + private headers = new Uint8Array(0); + private chunkBuffer = new Uint8Array(0); + private previousChunkEndTimePosition = 0; + private pagesFromRecorderCount = 0; + + public constructor( + private voiceRecording: VoiceRecording, + public readonly targetChunkLength: number, + ) { + super(); + this.voiceRecording.onDataAvailable = this.onDataAvailable; + } + + public async start(): Promise { + return this.voiceRecording.start(); + } + + /** + * Stops the recording and returns the remaining chunk (if any). + */ + public async stop(): Promise> { + await this.voiceRecording.stop(); + return this.extractChunk(); + } + + public get contentType(): string { + return this.voiceRecording.contentType; + } + + private get chunkLength(): number { + return this.voiceRecording.recorderSeconds - this.previousChunkEndTimePosition; + } + + private onDataAvailable = (data: ArrayBuffer): void => { + const dataArray = new Uint8Array(data); + this.pagesFromRecorderCount++; + + if (this.pagesFromRecorderCount <= 2) { + // first two pages contain the headers + this.headers = concat(this.headers, dataArray); + return; + } + + this.handleData(dataArray); + }; + + private handleData(data: Uint8Array): void { + this.chunkBuffer = concat(this.chunkBuffer, data); + this.emitChunkIfTargetLengthReached(); + } + + private emitChunkIfTargetLengthReached(): void { + if (this.chunkLength >= this.targetChunkLength) { + this.emitAndResetChunk(); + } + } + + /** + * Extracts the current chunk and resets the buffer. + */ + private extractChunk(): Optional { + if (this.chunkBuffer.length === 0) { + return null; + } + + const currentRecorderTime = this.voiceRecording.recorderSeconds; + const payload: ChunkRecordedPayload = { + buffer: concat(this.headers, this.chunkBuffer), + length: this.chunkLength, + }; + this.chunkBuffer = new Uint8Array(0); + this.previousChunkEndTimePosition = currentRecorderTime; + return payload; + } + + private emitAndResetChunk(): void { + if (this.chunkBuffer.length === 0) { + return; + } + + this.emit( + VoiceBroadcastRecorderEvent.ChunkRecorded, + this.extractChunk(), + ); + } + + public destroy(): void { + this.removeAllListeners(); + this.voiceRecording.destroy(); + } +} + +export const createVoiceBroadcastRecorder = (): VoiceBroadcastRecorder => { + const targetChunkLength = SdkConfig.get("voice_broadcast")?.chunk_length || DEFAULTS.voice_broadcast!.chunk_length; + return new VoiceBroadcastRecorder(new VoiceRecording(), targetChunkLength); +}; diff --git a/src/voice-broadcast/components/VoiceBroadcastBody.tsx b/src/voice-broadcast/components/VoiceBroadcastBody.tsx index 1a57b5c019..3591325585 100644 --- a/src/voice-broadcast/components/VoiceBroadcastBody.tsx +++ b/src/voice-broadcast/components/VoiceBroadcastBody.tsx @@ -31,7 +31,7 @@ export const VoiceBroadcastBody: React.FC = ({ mxEvent }) => { const client = MatrixClientPeg.get(); const room = client.getRoom(mxEvent.getRoomId()); const recording = VoiceBroadcastRecordingsStore.instance().getByInfoEvent(mxEvent, client); - const [recordingState, setRecordingState] = useState(recording.state); + const [recordingState, setRecordingState] = useState(recording.getState()); useTypedEventEmitter( recording, diff --git a/src/voice-broadcast/components/index.ts b/src/voice-broadcast/components/index.ts deleted file mode 100644 index e98500a5d7..0000000000 --- a/src/voice-broadcast/components/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* -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. -*/ - -export * from "./atoms/LiveBadge"; -export * from "./molecules/VoiceBroadcastRecordingBody"; -export * from "./VoiceBroadcastBody"; diff --git a/src/voice-broadcast/index.ts b/src/voice-broadcast/index.ts index 2ceca2d3ab..2f69b84918 100644 --- a/src/voice-broadcast/index.ts +++ b/src/voice-broadcast/index.ts @@ -21,10 +21,14 @@ limitations under the License. import { RelationType } from "matrix-js-sdk/src/matrix"; -export * from "./components"; -export * from "./models"; -export * from "./utils"; -export * from "./stores"; +export * from "./audio/VoiceBroadcastRecorder"; +export * from "./components/VoiceBroadcastBody"; +export * from "./components/atoms/LiveBadge"; +export * from "./components/molecules/VoiceBroadcastRecordingBody"; +export * from "./models/VoiceBroadcastRecording"; +export * from "./stores/VoiceBroadcastRecordingsStore"; +export * from "./utils/shouldDisplayAsVoiceBroadcastTile"; +export * from "./utils/startNewVoiceBroadcastRecording"; export const VoiceBroadcastInfoEventType = "io.element.voice_broadcast_info"; diff --git a/src/voice-broadcast/models/VoiceBroadcastRecording.ts b/src/voice-broadcast/models/VoiceBroadcastRecording.ts index e949644dee..97351b2e1b 100644 --- a/src/voice-broadcast/models/VoiceBroadcastRecording.ts +++ b/src/voice-broadcast/models/VoiceBroadcastRecording.ts @@ -14,10 +14,22 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixClient, MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix"; +import { logger } from "matrix-js-sdk/src/logger"; +import { IAbortablePromise, MatrixClient, MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix"; import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter"; -import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from ".."; +import { + ChunkRecordedPayload, + createVoiceBroadcastRecorder, + VoiceBroadcastInfoEventType, + VoiceBroadcastInfoState, + VoiceBroadcastRecorder, + VoiceBroadcastRecorderEvent, +} from ".."; +import { uploadFile } from "../../ContentMessages"; +import { IEncryptedFile } from "../../customisations/models/IMediaEventContent"; +import { createVoiceMessageContent } from "../../utils/createVoiceMessageContent"; +import { IDestroyable } from "../../utils/IDestroyable"; export enum VoiceBroadcastRecordingEvent { StateChanged = "liveness_changed", @@ -27,8 +39,12 @@ interface EventMap { [VoiceBroadcastRecordingEvent.StateChanged]: (state: VoiceBroadcastInfoState) => void; } -export class VoiceBroadcastRecording extends TypedEventEmitter { - private _state: VoiceBroadcastInfoState; +export class VoiceBroadcastRecording + extends TypedEventEmitter + implements IDestroyable { + private state: VoiceBroadcastInfoState; + private recorder: VoiceBroadcastRecorder; + private sequence = 1; public constructor( public readonly infoEvent: MatrixEvent, @@ -43,21 +59,89 @@ export class VoiceBroadcastRecording extends TypedEventEmitter { + this.state = !relatedEvents?.find((event: MatrixEvent) => { return event.getContent()?.state === VoiceBroadcastInfoState.Stopped; }) ? VoiceBroadcastInfoState.Started : VoiceBroadcastInfoState.Stopped; // TODO Michael W: add listening for updates } + public async start(): Promise { + return this.getRecorder().start(); + } + + public async stop(): Promise { + this.setState(VoiceBroadcastInfoState.Stopped); + await this.stopRecorder(); + await this.sendStoppedStateEvent(); + } + + public getState(): VoiceBroadcastInfoState { + return this.state; + } + + private getRecorder(): VoiceBroadcastRecorder { + if (!this.recorder) { + this.recorder = createVoiceBroadcastRecorder(); + this.recorder.on(VoiceBroadcastRecorderEvent.ChunkRecorded, this.onChunkRecorded); + } + + return this.recorder; + } + + public destroy(): void { + if (this.recorder) { + this.recorder.off(VoiceBroadcastRecorderEvent.ChunkRecorded, this.onChunkRecorded); + this.recorder.stop(); + } + + this.removeAllListeners(); + } + private setState(state: VoiceBroadcastInfoState): void { - this._state = state; + this.state = state; this.emit(VoiceBroadcastRecordingEvent.StateChanged, this.state); } - public async stop() { - this.setState(VoiceBroadcastInfoState.Stopped); - // TODO Michael W: add error handling + private onChunkRecorded = async (chunk: ChunkRecordedPayload): Promise => { + const { url, file } = await this.uploadFile(chunk); + await this.sendVoiceMessage(chunk, url, file); + }; + + private uploadFile(chunk: ChunkRecordedPayload): IAbortablePromise<{ url?: string, file?: IEncryptedFile }> { + return uploadFile( + this.client, + this.infoEvent.getRoomId(), + new Blob( + [chunk.buffer], + { + type: this.getRecorder().contentType, + }, + ), + ); + } + + private async sendVoiceMessage(chunk: ChunkRecordedPayload, url: string, file: IEncryptedFile): Promise { + const content = createVoiceMessageContent( + url, + this.getRecorder().contentType, + Math.round(chunk.length * 1000), + chunk.buffer.length, + file, + ); + content["m.relates_to"] = { + rel_type: RelationType.Reference, + event_id: this.infoEvent.getId(), + }; + content["io.element.voice_broadcast_chunk"] = { + sequence: this.sequence++, + }; + + await this.client.sendMessage(this.infoEvent.getRoomId(), content); + } + + private async sendStoppedStateEvent(): Promise { + // TODO Michael W: add error handling for state event await this.client.sendStateEvent( this.infoEvent.getRoomId(), VoiceBroadcastInfoEventType, @@ -72,7 +156,18 @@ export class VoiceBroadcastRecording extends TypedEventEmitter { + if (!this.recorder) { + return; + } + + try { + const lastChunk = await this.recorder.stop(); + if (lastChunk) { + await this.onChunkRecorded(lastChunk); + } + } catch (err) { + logger.warn("error stopping voice broadcast recorder", err); + } } } diff --git a/src/voice-broadcast/models/index.ts b/src/voice-broadcast/models/index.ts deleted file mode 100644 index 053c032156..0000000000 --- a/src/voice-broadcast/models/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* -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. -*/ - -export * from "./VoiceBroadcastRecording"; diff --git a/src/voice-broadcast/stores/VoiceBroadcastRecordingsStore.ts b/src/voice-broadcast/stores/VoiceBroadcastRecordingsStore.ts index a8fb681873..380fd1d318 100644 --- a/src/voice-broadcast/stores/VoiceBroadcastRecordingsStore.ts +++ b/src/voice-broadcast/stores/VoiceBroadcastRecordingsStore.ts @@ -31,7 +31,7 @@ interface EventMap { * This store provides access to the current and specific Voice Broadcast recordings. */ export class VoiceBroadcastRecordingsStore extends TypedEventEmitter { - private _current: VoiceBroadcastRecording | null; + private current: VoiceBroadcastRecording | null; private recordings = new Map(); public constructor() { @@ -39,15 +39,15 @@ export class VoiceBroadcastRecordingsStore extends TypedEventEmitter { + describe("with default values", () => { + it("should return the default config", () => { + expect(SdkConfig.get()).toEqual(DEFAULTS); + }); + }); + + describe("with custom values", () => { + beforeEach(() => { + SdkConfig.put({ + voice_broadcast: { + chunk_length: 1337, + }, + }); + }); + + it("should return the custom config", () => { + const customConfig = JSON.parse(JSON.stringify(DEFAULTS)); + customConfig.voice_broadcast.chunk_length = 1337; + expect(SdkConfig.get()).toEqual(customConfig); + }); + }); +}); diff --git a/test/voice-broadcast/audio/VoiceBroadcastRecorder-test.ts b/test/voice-broadcast/audio/VoiceBroadcastRecorder-test.ts new file mode 100644 index 0000000000..e60d7e2d96 --- /dev/null +++ b/test/voice-broadcast/audio/VoiceBroadcastRecorder-test.ts @@ -0,0 +1,209 @@ +/* +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 { mocked } from "jest-mock"; + +import { VoiceRecording } from "../../../src/audio/VoiceRecording"; +import SdkConfig from "../../../src/SdkConfig"; +import { concat } from "../../../src/utils/arrays"; +import { + ChunkRecordedPayload, + createVoiceBroadcastRecorder, + VoiceBroadcastRecorder, + VoiceBroadcastRecorderEvent, +} from "../../../src/voice-broadcast"; + +describe("VoiceBroadcastRecorder", () => { + describe("createVoiceBroadcastRecorder", () => { + beforeEach(() => { + jest.spyOn(SdkConfig, "get").mockImplementation((key: string) => { + if (key === "voice_broadcast") { + return { + chunk_length: 1337, + }; + } + }); + }); + + afterEach(() => { + mocked(SdkConfig.get).mockRestore(); + }); + + it("should return a VoiceBroadcastRecorder instance with targetChunkLength from config", () => { + const voiceBroadcastRecorder = createVoiceBroadcastRecorder(); + expect(voiceBroadcastRecorder).toBeInstanceOf(VoiceBroadcastRecorder); + expect(voiceBroadcastRecorder.targetChunkLength).toBe(1337); + }); + }); + + describe("instance", () => { + const chunkLength = 30; + const headers1 = new Uint8Array([1, 2]); + const headers2 = new Uint8Array([3, 4]); + const chunk1 = new Uint8Array([5, 6]); + const chunk2a = new Uint8Array([7, 8]); + const chunk2b = new Uint8Array([9, 10]); + const contentType = "test content type"; + + let voiceRecording: VoiceRecording; + let voiceBroadcastRecorder: VoiceBroadcastRecorder; + let onChunkRecorded: (chunk: ChunkRecordedPayload) => void; + + const itShouldNotEmitAChunkRecordedEvent = () => { + it("should not emit a ChunkRecorded event", () => { + expect(voiceRecording.emit).not.toHaveBeenCalledWith( + VoiceBroadcastRecorderEvent.ChunkRecorded, + expect.anything(), + ); + }); + }; + + beforeEach(() => { + voiceRecording = { + contentType, + start: jest.fn().mockResolvedValue(undefined), + stop: jest.fn().mockResolvedValue(undefined), + on: jest.fn(), + off: jest.fn(), + emit: jest.fn(), + destroy: jest.fn(), + recorderSeconds: 23, + } as unknown as VoiceRecording; + voiceBroadcastRecorder = new VoiceBroadcastRecorder(voiceRecording, chunkLength); + jest.spyOn(voiceBroadcastRecorder, "removeAllListeners"); + onChunkRecorded = jest.fn(); + voiceBroadcastRecorder.on(VoiceBroadcastRecorderEvent.ChunkRecorded, onChunkRecorded); + }); + + afterEach(() => { + voiceBroadcastRecorder.destroy(); + }); + + it("start should forward the call to VoiceRecording.start", async () => { + await voiceBroadcastRecorder.start(); + expect(voiceRecording.start).toHaveBeenCalled(); + }); + + describe("stop", () => { + beforeEach(async () => { + await voiceBroadcastRecorder.stop(); + }); + + it("should forward the call to VoiceRecording.stop", async () => { + expect(voiceRecording.stop).toHaveBeenCalled(); + }); + + itShouldNotEmitAChunkRecordedEvent(); + }); + + describe("when calling destroy", () => { + beforeEach(() => { + voiceBroadcastRecorder.destroy(); + }); + + it("should call VoiceRecording.destroy", () => { + expect(voiceRecording.destroy).toHaveBeenCalled(); + }); + + it("should remove all listeners", () => { + expect(voiceBroadcastRecorder.removeAllListeners).toHaveBeenCalled(); + }); + }); + + it("contentType should return the value from VoiceRecording", () => { + expect(voiceBroadcastRecorder.contentType).toBe(contentType); + }); + + describe("when the first page from recorder has been received", () => { + beforeEach(() => { + voiceRecording.onDataAvailable(headers1); + }); + + itShouldNotEmitAChunkRecordedEvent(); + }); + + describe("when a second page from recorder has been received", () => { + beforeEach(() => { + voiceRecording.onDataAvailable(headers1); + voiceRecording.onDataAvailable(headers2); + }); + + itShouldNotEmitAChunkRecordedEvent(); + }); + + describe("when a third page from recorder has been received", () => { + beforeEach(() => { + voiceRecording.onDataAvailable(headers1); + voiceRecording.onDataAvailable(headers2); + voiceRecording.onDataAvailable(chunk1); + }); + + itShouldNotEmitAChunkRecordedEvent(); + + describe("stop", () => { + let stopPayload: ChunkRecordedPayload; + + beforeEach(async () => { + stopPayload = await voiceBroadcastRecorder.stop(); + }); + + it("should return the remaining chunk", () => { + expect(stopPayload).toEqual({ + buffer: concat(headers1, headers2, chunk1), + length: 23, + }); + }); + }); + }); + + describe("when some chunks have been received", () => { + beforeEach(() => { + // simulate first chunk + voiceRecording.onDataAvailable(headers1); + voiceRecording.onDataAvailable(headers2); + // set recorder seconds to something greater than the test chunk length of 30 + // @ts-ignore + voiceRecording.recorderSeconds = 42; + voiceRecording.onDataAvailable(chunk1); + + // simulate a second chunk + voiceRecording.onDataAvailable(chunk2a); + // add another 30 seconds for the next chunk + // @ts-ignore + voiceRecording.recorderSeconds = 72; + voiceRecording.onDataAvailable(chunk2b); + }); + + it("should emit ChunkRecorded events", () => { + expect(onChunkRecorded).toHaveBeenNthCalledWith( + 1, + { + buffer: concat(headers1, headers2, chunk1), + length: 42, + }, + ); + + expect(onChunkRecorded).toHaveBeenNthCalledWith( + 2, + { + buffer: concat(headers1, headers2, chunk2a, chunk2b), + length: 72 - 42, // 72 (position at second chunk) - 42 (position of first chunk) + }, + ); + }); + }); + }); +}); diff --git a/test/voice-broadcast/components/VoiceBroadcastBody-test.tsx b/test/voice-broadcast/components/VoiceBroadcastBody-test.tsx index fed396a683..2c94ea6d0b 100644 --- a/test/voice-broadcast/components/VoiceBroadcastBody-test.tsx +++ b/test/voice-broadcast/components/VoiceBroadcastBody-test.tsx @@ -155,7 +155,7 @@ describe("VoiceBroadcastBody", () => { itShouldRenderANonLiveVoiceBroadcast(); it("should call stop on the recording", () => { - expect(recording.state).toBe(VoiceBroadcastInfoState.Stopped); + expect(recording.getState()).toBe(VoiceBroadcastInfoState.Stopped); expect(onRecordingStateChanged).toHaveBeenCalledWith(VoiceBroadcastInfoState.Stopped); }); }); diff --git a/test/voice-broadcast/models/VoiceBroadcastRecording-test.ts b/test/voice-broadcast/models/VoiceBroadcastRecording-test.ts index b0e9939164..357180c700 100644 --- a/test/voice-broadcast/models/VoiceBroadcastRecording-test.ts +++ b/test/voice-broadcast/models/VoiceBroadcastRecording-test.ts @@ -15,25 +15,57 @@ limitations under the License. */ import { mocked } from "jest-mock"; -import { EventTimelineSet, EventType, MatrixClient, MatrixEvent, RelationType, Room } from "matrix-js-sdk/src/matrix"; +import { + EventTimelineSet, + EventType, + MatrixClient, + MatrixEvent, + MsgType, + RelationType, + Room, +} from "matrix-js-sdk/src/matrix"; import { Relations } from "matrix-js-sdk/src/models/relations"; +import { uploadFile } from "../../../src/ContentMessages"; +import { IEncryptedFile } from "../../../src/customisations/models/IMediaEventContent"; +import { createVoiceMessageContent } from "../../../src/utils/createVoiceMessageContent"; import { + ChunkRecordedPayload, + createVoiceBroadcastRecorder, VoiceBroadcastInfoEventContent, VoiceBroadcastInfoEventType, VoiceBroadcastInfoState, + VoiceBroadcastRecorder, + VoiceBroadcastRecorderEvent, VoiceBroadcastRecording, VoiceBroadcastRecordingEvent, } from "../../../src/voice-broadcast"; import { mkEvent, mkStubRoom, stubClient } from "../../test-utils"; +jest.mock("../../../src/voice-broadcast/audio/VoiceBroadcastRecorder", () => ({ + ...jest.requireActual("../../../src/voice-broadcast/audio/VoiceBroadcastRecorder") as object, + createVoiceBroadcastRecorder: jest.fn(), +})); + +jest.mock("../../../src/ContentMessages", () => ({ + uploadFile: jest.fn(), +})); + +jest.mock("../../../src/utils/createVoiceMessageContent", () => ({ + createVoiceMessageContent: jest.fn(), +})); + describe("VoiceBroadcastRecording", () => { const roomId = "!room:example.com"; + const uploadedUrl = "mxc://example.com/vb"; + const uploadedFile = { file: true } as unknown as IEncryptedFile; let room: Room; let client: MatrixClient; let infoEvent: MatrixEvent; let voiceBroadcastRecording: VoiceBroadcastRecording; let onStateChanged: (state: VoiceBroadcastInfoState) => void; + let voiceBroadcastRecorder: VoiceBroadcastRecorder; + let onChunkRecorded: (chunk: ChunkRecordedPayload) => Promise; const mkVoiceBroadcastInfoEvent = (content: VoiceBroadcastInfoEventContent) => { return mkEvent({ @@ -48,6 +80,7 @@ describe("VoiceBroadcastRecording", () => { const setUpVoiceBroadcastRecording = () => { voiceBroadcastRecording = new VoiceBroadcastRecording(infoEvent, client); voiceBroadcastRecording.on(VoiceBroadcastRecordingEvent.StateChanged, onStateChanged); + jest.spyOn(voiceBroadcastRecording, "removeAllListeners"); }; beforeEach(() => { @@ -59,6 +92,65 @@ describe("VoiceBroadcastRecording", () => { } }); onStateChanged = jest.fn(); + voiceBroadcastRecorder = { + contentType: "audio/ogg", + on: jest.fn(), + off: jest.fn(), + start: jest.fn(), + stop: jest.fn(), + } as unknown as VoiceBroadcastRecorder; + mocked(createVoiceBroadcastRecorder).mockReturnValue(voiceBroadcastRecorder); + onChunkRecorded = jest.fn(); + + mocked(voiceBroadcastRecorder.on).mockImplementation( + (event: VoiceBroadcastRecorderEvent, listener: any): VoiceBroadcastRecorder => { + if (event === VoiceBroadcastRecorderEvent.ChunkRecorded) { + onChunkRecorded = listener; + } + + return voiceBroadcastRecorder; + }, + ); + + mocked(uploadFile).mockResolvedValue({ + url: uploadedUrl, + file: uploadedFile, + }); + + mocked(createVoiceMessageContent).mockImplementation(( + mxc: string, + mimetype: string, + duration: number, + size: number, + file?: IEncryptedFile, + waveform?: number[], + ) => { + return { + body: "Voice message", + msgtype: MsgType.Audio, + url: mxc, + file, + info: { + duration, + mimetype, + size, + }, + ["org.matrix.msc1767.text"]: "Voice message", + ["org.matrix.msc1767.file"]: { + url: mxc, + file, + name: "Voice message.ogg", + mimetype, + size, + }, + ["org.matrix.msc1767.audio"]: { + duration, + // https://github.com/matrix-org/matrix-doc/pull/3246 + waveform, + }, + ["org.matrix.msc3245.voice"]: {}, // No content, this is a rendering hint + }; + }); }); afterEach(() => { @@ -74,7 +166,7 @@ describe("VoiceBroadcastRecording", () => { }); it("should be in Started state", () => { - expect(voiceBroadcastRecording.state).toBe(VoiceBroadcastInfoState.Started); + expect(voiceBroadcastRecording.getState()).toBe(VoiceBroadcastInfoState.Started); }); describe("and calling stop()", () => { @@ -98,13 +190,155 @@ describe("VoiceBroadcastRecording", () => { }); it("should be in state stopped", () => { - expect(voiceBroadcastRecording.state).toBe(VoiceBroadcastInfoState.Stopped); + expect(voiceBroadcastRecording.getState()).toBe(VoiceBroadcastInfoState.Stopped); }); it("should emit a stopped state changed event", () => { expect(onStateChanged).toHaveBeenCalledWith(VoiceBroadcastInfoState.Stopped); }); }); + + describe("and calling start", () => { + beforeEach(async () => { + await voiceBroadcastRecording.start(); + }); + + it("should start the recorder", () => { + expect(voiceBroadcastRecorder.start).toHaveBeenCalled(); + }); + + describe("and a chunk has been recorded", () => { + beforeEach(async () => { + await onChunkRecorded({ + buffer: new Uint8Array([1, 2, 3]), + length: 23, + }); + }); + + it("should send a voice message", () => { + expect(uploadFile).toHaveBeenCalledWith( + client, + roomId, + new Blob([new Uint8Array([1, 2, 3])], { type: voiceBroadcastRecorder.contentType }), + ); + + expect(mocked(client.sendMessage)).toHaveBeenCalledWith( + roomId, + { + body: "Voice message", + file: { + file: true, + }, + info: { + duration: 23000, + mimetype: "audio/ogg", + size: 3, + }, + ["m.relates_to"]: { + event_id: infoEvent.getId(), + rel_type: "m.reference", + }, + msgtype: "m.audio", + ["org.matrix.msc1767.audio"]: { + duration: 23000, + waveform: undefined, + }, + ["org.matrix.msc1767.file"]: { + file: { + file: true, + }, + mimetype: "audio/ogg", + name: "Voice message.ogg", + size: 3, + url: "mxc://example.com/vb", + }, + ["org.matrix.msc1767.text"]: "Voice message", + ["org.matrix.msc3245.voice"]: {}, + url: "mxc://example.com/vb", + ["io.element.voice_broadcast_chunk"]: { + sequence: 1, + }, + }, + ); + }); + }); + + describe("and calling stop", () => { + beforeEach(async () => { + await onChunkRecorded({ + buffer: new Uint8Array([1, 2, 3]), + length: 23, + }); + mocked(voiceBroadcastRecorder.stop).mockResolvedValue({ + buffer: new Uint8Array([4, 5, 6]), + length: 42, + }); + await voiceBroadcastRecording.stop(); + }); + + it("should send the last chunk", () => { + expect(uploadFile).toHaveBeenCalledWith( + client, + roomId, + new Blob([new Uint8Array([4, 5, 6])], { type: voiceBroadcastRecorder.contentType }), + ); + + expect(mocked(client.sendMessage)).toHaveBeenCalledWith( + roomId, + { + body: "Voice message", + file: { + file: true, + }, + info: { + duration: 42000, + mimetype: "audio/ogg", + size: 3, + }, + ["m.relates_to"]: { + event_id: infoEvent.getId(), + rel_type: "m.reference", + }, + msgtype: "m.audio", + ["org.matrix.msc1767.audio"]: { + duration: 42000, + waveform: undefined, + }, + ["org.matrix.msc1767.file"]: { + file: { + file: true, + }, + mimetype: "audio/ogg", + name: "Voice message.ogg", + size: 3, + url: "mxc://example.com/vb", + }, + ["org.matrix.msc1767.text"]: "Voice message", + ["org.matrix.msc3245.voice"]: {}, + url: "mxc://example.com/vb", + ["io.element.voice_broadcast_chunk"]: { + sequence: 2, + }, + }, + ); + }); + }); + + describe("and calling destroy", () => { + beforeEach(() => { + voiceBroadcastRecording.destroy(); + }); + + it("should stop the recorder and remove all listeners", () => { + expect(mocked(voiceBroadcastRecorder.stop)).toHaveBeenCalled(); + expect(mocked(voiceBroadcastRecorder.off)).toHaveBeenCalledWith( + VoiceBroadcastRecorderEvent.ChunkRecorded, + onChunkRecorded, + ); + expect(mocked(voiceBroadcastRecording.removeAllListeners)).toHaveBeenCalled(); + }); + }); + }); }); describe("when created for a Voice Broadcast Info with a Stopped relation", () => { @@ -152,7 +386,7 @@ describe("VoiceBroadcastRecording", () => { }); it("should be in Stopped state", () => { - expect(voiceBroadcastRecording.state).toBe(VoiceBroadcastInfoState.Stopped); + expect(voiceBroadcastRecording.getState()).toBe(VoiceBroadcastInfoState.Stopped); }); }); }); diff --git a/test/voice-broadcast/stores/VoiceBroadcastRecordingsStore-test.ts b/test/voice-broadcast/stores/VoiceBroadcastRecordingsStore-test.ts index 9a3fa1ca96..a6a5c4ab23 100644 --- a/test/voice-broadcast/stores/VoiceBroadcastRecordingsStore-test.ts +++ b/test/voice-broadcast/stores/VoiceBroadcastRecordingsStore-test.ts @@ -76,7 +76,7 @@ describe("VoiceBroadcastRecordingsStore", () => { }); it("should return it as current", () => { - expect(recordings.current).toBe(recording); + expect(recordings.getCurrent()).toBe(recording); }); it("should return it by id", () => { diff --git a/test/voice-broadcast/utils/startNewVoiceBroadcastRecording-test.ts b/test/voice-broadcast/utils/startNewVoiceBroadcastRecording-test.ts index f1f630abc7..0096817b3f 100644 --- a/test/voice-broadcast/utils/startNewVoiceBroadcastRecording-test.ts +++ b/test/voice-broadcast/utils/startNewVoiceBroadcastRecording-test.ts @@ -109,6 +109,7 @@ describe("startNewVoiceBroadcastRecording", () => { return { infoEvent, client, + start: jest.fn(), } as unknown as VoiceBroadcastRecording; }); }); @@ -120,6 +121,7 @@ describe("startNewVoiceBroadcastRecording", () => { expect(ok).toBe(true); expect(mocked(room.off)).toHaveBeenCalledWith(RoomStateEvent.Events, roomOnStateEventsCallback); expect(recording.infoEvent).toBe(infoEvent); + expect(recording.start).toHaveBeenCalled(); done(); }); From 19bc3f1d9aaaace7df7c13772511b1453f612a6c Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Wed, 12 Oct 2022 13:24:30 +0200 Subject: [PATCH 19/33] Include device_id in voice broadcast info events (#9394) --- src/voice-broadcast/index.ts | 1 + src/voice-broadcast/models/VoiceBroadcastRecording.ts | 1 + src/voice-broadcast/utils/startNewVoiceBroadcastRecording.ts | 1 + test/voice-broadcast/models/VoiceBroadcastRecording-test.ts | 4 ++++ .../utils/startNewVoiceBroadcastRecording-test.ts | 1 + 5 files changed, 8 insertions(+) diff --git a/src/voice-broadcast/index.ts b/src/voice-broadcast/index.ts index 2f69b84918..2765cf4f25 100644 --- a/src/voice-broadcast/index.ts +++ b/src/voice-broadcast/index.ts @@ -40,6 +40,7 @@ export enum VoiceBroadcastInfoState { } export interface VoiceBroadcastInfoEventContent { + device_id: string; state: VoiceBroadcastInfoState; chunk_length?: number; ["m.relates_to"]?: { diff --git a/src/voice-broadcast/models/VoiceBroadcastRecording.ts b/src/voice-broadcast/models/VoiceBroadcastRecording.ts index 97351b2e1b..bea71cc274 100644 --- a/src/voice-broadcast/models/VoiceBroadcastRecording.ts +++ b/src/voice-broadcast/models/VoiceBroadcastRecording.ts @@ -146,6 +146,7 @@ export class VoiceBroadcastRecording this.infoEvent.getRoomId(), VoiceBroadcastInfoEventType, { + device_id: this.client.getDeviceId(), state: VoiceBroadcastInfoState.Stopped, ["m.relates_to"]: { rel_type: RelationType.Reference, diff --git a/src/voice-broadcast/utils/startNewVoiceBroadcastRecording.ts b/src/voice-broadcast/utils/startNewVoiceBroadcastRecording.ts index be88963e8c..272958e5d0 100644 --- a/src/voice-broadcast/utils/startNewVoiceBroadcastRecording.ts +++ b/src/voice-broadcast/utils/startNewVoiceBroadcastRecording.ts @@ -65,6 +65,7 @@ export const startNewVoiceBroadcastRecording = async ( roomId, VoiceBroadcastInfoEventType, { + device_id: client.getDeviceId(), state: VoiceBroadcastInfoState.Started, chunk_length: 300, } as VoiceBroadcastInfoEventContent, diff --git a/test/voice-broadcast/models/VoiceBroadcastRecording-test.ts b/test/voice-broadcast/models/VoiceBroadcastRecording-test.ts index 357180c700..5fca34e035 100644 --- a/test/voice-broadcast/models/VoiceBroadcastRecording-test.ts +++ b/test/voice-broadcast/models/VoiceBroadcastRecording-test.ts @@ -160,6 +160,7 @@ describe("VoiceBroadcastRecording", () => { describe("when created for a Voice Broadcast Info without relations", () => { beforeEach(() => { infoEvent = mkVoiceBroadcastInfoEvent({ + device_id: client.getDeviceId(), state: VoiceBroadcastInfoState.Started, }); setUpVoiceBroadcastRecording(); @@ -179,6 +180,7 @@ describe("VoiceBroadcastRecording", () => { roomId, VoiceBroadcastInfoEventType, { + device_id: client.getDeviceId(), state: VoiceBroadcastInfoState.Stopped, ["m.relates_to"]: { rel_type: RelationType.Reference, @@ -344,6 +346,7 @@ describe("VoiceBroadcastRecording", () => { describe("when created for a Voice Broadcast Info with a Stopped relation", () => { beforeEach(() => { infoEvent = mkVoiceBroadcastInfoEvent({ + device_id: client.getDeviceId(), state: VoiceBroadcastInfoState.Started, chunk_length: 300, }); @@ -353,6 +356,7 @@ describe("VoiceBroadcastRecording", () => { } as unknown as Relations; mocked(relationsContainer.getRelations).mockReturnValue([ mkVoiceBroadcastInfoEvent({ + device_id: client.getDeviceId(), state: VoiceBroadcastInfoState.Stopped, ["m.relates_to"]: { rel_type: RelationType.Reference, diff --git a/test/voice-broadcast/utils/startNewVoiceBroadcastRecording-test.ts b/test/voice-broadcast/utils/startNewVoiceBroadcastRecording-test.ts index 0096817b3f..570719539a 100644 --- a/test/voice-broadcast/utils/startNewVoiceBroadcastRecording-test.ts +++ b/test/voice-broadcast/utils/startNewVoiceBroadcastRecording-test.ts @@ -89,6 +89,7 @@ describe("startNewVoiceBroadcastRecording", () => { event: true, type: VoiceBroadcastInfoEventType, content: { + device_id: client.getDeviceId(), state: VoiceBroadcastInfoState.Started, }, user: client.getUserId(), From 533eda2273fad4ca280d3f38639c2ce358d3d3fe Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Wed, 12 Oct 2022 16:00:37 +0200 Subject: [PATCH 20/33] Extract voice broadcast header (#9393) --- res/css/_components.pcss | 1 + res/css/components/atoms/_Icon.pcss | 4 + .../atoms/_VoiceBroadcastHeader.pcss | 44 +++++++ src/components/atoms/Icon.tsx | 1 + src/i18n/strings/en_EN.json | 2 +- .../components/VoiceBroadcastBody.tsx | 7 +- .../components/atoms/VoiceBroadcastHeader.tsx | 55 +++++++++ .../molecules/VoiceBroadcastRecordingBody.tsx | 29 ++--- src/voice-broadcast/index.ts | 1 + .../components/VoiceBroadcastBody-test.tsx | 18 ++- .../atoms/VoiceBroadcastHeader-test.tsx | 60 ++++++++++ .../VoiceBroadcastHeader-test.tsx.snap | 109 ++++++++++++++++++ .../VoiceBroadcastRecordingBody-test.tsx | 40 +++---- .../VoiceBroadcastRecordingBody-test.tsx.snap | 26 +---- 14 files changed, 318 insertions(+), 79 deletions(-) create mode 100644 res/css/voice-broadcast/atoms/_VoiceBroadcastHeader.pcss create mode 100644 src/voice-broadcast/components/atoms/VoiceBroadcastHeader.tsx create mode 100644 test/voice-broadcast/components/atoms/VoiceBroadcastHeader-test.tsx create mode 100644 test/voice-broadcast/components/atoms/__snapshots__/VoiceBroadcastHeader-test.tsx.snap diff --git a/res/css/_components.pcss b/res/css/_components.pcss index efb3e9e594..faaf208948 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -364,4 +364,5 @@ @import "./views/voip/_PiPContainer.pcss"; @import "./views/voip/_VideoFeed.pcss"; @import "./voice-broadcast/atoms/_LiveBadge.pcss"; +@import "./voice-broadcast/atoms/_VoiceBroadcastHeader.pcss"; @import "./voice-broadcast/molecules/_VoiceBroadcastRecordingBody.pcss"; diff --git a/res/css/components/atoms/_Icon.pcss b/res/css/components/atoms/_Icon.pcss index 08a72d3d5b..1db3278fe9 100644 --- a/res/css/components/atoms/_Icon.pcss +++ b/res/css/components/atoms/_Icon.pcss @@ -32,3 +32,7 @@ limitations under the License. .mx_Icon_live-badge { background-color: #fff; } + +.mx_Icon_compound-secondary-content { + background-color: $secondary-content; +} diff --git a/res/css/voice-broadcast/atoms/_VoiceBroadcastHeader.pcss b/res/css/voice-broadcast/atoms/_VoiceBroadcastHeader.pcss new file mode 100644 index 0000000000..dc1522811c --- /dev/null +++ b/res/css/voice-broadcast/atoms/_VoiceBroadcastHeader.pcss @@ -0,0 +1,44 @@ +/* +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_VoiceBroadcastHeader { + align-items: flex-start; + display: flex; + gap: $spacing-8; + line-height: 20px; + margin-bottom: $spacing-8; + width: 266px; +} + +.mx_VoiceBroadcastHeader_sender { + font-size: $font-12px; + font-weight: $font-semi-bold; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.mx_VoiceBroadcastHeader_room { + font-size: $font-12px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.mx_VoiceBroadcastHeader_line { + align-items: center; + color: $secondary-content; + font-size: $font-12px; + display: flex; + gap: $spacing-4; +} diff --git a/src/components/atoms/Icon.tsx b/src/components/atoms/Icon.tsx index bb6ea61524..6ec0194cc9 100644 --- a/src/components/atoms/Icon.tsx +++ b/src/components/atoms/Icon.tsx @@ -29,6 +29,7 @@ const iconTypeMap = new Map([ export enum IconColour { Accent = "accent", LiveBadge = "live-badge", + CompoundSecondaryContent = "compound-secondary-content", } export enum IconSize { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5bd66ec9a3..4b759ca21e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -637,6 +637,7 @@ "See %(msgtype)s messages posted to this room": "See %(msgtype)s messages posted to this room", "See %(msgtype)s messages posted to your active room": "See %(msgtype)s messages posted to your active room", "Live": "Live", + "Voice broadcast": "Voice broadcast", "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", @@ -1850,7 +1851,6 @@ "Emoji": "Emoji", "Hide stickers": "Hide stickers", "Sticker": "Sticker", - "Voice broadcast": "Voice broadcast", "Voice Message": "Voice Message", "You do not have permission to start polls in this room.": "You do not have permission to start polls in this room.", "Poll": "Poll", diff --git a/src/voice-broadcast/components/VoiceBroadcastBody.tsx b/src/voice-broadcast/components/VoiceBroadcastBody.tsx index 3591325585..e36460b9f3 100644 --- a/src/voice-broadcast/components/VoiceBroadcastBody.tsx +++ b/src/voice-broadcast/components/VoiceBroadcastBody.tsx @@ -46,13 +46,10 @@ export const VoiceBroadcastBody: React.FC = ({ mxEvent }) => { recording.stop(); }; - const senderId = mxEvent.getSender(); - const sender = mxEvent.sender; return ; }; diff --git a/src/voice-broadcast/components/atoms/VoiceBroadcastHeader.tsx b/src/voice-broadcast/components/atoms/VoiceBroadcastHeader.tsx new file mode 100644 index 0000000000..d7175db30b --- /dev/null +++ b/src/voice-broadcast/components/atoms/VoiceBroadcastHeader.tsx @@ -0,0 +1,55 @@ +/* +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 { RoomMember } from "matrix-js-sdk/src/matrix"; + +import MemberAvatar from "../../../components/views/avatars/MemberAvatar"; +import { LiveBadge } from "../.."; +import { Icon, IconColour, IconType } from "../../../components/atoms/Icon"; +import { _t } from "../../../languageHandler"; + +interface VoiceBroadcastHeaderProps { + live: boolean; + sender: RoomMember; + roomName: string; + showBroadcast?: boolean; +} + +export const VoiceBroadcastHeader: React.FC = ({ + live, + sender, + roomName, + showBroadcast = false, +}) => { + const broadcast = showBroadcast + ?
+ + { _t("Voice broadcast") } +
+ : null; + const liveBadge = live ? : null; + return
+ +
+
+ { sender.name } +
+
+ { roomName } +
+ { broadcast } +
+ { liveBadge } +
; +}; diff --git a/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody.tsx b/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody.tsx index 13ea504ac4..0db9bb92e1 100644 --- a/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody.tsx +++ b/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody.tsx @@ -17,40 +17,31 @@ limitations under the License. import React, { MouseEventHandler } from "react"; import { RoomMember } from "matrix-js-sdk/src/matrix"; -import { LiveBadge } from "../.."; -import MemberAvatar from "../../../components/views/avatars/MemberAvatar"; +import { VoiceBroadcastHeader } from "../.."; interface VoiceBroadcastRecordingBodyProps { live: boolean; - member: RoomMember; onClick: MouseEventHandler; - title: string; - userId: string; + roomName: string; + sender: RoomMember; } export const VoiceBroadcastRecordingBody: React.FC = ({ live, - member, onClick, - title, - userId, + roomName, + sender, }) => { - const liveBadge = live - ? - : null; - return (
- -
-
- { title } -
-
- { liveBadge } +
); }; diff --git a/src/voice-broadcast/index.ts b/src/voice-broadcast/index.ts index 2765cf4f25..81f47a29ba 100644 --- a/src/voice-broadcast/index.ts +++ b/src/voice-broadcast/index.ts @@ -24,6 +24,7 @@ import { RelationType } from "matrix-js-sdk/src/matrix"; export * from "./audio/VoiceBroadcastRecorder"; export * from "./components/VoiceBroadcastBody"; export * from "./components/atoms/LiveBadge"; +export * from "./components/atoms/VoiceBroadcastHeader"; export * from "./components/molecules/VoiceBroadcastRecordingBody"; export * from "./models/VoiceBroadcastRecording"; export * from "./stores/VoiceBroadcastRecordingsStore"; diff --git a/test/voice-broadcast/components/VoiceBroadcastBody-test.tsx b/test/voice-broadcast/components/VoiceBroadcastBody-test.tsx index 2c94ea6d0b..d6f9da6a68 100644 --- a/test/voice-broadcast/components/VoiceBroadcastBody-test.tsx +++ b/test/voice-broadcast/components/VoiceBroadcastBody-test.tsx @@ -72,9 +72,8 @@ describe("VoiceBroadcastBody", () => { { onClick: expect.any(Function), live: true, - member: infoEvent.sender, - userId: client.getUserId(), - title: "@userId:matrix.org • test room", + sender: infoEvent.sender, + roomName: room.name, }, {}, ); @@ -89,9 +88,8 @@ describe("VoiceBroadcastBody", () => { { onClick: expect.any(Function), live: false, - member: infoEvent.sender, - userId: client.getUserId(), - title: "@userId:matrix.org • test room", + sender: infoEvent.sender, + roomName: room.name, }, {}, ); @@ -105,17 +103,17 @@ describe("VoiceBroadcastBody", () => { mocked(VoiceBroadcastRecordingBody).mockImplementation( ({ live, - member: _member, + sender, onClick, - title, - userId: _userId, + roomName, }) => { return (
-
{ title }
+
{ sender.name }
+
{ roomName }
{ live && "Live" }
); diff --git a/test/voice-broadcast/components/atoms/VoiceBroadcastHeader-test.tsx b/test/voice-broadcast/components/atoms/VoiceBroadcastHeader-test.tsx new file mode 100644 index 0000000000..6b07b213f7 --- /dev/null +++ b/test/voice-broadcast/components/atoms/VoiceBroadcastHeader-test.tsx @@ -0,0 +1,60 @@ +/* +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 { Container } from "react-dom"; +import { RoomMember } from "matrix-js-sdk/src/matrix"; +import { render, RenderResult } from "@testing-library/react"; + +import { VoiceBroadcastHeader } from "../../../../src/voice-broadcast"; + +describe("VoiceBroadcastHeader", () => { + const userId = "@user:example.com"; + const roomId = "!room:example.com"; + const roomName = "test room"; + const sender = new RoomMember(roomId, userId); + let container: Container; + + const renderHeader = (live: boolean, showBroadcast: boolean = undefined): RenderResult => { + return render(); + }; + + beforeAll(() => { + sender.name = "test user"; + }); + + describe("when rendering a live broadcast header with broadcast info", () => { + beforeEach(() => { + container = renderHeader(true, true).container; + }); + + it("should render the header with a live badge", () => { + expect(container).toMatchSnapshot(); + }); + }); + + describe("when rendering a non-live broadcast header", () => { + beforeEach(() => { + container = renderHeader(false).container; + }); + + it("should render the header without a live badge", () => { + expect(container).toMatchSnapshot(); + }); + }); +}); diff --git a/test/voice-broadcast/components/atoms/__snapshots__/VoiceBroadcastHeader-test.tsx.snap b/test/voice-broadcast/components/atoms/__snapshots__/VoiceBroadcastHeader-test.tsx.snap new file mode 100644 index 0000000000..3e3bd2b1d2 --- /dev/null +++ b/test/voice-broadcast/components/atoms/__snapshots__/VoiceBroadcastHeader-test.tsx.snap @@ -0,0 +1,109 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`VoiceBroadcastHeader when rendering a live broadcast header with broadcast info should render the header with a live badge 1`] = ` +
+
+ + + + +
+
+ test user +
+
+ test room +
+
+