From a45a662c579e35f96e822c5075e2f53d5af8c586 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 13 Oct 2022 19:11:30 +0100 Subject: [PATCH 1/4] Enable Cypress retries to combat flakiness (#9413) --- .percy.yml | 2 + cypress.config.ts | 4 ++ package.json | 4 +- yarn.lock | 166 +++++++++++++++++++++++----------------------- 4 files changed, 91 insertions(+), 85 deletions(-) diff --git a/.percy.yml b/.percy.yml index e50f0b0dbb..deca7f58f7 100644 --- a/.percy.yml +++ b/.percy.yml @@ -3,3 +3,5 @@ snapshot: widths: - 1024 - 1920 +percy: + defer-uploads: true diff --git a/cypress.config.ts b/cypress.config.ts index 9236ee2931..bc64b7d726 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -30,4 +30,8 @@ export default defineConfig({ experimentalSessionAndOrigin: true, specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}', }, + retries: { + runMode: 4, + openMode: 0, + }, }); diff --git a/package.json b/package.json index f5c817ffd8..164fb40674 100644 --- a/package.json +++ b/package.json @@ -136,8 +136,8 @@ "@babel/traverse": "^7.12.12", "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz", "@peculiar/webcrypto": "^1.1.4", - "@percy/cli": "^1.3.0", - "@percy/cypress": "^3.1.1", + "@percy/cli": "^1.11.0", + "@percy/cypress": "^3.1.2", "@sentry/types": "^6.10.0", "@sinonjs/fake-timers": "^9.1.2", "@testing-library/jest-dom": "^5.16.5", diff --git a/yarn.lock b/yarn.lock index 154f58e620..ab871f523f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1830,105 +1830,105 @@ tslib "^2.4.0" webcrypto-core "^1.7.4" -"@percy/cli-app@1.10.0": - version "1.10.0" - resolved "https://registry.yarnpkg.com/@percy/cli-app/-/cli-app-1.10.0.tgz#01dec25405bac83b4a9e8b652f623dc75af5468e" - integrity sha512-vREIM8WA07m+U/x0yA2dEGjZOPZtLcdRZd+N7/Nhcgp4dfq693wdPlJZTlVEx09nZR083iDuzYAy7SAH9LNjEA== +"@percy/cli-app@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@percy/cli-app/-/cli-app-1.11.0.tgz#aedf03af91bf66efaf9daacb9ed405c1fdb4376d" + integrity sha512-uZG/38nZYQQvD5mMUckgdHIVvuz/quV6JqEGDMKhDdgehX+Q1csHEeb/PXBGxLny7Ud1+s+8g9ZYm4oca87OTA== dependencies: - "@percy/cli-command" "1.10.0" - "@percy/cli-exec" "1.10.0" + "@percy/cli-command" "1.11.0" + "@percy/cli-exec" "1.11.0" -"@percy/cli-build@1.10.0": - version "1.10.0" - resolved "https://registry.yarnpkg.com/@percy/cli-build/-/cli-build-1.10.0.tgz#6075ce942a98949db53e7427369b8ab025e53ae3" - integrity sha512-dWK3uWYbyXFPk4goDll53UBmPtiEmx4tNYH3zKFKW13eke3rk8SBwtDrYW+Cd8vy/mPTGRqazNLQ2DXKaunZpw== +"@percy/cli-build@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@percy/cli-build/-/cli-build-1.11.0.tgz#1a93b96499b3b30adb086ef1f59dacd973d10c04" + integrity sha512-KvWnlP/2crZFCkzkWFIdsBPMeg69Kye23WFe4sLtoAIrid6o7qIwk6285Iijsc4uJm4Y19jgXRR/EsVz5FYUNw== dependencies: - "@percy/cli-command" "1.10.0" + "@percy/cli-command" "1.11.0" -"@percy/cli-command@1.10.0": - version "1.10.0" - resolved "https://registry.yarnpkg.com/@percy/cli-command/-/cli-command-1.10.0.tgz#f4c73bcd75552b05bbdb3e87c59ff7519441f2a8" - integrity sha512-isSVsHXvJtbJqToEPewtA13HqR7xT+4FnYE5c45NGKBKgi1CqoZNtXdvZG4Qq/AsQp2McEBmN2zfadyBHcwZ7g== +"@percy/cli-command@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@percy/cli-command/-/cli-command-1.11.0.tgz#db281e2b6d24d9172e0c49aa17d08f6524a7b8a1" + integrity sha512-5f4/FydmLzn82INMzfPhzq43uYBCIQv2ZCHK9hxyfc0qA6VUBc7gY+zwNp7hHgW7nAbWcDMxUqJrF9sts/BfqA== dependencies: - "@percy/config" "1.10.0" - "@percy/core" "1.10.0" - "@percy/logger" "1.10.0" + "@percy/config" "1.11.0" + "@percy/core" "1.11.0" + "@percy/logger" "1.11.0" -"@percy/cli-config@1.10.0": - version "1.10.0" - resolved "https://registry.yarnpkg.com/@percy/cli-config/-/cli-config-1.10.0.tgz#9883068d5235b86138692e5abbff8d35c6c01007" - integrity sha512-g0FTSmvSxvcFmHe4oqtOuj/vn590N6v+4+kxjIRCvWEPUK/JFyotvQvutCpbmVR9s1LCWEQ5MBjxuCbTdotIZA== +"@percy/cli-config@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@percy/cli-config/-/cli-config-1.11.0.tgz#9ea8112d8c38f5ae641393707d2d3aa4cc7dca45" + integrity sha512-hKxusrHMkUVn+Hvv/Vjo6SadqFlwXlkLFDGCNE8DvuEsP9YEALUZQq7/i+iQJAC7JuV4UsEnOOKuCTD+rS2xUQ== dependencies: - "@percy/cli-command" "1.10.0" + "@percy/cli-command" "1.11.0" -"@percy/cli-exec@1.10.0": - version "1.10.0" - resolved "https://registry.yarnpkg.com/@percy/cli-exec/-/cli-exec-1.10.0.tgz#38f349788bd7d38dde8306780e79c35c25e1e9d8" - integrity sha512-EIUbQwEELNyuFNdjHD7Q7yGnVFsYzan9mplwxj4wq9xar5qd64fYusjJBGZygCKxT+WkoSokbODaTXoACoKoqw== +"@percy/cli-exec@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@percy/cli-exec/-/cli-exec-1.11.0.tgz#4013a632441acb410148501fc5488e39b326c45a" + integrity sha512-y8C6s9q0QOmIuPucFjdn1oeJGiLaOlP55hQHeiXka/J84zBHw6N2vSwEqvdzHH2QY/VHLyIRC9NTBNNISv8ayQ== dependencies: - "@percy/cli-command" "1.10.0" + "@percy/cli-command" "1.11.0" cross-spawn "^7.0.3" which "^2.0.2" -"@percy/cli-snapshot@1.10.0": - version "1.10.0" - resolved "https://registry.yarnpkg.com/@percy/cli-snapshot/-/cli-snapshot-1.10.0.tgz#2a5cc9ea4a11b773298282632c9b5fe6abf9114b" - integrity sha512-myZy9wqLumKOWsnondTrBW0EUayHG6v4WT1ENBoFGHP3Bv0jxDwbs1RWEeQqa0NsooNHCWajd11Pr9+RS5w+TA== +"@percy/cli-snapshot@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@percy/cli-snapshot/-/cli-snapshot-1.11.0.tgz#ef7ba8aca26e03b1da6157e162ab00e87c8d7355" + integrity sha512-PUh6RXg91p0MHKMTv/btIdMjqn5R0KXz32SkKeQ4gVI2bPEWnsK5aeJaPGtpDzrt35cG7wpKtzF0uGmovIKpRg== dependencies: - "@percy/cli-command" "1.10.0" + "@percy/cli-command" "1.11.0" yaml "^2.0.0" -"@percy/cli-upload@1.10.0": - version "1.10.0" - resolved "https://registry.yarnpkg.com/@percy/cli-upload/-/cli-upload-1.10.0.tgz#db12afe7183b9e63f52c1684bb647fef94eb48e4" - integrity sha512-sApNzAUiqGuZb/DeKrsMI09XglUKxhHGdyW4YmnQBznnHJjE5xOaVjtJr7zfI6RSNhtofCWLqyH08Pf+iE9rBg== +"@percy/cli-upload@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@percy/cli-upload/-/cli-upload-1.11.0.tgz#60a85665f8ed6897c88793c70cd66a9476a94a4e" + integrity sha512-oI7zXU6EVukCWPFT3UXxd2XkRGDIGoPkv+beS157WrR+y3i8/zzp9V3r0UIMaL5gbOwY05TBHEogfqZht5hUXQ== dependencies: - "@percy/cli-command" "1.10.0" + "@percy/cli-command" "1.11.0" fast-glob "^3.2.11" image-size "^1.0.0" -"@percy/cli@^1.3.0": - version "1.10.0" - resolved "https://registry.yarnpkg.com/@percy/cli/-/cli-1.10.0.tgz#fbeeadc7b8baeadf637e3ac30ea65df3b2b60b2f" - integrity sha512-t/2vKCQ8bV5Rrut4lR1/xtM8UnZv5aa45XYZ0ZzGR6tDQsN+GOmgiH9stFiMp6xHaj/iVHpgAngBL8Ksm/ynGg== +"@percy/cli@^1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@percy/cli/-/cli-1.11.0.tgz#68709ebc4ea1ccddce607374c61d1ad9c9a2a44c" + integrity sha512-V6tIghu70uO1jQY6AJSbll6GMFZ26jkubgAnK4+KWa4g3hYRra7JvsSYkLlOE93x9L7Z7ZUbSTfhlpXGmh2UFA== dependencies: - "@percy/cli-app" "1.10.0" - "@percy/cli-build" "1.10.0" - "@percy/cli-command" "1.10.0" - "@percy/cli-config" "1.10.0" - "@percy/cli-exec" "1.10.0" - "@percy/cli-snapshot" "1.10.0" - "@percy/cli-upload" "1.10.0" - "@percy/client" "1.10.0" - "@percy/logger" "1.10.0" + "@percy/cli-app" "1.11.0" + "@percy/cli-build" "1.11.0" + "@percy/cli-command" "1.11.0" + "@percy/cli-config" "1.11.0" + "@percy/cli-exec" "1.11.0" + "@percy/cli-snapshot" "1.11.0" + "@percy/cli-upload" "1.11.0" + "@percy/client" "1.11.0" + "@percy/logger" "1.11.0" -"@percy/client@1.10.0": - version "1.10.0" - resolved "https://registry.yarnpkg.com/@percy/client/-/client-1.10.0.tgz#efe8727b08dbe1590c971810ceaf9bcd54cea8fa" - integrity sha512-Dc37kyXAg9O4ttJEUycduY8U6KDLiH5qWAJIBnSg+C2WSzFc6jv4sa9vowz5B/nUQ//Iq6mue00WIYRUyyg8Ww== +"@percy/client@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@percy/client/-/client-1.11.0.tgz#ac530ac5204196ee2bd8c0acbbf4ef0561f104a3" + integrity sha512-RyvPK7xXfP8kgu04KydCaGWevQUM2oeVZ3Pf/u0FKZQ/OUSTUugIPN3e67ersmoiCUw3TWVy/+UeM5BBB3zLfg== dependencies: - "@percy/env" "1.10.0" - "@percy/logger" "1.10.0" + "@percy/env" "1.11.0" + "@percy/logger" "1.11.0" -"@percy/config@1.10.0": - version "1.10.0" - resolved "https://registry.yarnpkg.com/@percy/config/-/config-1.10.0.tgz#cba859fe85f865216adb468c121b97d88ed72ab9" - integrity sha512-/UEulUsyObSQYQlWw3rjE3NBOjLF66HsPgXr7n6DBCpyVf6vD0OZD+1FGb8Dyi7uuzUTpmsOw0ij7mrjsXv83A== +"@percy/config@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@percy/config/-/config-1.11.0.tgz#35b335fd2698c39652a0688b7b4fc016336121cf" + integrity sha512-acpIqqH2hm8Aa96FL7FSfvMEFRpYC62lIia702XIZ0+IJZ0+SOH7DzhnyhyNf8OHMBQZWkxwkYlcdKUxT8KmaA== dependencies: - "@percy/logger" "1.10.0" + "@percy/logger" "1.11.0" ajv "^8.6.2" cosmiconfig "^7.0.0" yaml "^2.0.0" -"@percy/core@1.10.0": - version "1.10.0" - resolved "https://registry.yarnpkg.com/@percy/core/-/core-1.10.0.tgz#96cc1b43c5149bda86d719405e847f8c83067bd6" - integrity sha512-NU5gWcJ8655MFTkg1KgVTXEg8DXClMIh2ITmKM1XNH95wABEKosKKwggHUr8fcfNgZuEXy5a8tnfT8JZzyXX+A== +"@percy/core@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@percy/core/-/core-1.11.0.tgz#20d7068e37be4a7fda2cd7f10971eeab878d8e7a" + integrity sha512-IM94vccJEFzifH9DjL57S1DIgmF+ew0649oLQCIz19BhdcF9jsrOLHBSd0fwv+ftIAktzaNTThSlm/zREndEew== dependencies: - "@percy/client" "1.10.0" - "@percy/config" "1.10.0" - "@percy/dom" "1.10.0" - "@percy/logger" "1.10.0" + "@percy/client" "1.11.0" + "@percy/config" "1.11.0" + "@percy/dom" "1.11.0" + "@percy/logger" "1.11.0" content-disposition "^0.5.4" cross-spawn "^7.0.3" extract-zip "^2.0.1" @@ -1939,27 +1939,27 @@ rimraf "^3.0.2" ws "^8.0.0" -"@percy/cypress@^3.1.1": +"@percy/cypress@^3.1.2": version "3.1.2" resolved "https://registry.yarnpkg.com/@percy/cypress/-/cypress-3.1.2.tgz#a087d3c59a6b155eab5fdb4c237526b9cfacbc22" integrity sha512-JXrGDZbqwkzQd2h5T5D7PvqoucNaiMh4ChPp8cLQiEtRuLHta9nf1lEuXH+jnatGL2j+3jJFIHJ0L7XrgVnvQA== dependencies: "@percy/sdk-utils" "^1.3.1" -"@percy/dom@1.10.0": - version "1.10.0" - resolved "https://registry.yarnpkg.com/@percy/dom/-/dom-1.10.0.tgz#879d94fde1d5ae63f5dbb96b1a75e48ba8ca5525" - integrity sha512-aHCy+Vk8xc3azFDPSV4Z3+wiO/bp9OlGfi8aNwa6fpuEIx0SMN8TyLVGaKTwIlrhDVEqSbmTYsrh67HS+Uweqg== +"@percy/dom@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@percy/dom/-/dom-1.11.0.tgz#998080c3c3b5160eb1c58e8543ebb89ed0ca63a1" + integrity sha512-WNbMcMTy+HaSWGmW20NArG+nUnTMYcjCsLK1m3RqXvLSQMEH16olUV5YSIRV8YCPD/L6/2gZ8/YgV7bnKbFzxQ== -"@percy/env@1.10.0": - version "1.10.0" - resolved "https://registry.yarnpkg.com/@percy/env/-/env-1.10.0.tgz#79af82e30ed98c94162f1531705f8a134773cb54" - integrity sha512-//yfh7N++ncP/K7+zacLm8PoPVFJ1tL3hc/COzP2YWLjMcLBGDtjIWZvTLk09PnEzkZ+hGLZ06AJeEzQiixhyA== +"@percy/env@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@percy/env/-/env-1.11.0.tgz#002cc369d93a4cf9a8ceb2e71aa7cbc2d5faa288" + integrity sha512-aiAjyQUJlDinwCyxr9bujZY/BjyaIY0s5jfW2j3C+1HJ4uDi7CN1qb/+TqBhMO/2AEjR4eLIGRpBE3xSyO+Liw== -"@percy/logger@1.10.0": - version "1.10.0" - resolved "https://registry.yarnpkg.com/@percy/logger/-/logger-1.10.0.tgz#34ccccfb2949bd37bba3b23a462f3d7b4dcc8654" - integrity sha512-4t3V/Qlyup9mDAkf1KfENjaFVYcXVgXWeVasNRGYX5HBDbFfRB7G00uAfgK2Ja+QQGBmcY3ZA4o6+OXY88AjkQ== +"@percy/logger@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@percy/logger/-/logger-1.11.0.tgz#0decfb64bd399925b8a4edbe1dc17186bb631e00" + integrity sha512-CQZRvOmp67VFIx9hYN6Z9cMCU8oAqwG/3CpWnvpyUmWWIbzuVmwA4dk2F8AOnAXADtr09jVhN60sPzqhliQFRQ== "@percy/sdk-utils@^1.3.1": version "1.10.0" From 3a39dfc8516a7e55fc8799c1db7d8d4d05083c0d Mon Sep 17 00:00:00 2001 From: Kerry Date: Thu, 13 Oct 2022 21:33:55 +0200 Subject: [PATCH 2/4] Cypress - fix device manager test (#9408) --- cypress/e2e/settings/device-management.spec.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/cypress/e2e/settings/device-management.spec.ts b/cypress/e2e/settings/device-management.spec.ts index c3ef4db838..67b05dd7c6 100644 --- a/cypress/e2e/settings/device-management.spec.ts +++ b/cypress/e2e/settings/device-management.spec.ts @@ -107,9 +107,6 @@ describe("Device manager", () => { cy.get('[data-testid="device-detail-sign-out-cta"]').click(); }); - // list updated after sign out - cy.get('.mx_FilteredDeviceList_list').find('.mx_FilteredDeviceList_listItem').should('have.length', 1); - // no other sessions or security recommendations sections when only one session cy.contains('Other sessions').should('not.exist'); cy.get('[data-testid="security-recommendations-section"]').should('not.exist'); From 49d9e7523589d400bee36e20521ec22458c91db4 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Fri, 14 Oct 2022 06:13:17 +0200 Subject: [PATCH 3/4] Voice Broadcast playback UI (#9362) * Implement Voice Broadcast UI * Update src/voice-broadcast/models/VoiceBroadcastPlayback.ts Co-authored-by: Travis Ralston Co-authored-by: Travis Ralston --- res/css/_components.pcss | 2 + .../atoms/_PlaybackControlButton.pcss | 25 ++++ .../_VoiceBroadcastPlaybackBody.pcss | 27 ++++ src/components/atoms/Icon.tsx | 6 + .../views/messages/MessageEvent.tsx | 5 +- src/i18n/strings/en_EN.json | 2 + .../components/VoiceBroadcastBody.tsx | 29 +++- .../atoms/PlaybackControlButton.tsx | 53 ++++++++ .../molecules/VoiceBroadcastPlaybackBody.tsx | 56 ++++++++ .../hooks/useVoiceBroadcastPlayback.ts | 49 +++++++ src/voice-broadcast/index.ts | 7 +- .../models/VoiceBroadcastPlayback.ts | 76 +++++++++++ .../stores/VoiceBroadcastPlaybacksStore.ts | 71 ++++++++++ ...uldDisplayAsVoiceBroadcastRecordingTile.ts | 30 ++++ .../components/VoiceBroadcastBody-test.tsx | 45 +++++- .../atoms/PlaybackControlButton-test.tsx | 45 ++++++ .../PlaybackControlButton-test.tsx.snap | 55 ++++++++ .../VoiceBroadcastPlaybackBody-test.tsx | 69 ++++++++++ .../VoiceBroadcastPlaybackBody-test.tsx.snap | 76 +++++++++++ .../models/VoiceBroadcastPlayback-test.ts | 116 ++++++++++++++++ .../VoiceBroadcastPlaybacksStore-test.ts | 128 ++++++++++++++++++ ...splayAsVoiceBroadcastRecordingTile-test.ts | 98 ++++++++++++++ 22 files changed, 1064 insertions(+), 6 deletions(-) create mode 100644 res/css/voice-broadcast/atoms/_PlaybackControlButton.pcss create mode 100644 res/css/voice-broadcast/molecules/_VoiceBroadcastPlaybackBody.pcss create mode 100644 src/voice-broadcast/components/atoms/PlaybackControlButton.tsx create mode 100644 src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx create mode 100644 src/voice-broadcast/hooks/useVoiceBroadcastPlayback.ts create mode 100644 src/voice-broadcast/models/VoiceBroadcastPlayback.ts create mode 100644 src/voice-broadcast/stores/VoiceBroadcastPlaybacksStore.ts create mode 100644 src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastRecordingTile.ts create mode 100644 test/voice-broadcast/components/atoms/PlaybackControlButton-test.tsx create mode 100644 test/voice-broadcast/components/atoms/__snapshots__/PlaybackControlButton-test.tsx.snap create mode 100644 test/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody-test.tsx create mode 100644 test/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastPlaybackBody-test.tsx.snap create mode 100644 test/voice-broadcast/models/VoiceBroadcastPlayback-test.ts create mode 100644 test/voice-broadcast/stores/VoiceBroadcastPlaybacksStore-test.ts create mode 100644 test/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastRecordingTile-test.ts diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 966caed4cc..577d50f629 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -365,5 +365,7 @@ @import "./views/voip/_PiPContainer.pcss"; @import "./views/voip/_VideoFeed.pcss"; @import "./voice-broadcast/atoms/_LiveBadge.pcss"; +@import "./voice-broadcast/atoms/_PlaybackControlButton.pcss"; @import "./voice-broadcast/atoms/_VoiceBroadcastHeader.pcss"; +@import "./voice-broadcast/molecules/_VoiceBroadcastPlaybackBody.pcss"; @import "./voice-broadcast/molecules/_VoiceBroadcastRecordingBody.pcss"; diff --git a/res/css/voice-broadcast/atoms/_PlaybackControlButton.pcss b/res/css/voice-broadcast/atoms/_PlaybackControlButton.pcss new file mode 100644 index 0000000000..fc4c1386b6 --- /dev/null +++ b/res/css/voice-broadcast/atoms/_PlaybackControlButton.pcss @@ -0,0 +1,25 @@ +/* +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_BroadcastPlaybackControlButton { + align-items: center; + background-color: $background; + border-radius: 50%; + display: flex; + height: 32px; + justify-content: center; + width: 32px; +} diff --git a/res/css/voice-broadcast/molecules/_VoiceBroadcastPlaybackBody.pcss b/res/css/voice-broadcast/molecules/_VoiceBroadcastPlaybackBody.pcss new file mode 100644 index 0000000000..11921e1f95 --- /dev/null +++ b/res/css/voice-broadcast/molecules/_VoiceBroadcastPlaybackBody.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_VoiceBroadcastPlaybackBody { + background-color: $quinary-content; + border-radius: 8px; + display: inline-block; + padding: 12px; +} + +.mx_VoiceBroadcastPlaybackBody_controls { + display: flex; + justify-content: center; +} diff --git a/src/components/atoms/Icon.tsx b/src/components/atoms/Icon.tsx index 5778022764..9f5f3e2d3c 100644 --- a/src/components/atoms/Icon.tsx +++ b/src/components/atoms/Icon.tsx @@ -17,13 +17,19 @@ limitations under the License. import React from "react"; import liveIcon from "../../../res/img/element-icons/live.svg"; +import pauseIcon from "../../../res/img/element-icons/pause.svg"; +import playIcon from "../../../res/img/element-icons/play.svg"; export enum IconType { Live, + Pause, + Play, } const iconTypeMap = new Map([ [IconType.Live, liveIcon], + [IconType.Pause, pauseIcon], + [IconType.Play, playIcon], ]); export enum IconColour { diff --git a/src/components/views/messages/MessageEvent.tsx b/src/components/views/messages/MessageEvent.tsx index 4e09bfac49..91807d568f 100644 --- a/src/components/views/messages/MessageEvent.tsx +++ b/src/components/views/messages/MessageEvent.tsx @@ -58,6 +58,10 @@ interface IProps extends Omit>>([ [M_POLL_START.altName, MPollBody], [M_BEACON_INFO.name, MBeaconBody], [M_BEACON_INFO.altName, MBeaconBody], - [VoiceBroadcastInfoEventType, VoiceBroadcastBody], ]); export default class MessageEvent extends React.Component implements IMediaBody, IOperableEventTile { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 48d7e9b8a3..2a8b398514 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -638,6 +638,8 @@ "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", + "pause voice broadcast": "pause voice broadcast", + "resume voice broadcast": "resume voice broadcast", "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", diff --git a/src/voice-broadcast/components/VoiceBroadcastBody.tsx b/src/voice-broadcast/components/VoiceBroadcastBody.tsx index 3be79ca882..b90448de8a 100644 --- a/src/voice-broadcast/components/VoiceBroadcastBody.tsx +++ b/src/voice-broadcast/components/VoiceBroadcastBody.tsx @@ -15,19 +15,42 @@ limitations under the License. */ import React from "react"; +import { MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix"; import { VoiceBroadcastRecordingBody, VoiceBroadcastRecordingsStore, + shouldDisplayAsVoiceBroadcastRecordingTile, + VoiceBroadcastInfoEventType, + VoiceBroadcastPlaybacksStore, + VoiceBroadcastPlaybackBody, + VoiceBroadcastInfoState, } from ".."; import { IBodyProps } from "../../components/views/messages/IBodyProps"; import { MatrixClientPeg } from "../../MatrixClientPeg"; export const VoiceBroadcastBody: React.FC = ({ mxEvent }) => { const client = MatrixClientPeg.get(); - const recording = VoiceBroadcastRecordingsStore.instance().getByInfoEvent(mxEvent, client); + const room = client.getRoom(mxEvent.getRoomId()); + const relations = room?.getUnfilteredTimelineSet()?.relations?.getChildEventsForEvent( + mxEvent.getId(), + RelationType.Reference, + VoiceBroadcastInfoEventType, + ); + const relatedEvents = relations?.getRelations(); + const state = !relatedEvents?.find((event: MatrixEvent) => { + return event.getContent()?.state === VoiceBroadcastInfoState.Stopped; + }) ? VoiceBroadcastInfoState.Started : VoiceBroadcastInfoState.Stopped; - return ; + } + + const playback = VoiceBroadcastPlaybacksStore.instance().getByInfoEvent(mxEvent); + return ; }; diff --git a/src/voice-broadcast/components/atoms/PlaybackControlButton.tsx b/src/voice-broadcast/components/atoms/PlaybackControlButton.tsx new file mode 100644 index 0000000000..b67e6b3e24 --- /dev/null +++ b/src/voice-broadcast/components/atoms/PlaybackControlButton.tsx @@ -0,0 +1,53 @@ +/* +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 { VoiceBroadcastPlaybackState } from "../.."; +import { Icon, IconColour, IconType } from "../../../components/atoms/Icon"; +import AccessibleButton from "../../../components/views/elements/AccessibleButton"; +import { _t } from "../../../languageHandler"; + +const stateIconMap = new Map([ + [VoiceBroadcastPlaybackState.Playing, IconType.Pause], + [VoiceBroadcastPlaybackState.Paused, IconType.Play], + [VoiceBroadcastPlaybackState.Stopped, IconType.Play], +]); + +interface Props { + onClick: () => void; + state: VoiceBroadcastPlaybackState; +} + +export const PlaybackControlButton: React.FC = ({ + onClick, + state, +}) => { + const ariaLabel = state === VoiceBroadcastPlaybackState.Playing + ? _t("pause voice broadcast") + : _t("resume voice broadcast"); + + return + + ; +}; diff --git a/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx b/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx new file mode 100644 index 0000000000..6ebc67ee63 --- /dev/null +++ b/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx @@ -0,0 +1,56 @@ +/* +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 { + PlaybackControlButton, + VoiceBroadcastHeader, + VoiceBroadcastPlayback, +} from "../.."; +import { useVoiceBroadcastPlayback } from "../../hooks/useVoiceBroadcastPlayback"; + +interface VoiceBroadcastPlaybackBodyProps { + playback: VoiceBroadcastPlayback; +} + +export const VoiceBroadcastPlaybackBody: React.FC = ({ + playback, +}) => { + const { + roomName, + sender, + toggle, + playbackState, + } = useVoiceBroadcastPlayback(playback); + + return ( +
+ +
+ +
+
+ ); +}; diff --git a/src/voice-broadcast/hooks/useVoiceBroadcastPlayback.ts b/src/voice-broadcast/hooks/useVoiceBroadcastPlayback.ts new file mode 100644 index 0000000000..25fcb93d51 --- /dev/null +++ b/src/voice-broadcast/hooks/useVoiceBroadcastPlayback.ts @@ -0,0 +1,49 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { useState } from "react"; + +import { useTypedEventEmitter } from "../../hooks/useEventEmitter"; +import { MatrixClientPeg } from "../../MatrixClientPeg"; +import { + VoiceBroadcastPlayback, + VoiceBroadcastPlaybackEvent, + VoiceBroadcastPlaybackState, +} from ".."; + +export const useVoiceBroadcastPlayback = (playback: VoiceBroadcastPlayback) => { + const client = MatrixClientPeg.get(); + const room = client.getRoom(playback.infoEvent.getRoomId()); + const playbackToggle = () => { + playback.toggle(); + }; + + const [playbackState, setPlaybackState] = useState(playback.getState()); + useTypedEventEmitter( + playback, + VoiceBroadcastPlaybackEvent.StateChanged, + (state: VoiceBroadcastPlaybackState, _playback: VoiceBroadcastPlayback) => { + setPlaybackState(state); + }, + ); + + return { + roomName: room.name, + sender: playback.infoEvent.sender, + toggle: playbackToggle, + playbackState, + }; +}; diff --git a/src/voice-broadcast/index.ts b/src/voice-broadcast/index.ts index 12027c9b42..2a3bb6573f 100644 --- a/src/voice-broadcast/index.ts +++ b/src/voice-broadcast/index.ts @@ -21,13 +21,18 @@ limitations under the License. import { RelationType } from "matrix-js-sdk/src/matrix"; +export * from "./models/VoiceBroadcastPlayback"; +export * from "./models/VoiceBroadcastRecording"; export * from "./audio/VoiceBroadcastRecorder"; export * from "./components/VoiceBroadcastBody"; export * from "./components/atoms/LiveBadge"; +export * from "./components/atoms/PlaybackControlButton"; export * from "./components/atoms/VoiceBroadcastHeader"; +export * from "./components/molecules/VoiceBroadcastPlaybackBody"; export * from "./components/molecules/VoiceBroadcastRecordingBody"; -export * from "./models/VoiceBroadcastRecording"; +export * from "./stores/VoiceBroadcastPlaybacksStore"; export * from "./stores/VoiceBroadcastRecordingsStore"; +export * from "./utils/shouldDisplayAsVoiceBroadcastRecordingTile"; export * from "./utils/shouldDisplayAsVoiceBroadcastTile"; export * from "./utils/startNewVoiceBroadcastRecording"; export * from "./hooks/useVoiceBroadcastRecording"; diff --git a/src/voice-broadcast/models/VoiceBroadcastPlayback.ts b/src/voice-broadcast/models/VoiceBroadcastPlayback.ts new file mode 100644 index 0000000000..28bea62e10 --- /dev/null +++ b/src/voice-broadcast/models/VoiceBroadcastPlayback.ts @@ -0,0 +1,76 @@ +/* +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 { MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter"; + +import { IDestroyable } from "../../utils/IDestroyable"; + +export enum VoiceBroadcastPlaybackState { + Paused, + Playing, + Stopped, +} + +export enum VoiceBroadcastPlaybackEvent { + StateChanged = "state_changed", +} + +interface EventMap { + [VoiceBroadcastPlaybackEvent.StateChanged]: (state: VoiceBroadcastPlaybackState) => void; +} + +export class VoiceBroadcastPlayback + extends TypedEventEmitter + implements IDestroyable { + private state = VoiceBroadcastPlaybackState.Stopped; + + public constructor( + public readonly infoEvent: MatrixEvent, + ) { + super(); + } + + public start() { + this.setState(VoiceBroadcastPlaybackState.Playing); + } + + public stop() { + this.setState(VoiceBroadcastPlaybackState.Stopped); + } + + public toggle() { + if (this.state === VoiceBroadcastPlaybackState.Stopped) { + this.setState(VoiceBroadcastPlaybackState.Playing); + return; + } + + this.setState(VoiceBroadcastPlaybackState.Stopped); + } + + public getState(): VoiceBroadcastPlaybackState { + return this.state; + } + + private setState(state: VoiceBroadcastPlaybackState): void { + this.state = state; + this.emit(VoiceBroadcastPlaybackEvent.StateChanged, state); + } + + public destroy(): void { + this.removeAllListeners(); + } +} diff --git a/src/voice-broadcast/stores/VoiceBroadcastPlaybacksStore.ts b/src/voice-broadcast/stores/VoiceBroadcastPlaybacksStore.ts new file mode 100644 index 0000000000..1fdc8a9da5 --- /dev/null +++ b/src/voice-broadcast/stores/VoiceBroadcastPlaybacksStore.ts @@ -0,0 +1,71 @@ +/* +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 { MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter"; + +import { VoiceBroadcastPlayback } from ".."; + +export enum VoiceBroadcastPlaybacksStoreEvent { + CurrentChanged = "current_changed", +} + +interface EventMap { + [VoiceBroadcastPlaybacksStoreEvent.CurrentChanged]: (recording: VoiceBroadcastPlayback) => void; +} + +/** + * This store provides access to the current and specific Voice Broadcast playbacks. + */ +export class VoiceBroadcastPlaybacksStore extends TypedEventEmitter { + private current: VoiceBroadcastPlayback | null; + private playbacks = new Map(); + + public constructor() { + super(); + } + + public setCurrent(current: VoiceBroadcastPlayback): void { + if (this.current === current) return; + + this.current = current; + this.playbacks.set(current.infoEvent.getId(), current); + this.emit(VoiceBroadcastPlaybacksStoreEvent.CurrentChanged, current); + } + + public getCurrent(): VoiceBroadcastPlayback { + return this.current; + } + + public getByInfoEvent(infoEvent: MatrixEvent): VoiceBroadcastPlayback { + const infoEventId = infoEvent.getId(); + + if (!this.playbacks.has(infoEventId)) { + this.playbacks.set(infoEventId, new VoiceBroadcastPlayback(infoEvent)); + } + + return this.playbacks.get(infoEventId); + } + + public static readonly _instance = new VoiceBroadcastPlaybacksStore(); + + /** + * TODO Michael W: replace when https://github.com/matrix-org/matrix-react-sdk/pull/9293 has been merged + */ + public static instance() { + return VoiceBroadcastPlaybacksStore._instance; + } +} diff --git a/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastRecordingTile.ts b/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastRecordingTile.ts new file mode 100644 index 0000000000..b9964b6f2a --- /dev/null +++ b/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastRecordingTile.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 { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; + +import { VoiceBroadcastInfoState } from ".."; + +export const shouldDisplayAsVoiceBroadcastRecordingTile = ( + state: VoiceBroadcastInfoState, + client: MatrixClient, + event: MatrixEvent, +): boolean => { + const userId = client.getUserId(); + return !!userId + && userId === event.getSender() + && state !== VoiceBroadcastInfoState.Stopped; +}; diff --git a/test/voice-broadcast/components/VoiceBroadcastBody-test.tsx b/test/voice-broadcast/components/VoiceBroadcastBody-test.tsx index 50b4625fb6..84edf644f3 100644 --- a/test/voice-broadcast/components/VoiceBroadcastBody-test.tsx +++ b/test/voice-broadcast/components/VoiceBroadcastBody-test.tsx @@ -26,6 +26,10 @@ import { VoiceBroadcastRecordingBody, VoiceBroadcastRecordingsStore, VoiceBroadcastRecording, + shouldDisplayAsVoiceBroadcastRecordingTile, + VoiceBroadcastPlaybackBody, + VoiceBroadcastPlayback, + VoiceBroadcastPlaybacksStore, } from "../../../src/voice-broadcast"; import { mkEvent, stubClient } from "../../test-utils"; @@ -33,11 +37,20 @@ jest.mock("../../../src/voice-broadcast/components/molecules/VoiceBroadcastRecor VoiceBroadcastRecordingBody: jest.fn(), })); +jest.mock("../../../src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody", () => ({ + VoiceBroadcastPlaybackBody: jest.fn(), +})); + +jest.mock("../../../src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastRecordingTile", () => ({ + shouldDisplayAsVoiceBroadcastRecordingTile: jest.fn(), +})); + describe("VoiceBroadcastBody", () => { const roomId = "!room:example.com"; let client: MatrixClient; let infoEvent: MatrixEvent; let testRecording: VoiceBroadcastRecording; + let testPlayback: VoiceBroadcastPlayback; const mkVoiceBroadcastInfoEvent = (state: VoiceBroadcastInfoState) => { return mkEvent({ @@ -66,12 +79,19 @@ describe("VoiceBroadcastBody", () => { client = stubClient(); infoEvent = mkVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Started); testRecording = new VoiceBroadcastRecording(infoEvent, client); + testPlayback = new VoiceBroadcastPlayback(infoEvent); mocked(VoiceBroadcastRecordingBody).mockImplementation(({ recording }) => { if (testRecording === recording) { return
; } }); + mocked(VoiceBroadcastPlaybackBody).mockImplementation(({ playback }) => { + if (testPlayback === playback) { + return
; + } + }); + jest.spyOn(VoiceBroadcastRecordingsStore.instance(), "getByInfoEvent").mockImplementation( (getEvent: MatrixEvent, getClient: MatrixClient) => { if (getEvent === infoEvent && getClient === client) { @@ -79,12 +99,35 @@ describe("VoiceBroadcastBody", () => { } }, ); + + jest.spyOn(VoiceBroadcastPlaybacksStore.instance(), "getByInfoEvent").mockImplementation( + (getEvent: MatrixEvent) => { + if (getEvent === infoEvent) { + return testPlayback; + } + }, + ); }); - describe("when rendering a voice broadcast", () => { + describe("when displaying a voice broadcast recording", () => { + beforeEach(() => { + mocked(shouldDisplayAsVoiceBroadcastRecordingTile).mockReturnValue(true); + }); + it("should render a voice broadcast recording body", () => { renderVoiceBroadcast(); screen.getByTestId("voice-broadcast-recording-body"); }); }); + + describe("when displaying a voice broadcast playback", () => { + beforeEach(() => { + mocked(shouldDisplayAsVoiceBroadcastRecordingTile).mockReturnValue(false); + }); + + it("should render a voice broadcast playback body", () => { + renderVoiceBroadcast(); + screen.getByTestId("voice-broadcast-playback-body"); + }); + }); }); diff --git a/test/voice-broadcast/components/atoms/PlaybackControlButton-test.tsx b/test/voice-broadcast/components/atoms/PlaybackControlButton-test.tsx new file mode 100644 index 0000000000..f3e03d38f2 --- /dev/null +++ b/test/voice-broadcast/components/atoms/PlaybackControlButton-test.tsx @@ -0,0 +1,45 @@ +/* +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, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; + +import { PlaybackControlButton, VoiceBroadcastPlaybackState } from "../../../../src/voice-broadcast"; + +describe("PlaybackControlButton", () => { + let onClick: () => void; + + beforeEach(() => { + onClick = jest.fn(); + }); + + it.each([ + [VoiceBroadcastPlaybackState.Playing], + [VoiceBroadcastPlaybackState.Paused], + [VoiceBroadcastPlaybackState.Stopped], + ])("should render state »%s« as expected", (state: VoiceBroadcastPlaybackState) => { + const result = render(); + expect(result.container).toMatchSnapshot(); + }); + + it("should call onClick on click", async () => { + render(); + const button = screen.getByLabelText("pause voice broadcast"); + await userEvent.click(button); + expect(onClick).toHaveBeenCalled(); + }); +}); diff --git a/test/voice-broadcast/components/atoms/__snapshots__/PlaybackControlButton-test.tsx.snap b/test/voice-broadcast/components/atoms/__snapshots__/PlaybackControlButton-test.tsx.snap new file mode 100644 index 0000000000..0e674c8000 --- /dev/null +++ b/test/voice-broadcast/components/atoms/__snapshots__/PlaybackControlButton-test.tsx.snap @@ -0,0 +1,55 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PlaybackControlButton should render state »0« as expected 1`] = ` +
+
+