From 2c7247713a846258210f8904937cd78ce6d682a0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 22 Oct 2024 14:58:20 +0100 Subject: [PATCH 01/71] Enable key backup by default When we set up cross signing, so the key backup key will be stored locally along with the cross signing keys until the user sets up recovery (4s). This will mean that a user can restore their backup if they log in on a new device as long as they verify with the one they registered on. --- src/components/structures/auth/E2eSetup.tsx | 4 ++-- ...gningDialog.tsx => InitialCryptoSetup.tsx} | 24 ++++++++++++------- 2 files changed, 17 insertions(+), 11 deletions(-) rename src/components/views/dialogs/security/{CreateCrossSigningDialog.tsx => InitialCryptoSetup.tsx} (79%) diff --git a/src/components/structures/auth/E2eSetup.tsx b/src/components/structures/auth/E2eSetup.tsx index 80a135fe19..d2fb046fe7 100644 --- a/src/components/structures/auth/E2eSetup.tsx +++ b/src/components/structures/auth/E2eSetup.tsx @@ -11,7 +11,7 @@ import { MatrixClient } from "matrix-js-sdk/src/matrix"; import AuthPage from "../../views/auth/AuthPage"; import CompleteSecurityBody from "../../views/auth/CompleteSecurityBody"; -import CreateCrossSigningDialog from "../../views/dialogs/security/CreateCrossSigningDialog"; +import InitialCryptoSetup from "../../views/dialogs/security/InitialCryptoSetup"; interface IProps { matrixClient: MatrixClient; @@ -25,7 +25,7 @@ export default class E2eSetup extends React.Component { return ( - = ({ matrixClient, accountPassword, tokenLogin, onFinished }) => { +const InitialCryptoSetup: React.FC = ({ matrixClient, accountPassword, tokenLogin, onFinished }) => { const [error, setError] = useState(false); - const bootstrapCrossSigning = useCallback(async () => { + const doSetup = useCallback(async () => { const cryptoApi = matrixClient.getCrypto(); if (!cryptoApi) return; @@ -40,6 +40,12 @@ const CreateCrossSigningDialog: React.FC = ({ matrixClient, accountPasswo try { await createCrossSigning(matrixClient, tokenLogin, accountPassword); + + const backupInfo = await matrixClient.getKeyBackupVersion(); + if (backupInfo === null) { + await cryptoApi.resetKeyBackup(); + } + onFinished(true); } catch (e) { if (tokenLogin) { @@ -58,8 +64,8 @@ const CreateCrossSigningDialog: React.FC = ({ matrixClient, accountPasswo }, [onFinished]); useEffect(() => { - bootstrapCrossSigning(); - }, [bootstrapCrossSigning]); + doSetup(); + }, [doSetup]); let content; if (error) { @@ -69,7 +75,7 @@ const CreateCrossSigningDialog: React.FC = ({ matrixClient, accountPasswo
@@ -96,4 +102,4 @@ const CreateCrossSigningDialog: React.FC = ({ matrixClient, accountPasswo ); }; -export default CreateCrossSigningDialog; +export default InitialCryptoSetup; From 7f10cd6a9e9ca71f371565496c1146ac4cdf560f Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 22 Oct 2024 15:36:04 +0100 Subject: [PATCH 02/71] Fix name & tests --- src/components/structures/auth/E2eSetup.tsx | 4 ++-- ...ryptoSetup.tsx => InitialCryptoSetupDialog.tsx} | 4 ++-- ...-test.tsx => InitialCryptoSetupDialog-test.tsx} | 14 +++++++------- 3 files changed, 11 insertions(+), 11 deletions(-) rename src/components/views/dialogs/security/{InitialCryptoSetup.tsx => InitialCryptoSetupDialog.tsx} (95%) rename test/components/views/dialogs/security/{CreateCrossSigningDialog-test.tsx => InitialCryptoSetupDialog-test.tsx} (91%) diff --git a/src/components/structures/auth/E2eSetup.tsx b/src/components/structures/auth/E2eSetup.tsx index d2fb046fe7..4ba7be6e5a 100644 --- a/src/components/structures/auth/E2eSetup.tsx +++ b/src/components/structures/auth/E2eSetup.tsx @@ -11,7 +11,7 @@ import { MatrixClient } from "matrix-js-sdk/src/matrix"; import AuthPage from "../../views/auth/AuthPage"; import CompleteSecurityBody from "../../views/auth/CompleteSecurityBody"; -import InitialCryptoSetup from "../../views/dialogs/security/InitialCryptoSetup"; +import InitialCryptoSetupDialog from "../../views/dialogs/security/InitialCryptoSetup"; interface IProps { matrixClient: MatrixClient; @@ -25,7 +25,7 @@ export default class E2eSetup extends React.Component { return ( - = ({ matrixClient, accountPassword, tokenLogin, onFinished }) => { +const InitialCryptoSetupDialog: React.FC = ({ matrixClient, accountPassword, tokenLogin, onFinished }) => { const [error, setError] = useState(false); const doSetup = useCallback(async () => { @@ -102,4 +102,4 @@ const InitialCryptoSetup: React.FC = ({ matrixClient, accountPassword, to ); }; -export default InitialCryptoSetup; +export default InitialCryptoSetupDialog; diff --git a/test/components/views/dialogs/security/CreateCrossSigningDialog-test.tsx b/test/components/views/dialogs/security/InitialCryptoSetupDialog-test.tsx similarity index 91% rename from test/components/views/dialogs/security/CreateCrossSigningDialog-test.tsx rename to test/components/views/dialogs/security/InitialCryptoSetupDialog-test.tsx index 3e5dc4eb94..199174a634 100644 --- a/test/components/views/dialogs/security/CreateCrossSigningDialog-test.tsx +++ b/test/components/views/dialogs/security/InitialCryptoSetupDialog-test.tsx @@ -12,14 +12,14 @@ import { mocked } from "jest-mock"; import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { createCrossSigning } from "../../../../../src/CreateCrossSigning"; -import CreateCrossSigningDialog from "../../../../../src/components/views/dialogs/security/CreateCrossSigningDialog"; +import InitialCryptoSetupDialog from "../../../../../src/components/views/dialogs/security/InitialCryptoSetupDialog"; import { createTestClient } from "../../../../test-utils"; jest.mock("../../../../../src/CreateCrossSigning", () => ({ createCrossSigning: jest.fn(), })); -describe("CreateCrossSigningDialog", () => { +describe("InitialCryptoSetupDialog", () => { let client: MatrixClient; let createCrossSigningResolve: () => void; let createCrossSigningReject: (e: Error) => void; @@ -43,7 +43,7 @@ describe("CreateCrossSigningDialog", () => { const onFinished = jest.fn(); render( - { it("should display an error if createCrossSigning fails", async () => { render( - { const onFinished = jest.fn(); render( - { const onFinished = jest.fn(); render( - { it("should retry when the retry button is clicked", async () => { render( - Date: Tue, 22 Oct 2024 15:40:39 +0100 Subject: [PATCH 03/71] Fix import --- src/components/structures/auth/E2eSetup.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/auth/E2eSetup.tsx b/src/components/structures/auth/E2eSetup.tsx index 4ba7be6e5a..4063ae0252 100644 --- a/src/components/structures/auth/E2eSetup.tsx +++ b/src/components/structures/auth/E2eSetup.tsx @@ -11,7 +11,7 @@ import { MatrixClient } from "matrix-js-sdk/src/matrix"; import AuthPage from "../../views/auth/AuthPage"; import CompleteSecurityBody from "../../views/auth/CompleteSecurityBody"; -import InitialCryptoSetupDialog from "../../views/dialogs/security/InitialCryptoSetup"; +import InitialCryptoSetupDialog from "../../views/dialogs/security/InitialCryptoSetupDialog"; interface IProps { matrixClient: MatrixClient; From 6a912e28be76073661704a39dd911ed7ccdf2865 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 22 Oct 2024 16:03:26 +0100 Subject: [PATCH 04/71] Fix test --- test/unit-tests/components/structures/MatrixChat-test.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/unit-tests/components/structures/MatrixChat-test.tsx b/test/unit-tests/components/structures/MatrixChat-test.tsx index 7f565d682f..c042fd1b3b 100644 --- a/test/unit-tests/components/structures/MatrixChat-test.tsx +++ b/test/unit-tests/components/structures/MatrixChat-test.tsx @@ -148,6 +148,7 @@ describe("", () => { isRoomEncrypted: jest.fn(), logout: jest.fn(), getDeviceId: jest.fn(), + getKeyBackupVersion: jest.fn().mockResolvedValue(null), }); let mockClient: Mocked; const serverConfig = { @@ -1009,6 +1010,7 @@ describe("", () => { userHasCrossSigningKeys: jest.fn().mockResolvedValue(false), // This needs to not finish immediately because we need to test the screen appears bootstrapCrossSigning: jest.fn().mockImplementation(() => bootstrapDeferred.promise), + resetKeyBackup: jest.fn(), }; loginClient.getCrypto.mockReturnValue(mockCrypto as any); }); From c519361b4ef8fadc772dc8fda07256fc4646aeec Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 26 Nov 2024 13:44:06 +0000 Subject: [PATCH 05/71] Upgrade dependency to matrix-js-sdk@34.13.0-rc.0 --- package.json | 2 +- yarn.lock | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index a48284bb97..732447e64e 100644 --- a/package.json +++ b/package.json @@ -122,7 +122,7 @@ "maplibre-gl": "^4.0.0", "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "0.0.1", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "34.13.0-rc.0", "matrix-widget-api": "^1.10.0", "memoize-one": "^6.0.0", "mime": "^4.0.4", diff --git a/yarn.lock b/yarn.lock index a2bf286f1b..94218f883f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8352,9 +8352,10 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": - version "34.12.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/544ac86d2080da8e55d0b727cae826e42600c490" +matrix-js-sdk@34.13.0-rc.0: + version "34.13.0-rc.0" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-34.13.0-rc.0.tgz#788b80be39ca3a084d311c1415b45a67b8dfea66" + integrity sha512-91hxAAgIsO0hblVdV3YqbBqzFJA5wlBlrrqo/dyEXTMsU6qxf64ABCnnz/0pAbTlQTFLtogSIQrIRBo0NNbPvg== dependencies: "@babel/runtime" "^7.12.5" "@matrix-org/matrix-sdk-crypto-wasm" "^9.0.0" From 756ce2c6394e32c6e3807c803e37d013c2e4f36b Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 26 Nov 2024 13:56:17 +0000 Subject: [PATCH 06/71] v1.11.87-rc.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 732447e64e..a439264aba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "element-web", - "version": "1.11.86", + "version": "1.11.87-rc.0", "description": "A feature-rich client for Matrix.org", "author": "New Vector Ltd.", "repository": { From 2ebc1b4a897129f64ac16f435d3e1c8d7e44b77b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 26 Nov 2024 14:14:29 +0000 Subject: [PATCH 07/71] Fix deploy action Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .github/workflows/deploy.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a41a4dcec7..29c885badb 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,7 +1,8 @@ # Manual deploy workflow for deploying to app.element.io & staging.element.io # Runs automatically for staging.element.io when an RC or Release is published # Note: Does *NOT* run automatically for app.element.io so that it gets tested on staging.element.io beforehand -name: Build and Deploy ${{ inputs.site || 'staging.element.io' }} +name: Deploy release +run-name: Deploy ${{ github.ref_name }} to ${{ inputs.site || 'staging.element.io' }} on: release: types: [published] @@ -28,6 +29,8 @@ jobs: env: SITE: ${{ inputs.site || 'staging.element.io' }} steps: + - uses: actions/checkout@v4 + - name: Load GPG key run: | curl https://packages.element.io/element-release-key.gpg | gpg --import @@ -45,20 +48,20 @@ jobs: - name: Download current version for its old bundles id: current_download if: steps.current_version.outputs.version != github.ref_name - uses: element-hq/element-web/.github/actions/download-verify-element-tarball@${{ github.ref_name }} + uses: ./.github/actions/download-verify-element-tarball with: tag: steps.current_version.outputs.version - out-file-path: current_version + out-file-path: _current_version - name: Download target version - uses: element-hq/element-web/.github/actions/download-verify-element-tarball@${{ github.ref_name }} + uses: ./.github/actions/download-verify-element-tarball with: tag: ${{ github.ref_name }} out-file-path: _deploy - name: Merge current bundles into target if: steps.current_download.outcome == 'success' - run: cp -vnpr current_version/bundles/* _deploy/bundles/ + run: cp -vnpr _current_version/bundles/* _deploy/bundles/ - name: Copy config run: cp element.io/app/config.json _deploy/config.json From 94130be5a72381cf2c0d6ce078f12175bbf26366 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 26 Nov 2024 14:19:43 +0000 Subject: [PATCH 08/71] v1.11.87-rc.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a439264aba..866680dc41 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "element-web", - "version": "1.11.87-rc.0", + "version": "1.11.87-rc.1", "description": "A feature-rich client for Matrix.org", "author": "New Vector Ltd.", "repository": { From 4285b4b14041e9a0b4346bd6610281d9f1b6a743 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 26 Nov 2024 14:27:06 +0000 Subject: [PATCH 09/71] GPG_FINGERPRINT is in vars not secrets Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 29c885badb..a251e65509 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -36,7 +36,7 @@ jobs: curl https://packages.element.io/element-release-key.gpg | gpg --import gpg -k "$GPG_FINGERPRINT" env: - GPG_FINGERPRINT: ${{ secrets.GPG_FINGERPRINT }} + GPG_FINGERPRINT: ${{ vars.GPG_FINGERPRINT }} - name: Check current version on deployment id: current_version From 9860f9320a0eb56031266df83e9c991c8d90fef9 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 26 Nov 2024 14:29:01 +0000 Subject: [PATCH 10/71] v1.11.87-rc.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 866680dc41..0d4d410ecb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "element-web", - "version": "1.11.87-rc.1", + "version": "1.11.87-rc.2", "description": "A feature-rich client for Matrix.org", "author": "New Vector Ltd.", "repository": { From b4445fed5343ddf1bae244c8c484bde04947cfab Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 26 Nov 2024 14:41:00 +0000 Subject: [PATCH 11/71] Specify shell:bash in download-verify-element-tarball Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .github/actions/download-verify-element-tarball/action.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/actions/download-verify-element-tarball/action.yml b/.github/actions/download-verify-element-tarball/action.yml index 978b27bae4..ad9f3018b7 100644 --- a/.github/actions/download-verify-element-tarball/action.yml +++ b/.github/actions/download-verify-element-tarball/action.yml @@ -19,15 +19,19 @@ runs: out-file-path: ${{ runner.temp }}/download-verify-element-tarball - name: Verify tarball + shell: bash run: gpg --verify element-*.tar.gz.asc element-*.tar.gz working-directory: ${{ runner.temp }}/download-verify-element-tarball - name: Extract tarball + shell: bash run: tar xvzf element-*.tar.gz -C webapp --strip-components=1 working-directory: ${{ runner.temp }}/download-verify-element-tarball - name: Move webapp to out-file-path + shell: bash run: mv ${{ runner.temp }}/download-verify-element-tarball/webapp ${{ inputs.out-file-path }} - name: Clean up temp directory + shell: bash run: rm -R ${{ runner.temp }}/download-verify-element-tarball From 20532144d2268433e0176bf5db043ba43bde3c9a Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 26 Nov 2024 14:42:48 +0000 Subject: [PATCH 12/71] v1.11.87-rc.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0d4d410ecb..d5f8cfb04b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "element-web", - "version": "1.11.87-rc.2", + "version": "1.11.87-rc.3", "description": "A feature-rich client for Matrix.org", "author": "New Vector Ltd.", "repository": { From 4259e96c904d4ec33df579b77f9d1cab7b6932f4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 26 Nov 2024 14:53:17 +0000 Subject: [PATCH 13/71] Fix current version checking function Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .github/workflows/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a251e65509..b0cbaef403 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -41,7 +41,7 @@ jobs: - name: Check current version on deployment id: current_version run: | - echo "version=$(curl -s https://$SITE/version)" >> $GITHUB_OUTPUT + echo "version=v$(curl -s https://$SITE/version)" >> $GITHUB_OUTPUT # The current version bundle melding dance is skipped if the version we're deploying is the same # as then we're just doing a re-deploy of the same version with potentially different configs. @@ -50,7 +50,7 @@ jobs: if: steps.current_version.outputs.version != github.ref_name uses: ./.github/actions/download-verify-element-tarball with: - tag: steps.current_version.outputs.version + tag: ${{ steps.current_version.outputs.version }} out-file-path: _current_version - name: Download target version From 903928c33c553e7b172d92998aaddcee7ebd061e Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 26 Nov 2024 14:56:48 +0000 Subject: [PATCH 14/71] v1.11.87-rc.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d5f8cfb04b..bcd8d3c135 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "element-web", - "version": "1.11.87-rc.3", + "version": "1.11.87-rc.4", "description": "A feature-rich client for Matrix.org", "author": "New Vector Ltd.", "repository": { From bd0bb879ec33b369f18cd7db2208b46beb1c061b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 26 Nov 2024 15:03:39 +0000 Subject: [PATCH 15/71] Fix download-verify-element-tarball not consuming inputs.tag Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .github/actions/download-verify-element-tarball/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/download-verify-element-tarball/action.yml b/.github/actions/download-verify-element-tarball/action.yml index ad9f3018b7..665a5af62d 100644 --- a/.github/actions/download-verify-element-tarball/action.yml +++ b/.github/actions/download-verify-element-tarball/action.yml @@ -14,7 +14,7 @@ runs: id: current_download uses: robinraju/release-downloader@a96f54c1b5f5e09e47d9504526e96febd949d4c2 # v1 with: - tag: steps.current_version.outputs.version + tag: ${{ inputs.tag }} fileName: element-*.tar.gz* out-file-path: ${{ runner.temp }}/download-verify-element-tarball From 9ddd5d96eb5e46fa72276836dda58fe7569bd44c Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 26 Nov 2024 15:05:15 +0000 Subject: [PATCH 16/71] v1.11.87-rc.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bcd8d3c135..a8f914a779 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "element-web", - "version": "1.11.87-rc.4", + "version": "1.11.87-rc.5", "description": "A feature-rich client for Matrix.org", "author": "New Vector Ltd.", "repository": { From 3d48168394c12c599f1540e95331c39e27c0bdfe Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 26 Nov 2024 15:11:48 +0000 Subject: [PATCH 17/71] We have to make the tar output directory manually Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .github/actions/download-verify-element-tarball/action.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/actions/download-verify-element-tarball/action.yml b/.github/actions/download-verify-element-tarball/action.yml index 665a5af62d..e61b96596b 100644 --- a/.github/actions/download-verify-element-tarball/action.yml +++ b/.github/actions/download-verify-element-tarball/action.yml @@ -10,7 +10,7 @@ inputs: runs: using: composite steps: - - name: Download current version for its old bundles + - name: Download release tarball id: current_download uses: robinraju/release-downloader@a96f54c1b5f5e09e47d9504526e96febd949d4c2 # v1 with: @@ -25,7 +25,9 @@ runs: - name: Extract tarball shell: bash - run: tar xvzf element-*.tar.gz -C webapp --strip-components=1 + run: | + mkdir webapp + tar xvzf element-*.tar.gz -C webapp --strip-components=1 working-directory: ${{ runner.temp }}/download-verify-element-tarball - name: Move webapp to out-file-path From adfc66bd1b46b6f8104cedae32689446abc1c7f3 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 26 Nov 2024 15:15:08 +0000 Subject: [PATCH 18/71] v1.11.87-rc.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a8f914a779..404b30815a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "element-web", - "version": "1.11.87-rc.5", + "version": "1.11.87-rc.6", "description": "A feature-rich client for Matrix.org", "author": "New Vector Ltd.", "repository": { From c1549e6aaa6d583583402874b6170c70276c62f3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 26 Nov 2024 15:28:59 +0000 Subject: [PATCH 19/71] Fix running-workflow-name so it can self-detect Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b0cbaef403..ac6249d654 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -76,7 +76,7 @@ jobs: uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork with: ref: ${{ github.sha }} - running-workflow-name: "Build and Deploy ${{ env.SITE }}" + running-workflow-name: "Deploy to Cloudflare Pages" repo-token: ${{ secrets.GITHUB_TOKEN }} wait-interval: 10 check-regexp: ^((?!SonarCloud|SonarQube|issue|board|label|Release|prepare|GitHub Pages).)*$ From a891dcddc25325a32303cc01775cfe4b7a6e21a8 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 26 Nov 2024 15:33:55 +0000 Subject: [PATCH 20/71] v1.11.87-rc.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 404b30815a..e55447f0a7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "element-web", - "version": "1.11.87-rc.6", + "version": "1.11.87-rc.7", "description": "A feature-rich client for Matrix.org", "author": "New Vector Ltd.", "repository": { From b8f6bd1664d70e3722a42464faa31fa60622d5ac Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 2 Dec 2024 14:44:00 +0000 Subject: [PATCH 21/71] Secure backup prompt --- src/DeviceListener.ts | 28 ++++++++++++++-------- src/i18n/strings/en_EN.json | 3 +++ src/toasts/SetupEncryptionToast.ts | 38 +++++++++++++++++++++++++++--- 3 files changed, 56 insertions(+), 13 deletions(-) diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index 4f47cd7eac..e80c9402b2 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -292,21 +292,29 @@ export default class DeviceListener { await crypto.getUserDeviceInfo([cli.getSafeUserId()]); // cross signing isn't enabled - nag to enable it - // There are 2 different toasts for: + // There are 3 different toasts for: if (!(await crypto.getCrossSigningKeyId()) && (await crypto.userHasCrossSigningKeys())) { - // Cross-signing on account but this device doesn't trust the master key (verify this session) + // Toast 1. Cross-signing on account but this device doesn't trust the master key (verify this session) showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION); this.checkKeyBackupStatus(); } else { - // No cross-signing or key backup on account (set up encryption) - await cli.waitForClientWellKnown(); - if (isSecureBackupRequired(cli) && isLoggedIn()) { - // If we're meant to set up, and Secure Backup is required, - // trigger the flow directly without a toast once logged in. - hideSetupEncryptionToast(); - accessSecretStorage(); + const backupInfo = await this.getKeyBackupInfo(); + if (backupInfo) { + // Toast 2: Key backup is enabled but recovery (4S) is not set up: prompt user to set up recovery. + // Since we now enable key backup at registration time, this will be the common case for + // new users. + showSetupEncryptionToast(SetupKind.SET_UP_RECOVERY); } else { - showSetupEncryptionToast(SetupKind.SET_UP_ENCRYPTION); + // Toast 3: No cross-signing or key backup on account (set up encryption) + await cli.waitForClientWellKnown(); + if (isSecureBackupRequired(cli) && isLoggedIn()) { + // If we're meant to set up, and Secure Backup is required, + // trigger the flow directly without a toast once logged in. + hideSetupEncryptionToast(); + accessSecretStorage(); + } else { + showSetupEncryptionToast(SetupKind.SET_UP_ENCRYPTION); + } } } } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3b4765b0ad..807264c4d2 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -912,6 +912,9 @@ "warning": "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings." }, "reset_all_button": "Forgotten or lost all recovery methods? Reset all", + "set_up_recovery": "Set up recovery", + "set_up_recovery_later": "Not now", + "set_up_recovery_toast_title": "Set up recovery to protect your account", "set_up_toast_description": "Safeguard against losing access to encrypted messages & data", "set_up_toast_title": "Set up Secure Backup", "setup_secure_backup": { diff --git a/src/toasts/SetupEncryptionToast.ts b/src/toasts/SetupEncryptionToast.ts index 0dd54bb18f..8ae4830242 100644 --- a/src/toasts/SetupEncryptionToast.ts +++ b/src/toasts/SetupEncryptionToast.ts @@ -6,6 +6,9 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ +import KeyboardIcon from "@vector-im/compound-design-tokens/assets/web/icons/key"; +import { ComponentType } from "react"; + import Modal from "../Modal"; import { _t } from "../languageHandler"; import DeviceListener from "../DeviceListener"; @@ -23,33 +26,61 @@ const getTitle = (kind: Kind): string => { switch (kind) { case Kind.SET_UP_ENCRYPTION: return _t("encryption|set_up_toast_title"); + case Kind.SET_UP_RECOVERY: + return _t("encryption|set_up_recovery_toast_title"); case Kind.VERIFY_THIS_SESSION: return _t("encryption|verify_toast_title"); } }; -const getIcon = (kind: Kind): string => { +const getIcon = (kind: Kind): string | undefined => { switch (kind) { case Kind.SET_UP_ENCRYPTION: return "secure_backup"; + case Kind.SET_UP_RECOVERY: + return undefined; case Kind.VERIFY_THIS_SESSION: return "verification_warning"; } }; +// Gets the icon displayed on the prinary button +const getPrimaryIcon = (kind: Kind): ComponentType> | undefined => { + switch (kind) { + case Kind.SET_UP_RECOVERY: + return KeyboardIcon; + default: + return undefined; + } +}; + const getSetupCaption = (kind: Kind): string => { switch (kind) { case Kind.SET_UP_ENCRYPTION: return _t("action|continue"); + case Kind.SET_UP_RECOVERY: + return _t("encryption|set_up_recovery"); case Kind.VERIFY_THIS_SESSION: return _t("action|verify"); } }; +const getSecondaryButtonLabel = (kind: Kind): string => { + switch (kind) { + case Kind.SET_UP_RECOVERY: + return _t("encryption|set_up_recovery_later"); + case Kind.SET_UP_ENCRYPTION: + case Kind.VERIFY_THIS_SESSION: + return _t("encryption|verification|unverified_sessions_toast_reject"); + } +}; + const getDescription = (kind: Kind): string => { switch (kind) { case Kind.SET_UP_ENCRYPTION: return _t("encryption|set_up_toast_description"); + case Kind.SET_UP_RECOVERY: + return _t("encryption|set_up_recovery_toast_title"); case Kind.VERIFY_THIS_SESSION: return _t("encryption|verify_toast_description"); } @@ -57,6 +88,7 @@ const getDescription = (kind: Kind): string => { export enum Kind { SET_UP_ENCRYPTION = "set_up_encryption", + SET_UP_RECOVERY = "set_up_recovery", VERIFY_THIS_SESSION = "verify_this_session", } @@ -101,9 +133,9 @@ export const showToast = (kind: Kind): void => { description: getDescription(kind), primaryLabel: getSetupCaption(kind), onPrimaryClick: onAccept, - secondaryLabel: _t("encryption|verification|unverified_sessions_toast_reject"), + secondaryLabel: getSecondaryButtonLabel(kind), onSecondaryClick: onReject, - destructive: "secondary", + PrimaryIcon: getPrimaryIcon(kind), }, component: GenericToast, priority: kind === Kind.VERIFY_THIS_SESSION ? 95 : 40, From b5ae775d8f57bd70395c476f9034bff7d295d498 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 2 Dec 2024 15:08:35 +0000 Subject: [PATCH 22/71] Add mock so test passes again --- test/test-utils/test-utils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index f9aee512a3..3879f8af81 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -281,6 +281,7 @@ export function createTestClient(): MatrixClient { getLocalAliases: jest.fn().mockReturnValue([]), uploadDeviceSigningKeys: jest.fn(), isKeyBackupKeyStored: jest.fn().mockResolvedValue(null), + getKeyBackupVersion: jest.fn(), } as unknown as MatrixClient; client.reEmitter = new ReEmitter(client); From 2a5af99ac2adc9b619fce3fa68d10b4438ce82e9 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 2 Dec 2024 15:25:51 +0000 Subject: [PATCH 23/71] Fix tests --- test/unit-tests/DeviceListener-test.ts | 4 ++-- test/unit-tests/components/structures/MatrixChat-test.tsx | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test/unit-tests/DeviceListener-test.ts b/test/unit-tests/DeviceListener-test.ts index ad7f14e119..1c8fe1a1c7 100644 --- a/test/unit-tests/DeviceListener-test.ts +++ b/test/unit-tests/DeviceListener-test.ts @@ -352,13 +352,13 @@ describe("DeviceListener", () => { mockCrypto!.getCrossSigningKeyId.mockResolvedValue("abc"); }); - it("shows set up encryption toast when user has a key backup available", async () => { + it("shows set up recovery toast when user has a key backup available", async () => { // non falsy response mockCrypto.getKeyBackupInfo.mockResolvedValue({} as unknown as KeyBackupInfo); await createAndStart(); expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith( - SetupEncryptionToast.Kind.SET_UP_ENCRYPTION, + SetupEncryptionToast.Kind.SET_UP_RECOVERY, ); }); }); diff --git a/test/unit-tests/components/structures/MatrixChat-test.tsx b/test/unit-tests/components/structures/MatrixChat-test.tsx index bc6bb0f9ff..7e3bd7d4f1 100644 --- a/test/unit-tests/components/structures/MatrixChat-test.tsx +++ b/test/unit-tests/components/structures/MatrixChat-test.tsx @@ -148,6 +148,7 @@ describe("", () => { whoami: jest.fn(), logout: jest.fn(), getDeviceId: jest.fn(), + getKeyBackupVersion: jest.fn(), }); let mockClient: Mocked; const serverConfig = { From 0429809c00bee75c255ae981ee7b6e4372ef09c6 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 2 Dec 2024 19:10:17 +0100 Subject: [PATCH 24/71] New UX for Share dialog (#28598) * New UX for `ShareDialog` * Use new named import * Rewrite tests * Add e2e tests * Use `box-sizing` for social buttons * Update e2e tests --- .../e2e/share-dialog/share-dialog.spec.ts | 67 ++ .../share-dialog-event-linux.png | Bin 0 -> 17837 bytes .../share-dialog-room-linux.png | Bin 0 -> 15840 bytes .../share-dialog-user-linux.png | Bin 0 -> 16839 bytes res/css/_common.pcss | 14 +- res/css/views/dialogs/_ShareDialog.pcss | 105 ++- .../context_menus/MessageContextMenu.tsx | 2 +- src/components/views/dialogs/ShareDialog.tsx | 296 +++--- .../views/right_panel/RoomSummaryCard.tsx | 2 +- src/components/views/right_panel/UserInfo.tsx | 2 +- .../rooms/RoomHeader/CallGuestLinkButton.tsx | 2 +- src/i18n/strings/en_EN.json | 2 +- .../views/dialogs/ShareDialog-test.tsx | 176 ++-- .../__snapshots__/ShareDialog-test.tsx.snap | 852 ++++++++++++++++++ .../right_panel/RoomSummaryCard-test.tsx | 2 +- .../views/right_panel/UserInfo-test.tsx | 2 +- .../RoomHeader/CallGuestLinkButton-test.tsx | 2 +- 17 files changed, 1244 insertions(+), 282 deletions(-) create mode 100644 playwright/e2e/share-dialog/share-dialog.spec.ts create mode 100644 playwright/snapshots/share-dialog/share-dialog.spec.ts/share-dialog-event-linux.png create mode 100644 playwright/snapshots/share-dialog/share-dialog.spec.ts/share-dialog-room-linux.png create mode 100644 playwright/snapshots/share-dialog/share-dialog.spec.ts/share-dialog-user-linux.png create mode 100644 test/unit-tests/components/views/dialogs/__snapshots__/ShareDialog-test.tsx.snap diff --git a/playwright/e2e/share-dialog/share-dialog.spec.ts b/playwright/e2e/share-dialog/share-dialog.spec.ts new file mode 100644 index 0000000000..2999b74ca0 --- /dev/null +++ b/playwright/e2e/share-dialog/share-dialog.spec.ts @@ -0,0 +1,67 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only + * Please see LICENSE files in the repository root for full details. + */ + +import { test, expect } from "../../element-web-test"; + +test.describe("Share dialog", () => { + test.use({ + displayName: "Alice", + room: async ({ app, user, bot }, use) => { + const roomId = await app.client.createRoom({ name: "Alice room" }); + await use({ roomId }); + }, + }); + + test("should share a room", async ({ page, app, room }) => { + await app.viewRoomById(room.roomId); + await app.toggleRoomInfoPanel(); + await page.getByRole("menuitem", { name: "Copy link" }).click(); + + const dialog = page.getByRole("dialog", { name: "Share room" }); + await expect(dialog.getByText(`https://matrix.to/#/${room.roomId}`)).toBeVisible(); + expect(dialog).toMatchScreenshot("share-dialog-room.png", { + // QRCode and url changes at every run + mask: [page.locator(".mx_QRCode"), page.locator(".mx_ShareDialog_top > span")], + }); + }); + + test("should share a room member", async ({ page, app, room, user }) => { + await app.viewRoomById(room.roomId); + await app.client.sendMessage(room.roomId, { body: "hello", msgtype: "m.text" }); + + const rightPanel = await app.toggleRoomInfoPanel(); + await rightPanel.getByRole("menuitem", { name: "People" }).click(); + await rightPanel.getByRole("button", { name: `${user.userId} (power 100)` }).click(); + await rightPanel.getByRole("button", { name: "Share profile" }).click(); + + const dialog = page.getByRole("dialog", { name: "Share User" }); + await expect(dialog.getByText(`https://matrix.to/#/${user.userId}`)).toBeVisible(); + expect(dialog).toMatchScreenshot("share-dialog-user.png", { + // QRCode changes at every run + mask: [page.locator(".mx_QRCode")], + }); + }); + + test("should share an event", async ({ page, app, room }) => { + await app.viewRoomById(room.roomId); + await app.client.sendMessage(room.roomId, { body: "hello", msgtype: "m.text" }); + + const timelineMessage = page.locator(".mx_MTextBody", { hasText: "hello" }); + await timelineMessage.hover(); + await page.getByRole("button", { name: "Options", exact: true }).click(); + await page.getByRole("menuitem", { name: "Share" }).click(); + + const dialog = page.getByRole("dialog", { name: "Share Room Message" }); + await expect(dialog.getByRole("checkbox", { name: "Link to selected message" })).toBeChecked(); + expect(dialog).toMatchScreenshot("share-dialog-event.png", { + // QRCode and url changes at every run + mask: [page.locator(".mx_QRCode"), page.locator(".mx_ShareDialog_top > span")], + }); + await dialog.getByRole("checkbox", { name: "Link to selected message" }).click(); + await expect(dialog.getByRole("checkbox", { name: "Link to selected message" })).not.toBeChecked(); + }); +}); diff --git a/playwright/snapshots/share-dialog/share-dialog.spec.ts/share-dialog-event-linux.png b/playwright/snapshots/share-dialog/share-dialog.spec.ts/share-dialog-event-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..2f703cfc8ac4690eebf48176556e96371eb773fa GIT binary patch literal 17837 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 B;y?fZ literal 0 HcmV?d00001 diff --git a/playwright/snapshots/share-dialog/share-dialog.spec.ts/share-dialog-room-linux.png b/playwright/snapshots/share-dialog/share-dialog.spec.ts/share-dialog-room-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..725e095f6f9d29c047ebbee53dc065933ca80b42 GIT binary patch literal 15840 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 H>-YZysD(JC literal 0 HcmV?d00001 diff --git a/res/css/_common.pcss b/res/css/_common.pcss index 15ba02b6b8..74328af39b 100644 --- a/res/css/_common.pcss +++ b/res/css/_common.pcss @@ -596,7 +596,7 @@ legend { .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button), + ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button), .mx_Dialog input[type="submit"], .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton), .mx_Dialog_buttons input[type="submit"] { @@ -616,14 +616,16 @@ legend { .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):last-child { + ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not( + .mx_ShareDialog button + ):last-child { margin-right: 0px; } .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):focus, + ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button):focus, .mx_Dialog input[type="submit"]:focus, .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):focus, .mx_Dialog_buttons input[type="submit"]:focus { @@ -635,7 +637,7 @@ legend { .mx_Dialog_buttons button.mx_Dialog_primary:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button), + ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button), .mx_Dialog_buttons input[type="submit"].mx_Dialog_primary { color: var(--cpd-color-text-on-solid-primary); background-color: var(--cpd-color-bg-action-primary-rest); @@ -648,7 +650,7 @@ legend { .mx_Dialog_buttons button.danger:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(.mx_UserProfileSettings button):not( .mx_ThemeChoicePanel_CustomTheme button - ):not(.mx_UnpinAllDialog button), + ):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button), .mx_Dialog_buttons input[type="submit"].danger { background-color: var(--cpd-color-bg-critical-primary); border: solid 1px var(--cpd-color-bg-critical-primary); @@ -664,7 +666,7 @@ legend { .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):disabled, + ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button):disabled, .mx_Dialog input[type="submit"]:disabled, .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):disabled, .mx_Dialog_buttons input[type="submit"]:disabled { diff --git a/res/css/views/dialogs/_ShareDialog.pcss b/res/css/views/dialogs/_ShareDialog.pcss index 086222af31..cfede43aae 100644 --- a/res/css/views/dialogs/_ShareDialog.pcss +++ b/res/css/views/dialogs/_ShareDialog.pcss @@ -5,50 +5,73 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -.mx_ShareDialog hr { - margin-top: 25px; - margin-bottom: 25px; - border-color: $light-fg-color; -} +.mx_ShareDialog { + /* Value from figma design */ + width: 416px; -.mx_ShareDialog .mx_ShareDialog_content { - margin: 10px 0; + .mx_Dialog_header { + text-align: center; + margin-bottom: var(--cpd-space-6x); + /* Override dialog header padding to able to center it */ + padding-inline-end: 0; + } - .mx_CopyableText { - width: unset; /* full width */ + .mx_ShareDialog_content { + display: flex; + flex-direction: column; + gap: var(--cpd-space-6x); + align-items: center; - > a { - text-decoration: none; - flex-shrink: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + .mx_ShareDialog_top { + display: flex; + flex-direction: column; + gap: var(--cpd-space-4x); + align-items: center; + width: 100%; + + span { + text-align: center; + font: var(--cpd-font-body-sm-semibold); + color: var(--cpd-color-text-secondary); + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + width: 100%; + } + } + + label { + display: inline-flex; + gap: var(--cpd-space-3x); + justify-content: center; + align-items: center; + font: var(--cpd-font-body-md-medium); + } + + button { + width: 100%; + } + + .mx_ShareDialog_social { + display: flex; + gap: var(--cpd-space-3x); + justify-content: center; + + a { + width: 48px; + height: 48px; + border-radius: 99px; + box-sizing: border-box; + border: 1px solid var(--cpd-color-border-interactive-secondary); + display: flex; + justify-content: center; + align-items: center; + + img { + width: 24px; + height: 24px; + } + } } } } - -.mx_ShareDialog_split { - display: flex; - flex-wrap: wrap; -} - -.mx_ShareDialog_qrcode_container { - float: left; - height: 256px; - width: 256px; - margin-right: 64px; -} - -.mx_ShareDialog_qrcode_container + .mx_ShareDialog_social_container { - width: 299px; -} - -.mx_ShareDialog_social_container { - display: inline-block; -} - -.mx_ShareDialog_social_icon { - display: inline-grid; - margin-right: 10px; - margin-bottom: 10px; -} diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index 2b559aa74c..711ffbe70f 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -38,7 +38,7 @@ import ContextMenu, { toRightOf, MenuProps } from "../../structures/ContextMenu" import ReactionPicker from "../emojipicker/ReactionPicker"; import ViewSource from "../../structures/ViewSource"; import { createRedactEventDialog } from "../dialogs/ConfirmRedactDialog"; -import ShareDialog from "../dialogs/ShareDialog"; +import { ShareDialog } from "../dialogs/ShareDialog"; import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext"; import EndPollDialog from "../dialogs/EndPollDialog"; import { isPollEnded } from "../messages/MPollBody"; diff --git a/src/components/views/dialogs/ShareDialog.tsx b/src/components/views/dialogs/ShareDialog.tsx index f9382227e4..1796b79239 100644 --- a/src/components/views/dialogs/ShareDialog.tsx +++ b/src/components/views/dialogs/ShareDialog.tsx @@ -7,22 +7,23 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import * as React from "react"; +import React, { JSX, useMemo, useRef, useState } from "react"; import { Room, RoomMember, MatrixEvent, User } from "matrix-js-sdk/src/matrix"; +import { Checkbox, Button } from "@vector-im/compound-web"; +import LinkIcon from "@vector-im/compound-design-tokens/assets/web/icons/link"; +import CheckIcon from "@vector-im/compound-design-tokens/assets/web/icons/check"; import { _t } from "../../../languageHandler"; import QRCode from "../elements/QRCode"; import { RoomPermalinkCreator, makeUserPermalink } from "../../../utils/permalinks/Permalinks"; -import { selectText } from "../../../utils/strings"; -import StyledCheckbox from "../elements/StyledCheckbox"; -import SettingsStore from "../../../settings/SettingsStore"; +import { copyPlaintext } from "../../../utils/strings"; import { UIFeature } from "../../../settings/UIFeature"; import BaseDialog from "./BaseDialog"; -import CopyableText from "../elements/CopyableText"; import { XOR } from "../../../@types/common"; +import { useSettingValue } from "../../../hooks/useSettings.ts"; /* eslint-disable @typescript-eslint/no-require-imports */ -const socials = [ +const SOCIALS = [ { name: "Facebook", img: require("../../../../res/img/social/facebook.png"), @@ -33,11 +34,7 @@ const socials = [ img: require("../../../../res/img/social/twitter-2.png"), url: (url: string) => `https://twitter.com/home?status=${url}`, }, - /* // icon missing - name: 'Google Plus', - img: 'img/social/', - url: (url) => `https://plus.google.com/share?url=${url}`, - },*/ { + { name: "LinkedIn", img: require("../../../../res/img/social/linkedin.png"), url: (url: string) => `https://www.linkedin.com/shareArticle?mini=true&url=${url}`, @@ -78,160 +75,153 @@ interface Props extends BaseProps { * A matrix.to link will be generated out of it if it's not already a url. */ target: Room | User | RoomMember | URL; + /** + * Optional when the target is a Room, User, RoomMember or a URL. + * Mandatory when the target is a MatrixEvent. + */ permalinkCreator?: RoomPermalinkCreator; } interface EventProps extends BaseProps { + /** + * The target to link to. + */ target: MatrixEvent; + /** + * Optional when the target is a Room, User, RoomMember or a URL. + * Mandatory when the target is a MatrixEvent. + */ permalinkCreator: RoomPermalinkCreator; } -interface IState { - linkSpecificEvent: boolean; - permalinkCreator: RoomPermalinkCreator | null; +type ShareDialogProps = XOR; + +/** + * A dialog to share a link to a room, user, room member or a matrix event. + */ +export function ShareDialog({ target, customTitle, onFinished, permalinkCreator }: ShareDialogProps): JSX.Element { + const showQrCode = useSettingValue(UIFeature.ShareQRCode); + const showSocials = useSettingValue(UIFeature.ShareSocial); + + const timeoutIdRef = useRef(); + const [isCopied, setIsCopied] = useState(false); + + const [linkToSpecificEvent, setLinkToSpecificEvent] = useState(target instanceof MatrixEvent); + const { title, url, checkboxLabel } = useTargetValues(target, linkToSpecificEvent, permalinkCreator); + const newTitle = customTitle ?? title; + + return ( + +
+
+ {showQrCode && } + {url} +
+ {checkboxLabel && ( + + )} + + {showSocials && } +
+
+ ); } -export default class ShareDialog extends React.PureComponent, IState> { - public constructor(props: XOR) { - super(props); +/** + * Social links to share the link on different platforms. + */ +interface SocialLinksProps { + /** + * The URL to share. + */ + url: string; +} - let permalinkCreator: RoomPermalinkCreator | null = null; - if (props.target instanceof Room) { - permalinkCreator = new RoomPermalinkCreator(props.target); - permalinkCreator.load(); +/** + * The socials to share the link on. + */ +function SocialLinks({ url }: SocialLinksProps): JSX.Element { + return ( +
+ ); +} + +/** + * Get the title, url and checkbox label for the dialog based on the target. + * @param target + * @param linkToSpecificEvent + * @param permalinkCreator + */ +function useTargetValues( + target: ShareDialogProps["target"], + linkToSpecificEvent: boolean, + permalinkCreator?: RoomPermalinkCreator, +): { title: string; url: string; checkboxLabel?: string } { + return useMemo(() => { + if (target instanceof URL) return { title: _t("share|title_link"), url: target.toString() }; + if (target instanceof User || target instanceof RoomMember) + return { + title: _t("share|title_user"), + url: makeUserPermalink(target.userId), + }; + + if (target instanceof Room) { + const title = _t("share|title_room"); + const newPermalinkCreator = new RoomPermalinkCreator(target); + newPermalinkCreator.load(); + + const events = target.getLiveTimeline().getEvents(); + return { + title, + url: linkToSpecificEvent + ? newPermalinkCreator.forEvent(events[events.length - 1].getId()!) + : newPermalinkCreator.forShareableRoom(), + ...(events.length > 0 && { checkboxLabel: _t("share|permalink_most_recent") }), + }; } - this.state = { - // MatrixEvent defaults to share linkSpecificEvent - linkSpecificEvent: this.props.target instanceof MatrixEvent, - permalinkCreator, + // MatrixEvent is remaining and should have a permalinkCreator + const url = linkToSpecificEvent + ? permalinkCreator!.forEvent(target.getId()!) + : permalinkCreator!.forShareableRoom(); + return { + title: _t("share|title_message"), + url, + checkboxLabel: _t("share|permalink_message"), }; - } - - public static onLinkClick(e: React.MouseEvent): void { - e.preventDefault(); - selectText(e.currentTarget); - } - - private onLinkSpecificEventCheckboxClick = (): void => { - this.setState({ - linkSpecificEvent: !this.state.linkSpecificEvent, - }); - }; - - private getUrl(): string { - if (this.props.target instanceof URL) { - return this.props.target.toString(); - } else if (this.props.target instanceof Room) { - if (this.state.linkSpecificEvent) { - const events = this.props.target.getLiveTimeline().getEvents(); - return this.state.permalinkCreator!.forEvent(events[events.length - 1].getId()!); - } else { - return this.state.permalinkCreator!.forShareableRoom(); - } - } else if (this.props.target instanceof User || this.props.target instanceof RoomMember) { - return makeUserPermalink(this.props.target.userId); - } else if (this.state.linkSpecificEvent) { - return this.props.permalinkCreator!.forEvent(this.props.target.getId()!); - } else { - return this.props.permalinkCreator!.forShareableRoom(); - } - } - - public render(): React.ReactNode { - let title: string | undefined; - let checkbox: JSX.Element | undefined; - - if (this.props.target instanceof URL) { - title = this.props.customTitle ?? _t("share|title_link"); - } else if (this.props.target instanceof Room) { - title = this.props.customTitle ?? _t("share|title_room"); - - const events = this.props.target.getLiveTimeline().getEvents(); - if (events.length > 0) { - checkbox = ( -
- - {_t("share|permalink_most_recent")} - -
- ); - } - } else if (this.props.target instanceof User || this.props.target instanceof RoomMember) { - title = this.props.customTitle ?? _t("share|title_user"); - } else if (this.props.target instanceof MatrixEvent) { - title = this.props.customTitle ?? _t("share|title_message"); - checkbox = ( -
- - {_t("share|permalink_message")} - -
- ); - } - - const matrixToUrl = this.getUrl(); - const encodedUrl = encodeURIComponent(matrixToUrl); - - const showQrCode = SettingsStore.getValue(UIFeature.ShareQRCode); - const showSocials = SettingsStore.getValue(UIFeature.ShareSocial); - - let qrSocialSection; - if (showQrCode || showSocials) { - qrSocialSection = ( - <> -
-
- {showQrCode && ( -
- -
- )} - {showSocials && ( -
- {socials.map((social) => ( - - {social.name} - - ))} -
- )} -
- - ); - } - - return ( - - {this.props.subtitle &&

{this.props.subtitle}

} -
- matrixToUrl}> - - {matrixToUrl} - - - {checkbox} - {qrSocialSection} -
-
- ); - } + }, [target, linkToSpecificEvent, permalinkCreator]); } diff --git a/src/components/views/right_panel/RoomSummaryCard.tsx b/src/components/views/right_panel/RoomSummaryCard.tsx index d0e3694ea5..c8dd0b9738 100644 --- a/src/components/views/right_panel/RoomSummaryCard.tsx +++ b/src/components/views/right_panel/RoomSummaryCard.tsx @@ -47,7 +47,7 @@ import RoomAvatar from "../avatars/RoomAvatar"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases"; import Modal from "../../../Modal"; -import ShareDialog from "../dialogs/ShareDialog"; +import { ShareDialog } from "../dialogs/ShareDialog"; import { useEventEmitterState } from "../../../hooks/useEventEmitter"; import { E2EStatus } from "../../../utils/ShieldUtils"; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index b4a775367c..591e2327ae 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -63,7 +63,7 @@ import PowerSelector from "../elements/PowerSelector"; import MemberAvatar from "../avatars/MemberAvatar"; import PresenceLabel from "../rooms/PresenceLabel"; import BulkRedactDialog from "../dialogs/BulkRedactDialog"; -import ShareDialog from "../dialogs/ShareDialog"; +import { ShareDialog } from "../dialogs/ShareDialog"; import ErrorDialog from "../dialogs/ErrorDialog"; import QuestionDialog from "../dialogs/QuestionDialog"; import ConfirmUserActionDialog from "../dialogs/ConfirmUserActionDialog"; diff --git a/src/components/views/rooms/RoomHeader/CallGuestLinkButton.tsx b/src/components/views/rooms/RoomHeader/CallGuestLinkButton.tsx index ae8e7be16b..8c000bdf3b 100644 --- a/src/components/views/rooms/RoomHeader/CallGuestLinkButton.tsx +++ b/src/components/views/rooms/RoomHeader/CallGuestLinkButton.tsx @@ -12,7 +12,7 @@ import { logger } from "matrix-js-sdk/src/logger"; import { EventType, JoinRule, Room } from "matrix-js-sdk/src/matrix"; import Modal from "../../../../Modal"; -import ShareDialog from "../../dialogs/ShareDialog"; +import { ShareDialog } from "../../dialogs/ShareDialog"; import { _t } from "../../../../languageHandler"; import SettingsStore from "../../../../settings/SettingsStore"; import { calculateRoomVia } from "../../../../utils/permalinks/Permalinks"; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 31a6c71c1a..e601a7ecd5 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2944,7 +2944,7 @@ "warning": "WARNING: " }, "share": { - "link_title": "Link to room", + "link_copied": "Link copied", "permalink_message": "Link to selected message", "permalink_most_recent": "Link to most recent message", "share_call": "Conference invite link", diff --git a/test/unit-tests/components/views/dialogs/ShareDialog-test.tsx b/test/unit-tests/components/views/dialogs/ShareDialog-test.tsx index cb7d556235..c1d9883b7f 100644 --- a/test/unit-tests/components/views/dialogs/ShareDialog-test.tsx +++ b/test/unit-tests/components/views/dialogs/ShareDialog-test.tsx @@ -7,111 +7,139 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; -import { EventTimeline, MatrixEvent, Room, RoomMember } from "matrix-js-sdk/src/matrix"; -import { render, RenderOptions } from "jest-matrix-react"; +import { MatrixClient, MatrixEvent, Room, RoomMember } from "matrix-js-sdk/src/matrix"; +import { render, screen, act } from "jest-matrix-react"; +import userEvent from "@testing-library/user-event"; +import { waitFor } from "@testing-library/dom"; -import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; import SettingsStore from "../../../../../src/settings/SettingsStore"; -import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; -import { _t } from "../../../../../src/languageHandler"; -import ShareDialog from "../../../../../src/components/views/dialogs/ShareDialog"; +import { ShareDialog } from "../../../../../src/components/views/dialogs/ShareDialog"; import { UIFeature } from "../../../../../src/settings/UIFeature"; -import { stubClient } from "../../../../test-utils"; -jest.mock("../../../../../src/utils/ShieldUtils"); - -function getWrapper(): RenderOptions { - return { - wrapper: ({ children }) => ( - {children} - ), - }; -} +import { stubClient, withClientContextRenderOptions } from "../../../../test-utils"; +import * as StringsModule from "../../../../../src/utils/strings"; +import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks.ts"; describe("ShareDialog", () => { + let client: MatrixClient; let room: Room; - - const ROOM_ID = "!1:example.org"; + const copyTextFunc = jest.fn(); beforeEach(async () => { - stubClient(); - room = new Room(ROOM_ID, MatrixClientPeg.get()!, "@alice:example.org"); + client = stubClient(); + room = new Room("!1:example.org", client, "@alice:example.org"); + jest.spyOn(StringsModule, "copyPlaintext").mockImplementation(copyTextFunc); }); afterEach(() => { jest.restoreAllMocks(); + copyTextFunc.mockClear(); }); - it("renders room share dialog", () => { - const { container: withoutEvents } = render(, getWrapper()); - expect(withoutEvents).toHaveTextContent(_t("share|title_room")); + function renderComponent(target: Room | RoomMember | URL) { + return render(, withClientContextRenderOptions(client)); + } - jest.spyOn(room, "getLiveTimeline").mockReturnValue({ getEvents: () => [{} as MatrixEvent] } as EventTimeline); - const { container: withEvents } = render(, getWrapper()); - expect(withEvents).toHaveTextContent(_t("share|permalink_most_recent")); + const getUrl = () => new URL("https://matrix.org/"); + const getRoomMember = () => new RoomMember(room.roomId, "@alice:example.org"); + + test.each([ + { name: "an URL", title: "Share Link", url: "https://matrix.org/", getTarget: getUrl }, + { + name: "a room member", + title: "Share User", + url: "https://matrix.to/#/@alice:example.org", + getTarget: getRoomMember, + }, + ])("should render a share dialog for $name", async ({ title, url, getTarget }) => { + const { asFragment } = renderComponent(getTarget()); + + expect(screen.getByRole("heading", { name: title })).toBeInTheDocument(); + expect(screen.getByText(url)).toBeInTheDocument(); + expect(asFragment()).toMatchSnapshot(); + + await userEvent.click(screen.getByRole("button", { name: "Copy link" })); + expect(copyTextFunc).toHaveBeenCalledWith(url); }); - it("renders user share dialog", () => { - mockRoomMembers(room, 1); - const { container } = render( - , - getWrapper(), + it("should render a share dialog for a room", async () => { + const expectedURL = "https://matrix.to/#/!1:example.org"; + jest.spyOn(room.getLiveTimeline(), "getEvents").mockReturnValue([new MatrixEvent({ event_id: "!eventId" })]); + + const { asFragment } = renderComponent(room); + expect(screen.getByRole("heading", { name: "Share Room" })).toBeInTheDocument(); + expect(screen.getByText(expectedURL)).toBeInTheDocument(); + expect(screen.getByRole("checkbox", { name: "Link to most recent message" })).toBeInTheDocument(); + expect(asFragment()).toMatchSnapshot(); + + await userEvent.click(screen.getByRole("button", { name: "Copy link" })); + expect(copyTextFunc).toHaveBeenCalledWith(expectedURL); + + // Click on the checkbox to link to the most recent message + await userEvent.click(screen.getByRole("checkbox", { name: "Link to most recent message" })); + const newExpectedURL = "https://matrix.to/#/!1:example.org/!eventId"; + expect(screen.getByText(newExpectedURL)).toBeInTheDocument(); + }); + + it("should render a share dialog for a matrix event", async () => { + const matrixEvent = new MatrixEvent({ event_id: "!eventId" }); + const permalinkCreator = new RoomPermalinkCreator(room); + const expectedURL = "https://matrix.to/#/!1:example.org/!eventId"; + + const { asFragment } = render( + , + withClientContextRenderOptions(client), ); - expect(container).toHaveTextContent(_t("share|title_user")); + expect(screen.getByRole("heading", { name: "Share Room Message" })).toBeInTheDocument(); + expect(screen.getByText(expectedURL)).toBeInTheDocument(); + expect(screen.getByRole("checkbox", { name: "Link to selected message" })).toBeChecked(); + expect(asFragment()).toMatchSnapshot(); + + await userEvent.click(screen.getByRole("button", { name: "Copy link" })); + expect(copyTextFunc).toHaveBeenCalledWith(expectedURL); + + // Click on the checkbox to link to the room + await userEvent.click(screen.getByRole("checkbox", { name: "Link to selected message" })); + expect(screen.getByText("https://matrix.to/#/!1:example.org")).toBeInTheDocument(); }); - it("renders link share dialog", () => { - mockRoomMembers(room, 1); - const { container } = render( - , - getWrapper(), - ); - expect(container).toHaveTextContent(_t("share|title_link")); + it("should change the copy button text when clicked", async () => { + jest.useFakeTimers(); + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + // To not be bother with rtl warnings about QR code state update + jest.spyOn(SettingsStore, "getValue").mockReturnValue(false); + + renderComponent(room); + await user.click(screen.getByRole("button", { name: "Copy link" })); + // Move after `copyPlaintext` + await jest.advanceTimersToNextTimerAsync(); + expect(screen.getByRole("button", { name: "Link copied" })).toBeInTheDocument(); + + // 2 sec after the button should be back to normal + act(() => jest.advanceTimersByTime(2000)); + await waitFor(() => expect(screen.getByRole("button", { name: "Copy link" })).toBeInTheDocument()); }); - it("renders the QR code if configured", () => { + it("should not render the QR code if disabled", () => { const originalGetValue = SettingsStore.getValue; jest.spyOn(SettingsStore, "getValue").mockImplementation((feature) => { - if (feature === UIFeature.ShareQRCode) return true; + if (feature === UIFeature.ShareQRCode) return false; return originalGetValue(feature); }); - const { container } = render(, getWrapper()); - const qrCodesVisible = container.getElementsByClassName("mx_ShareDialog_qrcode_container").length > 0; - expect(qrCodesVisible).toBe(true); + + const { asFragment } = renderComponent(room); + expect(screen.queryByRole("img", { name: "QR code" })).toBeNull(); + expect(asFragment()).toMatchSnapshot(); }); - it("renders the social button if configured", () => { + it("should not render the socials if disabled", () => { const originalGetValue = SettingsStore.getValue; jest.spyOn(SettingsStore, "getValue").mockImplementation((feature) => { - if (feature === UIFeature.ShareSocial) return true; + if (feature === UIFeature.ShareSocial) return false; return originalGetValue(feature); }); - const { container } = render(, getWrapper()); - const qrCodesVisible = container.getElementsByClassName("mx_ShareDialog_social_container").length > 0; - expect(qrCodesVisible).toBe(true); - }); - it("renders custom title and subtitle", () => { - const { container } = render( - , - getWrapper(), - ); - expect(container).toHaveTextContent("test_title_123"); - expect(container).toHaveTextContent("custom_subtitle_1234"); + + const { asFragment } = renderComponent(room); + expect(screen.queryByRole("link", { name: "Reddit" })).toBeNull(); + expect(asFragment()).toMatchSnapshot(); }); }); -/** - * - * @param count the number of users to create - */ -function mockRoomMembers(room: Room, count: number) { - const members = Array(count) - .fill(0) - .map((_, index) => new RoomMember(room.roomId, "@alice:example.org")); - - room.currentState.setJoinedMemberCount(members.length); - room.getJoinedMembers = jest.fn().mockReturnValue(members); -} diff --git a/test/unit-tests/components/views/dialogs/__snapshots__/ShareDialog-test.tsx.snap b/test/unit-tests/components/views/dialogs/__snapshots__/ShareDialog-test.tsx.snap new file mode 100644 index 0000000000..ab8b8ffb58 --- /dev/null +++ b/test/unit-tests/components/views/dialogs/__snapshots__/ShareDialog-test.tsx.snap @@ -0,0 +1,852 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ShareDialog should not render the QR code if disabled 1`] = ` + +
+