diff --git a/cypress/e2e/login/login.spec.ts b/cypress/e2e/login/login.spec.ts index 1058287010..32a3babced 100644 --- a/cypress/e2e/login/login.spec.ts +++ b/cypress/e2e/login/login.spec.ts @@ -21,6 +21,10 @@ import { SynapseInstance } from "../../plugins/synapsedocker"; describe("Login", () => { let synapse: SynapseInstance; + beforeEach(() => { + cy.stubDefaultServer(); + }); + afterEach(() => { cy.stopSynapse(synapse); }); diff --git a/cypress/e2e/register/register.spec.ts b/cypress/e2e/register/register.spec.ts index 98ef2bd729..dacfe08bf8 100644 --- a/cypress/e2e/register/register.spec.ts +++ b/cypress/e2e/register/register.spec.ts @@ -22,6 +22,7 @@ describe("Registration", () => { let synapse: SynapseInstance; beforeEach(() => { + cy.stubDefaultServer(); cy.visit("/#/register"); cy.startSynapse("consent").then(data => { synapse = data; diff --git a/cypress/fixtures/matrix-org-client-login.json b/cypress/fixtures/matrix-org-client-login.json new file mode 100644 index 0000000000..d7c4fde1e5 --- /dev/null +++ b/cypress/fixtures/matrix-org-client-login.json @@ -0,0 +1,48 @@ +{ + "flows": [ + { + "type": "m.login.sso", + "identity_providers": [ + { + "id": "oidc-github", + "name": "GitHub", + "icon": "mxc://matrix.org/sVesTtrFDTpXRbYfpahuJsKP", + "brand": "github" + }, + { + "id": "oidc-google", + "name": "Google", + "icon": "mxc://matrix.org/ZlnaaZNPxtUuQemvgQzlOlkz", + "brand": "google" + }, + { + "id": "oidc-gitlab", + "name": "GitLab", + "icon": "mxc://matrix.org/MCVOEmFgVieKFshPxmnejWOq", + "brand": "gitlab" + }, + { + "id": "oidc-facebook", + "name": "Facebook", + "icon": "mxc://matrix.org/nsyeLIgzxazZmJadflMAsAWG", + "brand": "facebook" + }, + { + "id": "oidc-apple", + "name": "Apple", + "icon": "mxc://matrix.org/QQKNSOdLiMHtJhzeAObmkFiU", + "brand": "apple" + } + ] + }, + { + "type": "m.login.token" + }, + { + "type": "m.login.password" + }, + { + "type": "m.login.application_service" + } + ] +} diff --git a/cypress/fixtures/matrix-org-client-versions.json b/cypress/fixtures/matrix-org-client-versions.json new file mode 100644 index 0000000000..0e0cfae33d --- /dev/null +++ b/cypress/fixtures/matrix-org-client-versions.json @@ -0,0 +1,39 @@ +{ + "versions": [ + "r0.0.1", + "r0.1.0", + "r0.2.0", + "r0.3.0", + "r0.4.0", + "r0.5.0", + "r0.6.0", + "r0.6.1", + "v1.1", + "v1.2", + "v1.3", + "v1.4" + ], + "unstable_features": { + "org.matrix.label_based_filtering": true, + "org.matrix.e2e_cross_signing": true, + "org.matrix.msc2432": true, + "uk.half-shot.msc2666.mutual_rooms": true, + "io.element.e2ee_forced.public": false, + "io.element.e2ee_forced.private": false, + "io.element.e2ee_forced.trusted_private": false, + "org.matrix.msc3026.busy_presence": false, + "org.matrix.msc2285.stable": true, + "org.matrix.msc3827.stable": true, + "org.matrix.msc2716": false, + "org.matrix.msc3030": false, + "org.matrix.msc3440.stable": true, + "org.matrix.msc3771": true, + "org.matrix.msc3773": false, + "fi.mau.msc2815": false, + "org.matrix.msc3882": false, + "org.matrix.msc3881": false, + "org.matrix.msc3874": false, + "org.matrix.msc3886": false, + "org.matrix.msc3912": false + } + } diff --git a/cypress/fixtures/matrix-org-client-well-known.json b/cypress/fixtures/matrix-org-client-well-known.json new file mode 100644 index 0000000000..ed726e2421 --- /dev/null +++ b/cypress/fixtures/matrix-org-client-well-known.json @@ -0,0 +1,8 @@ +{ + "m.homeserver": { + "base_url": "https://matrix-client.matrix.org" + }, + "m.identity_server": { + "base_url": "https://vector.im" + } +} diff --git a/cypress/fixtures/vector-im-identity-v1.json b/cypress/fixtures/vector-im-identity-v1.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/cypress/fixtures/vector-im-identity-v1.json @@ -0,0 +1 @@ +{} diff --git a/cypress/support/network.ts b/cypress/support/network.ts index 73df049c6c..238c847184 100644 --- a/cypress/support/network.ts +++ b/cypress/support/network.ts @@ -20,10 +20,12 @@ declare global { // eslint-disable-next-line @typescript-eslint/no-namespace namespace Cypress { interface Chainable { - // Intercept all /_matrix/ networking requests for the logged in user and fail them + // Intercept all /_matrix/ networking requests for the logged-in user and fail them goOffline(): void; // Remove intercept on all /_matrix/ networking requests goOnline(): void; + // Intercept calls to vector.im/matrix.org so a login page can be shown offline + stubDefaultServer(): void; } } } @@ -58,5 +60,29 @@ Cypress.Commands.add("goOnline", (): void => { }); }); +Cypress.Commands.add("stubDefaultServer", (): void => { + cy.log("Stubbing vector.im and matrix.org network calls"); + // We intercept vector.im & matrix.org calls so that tests don't fail when it has issues + cy.intercept("GET", "https://vector.im/_matrix/identity/api/v1", { + fixture: "vector-im-identity-v1.json", + }); + cy.intercept("GET", "https://matrix.org/.well-known/matrix/client", { + fixture: "matrix-org-client-well-known.json", + }); + cy.intercept("GET", "https://matrix-client.matrix.org/_matrix/client/versions", { + fixture: "matrix-org-client-versions.json", + }); + cy.intercept("GET", "https://matrix-client.matrix.org/_matrix/client/r0/login", { + fixture: "matrix-org-client-login.json", + }); + cy.intercept("POST", "https://matrix-client.matrix.org/_matrix/client/r0/register?kind=guest", { + statusCode: 403, + body: { + errcode: "M_FORBIDDEN", + error: "Registration is not enabled on this homeserver.", + }, + }); +}); + // Needed to make this file a module export { }; diff --git a/package.json b/package.json index 8903d6a54c..1a7829fa14 100644 --- a/package.json +++ b/package.json @@ -160,7 +160,7 @@ "@types/katex": "^0.14.0", "@types/lodash": "^4.14.168", "@types/modernizr": "^3.5.3", - "@types/node": "^14.18.28", + "@types/node": "^16", "@types/pako": "^1.0.1", "@types/parse5": "^6.0.0", "@types/qrcode": "^1.3.5", @@ -212,7 +212,7 @@ "stylelint": "^14.9.1", "stylelint-config-standard": "^26.0.0", "stylelint-scss": "^4.2.0", - "typescript": "4.8.4", + "typescript": "4.9.3", "walk": "^2.3.14" }, "jest": { diff --git a/res/css/voice-broadcast/atoms/_VoiceBroadcastHeader.pcss b/res/css/voice-broadcast/atoms/_VoiceBroadcastHeader.pcss index 1ff29bd985..90092a35ac 100644 --- a/res/css/voice-broadcast/atoms/_VoiceBroadcastHeader.pcss +++ b/res/css/voice-broadcast/atoms/_VoiceBroadcastHeader.pcss @@ -40,8 +40,9 @@ limitations under the License. display: flex; gap: $spacing-4; - i { - flex-shrink: 0; + .mx_Spinner { + flex: 0 0 14px; + padding: 1px; } span { diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index c4971d24f1..99d963ac9b 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -149,14 +149,10 @@ declare global { // https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas interface OffscreenCanvas { - height: number; - width: number; - getContext: HTMLCanvasElement["getContext"]; convertToBlob(opts?: { type?: string; quality?: number; }): Promise; - transferToImageBitmap(): ImageBitmap; } interface HTMLAudioElement { diff --git a/src/DecryptionFailureTracker.ts b/src/DecryptionFailureTracker.ts index b0d9b7ef58..1b01b906b5 100644 --- a/src/DecryptionFailureTracker.ts +++ b/src/DecryptionFailureTracker.ts @@ -174,12 +174,12 @@ export class DecryptionFailureTracker { * Start checking for and tracking failures. */ public start(): void { - this.checkInterval = setInterval( + this.checkInterval = window.setInterval( () => this.checkFailures(Date.now()), DecryptionFailureTracker.CHECK_INTERVAL_MS, ); - this.trackInterval = setInterval( + this.trackInterval = window.setInterval( () => this.trackFailures(), DecryptionFailureTracker.TRACK_INTERVAL_MS, ); diff --git a/src/LegacyCallHandler.tsx b/src/LegacyCallHandler.tsx index 41098dcb4d..e13b0ec85c 100644 --- a/src/LegacyCallHandler.tsx +++ b/src/LegacyCallHandler.tsx @@ -254,7 +254,7 @@ export default class LegacyCallHandler extends EventEmitter { logger.log("Failed to check for protocol support and no retries remain: assuming no support", e); } else { logger.log("Failed to check for protocol support: will retry", e); - setTimeout(() => { + window.setTimeout(() => { this.checkProtocols(maxTries - 1); }, 10000); } diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 9351e91ae4..1c075e8c2c 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -584,7 +584,7 @@ async function doSetLoggedIn( // later than MatrixChat might assume. // // we fire it *synchronously* to make sure it fires before on_logged_in. - // (dis.dispatch uses `setTimeout`, which does not guarantee ordering.) + // (dis.dispatch uses `window.setTimeout`, which does not guarantee ordering.) dis.dispatch({ action: 'on_logging_in' }, true); if (clearStorageEnabled) { @@ -865,7 +865,7 @@ export async function onLoggedOut(): Promise { if (SdkConfig.get().logout_redirect_url) { logger.log("Redirecting to external provider to finish logout"); // XXX: Defer this so that it doesn't race with MatrixChat unmounting the world by going to /#/login - setTimeout(() => { + window.setTimeout(() => { window.location.href = SdkConfig.get().logout_redirect_url; }, 100); } diff --git a/src/NodeAnimator.tsx b/src/NodeAnimator.tsx index 77ab976347..a0d655be9b 100644 --- a/src/NodeAnimator.tsx +++ b/src/NodeAnimator.tsx @@ -119,7 +119,7 @@ export default class NodeAnimator extends React.Component { } // and then we animate to the resting state - setTimeout(() => { + window.setTimeout(() => { this.applyStyles(domNode as HTMLElement, restingStyle); }, 0); } diff --git a/src/PasswordReset.ts b/src/PasswordReset.ts index 8f3c9bd91e..1f2c541270 100644 --- a/src/PasswordReset.ts +++ b/src/PasswordReset.ts @@ -119,7 +119,7 @@ export default class PasswordReset { this.checkEmailLinkClicked() .then(() => resolve()) .catch(() => { - setTimeout( + window.setTimeout( () => this.tryCheckEmailLinkClicked(resolve), CHECK_EMAIL_VERIFIED_POLL_INTERVAL, ); diff --git a/src/audio/PlaybackClock.ts b/src/audio/PlaybackClock.ts index f38be9d134..c3fbb4a3f4 100644 --- a/src/audio/PlaybackClock.ts +++ b/src/audio/PlaybackClock.ts @@ -127,7 +127,7 @@ export class PlaybackClock implements IDestroyable { // cast to number because the types are wrong // 100ms interval to make sure the time is as accurate as possible without // being overly insane - this.timerId = setInterval(this.checkTime, 100); + this.timerId = window.setInterval(this.checkTime, 100); } } diff --git a/src/autocomplete/Autocompleter.ts b/src/autocomplete/Autocompleter.ts index 0c7ef1afb2..7f124213c7 100644 --- a/src/autocomplete/Autocompleter.ts +++ b/src/autocomplete/Autocompleter.ts @@ -35,7 +35,7 @@ export interface ISelectionRange { } export interface ICompletion { - type: "at-room" | "command" | "community" | "room" | "user"; + type?: "at-room" | "command" | "community" | "room" | "user"; completion: string; completionId?: string; component?: ReactElement; diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 4a2c37988a..38cb092a96 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -103,7 +103,7 @@ export default class EmojiProvider extends AutocompleteProvider { return []; // don't give any suggestions if the user doesn't want them } - let completions = []; + let completions: ISortedEmoji[] = []; const { command, range } = this.getCurrentCommand(query, selection); if (command && command[0].length > 2) { @@ -132,7 +132,7 @@ export default class EmojiProvider extends AutocompleteProvider { } // Finally, sort by original ordering sorters.push(c => c._orderBy); - completions = sortBy(uniq(completions), sorters); + completions = sortBy(uniq(completions), sorters); completions = completions.slice(0, LIMIT); @@ -141,9 +141,9 @@ export default class EmojiProvider extends AutocompleteProvider { this.recentlyUsed.forEach(emoji => { sorters.push(c => score(emoji.shortcodes[0], c.emoji.shortcodes[0])); }); - completions = sortBy(uniq(completions), sorters); + completions = sortBy(uniq(completions), sorters); - completions = completions.map(c => ({ + return completions.map(c => ({ completion: c.emoji.unicode, component: ( @@ -153,7 +153,7 @@ export default class EmojiProvider extends AutocompleteProvider { range, })); } - return completions; + return []; } getName() { diff --git a/src/components/structures/InteractiveAuth.tsx b/src/components/structures/InteractiveAuth.tsx index b33fb73791..8152aae251 100644 --- a/src/components/structures/InteractiveAuth.tsx +++ b/src/components/structures/InteractiveAuth.tsx @@ -127,7 +127,7 @@ export default class InteractiveAuthComponent extends React.Component { + this.intervalId = window.setInterval(() => { this.authLogic.poll(); }, 2000); } diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 04fb4a0fae..9327a45ea2 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -1965,7 +1965,7 @@ export default class MatrixChat extends React.PureComponent { this.accountPassword = password; // self-destruct the password after 5mins if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer); - this.accountPasswordTimer = setTimeout(() => { + this.accountPasswordTimer = window.setTimeout(() => { this.accountPassword = null; this.accountPasswordTimer = null; }, 60 * 5 * 1000); diff --git a/src/components/structures/ScrollPanel.tsx b/src/components/structures/ScrollPanel.tsx index e902599b0f..b8b6f53a5f 100644 --- a/src/components/structures/ScrollPanel.tsx +++ b/src/components/structures/ScrollPanel.tsx @@ -459,7 +459,7 @@ export default class ScrollPanel extends React.Component { if (this.unfillDebouncer) { clearTimeout(this.unfillDebouncer); } - this.unfillDebouncer = setTimeout(() => { + this.unfillDebouncer = window.setTimeout(() => { this.unfillDebouncer = null; debuglog("unfilling now", { backwards, origExcessHeight }); this.props.onUnfillRequest?.(backwards, markerScrollToken!); @@ -485,7 +485,7 @@ export default class ScrollPanel extends React.Component { // this will block the scroll event handler for +700ms // if messages are already cached in memory, // This would cause jumping to happen on Chrome/macOS. - return new Promise(resolve => setTimeout(resolve, 1)).then(() => { + return new Promise(resolve => window.setTimeout(resolve, 1)).then(() => { return this.props.onFillRequest(backwards); }).finally(() => { this.pendingFillRequests[dir] = false; diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index 30c1f4d154..7a495393cf 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -697,7 +697,7 @@ export default class InviteDialog extends React.PureComponent { + this.debounceTimer = window.setTimeout(() => { this.updateSuggestions(term); }, 150); // 150ms debounce (human reaction time + some) }; diff --git a/src/components/views/dialogs/SlidingSyncOptionsDialog.tsx b/src/components/views/dialogs/SlidingSyncOptionsDialog.tsx index ea5c77d7f7..f1f818313e 100644 --- a/src/components/views/dialogs/SlidingSyncOptionsDialog.tsx +++ b/src/components/views/dialogs/SlidingSyncOptionsDialog.tsx @@ -48,7 +48,7 @@ async function syncHealthCheck(cli: MatrixClient): Promise { */ async function proxyHealthCheck(endpoint: string, hsUrl?: string): Promise { const controller = new AbortController(); - const id = setTimeout(() => controller.abort(), 10 * 1000); // 10s + const id = window.setTimeout(() => controller.abort(), 10 * 1000); // 10s const res = await fetch(endpoint + "/client/server.json", { signal: controller.signal, }); diff --git a/src/components/views/dialogs/devtools/VerificationExplorer.tsx b/src/components/views/dialogs/devtools/VerificationExplorer.tsx index 6d3fe36245..6673c4ca04 100644 --- a/src/components/views/dialogs/devtools/VerificationExplorer.tsx +++ b/src/components/views/dialogs/devtools/VerificationExplorer.tsx @@ -51,7 +51,7 @@ const VerificationRequestExplorer: React.FC<{ if (request.timeout == 0) return; /* Note that request.timeout is a getter, so its value changes */ - const id = setInterval(() => { + const id = window.setInterval(() => { setRequestTimeout(request.timeout); }, 500); diff --git a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx index f000d3bf4b..085dd540b0 100644 --- a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx +++ b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx @@ -228,7 +228,7 @@ export const useWebSearchMetrics = (numResults: number, queryLength: number, via if (!queryLength) return; // send metrics after a 1s debounce - const timeoutId = setTimeout(() => { + const timeoutId = window.setTimeout(() => { PosthogAnalytics.instance.trackEvent({ eventName: "WebSearch", viaSpotlight, diff --git a/src/components/views/elements/DesktopCapturerSourcePicker.tsx b/src/components/views/elements/DesktopCapturerSourcePicker.tsx index f355cc2a5e..eb9bac2876 100644 --- a/src/components/views/elements/DesktopCapturerSourcePicker.tsx +++ b/src/components/views/elements/DesktopCapturerSourcePicker.tsx @@ -106,7 +106,7 @@ export default class DesktopCapturerSourcePicker extends React.Component< } async componentDidMount() { - // setInterval() first waits and then executes, therefore + // window.setInterval() first waits and then executes, therefore // we call getDesktopCapturerSources() here without any delay. // Otherwise the dialog would be left empty for some time. this.setState({ @@ -114,7 +114,7 @@ export default class DesktopCapturerSourcePicker extends React.Component< }); // We update the sources every 500ms to get newer thumbnails - this.interval = setInterval(async () => { + this.interval = window.setInterval(async () => { this.setState({ sources: await getDesktopCapturerSources(), }); diff --git a/src/components/views/elements/UseCaseSelection.tsx b/src/components/views/elements/UseCaseSelection.tsx index eaa0a9d3cf..cea0a232c1 100644 --- a/src/components/views/elements/UseCaseSelection.tsx +++ b/src/components/views/elements/UseCaseSelection.tsx @@ -35,7 +35,7 @@ export function UseCaseSelection({ onFinished }: Props) { // Call onFinished 1.5s after `selection` becomes truthy, to give time for the animation to run useEffect(() => { if (selection) { - let handler: number | null = setTimeout(() => { + let handler: number | null = window.setTimeout(() => { handler = null; onFinished(selection); }, TIMEOUT); diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index 95e0e24ae1..571665ef20 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -191,7 +191,7 @@ class EmojiPicker extends React.Component { this.setState({ filter }); // Header underlines need to be updated, but updating requires knowing // where the categories are, so we wait for a tick. - setTimeout(this.updateVisibility, 0); + window.setTimeout(this.updateVisibility, 0); }; private emojiMatchesFilter = (emoji: IEmoji, filter: string): boolean => { diff --git a/src/components/views/emojipicker/Search.tsx b/src/components/views/emojipicker/Search.tsx index de09421010..54a15e509e 100644 --- a/src/components/views/emojipicker/Search.tsx +++ b/src/components/views/emojipicker/Search.tsx @@ -31,8 +31,8 @@ class Search extends React.PureComponent { private inputRef = React.createRef(); componentDidMount() { - // For some reason, neither the autoFocus nor just calling focus() here worked, so here's a setTimeout - setTimeout(() => this.inputRef.current.focus(), 0); + // For some reason, neither the autoFocus nor just calling focus() here worked, so here's a window.setTimeout + window.setTimeout(() => this.inputRef.current.focus(), 0); } private onKeyDown = (ev: React.KeyboardEvent) => { diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index 43adf41d8c..7af72bae70 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -335,7 +335,7 @@ export default class MImageBody extends React.Component { // Add a 150ms timer for blurhash to first appear. if (this.props.mxEvent.getContent().info?.[BLURHASH_FIELD]) { this.clearBlurhashTimeout(); - this.timeout = setTimeout(() => { + this.timeout = window.setTimeout(() => { if (!this.state.imgLoaded || !this.state.imgError) { this.setState({ placeholder: Placeholder.Blurhash, diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index ab9c27f7fb..941e3496c3 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -130,7 +130,7 @@ export default class TextualBody extends React.Component { if (codes.length > 0) { // Do this asynchronously: parsing code takes time and we don't // need to block the DOM update on it. - setTimeout(() => { + window.setTimeout(() => { if (this.unmounted) return; for (let i = 0; i < codes.length; i++) { this.highlightCode(codes[i]); diff --git a/src/components/views/right_panel/EncryptionPanel.tsx b/src/components/views/right_panel/EncryptionPanel.tsx index d0d6c7bf5b..8e23668e08 100644 --- a/src/components/views/right_panel/EncryptionPanel.tsx +++ b/src/components/views/right_panel/EncryptionPanel.tsx @@ -111,8 +111,21 @@ const EncryptionPanel: React.FC = (props: IProps) => { const onStartVerification = useCallback(async () => { setRequesting(true); const cli = MatrixClientPeg.get(); - const roomId = await ensureDMExists(cli, member.userId); - const verificationRequest_ = await cli.requestVerificationDM(member.userId, roomId); + let verificationRequest_: VerificationRequest; + try { + const roomId = await ensureDMExists(cli, member.userId); + verificationRequest_ = await cli.requestVerificationDM(member.userId, roomId); + } catch (e) { + console.error("Error starting verification", e); + setRequesting(false); + + Modal.createDialog(ErrorDialog, { + headerImage: require("../../../../res/img/e2e/warning.svg").default, + title: _t("Error starting verification"), + description: _t("We were unable to start a chat with the other user."), + }); + return; + } setRequest(verificationRequest_); setPhase(verificationRequest_.phase); // Notify the RightPanelStore about this diff --git a/src/components/views/rooms/Autocomplete.tsx b/src/components/views/rooms/Autocomplete.tsx index 58a659f537..25c526d4ef 100644 --- a/src/components/views/rooms/Autocomplete.tsx +++ b/src/components/views/rooms/Autocomplete.tsx @@ -127,7 +127,7 @@ export default class Autocomplete extends React.PureComponent { } return new Promise((resolve) => { - this.debounceCompletionsRequest = setTimeout(() => { + this.debounceCompletionsRequest = window.setTimeout(() => { resolve(this.processQuery(query, selection)); }, autocompleteDelay); }); diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 152c592a02..cb996d8d34 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -199,7 +199,7 @@ export class MessageComposer extends React.Component { // that the ScrollPanel listening to the resizeNotifier can // correctly measure it's new height and scroll down to keep // at the bottom if it already is - setTimeout(() => { + window.setTimeout(() => { this.props.resizeNotifier.notifyTimelineHeightChanged(); }, 100); } @@ -395,7 +395,7 @@ export class MessageComposer extends React.Component { private onRecordingEndingSoon = ({ secondsLeft }) => { this.setState({ recordingTimeLeftSeconds: secondsLeft }); - setTimeout(() => this.setState({ recordingTimeLeftSeconds: null }), 3000); + window.setTimeout(() => this.setState({ recordingTimeLeftSeconds: null }), 3000); }; private setStickerPickerOpen = (isStickerPickerOpen: boolean) => { @@ -584,6 +584,7 @@ export class MessageComposer extends React.Component { setUpVoiceBroadcastPreRecording( this.props.room, MatrixClientPeg.get(), + SdkContextClass.instance.voiceBroadcastPlaybacksStore, VoiceBroadcastRecordingsStore.instance(), SdkContextClass.instance.voiceBroadcastPreRecordingStore, ); diff --git a/src/components/views/rooms/RoomBreadcrumbs.tsx b/src/components/views/rooms/RoomBreadcrumbs.tsx index 0ed34e9476..b6383bf83c 100644 --- a/src/components/views/rooms/RoomBreadcrumbs.tsx +++ b/src/components/views/rooms/RoomBreadcrumbs.tsx @@ -99,7 +99,7 @@ export default class RoomBreadcrumbs extends React.PureComponent // again and this time we want to show the newest breadcrumb because it'll be hidden // off screen for the animation. this.setState({ doAnimation: false, skipFirst: true }); - setTimeout(() => this.setState({ doAnimation: true, skipFirst: false }), 0); + window.setTimeout(() => this.setState({ doAnimation: true, skipFirst: false }), 0); }; private viewRoom = (room: Room, index: number, viaKeyboard = false) => { diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useIsFocused.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useIsFocused.ts index 99e6dbd9c8..b7c99b2786 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useIsFocused.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useIsFocused.ts @@ -28,7 +28,7 @@ export function useIsFocused() { } else { // To avoid a blink when we switch mode between plain text and rich text mode // We delay the unfocused action - timeoutIDRef.current = setTimeout(() => setIsFocused(false), 100); + timeoutIDRef.current = window.setTimeout(() => setIsFocused(false), 100); } }, [setIsFocused, timeoutIDRef]); diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts b/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts index 5b76703820..4d1dcaf2f1 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts @@ -37,7 +37,7 @@ export function focusComposer( if (timeoutId.current) { clearTimeout(timeoutId.current); } - timeoutId.current = setTimeout( + timeoutId.current = window.setTimeout( () => composerElement.current?.focus(), 200, ); diff --git a/src/components/views/settings/ThemeChoicePanel.tsx b/src/components/views/settings/ThemeChoicePanel.tsx index cc4545e9ea..4d1343da08 100644 --- a/src/components/views/settings/ThemeChoicePanel.tsx +++ b/src/components/views/settings/ThemeChoicePanel.tsx @@ -150,7 +150,7 @@ export default class ThemeChoicePanel extends React.Component { await SettingsStore.setValue("custom_themes", null, SettingLevel.ACCOUNT, currentThemes); this.setState({ customThemeUrl: "", customThemeMessage: { text: _t("Theme added!"), isError: false } }); - this.themeTimer = setTimeout(() => { + this.themeTimer = window.setTimeout(() => { this.setState({ customThemeMessage: { text: "", isError: false } }); }, 3000); }; diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index 07a08ec9c4..a266820189 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -127,7 +127,7 @@ const SessionManagerTab: React.FC = () => { const [expandedDeviceIds, setExpandedDeviceIds] = useState([]); const [selectedDeviceIds, setSelectedDeviceIds] = useState([]); const filteredDeviceListRef = useRef(null); - const scrollIntoViewTimeoutRef = useRef>(); + const scrollIntoViewTimeoutRef = useRef(); const matrixClient = useContext(MatrixClientContext); const userId = matrixClient.getUserId(); diff --git a/src/components/views/toasts/VerificationRequestToast.tsx b/src/components/views/toasts/VerificationRequestToast.tsx index 3b2e5c89a8..92d8b71582 100644 --- a/src/components/views/toasts/VerificationRequestToast.tsx +++ b/src/components/views/toasts/VerificationRequestToast.tsx @@ -57,7 +57,7 @@ export default class VerificationRequestToast extends React.PureComponent 0) { - this.intervalHandle = setInterval(() => { + this.intervalHandle = window.setInterval(() => { let { counter } = this.state; counter = Math.max(0, counter - 1); this.setState({ counter }); diff --git a/src/components/views/user-onboarding/UserOnboardingPage.tsx b/src/components/views/user-onboarding/UserOnboardingPage.tsx index cc90a3d09d..ab973a8bf9 100644 --- a/src/components/views/user-onboarding/UserOnboardingPage.tsx +++ b/src/components/views/user-onboarding/UserOnboardingPage.tsx @@ -55,7 +55,7 @@ export function UserOnboardingPage({ justRegistered = false }: Props) { const [showList, setShowList] = useState(false); useEffect(() => { if (initialSyncComplete) { - let handler: number | null = setTimeout(() => { + let handler: number | null = window.setTimeout(() => { handler = null; setShowList(true); }, ANIMATION_DURATION); diff --git a/src/components/views/voip/CallDuration.tsx b/src/components/views/voip/CallDuration.tsx index df59ba05d9..a3e44ab740 100644 --- a/src/components/views/voip/CallDuration.tsx +++ b/src/components/views/voip/CallDuration.tsx @@ -43,7 +43,7 @@ interface GroupCallDurationProps { export const GroupCallDuration: FC = ({ groupCall }) => { const [now, setNow] = useState(() => Date.now()); useEffect(() => { - const timer = setInterval(() => setNow(Date.now()), 1000); + const timer = window.setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(timer); }, []); diff --git a/src/components/views/voip/PipView.tsx b/src/components/views/voip/PipView.tsx index 27f7798f11..40a59710d4 100644 --- a/src/components/views/voip/PipView.tsx +++ b/src/components/views/voip/PipView.tsx @@ -367,14 +367,14 @@ class PipView extends React.Component { const pipMode = true; let pipContent: CreatePipChildren | null = null; - if (this.props.voiceBroadcastPreRecording) { - pipContent = this.createVoiceBroadcastPreRecordingPipContent(this.props.voiceBroadcastPreRecording); - } - if (this.props.voiceBroadcastPlayback) { pipContent = this.createVoiceBroadcastPlaybackPipContent(this.props.voiceBroadcastPlayback); } + if (this.props.voiceBroadcastPreRecording) { + pipContent = this.createVoiceBroadcastPreRecordingPipContent(this.props.voiceBroadcastPreRecording); + } + if (this.props.voiceBroadcastRecording) { pipContent = this.createVoiceBroadcastRecordingPipContent(this.props.voiceBroadcastRecording); } diff --git a/src/dispatcher/dispatcher.ts b/src/dispatcher/dispatcher.ts index 4d4f83d4de..970a9ab140 100644 --- a/src/dispatcher/dispatcher.ts +++ b/src/dispatcher/dispatcher.ts @@ -49,7 +49,7 @@ export class MatrixDispatcher extends Dispatcher { // if you dispatch from within a dispatch, so rather than action // handlers having to worry about not calling anything that might // then dispatch, we just do dispatches asynchronously. - setTimeout(super.dispatch.bind(this, payload), 0); + window.setTimeout(super.dispatch.bind(this, payload), 0); } } diff --git a/src/hooks/spotlight/useDebouncedCallback.ts b/src/hooks/spotlight/useDebouncedCallback.ts index 9548ce5e0c..c703595a18 100644 --- a/src/hooks/spotlight/useDebouncedCallback.ts +++ b/src/hooks/spotlight/useDebouncedCallback.ts @@ -30,7 +30,7 @@ export function useDebouncedCallback( callback(...params); }; if (enabled !== false) { - handle = setTimeout(doSearch, DEBOUNCE_TIMEOUT); + handle = window.setTimeout(doSearch, DEBOUNCE_TIMEOUT); return () => { if (handle) { clearTimeout(handle); diff --git a/src/hooks/useTimeout.ts b/src/hooks/useTimeout.ts index 07301a367a..91e2d7d7be 100644 --- a/src/hooks/useTimeout.ts +++ b/src/hooks/useTimeout.ts @@ -30,7 +30,7 @@ export const useTimeout = (handler: Handler, timeoutMs: number) => { // Set up timer useEffect(() => { - const timeoutID = setTimeout(() => { + const timeoutID = window.setTimeout(() => { savedHandler.current(); }, timeoutMs); return () => clearTimeout(timeoutID); @@ -49,7 +49,7 @@ export const useInterval = (handler: Handler, intervalMs: number) => { // Set up timer useEffect(() => { - const intervalID = setInterval(() => { + const intervalID = window.setInterval(() => { savedHandler.current(); }, intervalMs); return () => clearInterval(intervalID); diff --git a/src/hooks/useTimeoutToggle.ts b/src/hooks/useTimeoutToggle.ts index 0bdfe714a0..d7cd1be049 100644 --- a/src/hooks/useTimeoutToggle.ts +++ b/src/hooks/useTimeoutToggle.ts @@ -28,7 +28,7 @@ export const useTimeoutToggle = (defaultValue: boolean, timeoutMs: number) => { const toggle = () => { setValue(!defaultValue); - timeoutId.current = setTimeout(() => setValue(defaultValue), timeoutMs); + timeoutId.current = window.setTimeout(() => setValue(defaultValue), timeoutMs); }; useEffect(() => { diff --git a/src/hooks/useUserOnboardingContext.ts b/src/hooks/useUserOnboardingContext.ts index 90d1eb09c6..29c4fce6ea 100644 --- a/src/hooks/useUserOnboardingContext.ts +++ b/src/hooks/useUserOnboardingContext.ts @@ -68,7 +68,7 @@ function useUserOnboardingContextValue(defaultValue: T, callback: (cli: Matri } setValue(await handler(cli)); if (enabled) { - handle = setTimeout(repeater, USER_ONBOARDING_CONTEXT_INTERVAL); + handle = window.setTimeout(repeater, USER_ONBOARDING_CONTEXT_INTERVAL); } }; repeater().catch(err => logger.warn("could not update user onboarding context", err)); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c2f34afca9..4ee870bd72 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -660,6 +660,7 @@ "Change input device": "Change input device", "Live": "Live", "Voice broadcast": "Voice broadcast", + "Buffering…": "Buffering…", "Cannot reach homeserver": "Cannot reach homeserver", "Ensure you have a stable internet connection, or get in touch with the server admin": "Ensure you have a stable internet connection, or get in touch with the server admin", "Your %(brand)s is misconfigured": "Your %(brand)s is misconfigured", @@ -2151,6 +2152,8 @@ "The homeserver the user you're verifying is connected to": "The homeserver the user you're verifying is connected to", "Yours, or the other users' internet connection": "Yours, or the other users' internet connection", "Yours, or the other users' session": "Yours, or the other users' session", + "Error starting verification": "Error starting verification", + "We were unable to start a chat with the other user.": "We were unable to start a chat with the other user.", "Nothing pinned, yet": "Nothing pinned, yet", "If you have permissions, open the menu on any message and select Pin to stick them here.": "If you have permissions, open the menu on any message and select Pin to stick them here.", "Pinned messages": "Pinned messages", diff --git a/src/models/Call.ts b/src/models/Call.ts index 0e20c331fb..9d8d727f27 100644 --- a/src/models/Call.ts +++ b/src/models/Call.ts @@ -377,7 +377,7 @@ export class JitsiCall extends Call { this.participants = participants; if (allExpireAt < Infinity) { - this.participantsExpirationTimer = setTimeout(() => this.updateParticipants(), allExpireAt - now); + this.participantsExpirationTimer = window.setTimeout(() => this.updateParticipants(), allExpireAt - now); } } @@ -553,7 +553,7 @@ export class JitsiCall extends Call { // Tell others that we're connected, by adding our device to room state await this.addOurDevice(); // Re-add this device every so often so our video member event doesn't become stale - this.resendDevicesTimer = setInterval(async () => { + this.resendDevicesTimer = window.setInterval(async () => { logger.log(`Resending video member event for ${this.roomId}`); await this.addOurDevice(); }, (this.STUCK_DEVICE_TIMEOUT_MS * 3) / 4); @@ -814,7 +814,7 @@ export class ElementCall extends Call { // randomly between 2 and 8 seconds before terminating the call, to // probabilistically reduce event spam. If someone else beats us to it, // this timer will be automatically cleared upon the call's destruction. - this.terminationTimer = setTimeout( + this.terminationTimer = window.setTimeout( () => this.groupCall.terminate(), Math.random() * 6000 + 2000, ); diff --git a/src/rageshake/rageshake.ts b/src/rageshake/rageshake.ts index e8461eef76..d6d6665d6d 100644 --- a/src/rageshake/rageshake.ts +++ b/src/rageshake/rageshake.ts @@ -154,7 +154,7 @@ export class IndexedDBLogStore { // @ts-ignore this.db = event.target.result; // Periodically flush logs to local storage / indexeddb - setInterval(this.flush.bind(this), FLUSH_RATE_MS); + window.setInterval(this.flush.bind(this), FLUSH_RATE_MS); resolve(); }; diff --git a/src/stores/OwnBeaconStore.ts b/src/stores/OwnBeaconStore.ts index 846b7cac68..b03a8a1f21 100644 --- a/src/stores/OwnBeaconStore.ts +++ b/src/stores/OwnBeaconStore.ts @@ -437,7 +437,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient { return; } - this.locationInterval = setInterval(() => { + this.locationInterval = window.setInterval(() => { if (!this.lastPublishedPositionTimestamp) { return; } diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index 73d6bdbd51..abdbff0ffe 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -228,7 +228,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient implements if (!room) { logger.warn(`Live timeline event ${eventPayload.event.getId()} received without associated room`); logger.warn(`Queuing failed room update for retry as a result.`); - setTimeout(async () => { + window.setTimeout(async () => { const updatedRoom = this.matrixClient.getRoom(roomId); await tryUpdate(updatedRoom); }, 100); // 100ms should be enough for the room to show up diff --git a/src/theme.ts b/src/theme.ts index 84455dd6d6..114da50cf9 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -298,7 +298,7 @@ export async function setTheme(theme?: string): Promise { // In case of theme toggling (white => black => white) // Chrome doesn't fire the `load` event when the white theme is selected the second times - const intervalId = setInterval(() => { + const intervalId = window.setInterval(() => { if (isStyleSheetLoaded()) { clearInterval(intervalId); styleSheet.onload = undefined; diff --git a/src/utils/MultiInviter.ts b/src/utils/MultiInviter.ts index 3c539f7bf0..7208326a2b 100644 --- a/src/utils/MultiInviter.ts +++ b/src/utils/MultiInviter.ts @@ -241,7 +241,7 @@ export default class MultiInviter { break; case "M_LIMIT_EXCEEDED": // we're being throttled so wait a bit & try again - setTimeout(() => { + window.setTimeout(() => { this.doInvite(address, ignoreProfile).then(resolve, reject); }, 5000); return; diff --git a/src/utils/Timer.ts b/src/utils/Timer.ts index 38703c1299..f17745029e 100644 --- a/src/utils/Timer.ts +++ b/src/utils/Timer.ts @@ -55,7 +55,7 @@ export default class Timer { this.setNotStarted(); } else { const delta = this.timeout - elapsed; - this.timerHandle = setTimeout(this.onTimeout, delta); + this.timerHandle = window.setTimeout(this.onTimeout, delta); } }; @@ -78,7 +78,7 @@ export default class Timer { start() { if (!this.isRunning()) { this.startTs = Date.now(); - this.timerHandle = setTimeout(this.onTimeout, this.timeout); + this.timerHandle = window.setTimeout(this.onTimeout, this.timeout); } return this; } diff --git a/src/utils/WidgetUtils.ts b/src/utils/WidgetUtils.ts index 964d795576..e6f75bb025 100644 --- a/src/utils/WidgetUtils.ts +++ b/src/utils/WidgetUtils.ts @@ -166,7 +166,7 @@ export default class WidgetUtils { resolve(); } } - const timerId = setTimeout(() => { + const timerId = window.setTimeout(() => { MatrixClientPeg.get().removeListener(ClientEvent.AccountData, onAccountData); reject(new Error("Timed out waiting for widget ID " + widgetId + " to appear")); }, WIDGET_WAIT_TIME); @@ -221,7 +221,7 @@ export default class WidgetUtils { resolve(); } } - const timerId = setTimeout(() => { + const timerId = window.setTimeout(() => { MatrixClientPeg.get().removeListener(RoomStateEvent.Events, onRoomStateEvents); reject(new Error("Timed out waiting for widget ID " + widgetId + " to appear")); }, WIDGET_WAIT_TIME); diff --git a/src/utils/exportUtils/exportJS.js b/src/utils/exportUtils/exportJS.js index e082f88d98..6e309292da 100644 --- a/src/utils/exportUtils/exportJS.js +++ b/src/utils/exportUtils/exportJS.js @@ -27,7 +27,7 @@ function showToast(text) { const el = document.getElementById("snackbar"); el.innerHTML = text; el.className = "mx_show"; - setTimeout(() => { + window.setTimeout(() => { el.className = el.className.replace("mx_show", ""); }, 2000); } diff --git a/src/utils/image-media.ts b/src/utils/image-media.ts index c3320627d0..58558f7a25 100644 --- a/src/utils/image-media.ts +++ b/src/utils/image-media.ts @@ -77,10 +77,10 @@ export async function createThumbnail( } let canvas: HTMLCanvasElement | OffscreenCanvas; - let context: CanvasRenderingContext2D; + let context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D; try { canvas = new window.OffscreenCanvas(targetWidth, targetHeight); - context = canvas.getContext("2d"); + context = canvas.getContext("2d") as OffscreenCanvasRenderingContext2D; } catch (e) { // Fallback support for other browsers (Safari and Firefox for now) canvas = document.createElement("canvas"); @@ -92,7 +92,7 @@ export async function createThumbnail( context.drawImage(element, 0, 0, targetWidth, targetHeight); let thumbnailPromise: Promise; - if (window.OffscreenCanvas && canvas instanceof window.OffscreenCanvas) { + if (window.OffscreenCanvas && canvas instanceof OffscreenCanvas) { thumbnailPromise = canvas.convertToBlob({ type: mimeType }); } else { thumbnailPromise = new Promise(resolve => (canvas as HTMLCanvasElement).toBlob(resolve, mimeType)); diff --git a/src/utils/local-room.ts b/src/utils/local-room.ts index 8b1a2e6379..b85bb7de9d 100644 --- a/src/utils/local-room.ts +++ b/src/utils/local-room.ts @@ -102,10 +102,10 @@ export async function waitForRoomReadyAndApplyAfterCreateCallbacks( finish(); }; - const checkRoomStateIntervalHandle = setInterval(() => { + const checkRoomStateIntervalHandle = window.setInterval(() => { if (isRoomReady(client, localRoom)) finish(); }, 500); - const stopgapTimeoutHandle = setTimeout(stopgapFinish, 5000); + const stopgapTimeoutHandle = window.setTimeout(stopgapFinish, 5000); }); } diff --git a/src/utils/membership.ts b/src/utils/membership.ts index 1a48f1261c..a101142511 100644 --- a/src/utils/membership.ts +++ b/src/utils/membership.ts @@ -97,7 +97,7 @@ export async function waitForMember(client: MatrixClient, roomId: string, userId /* We don't want to hang if this goes wrong, so we proceed and hope the other user is already in the megolm session */ - setTimeout(resolve, timeout, false); + window.setTimeout(resolve, timeout, false); }).finally(() => { client.removeListener(RoomStateEvent.NewMember, handler); }); diff --git a/src/utils/promise.ts b/src/utils/promise.ts index 04a9ac8818..e478409eb3 100644 --- a/src/utils/promise.ts +++ b/src/utils/promise.ts @@ -18,7 +18,7 @@ limitations under the License. // or when the timeout of ms is reached with the value of given timeoutValue export async function timeout(promise: Promise, timeoutValue: Y, ms: number): Promise { const timeoutPromise = new Promise((resolve) => { - const timeoutId = setTimeout(resolve, ms, timeoutValue); + const timeoutId = window.setTimeout(resolve, ms, timeoutValue); promise.then(() => { clearTimeout(timeoutId); }); diff --git a/src/voice-broadcast/components/atoms/VoiceBroadcastHeader.tsx b/src/voice-broadcast/components/atoms/VoiceBroadcastHeader.tsx index be31cd4efe..64640ca793 100644 --- a/src/voice-broadcast/components/atoms/VoiceBroadcastHeader.tsx +++ b/src/voice-broadcast/components/atoms/VoiceBroadcastHeader.tsx @@ -25,6 +25,7 @@ import AccessibleButton from "../../../components/views/elements/AccessibleButto import { Icon as XIcon } from "../../../../res/img/element-icons/cancel-rounded.svg"; import Clock from "../../../components/views/audio_messages/Clock"; import { formatTimeLeft } from "../../../DateUtils"; +import Spinner from "../../../components/views/elements/Spinner"; interface VoiceBroadcastHeaderProps { live?: VoiceBroadcastLiveness; @@ -33,6 +34,7 @@ interface VoiceBroadcastHeaderProps { room: Room; microphoneLabel?: string; showBroadcast?: boolean; + showBuffering?: boolean; timeLeft?: number; showClose?: boolean; } @@ -44,47 +46,55 @@ export const VoiceBroadcastHeader: React.FC = ({ room, microphoneLabel, showBroadcast = false, + showBuffering = false, showClose = false, timeLeft, }) => { - const broadcast = showBroadcast - ?
+ const broadcast = showBroadcast && ( +
{ _t("Voice broadcast") }
- : null; + ); - const liveBadge = live === "not-live" - ? null - : ; + const liveBadge = live !== "not-live" && ( + + ); - const closeButton = showClose - ? + const closeButton = showClose && ( + - : null; + ); - const timeLeftLine = timeLeft - ?
+ const timeLeftLine = timeLeft && ( +
- : null; + ); + + const buffering = showBuffering && ( +
+ + { _t("Buffering…") } +
+ ); const microphoneLineClasses = classNames({ mx_VoiceBroadcastHeader_line: true, ["mx_VoiceBroadcastHeader_mic--clickable"]: onMicrophoneLineClick, }); - const microphoneLine = microphoneLabel - ?
{ microphoneLabel }
- : null; + ); return
@@ -95,6 +105,7 @@ export const VoiceBroadcastHeader: React.FC = ({ { microphoneLine } { timeLeftLine } { broadcast } + { buffering }
{ liveBadge } { closeButton } diff --git a/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx b/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx index 7ba06a1501..cc86e3304d 100644 --- a/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx +++ b/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx @@ -23,7 +23,6 @@ import { VoiceBroadcastPlayback, VoiceBroadcastPlaybackState, } from "../.."; -import Spinner from "../../../components/views/elements/Spinner"; import { useVoiceBroadcastPlayback } from "../../hooks/useVoiceBroadcastPlayback"; import { Icon as PlayIcon } from "../../../../res/img/element-icons/play.svg"; import { Icon as PauseIcon } from "../../../../res/img/element-icons/pause.svg"; @@ -54,40 +53,35 @@ export const VoiceBroadcastPlaybackBody: React.FC>; + let controlLabel: string; + let className = ""; - if (playbackState === VoiceBroadcastPlaybackState.Buffering) { - control = ; - } else { - let controlIcon: React.FC>; - let controlLabel: string; - let className = ""; - - switch (playbackState) { - case VoiceBroadcastPlaybackState.Stopped: - controlIcon = PlayIcon; - className = "mx_VoiceBroadcastControl-play"; - controlLabel = _t("play voice broadcast"); - break; - case VoiceBroadcastPlaybackState.Paused: - controlIcon = PlayIcon; - className = "mx_VoiceBroadcastControl-play"; - controlLabel = _t("resume voice broadcast"); - break; - case VoiceBroadcastPlaybackState.Playing: - controlIcon = PauseIcon; - controlLabel = _t("pause voice broadcast"); - break; - } - - control = ; + switch (playbackState) { + case VoiceBroadcastPlaybackState.Stopped: + controlIcon = PlayIcon; + className = "mx_VoiceBroadcastControl-play"; + controlLabel = _t("play voice broadcast"); + break; + case VoiceBroadcastPlaybackState.Paused: + controlIcon = PlayIcon; + className = "mx_VoiceBroadcastControl-play"; + controlLabel = _t("resume voice broadcast"); + break; + case VoiceBroadcastPlaybackState.Buffering: + case VoiceBroadcastPlaybackState.Playing: + controlIcon = PauseIcon; + controlLabel = _t("pause voice broadcast"); + break; } + const control = ; + let seekBackwardButton: ReactElement | null = null; let seekForwardButton: ReactElement | null = null; @@ -124,7 +118,8 @@ export const VoiceBroadcastPlaybackBody: React.FC
{ seekBackwardButton } diff --git a/src/voice-broadcast/hooks/useVoiceBroadcastPlayback.ts b/src/voice-broadcast/hooks/useVoiceBroadcastPlayback.ts index 0b515c4437..adeb19c231 100644 --- a/src/voice-broadcast/hooks/useVoiceBroadcastPlayback.ts +++ b/src/voice-broadcast/hooks/useVoiceBroadcastPlayback.ts @@ -27,6 +27,13 @@ import { export const useVoiceBroadcastPlayback = (playback: VoiceBroadcastPlayback) => { const client = MatrixClientPeg.get(); const room = client.getRoom(playback.infoEvent.getRoomId()); + + if (!room) { + throw new Error( + `Voice Broadcast room not found (event ${playback.infoEvent.getId()})`, + ); + } + const playbackToggle = () => { playback.toggle(); }; diff --git a/src/voice-broadcast/models/VoiceBroadcastPreRecording.ts b/src/voice-broadcast/models/VoiceBroadcastPreRecording.ts index f1e956c600..10995e5d49 100644 --- a/src/voice-broadcast/models/VoiceBroadcastPreRecording.ts +++ b/src/voice-broadcast/models/VoiceBroadcastPreRecording.ts @@ -18,6 +18,7 @@ import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix"; import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter"; import { IDestroyable } from "../../utils/IDestroyable"; +import { VoiceBroadcastPlaybacksStore } from "../stores/VoiceBroadcastPlaybacksStore"; import { VoiceBroadcastRecordingsStore } from "../stores/VoiceBroadcastRecordingsStore"; import { startNewVoiceBroadcastRecording } from "../utils/startNewVoiceBroadcastRecording"; @@ -34,6 +35,7 @@ export class VoiceBroadcastPreRecording public room: Room, public sender: RoomMember, private client: MatrixClient, + private playbacksStore: VoiceBroadcastPlaybacksStore, private recordingsStore: VoiceBroadcastRecordingsStore, ) { super(); @@ -43,6 +45,7 @@ export class VoiceBroadcastPreRecording await startNewVoiceBroadcastRecording( this.room, this.client, + this.playbacksStore, this.recordingsStore, ); this.emit("dismiss", this); diff --git a/src/voice-broadcast/utils/setUpVoiceBroadcastPreRecording.ts b/src/voice-broadcast/utils/setUpVoiceBroadcastPreRecording.ts index 8bd211f612..9d5d410aa2 100644 --- a/src/voice-broadcast/utils/setUpVoiceBroadcastPreRecording.ts +++ b/src/voice-broadcast/utils/setUpVoiceBroadcastPreRecording.ts @@ -18,6 +18,7 @@ import { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; import { checkVoiceBroadcastPreConditions, + VoiceBroadcastPlaybacksStore, VoiceBroadcastPreRecording, VoiceBroadcastPreRecordingStore, VoiceBroadcastRecordingsStore, @@ -26,6 +27,7 @@ import { export const setUpVoiceBroadcastPreRecording = ( room: Room, client: MatrixClient, + playbacksStore: VoiceBroadcastPlaybacksStore, recordingsStore: VoiceBroadcastRecordingsStore, preRecordingStore: VoiceBroadcastPreRecordingStore, ): VoiceBroadcastPreRecording | null => { @@ -39,7 +41,11 @@ export const setUpVoiceBroadcastPreRecording = ( const sender = room.getMember(userId); if (!sender) return null; - const preRecording = new VoiceBroadcastPreRecording(room, sender, client, recordingsStore); + // pause and clear current playback (if any) + playbacksStore.getCurrent()?.pause(); + playbacksStore.clearCurrent(); + + const preRecording = new VoiceBroadcastPreRecording(room, sender, client, playbacksStore, recordingsStore); preRecordingStore.setCurrent(preRecording); return preRecording; }; diff --git a/src/voice-broadcast/utils/startNewVoiceBroadcastRecording.ts b/src/voice-broadcast/utils/startNewVoiceBroadcastRecording.ts index ae4e40c4a3..5306a9d605 100644 --- a/src/voice-broadcast/utils/startNewVoiceBroadcastRecording.ts +++ b/src/voice-broadcast/utils/startNewVoiceBroadcastRecording.ts @@ -24,6 +24,7 @@ import { VoiceBroadcastRecordingsStore, VoiceBroadcastRecording, getChunkLength, + VoiceBroadcastPlaybacksStore, } from ".."; import { checkVoiceBroadcastPreConditions } from "./checkVoiceBroadcastPreConditions"; @@ -80,17 +81,23 @@ const startBroadcast = async ( /** * Starts a new Voice Broadcast Recording, if * - the user has the permissions to do so in the room + * - the user is not already recording a voice broadcast * - there is no other broadcast being recorded in the room, yet * Sends a voice_broadcast_info state event and waits for the event to actually appear in the room state. */ export const startNewVoiceBroadcastRecording = async ( room: Room, client: MatrixClient, + playbacksStore: VoiceBroadcastPlaybacksStore, recordingsStore: VoiceBroadcastRecordingsStore, ): Promise => { if (!checkVoiceBroadcastPreConditions(room, client, recordingsStore)) { return null; } + // pause and clear current playback (if any) + playbacksStore.getCurrent()?.pause(); + playbacksStore.clearCurrent(); + return startBroadcast(room, client, recordingsStore); }; diff --git a/test/ContentMessages-test.ts b/test/ContentMessages-test.ts index 3d812cbbee..e0fb6c0a32 100644 --- a/test/ContentMessages-test.ts +++ b/test/ContentMessages-test.ts @@ -91,7 +91,7 @@ describe("ContentMessages", () => { Object.defineProperty(global.Image.prototype, 'src', { // Define the property setter set(src) { - setTimeout(() => this.onload()); + window.setTimeout(() => this.onload()); }, }); Object.defineProperty(global.Image.prototype, 'height', { diff --git a/test/components/views/location/LocationShareMenu-test.tsx b/test/components/views/location/LocationShareMenu-test.tsx index f590bcdd7c..7624f6ff86 100644 --- a/test/components/views/location/LocationShareMenu-test.tsx +++ b/test/components/views/location/LocationShareMenu-test.tsx @@ -352,7 +352,7 @@ describe('', () => { // @ts-ignore mocked(SettingsStore.watchSetting).mockImplementation((featureName, roomId, callback) => { callback(featureName, roomId, SettingLevel.DEVICE, '', ''); - setTimeout(() => { + window.setTimeout(() => { callback(featureName, roomId, SettingLevel.DEVICE, '', ''); }, 1000); }); diff --git a/test/components/views/settings/Notifications-test.tsx b/test/components/views/settings/Notifications-test.tsx index da2bc9f3f8..df2a5f4b61 100644 --- a/test/components/views/settings/Notifications-test.tsx +++ b/test/components/views/settings/Notifications-test.tsx @@ -54,7 +54,7 @@ const encryptedGroupRule = { "conditions": [{ "kind": "event_match", "key": "typ // eslint-disable-next-line max-len const pushRules: IPushRules = { "global": { "underride": [{ "conditions": [{ "kind": "event_match", "key": "type", "pattern": "m.call.invite" }], "actions": ["notify", { "set_tweak": "sound", "value": "ring" }, { "set_tweak": "highlight", "value": false }], "rule_id": ".m.rule.call", "default": true, "enabled": true }, oneToOneRule, encryptedOneToOneRule, { "conditions": [{ "kind": "event_match", "key": "type", "pattern": "m.room.message" }], "actions": ["notify", { "set_tweak": "sound", "value": "default" }, { "set_tweak": "highlight", "value": false }], "rule_id": ".m.rule.message", "default": true, "enabled": true }, encryptedGroupRule, { "conditions": [{ "kind": "event_match", "key": "type", "pattern": "im.vector.modular.widgets" }, { "kind": "event_match", "key": "content.type", "pattern": "jitsi" }, { "kind": "event_match", "key": "state_key", "pattern": "*" }], "actions": ["notify", { "set_tweak": "highlight", "value": false }], "rule_id": ".im.vector.jitsi", "default": true, "enabled": true }], "sender": [], "room": [{ "actions": ["dont_notify"], "rule_id": "!zJPyWqpMorfCcWObge:matrix.org", "default": false, "enabled": true }], "content": [{ "actions": ["notify", { "set_tweak": "highlight", "value": false }], "pattern": "banana", "rule_id": "banana", "default": false, "enabled": true }, { "actions": ["notify", { "set_tweak": "sound", "value": "default" }, { "set_tweak": "highlight" }], "pattern": "kadev1", "rule_id": ".m.rule.contains_user_name", "default": true, "enabled": true }], "override": [{ "conditions": [], "actions": ["dont_notify"], "rule_id": ".m.rule.master", "default": true, "enabled": false }, { "conditions": [{ "kind": "event_match", "key": "content.msgtype", "pattern": "m.notice" }], "actions": ["dont_notify"], "rule_id": ".m.rule.suppress_notices", "default": true, "enabled": true }, { "conditions": [{ "kind": "event_match", "key": "type", "pattern": "m.room.member" }, { "kind": "event_match", "key": "content.membership", "pattern": "invite" }, { "kind": "event_match", "key": "state_key", "pattern": "@kadev1:matrix.org" }], "actions": ["notify", { "set_tweak": "sound", "value": "default" }, { "set_tweak": "highlight", "value": false }], "rule_id": ".m.rule.invite_for_me", "default": true, "enabled": true }, { "conditions": [{ "kind": "event_match", "key": "type", "pattern": "m.room.member" }], "actions": ["dont_notify"], "rule_id": ".m.rule.member_event", "default": true, "enabled": true }, { "conditions": [{ "kind": "contains_display_name" }], "actions": ["notify", { "set_tweak": "sound", "value": "default" }, { "set_tweak": "highlight" }], "rule_id": ".m.rule.contains_display_name", "default": true, "enabled": true }, { "conditions": [{ "kind": "event_match", "key": "content.body", "pattern": "@room" }, { "kind": "sender_notification_permission", "key": "room" }], "actions": ["notify", { "set_tweak": "highlight", "value": true }], "rule_id": ".m.rule.roomnotif", "default": true, "enabled": true }, { "conditions": [{ "kind": "event_match", "key": "type", "pattern": "m.room.tombstone" }, { "kind": "event_match", "key": "state_key", "pattern": "" }], "actions": ["notify", { "set_tweak": "highlight", "value": true }], "rule_id": ".m.rule.tombstone", "default": true, "enabled": true }, { "conditions": [{ "kind": "event_match", "key": "type", "pattern": "m.reaction" }], "actions": ["dont_notify"], "rule_id": ".m.rule.reaction", "default": true, "enabled": true }] }, "device": {} } as IPushRules; -const flushPromises = async () => await new Promise(resolve => setTimeout(resolve)); +const flushPromises = async () => await new Promise(resolve => window.setTimeout(resolve)); describe('', () => { const getComponent = () => render(); diff --git a/test/components/views/voip/PipView-test.tsx b/test/components/views/voip/PipView-test.tsx index 1dcc617e64..6a9105a413 100644 --- a/test/components/views/voip/PipView-test.tsx +++ b/test/components/views/voip/PipView-test.tsx @@ -184,6 +184,7 @@ describe("PipView", () => { room, alice, client, + voiceBroadcastPlaybacksStore, voiceBroadcastRecordingsStore, ); voiceBroadcastPreRecordingStore.setCurrent(voiceBroadcastPreRecording); @@ -271,6 +272,19 @@ describe("PipView", () => { }); }); + describe("when there is a voice broadcast playback and pre-recording", () => { + beforeEach(() => { + startVoiceBroadcastPlayback(room); + setUpVoiceBroadcastPreRecording(); + renderPip(); + }); + + it("should render the voice broadcast pre-recording PiP", () => { + // check for the „Go live“ button + expect(screen.queryByText("Go live")).toBeInTheDocument(); + }); + }); + describe("when there is a voice broadcast pre-recording", () => { beforeEach(() => { setUpVoiceBroadcastPreRecording(); diff --git a/test/setup/setupManualMocks.ts b/test/setup/setupManualMocks.ts index ada613feb9..8ee7750c9c 100644 --- a/test/setup/setupManualMocks.ts +++ b/test/setup/setupManualMocks.ts @@ -21,7 +21,7 @@ import fetch from 'node-fetch'; // jest 27 removes setImmediate from jsdom // polyfill until setImmediate use in client can be removed // @ts-ignore - we know the contract is wrong. That's why we're stubbing it. -global.setImmediate = callback => setTimeout(callback, 0); +global.setImmediate = callback => window.setTimeout(callback, 0); // Stub ResizeObserver // @ts-ignore - we know it's a duplicate (that's why we're stubbing it) diff --git a/test/test-utils/beacon.ts b/test/test-utils/beacon.ts index a58be78151..7ca5741bd5 100644 --- a/test/test-utils/beacon.ts +++ b/test/test-utils/beacon.ts @@ -179,7 +179,7 @@ export const watchPositionMockImplementation = (delays: number[], errorCodes: nu let totalDelay = 0; delays.map((delayMs, index) => { totalDelay += delayMs; - const timeout = setTimeout(() => { + const timeout = window.setTimeout(() => { if (errorCodes[index]) { error(getMockGeolocationPositionError(errorCodes[index], 'error message')); } else { diff --git a/test/test-utils/utilities.ts b/test/test-utils/utilities.ts index 76859da263..0f22ed8467 100644 --- a/test/test-utils/utilities.ts +++ b/test/test-utils/utilities.ts @@ -48,7 +48,7 @@ export function untilDispatch( let timeoutId; // set a timeout handler if needed if (timeout > 0) { - timeoutId = setTimeout(() => { + timeoutId = window.setTimeout(() => { if (!fulfilled) { reject(new Error(`untilDispatch: timed out at ${callerLine}`)); fulfilled = true; @@ -92,7 +92,7 @@ export function untilEmission( let timeoutId; // set a timeout handler if needed if (timeout > 0) { - timeoutId = setTimeout(() => { + timeoutId = window.setTimeout(() => { if (!fulfilled) { reject(new Error(`untilEmission: timed out at ${callerLine}`)); fulfilled = true; @@ -134,7 +134,7 @@ const findByTagAndAttr = (attr: string) => export const findByTagAndTestId = findByTagAndAttr('data-test-id'); -export const flushPromises = async () => await new Promise(resolve => setTimeout(resolve)); +export const flushPromises = async () => await new Promise(resolve => window.setTimeout(resolve)); // with jest's modern fake timers process.nextTick is also mocked, // flushing promises in the normal way then waits for some advancement diff --git a/test/voice-broadcast/components/atoms/VoiceBroadcastHeader-test.tsx b/test/voice-broadcast/components/atoms/VoiceBroadcastHeader-test.tsx index f056137813..e090841c82 100644 --- a/test/voice-broadcast/components/atoms/VoiceBroadcastHeader-test.tsx +++ b/test/voice-broadcast/components/atoms/VoiceBroadcastHeader-test.tsx @@ -35,12 +35,17 @@ describe("VoiceBroadcastHeader", () => { const sender = new RoomMember(roomId, userId); let container: Container; - const renderHeader = (live: VoiceBroadcastLiveness, showBroadcast: boolean = undefined): RenderResult => { + const renderHeader = ( + live: VoiceBroadcastLiveness, + showBroadcast?: boolean, + buffering?: boolean, + ): RenderResult => { return render(); }; @@ -51,6 +56,16 @@ describe("VoiceBroadcastHeader", () => { }); describe("when rendering a live broadcast header with broadcast info", () => { + beforeEach(() => { + container = renderHeader("live", true, true).container; + }); + + it("should render the header with a red live badge", () => { + expect(container).toMatchSnapshot(); + }); + }); + + describe("when rendering a buffering live broadcast header with broadcast info", () => { beforeEach(() => { container = renderHeader("live", true).container; }); diff --git a/test/voice-broadcast/components/atoms/__snapshots__/VoiceBroadcastHeader-test.tsx.snap b/test/voice-broadcast/components/atoms/__snapshots__/VoiceBroadcastHeader-test.tsx.snap index 1f4b657a22..c00d81e37d 100644 --- a/test/voice-broadcast/components/atoms/__snapshots__/VoiceBroadcastHeader-test.tsx.snap +++ b/test/voice-broadcast/components/atoms/__snapshots__/VoiceBroadcastHeader-test.tsx.snap @@ -1,5 +1,55 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`VoiceBroadcastHeader when rendering a buffering live broadcast header with broadcast info should render the header with a red live badge 1`] = ` +
+
+
+ room avatar: + !room:example.com +
+
+
+ !room:example.com +
+
+
+ + test user + +
+
+
+ Voice broadcast +
+
+
+
+ Live +
+
+
+`; + exports[`VoiceBroadcastHeader when rendering a live (grey) broadcast header with broadcast info should render the header with a grey live badge 1`] = `
Voice broadcast
+
+
+
+
+ Buffering… +
({ describe("VoiceBroadcastPreRecordingPip", () => { let renderResult: RenderResult; let preRecording: VoiceBroadcastPreRecording; + let playbacksStore: VoiceBroadcastPlaybacksStore; let recordingsStore: VoiceBroadcastRecordingsStore; let client: MatrixClient; let room: Room; @@ -51,6 +53,7 @@ describe("VoiceBroadcastPreRecordingPip", () => { client = stubClient(); room = new Room("!room@example.com", client, client.getUserId() || ""); sender = new RoomMember(room.roomId, client.getUserId() || ""); + playbacksStore = new VoiceBroadcastPlaybacksStore(); recordingsStore = new VoiceBroadcastRecordingsStore(); mocked(requestMediaPermissions).mockReturnValue(new Promise((r) => { r({ @@ -76,6 +79,7 @@ describe("VoiceBroadcastPreRecordingPip", () => { room, sender, client, + playbacksStore, recordingsStore, ); }); diff --git a/test/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastPlaybackBody-test.tsx.snap b/test/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastPlaybackBody-test.tsx.snap index f5d3e90b3c..67c2566fab 100644 --- a/test/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastPlaybackBody-test.tsx.snap +++ b/test/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastPlaybackBody-test.tsx.snap @@ -252,9 +252,17 @@ exports[`VoiceBroadcastPlaybackBody when rendering a buffering voice broadcast s class="mx_VoiceBroadcastHeader_line" >
- Voice broadcast + class="mx_Spinner" + > +
+
+ Buffering…
{ let client: MatrixClient; let room: Room; let sender: RoomMember; + let playbacksStore: VoiceBroadcastPlaybacksStore; let recordingsStore: VoiceBroadcastRecordingsStore; let preRecording: VoiceBroadcastPreRecording; let onDismiss: (voiceBroadcastPreRecording: VoiceBroadcastPreRecording) => void; @@ -38,12 +40,13 @@ describe("VoiceBroadcastPreRecording", () => { client = stubClient(); room = new Room(roomId, client, client.getUserId() || ""); sender = new RoomMember(roomId, client.getUserId() || ""); + playbacksStore = new VoiceBroadcastPlaybacksStore(); recordingsStore = new VoiceBroadcastRecordingsStore(); }); beforeEach(() => { onDismiss = jest.fn(); - preRecording = new VoiceBroadcastPreRecording(room, sender, client, recordingsStore); + preRecording = new VoiceBroadcastPreRecording(room, sender, client, playbacksStore, recordingsStore); preRecording.on("dismiss", onDismiss); }); @@ -56,6 +59,7 @@ describe("VoiceBroadcastPreRecording", () => { expect(startNewVoiceBroadcastRecording).toHaveBeenCalledWith( room, client, + playbacksStore, recordingsStore, ); }); diff --git a/test/voice-broadcast/stores/VoiceBroadcastPreRecordingStore-test.ts b/test/voice-broadcast/stores/VoiceBroadcastPreRecordingStore-test.ts index 36983ae601..97e944b564 100644 --- a/test/voice-broadcast/stores/VoiceBroadcastPreRecordingStore-test.ts +++ b/test/voice-broadcast/stores/VoiceBroadcastPreRecordingStore-test.ts @@ -18,6 +18,7 @@ import { mocked } from "jest-mock"; import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix"; import { + VoiceBroadcastPlaybacksStore, VoiceBroadcastPreRecording, VoiceBroadcastPreRecordingStore, VoiceBroadcastRecordingsStore, @@ -31,6 +32,7 @@ describe("VoiceBroadcastPreRecordingStore", () => { let client: MatrixClient; let room: Room; let sender: RoomMember; + let playbacksStore: VoiceBroadcastPlaybacksStore; let recordingsStore: VoiceBroadcastRecordingsStore; let store: VoiceBroadcastPreRecordingStore; let preRecording1: VoiceBroadcastPreRecording; @@ -39,6 +41,7 @@ describe("VoiceBroadcastPreRecordingStore", () => { client = stubClient(); room = new Room(roomId, client, client.getUserId() || ""); sender = new RoomMember(roomId, client.getUserId() || ""); + playbacksStore = new VoiceBroadcastPlaybacksStore(); recordingsStore = new VoiceBroadcastRecordingsStore(); }); @@ -46,7 +49,7 @@ describe("VoiceBroadcastPreRecordingStore", () => { store = new VoiceBroadcastPreRecordingStore(); jest.spyOn(store, "emit"); jest.spyOn(store, "removeAllListeners"); - preRecording1 = new VoiceBroadcastPreRecording(room, sender, client, recordingsStore); + preRecording1 = new VoiceBroadcastPreRecording(room, sender, client, playbacksStore, recordingsStore); jest.spyOn(preRecording1, "off"); }); @@ -117,7 +120,7 @@ describe("VoiceBroadcastPreRecordingStore", () => { beforeEach(() => { mocked(store.emit).mockClear(); mocked(preRecording1.off).mockClear(); - preRecording2 = new VoiceBroadcastPreRecording(room, sender, client, recordingsStore); + preRecording2 = new VoiceBroadcastPreRecording(room, sender, client, playbacksStore, recordingsStore); store.setCurrent(preRecording2); }); diff --git a/test/voice-broadcast/utils/setUpVoiceBroadcastPreRecording-test.ts b/test/voice-broadcast/utils/setUpVoiceBroadcastPreRecording-test.ts index 0b05d26912..4779813165 100644 --- a/test/voice-broadcast/utils/setUpVoiceBroadcastPreRecording-test.ts +++ b/test/voice-broadcast/utils/setUpVoiceBroadcastPreRecording-test.ts @@ -15,16 +15,20 @@ limitations under the License. */ import { mocked } from "jest-mock"; -import { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; +import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; import { checkVoiceBroadcastPreConditions, + VoiceBroadcastInfoState, + VoiceBroadcastPlayback, + VoiceBroadcastPlaybacksStore, VoiceBroadcastPreRecording, VoiceBroadcastPreRecordingStore, VoiceBroadcastRecordingsStore, } from "../../../src/voice-broadcast"; import { setUpVoiceBroadcastPreRecording } from "../../../src/voice-broadcast/utils/setUpVoiceBroadcastPreRecording"; import { mkRoomMemberJoinEvent, stubClient } from "../../test-utils"; +import { mkVoiceBroadcastInfoStateEvent } from "./test-utils"; jest.mock("../../../src/voice-broadcast/utils/checkVoiceBroadcastPreConditions"); @@ -34,11 +38,20 @@ describe("setUpVoiceBroadcastPreRecording", () => { let userId: string; let room: Room; let preRecordingStore: VoiceBroadcastPreRecordingStore; + let infoEvent: MatrixEvent; + let playback: VoiceBroadcastPlayback; + let playbacksStore: VoiceBroadcastPlaybacksStore; let recordingsStore: VoiceBroadcastRecordingsStore; const itShouldReturnNull = () => { it("should return null", () => { - expect(setUpVoiceBroadcastPreRecording(room, client, recordingsStore, preRecordingStore)).toBeNull(); + expect(setUpVoiceBroadcastPreRecording( + room, + client, + playbacksStore, + recordingsStore, + preRecordingStore, + )).toBeNull(); expect(checkVoiceBroadcastPreConditions).toHaveBeenCalledWith(room, client, recordingsStore); }); }; @@ -51,7 +64,16 @@ describe("setUpVoiceBroadcastPreRecording", () => { userId = clientUserId; room = new Room(roomId, client, userId); + infoEvent = mkVoiceBroadcastInfoStateEvent( + roomId, + VoiceBroadcastInfoState.Started, + client.getUserId()!, + client.getDeviceId()!, + ); preRecordingStore = new VoiceBroadcastPreRecordingStore(); + playback = new VoiceBroadcastPlayback(infoEvent, client); + jest.spyOn(playback, "pause"); + playbacksStore = new VoiceBroadcastPlaybacksStore(); recordingsStore = new VoiceBroadcastRecordingsStore(); }); @@ -85,15 +107,25 @@ describe("setUpVoiceBroadcastPreRecording", () => { itShouldReturnNull(); }); - describe("and there is a room member", () => { + describe("and there is a room member and listening to another broadcast", () => { beforeEach(() => { + playbacksStore.setCurrent(playback); room.currentState.setStateEvents([ mkRoomMemberJoinEvent(userId, roomId), ]); }); - it("should create a voice broadcast pre-recording", () => { - const result = setUpVoiceBroadcastPreRecording(room, client, recordingsStore, preRecordingStore); + it("should pause the current playback and create a voice broadcast pre-recording", () => { + const result = setUpVoiceBroadcastPreRecording( + room, + client, + playbacksStore, + recordingsStore, + preRecordingStore, + ); + expect(playback.pause).toHaveBeenCalled(); + expect(playbacksStore.getCurrent()).toBeNull(); + expect(checkVoiceBroadcastPreConditions).toHaveBeenCalledWith(room, client, recordingsStore); expect(result).toBeInstanceOf(VoiceBroadcastPreRecording); }); diff --git a/test/voice-broadcast/utils/startNewVoiceBroadcastRecording-test.ts b/test/voice-broadcast/utils/startNewVoiceBroadcastRecording-test.ts index 1873a4b513..5eac6ef803 100644 --- a/test/voice-broadcast/utils/startNewVoiceBroadcastRecording-test.ts +++ b/test/voice-broadcast/utils/startNewVoiceBroadcastRecording-test.ts @@ -15,7 +15,7 @@ limitations under the License. */ import { mocked } from "jest-mock"; -import { EventType, MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; +import { EventType, ISendEventResponse, MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; import Modal from "../../../src/Modal"; import { @@ -24,6 +24,8 @@ import { VoiceBroadcastInfoState, VoiceBroadcastRecordingsStore, VoiceBroadcastRecording, + VoiceBroadcastPlaybacksStore, + VoiceBroadcastPlayback, } from "../../../src/voice-broadcast"; import { mkEvent, stubClient } from "../../test-utils"; import { mkVoiceBroadcastInfoStateEvent } from "./test-utils"; @@ -38,6 +40,7 @@ describe("startNewVoiceBroadcastRecording", () => { const roomId = "!room:example.com"; const otherUserId = "@other:example.com"; let client: MatrixClient; + let playbacksStore: VoiceBroadcastPlaybacksStore; let recordingsStore: VoiceBroadcastRecordingsStore; let room: Room; let infoEvent: MatrixEvent; @@ -46,45 +49,50 @@ describe("startNewVoiceBroadcastRecording", () => { beforeEach(() => { client = stubClient(); - room = new Room(roomId, client, client.getUserId()); + room = new Room(roomId, client, client.getUserId()!); jest.spyOn(room.currentState, "maySendStateEvent"); mocked(client.getRoom).mockImplementation((getRoomId: string) => { if (getRoomId === roomId) { return room; } + + return null; }); mocked(client.sendStateEvent).mockImplementation(( sendRoomId: string, eventType: string, - _content: any, - _stateKey: string, - ) => { + content: any, + stateKey: string, + ): Promise => { if (sendRoomId === roomId && eventType === VoiceBroadcastInfoEventType) { - return Promise.resolve({ event_id: infoEvent.getId() }); + return Promise.resolve({ event_id: infoEvent.getId()! }); } - }); - recordingsStore = { - setCurrent: jest.fn(), - getCurrent: jest.fn(), - } as unknown as VoiceBroadcastRecordingsStore; + throw new Error("Unexpected sendStateEvent call"); + }); infoEvent = mkVoiceBroadcastInfoStateEvent( roomId, VoiceBroadcastInfoState.Started, - client.getUserId(), - client.getDeviceId(), + client.getUserId()!, + client.getDeviceId()!, ); otherEvent = mkEvent({ event: true, type: EventType.RoomMember, content: {}, - user: client.getUserId(), + user: client.getUserId()!, room: roomId, skey: "", }); + playbacksStore = new VoiceBroadcastPlaybacksStore(); + recordingsStore = { + setCurrent: jest.fn(), + getCurrent: jest.fn(), + } as unknown as VoiceBroadcastRecordingsStore; + mocked(VoiceBroadcastRecording).mockImplementation(( infoEvent: MatrixEvent, client: MatrixClient, @@ -106,22 +114,35 @@ describe("startNewVoiceBroadcastRecording", () => { mocked(room.currentState.maySendStateEvent).mockReturnValue(true); }); - describe("when there currently is no other broadcast", () => { - it("should create a new Voice Broadcast", async () => { + describe("when currently listening to a broadcast and there is no recording", () => { + let playback: VoiceBroadcastPlayback; + + beforeEach(() => { + playback = new VoiceBroadcastPlayback(infoEvent, client); + jest.spyOn(playback, "pause"); + playbacksStore.setCurrent(playback); + }); + + it("should stop listen to the current broadcast and create a new recording", async () => { mocked(client.sendStateEvent).mockImplementation(async ( _roomId: string, _eventType: string, _content: any, _stateKey = "", - ) => { - setTimeout(() => { + ): Promise => { + window.setTimeout(() => { // emit state events after resolving the promise room.currentState.setStateEvents([otherEvent]); room.currentState.setStateEvents([infoEvent]); }, 0); - return { event_id: infoEvent.getId() }; + return { event_id: infoEvent.getId()! }; }); - const recording = await startNewVoiceBroadcastRecording(room, client, recordingsStore); + const recording = await startNewVoiceBroadcastRecording(room, client, playbacksStore, recordingsStore); + expect(recording).not.toBeNull(); + + // expect to stop and clear the current playback + expect(playback.pause).toHaveBeenCalled(); + expect(playbacksStore.getCurrent()).toBeNull(); expect(client.sendStateEvent).toHaveBeenCalledWith( roomId, @@ -133,8 +154,8 @@ describe("startNewVoiceBroadcastRecording", () => { }, client.getUserId(), ); - expect(recording.infoEvent).toBe(infoEvent); - expect(recording.start).toHaveBeenCalled(); + expect(recording!.infoEvent).toBe(infoEvent); + expect(recording!.start).toHaveBeenCalled(); }); }); @@ -144,7 +165,7 @@ describe("startNewVoiceBroadcastRecording", () => { new VoiceBroadcastRecording(infoEvent, client), ); - result = await startNewVoiceBroadcastRecording(room, client, recordingsStore); + result = await startNewVoiceBroadcastRecording(room, client, playbacksStore, recordingsStore); }); it("should not start a voice broadcast", () => { @@ -162,12 +183,12 @@ describe("startNewVoiceBroadcastRecording", () => { mkVoiceBroadcastInfoStateEvent( roomId, VoiceBroadcastInfoState.Resumed, - client.getUserId(), - client.getDeviceId(), + client.getUserId()!, + client.getDeviceId()!, ), ]); - result = await startNewVoiceBroadcastRecording(room, client, recordingsStore); + result = await startNewVoiceBroadcastRecording(room, client, playbacksStore, recordingsStore); }); it("should not start a voice broadcast", () => { @@ -190,7 +211,7 @@ describe("startNewVoiceBroadcastRecording", () => { ), ]); - result = await startNewVoiceBroadcastRecording(room, client, recordingsStore); + result = await startNewVoiceBroadcastRecording(room, client, playbacksStore, recordingsStore); }); it("should not start a voice broadcast", () => { @@ -206,7 +227,7 @@ describe("startNewVoiceBroadcastRecording", () => { describe("when the current user is not allowed to send voice broadcast info state events", () => { beforeEach(async () => { mocked(room.currentState.maySendStateEvent).mockReturnValue(false); - result = await startNewVoiceBroadcastRecording(room, client, recordingsStore); + result = await startNewVoiceBroadcastRecording(room, client, playbacksStore, recordingsStore); }); it("should not start a voice broadcast", () => { diff --git a/yarn.lock b/yarn.lock index 33766642e4..acb7219d8e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2451,10 +2451,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.26.tgz#239e19f8b4ea1a9eb710528061c1d733dc561996" integrity sha512-0b+utRBSYj8L7XAp0d+DX7lI4cSmowNaaTkk6/1SKzbKkG+doLuPusB9EOvzLJ8ahJSk03bTLIL6cWaEd4dBKA== -"@types/node@^14.18.28": - version "14.18.28" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.28.tgz#ddb82da2fff476a8e827e8773c84c19d9c235278" - integrity sha512-CK2fnrQlIgKlCV3N2kM+Gznb5USlwA1KFX3rJVHmgVk6NJxFPuQ86pAcvKnu37IA4BGlSRz7sEE1lHL1aLZ/eQ== +"@types/node@^16": + version "16.18.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.3.tgz#d7f7ba828ad9e540270f01ce00d391c54e6e0abc" + integrity sha512-jh6m0QUhIRcZpNv7Z/rpN+ZWXOicUUQbSoWks7Htkbb9IjFQj4kzcX/xFCkjstCj5flMsN8FiSvt+q+Tcs4Llg== "@types/normalize-package-data@^2.4.0": version "2.4.1" @@ -9469,10 +9469,10 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" -typescript@4.8.4: - version "4.8.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6" - integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ== +typescript@4.9.3: + version "4.9.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.3.tgz#3aea307c1746b8c384435d8ac36b8a2e580d85db" + integrity sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA== ua-parser-js@^0.7.30: version "0.7.31"