diff --git a/.eslintrc.js b/.eslintrc.js index 7885cfd88d2..8cc5890ca46 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,12 +1,6 @@ module.exports = { - plugins: [ - "matrix-org", - ], - extends: [ - "plugin:matrix-org/babel", - "plugin:matrix-org/react", - "plugin:matrix-org/a11y", - ], + plugins: ["matrix-org"], + extends: ["plugin:matrix-org/babel", "plugin:matrix-org/react", "plugin:matrix-org/a11y"], env: { browser: true, node: true, @@ -19,7 +13,6 @@ module.exports = { "no-constant-condition": "off", "prefer-promise-reject-errors": "off", "no-async-promise-executor": "off", - "quotes": "off", "no-extra-boolean-cast": "off", // Bind or arrow functions in props causes performance issues (but we @@ -41,34 +34,47 @@ module.exports = { ], // Ban matrix-js-sdk/src imports in favour of matrix-js-sdk/src/matrix imports to prevent unleashing hell. - "no-restricted-imports": ["error", { - "paths": [{ - "name": "matrix-js-sdk", - "message": "Please use matrix-js-sdk/src/matrix instead", - }, { - "name": "matrix-js-sdk/", - "message": "Please use matrix-js-sdk/src/matrix instead", - }, { - "name": "matrix-js-sdk/src", - "message": "Please use matrix-js-sdk/src/matrix instead", - }, { - "name": "matrix-js-sdk/src/", - "message": "Please use matrix-js-sdk/src/matrix instead", - }, { - "name": "matrix-js-sdk/src/index", - "message": "Please use matrix-js-sdk/src/matrix instead", - }, { - "name": "matrix-react-sdk", - "message": "Please use matrix-react-sdk/src/index instead", - }, { - "name": "matrix-react-sdk/", - "message": "Please use matrix-react-sdk/src/index instead", - }], - "patterns": [{ - "group": ["matrix-js-sdk/lib", "matrix-js-sdk/lib/", "matrix-js-sdk/lib/**"], - "message": "Please use matrix-js-sdk/src/* instead", - }], - }], + "no-restricted-imports": [ + "error", + { + paths: [ + { + name: "matrix-js-sdk", + message: "Please use matrix-js-sdk/src/matrix instead", + }, + { + name: "matrix-js-sdk/", + message: "Please use matrix-js-sdk/src/matrix instead", + }, + { + name: "matrix-js-sdk/src", + message: "Please use matrix-js-sdk/src/matrix instead", + }, + { + name: "matrix-js-sdk/src/", + message: "Please use matrix-js-sdk/src/matrix instead", + }, + { + name: "matrix-js-sdk/src/index", + message: "Please use matrix-js-sdk/src/matrix instead", + }, + { + name: "matrix-react-sdk", + message: "Please use matrix-react-sdk/src/index instead", + }, + { + name: "matrix-react-sdk/", + message: "Please use matrix-react-sdk/src/index instead", + }, + ], + patterns: [ + { + group: ["matrix-js-sdk/lib", "matrix-js-sdk/lib/", "matrix-js-sdk/lib/**"], + message: "Please use matrix-js-sdk/src/* instead", + }, + ], + }, + ], // There are too many a11y violations to fix at once // Turn violated rules off until they are fixed @@ -91,19 +97,15 @@ module.exports = { }, overrides: [ { - files: [ - "src/**/*.{ts,tsx}", - "test/**/*.{ts,tsx}", - "cypress/**/*.ts", - ], - extends: [ - "plugin:matrix-org/typescript", - "plugin:matrix-org/react", - ], + files: ["src/**/*.{ts,tsx}", "test/**/*.{ts,tsx}", "cypress/**/*.ts"], + extends: ["plugin:matrix-org/typescript", "plugin:matrix-org/react"], rules: { + // temporary disabled + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/explicit-member-accessibility": "off", + // Things we do that break the ideal style "prefer-promise-reject-errors": "off", - "quotes": "off", "no-extra-boolean-cast": "off", // Remove Babel things manually due to override limitations @@ -117,10 +119,6 @@ module.exports = { "@typescript-eslint/ban-ts-comment": "off", // We're okay with assertion errors when we ask for them "@typescript-eslint/no-non-null-assertion": "off", - - // The non-TypeScript rule produces false positives - "func-call-spacing": "off", - "@typescript-eslint/func-call-spacing": ["error"], }, }, // temporary override for offending icon require files @@ -153,12 +151,12 @@ module.exports = { "src/components/views/rooms/MessageComposer.tsx", "src/components/views/rooms/ReplyPreview.tsx", "src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx", - "src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx" + "src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx", ], rules: { "@typescript-eslint/no-var-requires": "off", }, - } + }, ], settings: { react: { @@ -168,7 +166,7 @@ module.exports = { }; function buildRestrictedPropertiesOptions(properties, message) { - return properties.map(prop => { + return properties.map((prop) => { let [object, property] = prop.split("."); if (object === "*") { object = undefined; diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000000..a1feb1a6e43 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# prettier +526645c79160ab1ad4b4c3845de27d51263a405e diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2c068fff330..16574bad790 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1,4 @@ -* @matrix-org/element-web +* @matrix-org/element-web +/.github/workflows/** @matrix-org/element-web-app-team +/package.json @matrix-org/element-web-app-team +/yarn.lock @matrix-org/element-web-app-team diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 068c3ffcccb..0527dcf64c6 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,9 +2,9 @@ ## Checklist -* [ ] Tests written for new code (and old code if feasible) -* [ ] Linter and other CI checks pass -* [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-react-sdk/blob/develop/CONTRIBUTING.md)) +- [ ] Tests written for new code (and old code if feasible) +- [ ] Linter and other CI checks pass +- [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-react-sdk/blob/develop/CONTRIBUTING.md)) - - + --> + diff --git a/src/usercontent/index.ts b/src/usercontent/index.ts index 91a384cfc09..db710358c64 100644 --- a/src/usercontent/index.ts +++ b/src/usercontent/index.ts @@ -56,7 +56,7 @@ function remoteRender(event: MessageEvent): void { const body = document.body; // Don't display scrollbars if the link takes more than one line to display. - body.style .margin = "0px"; + body.style.margin = "0px"; body.style.overflow = "hidden"; body.appendChild(a); @@ -65,7 +65,7 @@ function remoteRender(event: MessageEvent): void { } } -window.onmessage = function(e: MessageEvent): void { +window.onmessage = function (e: MessageEvent): void { if (e.origin === window.location.origin) { if (e.data.blob) remoteRender(e); } diff --git a/src/utils/AutoDiscoveryUtils.tsx b/src/utils/AutoDiscoveryUtils.tsx index f995f93304c..54829cd407b 100644 --- a/src/utils/AutoDiscoveryUtils.tsx +++ b/src/utils/AutoDiscoveryUtils.tsx @@ -14,14 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ReactNode } from 'react'; +import React, { ReactNode } from "react"; import { AutoDiscovery } from "matrix-js-sdk/src/autodiscovery"; import { logger } from "matrix-js-sdk/src/logger"; import { _t, _td, newTranslatableError } from "../languageHandler"; import { makeType } from "./TypeUtils"; -import SdkConfig from '../SdkConfig'; -import { ValidatedServerConfig } from './ValidatedServerConfig'; +import SdkConfig from "../SdkConfig"; +import { ValidatedServerConfig } from "./ValidatedServerConfig"; const LIVELINESS_DISCOVERY_ERRORS: string[] = [ AutoDiscovery.ERROR_INVALID_HOMESERVER, @@ -44,7 +44,9 @@ export default class AutoDiscoveryUtils { */ static isLivelinessError(error: string | Error): boolean { if (!error) return false; - return !!LIVELINESS_DISCOVERY_ERRORS.find(e => typeof error === "string" ? e === error : e === error.message); + return !!LIVELINESS_DISCOVERY_ERRORS.find((e) => + typeof error === "string" ? e === error : e === error.message, + ); } /** @@ -75,11 +77,15 @@ export default class AutoDiscoveryUtils { }, { a: (sub) => { - return { sub }; + return ( + + {sub} + + ); }, }, ); @@ -96,20 +102,20 @@ export default class AutoDiscoveryUtils { if (pageName === "register") { body = _t( "You can register, but some features will be unavailable until the identity server is " + - "back online. If you keep seeing this warning, check your configuration or contact a server " + - "admin.", + "back online. If you keep seeing this warning, check your configuration or contact a server " + + "admin.", ); } else if (pageName === "reset_password") { body = _t( "You can reset your password, but some features will be unavailable until the identity " + - "server is back online. If you keep seeing this warning, check your configuration or contact " + - "a server admin.", + "server is back online. If you keep seeing this warning, check your configuration or contact " + + "a server admin.", ); } else { body = _t( "You can log in, but some features will be unavailable until the identity server is " + - "back online. If you keep seeing this warning, check your configuration or contact a server " + - "admin.", + "back online. If you keep seeing this warning, check your configuration or contact a server " + + "admin.", ); } } @@ -119,8 +125,8 @@ export default class AutoDiscoveryUtils { serverErrorIsFatal: isFatalError, serverDeadError: (
- { title } -
{ body }
+ {title} +
{body}
), }; @@ -150,7 +156,7 @@ export default class AutoDiscoveryUtils { }; if (identityUrl) { - wellknownConfig['m.identity_server'] = { + wellknownConfig["m.identity_server"] = { base_url: identityUrl, }; } @@ -183,7 +189,11 @@ export default class AutoDiscoveryUtils { * @returns {Promise} Resolves to the validated configuration. */ static buildValidatedConfigFromDiscovery( - serverName: string, discoveryResult, syntaxOnly=false, isSynthetic=false): ValidatedServerConfig { + serverName: string, + discoveryResult, + syntaxOnly = false, + isSynthetic = false, + ): ValidatedServerConfig { if (!discoveryResult || !discoveryResult["m.homeserver"]) { // This shouldn't happen without major misconfiguration, so we'll log a bit of information // in the log so we can find this bit of codee but otherwise tell teh user "it broke". @@ -191,8 +201,8 @@ export default class AutoDiscoveryUtils { throw newTranslatableError(_td("Unexpected error resolving homeserver configuration")); } - const hsResult = discoveryResult['m.homeserver']; - const isResult = discoveryResult['m.identity_server']; + const hsResult = discoveryResult["m.homeserver"]; + const isResult = discoveryResult["m.identity_server"]; const defaultConfig = SdkConfig.get("validated_server_config"); @@ -203,7 +213,7 @@ export default class AutoDiscoveryUtils { // lack of identity server provided by the discovery method), we intentionally do not // validate it. This has already been validated and this helps some off-the-grid usage // of Element. - let preferredIdentityUrl = defaultConfig && defaultConfig['isUrl']; + let preferredIdentityUrl = defaultConfig && defaultConfig["isUrl"]; if (isResult && isResult.state === AutoDiscovery.SUCCESS) { preferredIdentityUrl = isResult["base_url"]; } else if (isResult && isResult.state !== AutoDiscovery.PROMPT) { diff --git a/src/utils/BrowserWorkarounds.ts b/src/utils/BrowserWorkarounds.ts index ea8ea2a04f9..d241af37332 100644 --- a/src/utils/BrowserWorkarounds.ts +++ b/src/utils/BrowserWorkarounds.ts @@ -20,5 +20,5 @@ export function chromeFileInputFix(event: MouseEvent): void { // Workaround for Chromium Bug // Chrome does not fire onChange events if the same file is selected twice // Only required on Chromium-based browsers (Electron, Chrome, Edge, Opera, Vivaldi, etc) - event.currentTarget.value = ''; + event.currentTarget.value = ""; } diff --git a/src/utils/DMRoomMap.ts b/src/utils/DMRoomMap.ts index 811522a667c..2844f5c8efd 100644 --- a/src/utils/DMRoomMap.ts +++ b/src/utils/DMRoomMap.ts @@ -22,7 +22,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { Optional } from "matrix-events-sdk"; -import { MatrixClientPeg } from '../MatrixClientPeg'; +import { MatrixClientPeg } from "../MatrixClientPeg"; /** * Class that takes a Matrix Client and flips the m.direct map @@ -35,10 +35,10 @@ export default class DMRoomMap { private static sharedInstance: DMRoomMap; // TODO: convert these to maps - private roomToUser: {[key: string]: string} = null; - private userToRooms: {[key: string]: string[]} = null; + private roomToUser: { [key: string]: string } = null; + private userToRooms: { [key: string]: string[] } = null; private hasSentOutPatchDirectAccountDataPatch: boolean; - private mDirectEvent: {[key: string]: string[]}; + private mDirectEvent: { [key: string]: string[] }; constructor(private readonly matrixClient: MatrixClient) { // see onAccountData @@ -102,23 +102,24 @@ export default class DMRoomMap { const selfRoomIds = userToRooms[myUserId]; if (selfRoomIds) { // any self-chats that should not be self-chats? - const guessedUserIdsThatChanged = selfRoomIds.map((roomId) => { - const room = this.matrixClient.getRoom(roomId); - if (room) { - const userId = room.guessDMUserId(); - if (userId && userId !== myUserId) { - return { userId, roomId }; + const guessedUserIdsThatChanged = selfRoomIds + .map((roomId) => { + const room = this.matrixClient.getRoom(roomId); + if (room) { + const userId = room.guessDMUserId(); + if (userId && userId !== myUserId) { + return { userId, roomId }; + } } - } - }).filter((ids) => !!ids); //filter out + }) + .filter((ids) => !!ids); //filter out // these are actually all legit self-chats // bail out if (!guessedUserIdsThatChanged.length) { return false; } userToRooms[myUserId] = selfRoomIds.filter((roomId) => { - return !guessedUserIdsThatChanged - .some((ids) => ids.roomId === roomId); + return !guessedUserIdsThatChanged.some((ids) => ids.roomId === roomId); }); guessedUserIdsThatChanged.forEach(({ userId, roomId }) => { const roomIds = userToRooms[userId]; @@ -151,11 +152,12 @@ export default class DMRoomMap { let commonRooms = this.getDMRoomsForUserId(ids[0]); for (let i = 1; i < ids.length; i++) { const userRooms = this.getDMRoomsForUserId(ids[i]); - commonRooms = commonRooms.filter(r => userRooms.includes(r)); + commonRooms = commonRooms.filter((r) => userRooms.includes(r)); } - const joinedRooms = commonRooms.map(r => MatrixClientPeg.get().getRoom(r)) - .filter(r => r && r.getMyMembership() === 'join'); + const joinedRooms = commonRooms + .map((r) => MatrixClientPeg.get().getRoom(r)) + .filter((r) => r && r.getMyMembership() === "join"); return joinedRooms[0]; } @@ -182,15 +184,15 @@ export default class DMRoomMap { return this.roomToUser[roomId]; } - public getUniqueRoomsWithIndividuals(): {[userId: string]: Room} { + public getUniqueRoomsWithIndividuals(): { [userId: string]: Room } { if (!this.roomToUser) return {}; // No rooms means no map. return Object.keys(this.roomToUser) - .map(r => ({ userId: this.getUserIdForRoomId(r), room: this.matrixClient.getRoom(r) })) - .filter(r => r.userId && r.room && r.room.getInvitedAndJoinedMemberCount() === 2) + .map((r) => ({ userId: this.getUserIdForRoomId(r), room: this.matrixClient.getRoom(r) })) + .filter((r) => r.userId && r.room && r.room.getInvitedAndJoinedMemberCount() === 2) .reduce((obj, r) => (obj[r.userId] = r.room) && obj, {}); } - private getUserToRooms(): {[key: string]: string[]} { + private getUserToRooms(): { [key: string]: string[] } { if (!this.userToRooms) { const userToRooms = this.mDirectEvent; const myUserId = this.matrixClient.getUserId(); @@ -200,8 +202,9 @@ export default class DMRoomMap { // to avoid multiple devices fighting to correct // the account data, only try to send the corrected // version once. - logger.warn(`Invalid m.direct account data detected ` + - `(self-chats that shouldn't be), patching it up.`); + logger.warn( + `Invalid m.direct account data detected ` + `(self-chats that shouldn't be), patching it up.`, + ); if (neededPatching && !this.hasSentOutPatchDirectAccountDataPatch) { this.hasSentOutPatchDirectAccountDataPatch = true; this.matrixClient.setAccountData(EventType.Direct, userToRooms); diff --git a/src/utils/DecryptFile.ts b/src/utils/DecryptFile.ts index dbcb0a85edb..aa9aaa428e6 100644 --- a/src/utils/DecryptFile.ts +++ b/src/utils/DecryptFile.ts @@ -15,8 +15,8 @@ limitations under the License. */ // Pull in the encryption lib so that we can decrypt attachments. -import encrypt from 'matrix-encrypt-attachment'; -import { parseErrorResponse } from 'matrix-js-sdk/src/http-api'; +import encrypt from "matrix-encrypt-attachment"; +import { parseErrorResponse } from "matrix-js-sdk/src/http-api"; import { mediaFromContent } from "../customisations/Media"; import { IEncryptedFile, IMediaEventInfo } from "../customisations/models/IMediaEventContent"; @@ -47,10 +47,7 @@ export class DecryptError extends Error { * @param {IMediaEventInfo} info The info parameter taken from the matrix event. * @returns {Promise} Resolves to a Blob of the file. */ -export async function decryptFile( - file: IEncryptedFile, - info?: IMediaEventInfo, -): Promise { +export async function decryptFile(file: IEncryptedFile, info?: IMediaEventInfo): Promise { const media = mediaFromContent({ file }); let responseData: ArrayBuffer; @@ -74,7 +71,7 @@ export async function decryptFile( // they introduce XSS attacks if the Blob URI is viewed directly in the // browser (e.g. by copying the URI into a new tab or window.) // See warning at top of file. - let mimetype = info?.mimetype ? info.mimetype.split(";")[0].trim() : ''; + let mimetype = info?.mimetype ? info.mimetype.split(";")[0].trim() : ""; mimetype = getBlobSafeMimeType(mimetype); return new Blob([dataArray], { type: mimetype }); diff --git a/src/utils/DialogOpener.ts b/src/utils/DialogOpener.ts index 82d16962b26..49e6f85658f 100644 --- a/src/utils/DialogOpener.ts +++ b/src/utils/DialogOpener.ts @@ -43,8 +43,7 @@ export class DialogOpener { private isRegistered = false; - private constructor() { - } + private constructor() {} // We could do this in the constructor, but then we wouldn't have // a function to call from Lifecycle to capture the class. @@ -56,11 +55,17 @@ export class DialogOpener { private onDispatch = (payload: ActionPayload) => { switch (payload.action) { - case 'open_room_settings': - Modal.createDialog(RoomSettingsDialog, { - roomId: payload.room_id || SdkContextClass.instance.roomViewStore.getRoomId(), - initialTabId: payload.initial_tab_id, - }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); + case "open_room_settings": + Modal.createDialog( + RoomSettingsDialog, + { + roomId: payload.room_id || SdkContextClass.instance.roomViewStore.getRoomId(), + initialTabId: payload.initial_tab_id, + }, + /*className=*/ null, + /*isPriority=*/ false, + /*isStatic=*/ true, + ); break; case Action.OpenForwardDialog: Modal.createDialog(ForwardDialog, { @@ -70,31 +75,52 @@ export class DialogOpener { }); break; case Action.OpenReportEventDialog: - Modal.createDialog(ReportEventDialog, { - mxEvent: payload.event, - }, 'mx_Dialog_reportEvent'); + Modal.createDialog( + ReportEventDialog, + { + mxEvent: payload.event, + }, + "mx_Dialog_reportEvent", + ); break; case Action.OpenSpacePreferences: - Modal.createDialog(SpacePreferencesDialog, { - initialTabId: payload.initalTabId, - space: payload.space, - }, null, false, true); + Modal.createDialog( + SpacePreferencesDialog, + { + initialTabId: payload.initalTabId, + space: payload.space, + }, + null, + false, + true, + ); break; case Action.OpenSpaceSettings: - Modal.createDialog(SpaceSettingsDialog, { - matrixClient: payload.space.client, - space: payload.space, - }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); + Modal.createDialog( + SpaceSettingsDialog, + { + matrixClient: payload.space.client, + space: payload.space, + }, + /*className=*/ null, + /*isPriority=*/ false, + /*isStatic=*/ true, + ); break; case Action.OpenInviteDialog: - Modal.createDialog(InviteDialog, { - kind: payload.kind, - call: payload.call, - roomId: payload.roomId, - }, classnames("mx_InviteDialog_flexWrapper", payload.className), false, true).finished - .then((results) => { - payload.onFinishedCallback?.(results); - }); + Modal.createDialog( + InviteDialog, + { + kind: payload.kind, + call: payload.call, + roomId: payload.roomId, + }, + classnames("mx_InviteDialog_flexWrapper", payload.className), + false, + true, + ).finished.then((results) => { + payload.onFinishedCallback?.(results); + }); break; case Action.OpenAddToExistingSpaceDialog: { const space = payload.space; diff --git a/src/utils/DirectoryUtils.ts b/src/utils/DirectoryUtils.ts index c5bb9578299..a74cf6d7f9a 100644 --- a/src/utils/DirectoryUtils.ts +++ b/src/utils/DirectoryUtils.ts @@ -14,35 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { IInstance, IProtocol } from "matrix-js-sdk/src/client"; - -// XXX: We would ideally use a symbol here but we can't since we save this value to localStorage -export const ALL_ROOMS = "ALL_ROOMS"; +import { IProtocol } from "matrix-js-sdk/src/client"; export type Protocols = Record; - -// Find a protocol 'instance' with a given instance_id -// in the supplied protocols dict -export function instanceForInstanceId(protocols: Protocols, instanceId: string | null | undefined): IInstance | null { - if (!instanceId) return null; - for (const proto of Object.keys(protocols)) { - if (!Array.isArray(protocols[proto].instances)) continue; - for (const instance of protocols[proto].instances) { - if (instance.instance_id == instanceId) return instance; - } - } - return null; -} - -// given an instance_id, return the name of the protocol for -// that instance ID in the supplied protocols dict -export function protocolNameForInstanceId(protocols: Protocols, instanceId: string | null | undefined): string | null { - if (!instanceId) return null; - for (const proto of Object.keys(protocols)) { - if (!Array.isArray(protocols[proto].instances)) continue; - for (const instance of protocols[proto].instances) { - if (instance.instance_id == instanceId) return proto; - } - } - return null; -} diff --git a/src/utils/ErrorUtils.tsx b/src/utils/ErrorUtils.tsx index ff78fe076c4..432e5693bf6 100644 --- a/src/utils/ErrorUtils.tsx +++ b/src/utils/ErrorUtils.tsx @@ -17,7 +17,7 @@ limitations under the License. import React, { ReactNode } from "react"; import { MatrixError } from "matrix-js-sdk/src/http-api"; -import { _t, _td, Tags, TranslatedString } from '../languageHandler'; +import { _t, _td, Tags, TranslatedString } from "../languageHandler"; /** * Produce a translated error message for a @@ -40,48 +40,44 @@ export function messageForResourceLimitError( extraTranslations?: Tags, ): TranslatedString { let errString = strings[limitType]; - if (errString === undefined) errString = strings['']; + if (errString === undefined) errString = strings[""]; - const linkSub = sub => { + const linkSub = (sub) => { if (adminContact) { - return { sub }; + return ( + + {sub} + + ); } else { return sub; } }; - if (errString.includes('')) { - return _t(errString, {}, Object.assign({ 'a': linkSub }, extraTranslations)); + if (errString.includes("")) { + return _t(errString, {}, Object.assign({ a: linkSub }, extraTranslations)); } else { return _t(errString, {}, extraTranslations); } } export function messageForSyncError(err: Error): ReactNode { - if (err instanceof MatrixError && err.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') { - const limitError = messageForResourceLimitError( - err.data.limit_type, - err.data.admin_contact, - { - 'monthly_active_user': _td("This homeserver has hit its Monthly Active User limit."), - 'hs_blocked': _td("This homeserver has been blocked by its administrator."), - '': _td("This homeserver has exceeded one of its resource limits."), - }, + if (err instanceof MatrixError && err.errcode === "M_RESOURCE_LIMIT_EXCEEDED") { + const limitError = messageForResourceLimitError(err.data.limit_type, err.data.admin_contact, { + "monthly_active_user": _td("This homeserver has hit its Monthly Active User limit."), + "hs_blocked": _td("This homeserver has been blocked by its administrator."), + "": _td("This homeserver has exceeded one of its resource limits."), + }); + const adminContact = messageForResourceLimitError(err.data.limit_type, err.data.admin_contact, { + "": _td("Please contact your service administrator to continue using the service."), + }); + return ( +
+
{limitError}
+
{adminContact}
+
); - const adminContact = messageForResourceLimitError( - err.data.limit_type, - err.data.admin_contact, - { - '': _td("Please contact your service administrator to continue using the service."), - }, - ); - return
-
{ limitError }
-
{ adminContact }
-
; } else { - return
- { _t("Unable to connect to Homeserver. Retrying...") } -
; + return
{_t("Unable to connect to Homeserver. Retrying...")}
; } } diff --git a/src/utils/EventRenderingUtils.ts b/src/utils/EventRenderingUtils.ts index 3ba4ce5705c..047d75192f7 100644 --- a/src/utils/EventRenderingUtils.ts +++ b/src/utils/EventRenderingUtils.ts @@ -25,7 +25,11 @@ import { MatrixClientPeg } from "../MatrixClientPeg"; import { getMessageModerationState, isLocationEvent, MessageModerationState } from "./EventUtils"; import { ElementCall } from "../models/Call"; -export function getEventDisplayInfo(mxEvent: MatrixEvent, showHiddenEvents: boolean, hideEvent?: boolean): { +export function getEventDisplayInfo( + mxEvent: MatrixEvent, + showHiddenEvents: boolean, + hideEvent?: boolean, +): { isInfoMessage: boolean; hasRenderer: boolean; isBubbleMessage: boolean; @@ -55,17 +59,15 @@ export function getEventDisplayInfo(mxEvent: MatrixEvent, showHiddenEvents: bool let factory = pickFactory(mxEvent, MatrixClientPeg.get(), showHiddenEvents); // Info messages are basically information about commands processed on a room - let isBubbleMessage = ( + let isBubbleMessage = eventType.startsWith("m.key.verification") || (eventType === EventType.RoomMessage && msgtype?.startsWith("m.key.verification")) || - (eventType === EventType.RoomCreate) || - (eventType === EventType.RoomEncryption) || - (factory === JitsiEventFactory) - ); - const isLeftAlignedBubbleMessage = !isBubbleMessage && ( - eventType === EventType.CallInvite || ElementCall.CALL_EVENT_TYPE.matches(eventType) - ); - let isInfoMessage = ( + eventType === EventType.RoomCreate || + eventType === EventType.RoomEncryption || + factory === JitsiEventFactory; + const isLeftAlignedBubbleMessage = + !isBubbleMessage && (eventType === EventType.CallInvite || ElementCall.CALL_EVENT_TYPE.matches(eventType)); + let isInfoMessage = !isBubbleMessage && !isLeftAlignedBubbleMessage && eventType !== EventType.RoomMessage && @@ -73,15 +75,13 @@ export function getEventDisplayInfo(mxEvent: MatrixEvent, showHiddenEvents: bool eventType !== EventType.Sticker && eventType !== EventType.RoomCreate && !M_POLL_START.matches(eventType) && - !M_BEACON_INFO.matches(eventType) - ); + !M_BEACON_INFO.matches(eventType); // Some non-info messages want to be rendered in the appropriate bubble column but without the bubble background - const noBubbleEvent = ( + const noBubbleEvent = (eventType === EventType.RoomMessage && msgtype === MsgType.Emote) || M_POLL_START.matches(eventType) || M_BEACON_INFO.matches(eventType) || - isLocationEvent(mxEvent) - ); + isLocationEvent(mxEvent); // If we're showing hidden events in the timeline, we should use the // source tile when there's no regular tile for an event and also for diff --git a/src/utils/EventUtils.ts b/src/utils/EventUtils.ts index 69e322ac6dc..eeebabd5c1b 100644 --- a/src/utils/EventUtils.ts +++ b/src/utils/EventUtils.ts @@ -14,16 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { EventStatus, MatrixEvent } from 'matrix-js-sdk/src/models/event'; +import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event"; import { EventType, EVENT_VISIBILITY_CHANGE_TYPE, MsgType, RelationType } from "matrix-js-sdk/src/@types/event"; -import { MatrixClient } from 'matrix-js-sdk/src/client'; -import { logger } from 'matrix-js-sdk/src/logger'; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { logger } from "matrix-js-sdk/src/logger"; import { M_POLL_START } from "matrix-events-sdk"; import { M_LOCATION } from "matrix-js-sdk/src/@types/location"; -import { M_BEACON_INFO } from 'matrix-js-sdk/src/@types/beacon'; -import { THREAD_RELATION_TYPE } from 'matrix-js-sdk/src/models/thread'; +import { M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon"; +import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread"; -import { MatrixClientPeg } from '../MatrixClientPeg'; +import { MatrixClientPeg } from "../MatrixClientPeg"; import shouldHideEvent from "../shouldHideEvent"; import { GetRelationsForEvent } from "../components/views/rooms/EventTile"; import SettingsStore from "../settings/SettingsStore"; @@ -31,6 +31,7 @@ import defaultDispatcher from "../dispatcher/dispatcher"; import { TimelineRenderingType } from "../contexts/RoomContext"; import { launchPollEditor } from "../components/views/messages/MPollBody"; import { Action } from "../dispatcher/actions"; +import { ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload"; /** * Returns whether an event should allow actions like reply, reactions, edit, etc. @@ -47,13 +48,13 @@ export function isContentActionable(mxEvent: MatrixEvent): boolean { const isSent = !eventStatus || eventStatus === EventStatus.SENT; if (isSent && !mxEvent.isRedacted()) { - if (mxEvent.getType() === 'm.room.message') { + if (mxEvent.getType() === "m.room.message") { const content = mxEvent.getContent(); - if (content.msgtype && content.msgtype !== 'm.bad.encrypted' && content.hasOwnProperty('body')) { + if (content.msgtype && content.msgtype !== "m.bad.encrypted" && content.hasOwnProperty("body")) { return true; } } else if ( - mxEvent.getType() === 'm.sticker' || + mxEvent.getType() === "m.sticker" || M_POLL_START.matches(mxEvent.getType()) || M_BEACON_INFO.matches(mxEvent.getType()) ) { @@ -65,10 +66,7 @@ export function isContentActionable(mxEvent: MatrixEvent): boolean { } export function canEditContent(mxEvent: MatrixEvent): boolean { - const isCancellable = ( - mxEvent.getType() === EventType.RoomMessage || - M_POLL_START.matches(mxEvent.getType()) - ); + const isCancellable = mxEvent.getType() === EventType.RoomMessage || M_POLL_START.matches(mxEvent.getType()); if ( !isCancellable || @@ -83,11 +81,7 @@ export function canEditContent(mxEvent: MatrixEvent): boolean { const { msgtype, body } = mxEvent.getOriginalContent(); return ( M_POLL_START.matches(mxEvent.getType()) || - ( - (msgtype === MsgType.Text || msgtype === MsgType.Emote) && - !!body && - typeof body === 'string' - ) + ((msgtype === MsgType.Text || msgtype === MsgType.Emote) && !!body && typeof body === "string") ); } @@ -110,23 +104,24 @@ export function findEditableEvent({ events: MatrixEvent[]; isForward: boolean; fromEventId?: string; -}): MatrixEvent { +}): MatrixEvent | undefined { + if (!events.length) return; const maxIdx = events.length - 1; const inc = isForward ? 1 : -1; const beginIdx = isForward ? 0 : maxIdx; let endIdx = isForward ? maxIdx : 0; if (!fromEventId) { - endIdx = Math.min(Math.max(0, beginIdx + (inc * MAX_JUMP_DISTANCE)), maxIdx); + endIdx = Math.min(Math.max(0, beginIdx + inc * MAX_JUMP_DISTANCE), maxIdx); } let foundFromEventId = !fromEventId; - for (let i = beginIdx; i !== (endIdx + inc); i += inc) { + for (let i = beginIdx; i !== endIdx + inc; i += inc) { const e = events[i]; // find start event first if (!foundFromEventId && e.getId() === fromEventId) { foundFromEventId = true; // don't look further than MAX_JUMP_DISTANCE events from `fromEventId` // to not iterate potentially 1000nds of events on key up/down - endIdx = Math.min(Math.max(0, i + (inc * MAX_JUMP_DISTANCE)), maxIdx); + endIdx = Math.min(Math.max(0, i + inc * MAX_JUMP_DISTANCE), maxIdx); } else if (foundFromEventId && !shouldHideEvent(e) && canEditOwnEvent(e)) { // otherwise look for editable event return e; @@ -192,14 +187,16 @@ export function getMessageModerationState(mxEvent: MatrixEvent, client?: MatrixC } const room = client.getRoom(mxEvent.getRoomId()); - if (EVENT_VISIBILITY_CHANGE_TYPE.name - && room.currentState.maySendStateEvent(EVENT_VISIBILITY_CHANGE_TYPE.name, client.getUserId()) + if ( + EVENT_VISIBILITY_CHANGE_TYPE.name && + room.currentState.maySendStateEvent(EVENT_VISIBILITY_CHANGE_TYPE.name, client.getUserId()) ) { // We're a moderator (as indicated by prefixed event name), show the message. return MessageModerationState.SEE_THROUGH_FOR_CURRENT_USER; } - if (EVENT_VISIBILITY_CHANGE_TYPE.altName - && room.currentState.maySendStateEvent(EVENT_VISIBILITY_CHANGE_TYPE.altName, client.getUserId()) + if ( + EVENT_VISIBILITY_CHANGE_TYPE.altName && + room.currentState.maySendStateEvent(EVENT_VISIBILITY_CHANGE_TYPE.altName, client.getUserId()) ) { // We're a moderator (as indicated by unprefixed event name), show the message. return MessageModerationState.SEE_THROUGH_FOR_CURRENT_USER; @@ -211,10 +208,7 @@ export function getMessageModerationState(mxEvent: MatrixEvent, client?: MatrixC export function isVoiceMessage(mxEvent: MatrixEvent): boolean { const content = mxEvent.getContent(); // MSC2516 is a legacy identifier. See https://github.com/matrix-org/matrix-doc/pull/3245 - return ( - !!content['org.matrix.msc2516.voice'] || - !!content['org.matrix.msc3245.voice'] - ); + return !!content["org.matrix.msc2516.voice"] || !!content["org.matrix.msc3245.voice"]; } export async function fetchInitialEvent( @@ -232,15 +226,15 @@ export async function fetchInitialEvent( initialEvent = null; } - if (client.supportsExperimentalThreads() && + if ( + client.supportsExperimentalThreads() && initialEvent?.isRelation(THREAD_RELATION_TYPE.name) && !initialEvent.getThread() ) { const threadId = initialEvent.threadRootId; const room = client.getRoom(roomId); const mapper = client.getEventMapper(); - const rootEvent = room.findEventById(threadId) - ?? mapper(await client.fetchRoomEvent(roomId, threadId)); + const rootEvent = room.findEventById(threadId) ?? mapper(await client.fetchRoomEvent(roomId, threadId)); try { room.createThread(threadId, rootEvent, [initialEvent], true); } catch (e) { @@ -277,10 +271,7 @@ export const isLocationEvent = (event: MatrixEvent): boolean => { const eventType = event.getType(); return ( M_LOCATION.matches(eventType) || - ( - eventType === EventType.RoomMessage && - M_LOCATION.matches(event.getContent().msgtype) - ) + (eventType === EventType.RoomMessage && M_LOCATION.matches(event.getContent().msgtype)) ); }; @@ -291,3 +282,13 @@ export function hasThreadSummary(event: MatrixEvent): boolean { export function canPinEvent(event: MatrixEvent): boolean { return !M_BEACON_INFO.matches(event.getType()); } + +export const highlightEvent = (roomId: string, eventId: string): void => { + defaultDispatcher.dispatch({ + action: Action.ViewRoom, + event_id: eventId, + highlighted: true, + room_id: roomId, + metricsTrigger: undefined, // room doesn't change + }); +}; diff --git a/src/utils/FileDownloader.ts b/src/utils/FileDownloader.ts index 5ec91d71cc4..67591a93865 100644 --- a/src/utils/FileDownloader.ts +++ b/src/utils/FileDownloader.ts @@ -33,7 +33,7 @@ type DownloadOptions = { // set up the iframe as a singleton so we don't have to figure out destruction of it down the line. let managedIframe: HTMLIFrameElement; let onLoadPromise: Promise; -function getManagedIframe(): { iframe: HTMLIFrameElement, onLoadPromise: Promise } { +function getManagedIframe(): { iframe: HTMLIFrameElement; onLoadPromise: Promise } { if (managedIframe) return { iframe: managedIframe, onLoadPromise }; managedIframe = document.createElement("iframe"); @@ -49,7 +49,7 @@ function getManagedIframe(): { iframe: HTMLIFrameElement, onLoadPromise: Promise // noinspection JSConstantReassignment managedIframe.sandbox = "allow-scripts allow-downloads allow-downloads-without-user-activation"; - onLoadPromise = new Promise(resolve => { + onLoadPromise = new Promise((resolve) => { managedIframe.onload = () => { resolve(); }; @@ -75,8 +75,7 @@ export class FileDownloader { * @param iframeFn Function to get a pre-configured iframe. Set to null to have the downloader * use a generic, hidden, iframe. */ - constructor(private iframeFn: getIframeFn = null) { - } + constructor(private iframeFn: getIframeFn = null) {} private get iframe(): HTMLIFrameElement { const iframe = this.iframeFn?.(); @@ -92,11 +91,14 @@ export class FileDownloader { public async download({ blob, name, autoDownload = true, opts = DEFAULT_STYLES }: DownloadOptions) { const iframe = this.iframe; // get the iframe first just in case we need to await onload if (this.onLoadPromise) await this.onLoadPromise; - iframe.contentWindow.postMessage({ - ...opts, - blob: blob, - download: name, - auto: autoDownload, - }, '*'); + iframe.contentWindow.postMessage( + { + ...opts, + blob: blob, + download: name, + auto: autoDownload, + }, + "*", + ); } } diff --git a/src/utils/FileUtils.ts b/src/utils/FileUtils.ts index 948f023d7e7..b9cd9a79d33 100644 --- a/src/utils/FileUtils.ts +++ b/src/utils/FileUtils.ts @@ -15,10 +15,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import filesize from 'filesize'; +import { filesize } from "filesize"; -import { IMediaEventContent } from '../customisations/models/IMediaEventContent'; -import { _t } from '../languageHandler'; +import { IMediaEventContent } from "../customisations/models/IMediaEventContent"; +import { _t } from "../languageHandler"; /** * Extracts a human readable label for the file attachment to use as @@ -47,13 +47,16 @@ export function presentableTextForFile( // will have a 3 character (plus full stop) extension. The goal is to knock // the label down to 15-25 characters, not perfect accuracy. if (shortened && text.length > 19) { - const parts = text.split('.'); - let fileName = parts.slice(0, parts.length - 1).join('.').substring(0, 15); + const parts = text.split("."); + let fileName = parts + .slice(0, parts.length - 1) + .join(".") + .substring(0, 15); const extension = parts[parts.length - 1]; // Trim off any full stops from the file name to avoid a case where we // add an ellipsis that looks really funky. - fileName = fileName.replace(/\.*$/g, ''); + fileName = fileName.replace(/\.*$/g, ""); text = `${fileName}...${extension}`; } @@ -66,7 +69,7 @@ export function presentableTextForFile( // it since it is "ugly", users generally aren't aware what it // means and the type of the attachment can usually be inferred // from the file extension. - text += ' (' + filesize(content.info.size) + ')'; + text += " (" + filesize(content.info.size) + ")"; } return text; } diff --git a/src/utils/FontManager.ts b/src/utils/FontManager.ts index 197e4eda00e..4b26bc9db27 100644 --- a/src/utils/FontManager.ts +++ b/src/utils/FontManager.ts @@ -29,13 +29,15 @@ function safariVersionCheck(ua: string): boolean { if (safariVersionMatch) { const macOSVersionStr = safariVersionMatch[1]; const safariVersionStr = safariVersionMatch[2]; - const macOSVersion = macOSVersionStr.split("_").map(n => parseInt(n, 10)); - const safariVersion = safariVersionStr.split(".").map(n => parseInt(n, 10)); + const macOSVersion = macOSVersionStr.split("_").map((n) => parseInt(n, 10)); + const safariVersion = safariVersionStr.split(".").map((n) => parseInt(n, 10)); const colrFontSupported = macOSVersion[0] >= 10 && macOSVersion[1] >= 14 && safariVersion[0] >= 12; // https://www.colorfonts.wtf/ states safari supports COLR fonts from this version on - logger.log(`COLR support on Safari requires macOS 10.14 and Safari 12, ` + - `detected Safari ${safariVersionStr} on macOS ${macOSVersionStr}, ` + - `COLR supported: ${colrFontSupported}`); + logger.log( + `COLR support on Safari requires macOS 10.14 and Safari 12, ` + + `detected Safari ${safariVersionStr} on macOS ${macOSVersionStr}, ` + + `COLR supported: ${colrFontSupported}`, + ); return colrFontSupported; } } catch (err) { @@ -66,11 +68,12 @@ async function isColrFontSupported(): Promise { } try { - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); + const canvas = document.createElement("canvas"); + const context = canvas.getContext("2d"); const img = new Image(); // eslint-disable-next-line - const fontCOLR = 'd09GRgABAAAAAAKAAAwAAAAAAowAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABDT0xSAAACVAAAABYAAAAYAAIAJUNQQUwAAAJsAAAAEgAAABLJAAAQT1MvMgAAAYAAAAA6AAAAYBfxJ0pjbWFwAAABxAAAACcAAAAsAAzpM2dseWYAAAH0AAAAGgAAABoNIh0kaGVhZAAAARwAAAAvAAAANgxLumdoaGVhAAABTAAAABUAAAAkCAEEAmhtdHgAAAG8AAAABgAAAAYEAAAAbG9jYQAAAewAAAAGAAAABgANAABtYXhwAAABZAAAABsAAAAgAg4AHW5hbWUAAAIQAAAAOAAAAD4C5wsecG9zdAAAAkgAAAAMAAAAIAADAAB4AWNgZGAAYQ5+qdB4fpuvDNIsDCBwaQGTAIi+VlscBaJZGMDiHAxMIAoAtjIF/QB4AWNgZGBgYQACOAkUQQWMAAGRABAAAAB4AWNgZGBgYGJgAdMMUJILJMQgAWICAAH3AC4AeAFjYGFhYJzAwMrAwDST6QwDA0M/hGZ8zWDMyMmAChgFkDgKQMBw4CXDSwYWEBdIYgAFBgYA/8sIdAAABAAAAAAAAAB4AWNgYGBkYAZiBgYeBhYGBSDNAoRA/kuG//8hpDgjWJ4BAFVMBiYAAAAAAAANAAAAAQAAAAAEAAQAAAMAABEhESEEAPwABAD8AAAAeAEtxgUNgAAAAMHHIQTShTlOAty9/4bf7AARCwlBNhBw4L/43qXjYGUmf19TMuLcj/BJL3XfBg54AWNgZsALAAB9AAR4AWNgYGAEYj4gFgGygGwICQACOwAoAAAAAAABAAEAAQAAAA4AAAAAyP8AAA=='; + const fontCOLR = + "d09GRgABAAAAAAKAAAwAAAAAAowAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABDT0xSAAACVAAAABYAAAAYAAIAJUNQQUwAAAJsAAAAEgAAABLJAAAQT1MvMgAAAYAAAAA6AAAAYBfxJ0pjbWFwAAABxAAAACcAAAAsAAzpM2dseWYAAAH0AAAAGgAAABoNIh0kaGVhZAAAARwAAAAvAAAANgxLumdoaGVhAAABTAAAABUAAAAkCAEEAmhtdHgAAAG8AAAABgAAAAYEAAAAbG9jYQAAAewAAAAGAAAABgANAABtYXhwAAABZAAAABsAAAAgAg4AHW5hbWUAAAIQAAAAOAAAAD4C5wsecG9zdAAAAkgAAAAMAAAAIAADAAB4AWNgZGAAYQ5+qdB4fpuvDNIsDCBwaQGTAIi+VlscBaJZGMDiHAxMIAoAtjIF/QB4AWNgZGBgYQACOAkUQQWMAAGRABAAAAB4AWNgZGBgYGJgAdMMUJILJMQgAWICAAH3AC4AeAFjYGFhYJzAwMrAwDST6QwDA0M/hGZ8zWDMyMmAChgFkDgKQMBw4CXDSwYWEBdIYgAFBgYA/8sIdAAABAAAAAAAAAB4AWNgYGBkYAZiBgYeBhYGBSDNAoRA/kuG//8hpDgjWJ4BAFVMBiYAAAAAAAANAAAAAQAAAAAEAAQAAAMAABEhESEEAPwABAD8AAAAeAEtxgUNgAAAAMHHIQTShTlOAty9/4bf7AARCwlBNhBw4L/43qXjYGUmf19TMuLcj/BJL3XfBg54AWNgZsALAAB9AAR4AWNgYGAEYj4gFgGygGwICQACOwAoAAAAAAABAAEAAQAAAA4AAAAAyP8AAA=="; const svg = ` ; } diff --git a/src/utils/Mouse.ts b/src/utils/Mouse.ts index a85c6492c40..8788b4fe801 100644 --- a/src/utils/Mouse.ts +++ b/src/utils/Mouse.ts @@ -27,24 +27,22 @@ export function normalizeWheelEvent(event: WheelEvent): WheelEvent { let deltaY; let deltaZ; - if (event.deltaMode === 1) { // Units are lines - deltaX = (event.deltaX * LINE_HEIGHT); - deltaY = (event.deltaY * LINE_HEIGHT); - deltaZ = (event.deltaZ * LINE_HEIGHT); + if (event.deltaMode === 1) { + // Units are lines + deltaX = event.deltaX * LINE_HEIGHT; + deltaY = event.deltaY * LINE_HEIGHT; + deltaZ = event.deltaZ * LINE_HEIGHT; } else { deltaX = event.deltaX; deltaY = event.deltaY; deltaZ = event.deltaZ; } - return new WheelEvent( - "syntheticWheel", - { - deltaMode: 0, - deltaY: deltaY, - deltaX: deltaX, - deltaZ: deltaZ, - ...event, - }, - ); + return new WheelEvent("syntheticWheel", { + deltaMode: 0, + deltaY: deltaY, + deltaX: deltaX, + deltaZ: deltaZ, + ...event, + }); } diff --git a/src/utils/MultiInviter.ts b/src/utils/MultiInviter.ts index 3c539f7bf0c..e1f0de68142 100644 --- a/src/utils/MultiInviter.ts +++ b/src/utils/MultiInviter.ts @@ -21,8 +21,8 @@ import { MatrixClient } from "matrix-js-sdk/src/client"; import { EventType } from "matrix-js-sdk/src/@types/event"; import { HistoryVisibility } from "matrix-js-sdk/src/@types/partials"; -import { MatrixClientPeg } from '../MatrixClientPeg'; -import { AddressType, getAddressType } from '../UserAddress'; +import { MatrixClientPeg } from "../MatrixClientPeg"; +import { AddressType, getAddressType } from "../UserAddress"; import { _t } from "../languageHandler"; import Modal from "../Modal"; import SettingsStore from "../settings/SettingsStore"; @@ -38,7 +38,7 @@ interface IError { errcode: string; } -const UNKNOWN_PROFILE_ERRORS = ['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'M_PROFILE_UNDISCLOSED', 'M_PROFILE_NOT_FOUND']; +const UNKNOWN_PROFILE_ERRORS = ["M_NOT_FOUND", "M_USER_NOT_FOUND", "M_PROFILE_UNDISCLOSED", "M_PROFILE_NOT_FOUND"]; export type CompletionStates = Record; @@ -92,8 +92,8 @@ export default class MultiInviter { if (getAddressType(addr) === null) { this.completionStates[addr] = InviteState.Error; this.errors[addr] = { - errcode: 'M_INVALID', - errorText: _t('Unrecognised address'), + errcode: "M_INVALID", + errorText: _t("Unrecognised address"), }; } } @@ -112,7 +112,7 @@ export default class MultiInviter { return this.deferred.promise; } - return this.deferred.promise.then(async states => { + return this.deferred.promise.then(async (states) => { const invitedUsers = []; for (const [addr, state] of Object.entries(states)) { if (state === InviteState.Invited && getAddressType(addr) === AddressType.MatrixUserId) { @@ -134,7 +134,7 @@ export default class MultiInviter { if (!this.busy) return; this.canceled = true; - this.deferred.reject(new Error('canceled')); + this.deferred.reject(new Error("canceled")); } public getCompletionState(addr: string): InviteState { @@ -174,10 +174,10 @@ export default class MultiInviter { // The error handling during the invitation process covers any API. // Some errors must to me mapped from profile API errors to more specific ones to avoid collisions. switch (err.errcode) { - case 'M_FORBIDDEN': - throw new MatrixError({ errcode: 'M_PROFILE_UNDISCLOSED' }); - case 'M_NOT_FOUND': - throw new MatrixError({ errcode: 'M_USER_NOT_FOUND' }); + case "M_FORBIDDEN": + throw new MatrixError({ errcode: "M_PROFILE_UNDISCLOSED" }); + case "M_NOT_FOUND": + throw new MatrixError({ errcode: "M_USER_NOT_FOUND" }); default: throw err; } @@ -186,7 +186,7 @@ export default class MultiInviter { return this.matrixClient.invite(roomId, addr, this.reason); } else { - throw new Error('Unsupported address'); + throw new Error("Unsupported address"); } } @@ -195,99 +195,101 @@ export default class MultiInviter { logger.log(`Inviting ${address}`); const doInvite = this.inviteToRoom(this.roomId, address, ignoreProfile); - doInvite.then(() => { - if (this.canceled) { - return; - } - - this.completionStates[address] = InviteState.Invited; - delete this.errors[address]; + doInvite + .then(() => { + if (this.canceled) { + return; + } - resolve(); - this.progressCallback?.(); - }).catch((err) => { - if (this.canceled) { - return; - } + this.completionStates[address] = InviteState.Invited; + delete this.errors[address]; - logger.error(err); - - const isSpace = this.roomId && this.matrixClient.getRoom(this.roomId)?.isSpaceRoom(); - - let errorText: string; - let fatal = false; - switch (err.errcode) { - case "M_FORBIDDEN": - if (isSpace) { - errorText = _t('You do not have permission to invite people to this space.'); - } else { - errorText = _t('You do not have permission to invite people to this room.'); - } - fatal = true; - break; - case USER_ALREADY_INVITED: - if (isSpace) { - errorText = _t("User is already invited to the space"); - } else { - errorText = _t("User is already invited to the room"); - } - break; - case USER_ALREADY_JOINED: - if (isSpace) { - errorText = _t("User is already in the space"); - } else { - errorText = _t("User is already in the room"); - } - break; - case "M_LIMIT_EXCEEDED": - // we're being throttled so wait a bit & try again - setTimeout(() => { - this.doInvite(address, ignoreProfile).then(resolve, reject); - }, 5000); + resolve(); + this.progressCallback?.(); + }) + .catch((err) => { + if (this.canceled) { return; - case "M_NOT_FOUND": - case "M_USER_NOT_FOUND": - errorText = _t("User does not exist"); - break; - case "M_PROFILE_UNDISCLOSED": - errorText = _t("User may or may not exist"); - break; - case "M_PROFILE_NOT_FOUND": - if (!ignoreProfile) { - // Invite without the profile check - logger.warn(`User ${address} does not have a profile - inviting anyways automatically`); - this.doInvite(address, true).then(resolve, reject); + } + + logger.error(err); + + const isSpace = this.roomId && this.matrixClient.getRoom(this.roomId)?.isSpaceRoom(); + + let errorText: string; + let fatal = false; + switch (err.errcode) { + case "M_FORBIDDEN": + if (isSpace) { + errorText = _t("You do not have permission to invite people to this space."); + } else { + errorText = _t("You do not have permission to invite people to this room."); + } + fatal = true; + break; + case USER_ALREADY_INVITED: + if (isSpace) { + errorText = _t("User is already invited to the space"); + } else { + errorText = _t("User is already invited to the room"); + } + break; + case USER_ALREADY_JOINED: + if (isSpace) { + errorText = _t("User is already in the space"); + } else { + errorText = _t("User is already in the room"); + } + break; + case "M_LIMIT_EXCEEDED": + // we're being throttled so wait a bit & try again + window.setTimeout(() => { + this.doInvite(address, ignoreProfile).then(resolve, reject); + }, 5000); return; - } - break; - case "M_BAD_STATE": - errorText = _t("The user must be unbanned before they can be invited."); - break; - case "M_UNSUPPORTED_ROOM_VERSION": - if (isSpace) { - errorText = _t("The user's homeserver does not support the version of the space."); - } else { - errorText = _t("The user's homeserver does not support the version of the room."); - } - break; - } + case "M_NOT_FOUND": + case "M_USER_NOT_FOUND": + errorText = _t("User does not exist"); + break; + case "M_PROFILE_UNDISCLOSED": + errorText = _t("User may or may not exist"); + break; + case "M_PROFILE_NOT_FOUND": + if (!ignoreProfile) { + // Invite without the profile check + logger.warn(`User ${address} does not have a profile - inviting anyways automatically`); + this.doInvite(address, true).then(resolve, reject); + return; + } + break; + case "M_BAD_STATE": + errorText = _t("The user must be unbanned before they can be invited."); + break; + case "M_UNSUPPORTED_ROOM_VERSION": + if (isSpace) { + errorText = _t("The user's homeserver does not support the version of the space."); + } else { + errorText = _t("The user's homeserver does not support the version of the room."); + } + break; + } - if (!errorText) { - errorText = _t('Unknown server error'); - } + if (!errorText) { + errorText = _t("Unknown server error"); + } - this.completionStates[address] = InviteState.Error; - this.errors[address] = { errorText, errcode: err.errcode }; + this.completionStates[address] = InviteState.Error; + this.errors[address] = { errorText, errcode: err.errcode }; - this.busy = !fatal; - this._fatal = fatal; + this.busy = !fatal; + this._fatal = fatal; - if (fatal) { - reject(err); - } else { - resolve(); - } - }); + if (fatal) { + reject(err); + } else { + resolve(); + } + }); }); } @@ -301,12 +303,13 @@ export default class MultiInviter { if (Object.keys(this.errors).length > 0) { // There were problems inviting some people - see if we can invite them // without caring if they exist or not. - const unknownProfileUsers = Object.keys(this.errors) - .filter(a => UNKNOWN_PROFILE_ERRORS.includes(this.errors[a].errcode)); + const unknownProfileUsers = Object.keys(this.errors).filter((a) => + UNKNOWN_PROFILE_ERRORS.includes(this.errors[a].errcode), + ); if (unknownProfileUsers.length > 0) { const inviteUnknowns = () => { - const promises = unknownProfileUsers.map(u => this.doInvite(u, true)); + const promises = unknownProfileUsers.map((u) => this.doInvite(u, true)); Promise.all(promises).then(() => this.deferred.resolve(this.completionStates)); }; @@ -317,7 +320,7 @@ export default class MultiInviter { logger.log("Showing failed to invite dialog..."); Modal.createDialog(AskInviteAnywayDialog, { - unknownProfileUsers: unknownProfileUsers.map(u => ({ + unknownProfileUsers: unknownProfileUsers.map((u) => ({ userId: u, errorText: this.errors[u].errorText, })), @@ -354,8 +357,10 @@ export default class MultiInviter { return; } - this.doInvite(addr, ignoreProfile).then(() => { - this.inviteMore(nextIndex + 1, ignoreProfile); - }).catch(() => this.deferred.resolve(this.completionStates)); + this.doInvite(addr, ignoreProfile) + .then(() => { + this.inviteMore(nextIndex + 1, ignoreProfile); + }) + .catch(() => this.deferred.resolve(this.completionStates)); } } diff --git a/src/utils/NativeEventUtils.ts b/src/utils/NativeEventUtils.ts index 3094b57bd42..3ed637c7485 100644 --- a/src/utils/NativeEventUtils.ts +++ b/src/utils/NativeEventUtils.ts @@ -18,7 +18,8 @@ import React from "react"; // Wrap DOM event handlers with stopPropagation and preventDefault export const preventDefaultWrapper = - (callback: () => void) => (e?: T) => { + (callback: () => void) => + (e?: T) => { e?.stopPropagation(); e?.preventDefault(); callback(); diff --git a/src/utils/PasswordScorer.ts b/src/utils/PasswordScorer.ts index 9aae4039bd7..8a0d611773f 100644 --- a/src/utils/PasswordScorer.ts +++ b/src/utils/PasswordScorer.ts @@ -14,15 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import zxcvbn, { ZXCVBNFeedbackWarning } from 'zxcvbn'; +import zxcvbn, { ZXCVBNFeedbackWarning } from "zxcvbn"; -import { MatrixClientPeg } from '../MatrixClientPeg'; -import { _t, _td } from '../languageHandler'; +import { MatrixClientPeg } from "../MatrixClientPeg"; +import { _t, _td } from "../languageHandler"; -const ZXCVBN_USER_INPUTS = [ - 'riot', - 'matrix', -]; +const ZXCVBN_USER_INPUTS = ["riot", "matrix"]; // Translations for zxcvbn's suggestion strings _td("Use a few words, avoid common phrases"); @@ -40,8 +37,8 @@ _td("Predictable substitutions like '@' instead of 'a' don't help very much"); _td("Add another word or two. Uncommon words are better."); // and warnings -_td("Repeats like \"aaa\" are easy to guess"); -_td("Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\""); +_td('Repeats like "aaa" are easy to guess'); +_td('Repeats like "abcabcabc" are only slightly harder to guess than "abc"'); _td("Sequences like abc or 6543 are easy to guess"); _td("Recent years are easy to guess"); _td("Dates are often easy to guess"); @@ -73,8 +70,8 @@ export function scorePassword(password: string) { let zxcvbnResult = zxcvbn(password, userInputs); // Work around https://github.com/dropbox/zxcvbn/issues/216 - if (password.includes(' ')) { - const resultNoSpaces = zxcvbn(password.replace(/ /g, ''), userInputs); + if (password.includes(" ")) { + const resultNoSpaces = zxcvbn(password.replace(/ /g, ""), userInputs); if (resultNoSpaces.score < zxcvbnResult.score) zxcvbnResult = resultNoSpaces; } diff --git a/src/utils/PreferredRoomVersions.ts b/src/utils/PreferredRoomVersions.ts index 2dc269da6c2..b4944d4a008 100644 --- a/src/utils/PreferredRoomVersions.ts +++ b/src/utils/PreferredRoomVersions.ts @@ -52,4 +52,3 @@ export function doesRoomVersionSupport(roomVer: string, featureVer: string): boo // from a mile away and can course-correct this function if needed. return Number(roomVer) >= Number(featureVer); } - diff --git a/src/utils/ReactUtils.tsx b/src/utils/ReactUtils.tsx index 4cd2d750f36..59fa7a3b911 100644 --- a/src/utils/ReactUtils.tsx +++ b/src/utils/ReactUtils.tsx @@ -25,9 +25,7 @@ import React from "react"; export function jsxJoin(array: Array, joiner?: string | JSX.Element): JSX.Element { const newArray = []; array.forEach((element, index) => { - newArray.push(element, (index === array.length - 1) ? null : joiner); + newArray.push(element, index === array.length - 1 ? null : joiner); }); - return ( - { newArray } - ); + return {newArray}; } diff --git a/src/utils/Reply.ts b/src/utils/Reply.ts index 145753e0415..b6ee476bf64 100644 --- a/src/utils/Reply.ts +++ b/src/utils/Reply.ts @@ -36,11 +36,11 @@ export function getParentEventId(ev?: MatrixEvent): string | undefined { // Part of Replies fallback support export function stripPlainReply(body: string): string { // Removes lines beginning with `> ` until you reach one that doesn't. - const lines = body.split('\n'); - while (lines.length && lines[0].startsWith('> ')) lines.shift(); + const lines = body.split("\n"); + while (lines.length && lines[0].startsWith("> ")) lines.shift(); // Reply fallback has a blank line after it, so remove it to prevent leading newline - if (lines[0] === '') lines.shift(); - return lines.join('\n'); + if (lines[0] === "") lines.shift(); + return lines.join("\n"); } // Part of Replies fallback support @@ -52,24 +52,21 @@ export function stripHTMLReply(html: string): string { // anyways. However, we sanitize to 1) remove any mx-reply, so that we // don't generate a nested mx-reply, and 2) make sure that the HTML is // properly formatted (e.g. tags are closed where necessary) - return sanitizeHtml( - html, - { - allowedTags: false, // false means allow everything - allowedAttributes: false, - // we somehow can't allow all schemes, so we allow all that we - // know of and mxc (for img tags) - allowedSchemes: [...PERMITTED_URL_SCHEMES, 'mxc'], - exclusiveFilter: (frame) => frame.tag === "mx-reply", - }, - ); + return sanitizeHtml(html, { + allowedTags: false, // false means allow everything + allowedAttributes: false, + // we somehow can't allow all schemes, so we allow all that we + // know of and mxc (for img tags) + allowedSchemes: [...PERMITTED_URL_SCHEMES, "mxc"], + exclusiveFilter: (frame) => frame.tag === "mx-reply", + }); } // Part of Replies fallback support export function getNestedReplyText( ev: MatrixEvent, permalinkCreator: RoomPermalinkCreator, -): { body: string, html: string } | null { +): { body: string; html: string } | null { if (!ev) return null; let { body, formatted_body: html, msgtype } = ev.getContent(); @@ -86,7 +83,7 @@ export function getNestedReplyText( // Escape the body to use as HTML below. // We also run a nl2br over the result to fix the fallback representation. We do this // after converting the text to safe HTML to avoid user-provided BR's from being converted. - html = escapeHtml(body).replace(/\n/g, '
'); + html = escapeHtml(body).replace(/\n/g, "
"); } // dev note: do not rely on `body` being safe for HTML usage below. @@ -98,8 +95,9 @@ export function getNestedReplyText( if (M_BEACON_INFO.matches(ev.getType())) { const aTheir = isSelfLocation(ev.getContent()) ? "their" : "a"; return { - html: `
In reply to ${mxid}` - + `
shared ${aTheir} live location.
`, + html: + `
In reply to ${mxid}` + + `
shared ${aTheir} live location.
`, body: `> <${mxid}> shared ${aTheir} live location.\n\n`, }; } @@ -108,49 +106,56 @@ export function getNestedReplyText( switch (msgtype) { case MsgType.Text: case MsgType.Notice: { - html = `
In reply to ${mxid}` - + `
${html}
`; - const lines = body.trim().split('\n'); + html = + `
In reply to ${mxid}` + + `
${html}
`; + const lines = body.trim().split("\n"); if (lines.length > 0) { lines[0] = `<${mxid}> ${lines[0]}`; - body = lines.map((line) => `> ${line}`).join('\n') + '\n\n'; + body = lines.map((line) => `> ${line}`).join("\n") + "\n\n"; } break; } case MsgType.Image: - html = `
In reply to ${mxid}` - + `
sent an image.
`; + html = + `
In reply to ${mxid}` + + `
sent an image.
`; body = `> <${mxid}> sent an image.\n\n`; break; case MsgType.Video: - html = `
In reply to ${mxid}` - + `
sent a video.
`; + html = + `
In reply to ${mxid}` + + `
sent a video.
`; body = `> <${mxid}> sent a video.\n\n`; break; case MsgType.Audio: - html = `
In reply to ${mxid}` - + `
sent an audio file.
`; + html = + `
In reply to ${mxid}` + + `
sent an audio file.
`; body = `> <${mxid}> sent an audio file.\n\n`; break; case MsgType.File: - html = `
In reply to ${mxid}` - + `
sent a file.
`; + html = + `
In reply to ${mxid}` + + `
sent a file.
`; body = `> <${mxid}> sent a file.\n\n`; break; case MsgType.Location: { const aTheir = isSelfLocation(ev.getContent()) ? "their" : "a"; - html = `
In reply to ${mxid}` - + `
shared ${aTheir} location.
`; + html = + `
In reply to ${mxid}` + + `
shared ${aTheir} location.
`; body = `> <${mxid}> shared ${aTheir} location.\n\n`; break; } case MsgType.Emote: { - html = `
In reply to * ` - + `${mxid}
${html}
`; - const lines = body.trim().split('\n'); + html = + `
In reply to * ` + + `${mxid}
${html}
`; + const lines = body.trim().split("\n"); if (lines.length > 0) { lines[0] = `* <${mxid}> ${lines[0]}`; - body = lines.map((line) => `> ${line}`).join('\n') + '\n\n'; + body = lines.map((line) => `> ${line}`).join("\n") + "\n\n"; } break; } @@ -165,13 +170,13 @@ export function makeReplyMixIn(ev?: MatrixEvent): IEventRelation { if (!ev) return {}; const mixin: IEventRelation = { - 'm.in_reply_to': { - 'event_id': ev.getId(), + "m.in_reply_to": { + event_id: ev.getId(), }, }; if (ev.threadRootId) { - if (SettingsStore.getValue("feature_thread")) { + if (SettingsStore.getValue("feature_threadstable")) { mixin.is_falling_back = false; } else { // Clients that do not offer a threading UI should behave as follows when replying, for best interaction @@ -197,7 +202,8 @@ export function shouldDisplayReply(event: MatrixEvent): boolean { } const relation = event.getRelation(); - if (SettingsStore.getValue("feature_thread") && + if ( + SettingsStore.getValue("feature_threadstable") && relation?.rel_type === THREAD_RELATION_TYPE.name && relation?.is_falling_back ) { diff --git a/src/utils/ResizeNotifier.ts b/src/utils/ResizeNotifier.ts index 8bb7f52e578..d957ee75bc2 100644 --- a/src/utils/ResizeNotifier.ts +++ b/src/utils/ResizeNotifier.ts @@ -76,4 +76,3 @@ export default class ResizeNotifier extends EventEmitter { this.updateMiddlePanel(); } } - diff --git a/src/utils/RoomUpgrade.ts b/src/utils/RoomUpgrade.ts index 37aa61de304..6bb79642af1 100644 --- a/src/utils/RoomUpgrade.ts +++ b/src/utils/RoomUpgrade.ts @@ -39,7 +39,7 @@ export async function awaitRoomDownSync(cli: MatrixClient, roomId: string): Prom const room = cli.getRoom(roomId); if (room) return room; // already have the room - return new Promise(resolve => { + return new Promise((resolve) => { // We have to wait for the js-sdk to give us the room back so // we can more effectively abuse the MultiInviter behaviour // which heavily relies on the Room object being available. @@ -69,22 +69,21 @@ export async function upgradeRoom( let toInvite: string[] = []; if (inviteUsers) { - toInvite = [ - ...room.getMembersWithMembership("join"), - ...room.getMembersWithMembership("invite"), - ].map(m => m.userId).filter(m => m !== cli.getUserId()); + toInvite = [...room.getMembersWithMembership("join"), ...room.getMembersWithMembership("invite")] + .map((m) => m.userId) + .filter((m) => m !== cli.getUserId()); } let parentsToRelink: Room[] = []; if (updateSpaces) { parentsToRelink = Array.from(SpaceStore.instance.getKnownParents(room.roomId)) - .map(roomId => cli.getRoom(roomId)) - .filter(parent => parent?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId())); + .map((roomId) => cli.getRoom(roomId)) + .filter((parent) => parent?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId())); } const progress: IProgress = { roomUpgraded: false, - roomSynced: (awaitRoom || inviteUsers) ? false : undefined, + roomSynced: awaitRoom || inviteUsers ? false : undefined, inviteUsersProgress: inviteUsers ? 0 : undefined, inviteUsersTotal: toInvite.length, updateSpacesProgress: updateSpaces ? 0 : undefined, @@ -100,8 +99,8 @@ export async function upgradeRoom( logger.error(e); Modal.createDialog(ErrorDialog, { - title: _t('Error upgrading room'), - description: _t('Double check that your server supports the room version chosen and try again.'), + title: _t("Error upgrading room"), + description: _t("Double check that your server supports the room version chosen and try again."), }); throw e; } @@ -127,10 +126,15 @@ export async function upgradeRoom( try { for (const parent of parentsToRelink) { const currentEv = parent.currentState.getStateEvents(EventType.SpaceChild, room.roomId); - await cli.sendStateEvent(parent.roomId, EventType.SpaceChild, { - ...(currentEv?.getContent() || {}), // copy existing attributes like suggested - via: [cli.getDomain()], - }, newRoomId); + await cli.sendStateEvent( + parent.roomId, + EventType.SpaceChild, + { + ...(currentEv?.getContent() || {}), // copy existing attributes like suggested + via: [cli.getDomain()], + }, + newRoomId, + ); await cli.sendStateEvent(parent.roomId, EventType.SpaceChild, {}, room.roomId); progress.updateSpacesProgress++; diff --git a/src/utils/ShieldUtils.ts b/src/utils/ShieldUtils.ts index 296c68fa09e..6bf57801be7 100644 --- a/src/utils/ShieldUtils.ts +++ b/src/utils/ShieldUtils.ts @@ -17,12 +17,12 @@ limitations under the License. import { MatrixClient } from "matrix-js-sdk/src/client"; import { Room } from "matrix-js-sdk/src/models/room"; -import DMRoomMap from './DMRoomMap'; +import DMRoomMap from "./DMRoomMap"; export enum E2EStatus { Warning = "warning", Verified = "verified", - Normal = "normal" + Normal = "normal", } export async function shieldStatusForRoom(client: MatrixClient, room: Room): Promise { @@ -31,10 +31,10 @@ export async function shieldStatusForRoom(client: MatrixClient, room: Room): Pro const verified: string[] = []; const unverified: string[] = []; - members.filter((userId) => userId !== client.getUserId()) + members + .filter((userId) => userId !== client.getUserId()) .forEach((userId) => { - (client.checkUserTrust(userId).isCrossSigningVerified() ? - verified : unverified).push(userId); + (client.checkUserTrust(userId).isCrossSigningVerified() ? verified : unverified).push(userId); }); /* Alarm if any unverified users were verified before. */ @@ -46,10 +46,11 @@ export async function shieldStatusForRoom(client: MatrixClient, room: Room): Pro /* Check all verified user devices. */ /* Don't alarm if no other users are verified */ - const includeUser = (verified.length > 0) && // Don't alarm for self in rooms where nobody else is verified - !inDMMap && // Don't alarm for self in DMs with other users - (members.length !== 2) || // Don't alarm for self in 1:1 chats with other users - (members.length === 1); // Do alarm for self if we're alone in a room + const includeUser = + (verified.length > 0 && // Don't alarm for self in rooms where nobody else is verified + !inDMMap && // Don't alarm for self in DMs with other users + members.length !== 2) || // Don't alarm for self in 1:1 chats with other users + members.length === 1; // Do alarm for self if we're alone in a room const targets = includeUser ? [...verified, client.getUserId()] : verified; for (const userId of targets) { const devices = client.getStoredDevicesForUser(userId); diff --git a/src/utils/Singleflight.ts b/src/utils/Singleflight.ts index 93822594a2b..28d910b094d 100644 --- a/src/utils/Singleflight.ts +++ b/src/utils/Singleflight.ts @@ -41,8 +41,7 @@ const keyMap = new EnhancedMap>(); * variables to strings to essentially namespace the field, for most cases. */ export class Singleflight { - private constructor() { - } + private constructor() {} /** * A void marker to help with returning a value in a singleflight context. @@ -80,8 +79,7 @@ export class Singleflight { } class SingleflightContext { - public constructor(private instance: Object, private key: string) { - } + public constructor(private instance: Object, private key: string) {} /** * Forget this particular instance and key combination, discarding the result. diff --git a/src/utils/SnakedObject.ts b/src/utils/SnakedObject.ts index bce02512c05..f19488795e4 100644 --- a/src/utils/SnakedObject.ts +++ b/src/utils/SnakedObject.ts @@ -15,12 +15,11 @@ limitations under the License. */ export function snakeToCamel(s: string): string { - return s.replace(/._./g, v => `${v[0]}${v[2].toUpperCase()}`); + return s.replace(/._./g, (v) => `${v[0]}${v[2].toUpperCase()}`); } export class SnakedObject> { - public constructor(private obj: T) { - } + public constructor(private obj: T) {} public get(key: K, altCaseName?: string): T[K] { const val = this.obj[key]; diff --git a/src/utils/SortMembers.ts b/src/utils/SortMembers.ts index 74d6388c93f..4bc17be2c1e 100644 --- a/src/utils/SortMembers.ts +++ b/src/utils/SortMembers.ts @@ -16,41 +16,43 @@ limitations under the License. import { groupBy, mapValues, maxBy, minBy, sumBy, takeRight } from "lodash"; import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix"; +import { compare } from "matrix-js-sdk/src/utils"; import { Member } from "./direct-messages"; import DMRoomMap from "./DMRoomMap"; -import { compare } from "./strings"; -export const compareMembers = ( - activityScores: Record, - memberScores: Record, -) => (a: Member | RoomMember, b: Member | RoomMember): number => { - const aActivityScore = activityScores[a.userId]?.score ?? 0; - const aMemberScore = memberScores[a.userId]?.score ?? 0; - const aScore = aActivityScore + aMemberScore; - const aNumRooms = memberScores[a.userId]?.numRooms ?? 0; +export const compareMembers = + (activityScores: Record, memberScores: Record) => + (a: Member | RoomMember, b: Member | RoomMember): number => { + const aActivityScore = activityScores[a.userId]?.score ?? 0; + const aMemberScore = memberScores[a.userId]?.score ?? 0; + const aScore = aActivityScore + aMemberScore; + const aNumRooms = memberScores[a.userId]?.numRooms ?? 0; - const bActivityScore = activityScores[b.userId]?.score ?? 0; - const bMemberScore = memberScores[b.userId]?.score ?? 0; - const bScore = bActivityScore + bMemberScore; - const bNumRooms = memberScores[b.userId]?.numRooms ?? 0; + const bActivityScore = activityScores[b.userId]?.score ?? 0; + const bMemberScore = memberScores[b.userId]?.score ?? 0; + const bScore = bActivityScore + bMemberScore; + const bNumRooms = memberScores[b.userId]?.numRooms ?? 0; - if (aScore === bScore) { - if (aNumRooms === bNumRooms) { - return compare(a.userId, b.userId); - } + if (aScore === bScore) { + if (aNumRooms === bNumRooms) { + return compare(a.userId, b.userId); + } - return bNumRooms - aNumRooms; - } - return bScore - aScore; -}; + return bNumRooms - aNumRooms; + } + return bScore - aScore; + }; function joinedRooms(cli: MatrixClient): Room[] { - return cli.getRooms() - .filter(r => r.getMyMembership() === 'join') - // Skip low priority rooms and DMs - .filter(r => !DMRoomMap.shared().getUserIdForRoomId(r.roomId)) - .filter(r => !Object.keys(r.tags).includes("m.lowpriority")); + return ( + cli + .getRooms() + .filter((r) => r.getMyMembership() === "join") + // Skip low priority rooms and DMs + .filter((r) => !DMRoomMap.shared().getUserIdForRoomId(r.roomId)) + .filter((r) => !Object.keys(r.tags).includes("m.lowpriority")) + ); } interface IActivityScore { @@ -64,16 +66,16 @@ interface IActivityScore { // which are closer to "continue this conversation" rather than "this person exists". export function buildActivityScores(cli: MatrixClient): { [key: string]: IActivityScore } { const now = new Date().getTime(); - const earliestAgeConsidered = now - (60 * 60 * 1000); // 1 hour ago + const earliestAgeConsidered = now - 60 * 60 * 1000; // 1 hour ago const maxMessagesConsidered = 50; // so we don't iterate over a huge amount of traffic const events = joinedRooms(cli) - .flatMap(room => takeRight(room.getLiveTimeline().getEvents(), maxMessagesConsidered)) - .filter(ev => ev.getTs() > earliestAgeConsidered); - const senderEvents = groupBy(events, ev => ev.getSender()); - return mapValues(senderEvents, events => { - const lastEvent = maxBy(events, ev => ev.getTs()); + .flatMap((room) => takeRight(room.getLiveTimeline().getEvents(), maxMessagesConsidered)) + .filter((ev) => ev.getTs() > earliestAgeConsidered); + const senderEvents = groupBy(events, (ev) => ev.getSender()); + return mapValues(senderEvents, (events) => { + const lastEvent = maxBy(events, (ev) => ev.getTs()); const distanceFromNow = Math.abs(now - lastEvent.getTs()); // abs to account for slight future messages - const inverseTime = (now - earliestAgeConsidered) - distanceFromNow; + const inverseTime = now - earliestAgeConsidered - distanceFromNow; return { lastSpoke: lastEvent.getTs(), // Scores from being in a room give a 'good' score of about 1.0-1.5, so for our @@ -92,19 +94,18 @@ interface IMemberScore { export function buildMemberScores(cli: MatrixClient): { [key: string]: IMemberScore } { const maxConsideredMembers = 200; - const consideredRooms = joinedRooms(cli).filter(room => room.getJoinedMemberCount() < maxConsideredMembers); - const memberPeerEntries = consideredRooms - .flatMap(room => - room.getJoinedMembers().map(member => - ({ member, roomSize: room.getJoinedMemberCount() }))); + const consideredRooms = joinedRooms(cli).filter((room) => room.getJoinedMemberCount() < maxConsideredMembers); + const memberPeerEntries = consideredRooms.flatMap((room) => + room.getJoinedMembers().map((member) => ({ member, roomSize: room.getJoinedMemberCount() })), + ); const userMeta = groupBy(memberPeerEntries, ({ member }) => member.userId); - return mapValues(userMeta, roomMemberships => { + return mapValues(userMeta, (roomMemberships) => { const maximumPeers = maxConsideredMembers * roomMemberships.length; - const totalPeers = sumBy(roomMemberships, entry => entry.roomSize); + const totalPeers = sumBy(roomMemberships, (entry) => entry.roomSize); return { - member: minBy(roomMemberships, entry => entry.roomSize).member, + member: minBy(roomMemberships, (entry) => entry.roomSize).member, numRooms: roomMemberships.length, - score: Math.max(0, Math.pow(1 - (totalPeers / maximumPeers), 5)), + score: Math.max(0, Math.pow(1 - totalPeers / maximumPeers, 5)), }; }); } diff --git a/src/utils/StorageManager.ts b/src/utils/StorageManager.ts index 1145bcfb15d..249c64acb4e 100644 --- a/src/utils/StorageManager.ts +++ b/src/utils/StorageManager.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { LocalStorageCryptoStore } from 'matrix-js-sdk/src/crypto/store/localStorage-crypto-store'; +import { LocalStorageCryptoStore } from "matrix-js-sdk/src/crypto/store/localStorage-crypto-store"; import { IndexedDBStore } from "matrix-js-sdk/src/store/indexeddb"; import { IndexedDBCryptoStore } from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store"; import { logger } from "matrix-js-sdk/src/logger"; @@ -42,10 +42,11 @@ function error(msg: string, ...args: string[]) { export function tryPersistStorage() { if (navigator.storage && navigator.storage.persist) { - navigator.storage.persist().then(persistent => { + navigator.storage.persist().then((persistent) => { logger.log("StorageManager: Persistent?", persistent); }); - } else if (document.requestStorageAccess) { // Safari + } else if (document.requestStorageAccess) { + // Safari document.requestStorageAccess().then( () => logger.log("StorageManager: Persistent?", true), () => logger.log("StorageManager: Persistent?", false), @@ -101,8 +102,8 @@ export async function checkConsistency() { healthy = false; error( "Data exists in local storage and crypto is marked as initialised " + - " but no data found in crypto store. " + - "IndexedDB storage has likely been evicted by the browser!", + " but no data found in crypto store. " + + "IndexedDB storage has likely been evicted by the browser!", ); } @@ -123,9 +124,7 @@ export async function checkConsistency() { async function checkSyncStore() { let exists = false; try { - exists = await IndexedDBStore.exists( - indexedDB, SYNC_STORE_NAME, - ); + exists = await IndexedDBStore.exists(indexedDB, SYNC_STORE_NAME); log(`Sync store using IndexedDB contains data? ${exists}`); return { exists, healthy: true }; } catch (e) { @@ -138,9 +137,7 @@ async function checkSyncStore() { async function checkCryptoStore() { let exists = false; try { - exists = await IndexedDBCryptoStore.exists( - indexedDB, CRYPTO_STORE_NAME, - ); + exists = await IndexedDBCryptoStore.exists(indexedDB, CRYPTO_STORE_NAME); log(`Crypto store using IndexedDB contains data? ${exists}`); return { exists, healthy: true }; } catch (e) { @@ -183,7 +180,9 @@ async function idbInit(): Promise { idb = await new Promise((resolve, reject) => { const request = indexedDB.open("matrix-react-sdk", 1); request.onerror = reject; - request.onsuccess = () => { resolve(request.result); }; + request.onsuccess = () => { + resolve(request.result); + }; request.onupgradeneeded = () => { const db = request.result; db.createObjectStore("pickleKey"); @@ -192,10 +191,7 @@ async function idbInit(): Promise { }); } -export async function idbLoad( - table: string, - key: string | string[], -): Promise { +export async function idbLoad(table: string, key: string | string[]): Promise { if (!idb) { await idbInit(); } @@ -206,15 +202,13 @@ export async function idbLoad( const objectStore = txn.objectStore(table); const request = objectStore.get(key); request.onerror = reject; - request.onsuccess = (event) => { resolve(request.result); }; + request.onsuccess = (event) => { + resolve(request.result); + }; }); } -export async function idbSave( - table: string, - key: string | string[], - data: any, -): Promise { +export async function idbSave(table: string, key: string | string[], data: any): Promise { if (!idb) { await idbInit(); } @@ -225,14 +219,13 @@ export async function idbSave( const objectStore = txn.objectStore(table); const request = objectStore.put(data, key); request.onerror = reject; - request.onsuccess = (event) => { resolve(); }; + request.onsuccess = (event) => { + resolve(); + }; }); } -export async function idbDelete( - table: string, - key: string | string[], -): Promise { +export async function idbDelete(table: string, key: string | string[]): Promise { if (!idb) { await idbInit(); } @@ -243,6 +236,8 @@ export async function idbDelete( const objectStore = txn.objectStore(table); const request = objectStore.delete(key); request.onerror = reject; - request.onsuccess = () => { resolve(); }; + request.onsuccess = () => { + resolve(); + }; }); } diff --git a/src/utils/Timer.ts b/src/utils/Timer.ts index 38703c12998..f17745029e8 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/UrlUtils.ts b/src/utils/UrlUtils.ts index 6f441ff98e1..bc185b20567 100644 --- a/src/utils/UrlUtils.ts +++ b/src/utils/UrlUtils.ts @@ -23,13 +23,13 @@ import url from "url"; * @returns {string} The abbreviated url */ export function abbreviateUrl(u: string): string { - if (!u) return ''; + if (!u) return ""; const parsedUrl = url.parse(u); // if it's something we can't parse as a url then just return it if (!parsedUrl) return u; - if (parsedUrl.path === '/') { + if (parsedUrl.path === "/") { // we ignore query / hash parts: these aren't relevant for IS server URLs return parsedUrl.host; } @@ -38,10 +38,10 @@ export function abbreviateUrl(u: string): string { } export function unabbreviateUrl(u: string): string { - if (!u) return ''; + if (!u) return ""; let longUrl = u; - if (!u.startsWith('https://')) longUrl = 'https://' + u; + if (!u.startsWith("https://")) longUrl = "https://" + u; const parsed = url.parse(longUrl); if (parsed.hostname === null) return u; diff --git a/src/utils/UserInteractiveAuth.ts b/src/utils/UserInteractiveAuth.ts index e3088fb3cb4..5c7568ee6ea 100644 --- a/src/utils/UserInteractiveAuth.ts +++ b/src/utils/UserInteractiveAuth.ts @@ -25,13 +25,13 @@ type FunctionWithUIA = (auth?: IAuthData, ...args: A[]) => Promise( requestFunction: FunctionWithUIA, opts: Omit, -): ((...args: A[]) => Promise) { - return async function(...args): Promise { +): (...args: A[]) => Promise { + return async function (...args): Promise { return new Promise((resolve, reject) => { const boundFunction = requestFunction.bind(opts.matrixClient) as FunctionWithUIA; boundFunction(undefined, ...args) .then((res) => resolve(res as R)) - .catch(error => { + .catch((error) => { if (error.httpStatus !== 401 || !error.data?.flows) { // doesn't look like an interactive-auth failure return reject(error); diff --git a/src/utils/WellKnownUtils.ts b/src/utils/WellKnownUtils.ts index 451f956f16f..11137549a84 100644 --- a/src/utils/WellKnownUtils.ts +++ b/src/utils/WellKnownUtils.ts @@ -14,16 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { IClientWellKnown } from 'matrix-js-sdk/src/client'; -import { UnstableValue } from 'matrix-js-sdk/src/NamespacedValue'; +import { IClientWellKnown } from "matrix-js-sdk/src/client"; +import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; -import { MatrixClientPeg } from '../MatrixClientPeg'; +import { MatrixClientPeg } from "../MatrixClientPeg"; const CALL_BEHAVIOUR_WK_KEY = "io.element.call_behaviour"; const E2EE_WK_KEY = "io.element.e2ee"; const E2EE_WK_KEY_DEPRECATED = "im.vector.riot.e2ee"; -export const TILE_SERVER_WK_KEY = new UnstableValue( - "m.tile_server", "org.matrix.msc3488.tile_server"); +export const TILE_SERVER_WK_KEY = new UnstableValue("m.tile_server", "org.matrix.msc3488.tile_server"); const EMBEDDED_PAGES_WK_PROPERTY = "io.element.embedded_pages"; /* eslint-disable camelcase */ @@ -66,23 +65,16 @@ export function getTileServerWellKnown(): ITileServerWellKnown | undefined { return tileServerFromWellKnown(MatrixClientPeg.get().getClientWellKnown()); } -export function tileServerFromWellKnown( - clientWellKnown?: IClientWellKnown | undefined, -): ITileServerWellKnown { - return ( - clientWellKnown?.[TILE_SERVER_WK_KEY.name] ?? - clientWellKnown?.[TILE_SERVER_WK_KEY.altName] - ); +export function tileServerFromWellKnown(clientWellKnown?: IClientWellKnown | undefined): ITileServerWellKnown { + return clientWellKnown?.[TILE_SERVER_WK_KEY.name] ?? clientWellKnown?.[TILE_SERVER_WK_KEY.altName]; } export function getEmbeddedPagesWellKnown(): IEmbeddedPagesWellKnown | undefined { return embeddedPagesFromWellKnown(MatrixClientPeg.get()?.getClientWellKnown()); } -export function embeddedPagesFromWellKnown( - clientWellKnown?: IClientWellKnown, -): IEmbeddedPagesWellKnown { - return (clientWellKnown?.[EMBEDDED_PAGES_WK_PROPERTY]); +export function embeddedPagesFromWellKnown(clientWellKnown?: IClientWellKnown): IEmbeddedPagesWellKnown { + return clientWellKnown?.[EMBEDDED_PAGES_WK_PROPERTY]; } export function isSecureBackupRequired(): boolean { @@ -106,10 +98,7 @@ export function getSecureBackupSetupMethods(): SecureBackupSetupMethod[] { wellKnown["secure_backup_setup_methods"].includes(SecureBackupSetupMethod.Passphrase) ) ) { - return [ - SecureBackupSetupMethod.Key, - SecureBackupSetupMethod.Passphrase, - ]; + return [SecureBackupSetupMethod.Key, SecureBackupSetupMethod.Passphrase]; } return wellKnown["secure_backup_setup_methods"]; } diff --git a/src/utils/Whenable.ts b/src/utils/Whenable.ts index 70bd45eb09e..e421cb9fb65 100644 --- a/src/utils/Whenable.ts +++ b/src/utils/Whenable.ts @@ -28,7 +28,7 @@ export type WhenFn = (w: Whenable) => void; * the consumer needs to know *when* that happens. */ export abstract class Whenable implements IDestroyable { - private listeners: {condition: T | null, fn: WhenFn}[] = []; + private listeners: { condition: T | null; fn: WhenFn }[] = []; /** * Sets up a call to `fn` *when* the `condition` is met. diff --git a/src/utils/WidgetUtils.ts b/src/utils/WidgetUtils.ts index 964d795576a..feb73e954dd 100644 --- a/src/utils/WidgetUtils.ts +++ b/src/utils/WidgetUtils.ts @@ -25,11 +25,11 @@ import { ClientEvent, RoomStateEvent } from "matrix-js-sdk/src/matrix"; import { CallType } from "matrix-js-sdk/src/webrtc/call"; import { randomString, randomLowercaseString, randomUppercaseString } from "matrix-js-sdk/src/randomstring"; -import { MatrixClientPeg } from '../MatrixClientPeg'; -import PlatformPeg from '../PlatformPeg'; +import { MatrixClientPeg } from "../MatrixClientPeg"; +import PlatformPeg from "../PlatformPeg"; import SdkConfig from "../SdkConfig"; -import dis from '../dispatcher/dispatcher'; -import WidgetEchoStore from '../stores/WidgetEchoStore'; +import dis from "../dispatcher/dispatcher"; +import WidgetEchoStore from "../stores/WidgetEchoStore"; import { IntegrationManagers } from "../integrations/IntegrationManagers"; import { WidgetType } from "../widgets/WidgetType"; import { Jitsi } from "../widgets/Jitsi"; @@ -59,13 +59,13 @@ export default class WidgetUtils { */ static canUserModifyWidgets(roomId: string): boolean { if (!roomId) { - logger.warn('No room ID specified'); + logger.warn("No room ID specified"); return false; } const client = MatrixClientPeg.get(); if (!client) { - logger.warn('User must be be logged in'); + logger.warn("User must be be logged in"); return false; } @@ -77,7 +77,7 @@ export default class WidgetUtils { const me = client.credentials.userId; if (!me) { - logger.warn('Failed to get user ID'); + logger.warn("Failed to get user ID"); return false; } @@ -87,7 +87,7 @@ export default class WidgetUtils { } // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) - return room.currentState.maySendStateEvent('im.vector.modular.widgets', me); + return room.currentState.maySendStateEvent("im.vector.modular.widgets", me); } // TODO: Generify the name of this function. It's not just scalar. @@ -98,7 +98,7 @@ export default class WidgetUtils { */ static isScalarUrl(testUrlString: string): boolean { if (!testUrlString) { - logger.error('Scalar URL check failed. No URL specified'); + logger.error("Scalar URL check failed. No URL specified"); return false; } @@ -152,21 +152,21 @@ export default class WidgetUtils { } } - const startingAccountDataEvent = MatrixClientPeg.get().getAccountData('m.widgets'); + const startingAccountDataEvent = MatrixClientPeg.get().getAccountData("m.widgets"); if (eventInIntendedState(startingAccountDataEvent)) { resolve(); return; } function onAccountData(ev) { - const currentAccountDataEvent = MatrixClientPeg.get().getAccountData('m.widgets'); + const currentAccountDataEvent = MatrixClientPeg.get().getAccountData("m.widgets"); if (eventInIntendedState(currentAccountDataEvent)) { MatrixClientPeg.get().removeListener(ClientEvent.AccountData, onAccountData); clearTimeout(timerId); 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); @@ -192,7 +192,7 @@ export default class WidgetUtils { // we're waiting for it to be in function eventsInIntendedState(evList) { const widgetPresent = evList.some((ev) => { - return ev.getContent() && ev.getContent()['id'] === widgetId; + return ev.getContent() && ev.getContent()["id"] === widgetId; }); if (add) { return widgetPresent; @@ -203,7 +203,7 @@ export default class WidgetUtils { const room = MatrixClientPeg.get().getRoom(roomId); // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) - const startingWidgetEvents = room.currentState.getStateEvents('im.vector.modular.widgets'); + const startingWidgetEvents = room.currentState.getStateEvents("im.vector.modular.widgets"); if (eventsInIntendedState(startingWidgetEvents)) { resolve(); return; @@ -213,7 +213,7 @@ export default class WidgetUtils { if (ev.getRoomId() !== roomId || ev.getType() !== "im.vector.modular.widgets") return; // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) - const currentWidgetEvents = room.currentState.getStateEvents('im.vector.modular.widgets'); + const currentWidgetEvents = room.currentState.getStateEvents("im.vector.modular.widgets"); if (eventsInIntendedState(currentWidgetEvents)) { MatrixClientPeg.get().removeListener(RoomStateEvent.Events, onRoomStateEvents); @@ -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); @@ -263,7 +263,7 @@ export default class WidgetUtils { content: content, sender: client.getUserId(), state_key: widgetId, - type: 'm.widget', + type: "m.widget", id: widgetId, }; } @@ -272,11 +272,14 @@ export default class WidgetUtils { // since the widget won't appear added until this happens. If we don't // wait for this, the action will complete but if the user is fast enough, // the widget still won't actually be there. - return client.setAccountData('m.widgets', userWidgets).then(() => { - return WidgetUtils.waitForUserWidget(widgetId, addingWidget); - }).then(() => { - dis.dispatch({ action: "user_widget_updated" }); - }); + return client + .setAccountData("m.widgets", userWidgets) + .then(() => { + return WidgetUtils.waitForUserWidget(widgetId, addingWidget); + }) + .then(() => { + dis.dispatch({ action: "user_widget_updated" }); + }); } static setRoomWidget( @@ -309,22 +312,21 @@ export default class WidgetUtils { return WidgetUtils.setRoomWidgetContent(roomId, widgetId, content); } - static setRoomWidgetContent( - roomId: string, - widgetId: string, - content: IWidget, - ) { + static setRoomWidgetContent(roomId: string, widgetId: string, content: IWidget) { const addingWidget = !!content.url; WidgetEchoStore.setRoomWidgetEcho(roomId, widgetId, content); const client = MatrixClientPeg.get(); // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) - return client.sendStateEvent(roomId, "im.vector.modular.widgets", content, widgetId).then(() => { - return WidgetUtils.waitForRoomWidget(widgetId, roomId, addingWidget); - }).finally(() => { - WidgetEchoStore.removeRoomWidgetEcho(roomId, widgetId); - }); + return client + .sendStateEvent(roomId, "im.vector.modular.widgets", content, widgetId) + .then(() => { + return WidgetUtils.waitForRoomWidget(widgetId, roomId, addingWidget); + }) + .finally(() => { + WidgetEchoStore.removeRoomWidgetEcho(roomId, widgetId); + }); } /** @@ -334,7 +336,7 @@ export default class WidgetUtils { */ static getRoomWidgets(room: Room) { // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) - const appsStateEvents = room.currentState.getStateEvents('im.vector.modular.widgets'); + const appsStateEvents = room.currentState.getStateEvents("im.vector.modular.widgets"); if (!appsStateEvents) { return []; } @@ -351,9 +353,9 @@ export default class WidgetUtils { static getUserWidgets(): Record { const client = MatrixClientPeg.get(); if (!client) { - throw new Error('User not logged in'); + throw new Error("User not logged in"); } - const userWidgets = client.getAccountData('m.widgets'); + const userWidgets = client.getAccountData("m.widgets"); if (userWidgets && userWidgets.getContent()) { return userWidgets.getContent(); } @@ -383,12 +385,12 @@ export default class WidgetUtils { */ static getIntegrationManagerWidgets(): IWidgetEvent[] { const widgets = WidgetUtils.getUserWidgetsArray(); - return widgets.filter(w => w.content && w.content.type === "m.integration_manager"); + return widgets.filter((w) => w.content && w.content.type === "m.integration_manager"); } static getRoomWidgetsOfType(room: Room, type: WidgetType): MatrixEvent[] { const widgets = WidgetUtils.getRoomWidgets(room) || []; - return widgets.filter(w => { + return widgets.filter((w) => { const content = w.getContent(); return content.url && type.matches(content.type); }); @@ -397,9 +399,9 @@ export default class WidgetUtils { static async removeIntegrationManagerWidgets(): Promise { const client = MatrixClientPeg.get(); if (!client) { - throw new Error('User not logged in'); + throw new Error("User not logged in"); } - const widgets = client.getAccountData('m.widgets'); + const widgets = client.getAccountData("m.widgets"); if (!widgets) return; const userWidgets: Record = widgets.getContent() || {}; Object.entries(userWidgets).forEach(([key, widget]) => { @@ -407,16 +409,16 @@ export default class WidgetUtils { delete userWidgets[key]; } }); - await client.setAccountData('m.widgets', userWidgets); + await client.setAccountData("m.widgets", userWidgets); } static addIntegrationManagerWidget(name: string, uiUrl: string, apiUrl: string): Promise { return WidgetUtils.setUserWidget( - "integration_manager_" + (new Date().getTime()), + "integration_manager_" + new Date().getTime(), WidgetType.INTEGRATION_MANAGER, uiUrl, "Integration manager: " + name, - { "api_url": apiUrl }, + { api_url: apiUrl }, ); } @@ -427,17 +429,17 @@ export default class WidgetUtils { static async removeStickerpickerWidgets(): Promise { const client = MatrixClientPeg.get(); if (!client) { - throw new Error('User not logged in'); + throw new Error("User not logged in"); } - const widgets = client.getAccountData('m.widgets'); + const widgets = client.getAccountData("m.widgets"); if (!widgets) return; const userWidgets: Record = widgets.getContent() || {}; Object.entries(userWidgets).forEach(([key, widget]) => { - if (widget.content && widget.content.type === 'm.stickerpicker') { + if (widget.content && widget.content.type === "m.stickerpicker") { delete userWidgets[key]; } }); - await client.setAccountData('m.widgets', userWidgets); + await client.setAccountData("m.widgets", userWidgets); } static async addJitsiWidget( @@ -452,7 +454,7 @@ export default class WidgetUtils { const widgetId = randomString(24); // Must be globally unique let confId; - if (auth === 'openidtoken-jwt') { + if (auth === "openidtoken-jwt") { // Create conference ID from room ID // For compatibility with Jitsi, use base32 without padding. // More details here: @@ -465,8 +467,8 @@ export default class WidgetUtils { // TODO: Remove URL hacks when the mobile clients eventually support v2 widgets const widgetUrl = new URL(WidgetUtils.getLocalJitsiWrapperUrl({ auth })); - widgetUrl.search = ''; // Causes the URL class use searchParams instead - widgetUrl.searchParams.set('confId', confId); + widgetUrl.search = ""; // Causes the URL class use searchParams instead + widgetUrl.searchParams.set("confId", confId); await WidgetUtils.setRoomWidget(roomId, widgetId, WidgetType.JITSI, widgetUrl.toString(), name, { conferenceId: confId, @@ -498,26 +500,26 @@ export default class WidgetUtils { return app as IApp; } - static getLocalJitsiWrapperUrl(opts: {forLocalRender?: boolean, auth?: string} = {}) { + static getLocalJitsiWrapperUrl(opts: { forLocalRender?: boolean; auth?: string } = {}) { // NB. we can't just encodeURIComponent all of these because the $ signs need to be there const queryStringParts = [ - 'conferenceDomain=$domain', - 'conferenceId=$conferenceId', - 'isAudioOnly=$isAudioOnly', - 'isVideoChannel=$isVideoChannel', - 'displayName=$matrix_display_name', - 'avatarUrl=$matrix_avatar_url', - 'userId=$matrix_user_id', - 'roomId=$matrix_room_id', - 'theme=$theme', - 'roomName=$roomName', + "conferenceDomain=$domain", + "conferenceId=$conferenceId", + "isAudioOnly=$isAudioOnly", + "isVideoChannel=$isVideoChannel", + "displayName=$matrix_display_name", + "avatarUrl=$matrix_avatar_url", + "userId=$matrix_user_id", + "roomId=$matrix_room_id", + "theme=$theme", + "roomName=$roomName", `supportsScreensharing=${PlatformPeg.get().supportsJitsiScreensharing()}`, - 'language=$org.matrix.msc2873.client_language', + "language=$org.matrix.msc2873.client_language", ]; if (opts.auth) { queryStringParts.push(`auth=${opts.auth}`); } - const queryString = queryStringParts.join('&'); + const queryString = queryStringParts.join("&"); let baseUrl = window.location.href; if (window.location.protocol !== "https:" && !opts.forLocalRender) { @@ -550,7 +552,9 @@ export default class WidgetUtils { static editWidget(room: Room, app: IApp): void { // noinspection JSIgnoredPromiseFromCall - IntegrationManagers.sharedInstance().getPrimaryManager().open(room, 'type_' + app.type, app.id); + IntegrationManagers.sharedInstance() + .getPrimaryManager() + .open(room, "type_" + app.type, app.id); } static isManagedByManager(app) { diff --git a/src/utils/arrays.ts b/src/utils/arrays.ts index b82be21443a..cabfe3a83e3 100644 --- a/src/utils/arrays.ts +++ b/src/utils/arrays.ts @@ -70,7 +70,7 @@ export function arraySmoothingResample(input: number[], points: number): number[ // never end, and we can over-average the data. Instead, we'll get as far as // we can and do a followup fast resample (the neighbouring points will be close // to the actual waveform, so we can get away with this safely). - while (samples.length > (points * 2) || samples.length === 0) { + while (samples.length > points * 2 || samples.length === 0) { samples = []; for (let i = 1; i < input.length - 1; i += 2) { const prevPoint = input[i - 1]; @@ -102,7 +102,7 @@ export function arraySmoothingResample(input: number[], points: number): number[ export function arrayRescale(input: number[], newMin: number, newMax: number): number[] { const min: number = Math.min(...input); const max: number = Math.max(...input); - return input.map(v => percentageWithin(percentageOf(v, min, max), newMin, newMax)); + return input.map((v) => percentageWithin(percentageOf(v, min, max), newMin, newMax)); } /** @@ -174,8 +174,8 @@ export function arrayHasDiff(a: any[], b: any[]): boolean { if (a.length === b.length) { // When the lengths are equal, check to see if either array is missing // an element from the other. - if (b.some(i => !a.includes(i))) return true; - if (a.some(i => !b.includes(i))) return true; + if (b.some((i) => !a.includes(i))) return true; + if (a.some((i) => !b.includes(i))) return true; // if all the keys are common, say so return false; @@ -184,7 +184,7 @@ export function arrayHasDiff(a: any[], b: any[]): boolean { } } -export type Diff = { added: T[], removed: T[] }; +export type Diff = { added: T[]; removed: T[] }; /** * Performs a diff on two arrays. The result is what is different with the @@ -196,8 +196,8 @@ export type Diff = { added: T[], removed: T[] }; */ export function arrayDiff(a: T[], b: T[]): Diff { return { - added: b.filter(i => !a.includes(i)), - removed: a.filter(i => !b.includes(i)), + added: b.filter((i) => !a.includes(i)), + removed: a.filter((i) => !b.includes(i)), }; } @@ -208,7 +208,7 @@ export function arrayDiff(a: T[], b: T[]): Diff { * @returns The intersection of the arrays. */ export function arrayIntersection(a: T[], b: T[]): T[] { - return a.filter(i => b.includes(i)); + return a.filter((i) => b.includes(i)); } /** @@ -217,10 +217,12 @@ export function arrayIntersection(a: T[], b: T[]): T[] { * @returns The union of all given arrays. */ export function arrayUnion(...a: T[][]): T[] { - return Array.from(a.reduce((c, v) => { - v.forEach(i => c.add(i)); - return c; - }, new Set())); + return Array.from( + a.reduce((c, v) => { + v.forEach((i) => c.add(i)); + return c; + }, new Set()), + ); } /** @@ -246,8 +248,7 @@ export class ArrayUtil { * Create a new array helper. * @param a The array to help. Can be modified in-place. */ - constructor(private a: T[]) { - } + constructor(private a: T[]) {} /** * The value of this array, after all appropriate alterations. @@ -280,8 +281,7 @@ export class GroupedArray { * Creates a new group helper. * @param val The group to help. Can be modified in-place. */ - constructor(private val: Map) { - } + constructor(private val: Map) {} /** * The value of this group, after all applicable alterations. diff --git a/src/utils/beacon/bounds.ts b/src/utils/beacon/bounds.ts index 43c063b1c55..c7942953e40 100644 --- a/src/utils/beacon/bounds.ts +++ b/src/utils/beacon/bounds.ts @@ -36,8 +36,9 @@ export type Bounds = { * west of Greenwich has a negative longitude, min -180 */ export const getBeaconBounds = (beacons: Beacon[]): Bounds | undefined => { - const coords = beacons.filter(beacon => !!beacon.latestLocationState) - .map(beacon => parseGeoUri(beacon.latestLocationState.uri)); + const coords = beacons + .filter((beacon) => !!beacon.latestLocationState) + .map((beacon) => parseGeoUri(beacon.latestLocationState.uri)); if (!coords.length) { return; @@ -51,6 +52,6 @@ export const getBeaconBounds = (beacons: Beacon[]): Bounds | undefined => { north: sortedByLat[0].latitude, south: sortedByLat[sortedByLat.length - 1].latitude, east: sortedByLong[0].longitude, - west: sortedByLong[sortedByLong.length -1].longitude, + west: sortedByLong[sortedByLong.length - 1].longitude, }; }; diff --git a/src/utils/beacon/duration.ts b/src/utils/beacon/duration.ts index bbd51c7b5d4..136207b6f3c 100644 --- a/src/utils/beacon/duration.ts +++ b/src/utils/beacon/duration.ts @@ -25,7 +25,7 @@ import { Beacon } from "matrix-js-sdk/src/matrix"; * @returns remainingMs */ export const msUntilExpiry = (startTimestamp: number, durationMs: number): number => - Math.max(0, (startTimestamp + durationMs) - Date.now()); + Math.max(0, startTimestamp + durationMs - Date.now()); export const getBeaconMsUntilExpiry = (beaconInfo: BeaconInfoState): number => msUntilExpiry(beaconInfo.timestamp, beaconInfo.timeout); diff --git a/src/utils/beacon/geolocation.ts b/src/utils/beacon/geolocation.ts index 6925ca73b58..5142984478f 100644 --- a/src/utils/beacon/geolocation.ts +++ b/src/utils/beacon/geolocation.ts @@ -20,15 +20,15 @@ import { logger } from "matrix-js-sdk/src/logger"; // https://developer.mozilla.org/en-US/docs/Web/API/GeolocationPositionError export enum GeolocationError { // no navigator.geolocation - Unavailable = 'Unavailable', + Unavailable = "Unavailable", // The acquisition of the geolocation information failed because the page didn't have the permission to do it. - PermissionDenied = 'PermissionDenied', + PermissionDenied = "PermissionDenied", // The acquisition of the geolocation failed because at least one internal source of position returned an internal error. - PositionUnavailable = 'PositionUnavailable', + PositionUnavailable = "PositionUnavailable", // The time allowed to acquire the geolocation was reached before the information was obtained. - Timeout = 'Timeout', + Timeout = "Timeout", // other unexpected failure - Default = 'Default' + Default = "Default", } const GeolocationOptions = { @@ -37,12 +37,12 @@ const GeolocationOptions = { }; const isGeolocationPositionError = (error: unknown): error is GeolocationPositionError => - typeof error === 'object' && !!error['PERMISSION_DENIED']; + typeof error === "object" && !!error["PERMISSION_DENIED"]; /** * Maps GeolocationPositionError to our GeolocationError enum */ export const mapGeolocationError = (error: GeolocationPositionError | Error): GeolocationError => { - logger.error('Geolocation failed', error?.message ?? error); + logger.error("Geolocation failed", error?.message ?? error); if (isGeolocationPositionError(error)) { switch (error?.code) { @@ -83,9 +83,7 @@ export type TimedGeoUri = { }; export const genericPositionFromGeolocation = (geoPosition: GeolocationPosition): GenericPosition => { - const { - latitude, longitude, altitude, accuracy, - } = geoPosition.coords; + const { latitude, longitude, altitude, accuracy } = geoPosition.coords; return { // safari reports geolocation timestamps as Apple Cocoa Core Data timestamp @@ -93,23 +91,18 @@ export const genericPositionFromGeolocation = (geoPosition: GeolocationPosition) // they also use local time, not utc // to simplify, just use Date.now() timestamp: Date.now(), - latitude, longitude, altitude, accuracy, + latitude, + longitude, + altitude, + accuracy, }; }; export const getGeoUri = (position: GenericPosition): string => { const lat = position.latitude; const lon = position.longitude; - const alt = ( - Number.isFinite(position.altitude) - ? `,${position.altitude}` - : "" - ); - const acc = ( - Number.isFinite(position.accuracy) - ? `;u=${position.accuracy}` - : "" - ); + const alt = Number.isFinite(position.altitude) ? `,${position.altitude}` : ""; + const acc = Number.isFinite(position.accuracy) ? `;u=${position.accuracy}` : ""; return `geo:${lat},${lon}${alt}${acc}`; }; diff --git a/src/utils/beacon/getShareableLocation.ts b/src/utils/beacon/getShareableLocation.ts index b2a63db0607..1b353f2b04e 100644 --- a/src/utils/beacon/getShareableLocation.ts +++ b/src/utils/beacon/getShareableLocation.ts @@ -14,11 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { - MatrixClient, - MatrixEvent, - getBeaconInfoIdentifier, -} from "matrix-js-sdk/src/matrix"; +import { MatrixClient, MatrixEvent, getBeaconInfoIdentifier } from "matrix-js-sdk/src/matrix"; /** * Beacons should only have shareable locations (open in external mapping tool, forward) diff --git a/src/utils/beacon/index.ts b/src/utils/beacon/index.ts index 3da707b6036..34be8c9f5ea 100644 --- a/src/utils/beacon/index.ts +++ b/src/utils/beacon/index.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -export * from './duration'; -export * from './geolocation'; -export * from './useBeacon'; -export * from './useOwnLiveBeacons'; +export * from "./duration"; +export * from "./geolocation"; +export * from "./useBeacon"; +export * from "./useOwnLiveBeacons"; diff --git a/src/utils/beacon/timeline.ts b/src/utils/beacon/timeline.ts index 9c566e0d680..a04a61f3649 100644 --- a/src/utils/beacon/timeline.ts +++ b/src/utils/beacon/timeline.ts @@ -21,11 +21,8 @@ import { M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon"; * beacon_info events without live property set to true * should be displayed in the timeline */ -export const shouldDisplayAsBeaconTile = (event: MatrixEvent): boolean => ( +export const shouldDisplayAsBeaconTile = (event: MatrixEvent): boolean => M_BEACON_INFO.matches(event.getType()) && - ( - event.getContent()?.live || + (event.getContent()?.live || // redacted beacons should show 'message deleted' tile - event.isRedacted() - ) -); + event.isRedacted()); diff --git a/src/utils/beacon/useBeacon.ts b/src/utils/beacon/useBeacon.ts index e1dcfc49758..2726262ec4c 100644 --- a/src/utils/beacon/useBeacon.ts +++ b/src/utils/beacon/useBeacon.ts @@ -15,12 +15,7 @@ limitations under the License. */ import { useContext, useEffect, useState } from "react"; -import { - Beacon, - BeaconEvent, - MatrixEvent, - getBeaconInfoIdentifier, -} from "matrix-js-sdk/src/matrix"; +import { Beacon, BeaconEvent, MatrixEvent, getBeaconInfoIdentifier } from "matrix-js-sdk/src/matrix"; import MatrixClientContext from "../../contexts/MatrixClientContext"; import { useEventEmitterState } from "../../hooks/useEventEmitter"; @@ -56,11 +51,7 @@ export const useBeacon = (beaconInfoEvent: MatrixEvent): Beacon | undefined => { // beacon update will fire when this beacon is superseded // check the updated event id for equality to the matrix event - const beaconInstanceEventId = useEventEmitterState( - beacon, - BeaconEvent.Update, - () => beacon?.beaconInfoId, - ); + const beaconInstanceEventId = useEventEmitterState(beacon, BeaconEvent.Update, () => beacon?.beaconInfoId); useEffect(() => { if (beaconInstanceEventId && beaconInstanceEventId !== beaconInfoEvent.getId()) { diff --git a/src/utils/beacon/useLiveBeacons.ts b/src/utils/beacon/useLiveBeacons.ts index cbde1a40e76..fd6b2164d9e 100644 --- a/src/utils/beacon/useLiveBeacons.ts +++ b/src/utils/beacon/useLiveBeacons.ts @@ -14,12 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { - Beacon, - Room, - RoomStateEvent, - MatrixClient, -} from "matrix-js-sdk/src/matrix"; +import { Beacon, Room, RoomStateEvent, MatrixClient } from "matrix-js-sdk/src/matrix"; import { useEventEmitterState } from "../../hooks/useEventEmitter"; @@ -28,13 +23,11 @@ import { useEventEmitterState } from "../../hooks/useEventEmitter"; * * Beacons are removed from array when they become inactive */ -export const useLiveBeacons = (roomId: Room['roomId'], matrixClient: MatrixClient): Beacon[] => { +export const useLiveBeacons = (roomId: Room["roomId"], matrixClient: MatrixClient): Beacon[] => { const room = matrixClient.getRoom(roomId); - const liveBeacons = useEventEmitterState( - room.currentState, - RoomStateEvent.BeaconLiveness, - () => room.currentState?.liveBeaconIds.map(beaconIdentifier => room.currentState.beacons.get(beaconIdentifier)), + const liveBeacons = useEventEmitterState(room.currentState, RoomStateEvent.BeaconLiveness, () => + room.currentState?.liveBeaconIds.map((beaconIdentifier) => room.currentState.beacons.get(beaconIdentifier)), ); return liveBeacons; diff --git a/src/utils/beacon/useOwnLiveBeacons.ts b/src/utils/beacon/useOwnLiveBeacons.ts index d83a66a1d4f..a70eceb7df6 100644 --- a/src/utils/beacon/useOwnLiveBeacons.ts +++ b/src/utils/beacon/useOwnLiveBeacons.ts @@ -43,15 +43,13 @@ export const useOwnLiveBeacons = (liveBeaconIds: BeaconIdentifier[]): LiveBeacon const hasLocationPublishError = useEventEmitterState( OwnBeaconStore.instance, OwnBeaconStoreEvent.LocationPublishError, - () => - liveBeaconIds.some(OwnBeaconStore.instance.beaconHasLocationPublishError), + () => liveBeaconIds.some(OwnBeaconStore.instance.beaconHasLocationPublishError), ); const hasStopSharingError = useEventEmitterState( OwnBeaconStore.instance, OwnBeaconStoreEvent.BeaconUpdateError, - () => - liveBeaconIds.some(id => OwnBeaconStore.instance.beaconUpdateErrors.has(id)), + () => liveBeaconIds.some((id) => OwnBeaconStore.instance.beaconUpdateErrors.has(id)), ); useEffect(() => { @@ -66,21 +64,22 @@ export const useOwnLiveBeacons = (liveBeaconIds: BeaconIdentifier[]): LiveBeacon }, [liveBeaconIds]); // select the beacon with latest expiry to display expiry time - const beacon = liveBeaconIds.map(beaconId => OwnBeaconStore.instance.getBeaconById(beaconId)) + const beacon = liveBeaconIds + .map((beaconId) => OwnBeaconStore.instance.getBeaconById(beaconId)) .sort(sortBeaconsByLatestExpiry) .shift(); const onStopSharing = async () => { setStoppingInProgress(true); try { - await Promise.all(liveBeaconIds.map(beaconId => OwnBeaconStore.instance.stopBeacon(beaconId))); + await Promise.all(liveBeaconIds.map((beaconId) => OwnBeaconStore.instance.stopBeacon(beaconId))); } catch (error) { setStoppingInProgress(false); } }; const onResetLocationPublishError = () => { - liveBeaconIds.forEach(beaconId => { + liveBeaconIds.forEach((beaconId) => { OwnBeaconStore.instance.resetLocationPublishError(beaconId); }); }; diff --git a/src/utils/blobs.ts b/src/utils/blobs.ts index 9dea3d226c7..e1afe212a8d 100644 --- a/src/utils/blobs.ts +++ b/src/utils/blobs.ts @@ -49,34 +49,34 @@ limitations under the License. // text/html, text/xhtml, image/svg, image/svg+xml, image/pdf, and similar. const ALLOWED_BLOB_MIMETYPES = [ - 'image/jpeg', - 'image/gif', - 'image/png', - 'image/apng', - 'image/webp', - 'image/avif', + "image/jpeg", + "image/gif", + "image/png", + "image/apng", + "image/webp", + "image/avif", - 'video/mp4', - 'video/webm', - 'video/ogg', - 'video/quicktime', + "video/mp4", + "video/webm", + "video/ogg", + "video/quicktime", - 'audio/mp4', - 'audio/webm', - 'audio/aac', - 'audio/mpeg', - 'audio/ogg', - 'audio/wave', - 'audio/wav', - 'audio/x-wav', - 'audio/x-pn-wav', - 'audio/flac', - 'audio/x-flac', + "audio/mp4", + "audio/webm", + "audio/aac", + "audio/mpeg", + "audio/ogg", + "audio/wave", + "audio/wav", + "audio/x-wav", + "audio/x-pn-wav", + "audio/flac", + "audio/x-flac", ]; export function getBlobSafeMimeType(mimetype: string): string { if (!ALLOWED_BLOB_MIMETYPES.includes(mimetype)) { - return 'application/octet-stream'; + return "application/octet-stream"; } return mimetype; } diff --git a/src/utils/colour.ts b/src/utils/colour.ts index 96eabd4eb40..518b11f835a 100644 --- a/src/utils/colour.ts +++ b/src/utils/colour.ts @@ -14,12 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { split } from 'lodash'; +import { split } from "lodash"; export function textToHtmlRainbow(str: string): string { const frequency = (2 * Math.PI) / str.length; - return split(str, '') + return split(str, "") .map((c, i) => { if (c === " ") { return c; diff --git a/src/utils/createMatrixClient.ts b/src/utils/createMatrixClient.ts index e8b52768807..389d7269b4f 100644 --- a/src/utils/createMatrixClient.ts +++ b/src/utils/createMatrixClient.ts @@ -62,9 +62,7 @@ export default function createMatrixClient(opts: ICreateClientOpts): MatrixClien } if (indexedDB) { - storeOpts.cryptoStore = new IndexedDBCryptoStore( - indexedDB, "matrix-js-sdk:crypto", - ); + storeOpts.cryptoStore = new IndexedDBCryptoStore(indexedDB, "matrix-js-sdk:crypto"); } else if (localStorage) { storeOpts.cryptoStore = new LocalStorageCryptoStore(localStorage); } else { diff --git a/src/utils/device/clientInformation.ts b/src/utils/device/clientInformation.ts index 5c9b65b54bc..e97135ab1f8 100644 --- a/src/utils/device/clientInformation.ts +++ b/src/utils/device/clientInformation.ts @@ -70,21 +70,19 @@ export const recordClientInformation = async ( * @todo(kerrya) revisit after MSC3391: account data deletion is done * (PSBE-12) */ -export const removeClientInformation = async ( - matrixClient: MatrixClient, -): Promise => { +export const removeClientInformation = async (matrixClient: MatrixClient): Promise => { const deviceId = matrixClient.getDeviceId(); const type = getClientInformationEventType(deviceId); const clientInformation = getDeviceClientInformation(matrixClient, deviceId); - // if a non-empty client info event exists, overwrite to remove the content + // if a non-empty client info event exists, remove it if (clientInformation.name || clientInformation.version || clientInformation.url) { - await matrixClient.setAccountData(type, {}); + await matrixClient.deleteAccountData(type); } }; const sanitizeContentString = (value: unknown): string | undefined => - value && typeof value === 'string' ? value : undefined; + value && typeof value === "string" ? value : undefined; export const getDeviceClientInformation = (matrixClient: MatrixClient, deviceId: string): DeviceClientInformation => { const event = matrixClient.getAccountData(getClientInformationEventType(deviceId)); @@ -101,4 +99,3 @@ export const getDeviceClientInformation = (matrixClient: MatrixClient, deviceId: url: sanitizeContentString(url), }; }; - diff --git a/src/utils/device/parseUserAgent.ts b/src/utils/device/parseUserAgent.ts index 3eee6177652..724ef617da0 100644 --- a/src/utils/device/parseUserAgent.ts +++ b/src/utils/device/parseUserAgent.ts @@ -14,13 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import UAParser from 'ua-parser-js'; +import UAParser from "ua-parser-js"; export enum DeviceType { - Desktop = 'Desktop', - Mobile = 'Mobile', - Web = 'Web', - Unknown = 'Unknown', + Desktop = "Desktop", + Mobile = "Mobile", + Web = "Web", + Unknown = "Unknown", } export type ExtendedDeviceInformation = { deviceType: DeviceType; @@ -42,17 +42,13 @@ const getDeviceType = ( browser: UAParser.IBrowser, operatingSystem: UAParser.IOS, ): DeviceType => { - if (browser.name === 'Electron') { + if (browser.name === "Electron") { return DeviceType.Desktop; } if (!!browser.name) { return DeviceType.Web; } - if ( - device.type === 'mobile' || - operatingSystem.name?.includes('Android') || - userAgent.indexOf(IOS_KEYWORD) > -1 - ) { + if (device.type === "mobile" || operatingSystem.name?.includes("Android") || userAgent.indexOf(IOS_KEYWORD) > -1) { return DeviceType.Mobile; } return DeviceType.Unknown; @@ -72,18 +68,18 @@ const checkForCustomValues = (userAgent: string): CustomValues => { return {}; } - const mightHaveDevice = userAgent.includes('('); + const mightHaveDevice = userAgent.includes("("); if (!mightHaveDevice) { return {}; } - const deviceInfoSegments = userAgent.substring(userAgent.indexOf('(') + 1).split('; '); + const deviceInfoSegments = userAgent.substring(userAgent.indexOf("(") + 1).split("; "); const customDeviceModel = deviceInfoSegments[0] || undefined; const customDeviceOS = deviceInfoSegments[1] || undefined; return { customDeviceModel, customDeviceOS }; }; const concatenateNameAndVersion = (name?: string, version?: string): string | undefined => - name && [name, version].filter(Boolean).join(' '); + name && [name, version].filter(Boolean).join(" "); export const parseUserAgent = (userAgent?: string): ExtendedDeviceInformation => { if (!userAgent) { @@ -111,9 +107,8 @@ export const parseUserAgent = (userAgent?: string): ExtendedDeviceInformation => const client = concatenateNameAndVersion(browser.name, browser.version); // only try to parse custom model and OS when device type is known - const { customDeviceModel, customDeviceOS } = deviceType !== DeviceType.Unknown - ? checkForCustomValues(userAgent) - : {} as CustomValues; + const { customDeviceModel, customDeviceOS } = + deviceType !== DeviceType.Unknown ? checkForCustomValues(userAgent) : ({} as CustomValues); return { deviceType, diff --git a/src/utils/device/snoozeBulkUnverifiedDeviceReminder.ts b/src/utils/device/snoozeBulkUnverifiedDeviceReminder.ts new file mode 100644 index 00000000000..ec70f49240e --- /dev/null +++ b/src/utils/device/snoozeBulkUnverifiedDeviceReminder.ts @@ -0,0 +1,40 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { logger } from "matrix-js-sdk/src/logger"; + +const SNOOZE_KEY = "mx_snooze_bulk_unverified_device_nag"; +// one week +const snoozePeriod = 1000 * 60 * 60 * 24 * 7; +export const snoozeBulkUnverifiedDeviceReminder = () => { + try { + localStorage.setItem(SNOOZE_KEY, String(Date.now())); + } catch (error) { + logger.error("Failed to persist bulk unverified device nag snooze", error); + } +}; + +export const isBulkUnverifiedDeviceReminderSnoozed = () => { + try { + const snoozedTimestamp = localStorage.getItem(SNOOZE_KEY); + + const parsedTimestamp = Number.parseInt(snoozedTimestamp || "", 10); + + return Number.isInteger(parsedTimestamp) && parsedTimestamp + snoozePeriod > Date.now(); + } catch (error) { + return false; + } +}; diff --git a/src/utils/direct-messages.ts b/src/utils/direct-messages.ts index 2ed20b4f647..3e117c07172 100644 --- a/src/utils/direct-messages.ts +++ b/src/utils/direct-messages.ts @@ -29,10 +29,7 @@ import { privateShouldBeEncrypted } from "./rooms"; import { createDmLocalRoom } from "./dm/createDmLocalRoom"; import { startDm } from "./dm/startDm"; -export async function startDmOnFirstMessage( - client: MatrixClient, - targets: Member[], -): Promise { +export async function startDmOnFirstMessage(client: MatrixClient, targets: Member[]): Promise { const existingRoom = findDMRoom(client, targets); if (existingRoom) { dis.dispatch({ @@ -114,7 +111,7 @@ export class DirectoryMember extends Member { private readonly avatarUrl?: string; // eslint-disable-next-line camelcase - constructor(userDirResult: { user_id: string, display_name?: string, avatar_url?: string }) { + constructor(userDirResult: { user_id: string; display_name?: string; avatar_url?: string }) { super(); this._userId = userDirResult.user_id; this.displayName = userDirResult.display_name; @@ -147,7 +144,7 @@ export class ThreepidMember extends Member { // better type support in the react-sdk we can use this trick to determine the kind // of 3PID we're dealing with, if any. get isEmail(): boolean { - return this.id.includes('@'); + return this.id.includes("@"); } // These next class members are for the Member interface @@ -181,9 +178,9 @@ export async function determineCreateRoomEncryptionOption(client: MatrixClient, if (privateShouldBeEncrypted()) { // Check whether all users have uploaded device keys before. // If so, enable encryption in the new room. - const has3PidMembers = targets.some(t => t instanceof ThreepidMember); + const has3PidMembers = targets.some((t) => t instanceof ThreepidMember); if (!has3PidMembers) { - const targetIds = targets.map(t => t.userId); + const targetIds = targets.map((t) => t.userId); const allHaveDeviceKeys = await canEncryptToAllUsers(client, targetIds); if (allHaveDeviceKeys) { return true; diff --git a/src/utils/dm/createDmLocalRoom.ts b/src/utils/dm/createDmLocalRoom.ts index 9fe68986bcc..822ae9700b3 100644 --- a/src/utils/dm/createDmLocalRoom.ts +++ b/src/utils/dm/createDmLocalRoom.ts @@ -30,28 +30,27 @@ import { determineCreateRoomEncryptionOption, Member } from "../../../src/utils/ * @param {Member[]} targets DM partners * @returns {Promise} Resolves to the new local room */ -export async function createDmLocalRoom( - client: MatrixClient, - targets: Member[], -): Promise { +export async function createDmLocalRoom(client: MatrixClient, targets: Member[]): Promise { const userId = client.getUserId(); const localRoom = new LocalRoom(LOCAL_ROOM_ID_PREFIX + client.makeTxnId(), client, userId); const events = []; - events.push(new MatrixEvent({ - event_id: `~${localRoom.roomId}:${client.makeTxnId()}`, - type: EventType.RoomCreate, - content: { - creator: userId, - room_version: KNOWN_SAFE_ROOM_VERSION, - }, - state_key: "", - user_id: userId, - sender: userId, - room_id: localRoom.roomId, - origin_server_ts: Date.now(), - })); + events.push( + new MatrixEvent({ + event_id: `~${localRoom.roomId}:${client.makeTxnId()}`, + type: EventType.RoomCreate, + content: { + creator: userId, + room_version: KNOWN_SAFE_ROOM_VERSION, + }, + state_key: "", + user_id: userId, + sender: userId, + room_id: localRoom.roomId, + origin_server_ts: Date.now(), + }), + ); if (await determineCreateRoomEncryptionOption(client, targets)) { localRoom.encrypted = true; @@ -71,45 +70,51 @@ export async function createDmLocalRoom( ); } - events.push(new MatrixEvent({ - event_id: `~${localRoom.roomId}:${client.makeTxnId()}`, - type: EventType.RoomMember, - content: { - displayname: userId, - membership: "join", - }, - state_key: userId, - user_id: userId, - sender: userId, - room_id: localRoom.roomId, - })); - - targets.forEach((target: Member) => { - events.push(new MatrixEvent({ + events.push( + new MatrixEvent({ event_id: `~${localRoom.roomId}:${client.makeTxnId()}`, type: EventType.RoomMember, content: { - displayname: target.name, - avatar_url: target.getMxcAvatarUrl(), - membership: "invite", - isDirect: true, - }, - state_key: target.userId, - sender: userId, - room_id: localRoom.roomId, - })); - events.push(new MatrixEvent({ - event_id: `~${localRoom.roomId}:${client.makeTxnId()}`, - type: EventType.RoomMember, - content: { - displayname: target.name, - avatar_url: target.getMxcAvatarUrl(), + displayname: userId, membership: "join", }, - state_key: target.userId, - sender: target.userId, + state_key: userId, + user_id: userId, + sender: userId, room_id: localRoom.roomId, - })); + }), + ); + + targets.forEach((target: Member) => { + events.push( + new MatrixEvent({ + event_id: `~${localRoom.roomId}:${client.makeTxnId()}`, + type: EventType.RoomMember, + content: { + displayname: target.name, + avatar_url: target.getMxcAvatarUrl(), + membership: "invite", + isDirect: true, + }, + state_key: target.userId, + sender: userId, + room_id: localRoom.roomId, + }), + ); + events.push( + new MatrixEvent({ + event_id: `~${localRoom.roomId}:${client.makeTxnId()}`, + type: EventType.RoomMember, + content: { + displayname: target.name, + avatar_url: target.getMxcAvatarUrl(), + membership: "join", + }, + state_key: target.userId, + sender: target.userId, + room_id: localRoom.roomId, + }), + ); }); localRoom.targets = targets; diff --git a/src/utils/dm/findDMForUser.ts b/src/utils/dm/findDMForUser.ts index 47e3c87a74d..babf8bd2afd 100644 --- a/src/utils/dm/findDMForUser.ts +++ b/src/utils/dm/findDMForUser.ts @@ -30,29 +30,30 @@ import { getFunctionalMembers } from "../room/getFunctionalMembers"; */ export function findDMForUser(client: MatrixClient, userId: string): Room { const roomIds = DMRoomMap.shared().getDMRoomsForUserId(userId); - const rooms = roomIds.map(id => client.getRoom(id)); - const suitableDMRooms = rooms.filter(r => { - // Validate that we are joined and the other person is also joined. We'll also make sure - // that the room also looks like a DM (until we have canonical DMs to tell us). For now, - // a DM is a room of two people that contains those two people exactly. This does mean - // that bots, assistants, etc will ruin a room's DM-ness, though this is a problem for - // canonical DMs to solve. - if (r && r.getMyMembership() === "join") { - if (isLocalRoom(r)) return false; + const rooms = roomIds.map((id) => client.getRoom(id)); + const suitableDMRooms = rooms + .filter((r) => { + // Validate that we are joined and the other person is also joined. We'll also make sure + // that the room also looks like a DM (until we have canonical DMs to tell us). For now, + // a DM is a room of two people that contains those two people exactly. This does mean + // that bots, assistants, etc will ruin a room's DM-ness, though this is a problem for + // canonical DMs to solve. + if (r && r.getMyMembership() === "join") { + if (isLocalRoom(r)) return false; - const functionalUsers = getFunctionalMembers(r); - const members = r.currentState.getMembers(); - const joinedMembers = members.filter( - m => !functionalUsers.includes(m.userId) && isJoinedOrNearlyJoined(m.membership), - ); - const otherMember = joinedMembers.find(m => m.userId === userId); - return otherMember && joinedMembers.length === 2; - } - return false; - }).sort((r1, r2) => { - return r2.getLastActiveTimestamp() - - r1.getLastActiveTimestamp(); - }); + const functionalUsers = getFunctionalMembers(r); + const members = r.currentState.getMembers(); + const joinedMembers = members.filter( + (m) => !functionalUsers.includes(m.userId) && isJoinedOrNearlyJoined(m.membership), + ); + const otherMember = joinedMembers.find((m) => m.userId === userId); + return otherMember && joinedMembers.length === 2; + } + return false; + }) + .sort((r1, r2) => { + return r2.getLastActiveTimestamp() - r1.getLastActiveTimestamp(); + }); if (suitableDMRooms.length) { return suitableDMRooms[0]; } diff --git a/src/utils/dm/findDMRoom.ts b/src/utils/dm/findDMRoom.ts index 8cc6fa0d6d8..d8cbb56d905 100644 --- a/src/utils/dm/findDMRoom.ts +++ b/src/utils/dm/findDMRoom.ts @@ -28,7 +28,7 @@ import { findDMForUser } from "./findDMForUser"; * @returns {Room | null} Resolved so the room if found, else null */ export function findDMRoom(client: MatrixClient, targets: Member[]): Room | null { - const targetIds = targets.map(t => t.userId); + const targetIds = targets.map((t) => t.userId); let existingRoom: Room; if (targetIds.length === 1) { existingRoom = findDMForUser(client, targetIds[0]); diff --git a/src/utils/dm/startDm.ts b/src/utils/dm/startDm.ts index c608a8b18dd..ed5071bcf51 100644 --- a/src/utils/dm/startDm.ts +++ b/src/utils/dm/startDm.ts @@ -32,7 +32,7 @@ import createRoom from "../../createRoom"; * @returns {Promise { - const targetIds = targets.map(t => t.userId); + const targetIds = targets.map((t) => t.userId); // Check if there is already a DM with these people and reuse it if possible. let existingRoom: Room; @@ -69,14 +69,14 @@ export async function startDm(client: MatrixClient, targets: Member[], showSpinn createRoomOptions.createOpts = targetIds.reduce( (roomOptions, address) => { const type = getAddressType(address); - if (type === 'email') { + if (type === "email") { const invite: IInvite3PID = { id_server: client.getIdentityServerUrl(true), - medium: 'email', + medium: "email", address, }; roomOptions.invite_3pid.push(invite); - } else if (type === 'mx-user-id') { + } else if (type === "mx-user-id") { roomOptions.invite.push(address); } return roomOptions; diff --git a/src/utils/error.ts b/src/utils/error.ts index 8dec29e7f06..e52c0e4aeba 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -15,10 +15,7 @@ limitations under the License. */ export class GenericError extends Error { - constructor( - public readonly message: string, - public readonly description?: string | undefined, - ) { + constructor(public readonly message: string, public readonly description?: string | undefined) { super(message); } } diff --git a/src/utils/exportUtils/Exporter.ts b/src/utils/exportUtils/Exporter.ts index ec20f395e32..e855310ab69 100644 --- a/src/utils/exportUtils/Exporter.ts +++ b/src/utils/exportUtils/Exporter.ts @@ -48,9 +48,10 @@ export default abstract class Exporter { protected exportOptions: IExportOptions, protected setProgressText: React.Dispatch>, ) { - if (exportOptions.maxSize < 1 * 1024 * 1024|| // Less than 1 MB + if ( + exportOptions.maxSize < 1 * 1024 * 1024 || // Less than 1 MB exportOptions.maxSize > 8000 * 1024 * 1024 || // More than 8 GB - exportOptions.numberOfMessages > 10**8 + exportOptions.numberOfMessages > 10 ** 8 ) { throw new Error("Invalid export options"); } @@ -64,7 +65,7 @@ export default abstract class Exporter { protected onBeforeUnload(e: BeforeUnloadEvent): string { e.preventDefault(); - return e.returnValue = _t("Are you sure you want to exit during this export?"); + return (e.returnValue = _t("Are you sure you want to exit during this export?")); } protected updateProgress(progress: string, log = true, show = true): void { @@ -84,8 +85,7 @@ export default abstract class Exporter { // First try to use the real name of the room, then a translated copy of a generic name, // then finally hardcoded default to guarantee we'll have a name. const safeRoomName = sanitizeFilename(this.room.name ?? _t("Unnamed Room")).trim() || "Unnamed Room"; - const safeDate = formatFullDateNoDayISO(new Date()) - .replace(/:/g, '-'); // ISO format automatically removes a lot of stuff for us + const safeDate = formatFullDateNoDayISO(new Date()).replace(/:/g, "-"); // ISO format automatically removes a lot of stuff for us const safeBrand = sanitizeFilename(brand); return `${safeBrand} - ${safeRoomName} - Chat Export - ${safeDate}`; } @@ -93,7 +93,7 @@ export default abstract class Exporter { protected async downloadZIP(): Promise { const filename = this.destinationFileName; const filenameWithoutExt = filename.substring(0, filename.length - 4); // take off the .zip - const { default: JSZip } = await import('jszip'); + const { default: JSZip } = await import("jszip"); const zip = new JSZip(); // Create a writable stream to the directory @@ -125,13 +125,9 @@ export default abstract class Exporter { protected setEventMetadata(event: MatrixEvent): MatrixEvent { const roomState = this.client.getRoom(this.room.roomId).currentState; - event.sender = roomState.getSentinelMember( - event.getSender(), - ); + event.sender = roomState.getSentinelMember(event.getSender()); if (event.getType() === "m.room.member") { - event.target = roomState.getSentinelMember( - event.getStateKey(), - ); + event.target = roomState.getSentinelMember(event.getStateKey()); } return event; } @@ -146,7 +142,7 @@ export default abstract class Exporter { limit = 40; break; default: - limit = 10**8; + limit = 10 ** 8; } return limit; } @@ -154,7 +150,7 @@ export default abstract class Exporter { protected async getRequiredEvents(): Promise { const eventMapper = this.client.getEventMapper(); - let prevToken: string|null = null; + let prevToken: string | null = null; let limit = this.getLimit(); const events: MatrixEvent[] = []; @@ -188,26 +184,30 @@ export default abstract class Exporter { } if (this.exportType === ExportType.LastNMessages) { - this.updateProgress(_t("Fetched %(count)s events out of %(total)s", { - count: events.length, - total: this.exportOptions.numberOfMessages, - })); + this.updateProgress( + _t("Fetched %(count)s events out of %(total)s", { + count: events.length, + total: this.exportOptions.numberOfMessages, + }), + ); } else { - this.updateProgress(_t("Fetched %(count)s events so far", { - count: events.length, - })); + this.updateProgress( + _t("Fetched %(count)s events so far", { + count: events.length, + }), + ); } prevToken = res.end; } // Reverse the events so that we preserve the order - for (let i = 0; i < Math.floor(events.length/2); i++) { + for (let i = 0; i < Math.floor(events.length / 2); i++) { [events[i], events[events.length - i - 1]] = [events[events.length - i - 1], events[i]]; } const decryptionPromises = events - .filter(event => event.isEncrypted()) - .map(event => { + .filter((event) => event.isEncrypted()) + .map((event) => { return this.client.decryptEventIfNeeded(event, { isRetry: true, emit: false, @@ -242,11 +242,11 @@ export default abstract class Exporter { } public splitFileName(file: string): string[] { - const lastDot = file.lastIndexOf('.'); + const lastDot = file.lastIndexOf("."); if (lastDot === -1) return [file, ""]; const fileName = file.slice(0, lastDot); const ext = file.slice(lastDot + 1); - return [fileName, '.' + ext]; + return [fileName, "." + ext]; } public getFilePath(event: MatrixEvent): string { @@ -271,7 +271,7 @@ export default abstract class Exporter { if (event.getType() === "m.sticker") fileExt = ".png"; if (isVoiceMessage(event)) fileExt = ".ogg"; - return fileDirectory + "/" + fileName + '-' + fileDate + fileExt; + return fileDirectory + "/" + fileName + "-" + fileDate + fileExt; } protected isReply(event: MatrixEvent): boolean { diff --git a/src/utils/exportUtils/HtmlExport.tsx b/src/utils/exportUtils/HtmlExport.tsx index dcc7994ace6..2c7d6aebd67 100644 --- a/src/utils/exportUtils/HtmlExport.tsx +++ b/src/utils/exportUtils/HtmlExport.tsx @@ -62,7 +62,7 @@ export default class HTMLExporter extends Exporter { this.mediaOmitText = !this.exportOptions.attachmentsIncluded ? _t("Media omitted") : _t("Media omitted - file size limit exceeded"); - this.threadsEnabled = SettingsStore.getValue("feature_thread"); + this.threadsEnabled = SettingsStore.getValue("feature_threadstable"); } protected async getRoomAvatar() { @@ -106,31 +106,27 @@ export default class HTMLExporter extends Exporter { const exportedText = renderToStaticMarkup(

- { _t( + {_t( "This is the start of export of . Exported by at %(exportDate)s.", { exportDate, }, { - roomName: () => { this.room.name }, + roomName: () => {this.room.name}, exporterDetails: () => ( - - { exporterName ? ( + + {exporterName ? ( <> - { exporterName } - { " (" + exporter + ")" } + {exporterName} + {" (" + exporter + ")"} ) : ( - { exporter } - ) } + {exporter} + )} ), }, - ) } + )}

, ); @@ -224,12 +220,7 @@ export default class HTMLExporter extends Exporter { protected getAvatarURL(event: MatrixEvent): string { const member = event.sender; return ( - member.getMxcAvatarUrl() && - mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp( - 30, - 30, - "crop", - ) + member.getMxcAvatarUrl() && mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(30, 30, "crop") ); } @@ -241,7 +232,7 @@ export default class HTMLExporter extends Exporter { this.avatars.set(member.userId, true); const image = await fetch(avatarUrl); const blob = await image.blob(); - this.addFile(`users/${member.userId.replace(/:/g, '-')}.png`, blob); + this.addFile(`users/${member.userId.replace(/:/g, "-")}.png`, blob); } catch (err) { logger.log("Failed to fetch user's avatar" + err); } @@ -264,32 +255,34 @@ export default class HTMLExporter extends Exporter { } public getEventTile(mxEv: MatrixEvent, continuation: boolean) { - return
- - false} - isTwelveHour={false} - last={false} - lastInSection={false} - permalinkCreator={this.permalinkCreator} - lastSuccessful={false} - isSelectedEvent={false} - getRelationsForEvent={null} - showReactions={false} - layout={Layout.Group} - showReadReceipts={false} - /> - -
; + return ( +
+ + false} + isTwelveHour={false} + last={false} + lastInSection={false} + permalinkCreator={this.permalinkCreator} + lastSuccessful={false} + isSelectedEvent={false} + getRelationsForEvent={null} + showReactions={false} + layout={Layout.Group} + showReadReceipts={false} + /> + +
+ ); } protected async getEventTileMarkup(mxEv: MatrixEvent, continuation: boolean, filePath?: string) { @@ -305,11 +298,8 @@ export default class HTMLExporter extends Exporter { ) { // to linkify textual events, we'll need lifecycle methods which won't be invoked in renderToString // So, we'll have to render the component into a temporary root element - const tempRoot = document.createElement('div'); - ReactDOM.render( - EventTile, - tempRoot, - ); + const tempRoot = document.createElement("div"); + ReactDOM.render(EventTile, tempRoot); eventTileMarkup = tempRoot.innerHTML; } else { eventTileMarkup = renderToStaticMarkup(EventTile); @@ -319,17 +309,17 @@ export default class HTMLExporter extends Exporter { const mxc = mxEv.getContent().url ?? mxEv.getContent().file?.url; eventTileMarkup = eventTileMarkup.split(mxc).join(filePath); } - eventTileMarkup = eventTileMarkup.replace(/.*?<\/span>/, ''); + eventTileMarkup = eventTileMarkup.replace(/.*?<\/span>/, ""); if (hasAvatar) { eventTileMarkup = eventTileMarkup.replace( - encodeURI(this.getAvatarURL(mxEv)).replace(/&/g, '&'), + encodeURI(this.getAvatarURL(mxEv)).replace(/&/g, "&"), `users/${mxEv.sender.userId.replace(/:/g, "-")}.png`, ); } return eventTileMarkup; } - protected createModifiedEvent(text: string, mxEv: MatrixEvent, italic=true) { + protected createModifiedEvent(text: string, mxEv: MatrixEvent, italic = true) { const modifiedContent = { msgtype: "m.text", body: `${text}`, @@ -337,8 +327,8 @@ export default class HTMLExporter extends Exporter { formatted_body: `${text}`, }; if (italic) { - modifiedContent.formatted_body = '' + modifiedContent.formatted_body + ''; - modifiedContent.body = '*' + modifiedContent.body + '*'; + modifiedContent.formatted_body = "" + modifiedContent.formatted_body + ""; + modifiedContent.body = "*" + modifiedContent.body + "*"; } const modifiedEvent = new MatrixEvent(); modifiedEvent.event = mxEv.event; @@ -402,15 +392,20 @@ export default class HTMLExporter extends Exporter { let prevEvent = null; for (let i = start; i < Math.min(start + 1000, events.length); i++) { const event = events[i]; - this.updateProgress(_t("Processing event %(number)s out of %(total)s", { - number: i + 1, - total: events.length, - }), false, true); + this.updateProgress( + _t("Processing event %(number)s out of %(total)s", { + number: i + 1, + total: events.length, + }), + false, + true, + ); if (this.cancelled) return this.cleanUp(); if (!haveRendererForEvent(event, false)) continue; content += this.needsDateSeparator(event, prevEvent) ? this.getDateSeparator(event) : ""; - const shouldBeJoined = !this.needsDateSeparator(event, prevEvent) && + const shouldBeJoined = + !this.needsDateSeparator(event, prevEvent) && shouldFormContinuation(prevEvent, event, false, this.threadsEnabled); const body = await this.createMessageBody(event, shouldBeJoined); this.totalSize += Buffer.byteLength(body); @@ -427,10 +422,14 @@ export default class HTMLExporter extends Exporter { const res = await this.getRequiredEvents(); const fetchEnd = performance.now(); - this.updateProgress(_t("Fetched %(count)s events in %(seconds)ss", { - count: res.length, - seconds: (fetchEnd - fetchStart) / 1000, - }), true, false); + this.updateProgress( + _t("Fetched %(count)s events in %(seconds)ss", { + count: res.length, + seconds: (fetchEnd - fetchStart) / 1000, + }), + true, + false, + ); this.updateProgress(_t("Creating HTML...")); @@ -438,8 +437,8 @@ export default class HTMLExporter extends Exporter { for (let page = 0; page < res.length / 1000; page++) { const html = await this.createHTML(res, page * 1000); const document = new DOMParser().parseFromString(html, "text/html"); - document.querySelectorAll("*").forEach(element => { - element.classList.forEach(c => usedClasses.add(c)); + document.querySelectorAll("*").forEach((element) => { + element.classList.forEach((c) => usedClasses.add(c)); }); this.addFile(`messages${page ? page + 1 : ""}.html`, new Blob([html])); } @@ -456,10 +455,12 @@ export default class HTMLExporter extends Exporter { logger.info("Export cancelled successfully"); } else { this.updateProgress(_t("Export successful!")); - this.updateProgress(_t("Exported %(count)s events in %(seconds)s seconds", { - count: res.length, - seconds: (exportEnd - fetchStart) / 1000, - })); + this.updateProgress( + _t("Exported %(count)s events in %(seconds)s seconds", { + count: res.length, + seconds: (exportEnd - fetchStart) / 1000, + }), + ); } this.cleanUp(); diff --git a/src/utils/exportUtils/JSONExport.ts b/src/utils/exportUtils/JSONExport.ts index a0dc5e036e6..a050e32ef1f 100644 --- a/src/utils/exportUtils/JSONExport.ts +++ b/src/utils/exportUtils/JSONExport.ts @@ -84,10 +84,14 @@ export default class JSONExporter extends Exporter { protected async createOutput(events: MatrixEvent[]) { for (let i = 0; i < events.length; i++) { const event = events[i]; - this.updateProgress(_t("Processing event %(number)s out of %(total)s", { - number: i + 1, - total: events.length, - }), false, true); + this.updateProgress( + _t("Processing event %(number)s out of %(total)s", { + number: i + 1, + total: events.length, + }), + false, + true, + ); if (this.cancelled) return this.cleanUp(); if (!haveRendererForEvent(event, false)) continue; this.messages.push(await this.getJSONString(event)); @@ -103,7 +107,7 @@ export default class JSONExporter extends Exporter { const res = await this.getRequiredEvents(); const fetchEnd = performance.now(); - logger.log(`Fetched ${res.length} events in ${(fetchEnd - fetchStart)/1000}s`); + logger.log(`Fetched ${res.length} events in ${(fetchEnd - fetchStart) / 1000}s`); logger.info("Creating output..."); const text = await this.createOutput(res); @@ -122,10 +126,9 @@ export default class JSONExporter extends Exporter { logger.info("Export cancelled successfully"); } else { logger.info("Export successful!"); - logger.log(`Exported ${res.length} events in ${(exportEnd - fetchStart)/1000} seconds`); + logger.log(`Exported ${res.length} events in ${(exportEnd - fetchStart) / 1000} seconds`); } this.cleanUp(); } } - diff --git a/src/utils/exportUtils/PlainTextExport.ts b/src/utils/exportUtils/PlainTextExport.ts index 3150b15c642..d097d842a5b 100644 --- a/src/utils/exportUtils/PlainTextExport.ts +++ b/src/utils/exportUtils/PlainTextExport.ts @@ -61,7 +61,7 @@ export default class PlainTextExporter extends Exporter { rplSource = match[2].substring(1); // Get the first non-blank line from the source. - const lines = rplSource.split('\n').filter((line) => !/^\s*$/.test(line)); + const lines = rplSource.split("\n").filter((line) => !/^\s*$/.test(line)); if (lines.length > 0) { // Cut to a maximum length. rplSource = lines[0].substring(0, REPLY_SOURCE_MAX_LENGTH); @@ -111,10 +111,14 @@ export default class PlainTextExporter extends Exporter { let content = ""; for (let i = 0; i < events.length; i++) { const event = events[i]; - this.updateProgress(_t("Processing event %(number)s out of %(total)s", { - number: i + 1, - total: events.length, - }), false, true); + this.updateProgress( + _t("Processing event %(number)s out of %(total)s", { + number: i + 1, + total: events.length, + }), + false, + true, + ); if (this.cancelled) return this.cleanUp(); if (!haveRendererForEvent(event, false)) continue; const textForEvent = await this.plainTextForEvent(event); @@ -131,7 +135,7 @@ export default class PlainTextExporter extends Exporter { const res = await this.getRequiredEvents(); const fetchEnd = performance.now(); - logger.log(`Fetched ${res.length} events in ${(fetchEnd - fetchStart)/1000}s`); + logger.log(`Fetched ${res.length} events in ${(fetchEnd - fetchStart) / 1000}s`); this.updateProgress(_t("Creating output...")); const text = await this.createOutput(res); @@ -150,10 +154,9 @@ export default class PlainTextExporter extends Exporter { logger.info("Export cancelled successfully"); } else { logger.info("Export successful!"); - logger.log(`Exported ${res.length} events in ${(exportEnd - fetchStart)/1000} seconds`); + logger.log(`Exported ${res.length} events in ${(exportEnd - fetchStart) / 1000} seconds`); } this.cleanUp(); } } - diff --git a/src/utils/exportUtils/exportCSS.ts b/src/utils/exportUtils/exportCSS.ts index b85c2a9431a..f92e339b023 100644 --- a/src/utils/exportUtils/exportCSS.ts +++ b/src/utils/exportUtils/exportCSS.ts @@ -52,7 +52,7 @@ async function getRulesFromCssFile(path: string): Promise { // doesn't cull rules which won't apply due to the full selector not matching but gets rid of a LOT of cruft anyway. const getExportCSS = async (usedClasses: Set): Promise => { // only include bundle.css and the data-mx-theme=light styling - const stylesheets = Array.from(document.styleSheets).filter(s => { + const stylesheets = Array.from(document.styleSheets).filter((s) => { return s.href?.endsWith("bundle.css") || isLightTheme(s); }); @@ -70,12 +70,14 @@ const getExportCSS = async (usedClasses: Set): Promise => { const selectorText = (rule as CSSStyleRule).selectorText; // only skip the rule if all branches (,) of the selector are redundant - if (selectorText?.split(",").every(selector => { - const classes = selector.match(cssSelectorTextClassesRegex); - if (classes && !classes.every(c => usedClasses.has(c.substring(1)))) { - return true; // signal as a redundant selector - } - })) { + if ( + selectorText?.split(",").every((selector) => { + const classes = selector.match(cssSelectorTextClassesRegex); + if (classes && !classes.every((c) => usedClasses.has(c.substring(1)))) { + return true; // signal as a redundant selector + } + }) + ) { continue; // skip this rule as it is redundant } diff --git a/src/utils/exportUtils/exportCustomCSS.css b/src/utils/exportUtils/exportCustomCSS.css index a62f8906499..47dd4e5ec53 100644 --- a/src/utils/exportUtils/exportCustomCSS.css +++ b/src/utils/exportUtils/exportCustomCSS.css @@ -32,9 +32,8 @@ limitations under the License. bottom: 30px; font-size: 17px; padding: 6px 16px; - font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, - segoe ui, helvetica neue, helvetica, Ubuntu, roboto, noto, arial, - sans-serif; + font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Ubuntu, + roboto, noto, arial, sans-serif; font-weight: 400; line-height: 1.43; border-radius: 4px; @@ -126,7 +125,6 @@ a.mx_reply_anchor:hover { .mx_RedactedBody, .mx_HiddenBody { - padding-left: unset; } diff --git a/src/utils/exportUtils/exportJS.js b/src/utils/exportUtils/exportJS.js index e082f88d98d..f4d5df322b6 100644 --- a/src/utils/exportUtils/exportJS.js +++ b/src/utils/exportUtils/exportJS.js @@ -27,16 +27,15 @@ 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); } window.onload = () => { - document.querySelectorAll('.mx_reply_anchor').forEach(element => { - element.addEventListener('click', event => { - showToastIfNeeded(event.target.getAttribute("scroll-to")); + document.querySelectorAll(".mx_reply_anchor").forEach((element) => { + element.addEventListener("click", (event) => { + showToastIfNeeded(event.target.dataset.scrollTo); }); }); }; - diff --git a/src/utils/humanize.ts b/src/utils/humanize.ts index 47e2d83e8a0..9ae5b175891 100644 --- a/src/utils/humanize.ts +++ b/src/utils/humanize.ts @@ -36,7 +36,8 @@ export function humanizeTime(timeMillis: number): string { const hours = Math.ceil(minutes / 60); const days = Math.ceil(hours / 24); - if (msAgo >= 0) { // Past + if (msAgo >= 0) { + // Past if (msAgo <= MILLISECONDS_RECENT) return _t("a few seconds ago"); if (msAgo <= MILLISECONDS_1_MIN) return _t("about a minute ago"); if (minutes <= MINUTES_UNDER_1_HOUR) return _t("%(num)s minutes ago", { num: minutes }); @@ -44,7 +45,8 @@ export function humanizeTime(timeMillis: number): string { if (hours <= HOURS_UNDER_1_DAY) return _t("%(num)s hours ago", { num: hours }); if (hours <= HOURS_1_DAY) return _t("about a day ago"); return _t("%(num)s days ago", { num: days }); - } else { // Future + } else { + // Future msAgo = Math.abs(msAgo); if (msAgo <= MILLISECONDS_RECENT) return _t("a few seconds from now"); if (msAgo <= MILLISECONDS_1_MIN) return _t("about a minute from now"); diff --git a/src/utils/image-media.ts b/src/utils/image-media.ts index c3320627d01..02de0799282 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,10 +92,10 @@ 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)); + thumbnailPromise = new Promise((resolve) => (canvas as HTMLCanvasElement).toBlob(resolve, mimeType)); } const imageData = context.getImageData(0, 0, targetWidth, targetHeight); diff --git a/src/utils/iterables.ts b/src/utils/iterables.ts index 5fb8967a346..9ee29448d89 100644 --- a/src/utils/iterables.ts +++ b/src/utils/iterables.ts @@ -20,6 +20,6 @@ export function iterableIntersection(a: Iterable, b: Iterable): Iterabl return arrayIntersection(Array.from(a), Array.from(b)); } -export function iterableDiff(a: Iterable, b: Iterable): { added: Iterable, removed: Iterable } { +export function iterableDiff(a: Iterable, b: Iterable): { added: Iterable; removed: Iterable } { return arrayDiff(Array.from(a), Array.from(b)); } diff --git a/src/utils/leave-behaviour.ts b/src/utils/leave-behaviour.ts index f330e9b8734..8b1a900f8c9 100644 --- a/src/utils/leave-behaviour.ts +++ b/src/utils/leave-behaviour.ts @@ -39,7 +39,7 @@ import { SdkContextClass } from "../contexts/SDKContext"; export async function leaveRoomBehaviour(roomId: string, retry = true, spinner = true) { let spinnerModal: IHandle; if (spinner) { - spinnerModal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner'); + spinnerModal = Modal.createDialog(Spinner, null, "mx_Dialog_spinner"); } const cli = MatrixClientPeg.get(); @@ -56,25 +56,33 @@ export async function leaveRoomBehaviour(roomId: string, retry = true, spinner = const room = cli.getRoom(roomId); // await any queued messages being sent so that they do not fail - await Promise.all(room.getPendingEvents().filter(ev => { - return [EventStatus.QUEUED, EventStatus.ENCRYPTING, EventStatus.SENDING].includes(ev.status); - }).map(ev => new Promise((resolve, reject) => { - const handler = () => { - if (ev.status === EventStatus.NOT_SENT) { - spinnerModal?.close(); - reject(ev.error); - } - - if (!ev.status || ev.status === EventStatus.SENT) { - ev.off(MatrixEventEvent.Status, handler); - resolve(); - } - }; - - ev.on(MatrixEventEvent.Status, handler); - }))); - - let results: { [roomId: string]: Error & { errcode?: string, message: string, data?: Record } } = {}; + await Promise.all( + room + .getPendingEvents() + .filter((ev) => { + return [EventStatus.QUEUED, EventStatus.ENCRYPTING, EventStatus.SENDING].includes(ev.status); + }) + .map( + (ev) => + new Promise((resolve, reject) => { + const handler = () => { + if (ev.status === EventStatus.NOT_SENT) { + spinnerModal?.close(); + reject(ev.error); + } + + if (!ev.status || ev.status === EventStatus.SENT) { + ev.off(MatrixEventEvent.Status, handler); + resolve(); + } + }; + + ev.on(MatrixEventEvent.Status, handler); + }), + ), + ); + + let results: { [roomId: string]: Error & { errcode?: string; message: string; data?: Record } } = {}; if (!leavingAllVersions) { try { await cli.leave(roomId); @@ -91,7 +99,7 @@ export async function leaveRoomBehaviour(roomId: string, retry = true, spinner = } if (retry) { - const limitExceededError = Object.values(results).find(e => e?.errcode === "M_LIMIT_EXCEEDED"); + const limitExceededError = Object.values(results).find((e) => e?.errcode === "M_LIMIT_EXCEEDED"); if (limitExceededError) { await sleep(limitExceededError.data.retry_after_ms ?? 100); return leaveRoomBehaviour(roomId, false, false); @@ -100,26 +108,26 @@ export async function leaveRoomBehaviour(roomId: string, retry = true, spinner = spinnerModal?.close(); - const errors = Object.entries(results).filter(r => !!r[1]); + const errors = Object.entries(results).filter((r) => !!r[1]); if (errors.length > 0) { const messages = []; for (const roomErr of errors) { const err = roomErr[1]; // [0] is the roomId let message = _t("Unexpected server error trying to leave the room"); if (err.errcode && err.message) { - if (err.errcode === 'M_CANNOT_LEAVE_SERVER_NOTICE_ROOM') { + if (err.errcode === "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM") { Modal.createDialog(ErrorDialog, { title: _t("Can't leave Server Notices room"), description: _t( "This room is used for important messages from the Homeserver, " + - "so you cannot leave it.", + "so you cannot leave it.", ), }); return; } message = results[roomId].message; } - messages.push(message, React.createElement('BR')); // createElement to avoid using a tsx file in utils + messages.push(message, React.createElement("BR")); // createElement to avoid using a tsx file in utils } Modal.createDialog(ErrorDialog, { title: _t("Error leaving room"), @@ -158,16 +166,20 @@ export async function leaveRoomBehaviour(roomId: string, retry = true, spinner = } export const leaveSpace = (space: Room) => { - Modal.createDialog(LeaveSpaceDialog, { - space, - onFinished: async (leave: boolean, rooms: Room[]) => { - if (!leave) return; - await bulkSpaceBehaviour(space, rooms, room => leaveRoomBehaviour(room.roomId)); - - dis.dispatch({ - action: Action.AfterLeaveRoom, - room_id: space.roomId, - }); + Modal.createDialog( + LeaveSpaceDialog, + { + space, + onFinished: async (leave: boolean, rooms: Room[]) => { + if (!leave) return; + await bulkSpaceBehaviour(space, rooms, (room) => leaveRoomBehaviour(room.roomId)); + + dis.dispatch({ + action: Action.AfterLeaveRoom, + room_id: space.roomId, + }); + }, }, - }, "mx_LeaveSpaceDialog_wrapper"); + "mx_LeaveSpaceDialog_wrapper", + ); }; diff --git a/src/utils/local-room.ts b/src/utils/local-room.ts index 8b1a2e63791..b85bb7de9de 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/localRoom/isLocalRoom.ts b/src/utils/localRoom/isLocalRoom.ts index a31774ea5e4..f2d7e3acfd2 100644 --- a/src/utils/localRoom/isLocalRoom.ts +++ b/src/utils/localRoom/isLocalRoom.ts @@ -18,7 +18,7 @@ import { Room } from "matrix-js-sdk/src/matrix"; import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../models/LocalRoom"; -export function isLocalRoom(roomOrID: Room|string): boolean { +export function isLocalRoom(roomOrID: Room | string): boolean { if (typeof roomOrID === "string") { return roomOrID.startsWith(LOCAL_ROOM_ID_PREFIX); } diff --git a/src/utils/localRoom/isRoomReady.ts b/src/utils/localRoom/isRoomReady.ts index c26839236d7..32d7106b870 100644 --- a/src/utils/localRoom/isRoomReady.ts +++ b/src/utils/localRoom/isRoomReady.ts @@ -21,10 +21,7 @@ import { LocalRoom } from "../../models/LocalRoom"; /** * Tests whether a room created based on a local room is ready. */ -export function isRoomReady( - client: MatrixClient, - localRoom: LocalRoom, -): boolean { +export function isRoomReady(client: MatrixClient, localRoom: LocalRoom): boolean { // not ready if no actual room id exists if (!localRoom.actualRoomId) return false; diff --git a/src/utils/location/LocationShareErrors.ts b/src/utils/location/LocationShareErrors.ts index 81d4e50d314..a7f34b42217 100644 --- a/src/utils/location/LocationShareErrors.ts +++ b/src/utils/location/LocationShareErrors.ts @@ -17,18 +17,20 @@ limitations under the License. import { _t } from "../../languageHandler"; export enum LocationShareError { - MapStyleUrlNotConfigured = 'MapStyleUrlNotConfigured', - MapStyleUrlNotReachable = 'MapStyleUrlNotReachable', - Default = 'Default' + MapStyleUrlNotConfigured = "MapStyleUrlNotConfigured", + MapStyleUrlNotReachable = "MapStyleUrlNotReachable", + Default = "Default", } export const getLocationShareErrorMessage = (errorType?: LocationShareError): string => { switch (errorType) { case LocationShareError.MapStyleUrlNotConfigured: - return _t('This homeserver is not configured to display maps.'); + return _t("This homeserver is not configured to display maps."); case LocationShareError.MapStyleUrlNotReachable: default: - return _t(`This homeserver is not configured correctly to display maps, ` - + `or the configured map server may be unreachable.`); + return _t( + `This homeserver is not configured correctly to display maps, ` + + `or the configured map server may be unreachable.`, + ); } }; diff --git a/src/utils/location/findMapStyleUrl.ts b/src/utils/location/findMapStyleUrl.ts index 9eb9a6d3079..0653d65cf24 100644 --- a/src/utils/location/findMapStyleUrl.ts +++ b/src/utils/location/findMapStyleUrl.ts @@ -26,14 +26,12 @@ import { LocationShareError } from "./LocationShareErrors"; * that, defaults to the same tile server listed by matrix.org. */ export function findMapStyleUrl(): string { - const mapStyleUrl = ( - getTileServerWellKnown()?.map_style_url ?? - SdkConfig.get().map_style_url - ); + const mapStyleUrl = getTileServerWellKnown()?.map_style_url ?? SdkConfig.get().map_style_url; if (!mapStyleUrl) { - logger.error("'map_style_url' missing from homeserver .well-known area, and " + - "missing from from config.json."); + logger.error( + "'map_style_url' missing from homeserver .well-known area, and " + "missing from from config.json.", + ); throw new Error(LocationShareError.MapStyleUrlNotConfigured); } diff --git a/src/utils/location/index.ts b/src/utils/location/index.ts index a6aeaa65d67..f94c6a12dd3 100644 --- a/src/utils/location/index.ts +++ b/src/utils/location/index.ts @@ -14,9 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -export * from './findMapStyleUrl'; -export * from './isSelfLocation'; -export * from './locationEventGeoUri'; -export * from './LocationShareErrors'; -export * from './map'; -export * from './parseGeoUri'; +export * from "./findMapStyleUrl"; +export * from "./isSelfLocation"; +export * from "./locationEventGeoUri"; +export * from "./LocationShareErrors"; +export * from "./map"; +export * from "./parseGeoUri"; diff --git a/src/utils/location/locationEventGeoUri.ts b/src/utils/location/locationEventGeoUri.ts index eb81ac87c0b..2009edc3253 100644 --- a/src/utils/location/locationEventGeoUri.ts +++ b/src/utils/location/locationEventGeoUri.ts @@ -27,5 +27,5 @@ export const locationEventGeoUri = (mxEvent: MatrixEvent): string => { // https://github.com/matrix-org/matrix-doc/issues/3516 const content = mxEvent.getContent(); const loc = M_LOCATION.findIn(content) as { uri?: string }; - return loc ? loc.uri : content['geo_uri']; + return loc ? loc.uri : content["geo_uri"]; }; diff --git a/src/utils/location/map.ts b/src/utils/location/map.ts index 7dc8522271c..861515eb771 100644 --- a/src/utils/location/map.ts +++ b/src/utils/location/map.ts @@ -24,11 +24,7 @@ import { parseGeoUri } from "./parseGeoUri"; import { findMapStyleUrl } from "./findMapStyleUrl"; import { LocationShareError } from "./LocationShareErrors"; -export const createMap = ( - interactive: boolean, - bodyId: string, - onError: (error: Error) => void, -): maplibregl.Map => { +export const createMap = (interactive: boolean, bodyId: string, onError: (error: Error) => void): maplibregl.Map => { try { const styleUrl = findMapStyleUrl(); @@ -39,24 +35,23 @@ export const createMap = ( interactive, attributionControl: false, locale: { - 'AttributionControl.ToggleAttribution': _t('Toggle attribution'), - 'AttributionControl.MapFeedback': _t('Map feedback'), - 'FullscreenControl.Enter': _t('Enter fullscreen'), - 'FullscreenControl.Exit': _t('Exit fullscreen'), - 'GeolocateControl.FindMyLocation': _t('Find my location'), - 'GeolocateControl.LocationNotAvailable': _t('Location not available'), - 'LogoControl.Title': _t('Mapbox logo'), - 'NavigationControl.ResetBearing': _t('Reset bearing to north'), - 'NavigationControl.ZoomIn': _t('Zoom in'), - 'NavigationControl.ZoomOut': _t('Zoom out'), + "AttributionControl.ToggleAttribution": _t("Toggle attribution"), + "AttributionControl.MapFeedback": _t("Map feedback"), + "FullscreenControl.Enter": _t("Enter fullscreen"), + "FullscreenControl.Exit": _t("Exit fullscreen"), + "GeolocateControl.FindMyLocation": _t("Find my location"), + "GeolocateControl.LocationNotAvailable": _t("Location not available"), + "LogoControl.Title": _t("Mapbox logo"), + "NavigationControl.ResetBearing": _t("Reset bearing to north"), + "NavigationControl.ZoomIn": _t("Zoom in"), + "NavigationControl.ZoomOut": _t("Zoom out"), }, }); - map.addControl(new maplibregl.AttributionControl(), 'top-right'); + map.addControl(new maplibregl.AttributionControl(), "top-right"); - map.on('error', (e) => { + map.on("error", (e) => { logger.error( - "Failed to load map: check map_style_url in config.json has a " - + "valid URL and API key", + "Failed to load map: check map_style_url in config.json has a " + "valid URL and API key", e.error, ); onError(new Error(LocationShareError.MapStyleUrlNotReachable)); @@ -72,7 +67,7 @@ export const createMap = ( export const createMarker = (coords: GeolocationCoordinates, element: HTMLElement): maplibregl.Marker => { const marker = new maplibregl.Marker({ element, - anchor: 'bottom', + anchor: "bottom", offset: [0, -1], }).setLngLat({ lon: coords.longitude, lat: coords.latitude }); return marker; diff --git a/src/utils/location/parseGeoUri.ts b/src/utils/location/parseGeoUri.ts index 4c7291cd3e8..080ff5359bf 100644 --- a/src/utils/location/parseGeoUri.ts +++ b/src/utils/location/parseGeoUri.ts @@ -26,8 +26,8 @@ export const parseGeoUri = (uri: string): GeolocationCoordinates => { const m = uri.match(/^\s*geo:(.*?)\s*$/); if (!m) return; - const parts = m[1].split(';'); - const coords = parts[0].split(','); + const parts = m[1].split(";"); + const coords = parts[0].split(","); let uncertainty: number; for (const param of parts.slice(1)) { const m = param.match(/u=(.*)/); diff --git a/src/utils/location/useMap.ts b/src/utils/location/useMap.ts index 55770cc5e28..c4637a9a367 100644 --- a/src/utils/location/useMap.ts +++ b/src/utils/location/useMap.ts @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { useEffect, useState } from 'react'; -import { Map as MapLibreMap } from 'maplibre-gl'; +import { useEffect, useState } from "react"; +import { Map as MapLibreMap } from "maplibre-gl"; import { createMap } from "./map"; @@ -31,11 +31,7 @@ interface UseMapProps { * Make sure `onError` has a stable reference * As map is recreated on changes to it */ -export const useMap = ({ - interactive, - bodyId, - onError, -}: UseMapProps): MapLibreMap | undefined => { +export const useMap = ({ interactive, bodyId, onError }: UseMapProps): MapLibreMap | undefined => { const [map, setMap] = useState(); useEffect( @@ -59,4 +55,3 @@ export const useMap = ({ return map; }; - diff --git a/src/utils/maps.ts b/src/utils/maps.ts index 2afbc16bc55..2a82b0a2389 100644 --- a/src/utils/maps.ts +++ b/src/utils/maps.ts @@ -23,12 +23,12 @@ import { arrayDiff, arrayIntersection } from "./arrays"; * @param b The second Map. Must be defined. * @returns The difference between the keys of each Map. */ -export function mapDiff(a: Map, b: Map): { changed: K[], added: K[], removed: K[] } { +export function mapDiff(a: Map, b: Map): { changed: K[]; added: K[]; removed: K[] } { const aKeys = [...a.keys()]; const bKeys = [...b.keys()]; const keyDiff = arrayDiff(aKeys, bKeys); const possibleChanges = arrayIntersection(aKeys, bKeys); - const changes = possibleChanges.filter(k => a.get(k) !== b.get(k)); + const changes = possibleChanges.filter((k) => a.get(k) !== b.get(k)); return { changed: changes, added: keyDiff.added, removed: keyDiff.removed }; } diff --git a/src/utils/media/requestMediaPermissions.tsx b/src/utils/media/requestMediaPermissions.tsx index 7740fb8da4b..c7720fffeb4 100644 --- a/src/utils/media/requestMediaPermissions.tsx +++ b/src/utils/media/requestMediaPermissions.tsx @@ -47,11 +47,8 @@ export const requestMediaPermissions = async (video = true): Promise { - handler = function(_, __, member: RoomMember) { // eslint-disable-line @typescript-eslint/naming-convention + // eslint-disable-next-line @typescript-eslint/naming-convention + handler = function (_, __, member: RoomMember) { if (member.userId !== userId) return; if (member.roomId !== roomId) return; resolve(true); @@ -97,7 +98,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/notifications.ts b/src/utils/notifications.ts index 32296d62e6e..8929240e6fb 100644 --- a/src/utils/notifications.ts +++ b/src/utils/notifications.ts @@ -17,6 +17,8 @@ limitations under the License. import { MatrixClient } from "matrix-js-sdk/src/client"; import { LOCAL_NOTIFICATION_SETTINGS_PREFIX } from "matrix-js-sdk/src/@types/event"; import { LocalNotificationSettings } from "matrix-js-sdk/src/@types/local_notifications"; +import { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts"; +import { Room } from "matrix-js-sdk/src/models/room"; import SettingsStore from "../settings/SettingsStore"; @@ -43,7 +45,7 @@ export async function createLocalNotificationSettingsIfNeeded(cli: MatrixClient) if (!event) { // If any of the above is true, we fall in the "backwards compat" case, // and `is_silenced` will be set to `false` - const isSilenced = !deviceNotificationSettingsKeys.some(key => SettingsStore.getValue(key)); + const isSilenced = !deviceNotificationSettingsKeys.some((key) => SettingsStore.getValue(key)); await cli.setAccountData(eventType, { is_silenced: isSilenced, @@ -56,3 +58,32 @@ export function localNotificationsAreSilenced(cli: MatrixClient): boolean { const event = cli.getAccountData(eventType); return event?.getContent()?.is_silenced ?? false; } + +export function clearAllNotifications(client: MatrixClient): Promise> { + const receiptPromises = client.getRooms().reduce((promises, room: Room) => { + if (room.getUnreadNotificationCount() > 0) { + const roomEvents = room.getLiveTimeline().getEvents(); + const lastThreadEvents = room.lastThread?.events; + + const lastRoomEvent = roomEvents?.[roomEvents?.length - 1]; + const lastThreadLastEvent = lastThreadEvents?.[lastThreadEvents?.length - 1]; + + const lastEvent = + (lastRoomEvent?.getTs() ?? 0) > (lastThreadLastEvent?.getTs() ?? 0) + ? lastRoomEvent + : lastThreadLastEvent; + + if (lastEvent) { + const receiptType = SettingsStore.getValue("sendReadReceipts", room.roomId) + ? ReceiptType.Read + : ReceiptType.ReadPrivate; + const promise = client.sendReadReceipt(lastEvent, receiptType, true); + promises.push(promise); + } + } + + return promises; + }, []); + + return Promise.all(receiptPromises); +} diff --git a/src/utils/numbers.ts b/src/utils/numbers.ts index 6ba19d0bef8..180e4f79503 100644 --- a/src/utils/numbers.ts +++ b/src/utils/numbers.ts @@ -34,7 +34,7 @@ export function sum(...i: number[]): number { } export function percentageWithin(pct: number, min: number, max: number): number { - return (pct * (max - min)) + min; + return pct * (max - min) + min; } export function percentageOf(val: number, min: number, max: number): number { diff --git a/src/utils/objects.ts b/src/utils/objects.ts index 87fb4dd8e63..f3bc8e93f1b 100644 --- a/src/utils/objects.ts +++ b/src/utils/objects.ts @@ -16,7 +16,7 @@ limitations under the License. import { arrayDiff, arrayUnion, arrayIntersection } from "./arrays"; -type ObjectExcluding = {[k in Exclude]: O[k]}; +type ObjectExcluding = { [k in Exclude]: O[k] }; /** * Gets a new object which represents the provided object, excluding some properties. @@ -45,13 +45,13 @@ export function objectExcluding>(a: O, pr * @param props The property names to keep. * @returns The new object with only the provided properties. */ -export function objectWithOnly>(a: O, props: P): {[k in P[number]]: O[k]} { +export function objectWithOnly>(a: O, props: P): { [k in P[number]]: O[k] } { const existingProps = Object.keys(a) as (keyof O)[]; const diff = arrayDiff(existingProps, props); if (diff.removed.length === 0) { return objectShallowClone(a); } else { - return objectExcluding(a, diff.removed) as {[k in P[number]]: O[k]}; + return objectExcluding(a, diff.removed) as { [k in P[number]]: O[k] }; } } @@ -94,10 +94,10 @@ export function objectHasDiff(a: O, b: O): boolean { // if the amalgamation of both sets of keys has the a different length to the inputs then there must be a change if (possibleChanges.length !== aKeys.length) return true; - return possibleChanges.some(k => a[k] !== b[k]); + return possibleChanges.some((k) => a[k] !== b[k]); } -type Diff = { changed: K[], added: K[], removed: K[] }; +type Diff = { changed: K[]; added: K[]; removed: K[] }; /** * Determines the keys added, changed, and removed between two objects. @@ -112,7 +112,7 @@ export function objectDiff(a: O, b: O): Diff { const bKeys = Object.keys(b) as (keyof O)[]; const keyDiff = arrayDiff(aKeys, bKeys); const possibleChanges = arrayIntersection(aKeys, bKeys); - const changes = possibleChanges.filter(k => a[k] !== b[k]); + const changes = possibleChanges.filter((k) => a[k] !== b[k]); return { changed: changes, added: keyDiff.added, removed: keyDiff.removed }; } diff --git a/src/utils/pages.ts b/src/utils/pages.ts index 75e4fef9bf6..3b51d796917 100644 --- a/src/utils/pages.ts +++ b/src/utils/pages.ts @@ -17,14 +17,14 @@ limitations under the License. import { logger } from "matrix-js-sdk/src/logger"; import { IConfigOptions } from "../IConfigOptions"; -import { getEmbeddedPagesWellKnown } from '../utils/WellKnownUtils'; +import { getEmbeddedPagesWellKnown } from "../utils/WellKnownUtils"; import { SnakedObject } from "./SnakedObject"; export function getHomePageUrl(appConfig: IConfigOptions): string | null { const config = new SnakedObject(appConfig); const pagesConfig = config.get("embedded_pages"); - let pageUrl = pagesConfig ? (new SnakedObject(pagesConfig).get("home_url")) : null; + let pageUrl = pagesConfig ? new SnakedObject(pagesConfig).get("home_url") : null; if (!pageUrl) { // This is a deprecated config option for the home page @@ -34,7 +34,7 @@ export function getHomePageUrl(appConfig: IConfigOptions): string | null { if (pageUrl) { logger.warn( "You are using a deprecated config option: `welcomePageUrl`. Please use " + - "`embedded_pages.home_url` instead, per https://github.com/vector-im/element-web/issues/21428", + "`embedded_pages.home_url` instead, per https://github.com/vector-im/element-web/issues/21428", ); } } @@ -49,7 +49,5 @@ export function getHomePageUrl(appConfig: IConfigOptions): string | null { export function shouldUseLoginForWelcome(appConfig: IConfigOptions): boolean { const config = new SnakedObject(appConfig); const pagesConfig = config.get("embedded_pages"); - return pagesConfig - ? ((new SnakedObject(pagesConfig).get("login_for_welcome")) === true) - : false; + return pagesConfig ? new SnakedObject(pagesConfig).get("login_for_welcome") === true : false; } diff --git a/src/utils/permalinks/ElementPermalinkConstructor.ts b/src/utils/permalinks/ElementPermalinkConstructor.ts index d66c3ae031c..79ec0ed5662 100644 --- a/src/utils/permalinks/ElementPermalinkConstructor.ts +++ b/src/utils/permalinks/ElementPermalinkConstructor.ts @@ -44,9 +44,9 @@ export default class ElementPermalinkConstructor extends PermalinkConstructor { } forEntity(entityId: string): string { - if (entityId[0] === '!' || entityId[0] === '#') { + if (entityId[0] === "!" || entityId[0] === "#") { return this.forRoom(entityId); - } else if (entityId[0] === '@') { + } else if (entityId[0] === "@") { return this.forUser(entityId); } else throw new Error("Unrecognized entity"); } @@ -57,8 +57,8 @@ export default class ElementPermalinkConstructor extends PermalinkConstructor { } encodeServerCandidates(candidates?: string[]) { - if (!candidates || candidates.length === 0) return ''; - return `?via=${candidates.map(c => encodeURIComponent(c)).join("&via=")}`; + if (!candidates || candidates.length === 0) return ""; + return `?via=${candidates.map((c) => encodeURIComponent(c)).join("&via=")}`; } // Heavily inspired by/borrowed from the matrix-bot-sdk (with permission): @@ -82,7 +82,8 @@ export default class ElementPermalinkConstructor extends PermalinkConstructor { static parseAppRoute(route: string): PermalinkParts { const parts = route.split("/"); - if (parts.length < 2) { // we're expecting an entity and an ID of some kind at least + if (parts.length < 2) { + // we're expecting an entity and an ID of some kind at least throw new Error("URL is missing parts"); } @@ -93,13 +94,13 @@ export default class ElementPermalinkConstructor extends PermalinkConstructor { const entityType = parts[0]; const entity = parts[1]; - if (entityType === 'user') { + if (entityType === "user") { // Probably a user, no further parsing needed. return PermalinkParts.forUser(entity); - } else if (entityType === 'room') { + } else if (entityType === "room") { // Rejoin the rest because v3 events can have slashes (annoyingly) - const eventId = parts.length > 2 ? parts.slice(2).join('/') : ""; - const via = query.split(/&?via=/).filter(p => !!p); + const eventId = parts.length > 2 ? parts.slice(2).join("/") : ""; + const via = query.split(/&?via=/).filter((p) => !!p); return PermalinkParts.forEvent(entity, eventId, via); } else { throw new Error("Unknown entity type in permalink"); diff --git a/src/utils/permalinks/MatrixSchemePermalinkConstructor.ts b/src/utils/permalinks/MatrixSchemePermalinkConstructor.ts index 904fbb89397..080a666fbde 100644 --- a/src/utils/permalinks/MatrixSchemePermalinkConstructor.ts +++ b/src/utils/permalinks/MatrixSchemePermalinkConstructor.ts @@ -39,8 +39,10 @@ export default class MatrixSchemePermalinkConstructor extends PermalinkConstruct } forEvent(roomId: string, eventId: string, serverCandidates: string[]): string { - return `matrix:${this.encodeEntity(roomId)}` + - `/${this.encodeEntity(eventId)}${this.encodeServerCandidates(serverCandidates)}`; + return ( + `matrix:${this.encodeEntity(roomId)}` + + `/${this.encodeEntity(eventId)}${this.encodeServerCandidates(serverCandidates)}` + ); } forRoom(roomIdOrAlias: string, serverCandidates: string[]): string { @@ -61,8 +63,8 @@ export default class MatrixSchemePermalinkConstructor extends PermalinkConstruct } encodeServerCandidates(candidates: string[]) { - if (!candidates || candidates.length === 0) return ''; - return `?via=${candidates.map(c => encodeURIComponent(c)).join("&via=")}`; + if (!candidates || candidates.length === 0) return ""; + return `?via=${candidates.map((c) => encodeURIComponent(c)).join("&via=")}`; } parsePermalink(fullUrl: string): PermalinkParts { @@ -70,26 +72,28 @@ export default class MatrixSchemePermalinkConstructor extends PermalinkConstruct throw new Error("Does not appear to be a permalink"); } - const parts = fullUrl.substring("matrix:".length).split('/'); + const parts = fullUrl.substring("matrix:".length).split("/"); const identifier = parts[0]; const entityNoSigil = parts[1]; - if (identifier === 'u') { + if (identifier === "u") { // Probably a user, no further parsing needed. return PermalinkParts.forUser(`@${entityNoSigil}`); - } else if (identifier === 'r' || identifier === 'roomid') { - const sigil = identifier === 'r' ? '#' : '!'; + } else if (identifier === "r" || identifier === "roomid") { + const sigil = identifier === "r" ? "#" : "!"; - if (parts.length === 2) { // room without event permalink + if (parts.length === 2) { + // room without event permalink const [roomId, query = ""] = entityNoSigil.split("?"); - const via = query.split(/&?via=/g).filter(p => !!p); + const via = query.split(/&?via=/g).filter((p) => !!p); return PermalinkParts.forRoom(`${sigil}${roomId}`, via); } - if (parts[2] === 'e') { // event permalink - const eventIdAndQuery = parts.length > 3 ? parts.slice(3).join('/') : ""; + if (parts[2] === "e") { + // event permalink + const eventIdAndQuery = parts.length > 3 ? parts.slice(3).join("/") : ""; const [eventId, query = ""] = eventIdAndQuery.split("?"); - const via = query.split(/&?via=/g).filter(p => !!p); + const via = query.split(/&?via=/g).filter((p) => !!p); return PermalinkParts.forEvent(`${sigil}${entityNoSigil}`, `$${eventId}`, via); } diff --git a/src/utils/permalinks/MatrixToPermalinkConstructor.ts b/src/utils/permalinks/MatrixToPermalinkConstructor.ts index 3a57fc443f9..a451d82606a 100644 --- a/src/utils/permalinks/MatrixToPermalinkConstructor.ts +++ b/src/utils/permalinks/MatrixToPermalinkConstructor.ts @@ -48,8 +48,8 @@ export default class MatrixToPermalinkConstructor extends PermalinkConstructor { } encodeServerCandidates(candidates: string[]) { - if (!candidates || candidates.length === 0) return ''; - return `?via=${candidates.map(c => encodeURIComponent(c)).join("&via=")}`; + if (!candidates || candidates.length === 0) return ""; + return `?via=${candidates.map((c) => encodeURIComponent(c)).join("&via=")}`; } // Heavily inspired by/borrowed from the matrix-bot-sdk (with permission): @@ -62,20 +62,21 @@ export default class MatrixToPermalinkConstructor extends PermalinkConstructor { const parts = fullUrl.substring(`${baseUrl}/#/`.length).split("/"); const entity = parts[0]; - if (entity[0] === '@') { + if (entity[0] === "@") { // Probably a user, no further parsing needed. return PermalinkParts.forUser(entity); - } else if (entity[0] === '#' || entity[0] === '!') { - if (parts.length === 1) { // room without event permalink - const [roomId, query=""] = entity.split("?"); - const via = query.split(/&?via=/g).filter(p => !!p); + } else if (entity[0] === "#" || entity[0] === "!") { + if (parts.length === 1) { + // room without event permalink + const [roomId, query = ""] = entity.split("?"); + const via = query.split(/&?via=/g).filter((p) => !!p); return PermalinkParts.forRoom(roomId, via); } // rejoin the rest because v3 events can have slashes (annoyingly) - const eventIdAndQuery = parts.length > 1 ? parts.slice(1).join('/') : ""; - const [eventId, query=""] = eventIdAndQuery.split("?"); - const via = query.split(/&?via=/g).filter(p => !!p); + const eventIdAndQuery = parts.length > 1 ? parts.slice(1).join("/") : ""; + const [eventId, query = ""] = eventIdAndQuery.split("?"); + const via = query.split(/&?via=/g).filter((p) => !!p); return PermalinkParts.forEvent(entity, eventId, via); } else { diff --git a/src/utils/permalinks/Permalinks.ts b/src/utils/permalinks/Permalinks.ts index ce2f8aeb1d3..4133509f6f2 100644 --- a/src/utils/permalinks/Permalinks.ts +++ b/src/utils/permalinks/Permalinks.ts @@ -191,13 +191,18 @@ export class RoomPermalinkCreator { const serverName = getServerName(userId); const domain = getHostnameFromMatrixServerName(serverName) ?? serverName; - return !isHostnameIpAddress(domain) && + return ( + !isHostnameIpAddress(domain) && !isHostInRegex(domain, this.bannedHostsRegexps) && - isHostInRegex(domain, this.allowedHostsRegexps); + isHostInRegex(domain, this.allowedHostsRegexps) + ); }); - const maxEntry = allowedEntries.reduce((max, entry) => { - return (entry[1] > max[1]) ? entry : max; - }, [null, 0]); + const maxEntry = allowedEntries.reduce( + (max, entry) => { + return entry[1] > max[1] ? entry : max; + }, + [null, 0], + ); const [userId, powerLevel] = maxEntry; // object wasn't empty, and max entry wasn't a demotion from the default if (userId !== null && powerLevel >= 50) { @@ -219,11 +224,11 @@ export class RoomPermalinkCreator { const getRegex = (hostname) => new RegExp("^" + utils.globToRegexp(hostname, false) + "$"); const denied = aclEvent.getContent().deny || []; - denied.forEach(h => bannedHostsRegexps.push(getRegex(h))); + denied.forEach((h) => bannedHostsRegexps.push(getRegex(h))); const allowed = aclEvent.getContent().allow || []; allowedHostsRegexps = []; // we don't want to use the default rule here - allowed.forEach(h => allowedHostsRegexps.push(getRegex(h))); + allowed.forEach((h) => allowedHostsRegexps.push(getRegex(h))); } } this.bannedHostsRegexps = bannedHostsRegexps; @@ -248,8 +253,9 @@ export class RoomPermalinkCreator { candidates.add(getServerName(this.highestPlUserId)); } - const serversByPopulation = Object.keys(this.populationMap) - .sort((a, b) => this.populationMap[b] - this.populationMap[a]); + const serversByPopulation = Object.keys(this.populationMap).sort( + (a, b) => this.populationMap[b] - this.populationMap[a], + ); for (let i = 0; i < serversByPopulation.length && candidates.size < MAX_SERVER_CANDIDATES; i++) { const serverName = serversByPopulation[i]; @@ -283,7 +289,7 @@ export function makeRoomPermalink(roomId: string): string { // If the roomId isn't actually a room ID, don't try to list the servers. // Aliases are already routable, and don't need extra information. - if (roomId[0] !== '!') return getPermalinkConstructor().forRoom(roomId, []); + if (roomId[0] !== "!") return getPermalinkConstructor().forRoom(roomId, []); const client = MatrixClientPeg.get(); const room = client.getRoom(roomId); @@ -313,15 +319,15 @@ export function tryTransformEntityToPermalink(entity: string): string { if (!entity) return null; // Check to see if it is a bare entity for starters - if (entity[0] === '#' || entity[0] === '!') return makeRoomPermalink(entity); - if (entity[0] === '@') return makeUserPermalink(entity); + if (entity[0] === "#" || entity[0] === "!") return makeRoomPermalink(entity); + if (entity[0] === "@") return makeUserPermalink(entity); if (entity.slice(0, 7) === "matrix:") { try { const permalinkParts = parsePermalink(entity); if (permalinkParts) { if (permalinkParts.roomIdOrAlias) { - const eventIdPart = permalinkParts.eventId ? `/${permalinkParts.eventId}` : ''; + const eventIdPart = permalinkParts.eventId ? `/${permalinkParts.eventId}` : ""; let pl = matrixtoBaseUrl + `/#/${permalinkParts.roomIdOrAlias}${eventIdPart}`; if (permalinkParts.viaServers.length > 0) { pl += new MatrixToPermalinkConstructor().encodeServerCandidates(permalinkParts.viaServers); @@ -344,7 +350,8 @@ export function tryTransformEntityToPermalink(entity: string): string { * @returns {string} The transformed permalink or original URL if unable. */ export function tryTransformPermalinkToLocalHref(permalink: string): string { - if (!permalink.startsWith("http:") && + if ( + !permalink.startsWith("http:") && !permalink.startsWith("https:") && !permalink.startsWith("matrix:") && !permalink.startsWith("vector:") // Element Desktop @@ -367,7 +374,7 @@ export function tryTransformPermalinkToLocalHref(permalink: string): string { const permalinkParts = parsePermalink(permalink); if (permalinkParts) { if (permalinkParts.roomIdOrAlias) { - const eventIdPart = permalinkParts.eventId ? `/${permalinkParts.eventId}` : ''; + const eventIdPart = permalinkParts.eventId ? `/${permalinkParts.eventId}` : ""; permalink = `#/room/${permalinkParts.roomIdOrAlias}${eventIdPart}`; if (permalinkParts.viaServers.length > 0) { permalink += new MatrixToPermalinkConstructor().encodeServerCandidates(permalinkParts.viaServers); @@ -393,7 +400,7 @@ export function getPrimaryPermalinkEntity(permalink: string): string { if (m) { // A bit of a hack, but it gets the job done const handler = new ElementPermalinkConstructor("http://localhost"); - const entityInfo = m[1].split('#').slice(1).join('#'); + const entityInfo = m[1].split("#").slice(1).join("#"); permalinkParts = handler.parsePermalink(`http://localhost/#${entityInfo}`); } } @@ -452,7 +459,7 @@ function isHostInRegex(hostname: string, regexps: RegExp[]): boolean { if (!hostname) return true; // assumed if (regexps.length > 0 && !regexps[0].test) throw new Error(regexps[0].toString()); - return regexps.some(h => h.test(hostname)); + return regexps.some((h) => h.test(hostname)); } function isHostnameIpAddress(hostname: string): boolean { diff --git a/src/utils/permalinks/navigator.ts b/src/utils/permalinks/navigator.ts index ffa4678dbea..640bdabcb74 100644 --- a/src/utils/permalinks/navigator.ts +++ b/src/utils/permalinks/navigator.ts @@ -23,7 +23,8 @@ import { tryTransformPermalinkToLocalHref } from "./Permalinks"; */ export function navigateToPermalink(uri: string): void { const localUri = tryTransformPermalinkToLocalHref(uri); - if (!localUri || localUri === uri) { // parse failure can lead to an unmodified URL + if (!localUri || localUri === uri) { + // parse failure can lead to an unmodified URL throw new Error("Failed to transform URI"); } window.location.hash = localUri; // it'll just be a fragment diff --git a/src/utils/pillify.tsx b/src/utils/pillify.tsx index b7a1b4e5583..ecc208e7329 100644 --- a/src/utils/pillify.tsx +++ b/src/utils/pillify.tsx @@ -15,11 +15,11 @@ limitations under the License. */ import React from "react"; -import ReactDOM from 'react-dom'; -import { PushProcessor } from 'matrix-js-sdk/src/pushprocessor'; +import ReactDOM from "react-dom"; +import { PushProcessor } from "matrix-js-sdk/src/pushprocessor"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { MatrixClientPeg } from '../MatrixClientPeg'; +import { MatrixClientPeg } from "../MatrixClientPeg"; import SettingsStore from "../settings/SettingsStore"; import Pill, { PillType } from "../components/views/elements/Pill"; import { parsePermalink } from "./permalinks/Permalinks"; @@ -54,14 +54,11 @@ export function pillifyLinks(nodes: ArrayLike, mxEvent: MatrixEvent, pi // If the link is a (localised) matrix.to link, replace it with a pill // We don't want to pill event permalinks, so those are ignored. if (parts && !parts.eventId) { - const pillContainer = document.createElement('span'); + const pillContainer = document.createElement("span"); - const pill = ; + const pill = ( + + ); ReactDOM.render(pill, pillContainer); node.parentNode.replaceChild(pillContainer, node); @@ -111,13 +108,15 @@ export function pillifyLinks(nodes: ArrayLike, mxEvent: MatrixEvent, pi // Note we've checked roomNotifTextNodes.length > 0 so we'll do this at least once node = roomNotifTextNode.nextSibling; - const pillContainer = document.createElement('span'); - const pill = ; + const pillContainer = document.createElement("span"); + const pill = ( + + ); ReactDOM.render(pill, pillContainer); roomNotifTextNode.parentNode.replaceChild(pillContainer, roomNotifTextNode); diff --git a/src/utils/promise.ts b/src/utils/promise.ts index 04a9ac88181..e478409eb34 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/utils/read-receipts.ts b/src/utils/read-receipts.ts index 35eda2e3386..fc389f54a74 100644 --- a/src/utils/read-receipts.ts +++ b/src/utils/read-receipts.ts @@ -30,7 +30,7 @@ export function readReceiptChangeIsFor(event: MatrixEvent, client: MatrixClient) for (const [receiptType, receipt] of Object.entries(event.getContent()[eventId])) { if (!isSupportedReceiptType(receiptType)) continue; - if (Object.keys((receipt || {})).includes(myUserId)) return true; + if (Object.keys(receipt || {}).includes(myUserId)) return true; } } } diff --git a/src/utils/room/getJoinedNonFunctionalMembers.ts b/src/utils/room/getJoinedNonFunctionalMembers.ts index 912c4bf1f1c..20a1b37eb8b 100644 --- a/src/utils/room/getJoinedNonFunctionalMembers.ts +++ b/src/utils/room/getJoinedNonFunctionalMembers.ts @@ -23,5 +23,5 @@ import { getFunctionalMembers } from "./getFunctionalMembers"; */ export const getJoinedNonFunctionalMembers = (room: Room): RoomMember[] => { const functionalMembers = getFunctionalMembers(room); - return room.getJoinedMembers().filter(m => !functionalMembers.includes(m.userId)); + return room.getJoinedMembers().filter((m) => !functionalMembers.includes(m.userId)); }; diff --git a/src/utils/room/htmlToPlaintext.ts b/src/utils/room/htmlToPlaintext.ts index 883db8d360d..4b0272b4e15 100644 --- a/src/utils/room/htmlToPlaintext.ts +++ b/src/utils/room/htmlToPlaintext.ts @@ -15,5 +15,5 @@ limitations under the License. */ export function htmlToPlainText(html: string) { - return new DOMParser().parseFromString(html, 'text/html').documentElement.textContent; + return new DOMParser().parseFromString(html, "text/html").documentElement.textContent; } diff --git a/src/utils/rooms.ts b/src/utils/rooms.ts index 3be6c00e677..bd6fcf9d971 100644 --- a/src/utils/rooms.ts +++ b/src/utils/rooms.ts @@ -14,18 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { IInstance, IProtocol, IPublicRoomsChunkRoom, MatrixClient } from "matrix-js-sdk/src/client"; -import { ViewRoom as ViewRoomEvent } from "@matrix-org/analytics-events/types/typescript/ViewRoom"; - -import { Action } from "../dispatcher/actions"; -import { ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload"; import { getE2EEWellKnown } from "./WellKnownUtils"; -import dis from "../dispatcher/dispatcher"; -import { getDisplayAliasForAliasSet } from "../Rooms"; -import { _t } from "../languageHandler"; -import { instanceForInstanceId, protocolNameForInstanceId, ALL_ROOMS, Protocols } from "./DirectoryUtils"; -import SdkConfig from "../SdkConfig"; -import { GenericError } from "./error"; export function privateShouldBeEncrypted(): boolean { const e2eeWellKnown = getE2EEWellKnown(); @@ -35,146 +24,3 @@ export function privateShouldBeEncrypted(): boolean { } return true; } - -interface IShowRoomOpts { - roomAlias?: string; - autoJoin?: boolean; - shouldPeek?: boolean; - roomServer?: string; - metricsTrigger: ViewRoomEvent["trigger"]; -} - -export const showRoom = ( - client: MatrixClient, - room: IPublicRoomsChunkRoom | null, - { - roomAlias, - autoJoin = false, - shouldPeek = false, - roomServer, - }: IShowRoomOpts, -): void => { - const payload: ViewRoomPayload = { - action: Action.ViewRoom, - auto_join: autoJoin, - should_peek: shouldPeek, - metricsTrigger: "RoomDirectory", - }; - if (room) { - // Don't let the user view a room they won't be able to either - // peek or join: fail earlier so they don't have to click back - // to the directory. - if (client.isGuest()) { - if (!room.world_readable && !room.guest_can_join) { - dis.dispatch({ action: 'require_registration' }); - return; - } - } - - if (!roomAlias) { - roomAlias = getDisplayAliasForAliasSet(room.canonical_alias, room.aliases); - } - - payload.oob_data = { - avatarUrl: room.avatar_url, - // XXX: This logic is duplicated from the JS SDK which - // would normally decide what the name is. - name: room.name || roomAlias || _t('Unnamed room'), - }; - - if (roomServer) { - payload.via_servers = [roomServer]; - } - } - // It's not really possible to join Matrix rooms by ID because the HS has no way to know - // which servers to start querying. However, there's no other way to join rooms in - // this list without aliases at present, so if roomAlias isn't set here we have no - // choice but to supply the ID. - if (roomAlias) { - payload.room_alias = roomAlias; - } else { - payload.room_id = room.room_id; - } - dis.dispatch(payload); -}; - -interface IJoinRoomByAliasOpts { - instanceId?: string; - roomServer?: string; - protocols: Protocols; - metricsTrigger: ViewRoomEvent["trigger"]; -} - -export function joinRoomByAlias(cli: MatrixClient, alias: string, { - instanceId, - roomServer, - protocols, - metricsTrigger, -}: IJoinRoomByAliasOpts): void { - // If we don't have a particular instance id selected, just show that rooms alias - if (!instanceId || instanceId === ALL_ROOMS) { - // If the user specified an alias without a domain, add on whichever server is selected - // in the dropdown - if (!alias.includes(':')) { - alias = alias + ':' + roomServer; - } - showRoom(cli, null, { - roomAlias: alias, - autoJoin: true, - metricsTrigger, - }); - } else { - // This is a 3rd party protocol. Let's see if we can join it - const protocolName = protocolNameForInstanceId(protocols, instanceId); - const instance = instanceForInstanceId(protocols, instanceId); - const fields = protocolName - ? getFieldsForThirdPartyLocation(alias, protocols[protocolName], instance) - : null; - if (!fields) { - const brand = SdkConfig.get().brand; - throw new GenericError( - _t('Unable to join network'), - _t('%(brand)s does not know how to join a room on this network', { brand }), - ); - } - cli.getThirdpartyLocation(protocolName, fields).then((resp) => { - if (resp.length > 0 && resp[0].alias) { - showRoom(cli, null, { - roomAlias: resp[0].alias, - autoJoin: true, - metricsTrigger, - }); - } else { - throw new GenericError( - _t('Room not found'), - _t('Couldn\'t find a matching Matrix room'), - ); - } - }, (e) => { - throw new GenericError( - _t('Fetching third party location failed'), - _t('Unable to look up room ID from server'), - ); - }); - } -} - -export function getFieldsForThirdPartyLocation( - userInput: string, - protocol: IProtocol, - instance: IInstance, -): { searchFields?: string[] } | null { - // make an object with the fields specified by that protocol. We - // require that the values of all but the last field come from the - // instance. The last is the user input. - const requiredFields = protocol.location_fields; - if (!requiredFields) return null; - const fields = {}; - for (let i = 0; i < requiredFields.length - 1; ++i) { - const thisField = requiredFields[i]; - if (instance.fields[thisField] === undefined) return null; - fields[thisField] = instance.fields[thisField]; - } - fields[requiredFields[requiredFields.length - 1]] = userInput; - return fields; -} diff --git a/src/utils/sets.ts b/src/utils/sets.ts index da856af2b5f..68c226de39e 100644 --- a/src/utils/sets.ts +++ b/src/utils/sets.ts @@ -25,8 +25,8 @@ import { arrayDiff, Diff } from "./arrays"; export function setHasDiff(a: Set, b: Set): boolean { if (a.size === b.size) { // When the lengths are equal, check to see if either set is missing an element from the other. - if (Array.from(b).some(i => !a.has(i))) return true; - if (Array.from(a).some(i => !b.has(i))) return true; + if (Array.from(b).some((i) => !a.has(i))) return true; + if (Array.from(a).some((i) => !b.has(i))) return true; // if all the keys are common, say so return false; diff --git a/src/utils/space.tsx b/src/utils/space.tsx index 1e30b7235aa..0153edbd434 100644 --- a/src/utils/space.tsx +++ b/src/utils/space.tsx @@ -41,18 +41,20 @@ import { SdkContextClass } from "../contexts/SDKContext"; export const shouldShowSpaceSettings = (space: Room) => { const userId = space.client.getUserId(); - return space.getMyMembership() === "join" - && (space.currentState.maySendStateEvent(EventType.RoomAvatar, userId) - || space.currentState.maySendStateEvent(EventType.RoomName, userId) - || space.currentState.maySendStateEvent(EventType.RoomTopic, userId) - || space.currentState.maySendStateEvent(EventType.RoomJoinRules, userId)); + return ( + space.getMyMembership() === "join" && + (space.currentState.maySendStateEvent(EventType.RoomAvatar, userId) || + space.currentState.maySendStateEvent(EventType.RoomName, userId) || + space.currentState.maySendStateEvent(EventType.RoomTopic, userId) || + space.currentState.maySendStateEvent(EventType.RoomJoinRules, userId)) + ); }; export const makeSpaceParentEvent = (room: Room, canonical = false) => ({ type: EventType.SpaceParent, content: { - "via": calculateRoomVia(room), - "canonical": canonical, + via: calculateRoomVia(room), + canonical: canonical, }, state_key: room.roomId, }); @@ -85,19 +87,20 @@ export const showCreateNewRoom = async (space: Room, type?: RoomType): Promise - ( - (space?.getMyMembership() === "join" && space.canInvite(space.client.getUserId())) || - space.getJoinRule() === JoinRule.Public - ) && shouldShowComponent(UIComponent.InviteUsers); + ((space?.getMyMembership() === "join" && space.canInvite(space.client.getUserId())) || + space.getJoinRule() === JoinRule.Public) && + shouldShowComponent(UIComponent.InviteUsers); export const showSpaceInvite = (space: Room, initialText = ""): void => { if (space.getJoinRule() === "public") { const modal = Modal.createDialog(InfoDialog, { title: _t("Invite to %(spaceName)s", { spaceName: space.name }), - description: - { _t("Share your public space") } - modal.close()} /> - , + description: ( + + {_t("Share your public space")} + modal.close()} /> + + ), fixedWidth: false, button: false, className: "mx_SpacePanel_sharePublicSpace", @@ -109,27 +112,35 @@ export const showSpaceInvite = (space: Room, initialText = ""): void => { }; export const showAddExistingSubspace = (space: Room): void => { - Modal.createDialog(AddExistingSubspaceDialog, { - space, - onCreateSubspaceClick: () => showCreateNewSubspace(space), - onFinished: (added: boolean) => { - if (added && SdkContextClass.instance.roomViewStore.getRoomId() === space.roomId) { - defaultDispatcher.fire(Action.UpdateSpaceHierarchy); - } + Modal.createDialog( + AddExistingSubspaceDialog, + { + space, + onCreateSubspaceClick: () => showCreateNewSubspace(space), + onFinished: (added: boolean) => { + if (added && SdkContextClass.instance.roomViewStore.getRoomId() === space.roomId) { + defaultDispatcher.fire(Action.UpdateSpaceHierarchy); + } + }, }, - }, "mx_AddExistingToSpaceDialog_wrapper"); + "mx_AddExistingToSpaceDialog_wrapper", + ); }; export const showCreateNewSubspace = (space: Room): void => { - Modal.createDialog(CreateSubspaceDialog, { - space, - onAddExistingSpaceClick: () => showAddExistingSubspace(space), - onFinished: (added: boolean) => { - if (added && SdkContextClass.instance.roomViewStore.getRoomId() === space.roomId) { - defaultDispatcher.fire(Action.UpdateSpaceHierarchy); - } + Modal.createDialog( + CreateSubspaceDialog, + { + space, + onAddExistingSpaceClick: () => showAddExistingSubspace(space), + onFinished: (added: boolean) => { + if (added && SdkContextClass.instance.roomViewStore.getRoomId() === space.roomId) { + defaultDispatcher.fire(Action.UpdateSpaceHierarchy); + } + }, }, - }, "mx_CreateSubspaceDialog_wrapper"); + "mx_CreateSubspaceDialog_wrapper", + ); }; export const bulkSpaceBehaviour = async ( diff --git a/src/utils/stringOrderField.ts b/src/utils/stringOrderField.ts index da840792ee8..7c950e75fed 100644 --- a/src/utils/stringOrderField.ts +++ b/src/utils/stringOrderField.ts @@ -47,7 +47,9 @@ export function midPointsBetweenStrings( const step = (baseB - baseA) / BigInt(count + 1); const start = BigInt(baseA + step); - return Array(count).fill(undefined).map((_, i) => baseToString(start + (BigInt(i) * step), alphabet)); + return Array(count) + .fill(undefined) + .map((_, i) => baseToString(start + BigInt(i) * step, alphabet)); } interface IEntry { @@ -62,11 +64,7 @@ export const reorderLexicographically = ( maxLen = 50, ): IEntry[] => { // sanity check inputs - if ( - fromIndex < 0 || toIndex < 0 || - fromIndex > orders.length || toIndex > orders.length || - fromIndex === toIndex - ) { + if (fromIndex < 0 || toIndex < 0 || fromIndex > orders.length || toIndex > orders.length || fromIndex === toIndex) { return []; } @@ -82,9 +80,10 @@ export const reorderLexicographically = ( let rightBoundIdx = toIndex; let canMoveLeft = true; - const nextBase = newOrder[toIndex + 1]?.order !== undefined - ? stringToBase(newOrder[toIndex + 1].order) - : BigInt(Number.MAX_VALUE); + const nextBase = + newOrder[toIndex + 1]?.order !== undefined + ? stringToBase(newOrder[toIndex + 1].order) + : BigInt(Number.MAX_VALUE); // check how far left we would have to mutate to fit in that direction for (let i = toIndex - 1, j = 1; i >= 0; i--, j++) { @@ -95,7 +94,8 @@ export const reorderLexicographically = ( // verify the left move would be sufficient const firstOrderBase = newOrder[0].order === undefined ? undefined : stringToBase(newOrder[0].order); const bigToIndex = BigInt(toIndex); - if (leftBoundIdx === 0 && + if ( + leftBoundIdx === 0 && firstOrderBase !== undefined && nextBase - firstOrderBase <= bigToIndex && firstOrderBase <= bigToIndex @@ -106,9 +106,10 @@ export const reorderLexicographically = ( const canDisplaceRight = !orderToLeftUndefined; let canMoveRight = canDisplaceRight; if (canDisplaceRight) { - const prevBase = newOrder[toIndex - 1]?.order !== undefined - ? stringToBase(newOrder[toIndex - 1]?.order) - : BigInt(Number.MIN_VALUE); + const prevBase = + newOrder[toIndex - 1]?.order !== undefined + ? stringToBase(newOrder[toIndex - 1]?.order) + : BigInt(Number.MIN_VALUE); // check how far right we would have to mutate to fit in that direction for (let i = toIndex + 1, j = 1; i < newOrder.length; i++, j++) { @@ -117,10 +118,11 @@ export const reorderLexicographically = ( } // verify the right move would be sufficient - if (rightBoundIdx === newOrder.length - 1 && - (newOrder[rightBoundIdx] - ? stringToBase(newOrder[rightBoundIdx].order) - : BigInt(Number.MAX_VALUE)) - prevBase <= (rightBoundIdx - toIndex) + if ( + rightBoundIdx === newOrder.length - 1 && + (newOrder[rightBoundIdx] ? stringToBase(newOrder[rightBoundIdx].order) : BigInt(Number.MAX_VALUE)) - + prevBase <= + rightBoundIdx - toIndex ) { canMoveRight = false; } @@ -136,8 +138,9 @@ export const reorderLexicographically = ( } const prevOrder = newOrder[leftBoundIdx - 1]?.order ?? ""; - const nextOrder = newOrder[rightBoundIdx + 1]?.order - ?? DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 1).repeat(prevOrder.length || 1); + const nextOrder = + newOrder[rightBoundIdx + 1]?.order ?? + DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 1).repeat(prevOrder.length || 1); const changes = midPointsBetweenStrings(prevOrder, nextOrder, 1 + rightBoundIdx - leftBoundIdx, maxLen); diff --git a/src/utils/strings.ts b/src/utils/strings.ts index 8bf039656d8..b2dfe4d1bfc 100644 --- a/src/utils/strings.ts +++ b/src/utils/strings.ts @@ -72,17 +72,7 @@ export function selectText(target: Element) { */ export function copyNode(ref: Element): boolean { selectText(ref); - return document.execCommand('copy'); -} - -const collator = new Intl.Collator(); -/** - * Performant language-sensitive string comparison - * @param a the first string to compare - * @param b the second string to compare - */ -export function compare(a: string, b: string): number { - return collator.compare(a, b); + return document.execCommand("copy"); } /** diff --git a/src/utils/tooltipify.tsx b/src/utils/tooltipify.tsx index 843ee326ab5..8e9ccf4fc84 100644 --- a/src/utils/tooltipify.tsx +++ b/src/utils/tooltipify.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import React from "react"; -import ReactDOM from 'react-dom'; +import ReactDOM from "react-dom"; import PlatformPeg from "../PlatformPeg"; import LinkWithTooltip from "../components/views/elements/LinkWithTooltip"; @@ -44,8 +44,10 @@ export function tooltipifyLinks(rootNodes: ArrayLike, ignoredNodes: Ele continue; } - if (node.tagName === "A" && node.getAttribute("href") - && node.getAttribute("href") !== node.textContent.trim() + if ( + node.tagName === "A" && + node.getAttribute("href") && + node.getAttribute("href") !== node.textContent.trim() ) { let href = node.getAttribute("href"); try { @@ -57,9 +59,11 @@ export function tooltipifyLinks(rootNodes: ArrayLike, ignoredNodes: Ele // The node's innerHTML was already sanitized before being rendered in the first place, here we are just // wrapping the link with the LinkWithTooltip component, keeping the same children. Ideally we'd do this // without the superfluous span but this is not something React trivially supports at this time. - const tooltip = - - ; + const tooltip = ( + + + + ); ReactDOM.render(tooltip, node); containers.push(node); diff --git a/src/utils/useTooltip.tsx b/src/utils/useTooltip.tsx index 98b6ffa1bda..303bea17dbe 100644 --- a/src/utils/useTooltip.tsx +++ b/src/utils/useTooltip.tsx @@ -31,10 +31,7 @@ export function useTooltip(props: ComponentProps): [TooltipEvent // No need to fill up the DOM with hidden tooltip elements. Only add the // tooltip when we're hovering over the item (performance) - const tooltip = ; + const tooltip = ; return [{ showTooltip, hideTooltip }, tooltip]; } diff --git a/src/utils/validate/numberInRange.ts b/src/utils/validate/numberInRange.ts index 181641c9354..97d3e5fabba 100644 --- a/src/utils/validate/numberInRange.ts +++ b/src/utils/validate/numberInRange.ts @@ -20,5 +20,5 @@ limitations under the License. * - in a provided range (inclusive) */ export const validateNumberInRange = (min: number, max: number) => (value?: number) => { - return typeof value === 'number' && !(isNaN(value) || min > value || value > max); + return typeof value === "number" && !(isNaN(value) || min > value || value > max); }; diff --git a/src/utils/video-rooms.ts b/src/utils/video-rooms.ts index 7177e0c5e0d..f45fb8f45c6 100644 --- a/src/utils/video-rooms.ts +++ b/src/utils/video-rooms.ts @@ -17,5 +17,5 @@ limitations under the License. import type { Room } from "matrix-js-sdk/src/models/room"; import SettingsStore from "../settings/SettingsStore"; -export const isVideoRoom = (room: Room) => room.isElementVideoRoom() - || (SettingsStore.getValue("feature_element_call_video_rooms") && room.isCallRoom()); +export const isVideoRoom = (room: Room) => + room.isElementVideoRoom() || (SettingsStore.getValue("feature_element_call_video_rooms") && room.isCallRoom()); diff --git a/src/verification.ts b/src/verification.ts index 940de1cbaf4..61cead0f22b 100644 --- a/src/verification.ts +++ b/src/verification.ts @@ -15,14 +15,14 @@ limitations under the License. */ import { User } from "matrix-js-sdk/src/models/user"; -import { verificationMethods as VerificationMethods } from 'matrix-js-sdk/src/crypto'; +import { verificationMethods as VerificationMethods } from "matrix-js-sdk/src/crypto"; import { RoomMember } from "matrix-js-sdk/src/matrix"; -import { MatrixClientPeg } from './MatrixClientPeg'; +import { MatrixClientPeg } from "./MatrixClientPeg"; import dis from "./dispatcher/dispatcher"; -import Modal from './Modal'; +import Modal from "./Modal"; import { RightPanelPhases } from "./stores/right-panel/RightPanelStorePhases"; -import { accessSecretStorage } from './SecurityManager'; +import { accessSecretStorage } from "./SecurityManager"; import UntrustedDeviceDialog from "./components/views/dialogs/UntrustedDeviceDialog"; import { IDevice } from "./components/views/right_panel/UserInfo"; import ManualDeviceKeyVerificationDialog from "./components/views/dialogs/ManualDeviceKeyVerificationDialog"; @@ -47,7 +47,7 @@ async function enable4SIfNeeded() { export async function verifyDevice(user: User, device: IDevice) { const cli = MatrixClientPeg.get(); if (cli.isGuest()) { - dis.dispatch({ action: 'require_registration' }); + dis.dispatch({ action: "require_registration" }); return; } // if cross-signing is not explicitly disabled, check if it should be enabled first. @@ -81,7 +81,7 @@ export async function verifyDevice(user: User, device: IDevice) { export async function legacyVerifyUser(user: User) { const cli = MatrixClientPeg.get(); if (cli.isGuest()) { - dis.dispatch({ action: 'require_registration' }); + dis.dispatch({ action: "require_registration" }); return; } // if cross-signing is not explicitly disabled, check if it should be enabled first. @@ -97,7 +97,7 @@ export async function legacyVerifyUser(user: User) { export async function verifyUser(user: User) { const cli = MatrixClientPeg.get(); if (cli.isGuest()) { - dis.dispatch({ action: 'require_registration' }); + dis.dispatch({ action: "require_registration" }); return; } if (!(await enable4SIfNeeded())) { @@ -108,10 +108,8 @@ export async function verifyUser(user: User) { } function setRightPanel(state: IRightPanelCardState) { - if (RightPanelStore.instance.roomPhaseHistory.some((card) => (card.phase == RightPanelPhases.RoomSummary))) { - RightPanelStore.instance.pushCard( - { phase: RightPanelPhases.EncryptionPanel, state }, - ); + if (RightPanelStore.instance.roomPhaseHistory.some((card) => card.phase == RightPanelPhases.RoomSummary)) { + RightPanelStore.instance.pushCard({ phase: RightPanelPhases.EncryptionPanel, state }); } else { RightPanelStore.instance.setCards([ { phase: RightPanelPhases.RoomSummary }, diff --git a/src/voice-broadcast/audio/VoiceBroadcastRecorder.ts b/src/voice-broadcast/audio/VoiceBroadcastRecorder.ts index f521ed2105c..e8771b94fba 100644 --- a/src/voice-broadcast/audio/VoiceBroadcastRecorder.ts +++ b/src/voice-broadcast/audio/VoiceBroadcastRecorder.ts @@ -45,7 +45,8 @@ export interface ChunkRecordedPayload { */ export class VoiceBroadcastRecorder extends TypedEventEmitter - implements IDestroyable { + implements IDestroyable +{ private headers = new Uint8Array(0); private chunkBuffer = new Uint8Array(0); // position of the previous chunk in seconds @@ -54,10 +55,7 @@ export class VoiceBroadcastRecorder // current chunk length in seconds private currentChunkLength = 0; - public constructor( - private voiceRecording: VoiceRecording, - public readonly targetChunkLength: number, - ) { + public constructor(private voiceRecording: VoiceRecording, public readonly targetChunkLength: number) { super(); this.voiceRecording.onDataAvailable = this.onDataAvailable; } @@ -80,6 +78,7 @@ export class VoiceBroadcastRecorder const chunk = this.extractChunk(); this.currentChunkLength = 0; this.previousChunkEndTimePosition = 0; + this.headers = new Uint8Array(0); return chunk; } @@ -147,10 +146,7 @@ export class VoiceBroadcastRecorder return; } - this.emit( - VoiceBroadcastRecorderEvent.ChunkRecorded, - this.extractChunk(), - ); + this.emit(VoiceBroadcastRecorderEvent.ChunkRecorded, this.extractChunk()); } public destroy(): void { diff --git a/src/voice-broadcast/components/VoiceBroadcastBody.tsx b/src/voice-broadcast/components/VoiceBroadcastBody.tsx index 95bc9fde065..b5a3a7f4711 100644 --- a/src/voice-broadcast/components/VoiceBroadcastBody.tsx +++ b/src/voice-broadcast/components/VoiceBroadcastBody.tsx @@ -58,13 +58,9 @@ export const VoiceBroadcastBody: React.FC = ({ mxEvent }) => { if (shouldDisplayAsVoiceBroadcastRecordingTile(infoState, client, mxEvent)) { const recording = VoiceBroadcastRecordingsStore.instance().getByInfoEvent(mxEvent, client); - return ; + return ; } const playback = VoiceBroadcastPlaybacksStore.instance().getByInfoEvent(mxEvent, client); - return ; + return ; }; diff --git a/src/voice-broadcast/components/atoms/LiveBadge.tsx b/src/voice-broadcast/components/atoms/LiveBadge.tsx index ba94aa14a99..a5a96f460b0 100644 --- a/src/voice-broadcast/components/atoms/LiveBadge.tsx +++ b/src/voice-broadcast/components/atoms/LiveBadge.tsx @@ -14,14 +14,25 @@ See the License for the specific language governing permissions and limitations under the License. */ +import classNames from "classnames"; import React from "react"; import { Icon as LiveIcon } from "../../../../res/img/element-icons/live.svg"; import { _t } from "../../../languageHandler"; -export const LiveBadge: React.FC = () => { - return
- - { _t("Live") } -
; +interface Props { + grey?: boolean; +} + +export const LiveBadge: React.FC = ({ grey = false }) => { + const liveBadgeClasses = classNames("mx_LiveBadge", { + "mx_LiveBadge--grey": grey, + }); + + return ( +
+ + {_t("Live")} +
+ ); }; diff --git a/src/voice-broadcast/components/atoms/SeekButton.tsx b/src/voice-broadcast/components/atoms/SeekButton.tsx new file mode 100644 index 00000000000..0abb9865954 --- /dev/null +++ b/src/voice-broadcast/components/atoms/SeekButton.tsx @@ -0,0 +1,33 @@ +/* +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 AccessibleButton from "../../../components/views/elements/AccessibleButton"; + +interface Props { + icon: React.FC>; + label: string; + onClick: () => void; +} + +export const SeekButton: React.FC = ({ onClick, icon: Icon, label }) => { + return ( + + + + ); +}; diff --git a/src/voice-broadcast/components/atoms/VoiceBroadcastControl.tsx b/src/voice-broadcast/components/atoms/VoiceBroadcastControl.tsx index 276282d1982..684a5ea365f 100644 --- a/src/voice-broadcast/components/atoms/VoiceBroadcastControl.tsx +++ b/src/voice-broadcast/components/atoms/VoiceBroadcastControl.tsx @@ -26,17 +26,14 @@ interface Props { onClick: () => void; } -export const VoiceBroadcastControl: React.FC = ({ - className = "", - icon: Icon, - label, - onClick, -}) => { - return - - ; +export const VoiceBroadcastControl: React.FC = ({ className = "", icon: Icon, label, onClick }) => { + return ( + + + + ); }; diff --git a/src/voice-broadcast/components/atoms/VoiceBroadcastHeader.tsx b/src/voice-broadcast/components/atoms/VoiceBroadcastHeader.tsx index e80d53975fc..3814399b93f 100644 --- a/src/voice-broadcast/components/atoms/VoiceBroadcastHeader.tsx +++ b/src/voice-broadcast/components/atoms/VoiceBroadcastHeader.tsx @@ -12,74 +12,124 @@ limitations under the License. */ import React from "react"; -import { Room, RoomMember } from "matrix-js-sdk/src/matrix"; +import { Room } from "matrix-js-sdk/src/matrix"; +import classNames from "classnames"; -import { LiveBadge } from "../.."; +import { LiveBadge, VoiceBroadcastLiveness } from "../.."; import { Icon as LiveIcon } from "../../../../res/img/element-icons/live.svg"; import { Icon as MicrophoneIcon } from "../../../../res/img/voip/call-view/mic-on.svg"; import { Icon as TimerIcon } from "../../../../res/img/element-icons/Timer.svg"; import { _t } from "../../../languageHandler"; import RoomAvatar from "../../../components/views/avatars/RoomAvatar"; -import AccessibleButton from "../../../components/views/elements/AccessibleButton"; +import AccessibleButton, { ButtonEvent } from "../../../components/views/elements/AccessibleButton"; 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"; +import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; +import { Action } from "../../../dispatcher/actions"; +import dis from "../../../dispatcher/dispatcher"; +import AccessibleTooltipButton from "../../../components/views/elements/AccessibleTooltipButton"; interface VoiceBroadcastHeaderProps { - live?: boolean; + linkToRoom?: boolean; + live?: VoiceBroadcastLiveness; onCloseClick?: () => void; + onMicrophoneLineClick?: ((e: ButtonEvent) => void | Promise) | null; room: Room; - sender: RoomMember; + microphoneLabel?: string; showBroadcast?: boolean; + showBuffering?: boolean; timeLeft?: number; showClose?: boolean; } export const VoiceBroadcastHeader: React.FC = ({ - live = false, + linkToRoom = false, + live = "not-live", onCloseClick = () => {}, + onMicrophoneLineClick = null, room, - sender, + microphoneLabel, showBroadcast = false, + showBuffering = false, showClose = false, timeLeft, }) => { - const broadcast = showBroadcast - ?
+ const broadcast = showBroadcast && ( +
- { _t("Voice broadcast") } + {_t("Voice broadcast")}
- : null; + ); - const liveBadge = live ? : null; + const liveBadge = live !== "not-live" && ; - const closeButton = showClose - ? + const closeButton = showClose && ( + - : null; + ); - const timeLeftLine = timeLeft - ?
+ const timeLeftLine = timeLeft && ( +
- : null; + ); - return
- -
-
- { room.name } -
-
- - { sender.name } + const buffering = showBuffering && ( +
+ + {_t("Buffering…")} +
+ ); + + const microphoneLineClasses = classNames({ + mx_VoiceBroadcastHeader_line: true, + ["mx_VoiceBroadcastHeader_mic--clickable"]: onMicrophoneLineClick, + }); + + const microphoneLine = microphoneLabel && ( + + + {microphoneLabel} + + ); + + const onRoomAvatarOrNameClick = (): void => { + dis.dispatch({ + action: Action.ViewRoom, + room_id: room.roomId, + metricsTrigger: undefined, // other + }); + }; + + let roomAvatar = ; + let roomName =
{room.name}
; + + if (linkToRoom) { + roomAvatar = {roomAvatar}; + + roomName = {roomName}; + } + + return ( +
+ {roomAvatar} +
+ {roomName} + {microphoneLine} + {timeLeftLine} + {broadcast} + {buffering}
- { timeLeftLine } - { broadcast } + {liveBadge} + {closeButton}
- { liveBadge } - { closeButton } -
; + ); }; diff --git a/src/voice-broadcast/components/atoms/VoiceBroadcastRoomSubtitle.tsx b/src/voice-broadcast/components/atoms/VoiceBroadcastRoomSubtitle.tsx new file mode 100644 index 00000000000..17a94a1bd1c --- /dev/null +++ b/src/voice-broadcast/components/atoms/VoiceBroadcastRoomSubtitle.tsx @@ -0,0 +1,29 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; + +import { Icon as LiveIcon } from "../../../../res/img/element-icons/live.svg"; +import { _t } from "../../../languageHandler"; + +export const VoiceBroadcastRoomSubtitle = () => { + return ( +
+ + {_t("Live")} +
+ ); +}; diff --git a/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx b/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx index bb3de10c733..54c4e904126 100644 --- a/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx +++ b/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx @@ -14,7 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { ReactElement } from "react"; +import classNames from "classnames"; import { VoiceBroadcastControl, @@ -22,74 +23,97 @@ 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"; +import { Icon as Back30sIcon } from "../../../../res/img/element-icons/Back30s.svg"; +import { Icon as Forward30sIcon } from "../../../../res/img/element-icons/Forward30s.svg"; import { _t } from "../../../languageHandler"; import Clock from "../../../components/views/audio_messages/Clock"; import SeekBar from "../../../components/views/audio_messages/SeekBar"; +import { SeekButton } from "../atoms/SeekButton"; + +const SEEK_TIME = 30; interface VoiceBroadcastPlaybackBodyProps { + pip?: boolean; playback: VoiceBroadcastPlayback; } -export const VoiceBroadcastPlaybackBody: React.FC = ({ - playback, -}) => { - const { - duration, - live, - room, - sender, - toggle, - playbackState, - } = useVoiceBroadcastPlayback(playback); - - let control: React.ReactNode; - - if (playbackState === VoiceBroadcastPlaybackState.Buffering) { - control = ; - } else { - let controlIcon: React.FC>; - let controlLabel: string; - - switch (playbackState) { - case VoiceBroadcastPlaybackState.Stopped: - controlIcon = PlayIcon; - controlLabel = _t("play voice broadcast"); - break; - case VoiceBroadcastPlaybackState.Paused: - controlIcon = PlayIcon; - controlLabel = _t("resume voice broadcast"); - break; - case VoiceBroadcastPlaybackState.Playing: - controlIcon = PauseIcon; - controlLabel = _t("pause voice broadcast"); - break; - } - - control = ; +export const VoiceBroadcastPlaybackBody: React.FC = ({ pip = false, playback }) => { + const { times, liveness, playbackState, room, sender, toggle } = useVoiceBroadcastPlayback(playback); + + 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.Buffering: + case VoiceBroadcastPlaybackState.Playing: + controlIcon = PauseIcon; + controlLabel = _t("pause voice broadcast"); + break; + } + + const control = ( + + ); + + let seekBackwardButton: ReactElement | null = null; + let seekForwardButton: ReactElement | null = null; + + if (playbackState !== VoiceBroadcastPlaybackState.Stopped) { + const onSeekBackwardButtonClick = () => { + playback.skipTo(Math.max(0, times.position - SEEK_TIME)); + }; + + seekBackwardButton = ( + + ); + + const onSeekForwardButtonClick = () => { + playback.skipTo(Math.min(times.duration, times.position + SEEK_TIME)); + }; + + seekForwardButton = ( + + ); } + const classes = classNames({ + mx_VoiceBroadcastBody: true, + ["mx_VoiceBroadcastBody--pip"]: pip, + }); + return ( -
+
- { control } + {seekBackwardButton} + {control} + {seekForwardButton}
+
- - + +
); diff --git a/src/voice-broadcast/components/molecules/VoiceBroadcastPreRecordingPip.tsx b/src/voice-broadcast/components/molecules/VoiceBroadcastPreRecordingPip.tsx index b8dfd11811a..9b8ed0529f8 100644 --- a/src/voice-broadcast/components/molecules/VoiceBroadcastPreRecordingPip.tsx +++ b/src/voice-broadcast/components/molecules/VoiceBroadcastPreRecordingPip.tsx @@ -14,35 +14,77 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { useRef, useState } from "react"; import { VoiceBroadcastHeader } from "../.."; import AccessibleButton from "../../../components/views/elements/AccessibleButton"; import { VoiceBroadcastPreRecording } from "../../models/VoiceBroadcastPreRecording"; import { Icon as LiveIcon } from "../../../../res/img/element-icons/live.svg"; import { _t } from "../../../languageHandler"; +import { useAudioDeviceSelection } from "../../../hooks/useAudioDeviceSelection"; +import { DevicesContextMenu } from "../../../components/views/audio_messages/DevicesContextMenu"; interface Props { voiceBroadcastPreRecording: VoiceBroadcastPreRecording; } -export const VoiceBroadcastPreRecordingPip: React.FC = ({ - voiceBroadcastPreRecording, -}) => { - return
- - - - { _t("Go live") } - -
; +interface State { + showDeviceSelect: boolean; + disableStartButton: boolean; +} + +export const VoiceBroadcastPreRecordingPip: React.FC = ({ voiceBroadcastPreRecording }) => { + const pipRef = useRef(null); + const { currentDevice, currentDeviceLabel, devices, setDevice } = useAudioDeviceSelection(); + const [state, setState] = useState({ + showDeviceSelect: false, + disableStartButton: false, + }); + + const onDeviceSelect = (device: MediaDeviceInfo) => { + setState((state) => ({ + ...state, + showDeviceSelect: false, + })); + setDevice(device); + }; + + const onStartBroadcastClick = () => { + setState((state) => ({ + ...state, + disableStartButton: true, + })); + + voiceBroadcastPreRecording.start(); + }; + + return ( +
+ setState({ ...state, showDeviceSelect: true })} + room={voiceBroadcastPreRecording.room} + microphoneLabel={currentDeviceLabel} + showClose={true} + /> + + + {_t("Go live")} + + {state.showDeviceSelect && ( + + )} +
+ ); }; diff --git a/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody.tsx b/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody.tsx index 1b13377da9d..0d7b82474cd 100644 --- a/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody.tsx +++ b/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody.tsx @@ -20,19 +20,11 @@ interface VoiceBroadcastRecordingBodyProps { } export const VoiceBroadcastRecordingBody: React.FC = ({ recording }) => { - const { - live, - room, - sender, - } = useVoiceBroadcastRecording(recording); + const { live, room, sender } = useVoiceBroadcastRecording(recording); return (
- +
); }; diff --git a/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip.tsx b/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip.tsx index fdf0e7a2248..1f97c5ba34e 100644 --- a/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip.tsx +++ b/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip.tsx @@ -14,61 +14,83 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { useRef, useState } from "react"; -import { - VoiceBroadcastControl, - VoiceBroadcastInfoState, - VoiceBroadcastRecording, -} from "../.."; +import { VoiceBroadcastControl, VoiceBroadcastInfoState, VoiceBroadcastRecording } from "../.."; import { useVoiceBroadcastRecording } from "../../hooks/useVoiceBroadcastRecording"; import { VoiceBroadcastHeader } from "../atoms/VoiceBroadcastHeader"; import { Icon as StopIcon } from "../../../../res/img/element-icons/Stop.svg"; import { Icon as PauseIcon } from "../../../../res/img/element-icons/pause.svg"; import { Icon as RecordIcon } from "../../../../res/img/element-icons/Record.svg"; +import { Icon as MicrophoneIcon } from "../../../../res/img/element-icons/Mic.svg"; import { _t } from "../../../languageHandler"; +import { useAudioDeviceSelection } from "../../../hooks/useAudioDeviceSelection"; +import { DevicesContextMenu } from "../../../components/views/audio_messages/DevicesContextMenu"; +import AccessibleTooltipButton from "../../../components/views/elements/AccessibleTooltipButton"; interface VoiceBroadcastRecordingPipProps { recording: VoiceBroadcastRecording; } export const VoiceBroadcastRecordingPip: React.FC = ({ recording }) => { - const { - live, - timeLeft, - recordingState, - room, - sender, - stopRecording, - toggleRecording, - } = useVoiceBroadcastRecording(recording); + const pipRef = useRef(null); + const { live, timeLeft, recordingState, room, stopRecording, toggleRecording } = + useVoiceBroadcastRecording(recording); + const { currentDevice, devices, setDevice } = useAudioDeviceSelection(); - const toggleControl = recordingState === VoiceBroadcastInfoState.Paused - ? - : ; + const onDeviceSelect = async (device: MediaDeviceInfo) => { + setShowDeviceSelect(false); - return
- -
-
- { toggleControl } + if (currentDevice.deviceId === device.deviceId) { + // device unchanged + return; + } + + setDevice(device); + + if ([VoiceBroadcastInfoState.Paused, VoiceBroadcastInfoState.Stopped].includes(recordingState)) { + // Nothing to do in these cases. Resume will use the selected device. + return; + } + + // pause and resume to switch the input device + await recording.pause(); + await recording.resume(); + }; + + const [showDeviceSelect, setShowDeviceSelect] = useState(false); + + const toggleControl = + recordingState === VoiceBroadcastInfoState.Paused ? ( + ) : ( + + ); + + return ( +
+ +
+
+ {toggleControl} + setShowDeviceSelect(true)} title={_t("Change input device")}> + + + +
+ {showDeviceSelect && ( + + )}
-
; + ); }; diff --git a/src/voice-broadcast/hooks/useCurrentVoiceBroadcastPlayback.ts b/src/voice-broadcast/hooks/useCurrentVoiceBroadcastPlayback.ts new file mode 100644 index 00000000000..7ed15d06f2e --- /dev/null +++ b/src/voice-broadcast/hooks/useCurrentVoiceBroadcastPlayback.ts @@ -0,0 +1,42 @@ +/* +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 { VoiceBroadcastPlayback } from "../models/VoiceBroadcastPlayback"; +import { + VoiceBroadcastPlaybacksStore, + VoiceBroadcastPlaybacksStoreEvent, +} from "../stores/VoiceBroadcastPlaybacksStore"; + +export const useCurrentVoiceBroadcastPlayback = (voiceBroadcastPlaybackStore: VoiceBroadcastPlaybacksStore) => { + const [currentVoiceBroadcastPlayback, setVoiceBroadcastPlayback] = useState( + voiceBroadcastPlaybackStore.getCurrent(), + ); + + useTypedEventEmitter( + voiceBroadcastPlaybackStore, + VoiceBroadcastPlaybacksStoreEvent.CurrentChanged, + (playback: VoiceBroadcastPlayback) => { + setVoiceBroadcastPlayback(playback); + }, + ); + + return { + currentVoiceBroadcastPlayback, + }; +}; diff --git a/src/voice-broadcast/hooks/useCurrentVoiceBroadcastPreRecording.ts b/src/voice-broadcast/hooks/useCurrentVoiceBroadcastPreRecording.ts index ca9a5769eb0..e78b9656a8d 100644 --- a/src/voice-broadcast/hooks/useCurrentVoiceBroadcastPreRecording.ts +++ b/src/voice-broadcast/hooks/useCurrentVoiceBroadcastPreRecording.ts @@ -26,11 +26,7 @@ export const useCurrentVoiceBroadcastPreRecording = ( voiceBroadcastPreRecordingStore.getCurrent(), ); - useTypedEventEmitter( - voiceBroadcastPreRecordingStore, - "changed", - setCurrentVoiceBroadcastPreRecording, - ); + useTypedEventEmitter(voiceBroadcastPreRecordingStore, "changed", setCurrentVoiceBroadcastPreRecording); return { currentVoiceBroadcastPreRecording, diff --git a/src/voice-broadcast/hooks/useCurrentVoiceBroadcastRecording.ts b/src/voice-broadcast/hooks/useCurrentVoiceBroadcastRecording.ts index 7b5c597a181..4ba397082ee 100644 --- a/src/voice-broadcast/hooks/useCurrentVoiceBroadcastRecording.ts +++ b/src/voice-broadcast/hooks/useCurrentVoiceBroadcastRecording.ts @@ -19,9 +19,7 @@ import { useState } from "react"; import { VoiceBroadcastRecordingsStore, VoiceBroadcastRecordingsStoreEvent } from ".."; import { useTypedEventEmitter } from "../../hooks/useEventEmitter"; -export const useCurrentVoiceBroadcastRecording = ( - voiceBroadcastRecordingsStore: VoiceBroadcastRecordingsStore, -) => { +export const useCurrentVoiceBroadcastRecording = (voiceBroadcastRecordingsStore: VoiceBroadcastRecordingsStore) => { const [currentVoiceBroadcastRecording, setCurrentVoiceBroadcastRecording] = useState( voiceBroadcastRecordingsStore.getCurrent(), ); diff --git a/src/voice-broadcast/hooks/useHasRoomLiveVoiceBroadcast.ts b/src/voice-broadcast/hooks/useHasRoomLiveVoiceBroadcast.ts new file mode 100644 index 00000000000..c50f17d7fe8 --- /dev/null +++ b/src/voice-broadcast/hooks/useHasRoomLiveVoiceBroadcast.ts @@ -0,0 +1,31 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { useState } from "react"; +import { Room, RoomStateEvent } from "matrix-js-sdk/src/matrix"; + +import { hasRoomLiveVoiceBroadcast } from "../utils/hasRoomLiveVoiceBroadcast"; +import { useTypedEventEmitter } from "../../hooks/useEventEmitter"; + +export const useHasRoomLiveVoiceBroadcast = (room: Room) => { + const [hasLiveVoiceBroadcast, setHasLiveVoiceBroadcast] = useState(hasRoomLiveVoiceBroadcast(room).hasBroadcast); + + useTypedEventEmitter(room.currentState, RoomStateEvent.Update, () => { + setHasLiveVoiceBroadcast(hasRoomLiveVoiceBroadcast(room).hasBroadcast); + }); + + return hasLiveVoiceBroadcast; +}; diff --git a/src/voice-broadcast/hooks/useVoiceBroadcastPlayback.ts b/src/voice-broadcast/hooks/useVoiceBroadcastPlayback.ts index 94ea05eb0de..b594c307f6e 100644 --- a/src/voice-broadcast/hooks/useVoiceBroadcastPlayback.ts +++ b/src/voice-broadcast/hooks/useVoiceBroadcastPlayback.ts @@ -18,16 +18,16 @@ import { useState } from "react"; import { useTypedEventEmitter } from "../../hooks/useEventEmitter"; import { MatrixClientPeg } from "../../MatrixClientPeg"; -import { - VoiceBroadcastInfoState, - VoiceBroadcastPlayback, - VoiceBroadcastPlaybackEvent, - VoiceBroadcastPlaybackState, -} from ".."; +import { VoiceBroadcastPlayback, VoiceBroadcastPlaybackEvent, VoiceBroadcastPlaybackState } from ".."; 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(); }; @@ -41,26 +41,22 @@ export const useVoiceBroadcastPlayback = (playback: VoiceBroadcastPlayback) => { }, ); - const [playbackInfoState, setPlaybackInfoState] = useState(playback.getInfoState()); - useTypedEventEmitter( - playback, - VoiceBroadcastPlaybackEvent.InfoStateChanged, - setPlaybackInfoState, - ); + const [times, setTimes] = useState({ + duration: playback.durationSeconds, + position: playback.timeSeconds, + timeLeft: playback.timeLeftSeconds, + }); + useTypedEventEmitter(playback, VoiceBroadcastPlaybackEvent.TimesChanged, (t) => setTimes(t)); - const [duration, setDuration] = useState(playback.durationSeconds); - useTypedEventEmitter( - playback, - VoiceBroadcastPlaybackEvent.LengthChanged, - d => setDuration(d / 1000), - ); + const [liveness, setLiveness] = useState(playback.getLiveness()); + useTypedEventEmitter(playback, VoiceBroadcastPlaybackEvent.LivenessChanged, (l) => setLiveness(l)); return { - duration, - live: playbackInfoState !== VoiceBroadcastInfoState.Stopped, + times, + liveness: liveness, + playbackState, room: room, sender: playback.infoEvent.sender, toggle: playbackToggle, - playbackState, }; }; diff --git a/src/voice-broadcast/hooks/useVoiceBroadcastRecording.tsx b/src/voice-broadcast/hooks/useVoiceBroadcastRecording.tsx index 07c4427361b..e63c2bfbad7 100644 --- a/src/voice-broadcast/hooks/useVoiceBroadcastRecording.tsx +++ b/src/voice-broadcast/hooks/useVoiceBroadcastRecording.tsx @@ -16,11 +16,7 @@ limitations under the License. import React, { useState } from "react"; -import { - VoiceBroadcastInfoState, - VoiceBroadcastRecording, - VoiceBroadcastRecordingEvent, -} from ".."; +import { VoiceBroadcastInfoState, VoiceBroadcastRecording, VoiceBroadcastRecordingEvent } from ".."; import QuestionDialog from "../../components/views/dialogs/QuestionDialog"; import { useTypedEventEmitter } from "../../hooks/useEventEmitter"; import { _t } from "../../languageHandler"; @@ -28,26 +24,31 @@ import { MatrixClientPeg } from "../../MatrixClientPeg"; import Modal from "../../Modal"; const showStopBroadcastingDialog = async (): Promise => { - const { finished } = Modal.createDialog( - QuestionDialog, - { - title: _t("Stop live broadcasting?"), - description: ( -

- { _t("Are you sure you want to stop your live broadcast?" - + "This will end the broadcast and the full recording will be available in the room.") } -

- ), - button: _t("Yes, stop broadcast"), - }, - ); + const { finished } = Modal.createDialog(QuestionDialog, { + title: _t("Stop live broadcasting?"), + description: ( +

+ {_t( + "Are you sure you want to stop your live broadcast?" + + "This will end the broadcast and the full recording will be available in the room.", + )} +

+ ), + button: _t("Yes, stop broadcast"), + }); const [confirmed] = await finished; return confirmed; }; export const useVoiceBroadcastRecording = (recording: VoiceBroadcastRecording) => { const client = MatrixClientPeg.get(); - const room = client.getRoom(recording.infoEvent.getRoomId()); + const roomId = recording.infoEvent.getRoomId(); + const room = client.getRoom(roomId); + + if (!room) { + throw new Error("Unable to find voice broadcast room with Id: " + roomId); + } + const stopRecording = async () => { const confirmed = await showStopBroadcastingDialog(); @@ -66,17 +67,9 @@ export const useVoiceBroadcastRecording = (recording: VoiceBroadcastRecording) = ); const [timeLeft, setTimeLeft] = useState(recording.getTimeLeft()); - useTypedEventEmitter( - recording, - VoiceBroadcastRecordingEvent.TimeLeftChanged, - setTimeLeft, - ); + useTypedEventEmitter(recording, VoiceBroadcastRecordingEvent.TimeLeftChanged, setTimeLeft); - const live = [ - VoiceBroadcastInfoState.Started, - VoiceBroadcastInfoState.Paused, - VoiceBroadcastInfoState.Resumed, - ].includes(recordingState); + const live = [VoiceBroadcastInfoState.Started, VoiceBroadcastInfoState.Resumed].includes(recordingState); return { live, diff --git a/src/voice-broadcast/index.ts b/src/voice-broadcast/index.ts index 87ccd77e9fa..f71ce077ad8 100644 --- a/src/voice-broadcast/index.ts +++ b/src/voice-broadcast/index.ts @@ -29,29 +29,37 @@ export * from "./components/VoiceBroadcastBody"; export * from "./components/atoms/LiveBadge"; export * from "./components/atoms/VoiceBroadcastControl"; export * from "./components/atoms/VoiceBroadcastHeader"; +export * from "./components/atoms/VoiceBroadcastRoomSubtitle"; export * from "./components/molecules/VoiceBroadcastPlaybackBody"; export * from "./components/molecules/VoiceBroadcastPreRecordingPip"; export * from "./components/molecules/VoiceBroadcastRecordingBody"; export * from "./components/molecules/VoiceBroadcastRecordingPip"; export * from "./hooks/useCurrentVoiceBroadcastPreRecording"; export * from "./hooks/useCurrentVoiceBroadcastRecording"; +export * from "./hooks/useHasRoomLiveVoiceBroadcast"; export * from "./hooks/useVoiceBroadcastRecording"; export * from "./stores/VoiceBroadcastPlaybacksStore"; export * from "./stores/VoiceBroadcastPreRecordingStore"; export * from "./stores/VoiceBroadcastRecordingsStore"; export * from "./utils/checkVoiceBroadcastPreConditions"; +export * from "./utils/doClearCurrentVoiceBroadcastPlaybackIfStopped"; +export * from "./utils/doMaybeSetCurrentVoiceBroadcastPlayback"; export * from "./utils/getChunkLength"; export * from "./utils/getMaxBroadcastLength"; export * from "./utils/hasRoomLiveVoiceBroadcast"; export * from "./utils/findRoomLiveVoiceBroadcastFromUserAndDevice"; export * from "./utils/shouldDisplayAsVoiceBroadcastRecordingTile"; export * from "./utils/shouldDisplayAsVoiceBroadcastTile"; +export * from "./utils/shouldDisplayAsVoiceBroadcastStoppedText"; export * from "./utils/startNewVoiceBroadcastRecording"; +export * from "./utils/textForVoiceBroadcastStoppedEvent"; export * from "./utils/VoiceBroadcastResumer"; export const VoiceBroadcastInfoEventType = "io.element.voice_broadcast_info"; export const VoiceBroadcastChunkEventType = "io.element.voice_broadcast_chunk"; +export type VoiceBroadcastLiveness = "live" | "not-live" | "grey"; + export enum VoiceBroadcastInfoState { Started = "started", Paused = "paused", diff --git a/src/voice-broadcast/models/VoiceBroadcastPlayback.ts b/src/voice-broadcast/models/VoiceBroadcastPlayback.ts index 634e21dd887..57a39b492ee 100644 --- a/src/voice-broadcast/models/VoiceBroadcastPlayback.ts +++ b/src/voice-broadcast/models/VoiceBroadcastPlayback.ts @@ -18,6 +18,7 @@ import { EventType, MatrixClient, MatrixEvent, + MatrixEventEvent, MsgType, RelationType, } from "matrix-js-sdk/src/matrix"; @@ -30,7 +31,7 @@ import { PlaybackManager } from "../../audio/PlaybackManager"; import { UPDATE_EVENT } from "../../stores/AsyncStore"; import { MediaEventHelper } from "../../utils/MediaEventHelper"; import { IDestroyable } from "../../utils/IDestroyable"; -import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from ".."; +import { VoiceBroadcastLiveness, VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from ".."; import { RelationsHelper, RelationsHelperEvent } from "../../events/RelationsHelper"; import { VoiceBroadcastChunkEvents } from "../utils/VoiceBroadcastChunkEvents"; @@ -42,25 +43,32 @@ export enum VoiceBroadcastPlaybackState { } export enum VoiceBroadcastPlaybackEvent { - PositionChanged = "position_changed", - LengthChanged = "length_changed", + TimesChanged = "times_changed", + LivenessChanged = "liveness_changed", StateChanged = "state_changed", InfoStateChanged = "info_state_changed", } +type VoiceBroadcastPlaybackTimes = { + duration: number; + position: number; + timeLeft: number; +}; + interface EventMap { - [VoiceBroadcastPlaybackEvent.PositionChanged]: (position: number) => void; - [VoiceBroadcastPlaybackEvent.LengthChanged]: (length: number) => void; + [VoiceBroadcastPlaybackEvent.TimesChanged]: (times: VoiceBroadcastPlaybackTimes) => void; + [VoiceBroadcastPlaybackEvent.LivenessChanged]: (liveness: VoiceBroadcastLiveness) => void; [VoiceBroadcastPlaybackEvent.StateChanged]: ( state: VoiceBroadcastPlaybackState, - playback: VoiceBroadcastPlayback + playback: VoiceBroadcastPlayback, ) => void; [VoiceBroadcastPlaybackEvent.InfoStateChanged]: (state: VoiceBroadcastInfoState) => void; } export class VoiceBroadcastPlayback extends TypedEventEmitter - implements IDestroyable, PlaybackInterface { + implements IDestroyable, PlaybackInterface +{ private state = VoiceBroadcastPlaybackState.Stopped; private chunkEvents = new VoiceBroadcastChunkEvents(); private playbacks = new Map(); @@ -70,6 +78,7 @@ export class VoiceBroadcastPlayback /** @var current playback position in milliseconds */ private position = 0; public readonly liveData = new SimpleObservable(); + private liveness: VoiceBroadcastLiveness = "not-live"; // set vial addInfoEvent() in constructor private infoState!: VoiceBroadcastInfoState; @@ -79,12 +88,10 @@ export class VoiceBroadcastPlayback private chunkRelationHelper!: RelationsHelper; private infoRelationHelper!: RelationsHelper; - public constructor( - public readonly infoEvent: MatrixEvent, - private client: MatrixClient, - ) { + public constructor(public readonly infoEvent: MatrixEvent, private client: MatrixClient) { super(); this.addInfoEvent(this.infoEvent); + this.infoEvent.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction); this.setUpRelationsHelper(); } @@ -134,15 +141,17 @@ export class VoiceBroadcastPlayback return false; } + if (!event.getId() && !event.getTxnId()) { + // skip events without id and txn id + return false; + } + this.chunkEvents.addEvent(event); this.setDuration(this.chunkEvents.getLength()); - if (this.getState() !== VoiceBroadcastPlaybackState.Stopped) { - await this.enqueueChunk(event); - } - if (this.getState() === VoiceBroadcastPlaybackState.Buffering) { await this.start(); + this.updateLiveness(); } return true; @@ -165,18 +174,15 @@ export class VoiceBroadcastPlayback this.setInfoState(state); }; - private async enqueueChunks(): Promise { - const promises = this.chunkEvents.getEvents().reduce((promises, event: MatrixEvent) => { - if (!this.playbacks.has(event.getId() || "")) { - promises.push(this.enqueueChunk(event)); - } - return promises; - }, [] as Promise[]); - - await Promise.all(promises); - } + private onBeforeRedaction = () => { + if (this.getState() !== VoiceBroadcastPlaybackState.Stopped) { + this.stop(); + // destroy cleans up everything + this.destroy(); + } + }; - private async enqueueChunk(chunkEvent: MatrixEvent): Promise { + private async loadPlayback(chunkEvent: MatrixEvent): Promise { const eventId = chunkEvent.getId(); if (!eventId) { @@ -197,13 +203,18 @@ export class VoiceBroadcastPlayback }); } - private onPlaybackPositionUpdate = ( - event: MatrixEvent, - position: number, - ): void => { + private unloadPlayback(event: MatrixEvent): void { + const playback = this.playbacks.get(event.getId()!); + if (!playback) return; + + playback.destroy(); + this.playbacks.delete(event.getId()!); + } + + private onPlaybackPositionUpdate = (event: MatrixEvent, position: number): void => { if (event !== this.currentlyPlaying) return; - const newPosition = this.chunkEvents.getLengthTo(event) + (position * 1000); // observable sends seconds + const newPosition = this.chunkEvents.getLengthTo(event) + position * 1000; // observable sends seconds // do not jump backwards - this can happen when transiting from one to another chunk if (newPosition < this.position) return; @@ -212,23 +223,27 @@ export class VoiceBroadcastPlayback }; private setDuration(duration: number): void { - const shouldEmit = this.duration !== duration; - this.duration = duration; + if (this.duration === duration) return; - if (shouldEmit) { - this.emit(VoiceBroadcastPlaybackEvent.LengthChanged, this.duration); - this.liveData.update([this.timeSeconds, this.durationSeconds]); - } + this.duration = duration; + this.emitTimesChanged(); + this.liveData.update([this.timeSeconds, this.durationSeconds]); } private setPosition(position: number): void { - const shouldEmit = this.position !== position; + if (this.position === position) return; + this.position = position; + this.emitTimesChanged(); + this.liveData.update([this.timeSeconds, this.durationSeconds]); + } - if (shouldEmit) { - this.emit(VoiceBroadcastPlaybackEvent.PositionChanged, this.position); - this.liveData.update([this.timeSeconds, this.durationSeconds]); - } + private emitTimesChanged(): void { + this.emit(VoiceBroadcastPlaybackEvent.TimesChanged, { + duration: this.durationSeconds, + position: this.timeSeconds, + timeLeft: this.timeLeftSeconds, + }); } private onPlaybackStateChange = async (event: MatrixEvent, newState: PlaybackState): Promise => { @@ -236,6 +251,7 @@ export class VoiceBroadcastPlayback if (newState !== PlaybackState.Stopped) return; await this.playNext(); + this.unloadPlayback(event); }; private async playNext(): Promise { @@ -258,10 +274,11 @@ export class VoiceBroadcastPlayback private async playEvent(event: MatrixEvent): Promise { this.setState(VoiceBroadcastPlaybackState.Playing); this.currentlyPlaying = event; - await this.getPlaybackForEvent(event)?.play(); + const playback = await this.getOrLoadPlaybackForEvent(event); + playback?.play(); } - private getPlaybackForEvent(event: MatrixEvent): Playback | undefined { + private async getOrLoadPlaybackForEvent(event: MatrixEvent): Promise { const eventId = event.getId(); if (!eventId) { @@ -269,6 +286,10 @@ export class VoiceBroadcastPlayback return; } + if (!this.playbacks.has(eventId)) { + await this.loadPlayback(event); + } + const playback = this.playbacks.get(eventId); if (!playback) { @@ -276,9 +297,54 @@ export class VoiceBroadcastPlayback logger.warn("unable to find playback for event", event); } + // try to load the playback for the next event for a smooth(er) playback + const nextEvent = this.chunkEvents.getNext(event); + if (nextEvent) this.loadPlayback(nextEvent); + return playback; } + private getCurrentPlayback(): Playback | undefined { + if (!this.currentlyPlaying) return; + return this.playbacks.get(this.currentlyPlaying.getId()!); + } + + public getLiveness(): VoiceBroadcastLiveness { + return this.liveness; + } + + private setLiveness(liveness: VoiceBroadcastLiveness): void { + if (this.liveness === liveness) return; + + this.liveness = liveness; + this.emit(VoiceBroadcastPlaybackEvent.LivenessChanged, liveness); + } + + private updateLiveness(): void { + if (this.infoState === VoiceBroadcastInfoState.Stopped) { + this.setLiveness("not-live"); + return; + } + + if (this.infoState === VoiceBroadcastInfoState.Paused) { + this.setLiveness("grey"); + return; + } + + if ([VoiceBroadcastPlaybackState.Stopped, VoiceBroadcastPlaybackState.Paused].includes(this.state)) { + this.setLiveness("grey"); + return; + } + + if (this.currentlyPlaying && this.chunkEvents.isLast(this.currentlyPlaying)) { + this.setLiveness("live"); + return; + } + + this.setLiveness("grey"); + return; + } + public get currentState(): PlaybackState { return PlaybackState.Playing; } @@ -291,20 +357,24 @@ export class VoiceBroadcastPlayback return this.duration / 1000; } + public get timeLeftSeconds(): number { + return Math.round(this.durationSeconds) - this.timeSeconds; + } + public async skipTo(timeSeconds: number): Promise { const time = timeSeconds * 1000; const event = this.chunkEvents.findByTime(time); - if (!event) return; - - const currentPlayback = this.currentlyPlaying - ? this.getPlaybackForEvent(this.currentlyPlaying) - : null; + if (!event) { + logger.warn("voice broadcast chunk event to skip to not found"); + return; + } - const skipToPlayback = this.getPlaybackForEvent(event); + const currentPlayback = this.getCurrentPlayback(); + const skipToPlayback = await this.getOrLoadPlaybackForEvent(event); if (!skipToPlayback) { - logger.error("voice broadcast chunk to skip to not found", event); + logger.warn("voice broadcast chunk to skip to not found", event); return; } @@ -319,22 +389,23 @@ export class VoiceBroadcastPlayback const offsetInChunk = time - this.chunkEvents.getLengthTo(event); await skipToPlayback.skipTo(offsetInChunk / 1000); - if (currentPlayback !== skipToPlayback) { + if (this.state === VoiceBroadcastPlaybackState.Playing && !skipToPlayback.isPlaying) { await skipToPlayback.play(); } this.setPosition(time); + this.updateLiveness(); } public async start(): Promise { - await this.enqueueChunks(); const chunkEvents = this.chunkEvents.getEvents(); - const toPlay = this.getInfoState() === VoiceBroadcastInfoState.Stopped - ? chunkEvents[0] // start at the beginning for an ended voice broadcast - : chunkEvents[chunkEvents.length - 1]; // start at the current chunk for an ongoing voice broadcast + const toPlay = + this.getInfoState() === VoiceBroadcastInfoState.Stopped + ? chunkEvents[0] // start at the beginning for an ended voice broadcast + : chunkEvents[chunkEvents.length - 1]; // start at the current chunk for an ongoing voice broadcast - if (this.playbacks.has(toPlay?.getId() || "")) { + if (toPlay) { return this.playEvent(toPlay); } @@ -353,7 +424,7 @@ export class VoiceBroadcastPlayback this.setState(VoiceBroadcastPlaybackState.Paused); if (!this.currentlyPlaying) return; - this.getPlaybackForEvent(this.currentlyPlaying)?.pause(); + this.getCurrentPlayback()?.pause(); } public resume(): void { @@ -364,7 +435,7 @@ export class VoiceBroadcastPlayback } this.setState(VoiceBroadcastPlaybackState.Playing); - this.getPlaybackForEvent(this.currentlyPlaying)?.play(); + this.getCurrentPlayback()?.play(); } /** @@ -398,6 +469,7 @@ export class VoiceBroadcastPlayback this.state = state; this.emit(VoiceBroadcastPlaybackEvent.StateChanged, state, this); + this.updateLiveness(); } public getInfoState(): VoiceBroadcastInfoState { @@ -411,6 +483,7 @@ export class VoiceBroadcastPlayback this.infoState = state; this.emit(VoiceBroadcastPlaybackEvent.InfoStateChanged, state); + this.updateLiveness(); } public destroy(): void { @@ -419,7 +492,7 @@ export class VoiceBroadcastPlayback this.removeAllListeners(); this.chunkEvents = new VoiceBroadcastChunkEvents(); - this.playbacks.forEach(p => p.destroy()); + this.playbacks.forEach((p) => p.destroy()); this.playbacks = new Map(); } } diff --git a/src/voice-broadcast/models/VoiceBroadcastPreRecording.ts b/src/voice-broadcast/models/VoiceBroadcastPreRecording.ts index f1e956c6009..700bda8b8aa 100644 --- a/src/voice-broadcast/models/VoiceBroadcastPreRecording.ts +++ b/src/voice-broadcast/models/VoiceBroadcastPreRecording.ts @@ -18,33 +18,32 @@ 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"; type VoiceBroadcastPreRecordingEvent = "dismiss"; interface EventMap { - "dismiss": (voiceBroadcastPreRecording: VoiceBroadcastPreRecording) => void; + dismiss: (voiceBroadcastPreRecording: VoiceBroadcastPreRecording) => void; } export class VoiceBroadcastPreRecording extends TypedEventEmitter - implements IDestroyable { + implements IDestroyable +{ public constructor( public room: Room, public sender: RoomMember, private client: MatrixClient, + private playbacksStore: VoiceBroadcastPlaybacksStore, private recordingsStore: VoiceBroadcastRecordingsStore, ) { super(); } public start = async (): Promise => { - await startNewVoiceBroadcastRecording( - this.room, - this.client, - this.recordingsStore, - ); + await startNewVoiceBroadcastRecording(this.room, this.client, this.playbacksStore, this.recordingsStore); this.emit("dismiss", this); }; diff --git a/src/voice-broadcast/models/VoiceBroadcastRecording.ts b/src/voice-broadcast/models/VoiceBroadcastRecording.ts index e080c872247..e0627731eb9 100644 --- a/src/voice-broadcast/models/VoiceBroadcastRecording.ts +++ b/src/voice-broadcast/models/VoiceBroadcastRecording.ts @@ -56,7 +56,8 @@ interface EventMap { export class VoiceBroadcastRecording extends TypedEventEmitter - implements IDestroyable { + implements IDestroyable +{ private state: VoiceBroadcastInfoState; private recorder: VoiceBroadcastRecorder; private sequence = 1; @@ -108,8 +109,8 @@ export class VoiceBroadcastRecording private onChunkEvent = (event: MatrixEvent): void => { if ( - (!event.getId() && !event.getTxnId()) - || event.getContent()?.msgtype !== MsgType.Audio // don't add non-audio event + (!event.getId() && !event.getTxnId()) || + event.getContent()?.msgtype !== MsgType.Audio // don't add non-audio event ) { return; } @@ -119,15 +120,19 @@ export class VoiceBroadcastRecording private setInitialStateFromInfoEvent(): void { const room = this.client.getRoom(this.infoEvent.getRoomId()); - const relations = room?.getUnfilteredTimelineSet()?.relations?.getChildEventsForEvent( - this.infoEvent.getId(), - RelationType.Reference, - VoiceBroadcastInfoEventType, - ); + const relations = room + ?.getUnfilteredTimelineSet() + ?.relations?.getChildEventsForEvent( + this.infoEvent.getId(), + RelationType.Reference, + VoiceBroadcastInfoEventType, + ); const relatedEvents = relations?.getRelations(); this.state = !relatedEvents?.find((event: MatrixEvent) => { return event.getContent()?.state === VoiceBroadcastInfoState.Stopped; - }) ? VoiceBroadcastInfoState.Started : VoiceBroadcastInfoState.Stopped; + }) + ? VoiceBroadcastInfoState.Started + : VoiceBroadcastInfoState.Stopped; } public getTimeLeft(): number { @@ -222,8 +227,8 @@ export class VoiceBroadcastRecording private onAction = (payload: ActionPayload) => { if (payload.action !== "call_state") return; - // stop on any call action - this.stop(); + // pause on any call action + this.pause(); }; private setState(state: VoiceBroadcastInfoState): void { @@ -244,12 +249,9 @@ export class VoiceBroadcastRecording return uploadFile( this.client, this.infoEvent.getRoomId(), - new Blob( - [chunk.buffer], - { - type: this.getRecorder().contentType, - }, - ), + new Blob([chunk.buffer], { + type: this.getRecorder().contentType, + }), ); } diff --git a/src/voice-broadcast/stores/VoiceBroadcastPlaybacksStore.ts b/src/voice-broadcast/stores/VoiceBroadcastPlaybacksStore.ts index 03378d9492a..b49673c4fe3 100644 --- a/src/voice-broadcast/stores/VoiceBroadcastPlaybacksStore.ts +++ b/src/voice-broadcast/stores/VoiceBroadcastPlaybacksStore.ts @@ -25,7 +25,7 @@ export enum VoiceBroadcastPlaybacksStoreEvent { } interface EventMap { - [VoiceBroadcastPlaybacksStoreEvent.CurrentChanged]: (recording: VoiceBroadcastPlayback) => void; + [VoiceBroadcastPlaybacksStoreEvent.CurrentChanged]: (recording: VoiceBroadcastPlayback | null) => void; } /** @@ -35,7 +35,8 @@ interface EventMap { */ export class VoiceBroadcastPlaybacksStore extends TypedEventEmitter - implements IDestroyable { + implements IDestroyable +{ private current: VoiceBroadcastPlayback | null; /** Playbacks indexed by their info event id. */ @@ -53,7 +54,14 @@ export class VoiceBroadcastPlaybacksStore this.emit(VoiceBroadcastPlaybacksStoreEvent.CurrentChanged, current); } - public getCurrent(): VoiceBroadcastPlayback { + public clearCurrent(): void { + if (this.current === null) return; + + this.current = null; + this.emit(VoiceBroadcastPlaybacksStoreEvent.CurrentChanged, null); + } + + public getCurrent(): VoiceBroadcastPlayback | null { return this.current; } @@ -76,15 +84,16 @@ export class VoiceBroadcastPlaybacksStore playback.on(VoiceBroadcastPlaybackEvent.StateChanged, this.onPlaybackStateChanged); } - private onPlaybackStateChanged = ( - state: VoiceBroadcastPlaybackState, - playback: VoiceBroadcastPlayback, - ): void => { - if ([ - VoiceBroadcastPlaybackState.Buffering, - VoiceBroadcastPlaybackState.Playing, - ].includes(state)) { - this.pauseExcept(playback); + private onPlaybackStateChanged = (state: VoiceBroadcastPlaybackState, playback: VoiceBroadcastPlayback): void => { + switch (state) { + case VoiceBroadcastPlaybackState.Buffering: + case VoiceBroadcastPlaybackState.Playing: + this.pauseExcept(playback); + this.setCurrent(playback); + break; + case VoiceBroadcastPlaybackState.Stopped: + this.clearCurrent(); + break; } }; diff --git a/src/voice-broadcast/stores/VoiceBroadcastPreRecordingStore.ts b/src/voice-broadcast/stores/VoiceBroadcastPreRecordingStore.ts index faefea3ddf6..13fcd831d1d 100644 --- a/src/voice-broadcast/stores/VoiceBroadcastPreRecordingStore.ts +++ b/src/voice-broadcast/stores/VoiceBroadcastPreRecordingStore.ts @@ -27,7 +27,8 @@ interface EventMap { export class VoiceBroadcastPreRecordingStore extends TypedEventEmitter - implements IDestroyable { + implements IDestroyable +{ private current: VoiceBroadcastPreRecording | null = null; public setCurrent(current: VoiceBroadcastPreRecording): void { diff --git a/src/voice-broadcast/stores/VoiceBroadcastRecordingsStore.ts b/src/voice-broadcast/stores/VoiceBroadcastRecordingsStore.ts index b5c78a1b0e5..b6c8191f54b 100644 --- a/src/voice-broadcast/stores/VoiceBroadcastRecordingsStore.ts +++ b/src/voice-broadcast/stores/VoiceBroadcastRecordingsStore.ts @@ -31,7 +31,7 @@ interface EventMap { * This store provides access to the current and specific Voice Broadcast recordings. */ export class VoiceBroadcastRecordingsStore extends TypedEventEmitter { - private current: VoiceBroadcastRecording | null; + private current: VoiceBroadcastRecording | null = null; private recordings = new Map(); public constructor() { @@ -55,6 +55,10 @@ export class VoiceBroadcastRecordingsStore extends TypedEventEmitter this.equalByTxnIdOrId(event, e)); + return !!this.events.find((e) => this.equalByTxnIdOrId(event, e)); } /** @@ -93,21 +93,25 @@ export class VoiceBroadcastChunkEvents { return null; } + public isLast(event: MatrixEvent): boolean { + return this.events.indexOf(event) >= this.events.length - 1; + } + private calculateChunkLength(event: MatrixEvent): number { - return event.getContent()?.["org.matrix.msc1767.audio"]?.duration - || event.getContent()?.info?.duration - || 0; + return event.getContent()?.["org.matrix.msc1767.audio"]?.duration || event.getContent()?.info?.duration || 0; } private addOrReplaceEvent = (event: MatrixEvent): boolean => { - this.events = this.events.filter(e => !this.equalByTxnIdOrId(event, e)); + this.events = this.events.filter((e) => !this.equalByTxnIdOrId(event, e)); this.events.push(event); return true; }; private equalByTxnIdOrId(eventA: MatrixEvent, eventB: MatrixEvent): boolean { - return eventA.getTxnId() && eventB.getTxnId() && eventA.getTxnId() === eventB.getTxnId() - || eventA.getId() === eventB.getId(); + return ( + (eventA.getTxnId() && eventB.getTxnId() && eventA.getTxnId() === eventB.getTxnId()) || + eventA.getId() === eventB.getId() + ); } /** diff --git a/src/voice-broadcast/utils/VoiceBroadcastResumer.ts b/src/voice-broadcast/utils/VoiceBroadcastResumer.ts index be949d0eabe..a46953d815e 100644 --- a/src/voice-broadcast/utils/VoiceBroadcastResumer.ts +++ b/src/voice-broadcast/utils/VoiceBroadcastResumer.ts @@ -25,9 +25,7 @@ import { findRoomLiveVoiceBroadcastFromUserAndDevice } from "./findRoomLiveVoice * Handles voice broadcasts on app resume (after logging in, reload, crash…). */ export class VoiceBroadcastResumer implements IDestroyable { - public constructor( - private client: MatrixClient, - ) { + public constructor(private client: MatrixClient) { if (client.isInitialSyncComplete()) { this.resume(); } else { @@ -80,9 +78,10 @@ export class VoiceBroadcastResumer implements IDestroyable { }; // all events should reference the started event - const referencedEventId = infoEvent.getContent()?.state === VoiceBroadcastInfoState.Started - ? infoEvent.getId() - : infoEvent.getContent()?.["m.relates_to"]?.event_id; + const referencedEventId = + infoEvent.getContent()?.state === VoiceBroadcastInfoState.Started + ? infoEvent.getId() + : infoEvent.getContent()?.["m.relates_to"]?.event_id; if (referencedEventId) { content["m.relates_to"] = { diff --git a/src/voice-broadcast/utils/checkVoiceBroadcastPreConditions.tsx b/src/voice-broadcast/utils/checkVoiceBroadcastPreConditions.tsx index a76e6faa313..3fdf8c24401 100644 --- a/src/voice-broadcast/utils/checkVoiceBroadcastPreConditions.tsx +++ b/src/voice-broadcast/utils/checkVoiceBroadcastPreConditions.tsx @@ -25,8 +25,14 @@ import Modal from "../../Modal"; const showAlreadyRecordingDialog = () => { Modal.createDialog(InfoDialog, { title: _t("Can't start a new voice broadcast"), - description:

{ _t("You are already recording a voice broadcast. " - + "Please end your current voice broadcast to start a new one.") }

, + description: ( +

+ {_t( + "You are already recording a voice broadcast. " + + "Please end your current voice broadcast to start a new one.", + )} +

+ ), hasCloseButton: true, }); }; @@ -34,8 +40,14 @@ const showAlreadyRecordingDialog = () => { const showInsufficientPermissionsDialog = () => { Modal.createDialog(InfoDialog, { title: _t("Can't start a new voice broadcast"), - description:

{ _t("You don't have the required permissions to start a voice broadcast in this room. " - + "Contact a room administrator to upgrade your permissions.") }

, + description: ( +

+ {_t( + "You don't have the required permissions to start a voice broadcast in this room. " + + "Contact a room administrator to upgrade your permissions.", + )} +

+ ), hasCloseButton: true, }); }; @@ -43,8 +55,14 @@ const showInsufficientPermissionsDialog = () => { const showOthersAlreadyRecordingDialog = () => { Modal.createDialog(InfoDialog, { title: _t("Can't start a new voice broadcast"), - description:

{ _t("Someone else is already recording a voice broadcast. " - + "Wait for their voice broadcast to end to start a new one.") }

, + description: ( +

+ {_t( + "Someone else is already recording a voice broadcast. " + + "Wait for their voice broadcast to end to start a new one.", + )} +

+ ), hasCloseButton: true, }); }; diff --git a/src/voice-broadcast/utils/doClearCurrentVoiceBroadcastPlaybackIfStopped.ts b/src/voice-broadcast/utils/doClearCurrentVoiceBroadcastPlaybackIfStopped.ts new file mode 100644 index 00000000000..8a3bc8be9d0 --- /dev/null +++ b/src/voice-broadcast/utils/doClearCurrentVoiceBroadcastPlaybackIfStopped.ts @@ -0,0 +1,26 @@ +/* +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 { VoiceBroadcastPlaybacksStore, VoiceBroadcastPlaybackState } from ".."; + +export const doClearCurrentVoiceBroadcastPlaybackIfStopped = ( + voiceBroadcastPlaybacksStore: VoiceBroadcastPlaybacksStore, +) => { + if (voiceBroadcastPlaybacksStore.getCurrent()?.getState() === VoiceBroadcastPlaybackState.Stopped) { + // clear current if stopped + return; + } +}; diff --git a/src/voice-broadcast/utils/doMaybeSetCurrentVoiceBroadcastPlayback.ts b/src/voice-broadcast/utils/doMaybeSetCurrentVoiceBroadcastPlayback.ts new file mode 100644 index 00000000000..ad92a95977e --- /dev/null +++ b/src/voice-broadcast/utils/doMaybeSetCurrentVoiceBroadcastPlayback.ts @@ -0,0 +1,64 @@ +/* +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, Room } from "matrix-js-sdk/src/matrix"; + +import { + hasRoomLiveVoiceBroadcast, + VoiceBroadcastPlaybacksStore, + VoiceBroadcastPlaybackState, + VoiceBroadcastRecordingsStore, +} from ".."; + +/** + * When a live voice broadcast is in the room and + * another voice broadcast is not currently being listened to or recorded + * the live broadcast in the room is set as the current broadcast to listen to. + * When there is no live broadcast in the room: clear current broadcast. + * + * @param {Room} room The room to check for a live voice broadcast + * @param {MatrixClient} client + * @param {VoiceBroadcastPlaybacksStore} voiceBroadcastPlaybacksStore + * @param {VoiceBroadcastRecordingsStore} voiceBroadcastRecordingsStore + */ +export const doMaybeSetCurrentVoiceBroadcastPlayback = ( + room: Room, + client: MatrixClient, + voiceBroadcastPlaybacksStore: VoiceBroadcastPlaybacksStore, + voiceBroadcastRecordingsStore: VoiceBroadcastRecordingsStore, +): void => { + // do not disturb the current recording + if (voiceBroadcastRecordingsStore.hasCurrent()) return; + + const currentPlayback = voiceBroadcastPlaybacksStore.getCurrent(); + + if (currentPlayback && currentPlayback.getState() !== VoiceBroadcastPlaybackState.Stopped) { + // do not disturb the current playback + return; + } + + const { infoEvent } = hasRoomLiveVoiceBroadcast(room); + + if (infoEvent) { + // live broadcast in the room + no recording + not listening yet: set the current broadcast + const voiceBroadcastPlayback = voiceBroadcastPlaybacksStore.getByInfoEvent(infoEvent, client); + voiceBroadcastPlaybacksStore.setCurrent(voiceBroadcastPlayback); + return; + } + + // no broadcast; not listening: clear current + voiceBroadcastPlaybacksStore.clearCurrent(); +}; diff --git a/src/voice-broadcast/utils/getChunkLength.ts b/src/voice-broadcast/utils/getChunkLength.ts index 9eebfe49791..5c8f75a5ce4 100644 --- a/src/voice-broadcast/utils/getChunkLength.ts +++ b/src/voice-broadcast/utils/getChunkLength.ts @@ -23,7 +23,5 @@ import SdkConfig, { DEFAULTS } from "../../SdkConfig"; * - If that fails fall back to 120 (two minutes) */ export const getChunkLength = (): number => { - return SdkConfig.get("voice_broadcast")?.chunk_length - || DEFAULTS.voice_broadcast?.chunk_length - || 120; + return SdkConfig.get("voice_broadcast")?.chunk_length || DEFAULTS.voice_broadcast?.chunk_length || 120; }; diff --git a/src/voice-broadcast/utils/getMaxBroadcastLength.ts b/src/voice-broadcast/utils/getMaxBroadcastLength.ts index 15eb83b4a9a..2ac930f80a9 100644 --- a/src/voice-broadcast/utils/getMaxBroadcastLength.ts +++ b/src/voice-broadcast/utils/getMaxBroadcastLength.ts @@ -23,7 +23,5 @@ import SdkConfig, { DEFAULTS } from "../../SdkConfig"; * - If that fails fall back to four hours */ export const getMaxBroadcastLength = (): number => { - return SdkConfig.get("voice_broadcast")?.max_length - || DEFAULTS.voice_broadcast?.max_length - || 4 * 60 * 60; + return SdkConfig.get("voice_broadcast")?.max_length || DEFAULTS.voice_broadcast?.max_length || 4 * 60 * 60; }; diff --git a/src/voice-broadcast/utils/hasRoomLiveVoiceBroadcast.ts b/src/voice-broadcast/utils/hasRoomLiveVoiceBroadcast.ts index 577b9ed8805..ae506c68bac 100644 --- a/src/voice-broadcast/utils/hasRoomLiveVoiceBroadcast.ts +++ b/src/voice-broadcast/utils/hasRoomLiveVoiceBroadcast.ts @@ -19,36 +19,42 @@ import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from ".."; interface Result { + // whether there is a live broadcast in the room hasBroadcast: boolean; + // info event of any live broadcast in the room + infoEvent: MatrixEvent | null; + // whether the broadcast was started by the user startedByUser: boolean; } -/** - * Finds out whether there is a live broadcast in a room. - * Also returns if the user started the broadcast (if any). - */ -export const hasRoomLiveVoiceBroadcast = (room: Room, userId: string): Result => { +export const hasRoomLiveVoiceBroadcast = (room: Room, userId?: string): Result => { let hasBroadcast = false; let startedByUser = false; + let infoEvent: MatrixEvent | null = null; const stateEvents = room.currentState.getStateEvents(VoiceBroadcastInfoEventType); - stateEvents.forEach((event: MatrixEvent) => { + stateEvents.every((event: MatrixEvent) => { const state = event.getContent()?.state; if (state && state !== VoiceBroadcastInfoState.Stopped) { hasBroadcast = true; + infoEvent = event; // state key = sender's MXID if (event.getStateKey() === userId) { + infoEvent = event; startedByUser = true; // break here, because more than true / true is not possible return false; } } + + return true; }); return { hasBroadcast, + infoEvent, startedByUser, }; }; diff --git a/src/voice-broadcast/utils/setUpVoiceBroadcastPreRecording.ts b/src/voice-broadcast/utils/setUpVoiceBroadcastPreRecording.ts index 8bd211f6120..9d5d410aa2f 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/shouldDisplayAsVoiceBroadcastRecordingTile.ts b/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastRecordingTile.ts index b9964b6f2a9..c6891f3b77f 100644 --- a/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastRecordingTile.ts +++ b/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastRecordingTile.ts @@ -24,7 +24,5 @@ export const shouldDisplayAsVoiceBroadcastRecordingTile = ( event: MatrixEvent, ): boolean => { const userId = client.getUserId(); - return !!userId - && userId === event.getSender() - && state !== VoiceBroadcastInfoState.Stopped; + return !!userId && userId === event.getSender() && state !== VoiceBroadcastInfoState.Stopped; }; diff --git a/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastStoppedText.ts b/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastStoppedText.ts new file mode 100644 index 00000000000..22d6e79c37c --- /dev/null +++ b/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastStoppedText.ts @@ -0,0 +1,24 @@ +/* +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 { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from ".."; + +export const shouldDisplayAsVoiceBroadcastStoppedText = (event: MatrixEvent): boolean => + event.getType() === VoiceBroadcastInfoEventType && + event.getContent()?.state === VoiceBroadcastInfoState.Stopped && + !event.isRedacted(); diff --git a/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastTile.ts b/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastTile.ts index ef55eed3bbf..57a0f2f3c6a 100644 --- a/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastTile.ts +++ b/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastTile.ts @@ -18,10 +18,6 @@ import { MatrixEvent } from "matrix-js-sdk/src/matrix"; import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from ".."; -export const shouldDisplayAsVoiceBroadcastTile = (event: MatrixEvent) => ( - event.getType?.() === VoiceBroadcastInfoEventType - && ( - event.getContent?.()?.state === VoiceBroadcastInfoState.Started - || event.isRedacted() - ) -); +export const shouldDisplayAsVoiceBroadcastTile = (event: MatrixEvent) => + event.getType?.() === VoiceBroadcastInfoEventType && + (event.getContent?.()?.state === VoiceBroadcastInfoState.Started || event.isRedacted()); diff --git a/src/voice-broadcast/utils/startNewVoiceBroadcastRecording.ts b/src/voice-broadcast/utils/startNewVoiceBroadcastRecording.ts index ae4e40c4a36..a9cfeea9bb8 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"; @@ -50,10 +51,7 @@ const startBroadcast = async ( if (voiceBroadcastEvent?.getId() === result.event_id) { room.off(RoomStateEvent.Events, onRoomStateEvents); - const recording = new VoiceBroadcastRecording( - voiceBroadcastEvent, - client, - ); + const recording = new VoiceBroadcastRecording(voiceBroadcastEvent, client); recordingsStore.setCurrent(recording); recording.start(); resolve(recording); @@ -80,17 +78,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/src/voice-broadcast/utils/textForVoiceBroadcastStoppedEvent.tsx b/src/voice-broadcast/utils/textForVoiceBroadcastStoppedEvent.tsx new file mode 100644 index 00000000000..611908b7502 --- /dev/null +++ b/src/voice-broadcast/utils/textForVoiceBroadcastStoppedEvent.tsx @@ -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 React, { ReactNode } from "react"; +import { MatrixEvent } from "matrix-js-sdk/src/matrix"; + +import { MatrixClientPeg } from "../../MatrixClientPeg"; +import AccessibleButton from "../../components/views/elements/AccessibleButton"; +import { highlightEvent } from "../../utils/EventUtils"; +import { getSenderName } from "../../TextForEvent"; +import { _t } from "../../languageHandler"; + +export const textForVoiceBroadcastStoppedEvent = (event: MatrixEvent): (() => ReactNode) => { + return (): ReactNode => { + const ownUserId = MatrixClientPeg.get()?.getUserId(); + const startEventId = event.getRelation()?.event_id; + const roomId = event.getRoomId(); + + const templateTags = { + a: (text: string) => + startEventId && roomId ? ( + highlightEvent(roomId, startEventId)}> + {text} + + ) : ( + text + ), + }; + + if (ownUserId && ownUserId === event.getSender()) { + return _t("You ended a voice broadcast", {}, templateTags); + } + + return _t("%(senderName)s ended a voice broadcast", { senderName: getSenderName(event) }, templateTags); + }; +}; diff --git a/src/widgets/CapabilityText.tsx b/src/widgets/CapabilityText.tsx index e4790eaad2a..dbc16e004ac 100644 --- a/src/widgets/CapabilityText.tsx +++ b/src/widgets/CapabilityText.tsx @@ -21,7 +21,8 @@ import { getTimelineRoomIDFromCapability, isTimelineCapability, isTimelineCapabilityFor, - MatrixCapabilities, Symbols, + MatrixCapabilities, + Symbols, WidgetEventCapability, WidgetKind, } from "matrix-widget-api"; @@ -165,17 +166,27 @@ export class CapabilityText { const roomId = getTimelineRoomIDFromCapability(capability); const room = MatrixClientPeg.get().getRoom(roomId); return { - primary: _t("The above, but in as well", {}, { - Room: () => { - if (room) { - return - { room.name } - ; - } else { - return { roomId }; - } + primary: _t( + "The above, but in as well", + {}, + { + Room: () => { + if (room) { + return ( + + {room.name} + + ); + } else { + return ( + + {roomId} + + ); + } + }, }, - }), + ), }; } } @@ -193,9 +204,10 @@ export class CapabilityText { // See if we have a static line of text to provide for the given event type and // direction. The hope is that we do for common event types for friendlier copy. - const evSendRecv = eventCap.kind === EventKind.State - ? CapabilityText.stateSendRecvCaps - : CapabilityText.nonStateSendRecvCaps; + const evSendRecv = + eventCap.kind === EventKind.State + ? CapabilityText.stateSendRecvCaps + : CapabilityText.nonStateSendRecvCaps; if (evSendRecv[eventCap.eventType]) { const textForKind = evSendRecv[eventCap.eventType]; const textForDirection = textForKind[kind] || textForKind[GENERIC_WIDGET_KIND]; @@ -211,40 +223,57 @@ export class CapabilityText { if (kind === WidgetKind.Room) { if (eventCap.direction === EventDirection.Send) { return { - primary: _t("Send %(eventType)s events as you in this room", { - eventType: eventCap.eventType, - }, { - b: sub => { sub }, - }), + primary: _t( + "Send %(eventType)s events as you in this room", + { + eventType: eventCap.eventType, + }, + { + b: (sub) => {sub}, + }, + ), byline: CapabilityText.bylineFor(eventCap), }; } else { return { - primary: _t("See %(eventType)s events posted to this room", { - eventType: eventCap.eventType, - }, { - b: sub => { sub }, - }), + primary: _t( + "See %(eventType)s events posted to this room", + { + eventType: eventCap.eventType, + }, + { + b: (sub) => {sub}, + }, + ), byline: CapabilityText.bylineFor(eventCap), }; } - } else { // assume generic + } else { + // assume generic if (eventCap.direction === EventDirection.Send) { return { - primary: _t("Send %(eventType)s events as you in your active room", { - eventType: eventCap.eventType, - }, { - b: sub => { sub }, - }), + primary: _t( + "Send %(eventType)s events as you in your active room", + { + eventType: eventCap.eventType, + }, + { + b: (sub) => {sub}, + }, + ), byline: CapabilityText.bylineFor(eventCap), }; } else { return { - primary: _t("See %(eventType)s events posted to your active room", { - eventType: eventCap.eventType, - }, { - b: sub => { sub }, - }), + primary: _t( + "See %(eventType)s events posted to your active room", + { + eventType: eventCap.eventType, + }, + { + b: (sub) => {sub}, + }, + ), byline: CapabilityText.bylineFor(eventCap), }; } @@ -253,9 +282,13 @@ export class CapabilityText { // We don't have enough context to render this capability specially, so we'll present it as-is return { - primary: _t("The %(capability)s capability", { capability }, { - b: sub => { sub }, - }), + primary: _t( + "The %(capability)s capability", + { capability }, + { + b: (sub) => {sub}, + }, + ), }; } @@ -264,15 +297,17 @@ export class CapabilityText { if (!eventCap.keyStr) { if (eventCap.direction === EventDirection.Send) { return { - primary: kind === WidgetKind.Room - ? _t("Send messages as you in this room") - : _t("Send messages as you in your active room"), + primary: + kind === WidgetKind.Room + ? _t("Send messages as you in this room") + : _t("Send messages as you in your active room"), }; } else { return { - primary: kind === WidgetKind.Room - ? _t("See messages posted to this room") - : _t("See messages posted to your active room"), + primary: + kind === WidgetKind.Room + ? _t("See messages posted to this room") + : _t("See messages posted to your active room"), }; } } @@ -283,75 +318,85 @@ export class CapabilityText { case MsgType.Text: { if (eventCap.direction === EventDirection.Send) { return { - primary: kind === WidgetKind.Room - ? _t("Send text messages as you in this room") - : _t("Send text messages as you in your active room"), + primary: + kind === WidgetKind.Room + ? _t("Send text messages as you in this room") + : _t("Send text messages as you in your active room"), }; } else { return { - primary: kind === WidgetKind.Room - ? _t("See text messages posted to this room") - : _t("See text messages posted to your active room"), + primary: + kind === WidgetKind.Room + ? _t("See text messages posted to this room") + : _t("See text messages posted to your active room"), }; } } case MsgType.Emote: { if (eventCap.direction === EventDirection.Send) { return { - primary: kind === WidgetKind.Room - ? _t("Send emotes as you in this room") - : _t("Send emotes as you in your active room"), + primary: + kind === WidgetKind.Room + ? _t("Send emotes as you in this room") + : _t("Send emotes as you in your active room"), }; } else { return { - primary: kind === WidgetKind.Room - ? _t("See emotes posted to this room") - : _t("See emotes posted to your active room"), + primary: + kind === WidgetKind.Room + ? _t("See emotes posted to this room") + : _t("See emotes posted to your active room"), }; } } case MsgType.Image: { if (eventCap.direction === EventDirection.Send) { return { - primary: kind === WidgetKind.Room - ? _t("Send images as you in this room") - : _t("Send images as you in your active room"), + primary: + kind === WidgetKind.Room + ? _t("Send images as you in this room") + : _t("Send images as you in your active room"), }; } else { return { - primary: kind === WidgetKind.Room - ? _t("See images posted to this room") - : _t("See images posted to your active room"), + primary: + kind === WidgetKind.Room + ? _t("See images posted to this room") + : _t("See images posted to your active room"), }; } } case MsgType.Video: { if (eventCap.direction === EventDirection.Send) { return { - primary: kind === WidgetKind.Room - ? _t("Send videos as you in this room") - : _t("Send videos as you in your active room"), + primary: + kind === WidgetKind.Room + ? _t("Send videos as you in this room") + : _t("Send videos as you in your active room"), }; } else { return { - primary: kind === WidgetKind.Room - ? _t("See videos posted to this room") - : _t("See videos posted to your active room"), + primary: + kind === WidgetKind.Room + ? _t("See videos posted to this room") + : _t("See videos posted to your active room"), }; } } case MsgType.File: { if (eventCap.direction === EventDirection.Send) { return { - primary: kind === WidgetKind.Room - ? _t("Send general files as you in this room") - : _t("Send general files as you in your active room"), + primary: + kind === WidgetKind.Room + ? _t("Send general files as you in this room") + : _t("Send general files as you in your active room"), }; } else { return { - primary: kind === WidgetKind.Room - ? _t("See general files posted to this room") - : _t("See general files posted to your active room"), + primary: + kind === WidgetKind.Room + ? _t("See general files posted to this room") + : _t("See general files posted to your active room"), }; } } @@ -359,31 +404,47 @@ export class CapabilityText { let primary: TranslatedString; if (eventCap.direction === EventDirection.Send) { if (kind === WidgetKind.Room) { - primary = _t("Send %(msgtype)s messages as you in this room", { - msgtype: eventCap.keyStr, - }, { - b: sub => { sub }, - }); + primary = _t( + "Send %(msgtype)s messages as you in this room", + { + msgtype: eventCap.keyStr, + }, + { + b: (sub) => {sub}, + }, + ); } else { - primary = _t("Send %(msgtype)s messages as you in your active room", { - msgtype: eventCap.keyStr, - }, { - b: sub => { sub }, - }); + primary = _t( + "Send %(msgtype)s messages as you in your active room", + { + msgtype: eventCap.keyStr, + }, + { + b: (sub) => {sub}, + }, + ); } } else { if (kind === WidgetKind.Room) { - primary = _t("See %(msgtype)s messages posted to this room", { - msgtype: eventCap.keyStr, - }, { - b: sub => { sub }, - }); + primary = _t( + "See %(msgtype)s messages posted to this room", + { + msgtype: eventCap.keyStr, + }, + { + b: (sub) => {sub}, + }, + ); } else { - primary = _t("See %(msgtype)s messages posted to your active room", { - msgtype: eventCap.keyStr, - }, { - b: sub => { sub }, - }); + primary = _t( + "See %(msgtype)s messages posted to your active room", + { + msgtype: eventCap.keyStr, + }, + { + b: (sub) => {sub}, + }, + ); } } return { primary }; diff --git a/src/widgets/Jitsi.ts b/src/widgets/Jitsi.ts index 7d506ace129..d97a03eca2c 100644 --- a/src/widgets/Jitsi.ts +++ b/src/widgets/Jitsi.ts @@ -44,7 +44,7 @@ export class Jitsi { * * See https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification */ - public async getJitsiAuth(): Promise { + public async getJitsiAuth(): Promise { if (!this.preferredDomain) { return null; } @@ -73,7 +73,7 @@ export class Jitsi { let domain = SdkConfig.getObject("jitsi")?.get("preferred_domain") || "meet.element.io"; logger.log("Attempting to get Jitsi conference information from homeserver"); - const wkPreferredDomain = discoveryResponse?.[JITSI_WK_PROPERTY]?.['preferredDomain']; + const wkPreferredDomain = discoveryResponse?.[JITSI_WK_PROPERTY]?.["preferredDomain"]; if (wkPreferredDomain) domain = wkPreferredDomain; // Put the result into memory for us to use later diff --git a/src/widgets/ManagedHybrid.ts b/src/widgets/ManagedHybrid.ts index 86c035182ab..8962dab5c08 100644 --- a/src/widgets/ManagedHybrid.ts +++ b/src/widgets/ManagedHybrid.ts @@ -79,10 +79,7 @@ export async function addManagedHybridWidget(roomId: string) { // Ensure the widget is not already present in the room let widgets = WidgetStore.instance.getApps(roomId); - const existing = ( - widgets.some(w => w.id === widgetId) || - WidgetEchoStore.roomHasPendingWidgets(roomId, []) - ); + const existing = widgets.some((w) => w.id === widgetId) || WidgetEchoStore.roomHasPendingWidgets(roomId, []); if (existing) { logger.error(`Managed hybrid widget already present in room ${roomId}`); return; @@ -101,7 +98,7 @@ export async function addManagedHybridWidget(roomId: string) { return; } widgets = WidgetStore.instance.getApps(roomId); - const installedWidget = widgets.find(w => w.id === widgetId); + const installedWidget = widgets.find((w) => w.id === widgetId); if (!installedWidget) { return; } diff --git a/src/widgets/WidgetType.ts b/src/widgets/WidgetType.ts index e42f3ffa9b5..92db57ee71a 100644 --- a/src/widgets/WidgetType.ts +++ b/src/widgets/WidgetType.ts @@ -21,8 +21,7 @@ export class WidgetType { public static readonly INTEGRATION_MANAGER = new WidgetType("m.integration_manager", "m.integration_manager"); public static readonly CUSTOM = new WidgetType("m.custom", "m.custom"); - constructor(public readonly preferred: string, public readonly legacy: string) { - } + constructor(public readonly preferred: string, public readonly legacy: string) {} public matches(type: string): boolean { return type === this.preferred || type === this.legacy; @@ -30,8 +29,8 @@ export class WidgetType { static fromString(type: string): WidgetType { // First try and match it against something we're already aware of - const known = Object.values(WidgetType).filter(v => v instanceof WidgetType); - const knownMatch = known.find(w => w.matches(type)); + const known = Object.values(WidgetType).filter((v) => v instanceof WidgetType); + const knownMatch = known.find((w) => w.matches(type)); if (knownMatch) return knownMatch; // If that fails, invent a new widget type diff --git a/test/ContentMessages-test.ts b/test/ContentMessages-test.ts index 966b5d5159b..e2a47977912 100644 --- a/test/ContentMessages-test.ts +++ b/test/ContentMessages-test.ts @@ -63,24 +63,15 @@ describe("ContentMessages", () => { describe("sendStickerContentToRoom", () => { beforeEach(() => { mocked(client.sendStickerMessage).mockReturnValue(prom); - mocked(doMaybeLocalRoomAction).mockImplementation(( - roomId: string, - fn: (actualRoomId: string) => Promise, - client?: MatrixClient, - ) => { - return fn(roomId); - }); + mocked(doMaybeLocalRoomAction).mockImplementation( + (roomId: string, fn: (actualRoomId: string) => Promise, client?: MatrixClient) => { + return fn(roomId); + }, + ); }); it("should forward the call to doMaybeLocalRoomAction", async () => { - await contentMessages.sendStickerContentToRoom( - stickerUrl, - roomId, - null, - imageInfo, - text, - client, - ); + await contentMessages.sendStickerContentToRoom(stickerUrl, roomId, null, imageInfo, text, client); expect(client.sendStickerMessage).toHaveBeenCalledWith(roomId, null, stickerUrl, imageInfo, text); }); }); @@ -88,22 +79,25 @@ describe("ContentMessages", () => { describe("sendContentToRoom", () => { const roomId = "!roomId:server"; beforeEach(() => { - Object.defineProperty(global.Image.prototype, 'src', { + 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', { - get() { return 600; }, + Object.defineProperty(global.Image.prototype, "height", { + get() { + return 600; + }, }); - Object.defineProperty(global.Image.prototype, 'width', { - get() { return 800; }, + Object.defineProperty(global.Image.prototype, "width", { + get() { + return 800; + }, }); - mocked(doMaybeLocalRoomAction).mockImplementation(( - roomId: string, - fn: (actualRoomId: string) => Promise, - ) => fn(roomId)); + mocked(doMaybeLocalRoomAction).mockImplementation( + (roomId: string, fn: (actualRoomId: string) => Promise) => fn(roomId), + ); mocked(BlurhashEncoder.instance.getBlurhash).mockResolvedValue(undefined); }); @@ -111,34 +105,46 @@ describe("ContentMessages", () => { mocked(client.uploadContent).mockResolvedValue({ content_uri: "mxc://server/file" }); const file = new File([], "fileName", { type: "image/jpeg" }); await contentMessages.sendContentToRoom(file, roomId, undefined, client, undefined); - expect(client.sendMessage).toHaveBeenCalledWith(roomId, null, expect.objectContaining({ - url: "mxc://server/file", - msgtype: "m.image", - })); + expect(client.sendMessage).toHaveBeenCalledWith( + roomId, + null, + expect.objectContaining({ + url: "mxc://server/file", + msgtype: "m.image", + }), + ); }); it("should fall back to m.file for invalid image files", async () => { mocked(client.uploadContent).mockResolvedValue({ content_uri: "mxc://server/file" }); const file = new File([], "fileName", { type: "image/png" }); await contentMessages.sendContentToRoom(file, roomId, undefined, client, undefined); - expect(client.sendMessage).toHaveBeenCalledWith(roomId, null, expect.objectContaining({ - url: "mxc://server/file", - msgtype: "m.file", - })); + expect(client.sendMessage).toHaveBeenCalledWith( + roomId, + null, + expect.objectContaining({ + url: "mxc://server/file", + msgtype: "m.file", + }), + ); }); it("should use m.video for video files", async () => { - jest.spyOn(document, "createElement").mockImplementation(tagName => { + jest.spyOn(document, "createElement").mockImplementation((tagName) => { const element = createElement(tagName); if (tagName === "video") { - element.load = jest.fn(); - element.play = () => element.onloadeddata(new Event("loadeddata")); - element.pause = jest.fn(); - Object.defineProperty(element, 'videoHeight', { - get() { return 600; }, + (element).load = jest.fn(); + (element).play = () => element.onloadeddata(new Event("loadeddata")); + (element).pause = jest.fn(); + Object.defineProperty(element, "videoHeight", { + get() { + return 600; + }, }); - Object.defineProperty(element, 'videoWidth', { - get() { return 800; }, + Object.defineProperty(element, "videoWidth", { + get() { + return 800; + }, }); } return element; @@ -147,31 +153,43 @@ describe("ContentMessages", () => { mocked(client.uploadContent).mockResolvedValue({ content_uri: "mxc://server/file" }); const file = new File([], "fileName", { type: "video/mp4" }); await contentMessages.sendContentToRoom(file, roomId, undefined, client, undefined); - expect(client.sendMessage).toHaveBeenCalledWith(roomId, null, expect.objectContaining({ - url: "mxc://server/file", - msgtype: "m.video", - })); + expect(client.sendMessage).toHaveBeenCalledWith( + roomId, + null, + expect.objectContaining({ + url: "mxc://server/file", + msgtype: "m.video", + }), + ); }); it("should use m.audio for audio files", async () => { mocked(client.uploadContent).mockResolvedValue({ content_uri: "mxc://server/file" }); const file = new File([], "fileName", { type: "audio/mp3" }); await contentMessages.sendContentToRoom(file, roomId, undefined, client, undefined); - expect(client.sendMessage).toHaveBeenCalledWith(roomId, null, expect.objectContaining({ - url: "mxc://server/file", - msgtype: "m.audio", - })); + expect(client.sendMessage).toHaveBeenCalledWith( + roomId, + null, + expect.objectContaining({ + url: "mxc://server/file", + msgtype: "m.audio", + }), + ); }); it("should default to name 'Attachment' if file doesn't have a name", async () => { mocked(client.uploadContent).mockResolvedValue({ content_uri: "mxc://server/file" }); const file = new File([], "", { type: "text/plain" }); await contentMessages.sendContentToRoom(file, roomId, undefined, client, undefined); - expect(client.sendMessage).toHaveBeenCalledWith(roomId, null, expect.objectContaining({ - url: "mxc://server/file", - msgtype: "m.file", - body: "Attachment", - })); + expect(client.sendMessage).toHaveBeenCalledWith( + roomId, + null, + expect.objectContaining({ + url: "mxc://server/file", + msgtype: "m.file", + body: "Attachment", + }), + ); }); it("should keep RoomUpload's total and loaded values up to date", async () => { @@ -196,10 +214,9 @@ describe("ContentMessages", () => { const roomId = "!roomId:server"; beforeEach(() => { - mocked(doMaybeLocalRoomAction).mockImplementation(( - roomId: string, - fn: (actualRoomId: string) => Promise, - ) => fn(roomId)); + mocked(doMaybeLocalRoomAction).mockImplementation( + (roomId: string, fn: (actualRoomId: string) => Promise) => fn(roomId), + ); }); it("should return only uploads for the given relation", async () => { @@ -284,14 +301,19 @@ describe("uploadFile", () => { const res = await uploadFile(client, "!roomId:server", file, progressHandler); expect(res.url).toBeFalsy(); - expect(res.file).toEqual(expect.objectContaining({ - url: "mxc://server/file", - })); + expect(res.file).toEqual( + expect.objectContaining({ + url: "mxc://server/file", + }), + ); expect(encrypt.encryptAttachment).toHaveBeenCalled(); - expect(client.uploadContent).toHaveBeenCalledWith(expect.any(Blob), expect.objectContaining({ - progressHandler, - includeFilename: false, - })); + expect(client.uploadContent).toHaveBeenCalledWith( + expect.any(Blob), + expect.objectContaining({ + progressHandler, + includeFilename: false, + }), + ); expect(mocked(client.uploadContent).mock.calls[0][0]).not.toBe(file); }); diff --git a/test/DecryptionFailureTracker-test.js b/test/DecryptionFailureTracker-test.js index 8572365f08a..14f3a67b618 100644 --- a/test/DecryptionFailureTracker-test.js +++ b/test/DecryptionFailureTracker-test.js @@ -14,15 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixEvent } from 'matrix-js-sdk/src/matrix'; +import { MatrixEvent } from "matrix-js-sdk/src/matrix"; -import { DecryptionFailureTracker } from '../src/DecryptionFailureTracker'; +import { DecryptionFailureTracker } from "../src/DecryptionFailureTracker"; class MockDecryptionError extends Error { constructor(code) { super(); - this.code = code || 'MOCK_DECRYPTION_ERROR'; + this.code = code || "MOCK_DECRYPTION_ERROR"; } } @@ -37,12 +37,15 @@ function createFailedDecryptionEvent() { return event; } -describe('DecryptionFailureTracker', function() { - it('tracks a failed decryption for a visible event', function(done) { +describe("DecryptionFailureTracker", function () { + it("tracks a failed decryption for a visible event", function (done) { const failedDecryptionEvent = createFailedDecryptionEvent(); let count = 0; - const tracker = new DecryptionFailureTracker((total) => count += total, () => "UnknownError"); + const tracker = new DecryptionFailureTracker( + (total) => (count += total), + () => "UnknownError", + ); tracker.addVisibleEvent(failedDecryptionEvent); @@ -55,24 +58,27 @@ describe('DecryptionFailureTracker', function() { // Immediately track the newest failures tracker.trackFailures(); - expect(count).not.toBe(0, 'should track a failure for an event that failed decryption'); + expect(count).not.toBe(0, "should track a failure for an event that failed decryption"); done(); }); - it('tracks a failed decryption with expected raw error for a visible event', function(done) { + it("tracks a failed decryption with expected raw error for a visible event", function (done) { const failedDecryptionEvent = createFailedDecryptionEvent(); let count = 0; let reportedRawCode = ""; - const tracker = new DecryptionFailureTracker((total, errcode, rawCode) => { - count += total; - reportedRawCode = rawCode; - }, () => "UnknownError"); + const tracker = new DecryptionFailureTracker( + (total, errcode, rawCode) => { + count += total; + reportedRawCode = rawCode; + }, + () => "UnknownError", + ); tracker.addVisibleEvent(failedDecryptionEvent); - const err = new MockDecryptionError('INBOUND_SESSION_MISMATCH_ROOM_ID'); + const err = new MockDecryptionError("INBOUND_SESSION_MISMATCH_ROOM_ID"); tracker.eventDecrypted(failedDecryptionEvent, err); // Pretend "now" is Infinity @@ -81,17 +87,20 @@ describe('DecryptionFailureTracker', function() { // Immediately track the newest failures tracker.trackFailures(); - expect(count).not.toBe(0, 'should track a failure for an event that failed decryption'); - expect(reportedRawCode).toBe('INBOUND_SESSION_MISMATCH_ROOM_ID', 'Should add the rawCode to the event context'); + expect(count).not.toBe(0, "should track a failure for an event that failed decryption"); + expect(reportedRawCode).toBe("INBOUND_SESSION_MISMATCH_ROOM_ID", "Should add the rawCode to the event context"); done(); }); - it('tracks a failed decryption for an event that becomes visible later', function(done) { + it("tracks a failed decryption for an event that becomes visible later", function (done) { const failedDecryptionEvent = createFailedDecryptionEvent(); let count = 0; - const tracker = new DecryptionFailureTracker((total) => count += total, () => "UnknownError"); + const tracker = new DecryptionFailureTracker( + (total) => (count += total), + () => "UnknownError", + ); const err = new MockDecryptionError(); tracker.eventDecrypted(failedDecryptionEvent, err); @@ -104,16 +113,19 @@ describe('DecryptionFailureTracker', function() { // Immediately track the newest failures tracker.trackFailures(); - expect(count).not.toBe(0, 'should track a failure for an event that failed decryption'); + expect(count).not.toBe(0, "should track a failure for an event that failed decryption"); done(); }); - it('does not track a failed decryption for an event that never becomes visible', function(done) { + it("does not track a failed decryption for an event that never becomes visible", function (done) { const failedDecryptionEvent = createFailedDecryptionEvent(); let count = 0; - const tracker = new DecryptionFailureTracker((total) => count += total, () => "UnknownError"); + const tracker = new DecryptionFailureTracker( + (total) => (count += total), + () => "UnknownError", + ); const err = new MockDecryptionError(); tracker.eventDecrypted(failedDecryptionEvent, err); @@ -124,16 +136,19 @@ describe('DecryptionFailureTracker', function() { // Immediately track the newest failures tracker.trackFailures(); - expect(count).toBe(0, 'should not track a failure for an event that never became visible'); + expect(count).toBe(0, "should not track a failure for an event that never became visible"); done(); }); - it('does not track a failed decryption where the event is subsequently successfully decrypted', (done) => { + it("does not track a failed decryption where the event is subsequently successfully decrypted", (done) => { const decryptedEvent = createFailedDecryptionEvent(); - const tracker = new DecryptionFailureTracker((total) => { - expect(true).toBe(false, 'should not track an event that has since been decrypted correctly'); - }, () => "UnknownError"); + const tracker = new DecryptionFailureTracker( + (total) => { + expect(true).toBe(false, "should not track an event that has since been decrypted correctly"); + }, + () => "UnknownError", + ); tracker.addVisibleEvent(decryptedEvent); @@ -152,36 +167,45 @@ describe('DecryptionFailureTracker', function() { done(); }); - it('does not track a failed decryption where the event is subsequently successfully decrypted ' + - 'and later becomes visible', (done) => { - const decryptedEvent = createFailedDecryptionEvent(); - const tracker = new DecryptionFailureTracker((total) => { - expect(true).toBe(false, 'should not track an event that has since been decrypted correctly'); - }, () => "UnknownError"); - - const err = new MockDecryptionError(); - tracker.eventDecrypted(decryptedEvent, err); - - // Indicate successful decryption: clear data can be anything where the msgtype is not m.bad.encrypted - decryptedEvent.setClearData({}); - tracker.eventDecrypted(decryptedEvent, null); - - tracker.addVisibleEvent(decryptedEvent); - - // Pretend "now" is Infinity - tracker.checkFailures(Infinity); - - // Immediately track the newest failures - tracker.trackFailures(); - done(); - }); + it( + "does not track a failed decryption where the event is subsequently successfully decrypted " + + "and later becomes visible", + (done) => { + const decryptedEvent = createFailedDecryptionEvent(); + const tracker = new DecryptionFailureTracker( + (total) => { + expect(true).toBe(false, "should not track an event that has since been decrypted correctly"); + }, + () => "UnknownError", + ); + + const err = new MockDecryptionError(); + tracker.eventDecrypted(decryptedEvent, err); + + // Indicate successful decryption: clear data can be anything where the msgtype is not m.bad.encrypted + decryptedEvent.setClearData({}); + tracker.eventDecrypted(decryptedEvent, null); + + tracker.addVisibleEvent(decryptedEvent); + + // Pretend "now" is Infinity + tracker.checkFailures(Infinity); + + // Immediately track the newest failures + tracker.trackFailures(); + done(); + }, + ); - it('only tracks a single failure per event, despite multiple failed decryptions for multiple events', (done) => { + it("only tracks a single failure per event, despite multiple failed decryptions for multiple events", (done) => { const decryptedEvent = createFailedDecryptionEvent(); const decryptedEvent2 = createFailedDecryptionEvent(); let count = 0; - const tracker = new DecryptionFailureTracker((total) => count += total, () => "UnknownError"); + const tracker = new DecryptionFailureTracker( + (total) => (count += total), + () => "UnknownError", + ); tracker.addVisibleEvent(decryptedEvent); @@ -206,16 +230,19 @@ describe('DecryptionFailureTracker', function() { tracker.trackFailures(); tracker.trackFailures(); - expect(count).toBe(2, count + ' failures tracked, should only track a single failure per event'); + expect(count).toBe(2, count + " failures tracked, should only track a single failure per event"); done(); }); - it('should not track a failure for an event that was tracked previously', (done) => { + it("should not track a failure for an event that was tracked previously", (done) => { const decryptedEvent = createFailedDecryptionEvent(); let count = 0; - const tracker = new DecryptionFailureTracker((total) => count += total, () => "UnknownError"); + const tracker = new DecryptionFailureTracker( + (total) => (count += total), + () => "UnknownError", + ); tracker.addVisibleEvent(decryptedEvent); @@ -233,19 +260,22 @@ describe('DecryptionFailureTracker', function() { tracker.trackFailures(); - expect(count).toBe(1, 'should only track a single failure per event'); + expect(count).toBe(1, "should only track a single failure per event"); done(); }); - xit('should not track a failure for an event that was tracked in a previous session', (done) => { + xit("should not track a failure for an event that was tracked in a previous session", (done) => { // This test uses localStorage, clear it beforehand localStorage.clear(); const decryptedEvent = createFailedDecryptionEvent(); let count = 0; - const tracker = new DecryptionFailureTracker((total) => count += total, () => "UnknownError"); + const tracker = new DecryptionFailureTracker( + (total) => (count += total), + () => "UnknownError", + ); tracker.addVisibleEvent(decryptedEvent); @@ -260,7 +290,10 @@ describe('DecryptionFailureTracker', function() { tracker.trackFailures(); // Simulate the browser refreshing by destroying tracker and creating a new tracker - const secondTracker = new DecryptionFailureTracker((total) => count += total, () => "UnknownError"); + const secondTracker = new DecryptionFailureTracker( + (total) => (count += total), + () => "UnknownError", + ); secondTracker.addVisibleEvent(decryptedEvent); @@ -270,24 +303,24 @@ describe('DecryptionFailureTracker', function() { secondTracker.checkFailures(Infinity); secondTracker.trackFailures(); - expect(count).toBe(1, count + ' failures tracked, should only track a single failure per event'); + expect(count).toBe(1, count + " failures tracked, should only track a single failure per event"); done(); }); - it('should count different error codes separately for multiple failures with different error codes', () => { + it("should count different error codes separately for multiple failures with different error codes", () => { const counts = {}; const tracker = new DecryptionFailureTracker( - (total, errorCode) => counts[errorCode] = (counts[errorCode] || 0) + total, - (error) => error === "UnknownError" ? "UnknownError" : "OlmKeysNotSentError", + (total, errorCode) => (counts[errorCode] = (counts[errorCode] || 0) + total), + (error) => (error === "UnknownError" ? "UnknownError" : "OlmKeysNotSentError"), ); const decryptedEvent1 = createFailedDecryptionEvent(); const decryptedEvent2 = createFailedDecryptionEvent(); const decryptedEvent3 = createFailedDecryptionEvent(); - const error1 = new MockDecryptionError('UnknownError'); - const error2 = new MockDecryptionError('OlmKeysNotSentError'); + const error1 = new MockDecryptionError("UnknownError"); + const error2 = new MockDecryptionError("OlmKeysNotSentError"); tracker.addVisibleEvent(decryptedEvent1); tracker.addVisibleEvent(decryptedEvent2); @@ -305,23 +338,23 @@ describe('DecryptionFailureTracker', function() { tracker.trackFailures(); //expect(counts['UnknownError']).toBe(1, 'should track one UnknownError'); - expect(counts['OlmKeysNotSentError']).toBe(2, 'should track two OlmKeysNotSentError'); + expect(counts["OlmKeysNotSentError"]).toBe(2, "should track two OlmKeysNotSentError"); }); - it('should aggregate error codes correctly', () => { + it("should aggregate error codes correctly", () => { const counts = {}; const tracker = new DecryptionFailureTracker( - (total, errorCode) => counts[errorCode] = (counts[errorCode] || 0) + total, - (errorCode) => 'OlmUnspecifiedError', + (total, errorCode) => (counts[errorCode] = (counts[errorCode] || 0) + total), + (errorCode) => "OlmUnspecifiedError", ); const decryptedEvent1 = createFailedDecryptionEvent(); const decryptedEvent2 = createFailedDecryptionEvent(); const decryptedEvent3 = createFailedDecryptionEvent(); - const error1 = new MockDecryptionError('ERROR_CODE_1'); - const error2 = new MockDecryptionError('ERROR_CODE_2'); - const error3 = new MockDecryptionError('ERROR_CODE_3'); + const error1 = new MockDecryptionError("ERROR_CODE_1"); + const error2 = new MockDecryptionError("ERROR_CODE_2"); + const error3 = new MockDecryptionError("ERROR_CODE_3"); tracker.addVisibleEvent(decryptedEvent1); tracker.addVisibleEvent(decryptedEvent2); @@ -336,20 +369,22 @@ describe('DecryptionFailureTracker', function() { tracker.trackFailures(); - expect(counts['OlmUnspecifiedError']) - .toBe(3, 'should track three OlmUnspecifiedError, got ' + counts['OlmUnspecifiedError']); + expect(counts["OlmUnspecifiedError"]).toBe( + 3, + "should track three OlmUnspecifiedError, got " + counts["OlmUnspecifiedError"], + ); }); - it('should remap error codes correctly', () => { + it("should remap error codes correctly", () => { const counts = {}; const tracker = new DecryptionFailureTracker( - (total, errorCode) => counts[errorCode] = (counts[errorCode] || 0) + total, - (errorCode) => Array.from(errorCode).reverse().join(''), + (total, errorCode) => (counts[errorCode] = (counts[errorCode] || 0) + total), + (errorCode) => Array.from(errorCode).reverse().join(""), ); const decryptedEvent = createFailedDecryptionEvent(); - const error = new MockDecryptionError('ERROR_CODE_1'); + const error = new MockDecryptionError("ERROR_CODE_1"); tracker.addVisibleEvent(decryptedEvent); @@ -360,7 +395,6 @@ describe('DecryptionFailureTracker', function() { tracker.trackFailures(); - expect(counts['1_EDOC_RORRE']) - .toBe(1, 'should track remapped error code'); + expect(counts["1_EDOC_RORRE"]).toBe(1, "should track remapped error code"); }); }); diff --git a/test/DeviceListener-test.ts b/test/DeviceListener-test.ts index 8d0dd48570e..dea4ab19872 100644 --- a/test/DeviceListener-test.ts +++ b/test/DeviceListener-test.ts @@ -1,4 +1,3 @@ - /* Copyright 2022 The Matrix.org Foundation C.I.C. @@ -15,10 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { EventEmitter } from "events"; -import { mocked } from "jest-mock"; -import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; +import { Mocked, mocked } from "jest-mock"; +import { MatrixEvent, Room, MatrixClient } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; +import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo"; +import { CrossSigningInfo, DeviceTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning"; +import { CryptoEvent } from "matrix-js-sdk/src/crypto"; +import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup"; import DeviceListener from "../src/DeviceListener"; import { MatrixClientPeg } from "../src/MatrixClientPeg"; @@ -30,7 +32,9 @@ import dis from "../src/dispatcher/dispatcher"; import { Action } from "../src/dispatcher/actions"; import SettingsStore from "../src/settings/SettingsStore"; import { SettingLevel } from "../src/settings/SettingLevel"; -import { mockPlatformPeg } from "./test-utils"; +import { getMockClientWithEventEmitter, mockPlatformPeg } from "./test-utils"; +import { UIFeature } from "../src/settings/UIFeature"; +import { isBulkUnverifiedDeviceReminderSnoozed } from "../src/utils/device/snoozeBulkUnverifiedDeviceReminder"; // don't litter test console with logs jest.mock("matrix-js-sdk/src/logger"); @@ -41,56 +45,63 @@ jest.mock("../src/dispatcher/dispatcher", () => ({ })); jest.mock("../src/SecurityManager", () => ({ - isSecretStorageBeingAccessed: jest.fn(), accessSecretStorage: jest.fn(), + isSecretStorageBeingAccessed: jest.fn(), + accessSecretStorage: jest.fn(), +})); + +jest.mock("../src/utils/device/snoozeBulkUnverifiedDeviceReminder", () => ({ + isBulkUnverifiedDeviceReminderSnoozed: jest.fn(), })); -const deviceId = 'my-device-id'; - -class MockClient extends EventEmitter { - isGuest = jest.fn(); - getUserId = jest.fn(); - getKeyBackupVersion = jest.fn().mockResolvedValue(undefined); - getRooms = jest.fn().mockReturnValue([]); - doesServerSupportUnstableFeature = jest.fn().mockResolvedValue(true); - isCrossSigningReady = jest.fn().mockResolvedValue(true); - isSecretStorageReady = jest.fn().mockResolvedValue(true); - isCryptoEnabled = jest.fn().mockReturnValue(true); - isInitialSyncComplete = jest.fn().mockReturnValue(true); - getKeyBackupEnabled = jest.fn(); - getStoredDevicesForUser = jest.fn().mockReturnValue([]); - getCrossSigningId = jest.fn(); - getStoredCrossSigningForUser = jest.fn(); - waitForClientWellKnown = jest.fn(); - downloadKeys = jest.fn(); - isRoomEncrypted = jest.fn(); - getClientWellKnown = jest.fn(); - getDeviceId = jest.fn().mockReturnValue(deviceId); - setAccountData = jest.fn(); - getAccountData = jest.fn(); -} +const userId = "@user:server"; +const deviceId = "my-device-id"; const mockDispatcher = mocked(dis); const flushPromises = async () => await new Promise(process.nextTick); -describe('DeviceListener', () => { - let mockClient; +describe("DeviceListener", () => { + let mockClient: Mocked | undefined; // spy on various toasts' hide and show functions // easier than mocking - jest.spyOn(SetupEncryptionToast, 'showToast'); - jest.spyOn(SetupEncryptionToast, 'hideToast'); - jest.spyOn(BulkUnverifiedSessionsToast, 'showToast'); - jest.spyOn(BulkUnverifiedSessionsToast, 'hideToast'); - jest.spyOn(UnverifiedSessionToast, 'showToast'); - jest.spyOn(UnverifiedSessionToast, 'hideToast'); + jest.spyOn(SetupEncryptionToast, "showToast"); + jest.spyOn(SetupEncryptionToast, "hideToast"); + jest.spyOn(BulkUnverifiedSessionsToast, "showToast"); + jest.spyOn(BulkUnverifiedSessionsToast, "hideToast"); + jest.spyOn(UnverifiedSessionToast, "showToast"); + jest.spyOn(UnverifiedSessionToast, "hideToast"); beforeEach(() => { jest.resetAllMocks(); mockPlatformPeg({ - getAppVersion: jest.fn().mockResolvedValue('1.2.3'), + getAppVersion: jest.fn().mockResolvedValue("1.2.3"), }); - mockClient = new MockClient(); - jest.spyOn(MatrixClientPeg, 'get').mockReturnValue(mockClient); - jest.spyOn(SettingsStore, 'getValue').mockReturnValue(false); + mockClient = getMockClientWithEventEmitter({ + isGuest: jest.fn(), + getUserId: jest.fn().mockReturnValue(userId), + getKeyBackupVersion: jest.fn().mockResolvedValue(undefined), + getRooms: jest.fn().mockReturnValue([]), + doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(true), + isCrossSigningReady: jest.fn().mockResolvedValue(true), + isSecretStorageReady: jest.fn().mockResolvedValue(true), + isCryptoEnabled: jest.fn().mockReturnValue(true), + isInitialSyncComplete: jest.fn().mockReturnValue(true), + getKeyBackupEnabled: jest.fn(), + getStoredDevicesForUser: jest.fn().mockReturnValue([]), + getCrossSigningId: jest.fn(), + getStoredCrossSigningForUser: jest.fn(), + waitForClientWellKnown: jest.fn(), + downloadKeys: jest.fn(), + isRoomEncrypted: jest.fn(), + getClientWellKnown: jest.fn(), + getDeviceId: jest.fn().mockReturnValue(deviceId), + setAccountData: jest.fn(), + getAccountData: jest.fn(), + deleteAccountData: jest.fn(), + checkDeviceTrust: jest.fn().mockReturnValue(new DeviceTrustLevel(false, false, false, false)), + }); + jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient); + jest.spyOn(SettingsStore, "getValue").mockReturnValue(false); + mocked(isBulkUnverifiedDeviceReminderSnoozed).mockClear().mockReturnValue(false); }); const createAndStart = async (): Promise => { @@ -100,102 +111,97 @@ describe('DeviceListener', () => { return instance; }; - describe('client information', () => { - it('watches device client information setting', async () => { - const watchSettingSpy = jest.spyOn(SettingsStore, 'watchSetting'); - const unwatchSettingSpy = jest.spyOn(SettingsStore, 'unwatchSetting'); + describe("client information", () => { + it("watches device client information setting", async () => { + const watchSettingSpy = jest.spyOn(SettingsStore, "watchSetting"); + const unwatchSettingSpy = jest.spyOn(SettingsStore, "unwatchSetting"); const deviceListener = await createAndStart(); - expect(watchSettingSpy).toHaveBeenCalledWith( - 'deviceClientInformationOptIn', null, expect.any(Function), - ); + expect(watchSettingSpy).toHaveBeenCalledWith("deviceClientInformationOptIn", null, expect.any(Function)); deviceListener.stop(); expect(unwatchSettingSpy).toHaveBeenCalled(); }); - describe('when device client information feature is enabled', () => { + describe("when device client information feature is enabled", () => { beforeEach(() => { - jest.spyOn(SettingsStore, 'getValue').mockImplementation( - settingName => settingName === 'deviceClientInformationOptIn', + jest.spyOn(SettingsStore, "getValue").mockImplementation( + (settingName) => settingName === "deviceClientInformationOptIn", ); }); - it('saves client information on start', async () => { + it("saves client information on start", async () => { await createAndStart(); - expect(mockClient.setAccountData).toHaveBeenCalledWith( + expect(mockClient!.setAccountData).toHaveBeenCalledWith( `io.element.matrix_client_information.${deviceId}`, - { name: 'Element', url: 'localhost', version: '1.2.3' }, + { name: "Element", url: "localhost", version: "1.2.3" }, ); }); - it('catches error and logs when saving client information fails', async () => { - const errorLogSpy = jest.spyOn(logger, 'error'); - const error = new Error('oups'); - mockClient.setAccountData.mockRejectedValue(error); + it("catches error and logs when saving client information fails", async () => { + const errorLogSpy = jest.spyOn(logger, "error"); + const error = new Error("oups"); + mockClient!.setAccountData.mockRejectedValue(error); // doesn't throw await createAndStart(); - expect(errorLogSpy).toHaveBeenCalledWith( - 'Failed to update client information', - error, - ); + expect(errorLogSpy).toHaveBeenCalledWith("Failed to update client information", error); }); - it('saves client information on logged in action', async () => { + it("saves client information on logged in action", async () => { const instance = await createAndStart(); - mockClient.setAccountData.mockClear(); + mockClient!.setAccountData.mockClear(); // @ts-ignore calling private function instance.onAction({ action: Action.OnLoggedIn }); await flushPromises(); - expect(mockClient.setAccountData).toHaveBeenCalledWith( + expect(mockClient!.setAccountData).toHaveBeenCalledWith( `io.element.matrix_client_information.${deviceId}`, - { name: 'Element', url: 'localhost', version: '1.2.3' }, + { name: "Element", url: "localhost", version: "1.2.3" }, ); }); }); - describe('when device client information feature is disabled', () => { - const clientInfoEvent = new MatrixEvent({ type: `io.element.matrix_client_information.${deviceId}`, - content: { name: 'hello' }, + describe("when device client information feature is disabled", () => { + const clientInfoEvent = new MatrixEvent({ + type: `io.element.matrix_client_information.${deviceId}`, + content: { name: "hello" }, }); const emptyClientInfoEvent = new MatrixEvent({ type: `io.element.matrix_client_information.${deviceId}` }); beforeEach(() => { - jest.spyOn(SettingsStore, 'getValue').mockReturnValue(false); + jest.spyOn(SettingsStore, "getValue").mockReturnValue(false); - mockClient.getAccountData.mockReturnValue(undefined); + mockClient!.getAccountData.mockReturnValue(undefined); }); - it('does not save client information on start', async () => { + it("does not save client information on start", async () => { await createAndStart(); - expect(mockClient.setAccountData).not.toHaveBeenCalled(); + expect(mockClient!.setAccountData).not.toHaveBeenCalled(); }); - it('removes client information on start if it exists', async () => { - mockClient.getAccountData.mockReturnValue(clientInfoEvent); + it("removes client information on start if it exists", async () => { + mockClient!.getAccountData.mockReturnValue(clientInfoEvent); await createAndStart(); - expect(mockClient.setAccountData).toHaveBeenCalledWith( + expect(mockClient!.deleteAccountData).toHaveBeenCalledWith( `io.element.matrix_client_information.${deviceId}`, - {}, ); }); - it('does not try to remove client info event that are already empty', async () => { - mockClient.getAccountData.mockReturnValue(emptyClientInfoEvent); + it("does not try to remove client info event that are already empty", async () => { + mockClient!.getAccountData.mockReturnValue(emptyClientInfoEvent); await createAndStart(); - expect(mockClient.setAccountData).not.toHaveBeenCalled(); + expect(mockClient!.deleteAccountData).not.toHaveBeenCalled(); }); - it('does not save client information on logged in action', async () => { + it("does not save client information on logged in action", async () => { const instance = await createAndStart(); // @ts-ignore calling private function @@ -203,180 +209,332 @@ describe('DeviceListener', () => { await flushPromises(); - expect(mockClient.setAccountData).not.toHaveBeenCalled(); + expect(mockClient!.setAccountData).not.toHaveBeenCalled(); }); - it('saves client information after setting is enabled', async () => { - const watchSettingSpy = jest.spyOn(SettingsStore, 'watchSetting'); + it("saves client information after setting is enabled", async () => { + const watchSettingSpy = jest.spyOn(SettingsStore, "watchSetting"); await createAndStart(); const [settingName, roomId, callback] = watchSettingSpy.mock.calls[0]; - expect(settingName).toEqual('deviceClientInformationOptIn'); + expect(settingName).toEqual("deviceClientInformationOptIn"); expect(roomId).toBeNull(); - callback('deviceClientInformationOptIn', null, SettingLevel.DEVICE, SettingLevel.DEVICE, true); + callback("deviceClientInformationOptIn", null, SettingLevel.DEVICE, SettingLevel.DEVICE, true); await flushPromises(); - expect(mockClient.setAccountData).toHaveBeenCalledWith( + expect(mockClient!.setAccountData).toHaveBeenCalledWith( `io.element.matrix_client_information.${deviceId}`, - { name: 'Element', url: 'localhost', version: '1.2.3' }, + { name: "Element", url: "localhost", version: "1.2.3" }, ); }); }); }); - describe('recheck', () => { - it('does nothing when cross signing feature is not supported', async () => { - mockClient.doesServerSupportUnstableFeature.mockResolvedValue(false); + describe("recheck", () => { + it("does nothing when cross signing feature is not supported", async () => { + mockClient!.doesServerSupportUnstableFeature.mockResolvedValue(false); await createAndStart(); - expect(mockClient.isCrossSigningReady).not.toHaveBeenCalled(); + expect(mockClient!.isCrossSigningReady).not.toHaveBeenCalled(); }); - it('does nothing when crypto is not enabled', async () => { - mockClient.isCryptoEnabled.mockReturnValue(false); + it("does nothing when crypto is not enabled", async () => { + mockClient!.isCryptoEnabled.mockReturnValue(false); await createAndStart(); - expect(mockClient.isCrossSigningReady).not.toHaveBeenCalled(); + expect(mockClient!.isCrossSigningReady).not.toHaveBeenCalled(); }); - it('does nothing when initial sync is not complete', async () => { - mockClient.isInitialSyncComplete.mockReturnValue(false); + it("does nothing when initial sync is not complete", async () => { + mockClient!.isInitialSyncComplete.mockReturnValue(false); await createAndStart(); - expect(mockClient.isCrossSigningReady).not.toHaveBeenCalled(); + expect(mockClient!.isCrossSigningReady).not.toHaveBeenCalled(); }); - describe('set up encryption', () => { - const rooms = [ - { roomId: '!room1' }, - { roomId: '!room2' }, - ] as unknown as Room[]; + describe("set up encryption", () => { + const rooms = [{ roomId: "!room1" }, { roomId: "!room2" }] as unknown as Room[]; beforeEach(() => { - mockClient.isCrossSigningReady.mockResolvedValue(false); - mockClient.isSecretStorageReady.mockResolvedValue(false); - mockClient.getRooms.mockReturnValue(rooms); - mockClient.isRoomEncrypted.mockReturnValue(true); + mockClient!.isCrossSigningReady.mockResolvedValue(false); + mockClient!.isSecretStorageReady.mockResolvedValue(false); + mockClient!.getRooms.mockReturnValue(rooms); + mockClient!.isRoomEncrypted.mockReturnValue(true); }); - it('hides setup encryption toast when cross signing and secret storage are ready', async () => { - mockClient.isCrossSigningReady.mockResolvedValue(true); - mockClient.isSecretStorageReady.mockResolvedValue(true); + it("hides setup encryption toast when cross signing and secret storage are ready", async () => { + mockClient!.isCrossSigningReady.mockResolvedValue(true); + mockClient!.isSecretStorageReady.mockResolvedValue(true); await createAndStart(); expect(SetupEncryptionToast.hideToast).toHaveBeenCalled(); }); - it('hides setup encryption toast when it is dismissed', async () => { + it("hides setup encryption toast when it is dismissed", async () => { const instance = await createAndStart(); instance.dismissEncryptionSetup(); await flushPromises(); expect(SetupEncryptionToast.hideToast).toHaveBeenCalled(); }); - it('does not do any checks or show any toasts when secret storage is being accessed', async () => { + it("does not do any checks or show any toasts when secret storage is being accessed", async () => { mocked(isSecretStorageBeingAccessed).mockReturnValue(true); await createAndStart(); - expect(mockClient.downloadKeys).not.toHaveBeenCalled(); + expect(mockClient!.downloadKeys).not.toHaveBeenCalled(); expect(SetupEncryptionToast.showToast).not.toHaveBeenCalled(); }); - it('does not do any checks or show any toasts when no rooms are encrypted', async () => { - mockClient.isRoomEncrypted.mockReturnValue(false); + it("does not do any checks or show any toasts when no rooms are encrypted", async () => { + mockClient!.isRoomEncrypted.mockReturnValue(false); await createAndStart(); - expect(mockClient.downloadKeys).not.toHaveBeenCalled(); + expect(mockClient!.downloadKeys).not.toHaveBeenCalled(); expect(SetupEncryptionToast.showToast).not.toHaveBeenCalled(); }); - describe('when user does not have a cross signing id on this device', () => { + describe("when user does not have a cross signing id on this device", () => { beforeEach(() => { - mockClient.getCrossSigningId.mockReturnValue(undefined); + mockClient!.getCrossSigningId.mockReturnValue(null); }); - it('shows verify session toast when account has cross signing', async () => { - mockClient.getStoredCrossSigningForUser.mockReturnValue(true); + it("shows verify session toast when account has cross signing", async () => { + mockClient!.getStoredCrossSigningForUser.mockReturnValue(new CrossSigningInfo(userId)); await createAndStart(); - expect(mockClient.downloadKeys).toHaveBeenCalled(); + expect(mockClient!.downloadKeys).toHaveBeenCalled(); expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith( - SetupEncryptionToast.Kind.VERIFY_THIS_SESSION); + SetupEncryptionToast.Kind.VERIFY_THIS_SESSION, + ); }); - it('checks key backup status when when account has cross signing', async () => { - mockClient.getCrossSigningId.mockReturnValue(undefined); - mockClient.getStoredCrossSigningForUser.mockReturnValue(true); + it("checks key backup status when when account has cross signing", async () => { + mockClient!.getCrossSigningId.mockReturnValue(null); + mockClient!.getStoredCrossSigningForUser.mockReturnValue(new CrossSigningInfo(userId)); await createAndStart(); - expect(mockClient.getKeyBackupEnabled).toHaveBeenCalled(); + expect(mockClient!.getKeyBackupEnabled).toHaveBeenCalled(); }); }); - describe('when user does have a cross signing id on this device', () => { + describe("when user does have a cross signing id on this device", () => { beforeEach(() => { - mockClient.getCrossSigningId.mockReturnValue('abc'); + mockClient!.getCrossSigningId.mockReturnValue("abc"); }); - it('shows upgrade encryption toast when user has a key backup available', async () => { + it("shows upgrade encryption toast when user has a key backup available", async () => { // non falsy response - mockClient.getKeyBackupVersion.mockResolvedValue({}); + mockClient!.getKeyBackupVersion.mockResolvedValue({} as unknown as IKeyBackupInfo); await createAndStart(); expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith( - SetupEncryptionToast.Kind.UPGRADE_ENCRYPTION); + SetupEncryptionToast.Kind.UPGRADE_ENCRYPTION, + ); }); }); }); - describe('key backup status', () => { - it('checks keybackup status when cross signing and secret storage are ready', async () => { + describe("key backup status", () => { + it("checks keybackup status when cross signing and secret storage are ready", async () => { // default mocks set cross signing and secret storage to ready await createAndStart(); - expect(mockClient.getKeyBackupEnabled).toHaveBeenCalled(); + expect(mockClient!.getKeyBackupEnabled).toHaveBeenCalled(); expect(mockDispatcher.dispatch).not.toHaveBeenCalled(); }); - it('checks keybackup status when setup encryption toast has been dismissed', async () => { - mockClient.isCrossSigningReady.mockResolvedValue(false); + it("checks keybackup status when setup encryption toast has been dismissed", async () => { + mockClient!.isCrossSigningReady.mockResolvedValue(false); const instance = await createAndStart(); instance.dismissEncryptionSetup(); await flushPromises(); - expect(mockClient.getKeyBackupEnabled).toHaveBeenCalled(); + expect(mockClient!.getKeyBackupEnabled).toHaveBeenCalled(); }); - it('does not dispatch keybackup event when key backup check is not finished', async () => { + it("does not dispatch keybackup event when key backup check is not finished", async () => { // returns null when key backup status hasn't finished being checked - mockClient.getKeyBackupEnabled.mockReturnValue(null); + mockClient!.getKeyBackupEnabled.mockReturnValue(null); await createAndStart(); expect(mockDispatcher.dispatch).not.toHaveBeenCalled(); }); - it('dispatches keybackup event when key backup is not enabled', async () => { - mockClient.getKeyBackupEnabled.mockReturnValue(false); + it("dispatches keybackup event when key backup is not enabled", async () => { + mockClient!.getKeyBackupEnabled.mockReturnValue(false); await createAndStart(); expect(mockDispatcher.dispatch).toHaveBeenCalledWith({ action: Action.ReportKeyBackupNotEnabled }); }); - it('does not check key backup status again after check is complete', async () => { - mockClient.getKeyBackupEnabled.mockReturnValue(null); + it("does not check key backup status again after check is complete", async () => { + mockClient!.getKeyBackupEnabled.mockReturnValue(null); const instance = await createAndStart(); - expect(mockClient.getKeyBackupEnabled).toHaveBeenCalled(); + expect(mockClient!.getKeyBackupEnabled).toHaveBeenCalled(); // keyback check now complete - mockClient.getKeyBackupEnabled.mockReturnValue(true); + mockClient!.getKeyBackupEnabled.mockReturnValue(true); // trigger a recheck instance.dismissEncryptionSetup(); await flushPromises(); - expect(mockClient.getKeyBackupEnabled).toHaveBeenCalledTimes(2); + expect(mockClient!.getKeyBackupEnabled).toHaveBeenCalledTimes(2); // trigger another recheck instance.dismissEncryptionSetup(); await flushPromises(); // not called again, check was complete last time - expect(mockClient.getKeyBackupEnabled).toHaveBeenCalledTimes(2); + expect(mockClient!.getKeyBackupEnabled).toHaveBeenCalledTimes(2); + }); + }); + + describe("unverified sessions toasts", () => { + const currentDevice = new DeviceInfo(deviceId); + const device2 = new DeviceInfo("d2"); + const device3 = new DeviceInfo("d3"); + + const deviceTrustVerified = new DeviceTrustLevel(true, false, false, false); + const deviceTrustUnverified = new DeviceTrustLevel(false, false, false, false); + + beforeEach(() => { + mockClient!.isCrossSigningReady.mockResolvedValue(true); + mockClient!.getStoredDevicesForUser.mockReturnValue([currentDevice, device2, device3]); + // all devices verified by default + mockClient!.checkDeviceTrust.mockReturnValue(deviceTrustVerified); + mockClient!.deviceId = currentDevice.deviceId; + jest.spyOn(SettingsStore, "getValue").mockImplementation( + (settingName) => settingName === UIFeature.BulkUnverifiedSessionsReminder, + ); + }); + describe("bulk unverified sessions toasts", () => { + it("hides toast when cross signing is not ready", async () => { + mockClient!.isCrossSigningReady.mockResolvedValue(false); + await createAndStart(); + expect(BulkUnverifiedSessionsToast.hideToast).toHaveBeenCalled(); + expect(BulkUnverifiedSessionsToast.showToast).not.toHaveBeenCalled(); + }); + + it("hides toast when all devices at app start are verified", async () => { + await createAndStart(); + expect(BulkUnverifiedSessionsToast.hideToast).toHaveBeenCalled(); + expect(BulkUnverifiedSessionsToast.showToast).not.toHaveBeenCalled(); + }); + + it("hides toast when feature is disabled", async () => { + // BulkUnverifiedSessionsReminder set to false + jest.spyOn(SettingsStore, "getValue").mockReturnValue(false); + // currentDevice, device2 are verified, device3 is unverified + // ie if reminder was enabled it should be shown + mockClient!.checkDeviceTrust.mockImplementation((_userId, deviceId) => { + switch (deviceId) { + case currentDevice.deviceId: + case device2.deviceId: + return deviceTrustVerified; + default: + return deviceTrustUnverified; + } + }); + await createAndStart(); + expect(BulkUnverifiedSessionsToast.hideToast).toHaveBeenCalled(); + }); + + it("hides toast when current device is unverified", async () => { + // device2 verified, current and device3 unverified + mockClient!.checkDeviceTrust.mockImplementation((_userId, deviceId) => { + switch (deviceId) { + case device2.deviceId: + return deviceTrustVerified; + default: + return deviceTrustUnverified; + } + }); + await createAndStart(); + expect(BulkUnverifiedSessionsToast.hideToast).toHaveBeenCalled(); + expect(BulkUnverifiedSessionsToast.showToast).not.toHaveBeenCalled(); + }); + + it("hides toast when reminder is snoozed", async () => { + mocked(isBulkUnverifiedDeviceReminderSnoozed).mockReturnValue(true); + // currentDevice, device2 are verified, device3 is unverified + mockClient!.checkDeviceTrust.mockImplementation((_userId, deviceId) => { + switch (deviceId) { + case currentDevice.deviceId: + case device2.deviceId: + return deviceTrustVerified; + default: + return deviceTrustUnverified; + } + }); + await createAndStart(); + expect(BulkUnverifiedSessionsToast.showToast).not.toHaveBeenCalled(); + expect(BulkUnverifiedSessionsToast.hideToast).toHaveBeenCalled(); + }); + + it("shows toast with unverified devices at app start", async () => { + // currentDevice, device2 are verified, device3 is unverified + mockClient!.checkDeviceTrust.mockImplementation((_userId, deviceId) => { + switch (deviceId) { + case currentDevice.deviceId: + case device2.deviceId: + return deviceTrustVerified; + default: + return deviceTrustUnverified; + } + }); + await createAndStart(); + expect(BulkUnverifiedSessionsToast.showToast).toHaveBeenCalledWith( + new Set([device3.deviceId]), + ); + expect(BulkUnverifiedSessionsToast.hideToast).not.toHaveBeenCalled(); + }); + + it("hides toast when unverified sessions at app start have been dismissed", async () => { + // currentDevice, device2 are verified, device3 is unverified + mockClient!.checkDeviceTrust.mockImplementation((_userId, deviceId) => { + switch (deviceId) { + case currentDevice.deviceId: + case device2.deviceId: + return deviceTrustVerified; + default: + return deviceTrustUnverified; + } + }); + const instance = await createAndStart(); + expect(BulkUnverifiedSessionsToast.showToast).toHaveBeenCalledWith( + new Set([device3.deviceId]), + ); + + await instance.dismissUnverifiedSessions([device3.deviceId]); + await flushPromises(); + + expect(BulkUnverifiedSessionsToast.hideToast).toHaveBeenCalled(); + }); + + it("hides toast when unverified sessions are added after app start", async () => { + // currentDevice, device2 are verified, device3 is unverified + mockClient!.checkDeviceTrust.mockImplementation((_userId, deviceId) => { + switch (deviceId) { + case currentDevice.deviceId: + case device2.deviceId: + return deviceTrustVerified; + default: + return deviceTrustUnverified; + } + }); + mockClient!.getStoredDevicesForUser.mockReturnValue([currentDevice, device2]); + await createAndStart(); + + expect(BulkUnverifiedSessionsToast.hideToast).toHaveBeenCalled(); + + // add an unverified device + mockClient!.getStoredDevicesForUser.mockReturnValue([currentDevice, device2, device3]); + // trigger a recheck + mockClient!.emit(CryptoEvent.DevicesUpdated, [userId], false); + await flushPromises(); + + // bulk unverified sessions toast only shown for devices that were + // there at app start + // individual nags are shown for new unverified devices + expect(BulkUnverifiedSessionsToast.hideToast).toHaveBeenCalledTimes(2); + expect(BulkUnverifiedSessionsToast.showToast).not.toHaveBeenCalled(); + }); }); }); }); diff --git a/test/HtmlUtils-test.tsx b/test/HtmlUtils-test.tsx index c92a372a079..00b0564ff55 100644 --- a/test/HtmlUtils-test.tsx +++ b/test/HtmlUtils-test.tsx @@ -14,13 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React from "react"; // eslint-disable-next-line deprecate/import -import { mount } from 'enzyme'; -import { mocked } from 'jest-mock'; +import { mount } from "enzyme"; +import { mocked } from "jest-mock"; -import { topicToHtml } from '../src/HtmlUtils'; -import SettingsStore from '../src/settings/SettingsStore'; +import { topicToHtml } from "../src/HtmlUtils"; +import SettingsStore from "../src/settings/SettingsStore"; jest.mock("../src/settings/SettingsStore"); @@ -30,38 +30,39 @@ const enableHtmlTopicFeature = () => { }); }; -describe('HtmlUtils', () => { - it('converts plain text topic to HTML', () => { - const component = mount(
{ topicToHtml("pizza", null, null, false) }
); +describe("HtmlUtils", () => { + it("converts plain text topic to HTML", () => { + const component = mount(
{topicToHtml("pizza", null, null, false)}
); const wrapper = component.render(); expect(wrapper.children().first().html()).toEqual("pizza"); }); - it('converts plain text topic with emoji to HTML', () => { - const component = mount(
{ topicToHtml("pizza 🍕", null, null, false) }
); + it("converts plain text topic with emoji to HTML", () => { + const component = mount(
{topicToHtml("pizza 🍕", null, null, false)}
); const wrapper = component.render(); - expect(wrapper.children().first().html()).toEqual("pizza 🍕"); + expect(wrapper.children().first().html()).toEqual('pizza 🍕'); }); - it('converts literal HTML topic to HTML', async () => { + it("converts literal HTML topic to HTML", async () => { enableHtmlTopicFeature(); - const component = mount(
{ topicToHtml("pizza", null, null, false) }
); + const component = mount(
{topicToHtml("pizza", null, null, false)}
); const wrapper = component.render(); expect(wrapper.children().first().html()).toEqual("<b>pizza</b>"); }); - it('converts true HTML topic to HTML', async () => { + it("converts true HTML topic to HTML", async () => { enableHtmlTopicFeature(); - const component = mount(
{ topicToHtml("**pizza**", "pizza", null, false) }
); + const component = mount(
{topicToHtml("**pizza**", "pizza", null, false)}
); const wrapper = component.render(); expect(wrapper.children().first().html()).toEqual("pizza"); }); - it('converts true HTML topic with emoji to HTML', async () => { + it("converts true HTML topic with emoji to HTML", async () => { enableHtmlTopicFeature(); - const component = mount(
{ topicToHtml("**pizza** 🍕", "pizza 🍕", null, false) }
); + const component = mount(
{topicToHtml("**pizza** 🍕", "pizza 🍕", null, false)}
); const wrapper = component.render(); - expect(wrapper.children().first().html()) - .toEqual("pizza 🍕"); + expect(wrapper.children().first().html()).toEqual( + 'pizza 🍕', + ); }); }); diff --git a/test/KeyBindingsManager-test.ts b/test/KeyBindingsManager-test.ts index 7cf9f2dd3d6..1724307fbe4 100644 --- a/test/KeyBindingsManager-test.ts +++ b/test/KeyBindingsManager-test.ts @@ -14,14 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { isKeyComboMatch, KeyCombo } from '../src/KeyBindingsManager'; +import { isKeyComboMatch, KeyCombo } from "../src/KeyBindingsManager"; -function mockKeyEvent(key: string, modifiers?: { - ctrlKey?: boolean; - altKey?: boolean; - shiftKey?: boolean; - metaKey?: boolean; -}): KeyboardEvent { +function mockKeyEvent( + key: string, + modifiers?: { + ctrlKey?: boolean; + altKey?: boolean; + shiftKey?: boolean; + metaKey?: boolean; + }, +): KeyboardEvent { return { key, ctrlKey: modifiers?.ctrlKey ?? false, @@ -31,121 +34,140 @@ function mockKeyEvent(key: string, modifiers?: { } as KeyboardEvent; } -describe('KeyBindingsManager', () => { - it('should match basic key combo', () => { +describe("KeyBindingsManager", () => { + it("should match basic key combo", () => { const combo1: KeyCombo = { - key: 'k', + key: "k", }; - expect(isKeyComboMatch(mockKeyEvent('k'), combo1, false)).toBe(true); - expect(isKeyComboMatch(mockKeyEvent('n'), combo1, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent("k"), combo1, false)).toBe(true); + expect(isKeyComboMatch(mockKeyEvent("n"), combo1, false)).toBe(false); }); - it('should match key + modifier key combo', () => { + it("should match key + modifier key combo", () => { const combo: KeyCombo = { - key: 'k', + key: "k", ctrlKey: true, }; - expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, false)).toBe(true); - expect(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, false)).toBe(false); - expect(isKeyComboMatch(mockKeyEvent('k'), combo, false)).toBe(false); - expect(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true }), combo, false)).toBe(false); - expect(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true, metaKey: true }), combo, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent("k", { ctrlKey: true }), combo, false)).toBe(true); + expect(isKeyComboMatch(mockKeyEvent("n", { ctrlKey: true }), combo, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent("k"), combo, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent("k", { shiftKey: true }), combo, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent("k", { shiftKey: true, metaKey: true }), combo, false)).toBe(false); const combo2: KeyCombo = { - key: 'k', + key: "k", metaKey: true, }; - expect(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo2, false)).toBe(true); - expect(isKeyComboMatch(mockKeyEvent('n', { metaKey: true }), combo2, false)).toBe(false); - expect(isKeyComboMatch(mockKeyEvent('k'), combo2, false)).toBe(false); - expect(isKeyComboMatch(mockKeyEvent('k', { altKey: true, metaKey: true }), combo2, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent("k", { metaKey: true }), combo2, false)).toBe(true); + expect(isKeyComboMatch(mockKeyEvent("n", { metaKey: true }), combo2, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent("k"), combo2, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent("k", { altKey: true, metaKey: true }), combo2, false)).toBe(false); const combo3: KeyCombo = { - key: 'k', + key: "k", altKey: true, }; - expect(isKeyComboMatch(mockKeyEvent('k', { altKey: true }), combo3, false)).toBe(true); - expect(isKeyComboMatch(mockKeyEvent('n', { altKey: true }), combo3, false)).toBe(false); - expect(isKeyComboMatch(mockKeyEvent('k'), combo3, false)).toBe(false); - expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo3, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent("k", { altKey: true }), combo3, false)).toBe(true); + expect(isKeyComboMatch(mockKeyEvent("n", { altKey: true }), combo3, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent("k"), combo3, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent("k", { ctrlKey: true, metaKey: true }), combo3, false)).toBe(false); const combo4: KeyCombo = { - key: 'k', + key: "k", shiftKey: true, }; - expect(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true }), combo4, false)).toBe(true); - expect(isKeyComboMatch(mockKeyEvent('n', { shiftKey: true }), combo4, false)).toBe(false); - expect(isKeyComboMatch(mockKeyEvent('k'), combo4, false)).toBe(false); - expect(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true, ctrlKey: true }), combo4, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent("k", { shiftKey: true }), combo4, false)).toBe(true); + expect(isKeyComboMatch(mockKeyEvent("n", { shiftKey: true }), combo4, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent("k"), combo4, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent("k", { shiftKey: true, ctrlKey: true }), combo4, false)).toBe(false); }); - it('should match key + multiple modifiers key combo', () => { + it("should match key + multiple modifiers key combo", () => { const combo: KeyCombo = { - key: 'k', + key: "k", ctrlKey: true, altKey: true, }; - expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, false)).toBe(true); - expect(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true, altKey: true }), combo, false)).toBe(false); - expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo, false)).toBe(false); - expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true, shiftKey: true }), combo, - false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent("k", { ctrlKey: true, altKey: true }), combo, false)).toBe(true); + expect(isKeyComboMatch(mockKeyEvent("n", { ctrlKey: true, altKey: true }), combo, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent("k", { ctrlKey: true, metaKey: true }), combo, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent("k", { ctrlKey: true, metaKey: true, shiftKey: true }), combo, false)).toBe( + false, + ); const combo2: KeyCombo = { - key: 'k', + key: "k", ctrlKey: true, shiftKey: true, altKey: true, }; - expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, shiftKey: true, altKey: true }), combo2, - false)).toBe(true); - expect(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true, shiftKey: true, altKey: true }), combo2, - false)).toBe(false); - expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo2, false)).toBe(false); - expect(isKeyComboMatch(mockKeyEvent('k', - { ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo2, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent("k", { ctrlKey: true, shiftKey: true, altKey: true }), combo2, false)).toBe( + true, + ); + expect(isKeyComboMatch(mockKeyEvent("n", { ctrlKey: true, shiftKey: true, altKey: true }), combo2, false)).toBe( + false, + ); + expect(isKeyComboMatch(mockKeyEvent("k", { ctrlKey: true, metaKey: true }), combo2, false)).toBe(false); + expect( + isKeyComboMatch( + mockKeyEvent("k", { ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), + combo2, + false, + ), + ).toBe(false); const combo3: KeyCombo = { - key: 'k', + key: "k", ctrlKey: true, shiftKey: true, altKey: true, metaKey: true, }; - expect(isKeyComboMatch(mockKeyEvent('k', - { ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo3, false)).toBe(true); - expect(isKeyComboMatch(mockKeyEvent('n', - { ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo3, false)).toBe(false); - expect(isKeyComboMatch(mockKeyEvent('k', - { ctrlKey: true, shiftKey: true, altKey: true }), combo3, false)).toBe(false); + expect( + isKeyComboMatch( + mockKeyEvent("k", { ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), + combo3, + false, + ), + ).toBe(true); + expect( + isKeyComboMatch( + mockKeyEvent("n", { ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), + combo3, + false, + ), + ).toBe(false); + expect(isKeyComboMatch(mockKeyEvent("k", { ctrlKey: true, shiftKey: true, altKey: true }), combo3, false)).toBe( + false, + ); }); - it('should match ctrlOrMeta key combo', () => { + it("should match ctrlOrMeta key combo", () => { const combo: KeyCombo = { - key: 'k', + key: "k", ctrlOrCmdKey: true, }; // PC: - expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, false)).toBe(true); - expect(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo, false)).toBe(false); - expect(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent("k", { ctrlKey: true }), combo, false)).toBe(true); + expect(isKeyComboMatch(mockKeyEvent("k", { metaKey: true }), combo, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent("n", { ctrlKey: true }), combo, false)).toBe(false); // MAC: - expect(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo, true)).toBe(true); - expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, true)).toBe(false); - expect(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, true)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent("k", { metaKey: true }), combo, true)).toBe(true); + expect(isKeyComboMatch(mockKeyEvent("k", { ctrlKey: true }), combo, true)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent("n", { ctrlKey: true }), combo, true)).toBe(false); }); - it('should match advanced ctrlOrMeta key combo', () => { + it("should match advanced ctrlOrMeta key combo", () => { const combo: KeyCombo = { - key: 'k', + key: "k", ctrlOrCmdKey: true, altKey: true, }; // PC: - expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, false)).toBe(true); - expect(isKeyComboMatch(mockKeyEvent('k', { metaKey: true, altKey: true }), combo, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent("k", { ctrlKey: true, altKey: true }), combo, false)).toBe(true); + expect(isKeyComboMatch(mockKeyEvent("k", { metaKey: true, altKey: true }), combo, false)).toBe(false); // MAC: - expect(isKeyComboMatch(mockKeyEvent('k', { metaKey: true, altKey: true }), combo, true)).toBe(true); - expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, true)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent("k", { metaKey: true, altKey: true }), combo, true)).toBe(true); + expect(isKeyComboMatch(mockKeyEvent("k", { ctrlKey: true, altKey: true }), combo, true)).toBe(false); }); }); diff --git a/test/LegacyCallHandler-test.ts b/test/LegacyCallHandler-test.ts index 2fd774ae509..474fb7d070f 100644 --- a/test/LegacyCallHandler-test.ts +++ b/test/LegacyCallHandler-test.ts @@ -21,23 +21,28 @@ import { PushRuleKind, RuleId, TweakName, -} from 'matrix-js-sdk/src/matrix'; -import { CallEvent, CallState, CallType, MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; -import EventEmitter from 'events'; -import { mocked } from 'jest-mock'; -import { CallEventHandlerEvent } from 'matrix-js-sdk/src/webrtc/callEventHandler'; +} from "matrix-js-sdk/src/matrix"; +import { CallEvent, CallState, CallType, MatrixCall } from "matrix-js-sdk/src/webrtc/call"; +import EventEmitter from "events"; +import { mocked } from "jest-mock"; +import { CallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/callEventHandler"; import LegacyCallHandler, { - LegacyCallHandlerEvent, PROTOCOL_PSTN, PROTOCOL_PSTN_PREFIXED, PROTOCOL_SIP_NATIVE, PROTOCOL_SIP_VIRTUAL, -} from '../src/LegacyCallHandler'; -import { stubClient, mkStubRoom, untilDispatch } from './test-utils'; -import { MatrixClientPeg } from '../src/MatrixClientPeg'; -import DMRoomMap from '../src/utils/DMRoomMap'; -import SdkConfig from '../src/SdkConfig'; + LegacyCallHandlerEvent, + AudioID, + PROTOCOL_PSTN, + PROTOCOL_PSTN_PREFIXED, + PROTOCOL_SIP_NATIVE, + PROTOCOL_SIP_VIRTUAL, +} from "../src/LegacyCallHandler"; +import { stubClient, mkStubRoom, untilDispatch } from "./test-utils"; +import { MatrixClientPeg } from "../src/MatrixClientPeg"; +import DMRoomMap from "../src/utils/DMRoomMap"; +import SdkConfig from "../src/SdkConfig"; import { Action } from "../src/dispatcher/actions"; import { getFunctionalMembers } from "../src/utils/room/getFunctionalMembers"; -import SettingsStore from '../src/settings/SettingsStore'; -import { UIFeature } from '../src/settings/UIFeature'; +import SettingsStore from "../src/settings/SettingsStore"; +import { UIFeature } from "../src/settings/UIFeature"; jest.mock("../src/utils/room/getFunctionalMembers", () => ({ getFunctionalMembers: jest.fn(), @@ -67,34 +72,34 @@ const VIRTUAL_ROOM_BOB = "$virtual_bob_room:example.org"; const BOB_PHONE_NUMBER = "01818118181"; function mkStubDM(roomId, userId) { - const room = mkStubRoom(roomId, 'room', MatrixClientPeg.get()); + const room = mkStubRoom(roomId, "room", MatrixClientPeg.get()); room.getJoinedMembers = jest.fn().mockReturnValue([ { - userId: '@me:example.org', - name: 'Member', - rawDisplayName: 'Member', + userId: "@me:example.org", + name: "Member", + rawDisplayName: "Member", roomId: roomId, - membership: 'join', - getAvatarUrl: () => 'mxc://avatar.url/image.png', - getMxcAvatarUrl: () => 'mxc://avatar.url/image.png', + membership: "join", + getAvatarUrl: () => "mxc://avatar.url/image.png", + getMxcAvatarUrl: () => "mxc://avatar.url/image.png", }, { userId: userId, - name: 'Member', - rawDisplayName: 'Member', + name: "Member", + rawDisplayName: "Member", roomId: roomId, - membership: 'join', - getAvatarUrl: () => 'mxc://avatar.url/image.png', - getMxcAvatarUrl: () => 'mxc://avatar.url/image.png', + membership: "join", + getAvatarUrl: () => "mxc://avatar.url/image.png", + getMxcAvatarUrl: () => "mxc://avatar.url/image.png", }, { userId: FUNCTIONAL_USER, - name: 'Bot user', - rawDisplayName: 'Bot user', + name: "Bot user", + rawDisplayName: "Bot user", roomId: roomId, - membership: 'join', - getAvatarUrl: () => 'mxc://avatar.url/image.png', - getMxcAvatarUrl: () => 'mxc://avatar.url/image.png', + membership: "join", + getAvatarUrl: () => "mxc://avatar.url/image.png", + getMxcAvatarUrl: () => "mxc://avatar.url/image.png", }, ]); room.currentState.getMembers = room.getJoinedMembers; @@ -127,7 +132,7 @@ function untilCallHandlerEvent(callHandler: LegacyCallHandler, event: LegacyCall }); } -describe('LegacyCallHandler', () => { +describe("LegacyCallHandler", () => { let dmRoomMap; let callHandler; let audioElement: HTMLAudioElement; @@ -136,11 +141,11 @@ describe('LegacyCallHandler', () => { // what addresses the app has looked up via pstn and native lookup let pstnLookup: string; let nativeLookup: string; - const deviceId = 'my-device'; + const deviceId = "my-device"; beforeEach(async () => { stubClient(); - MatrixClientPeg.get().createCall = roomId => { + MatrixClientPeg.get().createCall = (roomId) => { if (fakeCall && fakeCall.roomId !== roomId) { throw new Error("Only one call is supported!"); } @@ -160,16 +165,14 @@ describe('LegacyCallHandler', () => { callHandler = new LegacyCallHandler(); callHandler.start(); - mocked(getFunctionalMembers).mockReturnValue([ - FUNCTIONAL_USER, - ]); + mocked(getFunctionalMembers).mockReturnValue([FUNCTIONAL_USER]); const nativeRoomAlice = mkStubDM(NATIVE_ROOM_ALICE, NATIVE_ALICE); const nativeRoomBob = mkStubDM(NATIVE_ROOM_BOB, NATIVE_BOB); const nativeRoomCharie = mkStubDM(NATIVE_ROOM_CHARLIE, NATIVE_CHARLIE); const virtualBobRoom = mkStubDM(VIRTUAL_ROOM_BOB, VIRTUAL_BOB); - MatrixClientPeg.get().getRoom = roomId => { + MatrixClientPeg.get().getRoom = (roomId) => { switch (roomId) { case NATIVE_ROOM_ALICE: return nativeRoomAlice; @@ -217,44 +220,50 @@ describe('LegacyCallHandler', () => { MatrixClientPeg.get().getThirdpartyUser = (proto, params) => { if ([PROTOCOL_PSTN, PROTOCOL_PSTN_PREFIXED].includes(proto)) { - pstnLookup = params['m.id.phone']; - return Promise.resolve([{ - userid: VIRTUAL_BOB, - protocol: "m.id.phone", - fields: { - is_native: true, - lookup_success: true, - }, - }]); - } else if (proto === PROTOCOL_SIP_NATIVE) { - nativeLookup = params['virtual_mxid']; - if (params['virtual_mxid'] === VIRTUAL_BOB) { - return Promise.resolve([{ - userid: NATIVE_BOB, - protocol: "im.vector.protocol.sip_native", + pstnLookup = params["m.id.phone"]; + return Promise.resolve([ + { + userid: VIRTUAL_BOB, + protocol: "m.id.phone", fields: { is_native: true, lookup_success: true, }, - }]); + }, + ]); + } else if (proto === PROTOCOL_SIP_NATIVE) { + nativeLookup = params["virtual_mxid"]; + if (params["virtual_mxid"] === VIRTUAL_BOB) { + return Promise.resolve([ + { + userid: NATIVE_BOB, + protocol: "im.vector.protocol.sip_native", + fields: { + is_native: true, + lookup_success: true, + }, + }, + ]); } return Promise.resolve([]); } else if (proto === PROTOCOL_SIP_VIRTUAL) { - if (params['native_mxid'] === NATIVE_BOB) { - return Promise.resolve([{ - userid: VIRTUAL_BOB, - protocol: "im.vector.protocol.sip_virtual", - fields: { - is_virtual: true, - lookup_success: true, + if (params["native_mxid"] === NATIVE_BOB) { + return Promise.resolve([ + { + userid: VIRTUAL_BOB, + protocol: "im.vector.protocol.sip_virtual", + fields: { + is_virtual: true, + lookup_success: true, + }, }, - }]); + ]); } return Promise.resolve([]); } }; - audioElement = document.createElement('audio'); + audioElement = document.createElement("audio"); audioElement.id = "remoteAudio"; document.body.appendChild(audioElement); }); @@ -271,7 +280,7 @@ describe('LegacyCallHandler', () => { SdkConfig.unset(); }); - it('should look up the correct user and start a call in the room when a phone number is dialled', async () => { + it("should look up the correct user and start a call in the room when a phone number is dialled", async () => { await callHandler.dialNumber(BOB_PHONE_NUMBER); expect(pstnLookup).toEqual(BOB_PHONE_NUMBER); @@ -289,7 +298,7 @@ describe('LegacyCallHandler', () => { expect(callHandler.roomIdForCall(fakeCall)).toEqual(NATIVE_ROOM_BOB); }); - it('should look up the correct user and start a call in the room when a call is transferred', async () => { + it("should look up the correct user and start a call in the room when a call is transferred", async () => { // we can pass a very minimal object as as the call since we pass consultFirst=true: // we don't need to actually do any transferring const mockTransferreeCall = { type: CallType.Voice }; @@ -304,7 +313,7 @@ describe('LegacyCallHandler', () => { expect(callHandler.roomIdForCall(fakeCall)).toEqual(NATIVE_ROOM_BOB); }); - it('should move calls between rooms when remote asserted identity changes', async () => { + it("should move calls between rooms when remote asserted identity changes", async () => { callHandler.placeCall(NATIVE_ROOM_ALICE, CallType.Voice); await untilCallHandlerEvent(callHandler, LegacyCallHandlerEvent.CallState); @@ -313,7 +322,7 @@ describe('LegacyCallHandler', () => { expect(callHandler.getCallForRoom(NATIVE_ROOM_ALICE)).toBe(fakeCall); let callRoomChangeEventCount = 0; - const roomChangePromise = new Promise(resolve => { + const roomChangePromise = new Promise((resolve) => { callHandler.addListener(LegacyCallHandlerEvent.CallChangeRoom, () => { ++callRoomChangeEventCount; resolve(); @@ -355,7 +364,7 @@ describe('LegacyCallHandler', () => { }); }); -describe('LegacyCallHandler without third party protocols', () => { +describe("LegacyCallHandler without third party protocols", () => { let dmRoomMap; let callHandler: LegacyCallHandler; let audioElement: HTMLAudioElement; @@ -363,7 +372,7 @@ describe('LegacyCallHandler without third party protocols', () => { beforeEach(() => { stubClient(); - MatrixClientPeg.get().createCall = roomId => { + MatrixClientPeg.get().createCall = (roomId) => { if (fakeCall && fakeCall.roomId !== roomId) { throw new Error("Only one call is supported!"); } @@ -380,7 +389,7 @@ describe('LegacyCallHandler without third party protocols', () => { const nativeRoomAlice = mkStubDM(NATIVE_ROOM_ALICE, NATIVE_ALICE); - MatrixClientPeg.get().getRoom = roomId => { + MatrixClientPeg.get().getRoom = (roomId) => { switch (roomId) { case NATIVE_ROOM_ALICE: return nativeRoomAlice; @@ -409,7 +418,7 @@ describe('LegacyCallHandler without third party protocols', () => { throw new Error("Endpoint unsupported."); }; - audioElement = document.createElement('audio'); + audioElement = document.createElement("audio"); audioElement.id = "remoteAudio"; document.body.appendChild(audioElement); }); @@ -426,7 +435,7 @@ describe('LegacyCallHandler without third party protocols', () => { SdkConfig.unset(); }); - it('should still start a native call', async () => { + it("should still start a native call", async () => { callHandler.placeCall(NATIVE_ROOM_ALICE, CallType.Voice); await untilCallHandlerEvent(callHandler, LegacyCallHandlerEvent.CallState); @@ -439,44 +448,47 @@ describe('LegacyCallHandler without third party protocols', () => { expect(callHandler.roomIdForCall(fakeCall)).toEqual(NATIVE_ROOM_ALICE); }); - describe('incoming calls', () => { - const roomId = 'test-room-id'; + describe("incoming calls", () => { + const roomId = "test-room-id"; const mockAudioElement = { play: jest.fn(), pause: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + muted: false, } as unknown as HTMLMediaElement; beforeEach(() => { jest.clearAllMocks(); - jest.spyOn(SettingsStore, 'getValue').mockImplementation(setting => - setting === UIFeature.Voip); + jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => setting === UIFeature.Voip); - jest.spyOn(MatrixClientPeg.get(), 'supportsVoip').mockReturnValue(true); + jest.spyOn(MatrixClientPeg.get(), "supportsVoip").mockReturnValue(true); MatrixClientPeg.get().isFallbackICEServerAllowed = jest.fn(); MatrixClientPeg.get().prepareToEncrypt = jest.fn(); MatrixClientPeg.get().pushRules = { global: { - [PushRuleKind.Override]: [{ - rule_id: RuleId.IncomingCall, - default: false, - enabled: true, - actions: [ - { - set_tweak: TweakName.Sound, - value: 'ring', - }, - ] - , - }], + [PushRuleKind.Override]: [ + { + rule_id: RuleId.IncomingCall, + default: false, + enabled: true, + actions: [ + { + set_tweak: TweakName.Sound, + value: "ring", + }, + ], + }, + ], }, }; - jest.spyOn(document, 'getElementById').mockReturnValue(mockAudioElement); + jest.spyOn(document, "getElementById").mockReturnValue(mockAudioElement); // silence local notifications by default - jest.spyOn(MatrixClientPeg.get(), 'getAccountData').mockImplementation((eventType) => { + jest.spyOn(MatrixClientPeg.get(), "getAccountData").mockImplementation((eventType) => { if (eventType.includes(LOCAL_NOTIFICATION_SETTINGS_PREFIX.name)) { return new MatrixEvent({ type: eventType, @@ -488,7 +500,20 @@ describe('LegacyCallHandler without third party protocols', () => { }); }); - it('listens for incoming call events when voip is enabled', () => { + it("should unmute
", ); fireEvent.click(container.querySelector("div")); expect(container.innerHTML).toBe( "", + '' + + "https://matrix.org" + + "
", ); }); }); diff --git a/test/components/views/elements/PollCreateDialog-test.tsx b/test/components/views/elements/PollCreateDialog-test.tsx index 1efb988409b..1459e501fca 100644 --- a/test/components/views/elements/PollCreateDialog-test.tsx +++ b/test/components/views/elements/PollCreateDialog-test.tsx @@ -24,16 +24,13 @@ import { M_POLL_START, M_TEXT, PollStartEvent, -} from 'matrix-events-sdk'; -import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; +} from "matrix-events-sdk"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { - findById, - getMockClientWithEventEmitter, -} from '../../../test-utils'; +import { findById, getMockClientWithEventEmitter } from "../../../test-utils"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import PollCreateDialog from "../../../../src/components/views/elements/PollCreateDialog"; -import MatrixClientContext from '../../../../src/contexts/MatrixClientContext'; +import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; // Fake date to give a predictable snapshot const realDateNow = Date.now; @@ -50,7 +47,7 @@ afterAll(() => { describe("PollCreateDialog", () => { const mockClient = getMockClientWithEventEmitter({ - sendEvent: jest.fn().mockResolvedValue({ event_id: '1' }), + sendEvent: jest.fn().mockResolvedValue({ event_id: "1" }), }); beforeEach(() => { @@ -58,48 +55,35 @@ describe("PollCreateDialog", () => { }); it("renders a blank poll", () => { - const dialog = mount( - , - { - wrappingComponent: MatrixClientContext.Provider, - wrappingComponentProps: { value: mockClient }, - }, - ); + const dialog = mount(, { + wrappingComponent: MatrixClientContext.Provider, + wrappingComponentProps: { value: mockClient }, + }); expect(dialog.html()).toMatchSnapshot(); }); it("autofocuses the poll topic on mount", () => { - const dialog = mount( - , - ); - expect(findById(dialog, 'poll-topic-input').at(0).props().autoFocus).toEqual(true); + const dialog = mount(); + expect(findById(dialog, "poll-topic-input").at(0).props().autoFocus).toEqual(true); }); it("autofocuses the new poll option field after clicking add option button", () => { - const dialog = mount( - , - ); - expect(findById(dialog, 'poll-topic-input').at(0).props().autoFocus).toEqual(true); + const dialog = mount(); + expect(findById(dialog, "poll-topic-input").at(0).props().autoFocus).toEqual(true); dialog.find("div.mx_PollCreateDialog_addOption").simulate("click"); - expect(findById(dialog, 'poll-topic-input').at(0).props().autoFocus).toEqual(false); - expect(findById(dialog, 'pollcreate_option_1').at(0).props().autoFocus).toEqual(false); - expect(findById(dialog, 'pollcreate_option_2').at(0).props().autoFocus).toEqual(true); + expect(findById(dialog, "poll-topic-input").at(0).props().autoFocus).toEqual(false); + expect(findById(dialog, "pollcreate_option_1").at(0).props().autoFocus).toEqual(false); + expect(findById(dialog, "pollcreate_option_2").at(0).props().autoFocus).toEqual(true); }); it("renders a question and some options", () => { - const dialog = mount( - , - ); + const dialog = mount(); expect(submitIsDisabled(dialog)).toBe(true); // When I set some values in the boxes - changeValue( - dialog, - "Question or topic", - "How many turnips is the optimal number?", - ); + changeValue(dialog, "Question or topic", "How many turnips is the optimal number?"); changeValue(dialog, "Option 1", "As many as my neighbour"); changeValue(dialog, "Option 2", "The question is meaningless"); dialog.find("div.mx_PollCreateDialog_addOption").simulate("click"); @@ -109,19 +93,11 @@ describe("PollCreateDialog", () => { it("renders info from a previous event", () => { const previousEvent: MatrixEvent = new MatrixEvent( - PollStartEvent.from( - "Poll Q", - ["Answer 1", "Answer 2"], - M_POLL_KIND_DISCLOSED, - ).serialize(), + PollStartEvent.from("Poll Q", ["Answer 1", "Answer 2"], M_POLL_KIND_DISCLOSED).serialize(), ); const dialog = mount( - , + , ); expect(submitIsDisabled(dialog)).toBe(false); @@ -129,17 +105,13 @@ describe("PollCreateDialog", () => { }); it("doesn't allow submitting until there are options", () => { - const dialog = mount( - , - ); + const dialog = mount(); expect(submitIsDisabled(dialog)).toBe(true); }); it("does allow submitting when there are options and a question", () => { // Given a dialog with no info in (which I am unable to submit) - const dialog = mount( - , - ); + const dialog = mount(); expect(submitIsDisabled(dialog)).toBe(true); // When I set some values in the boxes @@ -152,74 +124,42 @@ describe("PollCreateDialog", () => { }); it("shows the open poll description at first", () => { - const dialog = mount( - , - ); - expect( - dialog.find('select').prop("value"), - ).toEqual(M_POLL_KIND_DISCLOSED.name); - expect( - dialog.find('p').text(), - ).toEqual("Voters see results as soon as they have voted"); + const dialog = mount(); + expect(dialog.find("select").prop("value")).toEqual(M_POLL_KIND_DISCLOSED.name); + expect(dialog.find("p").text()).toEqual("Voters see results as soon as they have voted"); }); it("shows the closed poll description if we choose it", () => { - const dialog = mount( - , - ); + const dialog = mount(); changeKind(dialog, M_POLL_KIND_UNDISCLOSED.name); - expect( - dialog.find('select').prop("value"), - ).toEqual(M_POLL_KIND_UNDISCLOSED.name); - expect( - dialog.find('p').text(), - ).toEqual("Results are only revealed when you end the poll"); + expect(dialog.find("select").prop("value")).toEqual(M_POLL_KIND_UNDISCLOSED.name); + expect(dialog.find("p").text()).toEqual("Results are only revealed when you end the poll"); }); it("shows the open poll description if we choose it", () => { - const dialog = mount( - , - ); + const dialog = mount(); changeKind(dialog, M_POLL_KIND_UNDISCLOSED.name); changeKind(dialog, M_POLL_KIND_DISCLOSED.name); - expect( - dialog.find('select').prop("value"), - ).toEqual(M_POLL_KIND_DISCLOSED.name); - expect( - dialog.find('p').text(), - ).toEqual("Voters see results as soon as they have voted"); + expect(dialog.find("select").prop("value")).toEqual(M_POLL_KIND_DISCLOSED.name); + expect(dialog.find("p").text()).toEqual("Voters see results as soon as they have voted"); }); it("shows the closed poll description when editing a closed poll", () => { const previousEvent: MatrixEvent = new MatrixEvent( - PollStartEvent.from( - "Poll Q", - ["Answer 1", "Answer 2"], - M_POLL_KIND_UNDISCLOSED, - ).serialize(), + PollStartEvent.from("Poll Q", ["Answer 1", "Answer 2"], M_POLL_KIND_UNDISCLOSED).serialize(), ); previousEvent.event.event_id = "$prevEventId"; const dialog = mount( - , + , ); - expect( - dialog.find('select').prop("value"), - ).toEqual(M_POLL_KIND_UNDISCLOSED.name); - expect( - dialog.find('p').text(), - ).toEqual("Results are only revealed when you end the poll"); + expect(dialog.find("select").prop("value")).toEqual(M_POLL_KIND_UNDISCLOSED.name); + expect(dialog.find("p").text()).toEqual("Results are only revealed when you end the poll"); }); it("displays a spinner after submitting", () => { - const dialog = mount( - , - ); + const dialog = mount(); changeValue(dialog, "Question or topic", "Q"); changeValue(dialog, "Option 1", "A1"); changeValue(dialog, "Option 2", "A2"); @@ -230,9 +170,7 @@ describe("PollCreateDialog", () => { }); it("sends a poll create event when submitted", () => { - const dialog = mount( - , - ); + const dialog = mount(); changeValue(dialog, "Question or topic", "Q"); changeValue(dialog, "Option 1", "A1"); changeValue(dialog, "Option 2", "A2"); @@ -240,50 +178,40 @@ describe("PollCreateDialog", () => { dialog.find("button").simulate("click"); const [, , eventType, sentEventContent] = mockClient.sendEvent.mock.calls[0]; expect(M_POLL_START.matches(eventType)).toBeTruthy(); - expect(sentEventContent).toEqual( - { - [M_TEXT.name]: "Q\n1. A1\n2. A2", - [M_POLL_START.name]: { - "answers": [ - { - "id": expect.any(String), - [M_TEXT.name]: "A1", - }, - { - "id": expect.any(String), - [M_TEXT.name]: "A2", - }, - ], - "kind": M_POLL_KIND_DISCLOSED.name, - "max_selections": 1, - "question": { - "body": "Q", - "format": undefined, - "formatted_body": undefined, - "msgtype": "m.text", - [M_TEXT.name]: "Q", + expect(sentEventContent).toEqual({ + [M_TEXT.name]: "Q\n1. A1\n2. A2", + [M_POLL_START.name]: { + answers: [ + { + id: expect.any(String), + [M_TEXT.name]: "A1", + }, + { + id: expect.any(String), + [M_TEXT.name]: "A2", }, + ], + kind: M_POLL_KIND_DISCLOSED.name, + max_selections: 1, + question: { + body: "Q", + format: undefined, + formatted_body: undefined, + msgtype: "m.text", + [M_TEXT.name]: "Q", }, }, - ); + }); }); it("sends a poll edit event when editing", () => { const previousEvent: MatrixEvent = new MatrixEvent( - PollStartEvent.from( - "Poll Q", - ["Answer 1", "Answer 2"], - M_POLL_KIND_DISCLOSED, - ).serialize(), + PollStartEvent.from("Poll Q", ["Answer 1", "Answer 2"], M_POLL_KIND_DISCLOSED).serialize(), ); previousEvent.event.event_id = "$prevEventId"; const dialog = mount( - , + , ); changeValue(dialog, "Question or topic", "Poll Q updated"); @@ -293,65 +221,51 @@ describe("PollCreateDialog", () => { const [, , eventType, sentEventContent] = mockClient.sendEvent.mock.calls[0]; expect(M_POLL_START.matches(eventType)).toBeTruthy(); - expect(sentEventContent).toEqual( - { - "m.new_content": { - [M_TEXT.name]: "Poll Q updated\n1. Answer 1\n2. Answer 2 updated", - [M_POLL_START.name]: { - "answers": [ - { - "id": expect.any(String), - [M_TEXT.name]: "Answer 1", - }, - { - "id": expect.any(String), - [M_TEXT.name]: "Answer 2 updated", - }, - ], - "kind": M_POLL_KIND_UNDISCLOSED.name, - "max_selections": 1, - "question": { - "body": "Poll Q updated", - "format": undefined, - "formatted_body": undefined, - "msgtype": "m.text", - [M_TEXT.name]: "Poll Q updated", + expect(sentEventContent).toEqual({ + "m.new_content": { + [M_TEXT.name]: "Poll Q updated\n1. Answer 1\n2. Answer 2 updated", + [M_POLL_START.name]: { + answers: [ + { + id: expect.any(String), + [M_TEXT.name]: "Answer 1", + }, + { + id: expect.any(String), + [M_TEXT.name]: "Answer 2 updated", }, + ], + kind: M_POLL_KIND_UNDISCLOSED.name, + max_selections: 1, + question: { + body: "Poll Q updated", + format: undefined, + formatted_body: undefined, + msgtype: "m.text", + [M_TEXT.name]: "Poll Q updated", }, }, - "m.relates_to": { - "event_id": previousEvent.getId(), - "rel_type": "m.replace", - }, }, - ); + "m.relates_to": { + event_id: previousEvent.getId(), + rel_type: "m.replace", + }, + }); }); }); function createRoom(): Room { - return new Room( - "roomid", - MatrixClientPeg.get(), - "@name:example.com", - {}, - ); + return new Room("roomid", MatrixClientPeg.get(), "@name:example.com", {}); } function changeValue(wrapper: ReactWrapper, labelText: string, value: string) { - wrapper.find(`input[label="${labelText}"]`).simulate( - "change", - { target: { value: value } }, - ); + wrapper.find(`input[label="${labelText}"]`).simulate("change", { target: { value: value } }); } function changeKind(wrapper: ReactWrapper, value: string) { - wrapper.find("select").simulate( - "change", - { target: { value: value } }, - ); + wrapper.find("select").simulate("change", { target: { value: value } }); } function submitIsDisabled(wrapper: ReactWrapper) { return wrapper.find('button[type="submit"]').prop("aria-disabled") === true; } - diff --git a/test/components/views/elements/PowerSelector-test.tsx b/test/components/views/elements/PowerSelector-test.tsx index 9367d0b089f..4636aaafc79 100644 --- a/test/components/views/elements/PowerSelector-test.tsx +++ b/test/components/views/elements/PowerSelector-test.tsx @@ -14,12 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React from "react"; import { fireEvent, render, screen } from "@testing-library/react"; import PowerSelector from "../../../../src/components/views/elements/PowerSelector"; -describe('', () => { +describe("", () => { it("should reset back to custom value when custom input is blurred blank", async () => { const fn = jest.fn(); render(); @@ -59,4 +59,20 @@ describe('', () => { await screen.findByDisplayValue(40); expect(fn).toHaveBeenCalledWith(40, "key"); }); + + it("should reset when props get changed", async () => { + const fn = jest.fn(); + const { rerender } = render(); + + const select = screen.getByLabelText("Power level"); + fireEvent.change(select, { target: { value: "SELECT_VALUE_CUSTOM" } }); + + rerender(); + await screen.findByDisplayValue(51); + + rerender(); + const option = await screen.findByText("Moderator"); + expect(option.selected).toBeTruthy(); + expect(fn).not.toHaveBeenCalled(); + }); }); diff --git a/test/components/views/elements/ProgressBar-test.tsx b/test/components/views/elements/ProgressBar-test.tsx index 320304fb76b..ffdeb548379 100644 --- a/test/components/views/elements/ProgressBar-test.tsx +++ b/test/components/views/elements/ProgressBar-test.tsx @@ -28,7 +28,9 @@ describe("", () => { expect(progress.value).toBe(0); // Await the animation to conclude to our initial value of 50 - act(() => { jest.runAllTimers(); }); + act(() => { + jest.runAllTimers(); + }); expect(progress.position).toBe(0.5); // Move the needle to 80% @@ -36,7 +38,9 @@ describe("", () => { expect(progress.position).toBe(0.5); // Let the animaiton run a tiny bit, assert it has moved from where it was to where it needs to go - act(() => { jest.advanceTimersByTime(150); }); + act(() => { + jest.advanceTimersByTime(150); + }); expect(progress.position).toBeGreaterThan(0.5); expect(progress.position).toBeLessThan(0.8); }); diff --git a/test/components/views/elements/QRCode-test.tsx b/test/components/views/elements/QRCode-test.tsx index dbd240aa3d6..4148366063b 100644 --- a/test/components/views/elements/QRCode-test.tsx +++ b/test/components/views/elements/QRCode-test.tsx @@ -24,13 +24,13 @@ describe("", () => { it("renders a QR with defaults", async () => { const { container, getAllByAltText } = render(); - await waitFor(() => getAllByAltText('QR Code').length === 1); + await waitFor(() => getAllByAltText("QR Code").length === 1); expect(container).toMatchSnapshot(); }); it("renders a QR with high error correction level", async () => { const { container, getAllByAltText } = render(); - await waitFor(() => getAllByAltText('QR Code').length === 1); + await waitFor(() => getAllByAltText("QR Code").length === 1); expect(container).toMatchSnapshot(); }); }); diff --git a/test/components/views/elements/ReplyChain-test.tsx b/test/components/views/elements/ReplyChain-test.tsx index bcc33c1fedf..0bfafddbc91 100644 --- a/test/components/views/elements/ReplyChain-test.tsx +++ b/test/components/views/elements/ReplyChain-test.tsx @@ -14,12 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import * as testUtils from '../../../test-utils'; +import * as testUtils from "../../../test-utils"; import { getParentEventId } from "../../../../src/utils/Reply"; describe("ReplyChain", () => { - describe('getParentEventId', () => { - it('retrieves relation reply from unedited event', () => { + describe("getParentEventId", () => { + it("retrieves relation reply from unedited event", () => { const originalEventWithRelation = testUtils.mkEvent({ event: true, type: "m.room.message", @@ -28,7 +28,7 @@ describe("ReplyChain", () => { "body": "> Reply to this message\n\n foo", "m.relates_to": { "m.in_reply_to": { - "event_id": "$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og", + event_id: "$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og", }, }, }, @@ -36,11 +36,12 @@ describe("ReplyChain", () => { room: "room_id", }); - expect(getParentEventId(originalEventWithRelation)) - .toStrictEqual('$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og'); + expect(getParentEventId(originalEventWithRelation)).toStrictEqual( + "$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og", + ); }); - it('retrieves relation reply from original event when edited', () => { + it("retrieves relation reply from original event when edited", () => { const originalEventWithRelation = testUtils.mkEvent({ event: true, type: "m.room.message", @@ -49,7 +50,7 @@ describe("ReplyChain", () => { "body": "> Reply to this message\n\n foo", "m.relates_to": { "m.in_reply_to": { - "event_id": "$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og", + event_id: "$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og", }, }, }, @@ -64,12 +65,12 @@ describe("ReplyChain", () => { "msgtype": "m.text", "body": "> Reply to this message\n\n * foo bar", "m.new_content": { - "msgtype": "m.text", - "body": "foo bar", + msgtype: "m.text", + body: "foo bar", }, "m.relates_to": { - "rel_type": "m.replace", - "event_id": originalEventWithRelation.getId(), + rel_type: "m.replace", + event_id: originalEventWithRelation.getId(), }, }, user: "some_other_user", @@ -80,11 +81,12 @@ describe("ReplyChain", () => { originalEventWithRelation.makeReplaced(editEvent); // The relation should be pulled from the original event - expect(getParentEventId(originalEventWithRelation)) - .toStrictEqual('$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og'); + expect(getParentEventId(originalEventWithRelation)).toStrictEqual( + "$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og", + ); }); - it('retrieves relation reply from edit event when provided', () => { + it("retrieves relation reply from edit event when provided", () => { const originalEvent = testUtils.mkEvent({ event: true, type: "m.room.message", @@ -107,13 +109,13 @@ describe("ReplyChain", () => { "body": "foo bar", "m.relates_to": { "m.in_reply_to": { - "event_id": "$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og", + event_id: "$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og", }, }, }, "m.relates_to": { - "rel_type": "m.replace", - "event_id": originalEvent.getId(), + rel_type: "m.replace", + event_id: originalEvent.getId(), }, }, user: "some_other_user", @@ -124,11 +126,10 @@ describe("ReplyChain", () => { originalEvent.makeReplaced(editEvent); // The relation should be pulled from the edit event - expect(getParentEventId(originalEvent)) - .toStrictEqual('$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og'); + expect(getParentEventId(originalEvent)).toStrictEqual("$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og"); }); - it('prefers relation reply from edit event over original event', () => { + it("prefers relation reply from edit event over original event", () => { const originalEventWithRelation = testUtils.mkEvent({ event: true, type: "m.room.message", @@ -137,7 +138,7 @@ describe("ReplyChain", () => { "body": "> Reply to this message\n\n foo", "m.relates_to": { "m.in_reply_to": { - "event_id": "$111", + event_id: "$111", }, }, }, @@ -156,13 +157,13 @@ describe("ReplyChain", () => { "body": "foo bar", "m.relates_to": { "m.in_reply_to": { - "event_id": "$999", + event_id: "$999", }, }, }, "m.relates_to": { - "rel_type": "m.replace", - "event_id": originalEventWithRelation.getId(), + rel_type: "m.replace", + event_id: originalEventWithRelation.getId(), }, }, user: "some_other_user", @@ -173,10 +174,10 @@ describe("ReplyChain", () => { originalEventWithRelation.makeReplaced(editEvent); // The relation should be pulled from the edit event - expect(getParentEventId(originalEventWithRelation)).toStrictEqual('$999'); + expect(getParentEventId(originalEventWithRelation)).toStrictEqual("$999"); }); - it('able to clear relation reply from original event by providing empty relation field', () => { + it("able to clear relation reply from original event by providing empty relation field", () => { const originalEventWithRelation = testUtils.mkEvent({ event: true, type: "m.room.message", @@ -185,7 +186,7 @@ describe("ReplyChain", () => { "body": "> Reply to this message\n\n foo", "m.relates_to": { "m.in_reply_to": { - "event_id": "$111", + event_id: "$111", }, }, }, @@ -206,8 +207,8 @@ describe("ReplyChain", () => { "m.relates_to": {}, }, "m.relates_to": { - "rel_type": "m.replace", - "event_id": originalEventWithRelation.getId(), + rel_type: "m.replace", + event_id: originalEventWithRelation.getId(), }, }, user: "some_other_user", diff --git a/test/components/views/elements/StyledRadioGroup-test.tsx b/test/components/views/elements/StyledRadioGroup-test.tsx index 8868b741bd1..55fe40bcbe5 100644 --- a/test/components/views/elements/StyledRadioGroup-test.tsx +++ b/test/components/views/elements/StyledRadioGroup-test.tsx @@ -14,47 +14,47 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React from "react"; import { fireEvent, render } from "@testing-library/react"; import StyledRadioGroup from "../../../../src/components/views/elements/StyledRadioGroup"; -describe('', () => { +describe("", () => { const optionA = { - value: 'Anteater', + value: "Anteater", label: Anteater label, - description: 'anteater description', - className: 'a-class', + description: "anteater description", + className: "a-class", }; const optionB = { - value: 'Badger', + value: "Badger", label: Badger label, }; const optionC = { - value: 'Canary', + value: "Canary", label: Canary label, description: Canary description, }; const defaultDefinitions = [optionA, optionB, optionC]; const defaultProps = { - name: 'test', - className: 'test-class', + name: "test", + className: "test-class", definitions: defaultDefinitions, onChange: jest.fn(), }; const getComponent = (props = {}) => render(); const getInputByValue = (component, value) => component.container.querySelector(`input[value="${value}"]`); - const getCheckedInput = component => component.container.querySelector('input[checked]'); + const getCheckedInput = (component) => component.container.querySelector("input[checked]"); - it('renders radios correctly when no value is provided', () => { + it("renders radios correctly when no value is provided", () => { const component = getComponent(); expect(component.asFragment()).toMatchSnapshot(); expect(getCheckedInput(component)).toBeFalsy(); }); - it('selects correct button when value is provided', () => { + it("selects correct button when value is provided", () => { const component = getComponent({ value: optionC.value, }); @@ -62,14 +62,11 @@ describe('', () => { expect(getCheckedInput(component).value).toEqual(optionC.value); }); - it('selects correct buttons when definitions have checked prop', () => { - const definitions = [ - { ...optionA, checked: true }, - optionB, - { ...optionC, checked: false }, - ]; + it("selects correct buttons when definitions have checked prop", () => { + const definitions = [{ ...optionA, checked: true }, optionB, { ...optionC, checked: false }]; const component = getComponent({ - value: optionC.value, definitions, + value: optionC.value, + definitions, }); expect(getInputByValue(component, optionA.value)).toBeChecked(); @@ -78,26 +75,22 @@ describe('', () => { expect(getInputByValue(component, optionC.value)).not.toBeChecked(); }); - it('disables individual buttons based on definition.disabled', () => { - const definitions = [ - optionA, - { ...optionB, disabled: true }, - { ...optionC, disabled: true }, - ]; + it("disables individual buttons based on definition.disabled", () => { + const definitions = [optionA, { ...optionB, disabled: true }, { ...optionC, disabled: true }]; const component = getComponent({ definitions }); expect(getInputByValue(component, optionA.value)).not.toBeDisabled(); expect(getInputByValue(component, optionB.value)).toBeDisabled(); expect(getInputByValue(component, optionC.value)).toBeDisabled(); }); - it('disables all buttons with disabled prop', () => { + it("disables all buttons with disabled prop", () => { const component = getComponent({ disabled: true }); expect(getInputByValue(component, optionA.value)).toBeDisabled(); expect(getInputByValue(component, optionB.value)).toBeDisabled(); expect(getInputByValue(component, optionC.value)).toBeDisabled(); }); - it('calls onChange on click', () => { + it("calls onChange on click", () => { const onChange = jest.fn(); const component = getComponent({ value: optionC.value, diff --git a/test/components/views/elements/TooltipTarget-test.tsx b/test/components/views/elements/TooltipTarget-test.tsx index 56b0b37f62a..ed7ce265013 100644 --- a/test/components/views/elements/TooltipTarget-test.tsx +++ b/test/components/views/elements/TooltipTarget-test.tsx @@ -15,29 +15,26 @@ limitations under the License. */ import React from "react"; -import { - renderIntoDocument, - Simulate, -} from 'react-dom/test-utils'; +import { renderIntoDocument, Simulate } from "react-dom/test-utils"; import { act } from "react-dom/test-utils"; -import { Alignment } from '../../../../src/components/views/elements/Tooltip'; +import { Alignment } from "../../../../src/components/views/elements/Tooltip"; import TooltipTarget from "../../../../src/components/views/elements/TooltipTarget"; -describe('', () => { +describe("", () => { const defaultProps = { - "tooltipTargetClassName": 'test tooltipTargetClassName', - "className": 'test className', - "tooltipClassName": 'test tooltipClassName', - "label": 'test label', + "tooltipTargetClassName": "test tooltipTargetClassName", + "className": "test className", + "tooltipClassName": "test tooltipClassName", + "label": "test label", "alignment": Alignment.Left, - "id": 'test id', - 'data-test-id': 'test', + "id": "test id", + "data-test-id": "test", }; afterEach(() => { // clean up renderer tooltips - const wrapper = document.querySelector('.mx_Tooltip_wrapper'); + const wrapper = document.querySelector(".mx_Tooltip_wrapper"); while (wrapper?.firstChild) { wrapper.removeChild(wrapper.lastChild); } @@ -45,19 +42,19 @@ describe('', () => { const getComponent = (props = {}) => { const wrapper = renderIntoDocument( - // wrap in element so renderIntoDocument can render functional component + // wrap in element so renderIntoDocument can render functional component child , ) as HTMLSpanElement; - return wrapper.querySelector('[data-test-id=test]'); + return wrapper.querySelector("[data-test-id=test]"); }; - const getVisibleTooltip = () => document.querySelector('.mx_Tooltip.mx_Tooltip_visible'); + const getVisibleTooltip = () => document.querySelector(".mx_Tooltip.mx_Tooltip_visible"); - it('renders container', () => { + it("renders container", () => { const component = getComponent(); expect(component).toMatchSnapshot(); expect(getVisibleTooltip()).toBeFalsy(); @@ -72,7 +69,7 @@ describe('', () => { expect(getVisibleTooltip()).toMatchSnapshot(); }); - it('hides tooltip on mouseleave', () => { + it("hides tooltip on mouseleave", () => { const wrapper = getComponent(); act(() => { Simulate.mouseOver(wrapper); @@ -84,7 +81,7 @@ describe('', () => { expect(getVisibleTooltip()).toBeFalsy(); }); - it('displays tooltip on focus', () => { + it("displays tooltip on focus", () => { const wrapper = getComponent(); act(() => { Simulate.focus(wrapper); @@ -92,7 +89,7 @@ describe('', () => { expect(getVisibleTooltip()).toBeTruthy(); }); - it('hides tooltip on blur', async () => { + it("hides tooltip on blur", async () => { const wrapper = getComponent(); act(() => { Simulate.focus(wrapper); diff --git a/test/components/views/location/LiveDurationDropdown-test.tsx b/test/components/views/location/LiveDurationDropdown-test.tsx index 60bd7c77064..73003e589af 100644 --- a/test/components/views/location/LiveDurationDropdown-test.tsx +++ b/test/components/views/location/LiveDurationDropdown-test.tsx @@ -14,51 +14,52 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React from "react"; // eslint-disable-next-line deprecate/import -import { mount } from 'enzyme'; -import { act } from 'react-dom/test-utils'; +import { mount } from "enzyme"; +import { act } from "react-dom/test-utils"; -import LiveDurationDropdown, { DEFAULT_DURATION_MS } - from '../../../../src/components/views/location/LiveDurationDropdown'; -import { findById, mockPlatformPeg } from '../../../test-utils'; +import LiveDurationDropdown, { + DEFAULT_DURATION_MS, +} from "../../../../src/components/views/location/LiveDurationDropdown"; +import { findById, mockPlatformPeg } from "../../../test-utils"; mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) }); -describe('', () => { +describe("", () => { const defaultProps = { timeout: DEFAULT_DURATION_MS, onChange: jest.fn(), }; - const getComponent = (props = {}) => - mount(); + const getComponent = (props = {}) => mount(); const getOption = (wrapper, timeout) => findById(wrapper, `live-duration__${timeout}`).at(0); - const getSelectedOption = (wrapper) => findById(wrapper, 'live-duration_value'); - const openDropdown = (wrapper) => act(() => { - wrapper.find('[role="button"]').at(0).simulate('click'); - wrapper.setProps({}); - }); + const getSelectedOption = (wrapper) => findById(wrapper, "live-duration_value"); + const openDropdown = (wrapper) => + act(() => { + wrapper.find('[role="button"]').at(0).simulate("click"); + wrapper.setProps({}); + }); - it('renders timeout as selected option', () => { + it("renders timeout as selected option", () => { const wrapper = getComponent(); - expect(getSelectedOption(wrapper).text()).toEqual('Share for 15m'); + expect(getSelectedOption(wrapper).text()).toEqual("Share for 15m"); }); - it('renders non-default timeout as selected option', () => { + it("renders non-default timeout as selected option", () => { const timeout = 1234567; const wrapper = getComponent({ timeout }); expect(getSelectedOption(wrapper).text()).toEqual(`Share for 21m`); }); - it('renders a dropdown option for a non-default timeout value', () => { + it("renders a dropdown option for a non-default timeout value", () => { const timeout = 1234567; const wrapper = getComponent({ timeout }); openDropdown(wrapper); expect(getOption(wrapper, timeout).text()).toEqual(`Share for 21m`); }); - it('updates value on option selection', () => { + it("updates value on option selection", () => { const onChange = jest.fn(); const wrapper = getComponent({ onChange }); @@ -67,7 +68,7 @@ describe('', () => { openDropdown(wrapper); act(() => { - getOption(wrapper, ONE_HOUR).simulate('click'); + getOption(wrapper, ONE_HOUR).simulate("click"); }); expect(onChange).toHaveBeenCalledWith(ONE_HOUR); diff --git a/test/components/views/location/LocationPicker-test.tsx b/test/components/views/location/LocationPicker-test.tsx index 39b0b3b42e2..29b044630b6 100644 --- a/test/components/views/location/LocationPicker-test.tsx +++ b/test/components/views/location/LocationPicker-test.tsx @@ -14,34 +14,34 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React from "react"; import maplibregl from "maplibre-gl"; // eslint-disable-next-line deprecate/import import { mount } from "enzyme"; -import { act } from 'react-dom/test-utils'; +import { act } from "react-dom/test-utils"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; -import { MatrixClient } from 'matrix-js-sdk/src/client'; -import { mocked } from 'jest-mock'; -import { logger } from 'matrix-js-sdk/src/logger'; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { mocked } from "jest-mock"; +import { logger } from "matrix-js-sdk/src/logger"; import LocationPicker from "../../../../src/components/views/location/LocationPicker"; import { LocationShareType } from "../../../../src/components/views/location/shareLocation"; -import MatrixClientContext from '../../../../src/contexts/MatrixClientContext'; -import { MatrixClientPeg } from '../../../../src/MatrixClientPeg'; -import { findById, findByTestId, mockPlatformPeg } from '../../../test-utils'; -import { findMapStyleUrl, LocationShareError } from '../../../../src/utils/location'; +import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; +import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; +import { findById, findByTestId, mockPlatformPeg } from "../../../test-utils"; +import { findMapStyleUrl, LocationShareError } from "../../../../src/utils/location"; -jest.mock('../../../../src/utils/location/findMapStyleUrl', () => ({ - findMapStyleUrl: jest.fn().mockReturnValue('tileserver.com'), +jest.mock("../../../../src/utils/location/findMapStyleUrl", () => ({ + findMapStyleUrl: jest.fn().mockReturnValue("tileserver.com"), })); // dropdown uses this mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) }); describe("LocationPicker", () => { - describe('', () => { - const roomId = '!room:server.org'; - const userId = '@user:server.org'; + describe("", () => { + const roomId = "!room:server.org"; + const userId = "@user:server.org"; const sender = new RoomMember(roomId, userId); const defaultProps = { sender, @@ -56,10 +56,11 @@ describe("LocationPicker", () => { isGuest: jest.fn(), getClientWellKnown: jest.fn(), }; - const getComponent = (props = {}) => mount(, { - wrappingComponent: MatrixClientContext.Provider, - wrappingComponentProps: { value: mockClient }, - }); + const getComponent = (props = {}) => + mount(, { + wrappingComponent: MatrixClientContext.Provider, + wrappingComponentProps: { value: mockClient }, + }); const mockMap = new maplibregl.Map(); const mockGeolocate = new maplibregl.GeolocateControl(); @@ -82,33 +83,33 @@ describe("LocationPicker", () => { }; beforeEach(() => { - jest.spyOn(logger, 'error').mockRestore(); - jest.spyOn(MatrixClientPeg, 'get').mockReturnValue(mockClient as unknown as MatrixClient); + jest.spyOn(logger, "error").mockRestore(); + jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient as unknown as MatrixClient); jest.clearAllMocks(); mocked(mockMap).addControl.mockReset(); - mocked(findMapStyleUrl).mockReturnValue('tileserver.com'); + mocked(findMapStyleUrl).mockReturnValue("tileserver.com"); }); - it('displays error when map emits an error', () => { + it("displays error when map emits an error", () => { // suppress expected error log - jest.spyOn(logger, 'error').mockImplementation(() => { }); + jest.spyOn(logger, "error").mockImplementation(() => {}); const wrapper = getComponent(); act(() => { // @ts-ignore - mocked(mockMap).emit('error', { error: 'Something went wrong' }); + mocked(mockMap).emit("error", { error: "Something went wrong" }); wrapper.setProps({}); }); - expect(findByTestId(wrapper, 'map-rendering-error').find('p').text()).toEqual( - "This homeserver is not configured correctly to display maps, " - + "or the configured map server may be unreachable.", + expect(findByTestId(wrapper, "map-rendering-error").find("p").text()).toEqual( + "This homeserver is not configured correctly to display maps, " + + "or the configured map server may be unreachable.", ); }); - it('displays error when map display is not configured properly', () => { + it("displays error when map display is not configured properly", () => { // suppress expected error log - jest.spyOn(logger, 'error').mockImplementation(() => { }); + jest.spyOn(logger, "error").mockImplementation(() => {}); mocked(findMapStyleUrl).mockImplementation(() => { throw new Error(LocationShareError.MapStyleUrlNotConfigured); }); @@ -116,111 +117,111 @@ describe("LocationPicker", () => { const wrapper = getComponent(); wrapper.setProps({}); - expect(findByTestId(wrapper, 'map-rendering-error').find('p').text()).toEqual( + expect(findByTestId(wrapper, "map-rendering-error").find("p").text()).toEqual( "This homeserver is not configured to display maps.", ); }); - it('displays error when map setup throws', () => { + it("displays error when map setup throws", () => { // suppress expected error log - jest.spyOn(logger, 'error').mockImplementation(() => { }); + jest.spyOn(logger, "error").mockImplementation(() => {}); // throw an error - mocked(mockMap).addControl.mockImplementation(() => { throw new Error('oups'); }); + mocked(mockMap).addControl.mockImplementation(() => { + throw new Error("oups"); + }); const wrapper = getComponent(); wrapper.setProps({}); - expect(findByTestId(wrapper, 'map-rendering-error').find('p').text()).toEqual( - "This homeserver is not configured correctly to display maps, " - + "or the configured map server may be unreachable.", + expect(findByTestId(wrapper, "map-rendering-error").find("p").text()).toEqual( + "This homeserver is not configured correctly to display maps, " + + "or the configured map server may be unreachable.", ); }); - it('initiates map with geolocation', () => { + it("initiates map with geolocation", () => { getComponent(); expect(mockMap.addControl).toHaveBeenCalledWith(mockGeolocate); act(() => { // @ts-ignore - mocked(mockMap).emit('load'); + mocked(mockMap).emit("load"); }); expect(mockGeolocate.trigger).toHaveBeenCalled(); }); const testUserLocationShareTypes = (shareType: LocationShareType.Own | LocationShareType.Live) => { - describe('user location behaviours', () => { - it('closes and displays error when geolocation errors', () => { + describe("user location behaviours", () => { + it("closes and displays error when geolocation errors", () => { // suppress expected error log - jest.spyOn(logger, 'error').mockImplementation(() => { }); + jest.spyOn(logger, "error").mockImplementation(() => {}); const onFinished = jest.fn(); getComponent({ onFinished, shareType }); expect(mockMap.addControl).toHaveBeenCalledWith(mockGeolocate); act(() => { // @ts-ignore - mockMap.emit('load'); + mockMap.emit("load"); // @ts-ignore - mockGeolocate.emit('error', {}); + mockGeolocate.emit("error", {}); }); // dialog is closed on error expect(onFinished).toHaveBeenCalled(); }); - it('sets position on geolocate event', () => { + it("sets position on geolocate event", () => { const wrapper = getComponent({ shareType }); act(() => { // @ts-ignore - mocked(mockGeolocate).emit('geolocate', mockGeolocationPosition); + mocked(mockGeolocate).emit("geolocate", mockGeolocationPosition); wrapper.setProps({}); }); // marker added expect(maplibregl.Marker).toHaveBeenCalled(); - expect(mockMarker.setLngLat).toHaveBeenCalledWith(new maplibregl.LngLat( - 12.4, 43.2, - )); + expect(mockMarker.setLngLat).toHaveBeenCalledWith(new maplibregl.LngLat(12.4, 43.2)); // submit button is enabled when position is truthy - expect(findByTestId(wrapper, 'location-picker-submit-button').at(0).props().disabled).toBeFalsy(); - expect(wrapper.find('MemberAvatar').length).toBeTruthy(); + expect(findByTestId(wrapper, "location-picker-submit-button").at(0).props().disabled).toBeFalsy(); + expect(wrapper.find("MemberAvatar").length).toBeTruthy(); }); - it('disables submit button until geolocation completes', () => { + it("disables submit button until geolocation completes", () => { const onChoose = jest.fn(); const wrapper = getComponent({ shareType, onChoose }); // submit button is enabled when position is truthy - expect(findByTestId(wrapper, 'location-picker-submit-button').at(0).props().disabled).toBeTruthy(); + expect(findByTestId(wrapper, "location-picker-submit-button").at(0).props().disabled).toBeTruthy(); act(() => { - findByTestId(wrapper, 'location-picker-submit-button').at(0).simulate('click'); + findByTestId(wrapper, "location-picker-submit-button").at(0).simulate("click"); }); // nothing happens on button click expect(onChoose).not.toHaveBeenCalled(); act(() => { // @ts-ignore - mocked(mockGeolocate).emit('geolocate', mockGeolocationPosition); + mocked(mockGeolocate).emit("geolocate", mockGeolocationPosition); wrapper.setProps({}); }); // submit button is enabled when position is truthy - expect(findByTestId(wrapper, 'location-picker-submit-button').at(0).props().disabled).toBeFalsy(); + expect(findByTestId(wrapper, "location-picker-submit-button").at(0).props().disabled).toBeFalsy(); }); - it('submits location', () => { + it("submits location", () => { const onChoose = jest.fn(); const wrapper = getComponent({ onChoose, shareType }); act(() => { // @ts-ignore - mocked(mockGeolocate).emit('geolocate', mockGeolocationPosition); + mocked(mockGeolocate).emit("geolocate", mockGeolocationPosition); // make sure button is enabled wrapper.setProps({}); }); act(() => { - findByTestId(wrapper, 'location-picker-submit-button').at(0).simulate('click'); + findByTestId(wrapper, "location-picker-submit-button").at(0).simulate("click"); }); // content of this call is tested in LocationShareMenu-test @@ -229,67 +230,68 @@ describe("LocationPicker", () => { }); }; - describe('for Own location share type', () => { + describe("for Own location share type", () => { testUserLocationShareTypes(LocationShareType.Own); }); - describe('for Live location share type', () => { + describe("for Live location share type", () => { const shareType = LocationShareType.Live; testUserLocationShareTypes(shareType); const getOption = (wrapper, timeout) => findById(wrapper, `live-duration__${timeout}`).at(0); - const getDropdown = wrapper => findByTestId(wrapper, 'live-duration-dropdown'); - const getSelectedOption = (wrapper) => findById(wrapper, 'live-duration_value'); + const getDropdown = (wrapper) => findByTestId(wrapper, "live-duration-dropdown"); + const getSelectedOption = (wrapper) => findById(wrapper, "live-duration_value"); - const openDropdown = (wrapper) => act(() => { - const dropdown = getDropdown(wrapper); - dropdown.find('[role="button"]').at(0).simulate('click'); - wrapper.setProps({}); - }); + const openDropdown = (wrapper) => + act(() => { + const dropdown = getDropdown(wrapper); + dropdown.find('[role="button"]').at(0).simulate("click"); + wrapper.setProps({}); + }); - it('renders live duration dropdown with default option', () => { + it("renders live duration dropdown with default option", () => { const wrapper = getComponent({ shareType }); - expect(getSelectedOption(getDropdown(wrapper)).text()).toEqual('Share for 15m'); + expect(getSelectedOption(getDropdown(wrapper)).text()).toEqual("Share for 15m"); }); - it('updates selected duration', () => { + it("updates selected duration", () => { const wrapper = getComponent({ shareType }); openDropdown(wrapper); const dropdown = getDropdown(wrapper); act(() => { - getOption(dropdown, 3600000).simulate('click'); + getOption(dropdown, 3600000).simulate("click"); }); // value updated - expect(getSelectedOption(getDropdown(wrapper)).text()).toEqual('Share for 1h'); + expect(getSelectedOption(getDropdown(wrapper)).text()).toEqual("Share for 1h"); }); }); - describe('for Pin drop location share type', () => { + describe("for Pin drop location share type", () => { const shareType = LocationShareType.Pin; - it('initiates map with geolocation', () => { + it("initiates map with geolocation", () => { getComponent({ shareType }); expect(mockMap.addControl).toHaveBeenCalledWith(mockGeolocate); act(() => { // @ts-ignore - mocked(mockMap).emit('load'); + mocked(mockMap).emit("load"); }); expect(mockGeolocate.trigger).toHaveBeenCalled(); }); - it('removes geolocation control on geolocation error', () => { + it("removes geolocation control on geolocation error", () => { // suppress expected error log - jest.spyOn(logger, 'error').mockImplementation(() => { }); + jest.spyOn(logger, "error").mockImplementation(() => {}); const onFinished = jest.fn(); getComponent({ onFinished, shareType }); act(() => { // @ts-ignore - mockMap.emit('load'); + mockMap.emit("load"); // @ts-ignore - mockGeolocate.emit('error', {}); + mockGeolocate.emit("error", {}); }); expect(mockMap.removeControl).toHaveBeenCalledWith(mockGeolocate); @@ -297,47 +299,45 @@ describe("LocationPicker", () => { expect(onFinished).not.toHaveBeenCalled(); }); - it('does not set position on geolocate event', () => { + it("does not set position on geolocate event", () => { mocked(maplibregl.Marker).mockClear(); const wrapper = getComponent({ shareType }); act(() => { // @ts-ignore - mocked(mockGeolocate).emit('geolocate', mockGeolocationPosition); + mocked(mockGeolocate).emit("geolocate", mockGeolocationPosition); }); // marker not added - expect(wrapper.find('Marker').length).toBeFalsy(); + expect(wrapper.find("Marker").length).toBeFalsy(); }); - it('sets position on click event', () => { + it("sets position on click event", () => { const wrapper = getComponent({ shareType }); act(() => { // @ts-ignore - mocked(mockMap).emit('click', mockClickEvent); + mocked(mockMap).emit("click", mockClickEvent); wrapper.setProps({}); }); // marker added expect(maplibregl.Marker).toHaveBeenCalled(); - expect(mockMarker.setLngLat).toHaveBeenCalledWith(new maplibregl.LngLat( - 12.4, 43.2, - )); + expect(mockMarker.setLngLat).toHaveBeenCalledWith(new maplibregl.LngLat(12.4, 43.2)); // marker is set, icon not avatar - expect(wrapper.find('.mx_Marker_icon').length).toBeTruthy(); + expect(wrapper.find(".mx_Marker_icon").length).toBeTruthy(); }); - it('submits location', () => { + it("submits location", () => { const onChoose = jest.fn(); const wrapper = getComponent({ onChoose, shareType }); act(() => { // @ts-ignore - mocked(mockMap).emit('click', mockClickEvent); + mocked(mockMap).emit("click", mockClickEvent); wrapper.setProps({}); }); act(() => { - findByTestId(wrapper, 'location-picker-submit-button').at(0).simulate('click'); + findByTestId(wrapper, "location-picker-submit-button").at(0).simulate("click"); }); // content of this call is tested in LocationShareMenu-test diff --git a/test/components/views/location/LocationShareMenu-test.tsx b/test/components/views/location/LocationShareMenu-test.tsx index f590bcdd7c7..567fffa591e 100644 --- a/test/components/views/location/LocationShareMenu-test.tsx +++ b/test/components/views/location/LocationShareMenu-test.tsx @@ -14,43 +14,43 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React from "react"; // eslint-disable-next-line deprecate/import -import { mount, ReactWrapper } from 'enzyme'; -import { mocked } from 'jest-mock'; -import { RoomMember } from 'matrix-js-sdk/src/models/room-member'; -import { MatrixClient } from 'matrix-js-sdk/src/client'; -import { RelationType } from 'matrix-js-sdk/src/matrix'; -import { logger } from 'matrix-js-sdk/src/logger'; -import { M_ASSET, LocationAssetType } from 'matrix-js-sdk/src/@types/location'; -import { act } from 'react-dom/test-utils'; - -import LocationShareMenu from '../../../../src/components/views/location/LocationShareMenu'; -import MatrixClientContext from '../../../../src/contexts/MatrixClientContext'; -import { ChevronFace } from '../../../../src/components/structures/ContextMenu'; -import SettingsStore from '../../../../src/settings/SettingsStore'; -import { MatrixClientPeg } from '../../../../src/MatrixClientPeg'; -import { LocationShareType } from '../../../../src/components/views/location/shareLocation'; +import { mount, ReactWrapper } from "enzyme"; +import { mocked } from "jest-mock"; +import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { RelationType } from "matrix-js-sdk/src/matrix"; +import { logger } from "matrix-js-sdk/src/logger"; +import { M_ASSET, LocationAssetType } from "matrix-js-sdk/src/@types/location"; +import { act } from "react-dom/test-utils"; + +import LocationShareMenu from "../../../../src/components/views/location/LocationShareMenu"; +import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; +import { ChevronFace } from "../../../../src/components/structures/ContextMenu"; +import SettingsStore from "../../../../src/settings/SettingsStore"; +import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; +import { LocationShareType } from "../../../../src/components/views/location/shareLocation"; import { findByTagAndTestId, findByTestId, flushPromisesWithFakeTimers, getMockClientWithEventEmitter, setupAsyncStoreWithClient, -} from '../../../test-utils'; -import Modal from '../../../../src/Modal'; -import { DEFAULT_DURATION_MS } from '../../../../src/components/views/location/LiveDurationDropdown'; -import { OwnBeaconStore } from '../../../../src/stores/OwnBeaconStore'; -import { SettingLevel } from '../../../../src/settings/SettingLevel'; -import QuestionDialog from '../../../../src/components/views/dialogs/QuestionDialog'; +} from "../../../test-utils"; +import Modal from "../../../../src/Modal"; +import { DEFAULT_DURATION_MS } from "../../../../src/components/views/location/LiveDurationDropdown"; +import { OwnBeaconStore } from "../../../../src/stores/OwnBeaconStore"; +import { SettingLevel } from "../../../../src/settings/SettingLevel"; +import QuestionDialog from "../../../../src/components/views/dialogs/QuestionDialog"; jest.useFakeTimers(); -jest.mock('../../../../src/utils/location/findMapStyleUrl', () => ({ - findMapStyleUrl: jest.fn().mockReturnValue('test'), +jest.mock("../../../../src/utils/location/findMapStyleUrl", () => ({ + findMapStyleUrl: jest.fn().mockReturnValue("test"), })); -jest.mock('../../../../src/settings/SettingsStore', () => ({ +jest.mock("../../../../src/settings/SettingsStore", () => ({ getValue: jest.fn(), setValue: jest.fn(), monitorSetting: jest.fn(), @@ -58,44 +58,45 @@ jest.mock('../../../../src/settings/SettingsStore', () => ({ unwatchSetting: jest.fn(), })); -jest.mock('../../../../src/stores/OwnProfileStore', () => ({ +jest.mock("../../../../src/stores/OwnProfileStore", () => ({ OwnProfileStore: { instance: { - displayName: 'Ernie', - getHttpAvatarUrl: jest.fn().mockReturnValue('image.com/img'), + displayName: "Ernie", + getHttpAvatarUrl: jest.fn().mockReturnValue("image.com/img"), }, }, })); -jest.mock('../../../../src/Modal', () => ({ +jest.mock("../../../../src/Modal", () => ({ createDialog: jest.fn(), on: jest.fn(), off: jest.fn(), ModalManagerEvent: { Opened: "opened" }, })); -describe('', () => { - const userId = '@ernie:server.org'; +describe("", () => { + const userId = "@ernie:server.org"; const mockClient = getMockClientWithEventEmitter({ getUserId: jest.fn().mockReturnValue(userId), getClientWellKnown: jest.fn().mockResolvedValue({ - map_style_url: 'maps.com', + map_style_url: "maps.com", }), sendMessage: jest.fn(), - unstable_createLiveBeacon: jest.fn().mockResolvedValue({ event_id: '1' }), - unstable_setLiveBeacon: jest.fn().mockResolvedValue({ event_id: '1' }), + unstable_createLiveBeacon: jest.fn().mockResolvedValue({ event_id: "1" }), + unstable_setLiveBeacon: jest.fn().mockResolvedValue({ event_id: "1" }), getVisibleRooms: jest.fn().mockReturnValue([]), }); const defaultProps = { menuPosition: { - top: 1, left: 1, + top: 1, + left: 1, chevronFace: ChevronFace.Bottom, }, onFinished: jest.fn(), openMenu: jest.fn(), - roomId: '!room:server.org', - sender: new RoomMember('!room:server.org', userId), + roomId: "!room:server.org", + sender: new RoomMember("!room:server.org", userId), }; const position = { @@ -105,7 +106,7 @@ describe('', () => { accuracy: 10, }, timestamp: 1646305006802, - type: 'geolocate', + type: "geolocate", }; const makeOwnBeaconStore = async () => { @@ -122,11 +123,11 @@ describe('', () => { }); beforeEach(async () => { - jest.spyOn(logger, 'error').mockRestore(); + jest.spyOn(logger, "error").mockRestore(); mocked(SettingsStore).getValue.mockReturnValue(false); mockClient.sendMessage.mockClear(); - mockClient.unstable_createLiveBeacon.mockClear().mockResolvedValue({ event_id: '1' }); - jest.spyOn(MatrixClientPeg, 'get').mockReturnValue(mockClient as unknown as MatrixClient); + mockClient.unstable_createLiveBeacon.mockClear().mockResolvedValue({ event_id: "1" }); + jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient as unknown as MatrixClient); mocked(Modal).createDialog.mockClear(); jest.clearAllMocks(); @@ -135,20 +136,20 @@ describe('', () => { }); const getShareTypeOption = (component: ReactWrapper, shareType: LocationShareType) => - findByTagAndTestId(component, `share-location-option-${shareType}`, 'button'); + findByTagAndTestId(component, `share-location-option-${shareType}`, "button"); const getBackButton = (component: ReactWrapper) => - findByTagAndTestId(component, 'share-dialog-buttons-back', 'button'); + findByTagAndTestId(component, "share-dialog-buttons-back", "button"); const getCancelButton = (component: ReactWrapper) => - findByTagAndTestId(component, 'share-dialog-buttons-cancel', 'button'); + findByTagAndTestId(component, "share-dialog-buttons-cancel", "button"); const getSubmitButton = (component: ReactWrapper) => - findByTagAndTestId(component, 'location-picker-submit-button', 'button'); + findByTagAndTestId(component, "location-picker-submit-button", "button"); const setLocation = (component: ReactWrapper) => { // set the location - const locationPickerInstance = component.find('LocationPicker').instance(); + const locationPickerInstance = component.find("LocationPicker").instance(); act(() => { // @ts-ignore locationPickerInstance.onGeolocate(position); @@ -159,38 +160,38 @@ describe('', () => { const setShareType = (component: ReactWrapper, shareType: LocationShareType) => act(() => { - getShareTypeOption(component, shareType).at(0).simulate('click'); + getShareTypeOption(component, shareType).at(0).simulate("click"); component.setProps({}); }); - describe('when only Own share type is enabled', () => { + describe("when only Own share type is enabled", () => { beforeEach(() => enableSettings([])); - it('renders own and live location options', () => { + it("renders own and live location options", () => { const component = getComponent(); expect(getShareTypeOption(component, LocationShareType.Own).length).toBe(1); expect(getShareTypeOption(component, LocationShareType.Live).length).toBe(1); }); - it('renders back button from location picker screen', () => { + it("renders back button from location picker screen", () => { const component = getComponent(); setShareType(component, LocationShareType.Own); expect(getBackButton(component).length).toBe(1); }); - it('clicking cancel button from location picker closes dialog', () => { + it("clicking cancel button from location picker closes dialog", () => { const onFinished = jest.fn(); const component = getComponent({ onFinished }); act(() => { - getCancelButton(component).at(0).simulate('click'); + getCancelButton(component).at(0).simulate("click"); }); expect(onFinished).toHaveBeenCalled(); }); - it('creates static own location share event on submission', () => { + it("creates static own location share event on submission", () => { const onFinished = jest.fn(); const component = getComponent({ onFinished }); @@ -199,7 +200,7 @@ describe('', () => { setLocation(component); act(() => { - getSubmitButton(component).at(0).simulate('click'); + getSubmitButton(component).at(0).simulate("click"); component.setProps({}); }); @@ -207,66 +208,68 @@ describe('', () => { const [messageRoomId, relation, messageBody] = mockClient.sendMessage.mock.calls[0]; expect(messageRoomId).toEqual(defaultProps.roomId); expect(relation).toEqual(null); - expect(messageBody).toEqual(expect.objectContaining({ - [M_ASSET.name]: { - type: LocationAssetType.Self, - }, - })); + expect(messageBody).toEqual( + expect.objectContaining({ + [M_ASSET.name]: { + type: LocationAssetType.Self, + }, + }), + ); }); }); - describe('with pin drop share type enabled', () => { - it('renders share type switch with own and pin drop options', () => { + describe("with pin drop share type enabled", () => { + it("renders share type switch with own and pin drop options", () => { const component = getComponent(); - expect(component.find('LocationPicker').length).toBe(0); + expect(component.find("LocationPicker").length).toBe(0); expect(getShareTypeOption(component, LocationShareType.Own).length).toBe(1); expect(getShareTypeOption(component, LocationShareType.Pin).length).toBe(1); }); - it('does not render back button on share type screen', () => { + it("does not render back button on share type screen", () => { const component = getComponent(); expect(getBackButton(component).length).toBe(0); }); - it('clicking cancel button from share type screen closes dialog', () => { + it("clicking cancel button from share type screen closes dialog", () => { const onFinished = jest.fn(); const component = getComponent({ onFinished }); act(() => { - getCancelButton(component).at(0).simulate('click'); + getCancelButton(component).at(0).simulate("click"); }); expect(onFinished).toHaveBeenCalled(); }); - it('selecting own location share type advances to location picker', () => { + it("selecting own location share type advances to location picker", () => { const component = getComponent(); setShareType(component, LocationShareType.Own); - expect(component.find('LocationPicker').length).toBe(1); + expect(component.find("LocationPicker").length).toBe(1); }); - it('clicking back button from location picker screen goes back to share screen', () => { + it("clicking back button from location picker screen goes back to share screen", () => { const onFinished = jest.fn(); const component = getComponent({ onFinished }); // advance to location picker setShareType(component, LocationShareType.Own); - expect(component.find('LocationPicker').length).toBe(1); + expect(component.find("LocationPicker").length).toBe(1); act(() => { - getBackButton(component).at(0).simulate('click'); + getBackButton(component).at(0).simulate("click"); component.setProps({}); }); // back to share type - expect(component.find('ShareType').length).toBe(1); + expect(component.find("ShareType").length).toBe(1); }); - it('creates pin drop location share event on submission', () => { + it("creates pin drop location share event on submission", () => { const onFinished = jest.fn(); const component = getComponent({ onFinished }); @@ -276,7 +279,7 @@ describe('', () => { setLocation(component); act(() => { - getSubmitButton(component).at(0).simulate('click'); + getSubmitButton(component).at(0).simulate("click"); component.setProps({}); }); @@ -284,76 +287,81 @@ describe('', () => { const [messageRoomId, relation, messageBody] = mockClient.sendMessage.mock.calls[0]; expect(messageRoomId).toEqual(defaultProps.roomId); expect(relation).toEqual(null); - expect(messageBody).toEqual(expect.objectContaining({ - [M_ASSET.name]: { - type: LocationAssetType.Pin, - }, - })); + expect(messageBody).toEqual( + expect.objectContaining({ + [M_ASSET.name]: { + type: LocationAssetType.Pin, + }, + }), + ); }); }); - describe('with live location disabled', () => { + describe("with live location disabled", () => { beforeEach(() => enableSettings([])); const getToggle = (component: ReactWrapper) => - findByTestId(component, 'enable-live-share-toggle').find('[role="switch"]').at(0); + findByTestId(component, "enable-live-share-toggle").find('[role="switch"]').at(0); const getSubmitEnableButton = (component: ReactWrapper) => - findByTestId(component, 'enable-live-share-submit').at(0); + findByTestId(component, "enable-live-share-submit").at(0); - it('goes to labs flag screen after live options is clicked', () => { + it("goes to labs flag screen after live options is clicked", () => { const onFinished = jest.fn(); const component = getComponent({ onFinished }); setShareType(component, LocationShareType.Live); - expect(findByTestId(component, 'location-picker-enable-live-share')).toMatchSnapshot(); + expect(findByTestId(component, "location-picker-enable-live-share")).toMatchSnapshot(); }); - it('disables OK button when labs flag is not enabled', () => { + it("disables OK button when labs flag is not enabled", () => { const component = getComponent(); setShareType(component, LocationShareType.Live); - expect(getSubmitEnableButton(component).props()['disabled']).toBeTruthy(); + expect(getSubmitEnableButton(component).props()["disabled"]).toBeTruthy(); }); - it('enables OK button when labs flag is toggled to enabled', () => { + it("enables OK button when labs flag is toggled to enabled", () => { const component = getComponent(); setShareType(component, LocationShareType.Live); act(() => { - getToggle(component).simulate('click'); + getToggle(component).simulate("click"); component.setProps({}); }); - expect(getSubmitEnableButton(component).props()['disabled']).toBeFalsy(); + expect(getSubmitEnableButton(component).props()["disabled"]).toBeFalsy(); }); - it('enables live share setting on ok button submit', () => { + it("enables live share setting on ok button submit", () => { const component = getComponent(); setShareType(component, LocationShareType.Live); act(() => { - getToggle(component).simulate('click'); + getToggle(component).simulate("click"); component.setProps({}); }); act(() => { - getSubmitEnableButton(component).simulate('click'); + getSubmitEnableButton(component).simulate("click"); }); expect(SettingsStore.setValue).toHaveBeenCalledWith( - 'feature_location_share_live', undefined, SettingLevel.DEVICE, true, + "feature_location_share_live", + undefined, + SettingLevel.DEVICE, + true, ); }); - it('navigates to location picker when live share is enabled in settings store', () => { + it("navigates to location picker when live share is enabled in settings store", () => { // @ts-ignore mocked(SettingsStore.watchSetting).mockImplementation((featureName, roomId, callback) => { - callback(featureName, roomId, SettingLevel.DEVICE, '', ''); - setTimeout(() => { - callback(featureName, roomId, SettingLevel.DEVICE, '', ''); + callback(featureName, roomId, SettingLevel.DEVICE, "", ""); + window.setTimeout(() => { + callback(featureName, roomId, SettingLevel.DEVICE, "", ""); }, 1000); }); mocked(SettingsStore.getValue).mockReturnValue(false); @@ -362,7 +370,7 @@ describe('', () => { setShareType(component, LocationShareType.Live); // we're on enable live share screen - expect(findByTestId(component, 'location-picker-enable-live-share').length).toBeTruthy(); + expect(findByTestId(component, "location-picker-enable-live-share").length).toBeTruthy(); act(() => { mocked(SettingsStore.getValue).mockReturnValue(true); @@ -373,24 +381,24 @@ describe('', () => { component.setProps({}); // advanced to location picker - expect(component.find('LocationPicker').length).toBeTruthy(); + expect(component.find("LocationPicker").length).toBeTruthy(); }); }); - describe('Live location share', () => { + describe("Live location share", () => { beforeEach(() => enableSettings(["feature_location_share_live"])); - it('does not display live location share option when composer has a relation', () => { + it("does not display live location share option when composer has a relation", () => { const relation = { rel_type: RelationType.Thread, - event_id: '12345', + event_id: "12345", }; const component = getComponent({ relation }); expect(getShareTypeOption(component, LocationShareType.Live).length).toBeFalsy(); }); - it('creates beacon info event on submission', async () => { + it("creates beacon info event on submission", async () => { const onFinished = jest.fn(); const component = getComponent({ onFinished }); @@ -399,7 +407,7 @@ describe('', () => { setLocation(component); act(() => { - getSubmitButton(component).at(0).simulate('click'); + getSubmitButton(component).at(0).simulate("click"); component.setProps({}); }); @@ -409,21 +417,23 @@ describe('', () => { expect(onFinished).toHaveBeenCalled(); const [eventRoomId, eventContent] = mockClient.unstable_createLiveBeacon.mock.calls[0]; expect(eventRoomId).toEqual(defaultProps.roomId); - expect(eventContent).toEqual(expect.objectContaining({ - // default timeout - timeout: DEFAULT_DURATION_MS, - description: `Ernie's live location`, - live: true, - [M_ASSET.name]: { - type: LocationAssetType.Self, - }, - })); + expect(eventContent).toEqual( + expect.objectContaining({ + // default timeout + timeout: DEFAULT_DURATION_MS, + description: `Ernie's live location`, + live: true, + [M_ASSET.name]: { + type: LocationAssetType.Self, + }, + }), + ); }); - it('opens error dialog when beacon creation fails', async () => { + it("opens error dialog when beacon creation fails", async () => { // stub logger to keep console clean from expected error - const logSpy = jest.spyOn(logger, 'error').mockReturnValue(undefined); - const error = new Error('oh no'); + const logSpy = jest.spyOn(logger, "error").mockReturnValue(undefined); + const error = new Error("oh no"); mockClient.unstable_createLiveBeacon.mockRejectedValue(error); const component = getComponent(); @@ -432,7 +442,7 @@ describe('', () => { setLocation(component); act(() => { - getSubmitButton(component).at(0).simulate('click'); + getSubmitButton(component).at(0).simulate("click"); component.setProps({}); }); @@ -441,18 +451,21 @@ describe('', () => { await flushPromisesWithFakeTimers(); expect(logSpy).toHaveBeenCalledWith("We couldn't start sharing your live location", error); - expect(mocked(Modal).createDialog).toHaveBeenCalledWith(QuestionDialog, expect.objectContaining({ - button: 'Try again', - description: 'Element could not send your location. Please try again later.', - title: `We couldn't send your location`, - cancelButton: 'Cancel', - })); + expect(mocked(Modal).createDialog).toHaveBeenCalledWith( + QuestionDialog, + expect.objectContaining({ + button: "Try again", + description: "Element could not send your location. Please try again later.", + title: `We couldn't send your location`, + cancelButton: "Cancel", + }), + ); }); - it('opens error dialog when beacon creation fails with permission error', async () => { + it("opens error dialog when beacon creation fails with permission error", async () => { // stub logger to keep console clean from expected error - const logSpy = jest.spyOn(logger, 'error').mockReturnValue(undefined); - const error = { errcode: 'M_FORBIDDEN' } as unknown as Error; + const logSpy = jest.spyOn(logger, "error").mockReturnValue(undefined); + const error = { errcode: "M_FORBIDDEN" } as unknown as Error; mockClient.unstable_createLiveBeacon.mockRejectedValue(error); const component = getComponent(); @@ -461,7 +474,7 @@ describe('', () => { setLocation(component); act(() => { - getSubmitButton(component).at(0).simulate('click'); + getSubmitButton(component).at(0).simulate("click"); component.setProps({}); }); @@ -470,12 +483,15 @@ describe('', () => { await flushPromisesWithFakeTimers(); expect(logSpy).toHaveBeenCalledWith("Insufficient permissions to start sharing your live location", error); - expect(mocked(Modal).createDialog).toHaveBeenCalledWith(QuestionDialog, expect.objectContaining({ - button: 'OK', - description: 'You need to have the right permissions in order to share locations in this room.', - title: `You don't have permission to share locations`, - hasCancelButton: false, - })); + expect(mocked(Modal).createDialog).toHaveBeenCalledWith( + QuestionDialog, + expect.objectContaining({ + button: "OK", + description: "You need to have the right permissions in order to share locations in this room.", + title: `You don't have permission to share locations`, + hasCancelButton: false, + }), + ); }); }); }); diff --git a/test/components/views/location/LocationViewDialog-test.tsx b/test/components/views/location/LocationViewDialog-test.tsx index 04b08c9af60..31e40f94dbe 100644 --- a/test/components/views/location/LocationViewDialog-test.tsx +++ b/test/components/views/location/LocationViewDialog-test.tsx @@ -14,23 +14,23 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React from "react"; // eslint-disable-next-line deprecate/import -import { mount } from 'enzyme'; -import { RoomMember } from 'matrix-js-sdk/src/matrix'; -import { LocationAssetType } from 'matrix-js-sdk/src/@types/location'; -import maplibregl from 'maplibre-gl'; - -import LocationViewDialog from '../../../../src/components/views/location/LocationViewDialog'; -import { TILE_SERVER_WK_KEY } from '../../../../src/utils/WellKnownUtils'; -import { getMockClientWithEventEmitter, makeLocationEvent } from '../../../test-utils'; - -describe('', () => { - const roomId = '!room:server'; - const userId = '@user:server'; +import { mount } from "enzyme"; +import { RoomMember } from "matrix-js-sdk/src/matrix"; +import { LocationAssetType } from "matrix-js-sdk/src/@types/location"; +import maplibregl from "maplibre-gl"; + +import LocationViewDialog from "../../../../src/components/views/location/LocationViewDialog"; +import { TILE_SERVER_WK_KEY } from "../../../../src/utils/WellKnownUtils"; +import { getMockClientWithEventEmitter, makeLocationEvent } from "../../../test-utils"; + +describe("", () => { + const roomId = "!room:server"; + const userId = "@user:server"; const mockClient = getMockClientWithEventEmitter({ getClientWellKnown: jest.fn().mockReturnValue({ - [TILE_SERVER_WK_KEY.name]: { map_style_url: 'maps.com' }, + [TILE_SERVER_WK_KEY.name]: { map_style_url: "maps.com" }, }), isGuest: jest.fn().mockReturnValue(false), }); @@ -40,24 +40,23 @@ describe('', () => { mxEvent: defaultEvent, onFinished: jest.fn(), }; - const getComponent = (props = {}) => - mount(); + const getComponent = (props = {}) => mount(); beforeAll(() => { maplibregl.AttributionControl = jest.fn(); }); - it('renders map correctly', () => { + it("renders map correctly", () => { const component = getComponent(); - expect(component.find('Map')).toMatchSnapshot(); + expect(component.find("Map")).toMatchSnapshot(); }); - it('renders marker correctly for self share', () => { + it("renders marker correctly for self share", () => { const selfShareEvent = makeLocationEvent("geo:51.5076,-0.1276", LocationAssetType.Self); const member = new RoomMember(roomId, userId); // @ts-ignore cheat assignment to property selfShareEvent.sender = member; const component = getComponent({ mxEvent: selfShareEvent }); - expect(component.find('SmartMarker').props()['roomMember']).toEqual(member); + expect(component.find("SmartMarker").props()["roomMember"]).toEqual(member); }); }); diff --git a/test/components/views/location/Map-test.tsx b/test/components/views/location/Map-test.tsx index 0e22d1cdd8e..8cb3109a7e0 100644 --- a/test/components/views/location/Map-test.tsx +++ b/test/components/views/location/Map-test.tsx @@ -14,29 +14,29 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React from "react"; // eslint-disable-next-line deprecate/import -import { mount } from 'enzyme'; -import { act } from 'react-dom/test-utils'; -import maplibregl from 'maplibre-gl'; -import { ClientEvent } from 'matrix-js-sdk/src/matrix'; -import { logger } from 'matrix-js-sdk/src/logger'; - -import Map from '../../../../src/components/views/location/Map'; -import { findByTestId, getMockClientWithEventEmitter } from '../../../test-utils'; -import MatrixClientContext from '../../../../src/contexts/MatrixClientContext'; -import { TILE_SERVER_WK_KEY } from '../../../../src/utils/WellKnownUtils'; - -describe('', () => { +import { mount } from "enzyme"; +import { act } from "react-dom/test-utils"; +import maplibregl from "maplibre-gl"; +import { ClientEvent } from "matrix-js-sdk/src/matrix"; +import { logger } from "matrix-js-sdk/src/logger"; + +import Map from "../../../../src/components/views/location/Map"; +import { findByTestId, getMockClientWithEventEmitter } from "../../../test-utils"; +import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; +import { TILE_SERVER_WK_KEY } from "../../../../src/utils/WellKnownUtils"; + +describe("", () => { const defaultProps = { - centerGeoUri: 'geo:52,41', - id: 'test-123', + centerGeoUri: "geo:52,41", + id: "test-123", onError: jest.fn(), onClick: jest.fn(), }; const matrixClient = getMockClientWithEventEmitter({ getClientWellKnown: jest.fn().mockReturnValue({ - [TILE_SERVER_WK_KEY.name]: { map_style_url: 'maps.com' }, + [TILE_SERVER_WK_KEY.name]: { map_style_url: "maps.com" }, }), }); const getComponent = (props = {}) => @@ -52,33 +52,33 @@ describe('', () => { beforeEach(() => { jest.clearAllMocks(); matrixClient.getClientWellKnown.mockReturnValue({ - [TILE_SERVER_WK_KEY.name]: { map_style_url: 'maps.com' }, + [TILE_SERVER_WK_KEY.name]: { map_style_url: "maps.com" }, }); - jest.spyOn(logger, 'error').mockRestore(); + jest.spyOn(logger, "error").mockRestore(); }); const mockMap = new maplibregl.Map(); - it('renders', () => { + it("renders", () => { const component = getComponent(); expect(component).toBeTruthy(); }); - describe('onClientWellKnown emits', () => { - it('updates map style when style url is truthy', () => { + describe("onClientWellKnown emits", () => { + it("updates map style when style url is truthy", () => { getComponent(); act(() => { matrixClient.emit(ClientEvent.ClientWellKnown, { - [TILE_SERVER_WK_KEY.name]: { map_style_url: 'new.maps.com' }, + [TILE_SERVER_WK_KEY.name]: { map_style_url: "new.maps.com" }, }); }); - expect(mockMap.setStyle).toHaveBeenCalledWith('new.maps.com'); + expect(mockMap.setStyle).toHaveBeenCalledWith("new.maps.com"); }); - it('does not update map style when style url is truthy', () => { + it("does not update map style when style url is truthy", () => { getComponent(); act(() => { @@ -91,58 +91,60 @@ describe('', () => { }); }); - describe('map centering', () => { - it('does not try to center when no center uri provided', () => { + describe("map centering", () => { + it("does not try to center when no center uri provided", () => { getComponent({ centerGeoUri: null }); expect(mockMap.setCenter).not.toHaveBeenCalled(); }); - it('sets map center to centerGeoUri', () => { - getComponent({ centerGeoUri: 'geo:51,42' }); + it("sets map center to centerGeoUri", () => { + getComponent({ centerGeoUri: "geo:51,42" }); expect(mockMap.setCenter).toHaveBeenCalledWith({ lat: 51, lon: 42 }); }); - it('handles invalid centerGeoUri', () => { - const logSpy = jest.spyOn(logger, 'error').mockImplementation(); - getComponent({ centerGeoUri: '123 Sesame Street' }); + it("handles invalid centerGeoUri", () => { + const logSpy = jest.spyOn(logger, "error").mockImplementation(); + getComponent({ centerGeoUri: "123 Sesame Street" }); expect(mockMap.setCenter).not.toHaveBeenCalled(); - expect(logSpy).toHaveBeenCalledWith('Could not set map center'); + expect(logSpy).toHaveBeenCalledWith("Could not set map center"); }); - it('updates map center when centerGeoUri prop changes', () => { - const component = getComponent({ centerGeoUri: 'geo:51,42' }); + it("updates map center when centerGeoUri prop changes", () => { + const component = getComponent({ centerGeoUri: "geo:51,42" }); - component.setProps({ centerGeoUri: 'geo:53,45' }); - component.setProps({ centerGeoUri: 'geo:56,47' }); + component.setProps({ centerGeoUri: "geo:53,45" }); + component.setProps({ centerGeoUri: "geo:56,47" }); expect(mockMap.setCenter).toHaveBeenCalledWith({ lat: 51, lon: 42 }); expect(mockMap.setCenter).toHaveBeenCalledWith({ lat: 53, lon: 45 }); expect(mockMap.setCenter).toHaveBeenCalledWith({ lat: 56, lon: 47 }); }); }); - describe('map bounds', () => { - it('does not try to fit map bounds when no bounds provided', () => { + describe("map bounds", () => { + it("does not try to fit map bounds when no bounds provided", () => { getComponent({ bounds: null }); expect(mockMap.fitBounds).not.toHaveBeenCalled(); }); - it('fits map to bounds', () => { + it("fits map to bounds", () => { const bounds = { north: 51, south: 50, east: 42, west: 41 }; getComponent({ bounds }); - expect(mockMap.fitBounds).toHaveBeenCalledWith(new maplibregl.LngLatBounds([bounds.west, bounds.south], - [bounds.east, bounds.north]), { padding: 100, maxZoom: 15 }); + expect(mockMap.fitBounds).toHaveBeenCalledWith( + new maplibregl.LngLatBounds([bounds.west, bounds.south], [bounds.east, bounds.north]), + { padding: 100, maxZoom: 15 }, + ); }); - it('handles invalid bounds', () => { - const logSpy = jest.spyOn(logger, 'error').mockImplementation(); - const bounds = { north: 'a', south: 'b', east: 42, west: 41 }; + it("handles invalid bounds", () => { + const logSpy = jest.spyOn(logger, "error").mockImplementation(); + const bounds = { north: "a", south: "b", east: 42, west: 41 }; getComponent({ bounds }); expect(mockMap.fitBounds).not.toHaveBeenCalled(); - expect(logSpy).toHaveBeenCalledWith('Invalid map bounds'); + expect(logSpy).toHaveBeenCalledWith("Invalid map bounds"); }); - it('updates map bounds when bounds prop changes', () => { - const component = getComponent({ centerGeoUri: 'geo:51,42' }); + it("updates map bounds when bounds prop changes", () => { + const component = getComponent({ centerGeoUri: "geo:51,42" }); const bounds = { north: 51, south: 50, east: 42, west: 41 }; const bounds2 = { north: 53, south: 51, east: 45, west: 44 }; @@ -152,8 +154,8 @@ describe('', () => { }); }); - describe('children', () => { - it('renders without children', () => { + describe("children", () => { + it("renders without children", () => { const component = getComponent({ children: null }); component.setProps({}); @@ -162,18 +164,22 @@ describe('', () => { expect(component).toBeTruthy(); }); - it('renders children with map renderProp', () => { - const children = ({ map }) =>
Hello, world
; + it("renders children with map renderProp", () => { + const children = ({ map }) => ( +
+ Hello, world +
+ ); const component = getComponent({ children }); // renders child with map instance - expect(findByTestId(component, 'test-child').props()['data-map']).toEqual(mockMap); + expect(findByTestId(component, "test-child").props()["data-map"]).toEqual(mockMap); }); }); - describe('onClick', () => { - it('eats clicks to maplibre attribution button', () => { + describe("onClick", () => { + it("eats clicks to maplibre attribution button", () => { const onClick = jest.fn(); const component = getComponent({ onClick }); @@ -181,20 +187,20 @@ describe('', () => { // this is added to the dom by maplibregl // which is mocked // just fake the target - const fakeEl = document.createElement('div'); - fakeEl.className = 'maplibregl-ctrl-attrib-button'; - component.simulate('click', { target: fakeEl }); + const fakeEl = document.createElement("div"); + fakeEl.className = "maplibregl-ctrl-attrib-button"; + component.simulate("click", { target: fakeEl }); }); expect(onClick).not.toHaveBeenCalled(); }); - it('calls onClick', () => { + it("calls onClick", () => { const onClick = jest.fn(); const component = getComponent({ onClick }); act(() => { - component.simulate('click'); + component.simulate("click"); }); expect(onClick).toHaveBeenCalled(); diff --git a/test/components/views/location/MapError-test.tsx b/test/components/views/location/MapError-test.tsx index 27a42dd95a2..94e97c6f9e0 100644 --- a/test/components/views/location/MapError-test.tsx +++ b/test/components/views/location/MapError-test.tsx @@ -14,43 +14,43 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import { render, RenderResult } from '@testing-library/react'; +import React from "react"; +import { render, RenderResult } from "@testing-library/react"; -import { MapError, MapErrorProps } from '../../../../src/components/views/location/MapError'; -import { LocationShareError } from '../../../../src/utils/location'; +import { MapError, MapErrorProps } from "../../../../src/components/views/location/MapError"; +import { LocationShareError } from "../../../../src/utils/location"; -describe('', () => { +describe("", () => { const defaultProps = { onFinished: jest.fn(), error: LocationShareError.MapStyleUrlNotConfigured, - className: 'test', + className: "test", }; const getComponent = (props: Partial = {}): RenderResult => render(); - it('renders correctly for MapStyleUrlNotConfigured', () => { + it("renders correctly for MapStyleUrlNotConfigured", () => { const { container } = getComponent(); expect(container).toMatchSnapshot(); }); - it('renders correctly for MapStyleUrlNotReachable', () => { + it("renders correctly for MapStyleUrlNotReachable", () => { const { container } = getComponent({ error: LocationShareError.MapStyleUrlNotReachable, }); expect(container).toMatchSnapshot(); }); - it('does not render button when onFinished falsy', () => { + it("does not render button when onFinished falsy", () => { const { queryByText } = getComponent({ error: LocationShareError.MapStyleUrlNotReachable, onFinished: undefined, }); // no button - expect(queryByText('OK')).toBeFalsy(); + expect(queryByText("OK")).toBeFalsy(); }); - it('applies class when isMinimised is truthy', () => { + it("applies class when isMinimised is truthy", () => { const { container } = getComponent({ isMinimised: true, }); diff --git a/test/components/views/location/Marker-test.tsx b/test/components/views/location/Marker-test.tsx index 767f1097bd3..841a3fa47e6 100644 --- a/test/components/views/location/Marker-test.tsx +++ b/test/components/views/location/Marker-test.tsx @@ -14,39 +14,38 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React from "react"; // eslint-disable-next-line deprecate/import -import { mount } from 'enzyme'; -import { RoomMember } from 'matrix-js-sdk/src/matrix'; +import { mount } from "enzyme"; +import { RoomMember } from "matrix-js-sdk/src/matrix"; -import Marker from '../../../../src/components/views/location/Marker'; +import Marker from "../../../../src/components/views/location/Marker"; -describe('', () => { +describe("", () => { const defaultProps = { - id: 'abc123', + id: "abc123", }; - const getComponent = (props = {}) => - mount(); + const getComponent = (props = {}) => mount(); - it('renders with location icon when no room member', () => { + it("renders with location icon when no room member", () => { const component = getComponent(); expect(component).toMatchSnapshot(); }); - it('does not try to use member color without room member', () => { + it("does not try to use member color without room member", () => { const component = getComponent({ useMemberColor: true }); - expect(component.find('div').at(0).props().className).toEqual('mx_Marker mx_Marker_defaultColor'); + expect(component.find("div").at(0).props().className).toEqual("mx_Marker mx_Marker_defaultColor"); }); - it('uses member color class', () => { - const member = new RoomMember('!room:server', '@user:server'); + it("uses member color class", () => { + const member = new RoomMember("!room:server", "@user:server"); const component = getComponent({ useMemberColor: true, roomMember: member }); - expect(component.find('div').at(0).props().className).toEqual('mx_Marker mx_Username_color3'); + expect(component.find("div").at(0).props().className).toEqual("mx_Marker mx_Username_color3"); }); - it('renders member avatar when roomMember is truthy', () => { - const member = new RoomMember('!room:server', '@user:server'); + it("renders member avatar when roomMember is truthy", () => { + const member = new RoomMember("!room:server", "@user:server"); const component = getComponent({ roomMember: member }); - expect(component.find('MemberAvatar').length).toBeTruthy(); + expect(component.find("MemberAvatar").length).toBeTruthy(); }); }); diff --git a/test/components/views/location/SmartMarker-test.tsx b/test/components/views/location/SmartMarker-test.tsx index 3f617bcb5d4..569c80638a0 100644 --- a/test/components/views/location/SmartMarker-test.tsx +++ b/test/components/views/location/SmartMarker-test.tsx @@ -14,34 +14,33 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React from "react"; // eslint-disable-next-line deprecate/import -import { mount } from 'enzyme'; -import { mocked } from 'jest-mock'; -import maplibregl from 'maplibre-gl'; +import { mount } from "enzyme"; +import { mocked } from "jest-mock"; +import maplibregl from "maplibre-gl"; -import SmartMarker from '../../../../src/components/views/location/SmartMarker'; +import SmartMarker from "../../../../src/components/views/location/SmartMarker"; -jest.mock('../../../../src/utils/location/findMapStyleUrl', () => ({ - findMapStyleUrl: jest.fn().mockReturnValue('tileserver.com'), +jest.mock("../../../../src/utils/location/findMapStyleUrl", () => ({ + findMapStyleUrl: jest.fn().mockReturnValue("tileserver.com"), })); -describe('', () => { +describe("", () => { const mockMap = new maplibregl.Map(); const mockMarker = new maplibregl.Marker(); const defaultProps = { map: mockMap, - geoUri: 'geo:43.2,54.6', + geoUri: "geo:43.2,54.6", }; - const getComponent = (props = {}) => - mount(); + const getComponent = (props = {}) => mount(); beforeEach(() => { jest.clearAllMocks(); }); - it('creates a marker on mount', () => { + it("creates a marker on mount", () => { const component = getComponent(); expect(component).toMatchSnapshot(); @@ -55,11 +54,11 @@ describe('', () => { expect(mockMarker.addTo).toHaveBeenCalledWith(mockMap); }); - it('updates marker position on change', () => { - const component = getComponent({ geoUri: 'geo:40,50' }); + it("updates marker position on change", () => { + const component = getComponent({ geoUri: "geo:40,50" }); - component.setProps({ geoUri: 'geo:41,51' }); - component.setProps({ geoUri: 'geo:42,52' }); + component.setProps({ geoUri: "geo:41,51" }); + component.setProps({ geoUri: "geo:42,52" }); // marker added only once expect(maplibregl.Marker).toHaveBeenCalledTimes(1); @@ -69,7 +68,7 @@ describe('', () => { expect(mocked(mockMarker.setLngLat)).toHaveBeenCalledWith({ lat: 42, lon: 52 }); }); - it('removes marker on unmount', () => { + it("removes marker on unmount", () => { const component = getComponent(); expect(component).toMatchSnapshot(); diff --git a/test/components/views/location/ZoomButtons-test.tsx b/test/components/views/location/ZoomButtons-test.tsx index 1192b6aed07..5c5b63b299d 100644 --- a/test/components/views/location/ZoomButtons-test.tsx +++ b/test/components/views/location/ZoomButtons-test.tsx @@ -14,48 +14,47 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React from "react"; // eslint-disable-next-line deprecate/import -import { mount } from 'enzyme'; -import maplibregl from 'maplibre-gl'; -import { act } from 'react-dom/test-utils'; +import { mount } from "enzyme"; +import maplibregl from "maplibre-gl"; +import { act } from "react-dom/test-utils"; -import ZoomButtons from '../../../../src/components/views/location/ZoomButtons'; -import { findByTestId } from '../../../test-utils'; +import ZoomButtons from "../../../../src/components/views/location/ZoomButtons"; +import { findByTestId } from "../../../test-utils"; -describe('', () => { +describe("", () => { const mockMap = new maplibregl.Map(); const defaultProps = { map: mockMap, }; - const getComponent = (props = {}) => - mount(); + const getComponent = (props = {}) => mount(); beforeEach(() => { jest.clearAllMocks(); }); - it('renders buttons', () => { + it("renders buttons", () => { const component = getComponent(); expect(component).toMatchSnapshot(); }); - it('calls map zoom in on zoom in click', () => { + it("calls map zoom in on zoom in click", () => { const component = getComponent(); act(() => { - findByTestId(component, 'map-zoom-in-button').at(0).simulate('click'); + findByTestId(component, "map-zoom-in-button").at(0).simulate("click"); }); expect(mockMap.zoomIn).toHaveBeenCalled(); expect(component).toBeTruthy(); }); - it('calls map zoom out on zoom out click', () => { + it("calls map zoom out on zoom out click", () => { const component = getComponent(); act(() => { - findByTestId(component, 'map-zoom-out-button').at(0).simulate('click'); + findByTestId(component, "map-zoom-out-button").at(0).simulate("click"); }); expect(mockMap.zoomOut).toHaveBeenCalled(); diff --git a/test/components/views/location/shareLocation-test.ts b/test/components/views/location/shareLocation-test.ts index 63658045f82..910d4713627 100644 --- a/test/components/views/location/shareLocation-test.ts +++ b/test/components/views/location/shareLocation-test.ts @@ -47,13 +47,11 @@ describe("shareLocation", () => { } as unknown as MatrixClient; mocked(makeLocationContent).mockReturnValue(content); - mocked(doMaybeLocalRoomAction).mockImplementation(( - roomId: string, - fn: (actualRoomId: string) => Promise, - client?: MatrixClient, - ) => { - return fn(roomId); - }); + mocked(doMaybeLocalRoomAction).mockImplementation( + (roomId: string, fn: (actualRoomId: string) => Promise, client?: MatrixClient) => { + return fn(roomId); + }, + ); shareLocationFn = shareLocation(client, roomId, shareType, null, () => {}); }); diff --git a/test/components/views/messages/CallEvent-test.tsx b/test/components/views/messages/CallEvent-test.tsx index 04ca0a4bf78..c1f04e8978e 100644 --- a/test/components/views/messages/CallEvent-test.tsx +++ b/test/components/views/messages/CallEvent-test.tsx @@ -68,16 +68,18 @@ describe("CallEvent", () => { alice = mkRoomMember(room.roomId, "@alice:example.org"); bob = mkRoomMember(room.roomId, "@bob:example.org"); jest.spyOn(room, "getMember").mockImplementation( - userId => [alice, bob].find(member => member.userId === userId) ?? null, + (userId) => [alice, bob].find((member) => member.userId === userId) ?? null, ); - client.getRoom.mockImplementation(roomId => roomId === room.roomId ? room : null); + client.getRoom.mockImplementation((roomId) => (roomId === room.roomId ? room : null)); client.getRooms.mockReturnValue([room]); client.reEmitter.reEmit(room, [RoomStateEvent.Events]); - await Promise.all([CallStore.instance, WidgetMessagingStore.instance].map( - store => setupAsyncStoreWithClient(store, client), - )); + await Promise.all( + [CallStore.instance, WidgetMessagingStore.instance].map((store) => + setupAsyncStoreWithClient(store, client), + ), + ); MockedCall.create(room, "1"); const maybeCall = CallStore.instance.getCall(room.roomId); @@ -99,7 +101,9 @@ describe("CallEvent", () => { jest.restoreAllMocks(); }); - const renderEvent = () => { render(); }; + const renderEvent = () => { + render(); + }; it("shows a message and duration if the call was ended", () => { jest.advanceTimersByTime(90000); @@ -121,7 +125,10 @@ describe("CallEvent", () => { it("shows call details and connection controls if the call is loaded", async () => { jest.advanceTimersByTime(90000); - call.participants = new Set([alice, bob]); + call.participants = new Map([ + [alice, new Set(["a"])], + [bob, new Set(["b"])], + ]); renderEvent(); screen.getByText("@alice:example.org started a video call"); @@ -132,11 +139,13 @@ describe("CallEvent", () => { const dispatcherSpy = jest.fn(); const dispatcherRef = defaultDispatcher.register(dispatcherSpy); fireEvent.click(screen.getByRole("button", { name: "Join" })); - await waitFor(() => expect(dispatcherSpy).toHaveBeenCalledWith({ - action: Action.ViewRoom, - room_id: room.roomId, - view_call: true, - })); + await waitFor(() => + expect(dispatcherSpy).toHaveBeenCalledWith({ + action: Action.ViewRoom, + room_id: room.roomId, + view_call: true, + }), + ); defaultDispatcher.unregister(dispatcherRef); await act(() => call.connect()); diff --git a/test/components/views/messages/DateSeparator-test.tsx b/test/components/views/messages/DateSeparator-test.tsx index 58a7a77d9bb..9bcf371f24a 100644 --- a/test/components/views/messages/DateSeparator-test.tsx +++ b/test/components/views/messages/DateSeparator-test.tsx @@ -31,7 +31,7 @@ describe("DateSeparator", () => { const HOUR_MS = 3600000; const DAY_MS = HOUR_MS * 24; // Friday Dec 17 2021, 9:09am - const now = '2021-12-17T08:09:00.000Z'; + const now = "2021-12-17T08:09:00.000Z"; const nowMs = 1639728540000; const defaultProps = { ts: nowMs, @@ -47,23 +47,23 @@ describe("DateSeparator", () => { const mockClient = getMockClientWithEventEmitter({}); const getComponent = (props = {}) => - render(( + render( - - )); + , + ); type TestCase = [string, number, string]; const testCases: TestCase[] = [ - ['the exact same moment', nowMs, 'Today'], - ['same day as current day', nowMs - HOUR_MS, 'Today'], - ['day before the current day', nowMs - (HOUR_MS * 12), 'Yesterday'], - ['2 days ago', nowMs - DAY_MS * 2, 'Wednesday'], - ['144 hours ago', nowMs - HOUR_MS * 144, 'Sat, Dec 11 2021'], + ["the exact same moment", nowMs, "Today"], + ["same day as current day", nowMs - HOUR_MS, "Today"], + ["day before the current day", nowMs - HOUR_MS * 12, "Yesterday"], + ["2 days ago", nowMs - DAY_MS * 2, "Wednesday"], + ["144 hours ago", nowMs - HOUR_MS * 144, "Sat, Dec 11 2021"], [ - '6 days ago, but less than 144h', - new Date('Saturday Dec 11 2021 23:59:00 GMT+0100 (Central European Standard Time)').getTime(), - 'Saturday', + "6 days ago, but less than 144h", + new Date("Saturday Dec 11 2021 23:59:00 GMT+0100 (Central European Standard Time)").getTime(), + "Saturday", ], ]; @@ -80,24 +80,25 @@ describe("DateSeparator", () => { global.Date = RealDate; }); - it('renders the date separator correctly', () => { + it("renders the date separator correctly", () => { const { asFragment } = getComponent(); expect(asFragment()).toMatchSnapshot(); expect(SettingsStore.getValue).toHaveBeenCalledWith(UIFeature.TimelineEnableRelativeDates); }); - it.each(testCases)('formats date correctly when current time is %s', (_d, ts, result) => { + it.each(testCases)("formats date correctly when current time is %s", (_d, ts, result) => { expect(getComponent({ ts, forExport: false }).container.textContent).toEqual(result); }); - describe('when forExport is true', () => { - it.each(testCases)('formats date in full when current time is %s', (_d, ts) => { - expect(getComponent({ ts, forExport: true }).container.textContent) - .toEqual(formatFullDateNoTime(new Date(ts))); + describe("when forExport is true", () => { + it.each(testCases)("formats date in full when current time is %s", (_d, ts) => { + expect(getComponent({ ts, forExport: true }).container.textContent).toEqual( + formatFullDateNoTime(new Date(ts)), + ); }); }); - describe('when Settings.TimelineEnableRelativeDates is falsy', () => { + describe("when Settings.TimelineEnableRelativeDates is falsy", () => { beforeEach(() => { (SettingsStore.getValue as jest.Mock) = jest.fn((arg) => { if (arg === UIFeature.TimelineEnableRelativeDates) { @@ -105,13 +106,14 @@ describe("DateSeparator", () => { } }); }); - it.each(testCases)('formats date in full when current time is %s', (_d, ts) => { - expect(getComponent({ ts, forExport: false }).container.textContent) - .toEqual(formatFullDateNoTime(new Date(ts))); + it.each(testCases)("formats date in full when current time is %s", (_d, ts) => { + expect(getComponent({ ts, forExport: false }).container.textContent).toEqual( + formatFullDateNoTime(new Date(ts)), + ); }); }); - describe('when feature_jump_to_date is enabled', () => { + describe("when feature_jump_to_date is enabled", () => { beforeEach(() => { mocked(SettingsStore).getValue.mockImplementation((arg): any => { if (arg === "feature_jump_to_date") { @@ -119,7 +121,7 @@ describe("DateSeparator", () => { } }); }); - it('renders the date separator correctly', () => { + it("renders the date separator correctly", () => { const { asFragment } = getComponent(); expect(asFragment()).toMatchSnapshot(); }); diff --git a/test/components/views/messages/EncryptionEvent-test.tsx b/test/components/views/messages/EncryptionEvent-test.tsx index 7b70fa4f6fc..11268e077a1 100644 --- a/test/components/views/messages/EncryptionEvent-test.tsx +++ b/test/components/views/messages/EncryptionEvent-test.tsx @@ -14,22 +14,24 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React from "react"; import { mocked } from "jest-mock"; import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; -import { render, screen } from '@testing-library/react'; +import { render, screen } from "@testing-library/react"; import EncryptionEvent from "../../../../src/components/views/messages/EncryptionEvent"; import { createTestClient, mkMessage } from "../../../test-utils"; -import { MatrixClientPeg } from '../../../../src/MatrixClientPeg'; -import { LocalRoom } from '../../../../src/models/LocalRoom'; -import DMRoomMap from '../../../../src/utils/DMRoomMap'; -import MatrixClientContext from '../../../../src/contexts/MatrixClientContext'; +import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; +import { LocalRoom } from "../../../../src/models/LocalRoom"; +import DMRoomMap from "../../../../src/utils/DMRoomMap"; +import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; const renderEncryptionEvent = (client: MatrixClient, event: MatrixEvent) => { - render( - - ); + render( + + + , + ); }; const checkTexts = (title: string, subTitle: string) => { @@ -69,8 +71,8 @@ describe("EncryptionEvent", () => { renderEncryptionEvent(client, event); checkTexts( "Encryption enabled", - "Messages in this room are end-to-end encrypted. " - + "When people join, you can verify them in their profile, just tap on their avatar.", + "Messages in this room are end-to-end encrypted. " + + "When people join, you can verify them in their profile, just tap on their avatar.", ); }); @@ -83,10 +85,7 @@ describe("EncryptionEvent", () => { it("should show the expected texts", () => { renderEncryptionEvent(client, event); - checkTexts( - "Encryption enabled", - "Some encryption parameters have been changed.", - ); + checkTexts("Encryption enabled", "Some encryption parameters have been changed."); }); }); diff --git a/test/components/views/messages/MBeaconBody-test.tsx b/test/components/views/messages/MBeaconBody-test.tsx index dbfa1f39d13..b779c35a9ca 100644 --- a/test/components/views/messages/MBeaconBody-test.tsx +++ b/test/components/views/messages/MBeaconBody-test.tsx @@ -14,68 +14,58 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React from "react"; // eslint-disable-next-line deprecate/import -import { mount } from 'enzyme'; -import { act } from 'react-dom/test-utils'; -import maplibregl from 'maplibre-gl'; -import { - BeaconEvent, - getBeaconInfoIdentifier, - RelationType, - MatrixEvent, - EventType, -} from 'matrix-js-sdk/src/matrix'; -import { Relations } from 'matrix-js-sdk/src/models/relations'; -import { M_BEACON } from 'matrix-js-sdk/src/@types/beacon'; - -import MBeaconBody from '../../../../src/components/views/messages/MBeaconBody'; +import { mount } from "enzyme"; +import { act } from "react-dom/test-utils"; +import maplibregl from "maplibre-gl"; +import { BeaconEvent, getBeaconInfoIdentifier, RelationType, MatrixEvent, EventType } from "matrix-js-sdk/src/matrix"; +import { Relations } from "matrix-js-sdk/src/models/relations"; +import { M_BEACON } from "matrix-js-sdk/src/@types/beacon"; + +import MBeaconBody from "../../../../src/components/views/messages/MBeaconBody"; import { getMockClientWithEventEmitter, makeBeaconEvent, makeBeaconInfoEvent, makeRoomWithBeacons, makeRoomWithStateEvents, -} from '../../../test-utils'; -import { RoomPermalinkCreator } from '../../../../src/utils/permalinks/Permalinks'; -import { MediaEventHelper } from '../../../../src/utils/MediaEventHelper'; -import MatrixClientContext from '../../../../src/contexts/MatrixClientContext'; -import Modal from '../../../../src/Modal'; -import { TILE_SERVER_WK_KEY } from '../../../../src/utils/WellKnownUtils'; -import { MapError } from '../../../../src/components/views/location/MapError'; -import * as mapUtilHooks from '../../../../src/utils/location/useMap'; -import { LocationShareError } from '../../../../src/utils/location'; - -describe('', () => { +} from "../../../test-utils"; +import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks"; +import { MediaEventHelper } from "../../../../src/utils/MediaEventHelper"; +import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; +import Modal from "../../../../src/Modal"; +import { TILE_SERVER_WK_KEY } from "../../../../src/utils/WellKnownUtils"; +import { MapError } from "../../../../src/components/views/location/MapError"; +import * as mapUtilHooks from "../../../../src/utils/location/useMap"; +import { LocationShareError } from "../../../../src/utils/location"; + +describe("", () => { // 14.03.2022 16:15 const now = 1647270879403; // stable date for snapshots - jest.spyOn(global.Date, 'now').mockReturnValue(now); - const roomId = '!room:server'; - const aliceId = '@alice:server'; + jest.spyOn(global.Date, "now").mockReturnValue(now); + const roomId = "!room:server"; + const aliceId = "@alice:server"; const mockMap = new maplibregl.Map(); const mockMarker = new maplibregl.Marker(); const mockClient = getMockClientWithEventEmitter({ getClientWellKnown: jest.fn().mockReturnValue({ - [TILE_SERVER_WK_KEY.name]: { map_style_url: 'maps.com' }, + [TILE_SERVER_WK_KEY.name]: { map_style_url: "maps.com" }, }), getUserId: jest.fn().mockReturnValue(aliceId), getRoom: jest.fn(), redactEvent: jest.fn(), }); - const defaultEvent = makeBeaconInfoEvent(aliceId, - roomId, - { isLive: true }, - '$alice-room1-1', - ); + const defaultEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: true }, "$alice-room1-1"); const defaultProps = { mxEvent: defaultEvent, highlights: [], - highlightLink: '', + highlightLink: "", onHeightChanged: jest.fn(), onMessageAllowed: jest.fn(), // we dont use these and they pollute the snapshots @@ -89,7 +79,7 @@ describe('', () => { wrappingComponentProps: { value: mockClient }, }); - const modalSpy = jest.spyOn(Modal, 'createDialog').mockReturnValue(undefined); + const modalSpy = jest.spyOn(Modal, "createDialog").mockReturnValue(undefined); beforeAll(() => { maplibregl.AttributionControl = jest.fn(); @@ -100,73 +90,66 @@ describe('', () => { }); const testBeaconStatuses = () => { - it('renders stopped beacon UI for an explicitly stopped beacon', () => { - const beaconInfoEvent = makeBeaconInfoEvent(aliceId, - roomId, - { isLive: false }, - '$alice-room1-1', - ); + it("renders stopped beacon UI for an explicitly stopped beacon", () => { + const beaconInfoEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: false }, "$alice-room1-1"); makeRoomWithStateEvents([beaconInfoEvent], { roomId, mockClient }); const component = getComponent({ mxEvent: beaconInfoEvent }); expect(component.text()).toEqual("Live location ended"); }); - it('renders stopped beacon UI for an expired beacon', () => { - const beaconInfoEvent = makeBeaconInfoEvent(aliceId, + it("renders stopped beacon UI for an expired beacon", () => { + const beaconInfoEvent = makeBeaconInfoEvent( + aliceId, roomId, // puts this beacons live period in the past { isLive: true, timestamp: now - 600000, timeout: 500 }, - '$alice-room1-1', + "$alice-room1-1", ); makeRoomWithStateEvents([beaconInfoEvent], { roomId, mockClient }); const component = getComponent({ mxEvent: beaconInfoEvent }); expect(component.text()).toEqual("Live location ended"); }); - it('renders loading beacon UI for a beacon that has not started yet', () => { + it("renders loading beacon UI for a beacon that has not started yet", () => { const beaconInfoEvent = makeBeaconInfoEvent( aliceId, roomId, // puts this beacons start timestamp in the future { isLive: true, timestamp: now + 60000, timeout: 500 }, - '$alice-room1-1', + "$alice-room1-1", ); makeRoomWithStateEvents([beaconInfoEvent], { roomId, mockClient }); const component = getComponent({ mxEvent: beaconInfoEvent }); expect(component.text()).toEqual("Loading live location..."); }); - it('does not open maximised map when on click when beacon is stopped', () => { - const beaconInfoEvent = makeBeaconInfoEvent(aliceId, + it("does not open maximised map when on click when beacon is stopped", () => { + const beaconInfoEvent = makeBeaconInfoEvent( + aliceId, roomId, // puts this beacons live period in the past { isLive: true, timestamp: now - 600000, timeout: 500 }, - '$alice-room1-1', + "$alice-room1-1", ); makeRoomWithStateEvents([beaconInfoEvent], { roomId, mockClient }); const component = getComponent({ mxEvent: beaconInfoEvent }); act(() => { - component.find('.mx_MBeaconBody_map').at(0).simulate('click'); + component.find(".mx_MBeaconBody_map").at(0).simulate("click"); }); expect(modalSpy).not.toHaveBeenCalled(); }); - it('renders stopped UI when a beacon event is not the latest beacon for a user', () => { + it("renders stopped UI when a beacon event is not the latest beacon for a user", () => { const aliceBeaconInfo1 = makeBeaconInfoEvent( aliceId, roomId, // this one is a little older { isLive: true, timestamp: now - 500 }, - '$alice-room1-1', + "$alice-room1-1", ); aliceBeaconInfo1.event.origin_server_ts = now - 500; - const aliceBeaconInfo2 = makeBeaconInfoEvent( - aliceId, - roomId, - { isLive: true }, - '$alice-room1-2', - ); + const aliceBeaconInfo2 = makeBeaconInfoEvent(aliceId, roomId, { isLive: true }, "$alice-room1-2"); makeRoomWithStateEvents([aliceBeaconInfo1, aliceBeaconInfo2], { roomId, mockClient }); @@ -175,21 +158,16 @@ describe('', () => { expect(component.text()).toEqual("Live location ended"); }); - it('renders stopped UI when a beacon event is replaced', () => { + it("renders stopped UI when a beacon event is replaced", () => { const aliceBeaconInfo1 = makeBeaconInfoEvent( aliceId, roomId, // this one is a little older { isLive: true, timestamp: now - 500 }, - '$alice-room1-1', + "$alice-room1-1", ); aliceBeaconInfo1.event.origin_server_ts = now - 500; - const aliceBeaconInfo2 = makeBeaconInfoEvent( - aliceId, - roomId, - { isLive: true }, - '$alice-room1-2', - ); + const aliceBeaconInfo2 = makeBeaconInfoEvent(aliceId, roomId, { isLive: true }, "$alice-room1-2"); const room = makeRoomWithStateEvents([aliceBeaconInfo1], { roomId, mockClient }); const component = getComponent({ mxEvent: aliceBeaconInfo1 }); @@ -210,14 +188,9 @@ describe('', () => { testBeaconStatuses(); - describe('on liveness change', () => { - it('renders stopped UI when a beacon stops being live', () => { - const aliceBeaconInfo = makeBeaconInfoEvent( - aliceId, - roomId, - { isLive: true }, - '$alice-room1-1', - ); + describe("on liveness change", () => { + it("renders stopped UI when a beacon stops being live", () => { + const aliceBeaconInfo = makeBeaconInfoEvent(aliceId, roomId, { isLive: true }, "$alice-room1-1"); const room = makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient }); const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(aliceBeaconInfo)); @@ -236,97 +209,96 @@ describe('', () => { }); }); - describe('latestLocationState', () => { - const aliceBeaconInfo = makeBeaconInfoEvent( - aliceId, - roomId, - { isLive: true }, - '$alice-room1-1', - ); - - const location1 = makeBeaconEvent( - aliceId, { beaconInfoId: aliceBeaconInfo.getId(), geoUri: 'geo:51,41', timestamp: now + 1 }, - ); - const location2 = makeBeaconEvent( - aliceId, { beaconInfoId: aliceBeaconInfo.getId(), geoUri: 'geo:52,42', timestamp: now + 10000 }, - ); - - it('renders a live beacon without a location correctly', () => { + describe("latestLocationState", () => { + const aliceBeaconInfo = makeBeaconInfoEvent(aliceId, roomId, { isLive: true }, "$alice-room1-1"); + + const location1 = makeBeaconEvent(aliceId, { + beaconInfoId: aliceBeaconInfo.getId(), + geoUri: "geo:51,41", + timestamp: now + 1, + }); + const location2 = makeBeaconEvent(aliceId, { + beaconInfoId: aliceBeaconInfo.getId(), + geoUri: "geo:52,42", + timestamp: now + 10000, + }); + + it("renders a live beacon without a location correctly", () => { makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient }); const component = getComponent({ mxEvent: aliceBeaconInfo }); expect(component.text()).toEqual("Loading live location..."); }); - it('does nothing on click when a beacon has no location', () => { + it("does nothing on click when a beacon has no location", () => { makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient }); const component = getComponent({ mxEvent: aliceBeaconInfo }); act(() => { - component.find('.mx_MBeaconBody_map').at(0).simulate('click'); + component.find(".mx_MBeaconBody_map").at(0).simulate("click"); }); expect(modalSpy).not.toHaveBeenCalled(); }); - it('renders a live beacon with a location correctly', () => { + it("renders a live beacon with a location correctly", () => { const room = makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient }); const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(aliceBeaconInfo)); beaconInstance.addLocations([location1]); const component = getComponent({ mxEvent: aliceBeaconInfo }); - expect(component.find('Map').length).toBeTruthy; + expect(component.find("Map").length).toBeTruthy; }); - it('opens maximised map view on click when beacon has a live location', () => { + it("opens maximised map view on click when beacon has a live location", () => { const room = makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient }); const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(aliceBeaconInfo)); beaconInstance.addLocations([location1]); const component = getComponent({ mxEvent: aliceBeaconInfo }); act(() => { - component.find('Map').simulate('click'); + component.find("Map").simulate("click"); }); // opens modal expect(modalSpy).toHaveBeenCalled(); }); - it('does nothing on click when a beacon has no location', () => { + it("does nothing on click when a beacon has no location", () => { makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient }); const component = getComponent({ mxEvent: aliceBeaconInfo }); act(() => { - component.find('.mx_MBeaconBody_map').at(0).simulate('click'); + component.find(".mx_MBeaconBody_map").at(0).simulate("click"); }); expect(modalSpy).not.toHaveBeenCalled(); }); - it('renders a live beacon with a location correctly', () => { + it("renders a live beacon with a location correctly", () => { const room = makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient }); const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(aliceBeaconInfo)); beaconInstance.addLocations([location1]); const component = getComponent({ mxEvent: aliceBeaconInfo }); - expect(component.find('Map').length).toBeTruthy; + expect(component.find("Map").length).toBeTruthy; }); - it('opens maximised map view on click when beacon has a live location', () => { + it("opens maximised map view on click when beacon has a live location", () => { const room = makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient }); const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(aliceBeaconInfo)); beaconInstance.addLocations([location1]); const component = getComponent({ mxEvent: aliceBeaconInfo }); act(() => { - component.find('Map').simulate('click'); + component.find("Map").simulate("click"); }); // opens modal expect(modalSpy).toHaveBeenCalled(); }); - it('updates latest location', () => { + it("updates latest location", () => { const room = makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient }); const component = getComponent({ mxEvent: aliceBeaconInfo }); @@ -349,33 +321,30 @@ describe('', () => { }); }); - describe('redaction', () => { + describe("redaction", () => { const makeEvents = (): { beaconInfoEvent: MatrixEvent; location1: MatrixEvent; location2: MatrixEvent; } => { - const beaconInfoEvent = makeBeaconInfoEvent( - aliceId, - roomId, - { isLive: true }, - '$alice-room1-1', - ); + const beaconInfoEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: true }, "$alice-room1-1"); const location1 = makeBeaconEvent( - aliceId, { beaconInfoId: beaconInfoEvent.getId(), geoUri: 'geo:51,41', timestamp: now + 1 }, + aliceId, + { beaconInfoId: beaconInfoEvent.getId(), geoUri: "geo:51,41", timestamp: now + 1 }, roomId, ); - location1.event.event_id = '1'; + location1.event.event_id = "1"; const location2 = makeBeaconEvent( - aliceId, { beaconInfoId: beaconInfoEvent.getId(), geoUri: 'geo:52,42', timestamp: now + 10000 }, + aliceId, + { beaconInfoId: beaconInfoEvent.getId(), geoUri: "geo:52,42", timestamp: now + 10000 }, roomId, ); - location2.event.event_id = '2'; + location2.event.event_id = "2"; return { beaconInfoEvent, location1, location2 }; }; - const redactionEvent = new MatrixEvent({ type: EventType.RoomRedaction, content: { reason: 'test reason' } }); + const redactionEvent = new MatrixEvent({ type: EventType.RoomRedaction, content: { reason: "test reason" } }); const setupRoomWithBeacon = (beaconInfoEvent, locationEvents: MatrixEvent[] = []) => { const room = makeRoomWithStateEvents([beaconInfoEvent], { roomId, mockClient }); @@ -384,14 +353,14 @@ describe('', () => { }; const mockGetRelationsForEvent = (locationEvents: MatrixEvent[] = []) => { const relations = new Relations(RelationType.Reference, M_BEACON.name, mockClient); - jest.spyOn(relations, 'getRelations').mockReturnValue(locationEvents); + jest.spyOn(relations, "getRelations").mockReturnValue(locationEvents); const getRelationsForEvent = jest.fn().mockReturnValue(relations); return getRelationsForEvent; }; - it('does nothing when getRelationsForEvent is falsy', () => { + it("does nothing when getRelationsForEvent is falsy", () => { const { beaconInfoEvent, location1, location2 } = makeEvents(); setupRoomWithBeacon(beaconInfoEvent, [location1, location2]); @@ -405,10 +374,10 @@ describe('', () => { expect(mockClient.redactEvent).not.toHaveBeenCalled(); }); - it('cleans up redaction listener on unmount', () => { + it("cleans up redaction listener on unmount", () => { const { beaconInfoEvent, location1, location2 } = makeEvents(); setupRoomWithBeacon(beaconInfoEvent, [location1, location2]); - const removeListenerSpy = jest.spyOn(beaconInfoEvent, 'removeListener'); + const removeListenerSpy = jest.spyOn(beaconInfoEvent, "removeListener"); const component = getComponent({ mxEvent: beaconInfoEvent }); @@ -419,7 +388,7 @@ describe('', () => { expect(removeListenerSpy).toHaveBeenCalled(); }); - it('does nothing when beacon has no related locations', async () => { + it("does nothing when beacon has no related locations", async () => { const { beaconInfoEvent } = makeEvents(); // no locations setupRoomWithBeacon(beaconInfoEvent, []); @@ -432,12 +401,14 @@ describe('', () => { }); expect(getRelationsForEvent).toHaveBeenCalledWith( - beaconInfoEvent.getId(), RelationType.Reference, M_BEACON.name, + beaconInfoEvent.getId(), + RelationType.Reference, + M_BEACON.name, ); expect(mockClient.redactEvent).not.toHaveBeenCalled(); }); - it('redacts related locations on beacon redaction', async () => { + it("redacts related locations on beacon redaction", async () => { const { beaconInfoEvent, location1, location2 } = makeEvents(); setupRoomWithBeacon(beaconInfoEvent, [location1, location2]); @@ -450,43 +421,36 @@ describe('', () => { }); expect(getRelationsForEvent).toHaveBeenCalledWith( - beaconInfoEvent.getId(), RelationType.Reference, M_BEACON.name, + beaconInfoEvent.getId(), + RelationType.Reference, + M_BEACON.name, ); expect(mockClient.redactEvent).toHaveBeenCalledTimes(2); - expect(mockClient.redactEvent).toHaveBeenCalledWith( - roomId, - location1.getId(), - undefined, - { reason: 'test reason' }, - ); - expect(mockClient.redactEvent).toHaveBeenCalledWith( - roomId, - location2.getId(), - undefined, - { reason: 'test reason' }, - ); + expect(mockClient.redactEvent).toHaveBeenCalledWith(roomId, location1.getId(), undefined, { + reason: "test reason", + }); + expect(mockClient.redactEvent).toHaveBeenCalledWith(roomId, location2.getId(), undefined, { + reason: "test reason", + }); }); }); - describe('when map display is not configured', () => { + describe("when map display is not configured", () => { beforeEach(() => { // mock map utils to raise MapStyleUrlNotConfigured error - jest.spyOn(mapUtilHooks, 'useMap').mockImplementation( - ({ onError }) => { - onError(new Error(LocationShareError.MapStyleUrlNotConfigured)); - return mockMap; - }); + jest.spyOn(mapUtilHooks, "useMap").mockImplementation(({ onError }) => { + onError(new Error(LocationShareError.MapStyleUrlNotConfigured)); + return mockMap; + }); }); - it('renders maps unavailable error for a live beacon with location', () => { - const beaconInfoEvent = makeBeaconInfoEvent(aliceId, - roomId, - { isLive: true }, - '$alice-room1-1', - ); - const location1 = makeBeaconEvent( - aliceId, { beaconInfoId: beaconInfoEvent.getId(), geoUri: 'geo:51,41', timestamp: now + 1 }, - ); + it("renders maps unavailable error for a live beacon with location", () => { + const beaconInfoEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: true }, "$alice-room1-1"); + const location1 = makeBeaconEvent(aliceId, { + beaconInfoId: beaconInfoEvent.getId(), + geoUri: "geo:51,41", + timestamp: now + 1, + }); makeRoomWithBeacons(roomId, mockClient, [beaconInfoEvent], [location1]); diff --git a/test/components/views/messages/MImageBody-test.tsx b/test/components/views/messages/MImageBody-test.tsx index b2cbb856025..2399fe0fd37 100644 --- a/test/components/views/messages/MImageBody-test.tsx +++ b/test/components/views/messages/MImageBody-test.tsx @@ -48,8 +48,8 @@ describe("", () => { getIgnoredUsers: jest.fn(), getVersions: jest.fn().mockResolvedValue({ unstable_features: { - 'org.matrix.msc3882': true, - 'org.matrix.msc3886': true, + "org.matrix.msc3882": true, + "org.matrix.msc3886": true, }, }), }); @@ -75,11 +75,13 @@ describe("", () => { it("should show error when encrypted media cannot be downloaded", async () => { fetchMock.getOnce(url, { status: 500 }); - render(); + render( + , + ); await screen.findByText("Error downloading image"); }); @@ -88,11 +90,13 @@ describe("", () => { fetchMock.getOnce(url, "thisistotallyanencryptedpng"); mocked(encrypt.decryptAttachment).mockRejectedValue(new Error("Failed to decrypt")); - render(); + render( + , + ); await screen.findByText("Error decrypting image"); }); diff --git a/test/components/views/messages/MKeyVerificationConclusion-test.tsx b/test/components/views/messages/MKeyVerificationConclusion-test.tsx index 5484282b6f9..03966d16735 100644 --- a/test/components/views/messages/MKeyVerificationConclusion-test.tsx +++ b/test/components/views/messages/MKeyVerificationConclusion-test.tsx @@ -14,33 +14,40 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import TestRenderer from 'react-test-renderer'; -import { EventEmitter } from 'events'; -import { MatrixEvent, EventType } from 'matrix-js-sdk/src/matrix'; -import { CryptoEvent } from 'matrix-js-sdk/src/crypto'; -import { UserTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning'; -import { VerificationRequest } from 'matrix-js-sdk/src/crypto/verification/request/VerificationRequest'; +import React from "react"; +import TestRenderer from "react-test-renderer"; +import { EventEmitter } from "events"; +import { MatrixEvent, EventType } from "matrix-js-sdk/src/matrix"; +import { CryptoEvent } from "matrix-js-sdk/src/crypto"; +import { UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning"; +import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; -import { MatrixClientPeg } from '../../../../src/MatrixClientPeg'; -import MKeyVerificationConclusion from '../../../../src/components/views/messages/MKeyVerificationConclusion'; -import { getMockClientWithEventEmitter } from '../../../test-utils'; +import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; +import MKeyVerificationConclusion from "../../../../src/components/views/messages/MKeyVerificationConclusion"; +import { getMockClientWithEventEmitter } from "../../../test-utils"; -const trustworthy = ({ isCrossSigningVerified: () => true }) as unknown as UserTrustLevel; -const untrustworthy = ({ isCrossSigningVerified: () => false }) as unknown as UserTrustLevel; +const trustworthy = { isCrossSigningVerified: () => true } as unknown as UserTrustLevel; +const untrustworthy = { isCrossSigningVerified: () => false } as unknown as UserTrustLevel; describe("MKeyVerificationConclusion", () => { - const userId = '@user:server'; + const userId = "@user:server"; const mockClient = getMockClientWithEventEmitter({ getRoom: jest.fn(), getUserId: jest.fn().mockReturnValue(userId), checkUserTrust: jest.fn(), }); - const getMockVerificationRequest = ( - { pending, cancelled, done, otherUserId }: - { pending?: boolean, cancelled?: boolean, done?: boolean, otherUserId?: string }, - ) => { + const getMockVerificationRequest = ({ + pending, + cancelled, + done, + otherUserId, + }: { + pending?: boolean; + cancelled?: boolean; + done?: boolean; + otherUserId?: string; + }) => { class MockVerificationRequest extends EventEmitter { constructor( public readonly pending: boolean, @@ -60,41 +67,33 @@ describe("MKeyVerificationConclusion", () => { }); afterAll(() => { - jest.spyOn(MatrixClientPeg, 'get').mockRestore(); + jest.spyOn(MatrixClientPeg, "get").mockRestore(); }); it("shouldn't render if there's no verificationRequest", () => { const event = new MatrixEvent({}); - const renderer = TestRenderer.create( - , - ); + const renderer = TestRenderer.create(); expect(renderer.toJSON()).toBeNull(); }); it("shouldn't render if the verificationRequest is pending", () => { const event = new MatrixEvent({}); event.verificationRequest = getMockVerificationRequest({ pending: true }); - const renderer = TestRenderer.create( - , - ); + const renderer = TestRenderer.create(); expect(renderer.toJSON()).toBeNull(); }); it("shouldn't render if the event type is cancel but the request type isn't", () => { const event = new MatrixEvent({ type: EventType.KeyVerificationCancel }); event.verificationRequest = getMockVerificationRequest({ cancelled: false }); - const renderer = TestRenderer.create( - , - ); + const renderer = TestRenderer.create(); expect(renderer.toJSON()).toBeNull(); }); it("shouldn't render if the event type is done but the request type isn't", () => { const event = new MatrixEvent({ type: "m.key.verification.done" }); event.verificationRequest = getMockVerificationRequest({ done: false }); - const renderer = TestRenderer.create( - , - ); + const renderer = TestRenderer.create(); expect(renderer.toJSON()).toBeNull(); }); @@ -103,9 +102,7 @@ describe("MKeyVerificationConclusion", () => { const event = new MatrixEvent({ type: "m.key.verification.done" }); event.verificationRequest = getMockVerificationRequest({ done: true }); - const renderer = TestRenderer.create( - , - ); + const renderer = TestRenderer.create(); expect(renderer.toJSON()).toBeNull(); }); @@ -114,9 +111,7 @@ describe("MKeyVerificationConclusion", () => { const event = new MatrixEvent({ type: "m.key.verification.done" }); event.verificationRequest = getMockVerificationRequest({ done: true, otherUserId: "@someuser:domain" }); - const renderer = TestRenderer.create( - , - ); + const renderer = TestRenderer.create(); expect(renderer.toJSON()).toBeNull(); mockClient.checkUserTrust.mockReturnValue(trustworthy); diff --git a/test/components/views/messages/MLocationBody-test.tsx b/test/components/views/messages/MLocationBody-test.tsx index 857926dd066..cae3727874c 100644 --- a/test/components/views/messages/MLocationBody-test.tsx +++ b/test/components/views/messages/MLocationBody-test.tsx @@ -14,33 +14,33 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React from "react"; // eslint-disable-next-line deprecate/import import { mount } from "enzyme"; import { LocationAssetType } from "matrix-js-sdk/src/@types/location"; -import { ClientEvent, RoomMember } from 'matrix-js-sdk/src/matrix'; -import maplibregl from 'maplibre-gl'; -import { logger } from 'matrix-js-sdk/src/logger'; -import { act } from 'react-dom/test-utils'; -import { SyncState } from 'matrix-js-sdk/src/sync'; +import { ClientEvent, RoomMember } from "matrix-js-sdk/src/matrix"; +import maplibregl from "maplibre-gl"; +import { logger } from "matrix-js-sdk/src/logger"; +import { act } from "react-dom/test-utils"; +import { SyncState } from "matrix-js-sdk/src/sync"; import MLocationBody from "../../../../src/components/views/messages/MLocationBody"; import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks"; import { MediaEventHelper } from "../../../../src/utils/MediaEventHelper"; -import Modal from '../../../../src/Modal'; +import Modal from "../../../../src/Modal"; import SdkConfig from "../../../../src/SdkConfig"; -import { TILE_SERVER_WK_KEY } from '../../../../src/utils/WellKnownUtils'; +import { TILE_SERVER_WK_KEY } from "../../../../src/utils/WellKnownUtils"; import { makeLocationEvent } from "../../../test-utils/location"; -import { getMockClientWithEventEmitter } from '../../../test-utils'; +import { getMockClientWithEventEmitter } from "../../../test-utils"; describe("MLocationBody", () => { - describe('', () => { - const roomId = '!room:server'; - const userId = '@user:server'; + describe("", () => { + const roomId = "!room:server"; + const userId = "@user:server"; const mockClient = getMockClientWithEventEmitter({ getClientWellKnown: jest.fn().mockReturnValue({ - [TILE_SERVER_WK_KEY.name]: { map_style_url: 'maps.com' }, + [TILE_SERVER_WK_KEY.name]: { map_style_url: "maps.com" }, }), isGuest: jest.fn().mockReturnValue(false), }); @@ -48,26 +48,27 @@ describe("MLocationBody", () => { const defaultProps = { mxEvent: defaultEvent, highlights: [], - highlightLink: '', + highlightLink: "", onHeightChanged: jest.fn(), onMessageAllowed: jest.fn(), permalinkCreator: {} as RoomPermalinkCreator, mediaEventHelper: {} as MediaEventHelper, }; - const getComponent = (props = {}) => mount(, { - wrappingComponent: MatrixClientContext.Provider, - wrappingComponentProps: { value: mockClient }, - }); + const getComponent = (props = {}) => + mount(, { + wrappingComponent: MatrixClientContext.Provider, + wrappingComponentProps: { value: mockClient }, + }); const getMapErrorComponent = () => { const mockMap = new maplibregl.Map(); mockClient.getClientWellKnown.mockReturnValue({ - [TILE_SERVER_WK_KEY.name]: { map_style_url: 'bad-tile-server.com' }, + [TILE_SERVER_WK_KEY.name]: { map_style_url: "bad-tile-server.com" }, }); const component = getComponent(); // simulate error initialising map in maplibregl // @ts-ignore - mockMap.emit('error', { status: 404 }); + mockMap.emit("error", { status: 404 }); return component; }; @@ -80,33 +81,33 @@ describe("MLocationBody", () => { jest.clearAllMocks(); }); - describe('with error', () => { + describe("with error", () => { let sdkConfigSpy; beforeEach(() => { // eat expected errors to keep console clean - jest.spyOn(logger, 'error').mockImplementation(() => { }); + jest.spyOn(logger, "error").mockImplementation(() => {}); mockClient.getClientWellKnown.mockReturnValue({}); - sdkConfigSpy = jest.spyOn(SdkConfig, 'get').mockReturnValue({}); + sdkConfigSpy = jest.spyOn(SdkConfig, "get").mockReturnValue({}); }); afterAll(() => { sdkConfigSpy.mockRestore(); - jest.spyOn(logger, 'error').mockRestore(); + jest.spyOn(logger, "error").mockRestore(); }); - it('displays correct fallback content without error style when map_style_url is not configured', () => { + it("displays correct fallback content without error style when map_style_url is not configured", () => { const component = getComponent(); expect(component.find(".mx_EventTile_body")).toMatchSnapshot(); }); - it('displays correct fallback content when map_style_url is misconfigured', () => { + it("displays correct fallback content when map_style_url is misconfigured", () => { const component = getMapErrorComponent(); component.setProps({}); expect(component.find(".mx_EventTile_body")).toMatchSnapshot(); }); - it('should clear the error on reconnect', () => { + it("should clear the error on reconnect", () => { const component = getMapErrorComponent(); expect((component.state() as React.ComponentState).error).toBeDefined(); mockClient.emit(ClientEvent.Sync, SyncState.Reconnecting, SyncState.Error); @@ -114,57 +115,58 @@ describe("MLocationBody", () => { }); }); - describe('without error', () => { + describe("without error", () => { beforeEach(() => { mockClient.getClientWellKnown.mockReturnValue({ - [TILE_SERVER_WK_KEY.name]: { map_style_url: 'maps.com' }, + [TILE_SERVER_WK_KEY.name]: { map_style_url: "maps.com" }, }); // MLocationBody uses random number for map id // stabilise for test - jest.spyOn(global.Math, 'random').mockReturnValue(0.123456); + jest.spyOn(global.Math, "random").mockReturnValue(0.123456); }); afterAll(() => { - jest.spyOn(global.Math, 'random').mockRestore(); + jest.spyOn(global.Math, "random").mockRestore(); }); - it('renders map correctly', () => { + it("renders map correctly", () => { const mockMap = new maplibregl.Map(); const component = getComponent(); expect(component).toMatchSnapshot(); // map was centered expect(mockMap.setCenter).toHaveBeenCalledWith({ - lat: 51.5076, lon: -0.1276, + lat: 51.5076, + lon: -0.1276, }); }); - it('opens map dialog on click', () => { - const modalSpy = jest.spyOn(Modal, 'createDialog').mockReturnValue(undefined); + it("opens map dialog on click", () => { + const modalSpy = jest.spyOn(Modal, "createDialog").mockReturnValue(undefined); const component = getComponent(); act(() => { - component.find('Map').at(0).simulate('click'); + component.find("Map").at(0).simulate("click"); }); expect(modalSpy).toHaveBeenCalled(); }); - it('renders marker correctly for a non-self share', () => { + it("renders marker correctly for a non-self share", () => { const mockMap = new maplibregl.Map(); const component = getComponent(); - expect(component.find('SmartMarker').at(0).props()).toEqual( + expect(component.find("SmartMarker").at(0).props()).toEqual( expect.objectContaining({ map: mockMap, - geoUri: 'geo:51.5076,-0.1276', + geoUri: "geo:51.5076,-0.1276", roomMember: undefined, }), ); }); - it('renders marker correctly for a self share', () => { + it("renders marker correctly for a self share", () => { const selfShareEvent = makeLocationEvent("geo:51.5076,-0.1276", LocationAssetType.Self); const member = new RoomMember(roomId, userId); // @ts-ignore cheat assignment to property @@ -172,9 +174,7 @@ describe("MLocationBody", () => { const component = getComponent({ mxEvent: selfShareEvent }); // render self locations with user avatars - expect(component.find('SmartMarker').at(0).props()['roomMember']).toEqual( - member, - ); + expect(component.find("SmartMarker").at(0).props()["roomMember"]).toEqual(member); }); }); }); diff --git a/test/components/views/messages/MPollBody-test.tsx b/test/components/views/messages/MPollBody-test.tsx index e462584098e..c3907a61b9b 100644 --- a/test/components/views/messages/MPollBody-test.tsx +++ b/test/components/views/messages/MPollBody-test.tsx @@ -49,7 +49,7 @@ const CHECKED = "mx_MPollBody_option_checked"; const mockClient = getMockClientWithEventEmitter({ getUserId: jest.fn().mockReturnValue("@me:example.com"), - sendEvent: jest.fn().mockReturnValue(Promise.resolve({ "event_id": "fake_send_id" })), + sendEvent: jest.fn().mockReturnValue(Promise.resolve({ event_id: "fake_send_id" })), getRoom: jest.fn(), }); @@ -76,9 +76,7 @@ describe("MPollBody", () => { const ev2 = responseEvent(); const badEvent = badResponseEvent(); - const voteRelations = new RelatedRelations([ - newVoteRelations([ev1, badEvent, ev2]), - ]); + const voteRelations = new RelatedRelations([newVoteRelations([ev1, badEvent, ev2])]); expect( allVotes( { getRoomId: () => "$room" } as MatrixEvent, @@ -87,21 +85,13 @@ describe("MPollBody", () => { new RelatedRelations([newEndRelations([])]), ), ).toEqual([ - new UserVote( - ev1.getTs(), - ev1.getSender(), - ev1.getContent()[M_POLL_RESPONSE.name].answers, - ), + new UserVote(ev1.getTs(), ev1.getSender(), ev1.getContent()[M_POLL_RESPONSE.name].answers), new UserVote( badEvent.getTs(), badEvent.getSender(), [], // should be spoiled ), - new UserVote( - ev2.getTs(), - ev2.getSender(), - ev2.getContent()[M_POLL_RESPONSE.name].answers, - ), + new UserVote(ev2.getTs(), ev2.getSender(), ev2.getContent()[M_POLL_RESPONSE.name].answers), ]); }); @@ -117,13 +107,7 @@ describe("MPollBody", () => { setRedactionAllowedForMeOnly(mockClient); - expect( - pollEndTs( - { getRoomId: () => "$room" } as MatrixEvent, - mockClient, - endRelations, - ), - ).toBe(12); + expect(pollEndTs({ getRoomId: () => "$room" } as MatrixEvent, mockClient, endRelations)).toBe(12); }); it("ignores unauthorised end poll event when finding end ts", () => { @@ -138,13 +122,7 @@ describe("MPollBody", () => { setRedactionAllowedForMeOnly(mockClient); - expect( - pollEndTs( - { getRoomId: () => "$room" } as MatrixEvent, - mockClient, - endRelations, - ), - ).toBe(13); + expect(pollEndTs({ getRoomId: () => "$room" } as MatrixEvent, mockClient, endRelations)).toBe(13); }); it("counts only votes before the end poll event", () => { @@ -157,18 +135,9 @@ describe("MPollBody", () => { responseEvent("ps@matrix.org", "wings", 19), ]), ]); - const endRelations = new RelatedRelations([ - newEndRelations([ - endEvent("@me:example.com", 25), - ]), - ]); + const endRelations = new RelatedRelations([newEndRelations([endEvent("@me:example.com", 25)])]); expect( - allVotes( - { getRoomId: () => "$room" } as MatrixEvent, - MatrixClientPeg.get(), - voteRelations, - endRelations, - ), + allVotes({ getRoomId: () => "$room" } as MatrixEvent, MatrixClientPeg.get(), voteRelations, endRelations), ).toEqual([ new UserVote(13, "sf@matrix.org", ["wings"]), new UserVote(13, "id@matrix.org", ["wings"]), @@ -184,8 +153,7 @@ describe("MPollBody", () => { expect(votesCount(body, "italian")).toBe(""); expect(votesCount(body, "wings")).toBe(""); expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("No votes cast"); - expect(body.find('h2').html()) - .toEqual("

What should we order for the party?

"); + expect(body.find("h2").html()).toEqual("

What should we order for the party?

"); }); it("finds votes from multiple people", () => { @@ -210,9 +178,7 @@ describe("MPollBody", () => { responseEvent("@catrd:example.com", "poutine"), responseEvent("@dune2:example.com", "wings"), ]; - const ends = [ - endEvent("@notallowed:example.com", 12), - ]; + const ends = [endEvent("@notallowed:example.com", 12)]; const body = newMPollBody(votes, ends); // Even though an end event was sent, we render the poll as unfinished @@ -236,27 +202,23 @@ describe("MPollBody", () => { expect(votesCount(body, "poutine")).toBe(""); expect(votesCount(body, "italian")).toBe(""); expect(votesCount(body, "wings")).toBe(""); - expect(body.find(".mx_MPollBody_totalVotes").text()).toBe( - "4 votes cast. Vote to see the results"); + expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("4 votes cast. Vote to see the results"); }); it("hides a single vote if I have not voted", () => { - const votes = [ - responseEvent("@alice:example.com", "pizza"), - ]; + const votes = [responseEvent("@alice:example.com", "pizza")]; const body = newMPollBody(votes); expect(votesCount(body, "pizza")).toBe(""); expect(votesCount(body, "poutine")).toBe(""); expect(votesCount(body, "italian")).toBe(""); expect(votesCount(body, "wings")).toBe(""); - expect(body.find(".mx_MPollBody_totalVotes").text()).toBe( - "1 vote cast. Vote to see the results"); + expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("1 vote cast. Vote to see the results"); }); it("takes someone's most recent vote if they voted several times", () => { const votes = [ responseEvent("@me:example.com", "pizza", 12), - responseEvent("@me:example.com", "wings", 20), // latest me + responseEvent("@me:example.com", "wings", 20), // latest me responseEvent("@qbert:example.com", "pizza", 14), responseEvent("@qbert:example.com", "poutine", 16), // latest qbert responseEvent("@qbert:example.com", "wings", 15), @@ -321,12 +283,12 @@ describe("MPollBody", () => { const votes = [responseEvent("@me:example.com", "pizza", 100)]; const body = newMPollBody(votes); const props: IBodyProps = body.instance().props as IBodyProps; - const voteRelations: Relations = props.getRelationsForEvent( - "$mypoll", "m.reference", M_POLL_RESPONSE.name); + const voteRelations = props!.getRelationsForEvent!("$mypoll", "m.reference", M_POLL_RESPONSE.name); + expect(voteRelations).toBeDefined(); clickRadio(body, "pizza"); // When a new vote from me comes in - voteRelations.addEvent(responseEvent("@me:example.com", "wings", 101)); + voteRelations!.addEvent(responseEvent("@me:example.com", "wings", 101)); // Then the new vote is counted, not the old one expect(votesCount(body, "pizza")).toBe("0 votes"); @@ -342,12 +304,12 @@ describe("MPollBody", () => { const votes = [responseEvent("@me:example.com", "pizza")]; const body = newMPollBody(votes); const props: IBodyProps = body.instance().props as IBodyProps; - const voteRelations: Relations = props.getRelationsForEvent( - "$mypoll", "m.reference", M_POLL_RESPONSE.name); + const voteRelations = props!.getRelationsForEvent!("$mypoll", "m.reference", M_POLL_RESPONSE.name); + expect(voteRelations).toBeDefined(); clickRadio(body, "pizza"); // When a new vote from someone else comes in - voteRelations.addEvent(responseEvent("@xx:example.com", "wings", 101)); + voteRelations!.addEvent(responseEvent("@xx:example.com", "wings", 101)); // Then my vote is still for pizza // NOTE: the new event does not affect the counts for other people - @@ -367,10 +329,7 @@ describe("MPollBody", () => { it("highlights my vote even if I did it on another device", () => { // Given I voted italian - const votes = [ - responseEvent("@me:example.com", "italian"), - responseEvent("@nf:example.com", "wings"), - ]; + const votes = [responseEvent("@me:example.com", "italian"), responseEvent("@nf:example.com", "wings")]; const body = newMPollBody(votes); // But I didn't click anything locally @@ -382,10 +341,7 @@ describe("MPollBody", () => { it("ignores extra answers", () => { // When cb votes for 2 things, we consider the first only - const votes = [ - responseEvent("@cb:example.com", ["pizza", "wings"]), - responseEvent("@me:example.com", "wings"), - ]; + const votes = [responseEvent("@cb:example.com", ["pizza", "wings"]), responseEvent("@me:example.com", "wings")]; const body = newMPollBody(votes); expect(votesCount(body, "pizza")).toBe("1 vote"); expect(votesCount(body, "poutine")).toBe("0 votes"); @@ -468,14 +424,12 @@ describe("MPollBody", () => { it("renders the first 20 answers if 21 were given", () => { const answers = Array.from(Array(21).keys()).map((i) => { - return { "id": `id${i}`, [M_TEXT.name]: `Name ${i}` }; + return { id: `id${i}`, [M_TEXT.name]: `Name ${i}` }; }); const votes = []; const ends = []; const body = newMPollBody(votes, ends, answers); - expect( - body.find('.mx_MPollBody_option').length, - ).toBe(20); + expect(body.find(".mx_MPollBody_option").length).toBe(20); }); it("hides scores if I voted but the poll is undisclosed", () => { @@ -491,8 +445,7 @@ describe("MPollBody", () => { expect(votesCount(body, "poutine")).toBe(""); expect(votesCount(body, "italian")).toBe(""); expect(votesCount(body, "wings")).toBe(""); - expect(body.find(".mx_MPollBody_totalVotes").text()).toBe( - "Results will be visible when the poll is ended"); + expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Results will be visible when the poll is ended"); }); it("highlights my vote if the poll is undisclosed", () => { @@ -520,16 +473,13 @@ describe("MPollBody", () => { responseEvent("@catrd:example.com", "poutine"), responseEvent("@dune2:example.com", "wings"), ]; - const ends = [ - endEvent("@me:example.com", 12), - ]; + const ends = [endEvent("@me:example.com", 12)]; const body = newMPollBody(votes, ends, null, false); expect(endedVotesCount(body, "pizza")).toBe("3 votes"); expect(endedVotesCount(body, "poutine")).toBe("1 vote"); expect(endedVotesCount(body, "italian")).toBe("0 votes"); expect(endedVotesCount(body, "wings")).toBe("1 vote"); - expect(body.find(".mx_MPollBody_totalVotes").text()).toBe( - "Final result based on 5 votes"); + expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Final result based on 5 votes"); }); it("sends a vote event when I choose an option", () => { @@ -546,9 +496,7 @@ describe("MPollBody", () => { clickRadio(body, "wings"); clickRadio(body, "wings"); clickRadio(body, "wings"); - expect(mockClient.sendEvent).toHaveBeenCalledWith( - ...expectedResponseEventCall("wings"), - ); + expect(mockClient.sendEvent).toHaveBeenCalledWith(...expectedResponseEventCall("wings")); }); it("sends no vote event when I click what I already chose", () => { @@ -574,13 +522,8 @@ describe("MPollBody", () => { }); it("sends no events when I click in an ended poll", () => { - const ends = [ - endEvent("@me:example.com", 25), - ]; - const votes = [ - responseEvent("@uy:example.com", "wings", 15), - responseEvent("@uy:example.com", "poutine", 15), - ]; + const ends = [endEvent("@me:example.com", 25)]; + const votes = [responseEvent("@uy:example.com", "wings", 15), responseEvent("@uy:example.com", "poutine", 15)]; const body = newMPollBody(votes, ends); clickEndedOption(body, "wings"); clickEndedOption(body, "italian"); @@ -620,9 +563,7 @@ describe("MPollBody", () => { responseEvent("@fa:example.com", "poutine", 18), responseEvent("@of:example.com", "poutine", 31), // Late ]; - const ends = [ - endEvent("@me:example.com", 25), - ]; + const ends = [endEvent("@me:example.com", 25)]; expect(runFindTopAnswer(votes, ends)).toEqual("Italian, Pizza and Poutine"); }); @@ -644,7 +585,7 @@ describe("MPollBody", () => { it("counts votes as normal if the poll is ended", () => { const votes = [ responseEvent("@me:example.com", "pizza", 12), - responseEvent("@me:example.com", "wings", 20), // latest me + responseEvent("@me:example.com", "wings", 20), // latest me responseEvent("@qbert:example.com", "pizza", 14), responseEvent("@qbert:example.com", "poutine", 16), // latest qbert responseEvent("@qbert:example.com", "wings", 15), @@ -655,9 +596,7 @@ describe("MPollBody", () => { expect(endedVotesCount(body, "poutine")).toBe("1 vote"); expect(endedVotesCount(body, "italian")).toBe("0 votes"); expect(endedVotesCount(body, "wings")).toBe("1 vote"); - expect( - body.find(".mx_MPollBody_totalVotes").text(), - ).toBe("Final result based on 2 votes"); + expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Final result based on 2 votes"); }); it("counts a single vote as normal if the poll is ended", () => { @@ -668,9 +607,7 @@ describe("MPollBody", () => { expect(endedVotesCount(body, "poutine")).toBe("1 vote"); expect(endedVotesCount(body, "italian")).toBe("0 votes"); expect(endedVotesCount(body, "wings")).toBe("0 votes"); - expect( - body.find(".mx_MPollBody_totalVotes").text(), - ).toBe("Final result based on 1 vote"); + expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Final result based on 1 vote"); }); it("shows ended vote counts of different numbers", () => { @@ -690,18 +627,16 @@ describe("MPollBody", () => { expect(endedVotesCount(body, "poutine")).toBe("0 votes"); expect(endedVotesCount(body, "italian")).toBe("0 votes"); expect(endedVotesCount(body, "wings")).toBe("3 votes"); - expect( - body.find(".mx_MPollBody_totalVotes").text(), - ).toBe("Final result based on 5 votes"); + expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Final result based on 5 votes"); }); it("ignores votes that arrived after poll ended", () => { const votes = [ - responseEvent("@sd:example.com", "wings", 30), // Late + responseEvent("@sd:example.com", "wings", 30), // Late responseEvent("@ff:example.com", "wings", 20), responseEvent("@ut:example.com", "wings", 14), responseEvent("@iu:example.com", "wings", 15), - responseEvent("@jf:example.com", "wings", 35), // Late + responseEvent("@jf:example.com", "wings", 35), // Late responseEvent("@wf:example.com", "pizza", 15), responseEvent("@ld:example.com", "pizza", 15), ]; @@ -712,23 +647,21 @@ describe("MPollBody", () => { expect(endedVotesCount(body, "poutine")).toBe("0 votes"); expect(endedVotesCount(body, "italian")).toBe("0 votes"); expect(endedVotesCount(body, "wings")).toBe("3 votes"); - expect( - body.find(".mx_MPollBody_totalVotes").text(), - ).toBe("Final result based on 5 votes"); + expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Final result based on 5 votes"); }); it("counts votes that arrived after an unauthorised poll end event", () => { const votes = [ - responseEvent("@sd:example.com", "wings", 30), // Late + responseEvent("@sd:example.com", "wings", 30), // Late responseEvent("@ff:example.com", "wings", 20), responseEvent("@ut:example.com", "wings", 14), responseEvent("@iu:example.com", "wings", 15), - responseEvent("@jf:example.com", "wings", 35), // Late + responseEvent("@jf:example.com", "wings", 35), // Late responseEvent("@wf:example.com", "pizza", 15), responseEvent("@ld:example.com", "pizza", 15), ]; const ends = [ - endEvent("@unauthorised:example.com", 5), // Should be ignored + endEvent("@unauthorised:example.com", 5), // Should be ignored endEvent("@me:example.com", 25), ]; const body = newMPollBody(votes, ends); @@ -737,9 +670,7 @@ describe("MPollBody", () => { expect(endedVotesCount(body, "poutine")).toBe("0 votes"); expect(endedVotesCount(body, "italian")).toBe("0 votes"); expect(endedVotesCount(body, "wings")).toBe("3 votes"); - expect( - body.find(".mx_MPollBody_totalVotes").text(), - ).toBe("Final result based on 5 votes"); + expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Final result based on 5 votes"); }); it("ignores votes that arrived after the first end poll event", () => { @@ -747,11 +678,11 @@ describe("MPollBody", () => { // "Votes sent on or before the end event's timestamp are valid votes" const votes = [ - responseEvent("@sd:example.com", "wings", 30), // Late + responseEvent("@sd:example.com", "wings", 30), // Late responseEvent("@ff:example.com", "wings", 20), responseEvent("@ut:example.com", "wings", 14), - responseEvent("@iu:example.com", "wings", 25), // Just on time - responseEvent("@jf:example.com", "wings", 35), // Late + responseEvent("@iu:example.com", "wings", 25), // Just on time + responseEvent("@jf:example.com", "wings", 35), // Late responseEvent("@wf:example.com", "pizza", 15), responseEvent("@ld:example.com", "pizza", 15), ]; @@ -766,9 +697,7 @@ describe("MPollBody", () => { expect(endedVotesCount(body, "poutine")).toBe("0 votes"); expect(endedVotesCount(body, "italian")).toBe("0 votes"); expect(endedVotesCount(body, "wings")).toBe("3 votes"); - expect( - body.find(".mx_MPollBody_totalVotes").text(), - ).toBe("Final result based on 5 votes"); + expect(body.find(".mx_MPollBody_totalVotes").text()).toBe("Final result based on 5 votes"); }); it("highlights the winning vote in an ended poll", () => { @@ -786,12 +715,8 @@ describe("MPollBody", () => { expect(endedVoteChecked(body, "pizza")).toBe(false); // Double-check by looking for the endedOptionWinner class - expect( - endedVoteDiv(body, "wings").hasClass("mx_MPollBody_endedOptionWinner"), - ).toBe(true); - expect( - endedVoteDiv(body, "pizza").hasClass("mx_MPollBody_endedOptionWinner"), - ).toBe(false); + expect(endedVoteDiv(body, "wings").hasClass("mx_MPollBody_endedOptionWinner")).toBe(true); + expect(endedVoteDiv(body, "pizza").hasClass("mx_MPollBody_endedOptionWinner")).toBe(false); }); it("highlights multiple winning votes", () => { @@ -834,9 +759,9 @@ describe("MPollBody", () => { it("says poll is not ended if asking for relations returns undefined", () => { const pollEvent = new MatrixEvent({ - "event_id": "$mypoll", - "room_id": "#myroom:example.com", - "content": newPollStart([]), + event_id: "$mypoll", + room_id: "#myroom:example.com", + content: newPollStart([]), }); mockClient.getRoom.mockImplementation((_roomId) => { return { @@ -847,45 +772,38 @@ describe("MPollBody", () => { }, } as unknown as Room; }); - const getRelationsForEvent = - (eventId: string, relationType: string, eventType: string) => { - expect(eventId).toBe("$mypoll"); - expect(relationType).toBe("m.reference"); - expect(M_POLL_END.matches(eventType)).toBe(true); - return undefined; - }; - expect( - isPollEnded( - pollEvent, - MatrixClientPeg.get(), - getRelationsForEvent, - ), - ).toBe(false); + const getRelationsForEvent = (eventId: string, relationType: string, eventType: string) => { + expect(eventId).toBe("$mypoll"); + expect(relationType).toBe("m.reference"); + expect(M_POLL_END.matches(eventType)).toBe(true); + return undefined; + }; + expect(isPollEnded(pollEvent, MatrixClientPeg.get(), getRelationsForEvent)).toBe(false); }); it("Displays edited content and new answer IDs if the poll has been edited", () => { const pollEvent = new MatrixEvent({ - "type": M_POLL_START.name, - "event_id": "$mypoll", - "room_id": "#myroom:example.com", - "content": newPollStart( + type: M_POLL_START.name, + event_id: "$mypoll", + room_id: "#myroom:example.com", + content: newPollStart( [ - { "id": "o1", [M_TEXT.name]: "old answer 1" }, - { "id": "o2", [M_TEXT.name]: "old answer 2" }, + { id: "o1", [M_TEXT.name]: "old answer 1" }, + { id: "o2", [M_TEXT.name]: "old answer 2" }, ], "old question", ), }); const replacingEvent = new MatrixEvent({ - "type": M_POLL_START.name, - "event_id": "$mypollreplacement", - "room_id": "#myroom:example.com", - "content": { + type: M_POLL_START.name, + event_id: "$mypollreplacement", + room_id: "#myroom:example.com", + content: { "m.new_content": newPollStart( [ - { "id": "n1", [M_TEXT.name]: "new answer 1" }, - { "id": "n2", [M_TEXT.name]: "new answer 2" }, - { "id": "n3", [M_TEXT.name]: "new answer 3" }, + { id: "n1", [M_TEXT.name]: "new answer 1" }, + { id: "n2", [M_TEXT.name]: "new answer 2" }, + { id: "n3", [M_TEXT.name]: "new answer 3" }, ], "new question", ), @@ -893,18 +811,15 @@ describe("MPollBody", () => { }); pollEvent.makeReplaced(replacingEvent); const body = newMPollBodyFromEvent(pollEvent, []); - expect(body.find('h2').html()) - .toEqual( - "

new question" - + " (edited)" - + "

", - ); + expect(body.find("h2").html()).toEqual( + "

new question" + ' (edited)' + "

", + ); const inputs = body.find('input[type="radio"]'); expect(inputs).toHaveLength(3); expect(inputs.at(0).prop("value")).toEqual("n1"); expect(inputs.at(1).prop("value")).toEqual("n2"); expect(inputs.at(2).prop("value")).toEqual("n3"); - const options = body.find('.mx_MPollBody_optionText'); + const options = body.find(".mx_MPollBody_optionText"); expect(options).toHaveLength(3); expect(options.at(0).text()).toEqual("new answer 1"); expect(options.at(1).text()).toEqual("new answer 2"); @@ -1025,10 +940,7 @@ function newEndRelations(relationEvents: Array): Relations { return newRelations(relationEvents, M_POLL_END.name); } -function newRelations( - relationEvents: Array, - eventType: string, -): Relations { +function newRelations(relationEvents: Array, eventType: string): Relations { const voteRelations = new Relations("m.reference", eventType, null); for (const ev of relationEvents) { voteRelations.addEvent(ev); @@ -1043,10 +955,10 @@ function newMPollBody( disclosed = true, ): ReactWrapper { const mxEvent = new MatrixEvent({ - "type": M_POLL_START.name, - "event_id": "$mypoll", - "room_id": "#myroom:example.com", - "content": newPollStart(answers, null, disclosed), + type: M_POLL_START.name, + event_id: "$mypoll", + room_id: "#myroom:example.com", + content: newPollStart(answers, null, disclosed), }); return newMPollBodyFromEvent(mxEvent, relationEvents, endEvents); } @@ -1058,10 +970,10 @@ function newMPollBodyFromEvent( ): ReactWrapper { const voteRelations = newVoteRelations(relationEvents); const endRelations = newEndRelations(endEvents); - return mount( { + return mount( + { expect(eventId).toBe("$mypoll"); expect(relationType).toBe("m.reference"); if (M_POLL_RESPONSE.matches(eventType)) { @@ -1071,22 +983,22 @@ function newMPollBodyFromEvent( } else { fail("Unexpected eventType: " + eventType); } - } - } - - // We don't use any of these props, but they're required. - highlightLink="unused" - highlights={[]} - mediaEventHelper={null} - onHeightChanged={() => {}} - onMessageAllowed={() => {}} - permalinkCreator={null} - />, { - wrappingComponent: MatrixClientContext.Provider, - wrappingComponentProps: { - value: mockClient, + }} + // We don't use any of these props, but they're required. + highlightLink="unused" + highlights={[]} + mediaEventHelper={null} + onHeightChanged={() => {}} + onMessageAllowed={() => {}} + permalinkCreator={null} + />, + { + wrappingComponent: MatrixClientContext.Provider, + wrappingComponentProps: { + value: mockClient, + }, }, - }); + ); } function clickRadio(wrapper: ReactWrapper, value: string) { @@ -1102,21 +1014,15 @@ function clickEndedOption(wrapper: ReactWrapper, value: string) { } function voteButton(wrapper: ReactWrapper, value: string): ReactWrapper { - return wrapper.find( - `div.mx_MPollBody_option`, - ).findWhere(w => w.key() === value); + return wrapper.find(`div.mx_MPollBody_option`).findWhere((w) => w.key() === value); } function votesCount(wrapper: ReactWrapper, value: string): string { - return wrapper.find( - `StyledRadioButton[value="${value}"] .mx_MPollBody_optionVoteCount`, - ).text(); + return wrapper.find(`StyledRadioButton[value="${value}"] .mx_MPollBody_optionVoteCount`).text(); } function endedVoteChecked(wrapper: ReactWrapper, value: string): boolean { - return endedVoteDiv(wrapper, value) - .closest(".mx_MPollBody_option") - .hasClass("mx_MPollBody_option_checked"); + return endedVoteDiv(wrapper, value).closest(".mx_MPollBody_option").hasClass("mx_MPollBody_option_checked"); } function endedVoteDiv(wrapper: ReactWrapper, value: string): ReactWrapper { @@ -1124,22 +1030,16 @@ function endedVoteDiv(wrapper: ReactWrapper, value: string): ReactWrapper { } function endedVotesCount(wrapper: ReactWrapper, value: string): string { - return wrapper.find( - `div[data-value="${value}"] .mx_MPollBody_optionVoteCount`, - ).text(); + return wrapper.find(`div[data-value="${value}"] .mx_MPollBody_optionVoteCount`).text(); } -function newPollStart( - answers?: POLL_ANSWER[], - question?: string, - disclosed = true, -): M_POLL_START_EVENT_CONTENT { +function newPollStart(answers?: POLL_ANSWER[], question?: string, disclosed = true): M_POLL_START_EVENT_CONTENT { if (!answers) { answers = [ - { "id": "pizza", [M_TEXT.name]: "Pizza" }, - { "id": "poutine", [M_TEXT.name]: "Poutine" }, - { "id": "italian", [M_TEXT.name]: "Italian" }, - { "id": "wings", [M_TEXT.name]: "Wings" }, + { id: "pizza", [M_TEXT.name]: "Pizza" }, + { id: "poutine", [M_TEXT.name]: "Poutine" }, + { id: "italian", [M_TEXT.name]: "Italian" }, + { id: "wings", [M_TEXT.name]: "Wings" }, ]; } @@ -1147,43 +1047,35 @@ function newPollStart( question = "What should we order for the party?"; } - const answersFallback = answers - .map((a, i) => `${i + 1}. ${a[M_TEXT.name]}`) - .join("\n"); + const answersFallback = answers.map((a, i) => `${i + 1}. ${a[M_TEXT.name]}`).join("\n"); const fallback = `${question}\n${answersFallback}`; return { [M_POLL_START.name]: { - "question": { + question: { [M_TEXT.name]: question, }, - "kind": ( - disclosed - ? M_POLL_KIND_DISCLOSED.name - : M_POLL_KIND_UNDISCLOSED.name - ), - "answers": answers, + kind: disclosed ? M_POLL_KIND_DISCLOSED.name : M_POLL_KIND_UNDISCLOSED.name, + answers: answers, }, [M_TEXT.name]: fallback, }; } function badResponseEvent(): MatrixEvent { - return new MatrixEvent( - { - "event_id": nextId(), - "type": M_POLL_RESPONSE.name, - "sender": "@malicious:example.com", - "content": { - "m.relates_to": { - "rel_type": "m.reference", - "event_id": "$mypoll", - }, - // Does not actually contain a response + return new MatrixEvent({ + event_id: nextId(), + type: M_POLL_RESPONSE.name, + sender: "@malicious:example.com", + content: { + "m.relates_to": { + rel_type: "m.reference", + event_id: "$mypoll", }, + // Does not actually contain a response }, - ); + }); } function responseEvent( @@ -1192,116 +1084,103 @@ function responseEvent( ts = 0, ): MatrixEvent { const ans = typeof answers === "string" ? [answers] : answers; - return new MatrixEvent( - { - "event_id": nextId(), - "room_id": "#myroom:example.com", - "origin_server_ts": ts, - "type": M_POLL_RESPONSE.name, - "sender": sender, - "content": { - "m.relates_to": { - "rel_type": "m.reference", - "event_id": "$mypoll", - }, - [M_POLL_RESPONSE.name]: { - "answers": ans, - }, + return new MatrixEvent({ + event_id: nextId(), + room_id: "#myroom:example.com", + origin_server_ts: ts, + type: M_POLL_RESPONSE.name, + sender: sender, + content: { + "m.relates_to": { + rel_type: "m.reference", + event_id: "$mypoll", + }, + [M_POLL_RESPONSE.name]: { + answers: ans, }, }, - ); + }); } function expectedResponseEvent(answer: string) { return { - "content": { + content: { [M_POLL_RESPONSE.name]: { - "answers": [answer], + answers: [answer], }, "m.relates_to": { - "event_id": "$mypoll", - "rel_type": "m.reference", + event_id: "$mypoll", + rel_type: "m.reference", }, }, - "roomId": "#myroom:example.com", - "eventType": M_POLL_RESPONSE.name, - "txnId": undefined, - "callback": undefined, + roomId: "#myroom:example.com", + eventType: M_POLL_RESPONSE.name, + txnId: undefined, + callback: undefined, }; } function expectedResponseEventCall(answer: string) { - const { - content, roomId, eventType, - } = expectedResponseEvent(answer); - return [ - roomId, eventType, content, - ]; + const { content, roomId, eventType } = expectedResponseEvent(answer); + return [roomId, eventType, content]; } -function endEvent( - sender = "@me:example.com", - ts = 0, -): MatrixEvent { - return new MatrixEvent( - { - "event_id": nextId(), - "room_id": "#myroom:example.com", - "origin_server_ts": ts, - "type": M_POLL_END.name, - "sender": sender, - "content": { - "m.relates_to": { - "rel_type": "m.reference", - "event_id": "$mypoll", - }, - [M_POLL_END.name]: {}, - [M_TEXT.name]: "The poll has ended. Something.", +function endEvent(sender = "@me:example.com", ts = 0): MatrixEvent { + return new MatrixEvent({ + event_id: nextId(), + room_id: "#myroom:example.com", + origin_server_ts: ts, + type: M_POLL_END.name, + sender: sender, + content: { + "m.relates_to": { + rel_type: "m.reference", + event_id: "$mypoll", }, + [M_POLL_END.name]: {}, + [M_TEXT.name]: "The poll has ended. Something.", }, - ); + }); } function runIsPollEnded(ends: MatrixEvent[]) { const pollEvent = new MatrixEvent({ - "event_id": "$mypoll", - "room_id": "#myroom:example.com", - "type": M_POLL_START.name, - "content": newPollStart(), + event_id: "$mypoll", + room_id: "#myroom:example.com", + type: M_POLL_START.name, + content: newPollStart(), }); setRedactionAllowedForMeOnly(mockClient); - const getRelationsForEvent = - (eventId: string, relationType: string, eventType: string) => { - expect(eventId).toBe("$mypoll"); - expect(relationType).toBe("m.reference"); - expect(M_POLL_END.matches(eventType)).toBe(true); - return newEndRelations(ends); - }; + const getRelationsForEvent = (eventId: string, relationType: string, eventType: string) => { + expect(eventId).toBe("$mypoll"); + expect(relationType).toBe("m.reference"); + expect(M_POLL_END.matches(eventType)).toBe(true); + return newEndRelations(ends); + }; return isPollEnded(pollEvent, mockClient, getRelationsForEvent); } function runFindTopAnswer(votes: MatrixEvent[], ends: MatrixEvent[]) { const pollEvent = new MatrixEvent({ - "event_id": "$mypoll", - "room_id": "#myroom:example.com", - "type": M_POLL_START.name, - "content": newPollStart(), - }); - - const getRelationsForEvent = - (eventId: string, relationType: string, eventType: string) => { - expect(eventId).toBe("$mypoll"); - expect(relationType).toBe("m.reference"); - if (M_POLL_RESPONSE.matches(eventType)) { - return newVoteRelations(votes); - } else if (M_POLL_END.matches(eventType)) { - return newEndRelations(ends); - } else { - fail(`eventType should be end or vote but was ${eventType}`); - } - }; + event_id: "$mypoll", + room_id: "#myroom:example.com", + type: M_POLL_START.name, + content: newPollStart(), + }); + + const getRelationsForEvent = (eventId: string, relationType: string, eventType: string) => { + expect(eventId).toBe("$mypoll"); + expect(relationType).toBe("m.reference"); + if (M_POLL_RESPONSE.matches(eventType)) { + return newVoteRelations(votes); + } else if (M_POLL_END.matches(eventType)) { + return newEndRelations(ends); + } else { + fail(`eventType should be end or vote but was ${eventType}`); + } + }; return findTopAnswer(pollEvent, MatrixClientPeg.get(), getRelationsForEvent); } diff --git a/test/components/views/messages/MVideoBody-test.tsx b/test/components/views/messages/MVideoBody-test.tsx index 42f804c1bc5..cbfed1f3069 100644 --- a/test/components/views/messages/MVideoBody-test.tsx +++ b/test/components/views/messages/MVideoBody-test.tsx @@ -14,34 +14,35 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -// eslint-disable-next-line deprecate/import -import { mount, ReactWrapper } from "enzyme"; -import { MatrixEvent } from 'matrix-js-sdk/src/matrix'; +import React from "react"; +import { MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { render, RenderResult } from "@testing-library/react"; import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks"; import { MediaEventHelper } from "../../../../src/utils/MediaEventHelper"; -import { getMockClientWithEventEmitter } from '../../../test-utils'; -import MVideoBody from '../../../../src/components/views/messages/MVideoBody'; +import { getMockClientWithEventEmitter } from "../../../test-utils"; +import MVideoBody from "../../../../src/components/views/messages/MVideoBody"; -jest.mock( - "../../../../src/customisations/Media", - () => { - return { mediaFromContent: () => { return { isEncrypted: false }; } }; - }, -); +jest.mock("../../../../src/customisations/Media", () => { + return { + mediaFromContent: () => { + return { isEncrypted: false }; + }, + }; +}); describe("MVideoBody", () => { - it('does not crash when given a portrait image', () => { + it("does not crash when given a portrait image", () => { // Check for an unreliable crash caused by a fractional-sized // image dimension being used for a CanvasImageData. - expect(makeMVideoBody(720, 1280).html()).toMatchSnapshot(); + const { asFragment } = makeMVideoBody(720, 1280); + expect(asFragment()).toMatchSnapshot(); // If we get here, we did not crash. }); }); -function makeMVideoBody(w: number, h: number): ReactWrapper, MVideoBody> { +function makeMVideoBody(w: number, h: number): RenderResult { const content = { info: { "w": w, @@ -68,7 +69,7 @@ function makeMVideoBody(w: number, h: number): ReactWrapper, M const defaultProps = { mxEvent: event, highlights: [], - highlightLink: '', + highlightLink: "", onHeightChanged: jest.fn(), onMessageAllowed: jest.fn(), permalinkCreator: {} as RoomPermalinkCreator, @@ -79,8 +80,9 @@ function makeMVideoBody(w: number, h: number): ReactWrapper, M mxcUrlToHttp: jest.fn(), }); - return mount(, { - wrappingComponent: MatrixClientContext.Provider, - wrappingComponentProps: { value: mockClient }, - }); + return render( + + + , + ); } diff --git a/test/components/views/messages/MessageActionBar-test.tsx b/test/components/views/messages/MessageActionBar-test.tsx index 670d39ec64c..a3a3ffe1418 100644 --- a/test/components/views/messages/MessageActionBar-test.tsx +++ b/test/components/views/messages/MessageActionBar-test.tsx @@ -14,57 +14,50 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import { render, fireEvent } from '@testing-library/react'; -import { act } from 'react-test-renderer'; -import { - EventType, - EventStatus, - MatrixEvent, - MatrixEventEvent, - MsgType, - Room, -} from 'matrix-js-sdk/src/matrix'; -import { FeatureSupport, Thread } from 'matrix-js-sdk/src/models/thread'; - -import MessageActionBar from '../../../../src/components/views/messages/MessageActionBar'; +import React from "react"; +import { render, fireEvent } from "@testing-library/react"; +import { act } from "react-test-renderer"; +import { EventType, EventStatus, MatrixEvent, MatrixEventEvent, MsgType, Room } from "matrix-js-sdk/src/matrix"; +import { FeatureSupport, Thread } from "matrix-js-sdk/src/models/thread"; + +import MessageActionBar from "../../../../src/components/views/messages/MessageActionBar"; import { getMockClientWithEventEmitter, mockClientMethodsUser, mockClientMethodsEvents, makeBeaconInfoEvent, -} from '../../../test-utils'; -import { RoomPermalinkCreator } from '../../../../src/utils/permalinks/Permalinks'; -import RoomContext, { TimelineRenderingType } from '../../../../src/contexts/RoomContext'; -import { IRoomState } from '../../../../src/components/structures/RoomView'; -import dispatcher from '../../../../src/dispatcher/dispatcher'; -import SettingsStore from '../../../../src/settings/SettingsStore'; -import { Action } from '../../../../src/dispatcher/actions'; -import { UserTab } from '../../../../src/components/views/dialogs/UserTab'; - -jest.mock('../../../../src/dispatcher/dispatcher'); - -describe('', () => { - const userId = '@alice:server.org'; - const roomId = '!room:server.org'; +} from "../../../test-utils"; +import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks"; +import RoomContext, { TimelineRenderingType } from "../../../../src/contexts/RoomContext"; +import { IRoomState } from "../../../../src/components/structures/RoomView"; +import dispatcher from "../../../../src/dispatcher/dispatcher"; +import SettingsStore from "../../../../src/settings/SettingsStore"; +import { Action } from "../../../../src/dispatcher/actions"; +import { UserTab } from "../../../../src/components/views/dialogs/UserTab"; + +jest.mock("../../../../src/dispatcher/dispatcher"); + +describe("", () => { + const userId = "@alice:server.org"; + const roomId = "!room:server.org"; const alicesMessageEvent = new MatrixEvent({ type: EventType.RoomMessage, sender: userId, room_id: roomId, content: { msgtype: MsgType.Text, - body: 'Hello', + body: "Hello", }, event_id: "$alices_message", }); const bobsMessageEvent = new MatrixEvent({ type: EventType.RoomMessage, - sender: '@bob:server.org', + sender: "@bob:server.org", room_id: roomId, content: { msgtype: MsgType.Text, - body: 'I am bob', + body: "I am bob", }, event_id: "$bobs_message", }); @@ -84,7 +77,7 @@ describe('', () => { const localStorageMock = (() => { let store = {}; return { - getItem: jest.fn().mockImplementation(key => store[key] ?? null), + getItem: jest.fn().mockImplementation((key) => store[key] ?? null), setItem: jest.fn().mockImplementation((key, value) => { store[key] = value; }), @@ -94,13 +87,13 @@ describe('', () => { removeItem: jest.fn().mockImplementation((key) => delete store[key]), }; })(); - Object.defineProperty(window, 'localStorage', { + Object.defineProperty(window, "localStorage", { value: localStorageMock, writable: true, }); const room = new Room(roomId, client, userId); - jest.spyOn(room, 'getPendingEvents').mockReturnValue([]); + jest.spyOn(room, "getPendingEvents").mockReturnValue([]); client.getRoom.mockReturnValue(room); @@ -121,22 +114,23 @@ describe('', () => { render( - ); + , + ); beforeEach(() => { jest.clearAllMocks(); alicesMessageEvent.setStatus(EventStatus.SENT); - jest.spyOn(SettingsStore, 'getValue').mockReturnValue(false); - jest.spyOn(SettingsStore, 'setValue').mockResolvedValue(undefined); + jest.spyOn(SettingsStore, "getValue").mockReturnValue(false); + jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined); }); afterAll(() => { - jest.spyOn(SettingsStore, 'getValue').mockRestore(); - jest.spyOn(SettingsStore, 'setValue').mockRestore(); + jest.spyOn(SettingsStore, "getValue").mockRestore(); + jest.spyOn(SettingsStore, "setValue").mockRestore(); }); - it('kills event listeners on unmount', () => { - const offSpy = jest.spyOn(alicesMessageEvent, 'off').mockClear(); + it("kills event listeners on unmount", () => { + const offSpy = jest.spyOn(alicesMessageEvent, "off").mockClear(); const wrapper = getComponent({ mxEvent: alicesMessageEvent }); act(() => { @@ -150,24 +144,24 @@ describe('', () => { expect(client.decryptEventIfNeeded).toHaveBeenCalled(); }); - describe('decryption', () => { - it('decrypts event if needed', () => { + describe("decryption", () => { + it("decrypts event if needed", () => { getComponent({ mxEvent: alicesMessageEvent }); expect(client.decryptEventIfNeeded).toHaveBeenCalled(); }); - it('updates component on decrypted event', () => { + it("updates component on decrypted event", () => { const decryptingEvent = new MatrixEvent({ type: EventType.RoomMessageEncrypted, sender: userId, room_id: roomId, content: {}, }); - jest.spyOn(decryptingEvent, 'isBeingDecrypted').mockReturnValue(true); + jest.spyOn(decryptingEvent, "isBeingDecrypted").mockReturnValue(true); const { queryByLabelText } = getComponent({ mxEvent: decryptingEvent }); // still encrypted event is not actionable => no reply button - expect(queryByLabelText('Reply')).toBeFalsy(); + expect(queryByLabelText("Reply")).toBeFalsy(); act(() => { // ''decrypt'' the event @@ -177,46 +171,46 @@ describe('', () => { }); // new available actions after decryption - expect(queryByLabelText('Reply')).toBeTruthy(); + expect(queryByLabelText("Reply")).toBeTruthy(); }); }); - describe('status', () => { - it('updates component when event status changes', () => { + describe("status", () => { + it("updates component when event status changes", () => { alicesMessageEvent.setStatus(EventStatus.QUEUED); const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent }); // pending event status, cancel action available - expect(queryByLabelText('Delete')).toBeTruthy(); + expect(queryByLabelText("Delete")).toBeTruthy(); act(() => { alicesMessageEvent.setStatus(EventStatus.SENT); }); // event is sent, no longer cancelable - expect(queryByLabelText('Delete')).toBeFalsy(); + expect(queryByLabelText("Delete")).toBeFalsy(); }); }); - describe('redaction', () => { + describe("redaction", () => { // this doesn't do what it's supposed to // because beforeRedaction event is fired... before redaction // event is unchanged at point when this component updates // TODO file bug - xit('updates component on before redaction event', () => { + xit("updates component on before redaction event", () => { const event = new MatrixEvent({ type: EventType.RoomMessage, sender: userId, room_id: roomId, content: { msgtype: MsgType.Text, - body: 'Hello', + body: "Hello", }, }); const { queryByLabelText } = getComponent({ mxEvent: event }); // no pending redaction => no delete button - expect(queryByLabelText('Delete')).toBeFalsy(); + expect(queryByLabelText("Delete")).toBeFalsy(); act(() => { const redactionEvent = new MatrixEvent({ @@ -229,110 +223,110 @@ describe('', () => { }); // updated with local redaction event, delete now available - expect(queryByLabelText('Delete')).toBeTruthy(); + expect(queryByLabelText("Delete")).toBeTruthy(); }); }); - describe('options button', () => { - it('renders options menu', () => { + describe("options button", () => { + it("renders options menu", () => { const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent }); - expect(queryByLabelText('Options')).toBeTruthy(); + expect(queryByLabelText("Options")).toBeTruthy(); }); - it('opens message context menu on click', () => { + it("opens message context menu on click", () => { const { getByTestId, queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent }); act(() => { - fireEvent.click(queryByLabelText('Options')); + fireEvent.click(queryByLabelText("Options")); }); - expect(getByTestId('mx_MessageContextMenu')).toBeTruthy(); + expect(getByTestId("mx_MessageContextMenu")).toBeTruthy(); }); }); - describe('reply button', () => { - it('renders reply button on own actionable event', () => { + describe("reply button", () => { + it("renders reply button on own actionable event", () => { const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent }); - expect(queryByLabelText('Reply')).toBeTruthy(); + expect(queryByLabelText("Reply")).toBeTruthy(); }); - it('renders reply button on others actionable event', () => { + it("renders reply button on others actionable event", () => { const { queryByLabelText } = getComponent({ mxEvent: bobsMessageEvent }, { canSendMessages: true }); - expect(queryByLabelText('Reply')).toBeTruthy(); + expect(queryByLabelText("Reply")).toBeTruthy(); }); - it('does not render reply button on non-actionable event', () => { + it("does not render reply button on non-actionable event", () => { // redacted event is not actionable const { queryByLabelText } = getComponent({ mxEvent: redactedEvent }); - expect(queryByLabelText('Reply')).toBeFalsy(); + expect(queryByLabelText("Reply")).toBeFalsy(); }); - it('does not render reply button when user cannot send messaged', () => { + it("does not render reply button when user cannot send messaged", () => { // redacted event is not actionable const { queryByLabelText } = getComponent({ mxEvent: redactedEvent }, { canSendMessages: false }); - expect(queryByLabelText('Reply')).toBeFalsy(); + expect(queryByLabelText("Reply")).toBeFalsy(); }); - it('dispatches reply event on click', () => { + it("dispatches reply event on click", () => { const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent }); act(() => { - fireEvent.click(queryByLabelText('Reply')); + fireEvent.click(queryByLabelText("Reply")); }); expect(dispatcher.dispatch).toHaveBeenCalledWith({ - action: 'reply_to_event', + action: "reply_to_event", event: alicesMessageEvent, context: TimelineRenderingType.Room, }); }); }); - describe('react button', () => { - it('renders react button on own actionable event', () => { + describe("react button", () => { + it("renders react button on own actionable event", () => { const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent }); - expect(queryByLabelText('React')).toBeTruthy(); + expect(queryByLabelText("React")).toBeTruthy(); }); - it('renders react button on others actionable event', () => { + it("renders react button on others actionable event", () => { const { queryByLabelText } = getComponent({ mxEvent: bobsMessageEvent }); - expect(queryByLabelText('React')).toBeTruthy(); + expect(queryByLabelText("React")).toBeTruthy(); }); - it('does not render react button on non-actionable event', () => { + it("does not render react button on non-actionable event", () => { // redacted event is not actionable const { queryByLabelText } = getComponent({ mxEvent: redactedEvent }); - expect(queryByLabelText('React')).toBeFalsy(); + expect(queryByLabelText("React")).toBeFalsy(); }); - it('does not render react button when user cannot react', () => { + it("does not render react button when user cannot react", () => { // redacted event is not actionable const { queryByLabelText } = getComponent({ mxEvent: redactedEvent }, { canReact: false }); - expect(queryByLabelText('React')).toBeFalsy(); + expect(queryByLabelText("React")).toBeFalsy(); }); - it('opens reaction picker on click', () => { + it("opens reaction picker on click", () => { const { queryByLabelText, getByTestId } = getComponent({ mxEvent: alicesMessageEvent }); act(() => { - fireEvent.click(queryByLabelText('React')); + fireEvent.click(queryByLabelText("React")); }); - expect(getByTestId('mx_EmojiPicker')).toBeTruthy(); + expect(getByTestId("mx_EmojiPicker")).toBeTruthy(); }); }); - describe('cancel button', () => { - it('renders cancel button for an event with a cancelable status', () => { + describe("cancel button", () => { + it("renders cancel button for an event with a cancelable status", () => { alicesMessageEvent.setStatus(EventStatus.QUEUED); const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent }); - expect(queryByLabelText('Delete')).toBeTruthy(); + expect(queryByLabelText("Delete")).toBeTruthy(); }); - it('renders cancel button for an event with a pending edit', () => { + it("renders cancel button for an event with a pending edit", () => { const event = new MatrixEvent({ type: EventType.RoomMessage, sender: userId, room_id: roomId, content: { msgtype: MsgType.Text, - body: 'Hello', + body: "Hello", }, }); event.setStatus(EventStatus.SENT); @@ -342,23 +336,23 @@ describe('', () => { room_id: roomId, content: { msgtype: MsgType.Text, - body: 'replacing event body', + body: "replacing event body", }, }); replacingEvent.setStatus(EventStatus.QUEUED); event.makeReplaced(replacingEvent); const { queryByLabelText } = getComponent({ mxEvent: event }); - expect(queryByLabelText('Delete')).toBeTruthy(); + expect(queryByLabelText("Delete")).toBeTruthy(); }); - it('renders cancel button for an event with a pending redaction', () => { + it("renders cancel button for an event with a pending redaction", () => { const event = new MatrixEvent({ type: EventType.RoomMessage, sender: userId, room_id: roomId, content: { msgtype: MsgType.Text, - body: 'Hello', + body: "Hello", }, }); event.setStatus(EventStatus.SENT); @@ -372,45 +366,51 @@ describe('', () => { event.markLocallyRedacted(redactionEvent); const { queryByLabelText } = getComponent({ mxEvent: event }); - expect(queryByLabelText('Delete')).toBeTruthy(); + expect(queryByLabelText("Delete")).toBeTruthy(); }); - it('renders cancel and retry button for an event with NOT_SENT status', () => { + it("renders cancel and retry button for an event with NOT_SENT status", () => { alicesMessageEvent.setStatus(EventStatus.NOT_SENT); const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent }); - expect(queryByLabelText('Retry')).toBeTruthy(); - expect(queryByLabelText('Delete')).toBeTruthy(); + expect(queryByLabelText("Retry")).toBeTruthy(); + expect(queryByLabelText("Delete")).toBeTruthy(); }); - it.todo('unsends event on cancel click'); - it.todo('retrys event on retry click'); + it.todo("unsends event on cancel click"); + it.todo("retrys event on retry click"); }); - describe('thread button', () => { + describe("thread button", () => { beforeEach(() => { Thread.setServerSideSupport(FeatureSupport.Stable); }); - describe('when threads feature is not enabled', () => { - it('does not render thread button when threads does not have server support', () => { - jest.spyOn(SettingsStore, 'getValue').mockReturnValue(false); + describe("when threads feature is not enabled", () => { + beforeEach(() => { + jest.spyOn(SettingsStore, "getValue").mockImplementation( + (setting) => setting !== "feature_threadstable", + ); + }); + + it("does not render thread button when threads does not have server support", () => { + jest.spyOn(SettingsStore, "getValue").mockReturnValue(false); Thread.setServerSideSupport(FeatureSupport.None); const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent }); - expect(queryByLabelText('Reply in thread')).toBeFalsy(); + expect(queryByLabelText("Reply in thread")).toBeFalsy(); }); - it('renders thread button when threads has server support', () => { - jest.spyOn(SettingsStore, 'getValue').mockReturnValue(false); + it("renders thread button when threads has server support", () => { + jest.spyOn(SettingsStore, "getValue").mockReturnValue(false); const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent }); - expect(queryByLabelText('Reply in thread')).toBeTruthy(); + expect(queryByLabelText("Reply in thread")).toBeTruthy(); }); - it('opens user settings on click', () => { - jest.spyOn(SettingsStore, 'getValue').mockReturnValue(false); + it("opens user settings on click", () => { + jest.spyOn(SettingsStore, "getValue").mockReturnValue(false); const { getByLabelText } = getComponent({ mxEvent: alicesMessageEvent }); act(() => { - fireEvent.click(getByLabelText('Reply in thread')); + fireEvent.click(getByLabelText("Reply in thread")); }); expect(dispatcher.dispatch).toHaveBeenCalledWith({ @@ -420,27 +420,29 @@ describe('', () => { }); }); - describe('when threads feature is enabled', () => { + describe("when threads feature is enabled", () => { beforeEach(() => { - jest.spyOn(SettingsStore, 'getValue').mockImplementation(setting => setting === 'feature_thread'); + jest.spyOn(SettingsStore, "getValue").mockImplementation( + (setting) => setting === "feature_threadstable", + ); }); - it('renders thread button on own actionable event', () => { + it("renders thread button on own actionable event", () => { const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent }); - expect(queryByLabelText('Reply in thread')).toBeTruthy(); + expect(queryByLabelText("Reply in thread")).toBeTruthy(); }); - it('does not render thread button for a beacon_info event', () => { + it("does not render thread button for a beacon_info event", () => { const beaconInfoEvent = makeBeaconInfoEvent(userId, roomId); const { queryByLabelText } = getComponent({ mxEvent: beaconInfoEvent }); - expect(queryByLabelText('Reply in thread')).toBeFalsy(); + expect(queryByLabelText("Reply in thread")).toBeFalsy(); }); - it('opens thread on click', () => { + it("opens thread on click", () => { const { getByLabelText } = getComponent({ mxEvent: alicesMessageEvent }); act(() => { - fireEvent.click(getByLabelText('Reply in thread')); + fireEvent.click(getByLabelText("Reply in thread")); }); expect(dispatcher.dispatch).toHaveBeenCalledWith({ @@ -450,26 +452,26 @@ describe('', () => { }); }); - it('opens parent thread for a thread reply message', () => { + it("opens parent thread for a thread reply message", () => { const threadReplyEvent = new MatrixEvent({ type: EventType.RoomMessage, sender: userId, room_id: roomId, content: { msgtype: MsgType.Text, - body: 'this is a thread reply', + body: "this is a thread reply", }, }); // mock the thread stuff - jest.spyOn(threadReplyEvent, 'isThreadRoot', 'get').mockReturnValue(false); + jest.spyOn(threadReplyEvent, "isThreadRoot", "get").mockReturnValue(false); // set alicesMessageEvent as the root event - jest.spyOn(threadReplyEvent, 'getThread').mockReturnValue( - { rootEvent: alicesMessageEvent } as unknown as Thread, - ); + jest.spyOn(threadReplyEvent, "getThread").mockReturnValue({ + rootEvent: alicesMessageEvent, + } as unknown as Thread); const { getByLabelText } = getComponent({ mxEvent: threadReplyEvent }); act(() => { - fireEvent.click(getByLabelText('Reply in thread')); + fireEvent.click(getByLabelText("Reply in thread")); }); expect(dispatcher.dispatch).toHaveBeenCalledWith({ @@ -484,113 +486,115 @@ describe('', () => { }); }); - describe('favourite button', () => { + describe("favourite button", () => { //for multiple event usecase const favButton = (evt: MatrixEvent) => { return getComponent({ mxEvent: evt }).getByTestId(evt.getId()); }; - describe('when favourite_messages feature is enabled', () => { + describe("when favourite_messages feature is enabled", () => { beforeEach(() => { - jest.spyOn(SettingsStore, 'getValue') - .mockImplementation(setting => setting === 'feature_favourite_messages'); + jest.spyOn(SettingsStore, "getValue").mockImplementation( + (setting) => setting === "feature_favourite_messages", + ); localStorageMock.clear(); }); - it('renders favourite button on own actionable event', () => { + it("renders favourite button on own actionable event", () => { const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent }); - expect(queryByLabelText('Favourite')).toBeTruthy(); + expect(queryByLabelText("Favourite")).toBeTruthy(); }); - it('renders favourite button on other actionable events', () => { + it("renders favourite button on other actionable events", () => { const { queryByLabelText } = getComponent({ mxEvent: bobsMessageEvent }); - expect(queryByLabelText('Favourite')).toBeTruthy(); + expect(queryByLabelText("Favourite")).toBeTruthy(); }); - it('does not render Favourite button on non-actionable event', () => { + it("does not render Favourite button on non-actionable event", () => { //redacted event is not actionable const { queryByLabelText } = getComponent({ mxEvent: redactedEvent }); - expect(queryByLabelText('Favourite')).toBeFalsy(); + expect(queryByLabelText("Favourite")).toBeFalsy(); }); - it('remembers favourited state of multiple events, and handles the localStorage of the events accordingly', - () => { - const alicesAction = favButton(alicesMessageEvent); - const bobsAction = favButton(bobsMessageEvent); - - //default state before being clicked - expect(alicesAction.classList).not.toContain('mx_MessageActionBar_favouriteButton_fillstar'); - expect(bobsAction.classList).not.toContain('mx_MessageActionBar_favouriteButton_fillstar'); - expect(localStorageMock.getItem('io_element_favouriteMessages')).toBeNull(); - - //if only alice's event is fired - act(() => { - fireEvent.click(alicesAction); - }); - - expect(alicesAction.classList).toContain('mx_MessageActionBar_favouriteButton_fillstar'); - expect(bobsAction.classList).not.toContain('mx_MessageActionBar_favouriteButton_fillstar'); - expect(localStorageMock.setItem) - .toHaveBeenCalledWith('io_element_favouriteMessages', '["$alices_message"]'); - - //when bob's event is fired,both should be styled and stored in localStorage - act(() => { - fireEvent.click(bobsAction); - }); - - expect(alicesAction.classList).toContain('mx_MessageActionBar_favouriteButton_fillstar'); - expect(bobsAction.classList).toContain('mx_MessageActionBar_favouriteButton_fillstar'); - expect(localStorageMock.setItem) - .toHaveBeenCalledWith('io_element_favouriteMessages', '["$alices_message","$bobs_message"]'); - - //finally, at this point the localStorage should contain the two eventids - expect(localStorageMock.getItem('io_element_favouriteMessages')) - .toEqual('["$alices_message","$bobs_message"]'); - - //if decided to unfavourite bob's event by clicking again - act(() => { - fireEvent.click(bobsAction); - }); - expect(bobsAction.classList).not.toContain('mx_MessageActionBar_favouriteButton_fillstar'); - expect(alicesAction.classList).toContain('mx_MessageActionBar_favouriteButton_fillstar'); - expect(localStorageMock.getItem('io_element_favouriteMessages')).toEqual('["$alices_message"]'); + it("remembers favourited state of multiple events, and handles the localStorage of the events accordingly", () => { + const alicesAction = favButton(alicesMessageEvent); + const bobsAction = favButton(bobsMessageEvent); + + //default state before being clicked + expect(alicesAction.classList).not.toContain("mx_MessageActionBar_favouriteButton_fillstar"); + expect(bobsAction.classList).not.toContain("mx_MessageActionBar_favouriteButton_fillstar"); + expect(localStorageMock.getItem("io_element_favouriteMessages")).toBeNull(); + + //if only alice's event is fired + act(() => { + fireEvent.click(alicesAction); + }); + + expect(alicesAction.classList).toContain("mx_MessageActionBar_favouriteButton_fillstar"); + expect(bobsAction.classList).not.toContain("mx_MessageActionBar_favouriteButton_fillstar"); + expect(localStorageMock.setItem).toHaveBeenCalledWith( + "io_element_favouriteMessages", + '["$alices_message"]', + ); + + //when bob's event is fired,both should be styled and stored in localStorage + act(() => { + fireEvent.click(bobsAction); + }); + + expect(alicesAction.classList).toContain("mx_MessageActionBar_favouriteButton_fillstar"); + expect(bobsAction.classList).toContain("mx_MessageActionBar_favouriteButton_fillstar"); + expect(localStorageMock.setItem).toHaveBeenCalledWith( + "io_element_favouriteMessages", + '["$alices_message","$bobs_message"]', + ); + + //finally, at this point the localStorage should contain the two eventids + expect(localStorageMock.getItem("io_element_favouriteMessages")).toEqual( + '["$alices_message","$bobs_message"]', + ); + + //if decided to unfavourite bob's event by clicking again + act(() => { + fireEvent.click(bobsAction); }); + expect(bobsAction.classList).not.toContain("mx_MessageActionBar_favouriteButton_fillstar"); + expect(alicesAction.classList).toContain("mx_MessageActionBar_favouriteButton_fillstar"); + expect(localStorageMock.getItem("io_element_favouriteMessages")).toEqual('["$alices_message"]'); + }); }); - describe('when favourite_messages feature is disabled', () => { - it('does not render', () => { - jest.spyOn(SettingsStore, 'getValue').mockReturnValue(false); + describe("when favourite_messages feature is disabled", () => { + it("does not render", () => { + jest.spyOn(SettingsStore, "getValue").mockReturnValue(false); const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent }); - expect(queryByLabelText('Favourite')).toBeFalsy(); + expect(queryByLabelText("Favourite")).toBeFalsy(); }); }); }); - it.each([ - ["React"], - ["Reply"], - ["Reply in thread"], - ["Favourite"], - ["Edit"], - ])("does not show context menu when right-clicking", (buttonLabel: string) => { - // For favourite button - jest.spyOn(SettingsStore, 'getValue').mockReturnValue(true); - - const event = new MouseEvent("contextmenu", { - bubbles: true, - cancelable: true, - }); - event.stopPropagation = jest.fn(); - event.preventDefault = jest.fn(); + it.each([["React"], ["Reply"], ["Reply in thread"], ["Favourite"], ["Edit"]])( + "does not show context menu when right-clicking", + (buttonLabel: string) => { + // For favourite button + jest.spyOn(SettingsStore, "getValue").mockReturnValue(true); - const { queryByTestId, queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent }); - act(() => { - fireEvent(queryByLabelText(buttonLabel), event); - }); - expect(event.stopPropagation).toHaveBeenCalled(); - expect(event.preventDefault).toHaveBeenCalled(); - expect(queryByTestId("mx_MessageContextMenu")).toBeFalsy(); - }); + const event = new MouseEvent("contextmenu", { + bubbles: true, + cancelable: true, + }); + event.stopPropagation = jest.fn(); + event.preventDefault = jest.fn(); + + const { queryByTestId, queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent }); + act(() => { + fireEvent(queryByLabelText(buttonLabel), event); + }); + expect(event.stopPropagation).toHaveBeenCalled(); + expect(event.preventDefault).toHaveBeenCalled(); + expect(queryByTestId("mx_MessageContextMenu")).toBeFalsy(); + }, + ); it("does shows context menu when right-clicking options", () => { const { queryByTestId, queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent }); diff --git a/test/components/views/messages/MessageEvent-test.tsx b/test/components/views/messages/MessageEvent-test.tsx index dadddca093a..6ec3d490e38 100644 --- a/test/components/views/messages/MessageEvent-test.tsx +++ b/test/components/views/messages/MessageEvent-test.tsx @@ -26,11 +26,11 @@ import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalink jest.mock("../../../../src/components/views/messages/UnknownBody", () => ({ __esModule: true, - default: () => (
), + default: () =>
, })); jest.mock("../../../../src/voice-broadcast/components/VoiceBroadcastBody", () => ({ - VoiceBroadcastBody: () => (
), + VoiceBroadcastBody: () =>
, })); describe("MessageEvent", () => { @@ -39,11 +39,13 @@ describe("MessageEvent", () => { let event: MatrixEvent; const renderMessageEvent = (): RenderResult => { - return render(); + return render( + , + ); }; beforeEach(() => { diff --git a/test/components/views/messages/TextualBody-test.tsx b/test/components/views/messages/TextualBody-test.tsx index 0ca41b8fa71..01e969a0317 100644 --- a/test/components/views/messages/TextualBody-test.tsx +++ b/test/components/views/messages/TextualBody-test.tsx @@ -31,7 +31,7 @@ import { MediaEventHelper } from "../../../../src/utils/MediaEventHelper"; describe("", () => { afterEach(() => { - jest.spyOn(MatrixClientPeg, 'get').mockRestore(); + jest.spyOn(MatrixClientPeg, "get").mockRestore(); }); const defaultRoom = mkStubRoom("room_id", "test room", undefined); @@ -58,7 +58,7 @@ describe("", () => { const defaultProps = { mxEvent: defaultEvent, highlights: [], - highlightLink: '', + highlightLink: "", onMessageAllowed: jest.fn(), onHeightChanged: jest.fn(), permalinkCreator: new RoomPermalinkCreator(defaultRoom), @@ -107,7 +107,7 @@ describe("", () => { const wrapper = getComponent({ mxEvent: ev }); expect(wrapper.text()).toBe(ev.getContent().body); const content = wrapper.find(".mx_EventTile_body"); - expect(content.html()).toBe(`${ ev.getContent().body }`); + expect(content.html()).toBe(`${ev.getContent().body}`); }); describe("renders plain-text m.text correctly", () => { @@ -130,7 +130,7 @@ describe("", () => { const wrapper = getComponent({ mxEvent: ev }); expect(wrapper.text()).toBe(ev.getContent().body); const content = wrapper.find(".mx_EventTile_body"); - expect(content.html()).toBe(`${ ev.getContent().body }`); + expect(content.html()).toBe(`${ev.getContent().body}`); }); // If pills were rendered within a Portal/same shadow DOM then it'd be easier to test @@ -149,9 +149,11 @@ describe("", () => { const wrapper = getComponent({ mxEvent: ev }); expect(wrapper.text()).toBe(ev.getContent().body); const content = wrapper.find(".mx_EventTile_body"); - expect(content.html()).toBe('' + - 'Visit ' + - 'https://matrix.org/'); + expect(content.html()).toBe( + '' + + 'Visit ' + + "https://matrix.org/", + ); }); }); @@ -188,8 +190,11 @@ describe("", () => { const wrapper = getComponent({ mxEvent: ev }, matrixClient); expect(wrapper.text()).toBe("foo baz bar del u"); const content = wrapper.find(".mx_EventTile_body"); - expect(content.html()).toBe('' + - ev.getContent().formatted_body + ''); + expect(content.html()).toBe( + '' + + ev.getContent().formatted_body + + "", + ); }); it("spoilers get injected properly into the DOM", () => { @@ -201,7 +206,7 @@ describe("", () => { body: "Hey [Spoiler for movie](mxc://someserver/somefile)", msgtype: "m.text", format: "org.matrix.custom.html", - formatted_body: "Hey the movie was awesome", + formatted_body: 'Hey the movie was awesome', }, event: true, }); @@ -209,12 +214,14 @@ describe("", () => { const wrapper = getComponent({ mxEvent: ev }, matrixClient); expect(wrapper.text()).toBe("Hey (movie) the movie was awesome"); const content = wrapper.find(".mx_EventTile_body"); - expect(content.html()).toBe('' + - 'Hey ' + - '' + - '(movie) ' + - 'the movie was awesome' + - ''); + expect(content.html()).toBe( + '' + + "Hey " + + '' + + '(movie) ' + + 'the movie was awesome' + + "", + ); }); it("linkification is not applied to code blocks", () => { @@ -247,7 +254,7 @@ describe("", () => { body: "Hey User", msgtype: "m.text", format: "org.matrix.custom.html", - formatted_body: "Hey Member", + formatted_body: 'Hey Member', }, event: true, }); @@ -290,8 +297,8 @@ describe("", () => { msgtype: "m.text", format: "org.matrix.custom.html", formatted_body: - "An event link with text", + 'An event link with text', }, event: true, }); @@ -301,9 +308,9 @@ describe("", () => { const content = wrapper.find(".mx_EventTile_body"); expect(content.html()).toBe( '' + - 'An event link with text', + 'An event link with text', ); }); @@ -319,8 +326,8 @@ describe("", () => { msgtype: "m.text", format: "org.matrix.custom.html", formatted_body: - "A room link with vias", + 'A room link with vias', }, event: true, }); @@ -330,17 +337,17 @@ describe("", () => { const content = wrapper.find(".mx_EventTile_body"); expect(content.html()).toBe( '' + - 'A ' + - 'room name with vias', + 'A ' + + 'room name with vias', ); }); - it('renders formatted body without html corretly', () => { + it("renders formatted body without html corretly", () => { const ev = mkEvent({ type: "m.room.message", room: "room_id", @@ -358,15 +365,13 @@ describe("", () => { const content = wrapper.find(".mx_EventTile_body"); expect(content.html()).toBe( - '' + - 'escaped *markdown*' + - '', + '' + "escaped *markdown*" + "", ); }); }); it("renders url previews correctly", () => { - languageHandler.setMissingEntryGenerator(key => key.split('|', 2)[1]); + languageHandler.setMissingEntryGenerator((key) => key.split("|", 2)[1]); const matrixClient = getMockClientWithEventEmitter({ getRoom: () => mkStubRoom("room_id", "room name", undefined), @@ -408,21 +413,24 @@ describe("", () => { }, event: true, }); - jest.spyOn(ev, 'replacingEventDate').mockReturnValue(new Date(1993, 7, 3)); + jest.spyOn(ev, "replacingEventDate").mockReturnValue(new Date(1993, 7, 3)); ev.makeReplaced(ev2); - wrapper.setProps({ - mxEvent: ev, - replacingEventId: ev.getId(), - }, () => { - expect(wrapper.text()).toBe(ev2.getContent()["m.new_content"].body + "(edited)"); - - // XXX: this is to give TextualBody enough time for state to settle - wrapper.setState({}, () => { - widgets = wrapper.find("LinkPreviewGroup"); - // at this point we should have exactly two links (not the matrix.org one anymore) - expect(widgets.at(0).prop("links")).toEqual(["https://vector.im/", "https://riot.im/"]); - }); - }); + wrapper.setProps( + { + mxEvent: ev, + replacingEventId: ev.getId(), + }, + () => { + expect(wrapper.text()).toBe(ev2.getContent()["m.new_content"].body + "(edited)"); + + // XXX: this is to give TextualBody enough time for state to settle + wrapper.setState({}, () => { + widgets = wrapper.find("LinkPreviewGroup"); + // at this point we should have exactly two links (not the matrix.org one anymore) + expect(widgets.at(0).prop("links")).toEqual(["https://vector.im/", "https://riot.im/"]); + }); + }, + ); }); }); diff --git a/test/components/views/messages/__snapshots__/MVideoBody-test.tsx.snap b/test/components/views/messages/__snapshots__/MVideoBody-test.tsx.snap index d5346f96cbb..c7b62b5882d 100644 --- a/test/components/views/messages/__snapshots__/MVideoBody-test.tsx.snap +++ b/test/components/views/messages/__snapshots__/MVideoBody-test.tsx.snap @@ -1,3 +1,25 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`MVideoBody does not crash when given a portrait image 1`] = `"
"`; +exports[`MVideoBody does not crash when given a portrait image 1`] = ` + + +
+ ", - msgtype: "m.text", + searchResult={SearchResult.fromJson( + { + rank: 0.00424866, + result: { + content: { + body: "This is an example text message", + format: "org.matrix.custom.html", + formatted_body: "This is an example text message", + msgtype: "m.text", + }, + event_id: "$144429830826TWwbB:localhost", + origin_server_ts: 1432735824653, + room_id: ROOM_ID, + sender: "@example:example.org", + type: "m.room.message", + unsigned: { + age: 1234, + }, }, - event_id: "$144429830826TWwbB:localhost", - origin_server_ts: 1432735824653, - room_id: "!qPewotXpIctQySfjSy:localhost", - sender: "@example:example.org", - type: "m.room.message", - unsigned: { - age: 1234, + context: { + end: "", + start: "", + profile_info: {}, + events_before: [ + { + type: EventType.CallInvite, + sender: "@user1:server", + room_id: ROOM_ID, + origin_server_ts: 1432735824652, + content: { call_id: "call.1" }, + event_id: "$1:server", + }, + ], + events_after: [ + { + type: EventType.CallAnswer, + sender: "@user2:server", + room_id: ROOM_ID, + origin_server_ts: 1432735824654, + content: { call_id: "call.1" }, + event_id: "$2:server", + }, + ], }, }, - context: { - end: "", - start: "", - profile_info: {}, - events_before: [{ - type: EventType.CallInvite, - sender: "@user1:server", - room_id: "!qPewotXpIctQySfjSy:localhost", - origin_server_ts: 1432735824652, - content: { call_id: "call.1" }, - event_id: "$1:server", - }], - events_after: [{ - type: EventType.CallAnswer, - sender: "@user2:server", - room_id: "!qPewotXpIctQySfjSy:localhost", - origin_server_ts: 1432735824654, - content: { call_id: "call.1" }, - event_id: "$2:server", - }], - }, - }, o => new MatrixEvent(o))} + (o) => new MatrixEvent(o), + )} />, ); - const tiles = wrapper.find(EventTile); + const tiles = container.querySelectorAll(".mx_EventTile"); expect(tiles.length).toEqual(2); - expect(tiles.at(0).prop("mxEvent").getId()).toBe("$1:server"); - // @ts-ignore accessing private property - expect(tiles.at(0).prop("callEventGrouper").events.size).toBe(2); - expect(tiles.at(1).prop("mxEvent").getId()).toBe("$144429830826TWwbB:localhost"); + expect(tiles[0].dataset.eventId).toBe("$1:server"); + expect(tiles[1].dataset.eventId).toBe("$144429830826TWwbB:localhost"); }); }); diff --git a/test/components/views/rooms/SendMessageComposer-test.tsx b/test/components/views/rooms/SendMessageComposer-test.tsx index b9bdae45d4e..1005758ae99 100644 --- a/test/components/views/rooms/SendMessageComposer-test.tsx +++ b/test/components/views/rooms/SendMessageComposer-test.tsx @@ -15,17 +15,13 @@ limitations under the License. */ import React from "react"; -import { act } from "react-dom/test-utils"; -import { sleep } from "matrix-js-sdk/src/utils"; -import { MatrixClient, MsgType, RelationType } from "matrix-js-sdk/src/matrix"; -// eslint-disable-next-line deprecate/import -import { mount } from 'enzyme'; +import { fireEvent, render, waitFor } from "@testing-library/react"; +import { MatrixClient, MsgType } from "matrix-js-sdk/src/matrix"; import { mocked } from "jest-mock"; import SendMessageComposer, { createMessageContent, isQuickReaction, - SendMessageComposer as SendMessageComposerClass, } from "../../../../src/components/views/rooms/SendMessageComposer"; import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; import RoomContext, { TimelineRenderingType } from "../../../../src/contexts/RoomContext"; @@ -34,8 +30,8 @@ import { createPartCreator, createRenderer } from "../../../editor/mock"; import { createTestClient, mkEvent, mkStubRoom } from "../../../test-utils"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import defaultDispatcher from "../../../../src/dispatcher/dispatcher"; -import DocumentOffset from '../../../../src/editor/offset'; -import { Layout } from '../../../../src/settings/enums/Layout'; +import DocumentOffset from "../../../../src/editor/offset"; +import { Layout } from "../../../../src/settings/enums/Layout"; import { IRoomState } from "../../../../src/components/structures/RoomView"; import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks"; import { mockPlatformPeg } from "../../../test-utils/platform"; @@ -46,16 +42,7 @@ jest.mock("../../../../src/utils/local-room", () => ({ doMaybeLocalRoomAction: jest.fn(), })); -const WrapWithProviders: React.FC<{ - roomContext: IRoomState; - client: MatrixClient; -}> = ({ children, roomContext, client }) => - - { children } - -; - -describe('', () => { +describe("", () => { const defaultRoomContext: IRoomState = { roomLoading: true, peekLoading: false, @@ -169,16 +156,16 @@ describe('', () => { describe("functions correctly mounted", () => { const mockClient = createTestClient(); - jest.spyOn(MatrixClientPeg, 'get').mockReturnValue(mockClient); - const mockRoom = mkStubRoom('myfakeroom', 'myfakeroom', mockClient) as any; + jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient); + const mockRoom = mkStubRoom("myfakeroom", "myfakeroom", mockClient) as any; const mockEvent = mkEvent({ type: "m.room.message", - room: 'myfakeroom', - user: 'myfakeuser', - content: { "msgtype": "m.text", "body": "Replying to this" }, + room: "myfakeroom", + user: "myfakeuser", + content: { msgtype: "m.text", body: "Replying to this" }, event: true, }); - mockRoom.findEventById = jest.fn(eventId => { + mockRoom.findEventById = jest.fn((eventId) => { return eventId === mockEvent.getId() ? mockEvent : null; }); @@ -194,44 +181,48 @@ describe('', () => { toggleStickerPickerOpen: jest.fn(), permalinkCreator: new RoomPermalinkCreator(mockRoom), }; + const getRawComponent = (props = {}, roomContext = defaultRoomContext, client = mockClient) => ( + + + + + + ); const getComponent = (props = {}, roomContext = defaultRoomContext, client = mockClient) => { - return mount(, { - wrappingComponent: WrapWithProviders, - wrappingComponentProps: { roomContext, client }, - }); + return render(getRawComponent(props, roomContext, client)); }; it("renders text and placeholder correctly", () => { - const wrapper = getComponent({ placeholder: "placeholder string" }); + const { container } = getComponent({ placeholder: "placeholder string" }); - expect(wrapper.find('[aria-label="placeholder string"]')).toHaveLength(1); + expect(container.querySelectorAll('[aria-label="placeholder string"]')).toHaveLength(1); - addTextToComposer(wrapper, "Test Text"); + addTextToComposer(container, "Test Text"); - expect(wrapper.text()).toBe("Test Text"); + expect(container.textContent).toBe("Test Text"); }); it("correctly persists state to and from localStorage", () => { - const wrapper = getComponent({ replyToEvent: mockEvent }); + const props = { replyToEvent: mockEvent }; + const { container, unmount, rerender } = getComponent(props); - addTextToComposer(wrapper, "Test Text"); + addTextToComposer(container, "Test Text"); - // @ts-ignore - const key = wrapper.find(SendMessageComposerClass).instance().editorStateKey; + const key = "mx_cider_state_myfakeroom"; - expect(wrapper.text()).toBe("Test Text"); + expect(container.textContent).toBe("Test Text"); expect(localStorage.getItem(key)).toBeNull(); // ensure the right state was persisted to localStorage - wrapper.unmount(); + unmount(); expect(JSON.parse(localStorage.getItem(key))).toStrictEqual({ - parts: [{ "type": "plain", "text": "Test Text" }], + parts: [{ type: "plain", text: "Test Text" }], replyEventId: mockEvent.getId(), }); // ensure the correct model is re-loaded - wrapper.mount(); - expect(wrapper.text()).toBe("Test Text"); + rerender(getRawComponent(props)); + expect(container.textContent).toBe("Test Text"); expect(spyDispatcher).toHaveBeenCalledWith({ action: "reply_to_event", event: mockEvent, @@ -239,95 +230,70 @@ describe('', () => { }); // now try with localStorage wiped out - wrapper.unmount(); + unmount(); localStorage.removeItem(key); - wrapper.mount(); - expect(wrapper.text()).toBe(""); + rerender(getRawComponent(props)); + expect(container.textContent).toBe(""); }); it("persists state correctly without replyToEvent onbeforeunload", () => { - const wrapper = getComponent(); + const { container } = getComponent(); - addTextToComposer(wrapper, "Hello World"); + addTextToComposer(container, "Hello World"); - // @ts-ignore - const key = wrapper.find(SendMessageComposerClass).instance().editorStateKey; + const key = "mx_cider_state_myfakeroom"; - expect(wrapper.text()).toBe("Hello World"); + expect(container.textContent).toBe("Hello World"); expect(localStorage.getItem(key)).toBeNull(); // ensure the right state was persisted to localStorage - window.dispatchEvent(new Event('beforeunload')); + window.dispatchEvent(new Event("beforeunload")); expect(JSON.parse(localStorage.getItem(key))).toStrictEqual({ - parts: [{ "type": "plain", "text": "Hello World" }], + parts: [{ type: "plain", text: "Hello World" }], }); }); it("persists to session history upon sending", async () => { mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) }); - const wrapper = getComponent({ replyToEvent: mockEvent }); + const { container } = getComponent({ replyToEvent: mockEvent }); - addTextToComposer(wrapper, "This is a message"); - act(() => { - wrapper.find(".mx_SendMessageComposer").simulate("keydown", { key: "Enter" }); - wrapper.update(); - }); - await sleep(10); // await the async _sendMessage - wrapper.update(); - expect(spyDispatcher).toHaveBeenCalledWith({ - action: "reply_to_event", - event: null, - context: TimelineRenderingType.Room, + addTextToComposer(container, "This is a message"); + fireEvent.keyDown(container.querySelector(".mx_SendMessageComposer"), { key: "Enter" }); + + await waitFor(() => { + expect(spyDispatcher).toHaveBeenCalledWith({ + action: "reply_to_event", + event: null, + context: TimelineRenderingType.Room, + }); }); - expect(wrapper.text()).toBe(""); + expect(container.textContent).toBe(""); const str = sessionStorage.getItem(`mx_cider_history_${mockRoom.roomId}[0]`); expect(JSON.parse(str)).toStrictEqual({ - parts: [{ "type": "plain", "text": "This is a message" }], + parts: [{ type: "plain", text: "This is a message" }], replyEventId: mockEvent.getId(), }); }); - it('correctly sets the editorStateKey for threads', () => { - const relation = { - rel_type: RelationType.Thread, - event_id: "myFakeThreadId", - }; - const includeReplyLegacyFallback = false; - const wrapper = getComponent({ relation, includeReplyLegacyFallback }); - const instance = wrapper.find(SendMessageComposerClass).instance(); - // @ts-ignore - const key = instance.editorStateKey; - expect(key).toEqual('mx_cider_state_myfakeroom_myFakeThreadId'); - }); - it("correctly sends a message", () => { - mocked(doMaybeLocalRoomAction).mockImplementation(( - roomId: string, - fn: (actualRoomId: string) => Promise, - _client?: MatrixClient, - ) => { - return fn(roomId); - }); + mocked(doMaybeLocalRoomAction).mockImplementation( + (roomId: string, fn: (actualRoomId: string) => Promise, _client?: MatrixClient) => { + return fn(roomId); + }, + ); mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) }); - const wrapper = getComponent(); + const { container } = getComponent(); - addTextToComposer(wrapper, "test message"); - act(() => { - wrapper.find(".mx_SendMessageComposer").simulate("keydown", { key: "Enter" }); - wrapper.update(); - }); + addTextToComposer(container, "test message"); + fireEvent.keyDown(container.querySelector(".mx_SendMessageComposer"), { key: "Enter" }); - expect(mockClient.sendMessage).toHaveBeenCalledWith( - "myfakeroom", - null, - { - "body": "test message", - "msgtype": MsgType.Text, - }, - ); + expect(mockClient.sendMessage).toHaveBeenCalledWith("myfakeroom", null, { + body: "test message", + msgtype: MsgType.Text, + }); }); }); @@ -367,4 +333,3 @@ describe('', () => { }); }); }); - diff --git a/test/components/views/rooms/VoiceRecordComposerTile-test.tsx b/test/components/views/rooms/VoiceRecordComposerTile-test.tsx index eb9f72d7832..e9f7615cca0 100644 --- a/test/components/views/rooms/VoiceRecordComposerTile-test.tsx +++ b/test/components/views/rooms/VoiceRecordComposerTile-test.tsx @@ -65,13 +65,11 @@ describe("", () => { recorder: mockRecorder, }); - mocked(doMaybeLocalRoomAction).mockImplementation(( - roomId: string, - fn: (actualRoomId: string) => Promise, - _client?: MatrixClient, - ) => { - return fn(roomId); - }); + mocked(doMaybeLocalRoomAction).mockImplementation( + (roomId: string, fn: (actualRoomId: string) => Promise, _client?: MatrixClient) => { + return fn(roomId); + }, + ); }); describe("send", () => { @@ -81,25 +79,21 @@ describe("", () => { "body": "Voice message", "file": undefined, "info": { - "duration": 1337000, - "mimetype": "audio/ogg", - "size": undefined, + duration: 1337000, + mimetype: "audio/ogg", + size: undefined, }, "msgtype": MsgType.Audio, "org.matrix.msc1767.audio": { - "duration": 1337000, - "waveform": [ - 1434, - 2560, - 3686, - ], + duration: 1337000, + waveform: [1434, 2560, 3686], }, "org.matrix.msc1767.file": { - "file": undefined, - "mimetype": "audio/ogg", - "name": "Voice message.ogg", - "size": undefined, - "url": "mxc://example.com/voice", + file: undefined, + mimetype: "audio/ogg", + name: "Voice message.ogg", + size: undefined, + url: "mxc://example.com/voice", }, "org.matrix.msc1767.text": "Voice message", "org.matrix.msc3245.voice": {}, diff --git a/test/components/views/rooms/__snapshots__/RoomTile-test.tsx.snap b/test/components/views/rooms/__snapshots__/RoomTile-test.tsx.snap new file mode 100644 index 00000000000..b4114bcb537 --- /dev/null +++ b/test/components/views/rooms/__snapshots__/RoomTile-test.tsx.snap @@ -0,0 +1,81 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RoomTile should render the room 1`] = ` +
+
+
+ + + + +
+
+
+ + !1:​example.org + +
+
+ + +`; diff --git a/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx index 00d6a43f977..cc53c88dc03 100644 --- a/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx @@ -16,66 +16,37 @@ limitations under the License. import "@testing-library/jest-dom"; import React from "react"; -import { render, screen, waitFor } from "@testing-library/react"; -import { WysiwygProps } from "@matrix-org/matrix-wysiwyg"; +import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; import RoomContext from "../../../../../src/contexts/RoomContext"; import defaultDispatcher from "../../../../../src/dispatcher/dispatcher"; import { Action } from "../../../../../src/dispatcher/actions"; import { IRoomState } from "../../../../../src/components/structures/RoomView"; -import { createTestClient, getRoomContext, mkEvent, mkStubRoom } from "../../../../test-utils"; -import { EditWysiwygComposer } - from "../../../../../src/components/views/rooms/wysiwyg_composer"; +import { createTestClient, flushPromises, getRoomContext, mkEvent, mkStubRoom } from "../../../../test-utils"; +import { EditWysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer"; import EditorStateTransfer from "../../../../../src/utils/EditorStateTransfer"; -const mockClear = jest.fn(); - -let initialContent: string; -const defaultContent = 'html'; -let mockContent = defaultContent; - -// The wysiwyg fetch wasm bytes and a specific workaround is needed to make it works in a node (jest) environnement -// See https://github.com/matrix-org/matrix-wysiwyg/blob/main/platforms/web/test.setup.ts -jest.mock("@matrix-org/matrix-wysiwyg", () => ({ - useWysiwyg: (props: WysiwygProps) => { - initialContent = props.initialContent; - return { - ref: { current: null }, - content: mockContent, - isWysiwygReady: true, - wysiwyg: { clear: mockClear }, - formattingStates: { - bold: 'enabled', - italic: 'enabled', - underline: 'enabled', - strikeThrough: 'enabled', - }, - }; - }, -})); - -describe('EditWysiwygComposer', () => { +describe("EditWysiwygComposer", () => { afterEach(() => { jest.resetAllMocks(); - mockContent = defaultContent; }); const mockClient = createTestClient(); const mockEvent = mkEvent({ type: "m.room.message", - room: 'myfakeroom', - user: 'myfakeuser', + room: "myfakeroom", + user: "myfakeuser", content: { - "msgtype": "m.text", - "body": "Replying to this", - "format": "org.matrix.custom.html", - "formatted_body": "Replying to this new content", + msgtype: "m.text", + body: "Replying to this", + format: "org.matrix.custom.html", + formatted_body: "Replying to this new content", }, event: true, }); - const mockRoom = mkStubRoom('myfakeroom', 'myfakeroom', mockClient) as any; - mockRoom.findEventById = jest.fn(eventId => { + const mockRoom = mkStubRoom("myfakeroom", "myfakeroom", mockClient) as any; + mockRoom.findEventById = jest.fn((eventId) => { return eventId === mockEvent.getId() ? mockEvent : null; }); @@ -93,46 +64,100 @@ describe('EditWysiwygComposer', () => { ); }; - describe('Initialize with content', () => { - it('Should initialize useWysiwyg with html content', async () => { + describe("Initialize with content", () => { + it("Should initialize useWysiwyg with html content", async () => { // When - customRender(true); + customRender(false, editorStateTransfer); + await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true")); // Then - expect(initialContent).toBe(mockEvent.getContent()['formatted_body']); + await waitFor(() => + expect(screen.getByRole("textbox")).toContainHTML(mockEvent.getContent()["formatted_body"]), + ); }); - it('Should initialize useWysiwyg with plain text content', async () => { + it("Should initialize useWysiwyg with plain text content", async () => { // When const mockEvent = mkEvent({ type: "m.room.message", - room: 'myfakeroom', - user: 'myfakeuser', + room: "myfakeroom", + user: "myfakeuser", content: { - "msgtype": "m.text", - "body": "Replying to this", + msgtype: "m.text", + body: "Replying to this", }, event: true, }); const editorStateTransfer = new EditorStateTransfer(mockEvent); + customRender(false, editorStateTransfer); + await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true")); - customRender(true, editorStateTransfer); + // Then + await waitFor(() => expect(screen.getByRole("textbox")).toContainHTML(mockEvent.getContent()["body"])); + }); + + it("Should ignore when formatted_body is not filled", async () => { + // When + const mockEvent = mkEvent({ + type: "m.room.message", + room: "myfakeroom", + user: "myfakeuser", + content: { + msgtype: "m.text", + body: "Replying to this", + format: "org.matrix.custom.html", + }, + event: true, + }); + + const editorStateTransfer = new EditorStateTransfer(mockEvent); + customRender(false, editorStateTransfer); + + // Then + await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true")); + }); + + it("Should strip tag from initial content", async () => { + // When + const mockEvent = mkEvent({ + type: "m.room.message", + room: "myfakeroom", + user: "myfakeuser", + content: { + msgtype: "m.text", + body: "Replying to this", + format: "org.matrix.custom.html", + formatted_body: "ReplyMy content", + }, + event: true, + }); + + const editorStateTransfer = new EditorStateTransfer(mockEvent); + customRender(false, editorStateTransfer); + await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true")); // Then - expect(initialContent).toBe(mockEvent.getContent().body); + await waitFor(() => { + expect(screen.getByRole("textbox")).not.toContainHTML("Reply"); + expect(screen.getByRole("textbox")).toContainHTML("My content"); + }); }); }); - describe('Edit and save actions', () => { + describe("Edit and save actions", () => { + beforeEach(async () => { + customRender(); + await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true")); + }); + const spyDispatcher = jest.spyOn(defaultDispatcher, "dispatch"); afterEach(() => { spyDispatcher.mockRestore(); }); - it('Should cancel edit on cancel button click', async () => { + it("Should cancel edit on cancel button click", async () => { // When - customRender(true); - (await screen.findByText('Cancel')).click(); + screen.getByText("Cancel").click(); // Then expect(spyDispatcher).toBeCalledWith({ @@ -146,48 +171,43 @@ describe('EditWysiwygComposer', () => { }); }); - it('Should send message on save button click', async () => { + it("Should send message on save button click", async () => { // When const spyDispatcher = jest.spyOn(defaultDispatcher, "dispatch"); - - const renderer = customRender(true); - - mockContent = 'my new content'; - renderer.rerender( - - - - ); - - (await screen.findByText('Save')).click(); + fireEvent.input(screen.getByRole("textbox"), { + data: "foo bar", + inputType: "insertText", + }); + await waitFor(() => expect(screen.getByText("Save")).not.toHaveAttribute("disabled")); // Then + screen.getByText("Save").click(); const expectedContent = { - "body": ` * ${mockContent}`, + "body": ` * foo bar`, "format": "org.matrix.custom.html", - "formatted_body": ` * ${mockContent}`, + "formatted_body": ` * foo bar`, "m.new_content": { - "body": mockContent, - "format": "org.matrix.custom.html", - "formatted_body": mockContent, - "msgtype": "m.text", + body: "foo bar", + format: "org.matrix.custom.html", + formatted_body: "foo bar", + msgtype: "m.text", }, "m.relates_to": { - "event_id": mockEvent.getId(), - "rel_type": "m.replace", + event_id: mockEvent.getId(), + rel_type: "m.replace", }, "msgtype": "m.text", }; expect(mockClient.sendMessage).toBeCalledWith(mockEvent.getRoomId(), null, expectedContent); - expect(spyDispatcher).toBeCalledWith({ action: 'message_sent' }); + expect(spyDispatcher).toBeCalledWith({ action: "message_sent" }); }); }); - it('Should focus when receiving an Action.FocusEditMessageComposer action', async () => { + it("Should focus when receiving an Action.FocusEditMessageComposer action", async () => { // Given we don't have focus customRender(); - screen.getByLabelText('Bold').focus(); - expect(screen.getByRole('textbox')).not.toHaveFocus(); + screen.getByLabelText("Bold").focus(); + expect(screen.getByRole("textbox")).not.toHaveFocus(); // When we send the right action defaultDispatcher.dispatch({ @@ -196,31 +216,32 @@ describe('EditWysiwygComposer', () => { }); // Then the component gets the focus - await waitFor(() => expect(screen.getByRole('textbox')).toHaveFocus()); + await waitFor(() => expect(screen.getByRole("textbox")).toHaveFocus()); }); - it('Should not focus when disabled', async () => { + it("Should not focus when disabled", async () => { // Given we don't have focus and we are disabled customRender(true); - screen.getByLabelText('Bold').focus(); - expect(screen.getByRole('textbox')).not.toHaveFocus(); + screen.getByLabelText("Bold").focus(); + expect(screen.getByRole("textbox")).not.toHaveFocus(); // When we send an action that would cause us to get focus - defaultDispatcher.dispatch({ - action: Action.FocusEditMessageComposer, - context: null, - }); - // (Send a second event to exercise the clearTimeout logic) - defaultDispatcher.dispatch({ - action: Action.FocusEditMessageComposer, - context: null, + act(() => { + defaultDispatcher.dispatch({ + action: Action.FocusEditMessageComposer, + context: null, + }); + // (Send a second event to exercise the clearTimeout logic) + defaultDispatcher.dispatch({ + action: Action.FocusEditMessageComposer, + context: null, + }); }); // Wait for event dispatch to happen - await new Promise((r) => setTimeout(r, 200)); + await flushPromises(); // Then we don't get it because we are disabled - expect(screen.getByRole('textbox')).not.toHaveFocus(); + expect(screen.getByRole("textbox")).not.toHaveFocus(); }); }); - diff --git a/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx index 3b5b8885d8f..669c611f8ce 100644 --- a/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx @@ -16,8 +16,7 @@ limitations under the License. import "@testing-library/jest-dom"; import React from "react"; -import { render, screen, waitFor } from "@testing-library/react"; -import { WysiwygProps } from "@matrix-org/matrix-wysiwyg"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; import RoomContext from "../../../../../src/contexts/RoomContext"; @@ -26,32 +25,21 @@ import { Action } from "../../../../../src/dispatcher/actions"; import { IRoomState } from "../../../../../src/components/structures/RoomView"; import { createTestClient, flushPromises, getRoomContext, mkEvent, mkStubRoom } from "../../../../test-utils"; import { SendWysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer"; -import * as useComposerFunctions - from "../../../../../src/components/views/rooms/wysiwyg_composer/hooks/useComposerFunctions"; import { aboveLeftOf } from "../../../../../src/components/structures/ContextMenu"; +import { ComposerInsertPayload, ComposerType } from "../../../../../src/dispatcher/payloads/ComposerInsertPayload"; +import { setSelection } from "../../../../../src/components/views/rooms/wysiwyg_composer/utils/selection"; -const mockClear = jest.fn(); - -// The wysiwyg fetch wasm bytes and a specific workaround is needed to make it works in a node (jest) environnement -// See https://github.com/matrix-org/matrix-wysiwyg/blob/main/platforms/web/test.setup.ts -jest.mock("@matrix-org/matrix-wysiwyg", () => ({ - useWysiwyg: (props: WysiwygProps) => { - return { - ref: { current: null }, - content: 'html', - isWysiwygReady: true, - wysiwyg: { clear: mockClear }, - formattingStates: { - bold: 'enabled', - italic: 'enabled', - underline: 'enabled', - strikeThrough: 'enabled', - }, - }; +jest.mock("../../../../../src/components/views/rooms/EmojiButton", () => ({ + EmojiButton: ({ addEmoji }: { addEmoji: (emoji: string) => void }) => { + return ( + + ); }, })); -describe('SendWysiwygComposer', () => { +describe("SendWysiwygComposer", () => { afterEach(() => { jest.resetAllMocks(); }); @@ -59,58 +47,90 @@ describe('SendWysiwygComposer', () => { const mockClient = createTestClient(); const mockEvent = mkEvent({ type: "m.room.message", - room: 'myfakeroom', - user: 'myfakeuser', - content: { "msgtype": "m.text", "body": "Replying to this" }, + room: "myfakeroom", + user: "myfakeuser", + content: { msgtype: "m.text", body: "Replying to this" }, event: true, }); - const mockRoom = mkStubRoom('myfakeroom', 'myfakeroom', mockClient) as any; - mockRoom.findEventById = jest.fn(eventId => { + const mockRoom = mkStubRoom("myfakeroom", "myfakeroom", mockClient) as any; + mockRoom.findEventById = jest.fn((eventId) => { return eventId === mockEvent.getId() ? mockEvent : null; }); const defaultRoomContext: IRoomState = getRoomContext(mockRoom, {}); + const registerId = defaultDispatcher.register((payload) => { + switch (payload.action) { + case Action.ComposerInsert: { + if (payload.composerType) break; + + // re-dispatch to the correct composer + defaultDispatcher.dispatch({ + ...(payload as ComposerInsertPayload), + composerType: ComposerType.Send, + }); + break; + } + } + }); + + afterAll(() => { + defaultDispatcher.unregister(registerId); + }); + const customRender = ( - onChange = (_content: string) => void 0, - onSend = () => void 0, + onChange = (_content: string): void => void 0, + onSend = (): void => void 0, disabled = false, - isRichTextEnabled = true) => { + isRichTextEnabled = true, + placeholder?: string, + ) => { return render( - + , ); }; - it('Should render WysiwygComposer when isRichTextEnabled is at true', () => { + it("Should render WysiwygComposer when isRichTextEnabled is at true", () => { // When customRender(jest.fn(), jest.fn(), false, true); // Then - expect(screen.getByTestId('WysiwygComposer')).toBeTruthy(); + expect(screen.getByTestId("WysiwygComposer")).toBeTruthy(); }); - it('Should render PlainTextComposer when isRichTextEnabled is at false', () => { + it("Should render PlainTextComposer when isRichTextEnabled is at false", () => { // When customRender(jest.fn(), jest.fn(), false, false); // Then - expect(screen.getByTestId('PlainTextComposer')).toBeTruthy(); + expect(screen.getByTestId("PlainTextComposer")).toBeTruthy(); }); - describe.each([{ isRichTextEnabled: true }, { isRichTextEnabled: false }])( - 'Should focus when receiving an Action.FocusSendMessageComposer action', - ({ isRichTextEnabled }) => { + describe.each([ + { isRichTextEnabled: true, emptyContent: "
" }, + { isRichTextEnabled: false, emptyContent: "" }, + ])( + "Should focus when receiving an Action.FocusSendMessageComposer action", + ({ isRichTextEnabled, emptyContent }) => { afterEach(() => { jest.resetAllMocks(); }); - it('Should focus when receiving an Action.FocusSendMessageComposer action', async () => { - // Given we don't have focus + it("Should focus when receiving an Action.FocusSendMessageComposer action", async () => { + // Given we don't have focus customRender(jest.fn(), jest.fn(), false, isRichTextEnabled); + await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true")); // When we send the right action defaultDispatcher.dispatch({ @@ -119,14 +139,19 @@ describe('SendWysiwygComposer', () => { }); // Then the component gets the focus - await waitFor(() => expect(screen.getByRole('textbox')).toHaveFocus()); + await waitFor(() => expect(screen.getByRole("textbox")).toHaveFocus()); }); - it('Should focus and clear when receiving an Action.ClearAndFocusSendMessageComposer', async () => { - // Given we don't have focus - const mock = jest.spyOn(useComposerFunctions, 'useComposerFunctions'); - mock.mockReturnValue({ clear: mockClear }); - customRender(jest.fn(), jest.fn(), false, isRichTextEnabled); + it("Should focus and clear when receiving an Action.ClearAndFocusSendMessageComposer", async () => { + // Given we don't have focus + const onChange = jest.fn(); + customRender(onChange, jest.fn(), false, isRichTextEnabled); + await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true")); + + fireEvent.input(screen.getByRole("textbox"), { + data: "foo bar", + inputType: "insertText", + }); // When we send the right action defaultDispatcher.dispatch({ @@ -135,15 +160,16 @@ describe('SendWysiwygComposer', () => { }); // Then the component gets the focus - await waitFor(() => expect(screen.getByRole('textbox')).toHaveFocus()); - expect(mockClear).toBeCalledTimes(1); - - mock.mockRestore(); + await waitFor(() => { + expect(screen.getByRole("textbox")).toHaveTextContent(/^$/); + expect(screen.getByRole("textbox")).toHaveFocus(); + }); }); - it('Should focus when receiving a reply_to_event action', async () => { - // Given we don't have focus + it("Should focus when receiving a reply_to_event action", async () => { + // Given we don't have focus customRender(jest.fn(), jest.fn(), false, isRichTextEnabled); + await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true")); // When we send the right action defaultDispatcher.dispatch({ @@ -152,13 +178,13 @@ describe('SendWysiwygComposer', () => { }); // Then the component gets the focus - await waitFor(() => expect(screen.getByRole('textbox')).toHaveFocus()); + await waitFor(() => expect(screen.getByRole("textbox")).toHaveFocus()); }); - it('Should not focus when disabled', async () => { - // Given we don't have focus and we are disabled + it("Should not focus when disabled", async () => { + // Given we don't have focus and we are disabled customRender(jest.fn(), jest.fn(), true, isRichTextEnabled); - expect(screen.getByRole('textbox')).not.toHaveFocus(); + expect(screen.getByRole("textbox")).not.toHaveFocus(); // When we send an action that would cause us to get focus defaultDispatcher.dispatch({ @@ -175,8 +201,114 @@ describe('SendWysiwygComposer', () => { await flushPromises(); // Then we don't get it because we are disabled - expect(screen.getByRole('textbox')).not.toHaveFocus(); + expect(screen.getByRole("textbox")).not.toHaveFocus(); + }); + }, + ); + + describe.each([{ isRichTextEnabled: true }, { isRichTextEnabled: false }])( + "Placeholder when %s", + ({ isRichTextEnabled }) => { + afterEach(() => { + jest.resetAllMocks(); + }); + + it("Should not has placeholder", async () => { + // When + customRender(jest.fn(), jest.fn(), false, isRichTextEnabled); + await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true")); + + // Then + expect(screen.getByRole("textbox")).not.toHaveClass("mx_WysiwygComposer_Editor_content_placeholder"); + }); + + it("Should has placeholder", async () => { + // When + customRender(jest.fn(), jest.fn(), false, isRichTextEnabled, "my placeholder"); + await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true")); + + // Then + expect(screen.getByRole("textbox")).toHaveClass("mx_WysiwygComposer_Editor_content_placeholder"); }); - }); -}); + it("Should display or not placeholder when editor content change", async () => { + // When + customRender(jest.fn(), jest.fn(), false, isRichTextEnabled, "my placeholder"); + await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true")); + screen.getByRole("textbox").innerHTML = "f"; + fireEvent.input(screen.getByRole("textbox"), { + data: "f", + inputType: "insertText", + }); + + // Then + await waitFor(() => + expect(screen.getByRole("textbox")).not.toHaveClass( + "mx_WysiwygComposer_Editor_content_placeholder", + ), + ); + + // When + screen.getByRole("textbox").innerHTML = ""; + fireEvent.input(screen.getByRole("textbox"), { + inputType: "deleteContentBackward", + }); + + // Then + await waitFor(() => + expect(screen.getByRole("textbox")).toHaveClass("mx_WysiwygComposer_Editor_content_placeholder"), + ); + }); + }, + ); + + describe.each([{ isRichTextEnabled: true }, { isRichTextEnabled: false }])( + "Emoji when %s", + ({ isRichTextEnabled }) => { + let emojiButton: HTMLElement; + + beforeEach(async () => { + customRender(jest.fn(), jest.fn(), false, isRichTextEnabled); + await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true")); + emojiButton = screen.getByLabelText("Emoji"); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it("Should add an emoji in an empty composer", async () => { + // When + emojiButton.click(); + + // Then + await waitFor(() => expect(screen.getByRole("textbox")).toHaveTextContent(/🦫/)); + }); + + it("Should add an emoji in the middle of a word", async () => { + // When + screen.getByRole("textbox").focus(); + screen.getByRole("textbox").innerHTML = "word"; + fireEvent.input(screen.getByRole("textbox"), { + data: "word", + inputType: "insertText", + }); + + const textNode = screen.getByRole("textbox").firstChild; + setSelection({ + anchorNode: textNode, + anchorOffset: 2, + focusNode: textNode, + focusOffset: 2, + }); + // the event is not automatically fired by jest + document.dispatchEvent(new CustomEvent("selectionchange")); + + emojiButton.click(); + + // Then + await waitFor(() => expect(screen.getByRole("textbox")).toHaveTextContent(/wo🦫rd/)); + }); + }, + ); +}); diff --git a/test/components/views/rooms/wysiwyg_composer/components/FormattingButtons-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/FormattingButtons-test.tsx index e935b62ae5e..d143e43a628 100644 --- a/test/components/views/rooms/wysiwyg_composer/components/FormattingButtons-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/components/FormattingButtons-test.tsx @@ -14,65 +14,86 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React from "react"; import { render, screen } from "@testing-library/react"; -import userEvent from '@testing-library/user-event'; +import userEvent from "@testing-library/user-event"; +import { AllActionStates, FormattingFunctions } from "@matrix-org/matrix-wysiwyg"; -import { FormattingButtons } - from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/FormattingButtons"; +import { FormattingButtons } from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/FormattingButtons"; -describe('FormattingButtons', () => { +describe("FormattingButtons", () => { const wysiwyg = { bold: jest.fn(), italic: jest.fn(), underline: jest.fn(), strikeThrough: jest.fn(), - } as any; + inlineCode: jest.fn(), + } as unknown as FormattingFunctions; - const formattingStates = { - bold: 'reversed', - italic: 'reversed', - underline: 'enabled', - strikeThrough: 'enabled', - } as any; + const actionStates = { + bold: "reversed", + italic: "reversed", + underline: "enabled", + strikeThrough: "enabled", + inlineCode: "enabled", + } as AllActionStates; afterEach(() => { jest.resetAllMocks(); }); - it('Should have the correspond CSS classes', () => { + it("Should have the correspond CSS classes", () => { // When - render(); + render(); // Then - expect(screen.getByLabelText('Bold')).toHaveClass('mx_FormattingButtons_active'); - expect(screen.getByLabelText('Italic')).toHaveClass('mx_FormattingButtons_active'); - expect(screen.getByLabelText('Underline')).not.toHaveClass('mx_FormattingButtons_active'); - expect(screen.getByLabelText('Strikethrough')).not.toHaveClass('mx_FormattingButtons_active'); + expect(screen.getByLabelText("Bold")).toHaveClass("mx_FormattingButtons_active"); + expect(screen.getByLabelText("Italic")).toHaveClass("mx_FormattingButtons_active"); + expect(screen.getByLabelText("Underline")).not.toHaveClass("mx_FormattingButtons_active"); + expect(screen.getByLabelText("Strikethrough")).not.toHaveClass("mx_FormattingButtons_active"); + expect(screen.getByLabelText("Code")).not.toHaveClass("mx_FormattingButtons_active"); }); - it('Should call wysiwyg function on button click', () => { + it("Should call wysiwyg function on button click", () => { // When - render(); - screen.getByLabelText('Bold').click(); - screen.getByLabelText('Italic').click(); - screen.getByLabelText('Underline').click(); - screen.getByLabelText('Strikethrough').click(); + render(); + screen.getByLabelText("Bold").click(); + screen.getByLabelText("Italic").click(); + screen.getByLabelText("Underline").click(); + screen.getByLabelText("Strikethrough").click(); + screen.getByLabelText("Code").click(); // Then expect(wysiwyg.bold).toHaveBeenCalledTimes(1); expect(wysiwyg.italic).toHaveBeenCalledTimes(1); expect(wysiwyg.underline).toHaveBeenCalledTimes(1); expect(wysiwyg.strikeThrough).toHaveBeenCalledTimes(1); + expect(wysiwyg.inlineCode).toHaveBeenCalledTimes(1); }); - it('Should display the tooltip on mouse over', async () => { + it("Should display the tooltip on mouse over", async () => { // When const user = userEvent.setup(); - render(); - await user.hover(screen.getByLabelText('Bold')); + render(); + await user.hover(screen.getByLabelText("Bold")); // Then - expect(await screen.findByText('Bold')).toBeTruthy(); + expect(await screen.findByText("Bold")).toBeTruthy(); + }); + + it("Should not have hover style when active", async () => { + // When + const user = userEvent.setup(); + render(); + await user.hover(screen.getByLabelText("Bold")); + + // Then + expect(screen.getByLabelText("Bold")).not.toHaveClass("mx_FormattingButtons_Button_hover"); + + // When + await user.hover(screen.getByLabelText("Underline")); + + // Then + expect(screen.getByLabelText("Underline")).toHaveClass("mx_FormattingButtons_Button_hover"); }); }); diff --git a/test/components/views/rooms/wysiwyg_composer/components/PlainTextComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/PlainTextComposer-test.tsx index 9c2e10100fe..bb41b18dc8b 100644 --- a/test/components/views/rooms/wysiwyg_composer/components/PlainTextComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/components/PlainTextComposer-test.tsx @@ -14,84 +14,89 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React from "react"; import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import { PlainTextComposer } - from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer"; +import { PlainTextComposer } from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer"; -describe('PlainTextComposer', () => { +describe("PlainTextComposer", () => { const customRender = ( onChange = (_content: string) => void 0, onSend = () => void 0, disabled = false, - initialContent?: string) => { + initialContent?: string, + ) => { return render( - , + , ); }; - it('Should have contentEditable at false when disabled', () => { + it("Should have contentEditable at false when disabled", () => { // When customRender(jest.fn(), jest.fn(), true); // Then - expect(screen.getByRole('textbox')).toHaveAttribute('contentEditable', "false"); + expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "false"); }); - it('Should have focus', () => { + it("Should have focus", () => { // When customRender(jest.fn(), jest.fn(), false); // Then - expect(screen.getByRole('textbox')).toHaveFocus(); + expect(screen.getByRole("textbox")).toHaveFocus(); }); - it('Should call onChange handler', async () => { + it("Should call onChange handler", async () => { // When - const content = 'content'; + const content = "content"; const onChange = jest.fn(); customRender(onChange, jest.fn()); - await userEvent.type(screen.getByRole('textbox'), content); + await userEvent.type(screen.getByRole("textbox"), content); // Then expect(onChange).toBeCalledWith(content); }); - it('Should call onSend when Enter is pressed', async () => { + it("Should call onSend when Enter is pressed", async () => { //When const onSend = jest.fn(); customRender(jest.fn(), onSend); - await userEvent.type(screen.getByRole('textbox'), '{enter}'); + await userEvent.type(screen.getByRole("textbox"), "{enter}"); // Then it sends a message expect(onSend).toBeCalledTimes(1); }); - it('Should clear textbox content when clear is called', async () => { + it("Should clear textbox content when clear is called", async () => { //When let composer; render( - { (ref, composerFunctions) => { + {(ref, composerFunctions) => { composer = composerFunctions; return null; - } } + }} , ); - await userEvent.type(screen.getByRole('textbox'), 'content'); - expect(screen.getByRole('textbox').innerHTML).toBe('content'); + await userEvent.type(screen.getByRole("textbox"), "content"); + expect(screen.getByRole("textbox").innerHTML).toBe("content"); composer.clear(); // Then - expect(screen.getByRole('textbox').innerHTML).toBeFalsy(); + expect(screen.getByRole("textbox").innerHTML).toBeFalsy(); }); - it('Should have data-is-expanded when it has two lines', async () => { + it("Should have data-is-expanded when it has two lines", async () => { let resizeHandler: ResizeObserverCallback = jest.fn(); let editor: Element | null = null; - jest.spyOn(global, 'ResizeObserver').mockImplementation((handler) => { + jest.spyOn(global, "ResizeObserver").mockImplementation((handler) => { resizeHandler = handler; return { observe: (element) => { @@ -100,21 +105,18 @@ describe('PlainTextComposer', () => { unobserve: jest.fn(), disconnect: jest.fn(), }; - }, - ); - jest.spyOn(global, 'requestAnimationFrame').mockImplementation(cb => { + }); + jest.spyOn(global, "requestAnimationFrame").mockImplementation((cb) => { cb(0); return 0; }); //When - render( - , - ); + render(); // Then - expect(screen.getByTestId('WysiwygComposerEditor').attributes['data-is-expanded'].value).toBe('false'); - expect(editor).toBe(screen.getByRole('textbox')); + expect(screen.getByTestId("WysiwygComposerEditor").attributes["data-is-expanded"].value).toBe("false"); + expect(editor).toBe(screen.getByRole("textbox")); // When resizeHandler( @@ -124,7 +126,7 @@ describe('PlainTextComposer', () => { jest.runAllTimers(); // Then - expect(screen.getByTestId('WysiwygComposerEditor').attributes['data-is-expanded'].value).toBe('true'); + expect(screen.getByTestId("WysiwygComposerEditor").attributes["data-is-expanded"].value).toBe("true"); (global.ResizeObserver as jest.Mock).mockRestore(); (global.requestAnimationFrame as jest.Mock).mockRestore(); diff --git a/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx index 64be2edfb36..43dce76c7f9 100644 --- a/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx @@ -16,119 +16,119 @@ limitations under the License. import "@testing-library/jest-dom"; import React from "react"; -import { render, screen } from "@testing-library/react"; -import { InputEventProcessor, Wysiwyg, WysiwygProps } from "@matrix-org/matrix-wysiwyg"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; -import { WysiwygComposer } - from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer"; +import { WysiwygComposer } from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer"; import SettingsStore from "../../../../../../src/settings/SettingsStore"; -let inputEventProcessor: InputEventProcessor | null = null; - -// The wysiwyg fetch wasm bytes and a specific workaround is needed to make it works in a node (jest) environnement -// See https://github.com/matrix-org/matrix-wysiwyg/blob/main/platforms/web/test.setup.ts -jest.mock("@matrix-org/matrix-wysiwyg", () => ({ - useWysiwyg: (props: WysiwygProps) => { - inputEventProcessor = props.inputEventProcessor ?? null; - return { - ref: { current: null }, - content: 'html', - isWysiwygReady: true, - wysiwyg: { clear: () => void 0 }, - formattingStates: { - bold: 'enabled', - italic: 'enabled', - underline: 'enabled', - strikeThrough: 'enabled', - }, - }; - }, -})); - -describe('WysiwygComposer', () => { +describe("WysiwygComposer", () => { const customRender = ( onChange = (_content: string) => void 0, onSend = () => void 0, disabled = false, - initialContent?: string) => { + initialContent?: string, + ) => { return render( , - ); }; - it('Should have contentEditable at false when disabled', () => { + it("Should have contentEditable at false when disabled", () => { // When customRender(jest.fn(), jest.fn(), true); // Then - expect(screen.getByRole('textbox')).toHaveAttribute('contentEditable', "false"); + expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "false"); }); - it('Should have focus', () => { - // When - customRender(jest.fn(), jest.fn(), false); + describe("Standard behavior", () => { + const onChange = jest.fn(); + const onSend = jest.fn(); + beforeEach(async () => { + customRender(onChange, onSend); + await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true")); + }); - // Then - expect(screen.getByRole('textbox')).toHaveFocus(); - }); + afterEach(() => { + onChange.mockReset(); + onSend.mockReset(); + }); - it('Should call onChange handler', (done) => { - const html = 'html'; - customRender((content) => { - expect(content).toBe((html)); - done(); - }, jest.fn()); - }); + it("Should have contentEditable at true", async () => { + // Then + await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true")); + }); - it('Should call onSend when Enter is pressed ', () => { - //When - const onSend = jest.fn(); - customRender(jest.fn(), onSend); + it("Should have focus", async () => { + // Then + await waitFor(() => expect(screen.getByRole("textbox")).toHaveFocus()); + }); - // When we tell its inputEventProcessor that the user pressed Enter - const event = new InputEvent("insertParagraph", { inputType: "insertParagraph" }); - const wysiwyg = { actions: { clear: () => {} } } as Wysiwyg; - inputEventProcessor(event, wysiwyg); + it("Should call onChange handler", async () => { + // When + fireEvent.input(screen.getByRole("textbox"), { + data: "foo bar", + inputType: "insertText", + }); + + // Then + await waitFor(() => expect(onChange).toBeCalledWith("foo bar")); + }); - // Then it sends a message - expect(onSend).toBeCalledTimes(1); + it("Should call onSend when Enter is pressed ", async () => { + //When + fireEvent( + screen.getByRole("textbox"), + new InputEvent("input", { + inputType: "insertParagraph", + }), + ); + + // Then it sends a message + await waitFor(() => expect(onSend).toBeCalledTimes(1)); + }); }); - describe('When settings require Ctrl+Enter to send', () => { - beforeEach(() => { + describe("When settings require Ctrl+Enter to send", () => { + const onChange = jest.fn(); + const onSend = jest.fn(); + beforeEach(async () => { jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => { if (name === "MessageComposerInput.ctrlEnterToSend") return true; }); + customRender(onChange, onSend); + await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true")); }); - it('Should not call onSend when Enter is pressed', async () => { - // Given a composer - const onSend = jest.fn(); - customRender(() => {}, onSend, false); + afterEach(() => { + onChange.mockReset(); + onSend.mockReset(); + }); - // When we tell its inputEventProcesser that the user pressed Enter - const event = new InputEvent("input", { inputType: "insertParagraph" }); - const wysiwyg = { actions: { clear: () => {} } } as Wysiwyg; - inputEventProcessor(event, wysiwyg); + it("Should not call onSend when Enter is pressed", async () => { + // When + fireEvent( + screen.getByRole("textbox"), + new InputEvent("input", { + inputType: "insertParagraph", + }), + ); // Then it does not send a message - expect(onSend).toBeCalledTimes(0); + await waitFor(() => expect(onSend).toBeCalledTimes(0)); }); - it('Should send a message when Ctrl+Enter is pressed', async () => { - // Given a composer - const onSend = jest.fn(); - customRender(() => {}, onSend, false); - - // When we tell its inputEventProcesser that the user pressed Ctrl+Enter - const event = new InputEvent("input", { inputType: "sendMessage" }); - const wysiwyg = { actions: { clear: () => {} } } as Wysiwyg; - inputEventProcessor(event, wysiwyg); + it("Should send a message when Ctrl+Enter is pressed", async () => { + // When + fireEvent( + screen.getByRole("textbox"), + new InputEvent("input", { + inputType: "sendMessage", + }), + ); // Then it sends a message - expect(onSend).toBeCalledTimes(1); + await waitFor(() => expect(onSend).toBeCalledTimes(1)); }); }); }); - diff --git a/test/components/views/rooms/wysiwyg_composer/utils/createMessageContent-test.ts b/test/components/views/rooms/wysiwyg_composer/utils/createMessageContent-test.ts index 4c7028749c4..e654186617b 100644 --- a/test/components/views/rooms/wysiwyg_composer/utils/createMessageContent-test.ts +++ b/test/components/views/rooms/wysiwyg_composer/utils/createMessageContent-test.ts @@ -16,21 +16,20 @@ limitations under the License. import { mkEvent } from "../../../../../test-utils"; import { RoomPermalinkCreator } from "../../../../../../src/utils/permalinks/Permalinks"; -import { createMessageContent } - from "../../../../../../src/components/views/rooms/wysiwyg_composer/utils/createMessageContent"; +import { createMessageContent } from "../../../../../../src/components/views/rooms/wysiwyg_composer/utils/createMessageContent"; -describe('createMessageContent', () => { +describe("createMessageContent", () => { const permalinkCreator = { forEvent(eventId: string): string { return "$$permalink$$"; }, } as RoomPermalinkCreator; - const message = 'hello world'; + const message = "hello world"; const mockEvent = mkEvent({ type: "m.room.message", - room: 'myfakeroom', - user: 'myfakeuser', - content: { "msgtype": "m.text", "body": "Replying to this" }, + room: "myfakeroom", + user: "myfakeuser", + content: { msgtype: "m.text", body: "Replying to this" }, event: true, }); @@ -44,14 +43,14 @@ describe('createMessageContent', () => { // Then expect(content).toEqual({ - "body": "hello world", - "format": "org.matrix.custom.html", - "formatted_body": message, - "msgtype": "m.text", + body: "hello world", + format: "org.matrix.custom.html", + formatted_body: message, + msgtype: "m.text", }); }); - it('Should add reply to message content', () => { + it("Should add reply to message content", () => { // When const content = createMessageContent(message, true, { permalinkCreator, replyToEvent: mockEvent }); @@ -59,13 +58,14 @@ describe('createMessageContent', () => { expect(content).toEqual({ "body": "> Replying to this\n\nhello world", "format": "org.matrix.custom.html", - "formatted_body": "
In reply to" + - " myfakeuser"+ - "
Replying to this
hello world", + "formatted_body": + '
In reply to' + + ' myfakeuser' + + "
Replying to this
hello world", "msgtype": "m.text", "m.relates_to": { "m.in_reply_to": { - "event_id": mockEvent.getId(), + event_id: mockEvent.getId(), }, }, }); @@ -86,31 +86,31 @@ describe('createMessageContent', () => { "formatted_body": message, "msgtype": "m.text", "m.relates_to": { - "event_id": "myFakeThreadId", - "rel_type": "m.thread", + event_id: "myFakeThreadId", + rel_type: "m.thread", }, }); }); - it('Should add fields related to edition', () => { + it("Should add fields related to edition", () => { // When const editedEvent = mkEvent({ type: "m.room.message", - room: 'myfakeroom', - user: 'myfakeuser2', + room: "myfakeroom", + user: "myfakeuser2", content: { "msgtype": "m.text", "body": "First message", "formatted_body": "First Message", "m.relates_to": { "m.in_reply_to": { - "event_id": 'eventId', + event_id: "eventId", }, - } }, + }, + }, event: true, }); - const content = - createMessageContent(message, true, { permalinkCreator, editedEvent }); + const content = createMessageContent(message, true, { permalinkCreator, editedEvent }); // Then expect(content).toEqual({ @@ -119,14 +119,14 @@ describe('createMessageContent', () => { "formatted_body": ` * ${message}`, "msgtype": "m.text", "m.new_content": { - "body": "hello world", - "format": "org.matrix.custom.html", - "formatted_body": message, - "msgtype": "m.text", + body: "hello world", + format: "org.matrix.custom.html", + formatted_body: message, + msgtype: "m.text", }, "m.relates_to": { - "event_id": editedEvent.getId(), - "rel_type": "m.replace", + event_id: editedEvent.getId(), + rel_type: "m.replace", }, }); }); diff --git a/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts b/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts index 0829b19adb2..ceb00ade79f 100644 --- a/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts +++ b/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts @@ -17,40 +17,38 @@ limitations under the License. import { EventStatus } from "matrix-js-sdk/src/matrix"; import { IRoomState } from "../../../../../../src/components/structures/RoomView"; -import { editMessage, sendMessage } - from "../../../../../../src/components/views/rooms/wysiwyg_composer/utils/message"; +import { editMessage, sendMessage } from "../../../../../../src/components/views/rooms/wysiwyg_composer/utils/message"; import { createTestClient, getRoomContext, mkEvent, mkStubRoom } from "../../../../../test-utils"; import defaultDispatcher from "../../../../../../src/dispatcher/dispatcher"; import SettingsStore from "../../../../../../src/settings/SettingsStore"; import { SettingLevel } from "../../../../../../src/settings/SettingLevel"; import { RoomPermalinkCreator } from "../../../../../../src/utils/permalinks/Permalinks"; import EditorStateTransfer from "../../../../../../src/utils/EditorStateTransfer"; -import * as ConfirmRedactDialog - from "../../../../../../src/components/views/dialogs/ConfirmRedactDialog"; +import * as ConfirmRedactDialog from "../../../../../../src/components/views/dialogs/ConfirmRedactDialog"; -describe('message', () => { +describe("message", () => { const permalinkCreator = { forEvent(eventId: string): string { return "$$permalink$$"; }, } as RoomPermalinkCreator; - const message = 'hello world'; + const message = "hello world"; const mockEvent = mkEvent({ type: "m.room.message", - room: 'myfakeroom', - user: 'myfakeuser', + room: "myfakeroom", + user: "myfakeuser", content: { - "msgtype": "m.text", - "body": "Replying to this", - "format": 'org.matrix.custom.html', - "formatted_body": 'Replying to this', + msgtype: "m.text", + body: "Replying to this", + format: "org.matrix.custom.html", + formatted_body: "Replying to this", }, event: true, }); const mockClient = createTestClient(); - const mockRoom = mkStubRoom('myfakeroom', 'myfakeroom', mockClient) as any; - mockRoom.findEventById = jest.fn(eventId => { + const mockRoom = mkStubRoom("myfakeroom", "myfakeroom", mockClient) as any; + mockRoom.findEventById = jest.fn((eventId) => { return eventId === mockEvent.getId() ? mockEvent : null; }); @@ -62,41 +60,41 @@ describe('message', () => { jest.resetAllMocks(); }); - describe('sendMessage', () => { - it('Should not send empty html message', async () => { + describe("sendMessage", () => { + it("Should not send empty html message", async () => { // When - await sendMessage('', true, { roomContext: defaultRoomContext, mxClient: mockClient, permalinkCreator }); + await sendMessage("", true, { roomContext: defaultRoomContext, mxClient: mockClient, permalinkCreator }); // Then expect(mockClient.sendMessage).toBeCalledTimes(0); expect(spyDispatcher).toBeCalledTimes(0); }); - it('Should send html message', async () => { + it("Should send html message", async () => { // When - await sendMessage( - message, - true, - { roomContext: defaultRoomContext, mxClient: mockClient, permalinkCreator }, - ); + await sendMessage(message, true, { + roomContext: defaultRoomContext, + mxClient: mockClient, + permalinkCreator, + }); // Then const expectedContent = { - "body": "hello world", - "format": "org.matrix.custom.html", - "formatted_body": "hello world", - "msgtype": "m.text", + body: "hello world", + format: "org.matrix.custom.html", + formatted_body: "hello world", + msgtype: "m.text", }; - expect(mockClient.sendMessage).toBeCalledWith('myfakeroom', null, expectedContent); - expect(spyDispatcher).toBeCalledWith({ action: 'message_sent' }); + expect(mockClient.sendMessage).toBeCalledWith("myfakeroom", null, expectedContent); + expect(spyDispatcher).toBeCalledWith({ action: "message_sent" }); }); - it('Should send reply to html message', async () => { + it("Should send reply to html message", async () => { const mockReplyEvent = mkEvent({ type: "m.room.message", - room: 'myfakeroom', - user: 'myfakeuser2', - content: { "msgtype": "m.text", "body": "My reply" }, + room: "myfakeroom", + user: "myfakeuser2", + content: { msgtype: "m.text", body: "My reply" }, event: true, }); @@ -110,7 +108,7 @@ describe('message', () => { // Then expect(spyDispatcher).toBeCalledWith({ - action: 'reply_to_event', + action: "reply_to_event", event: null, context: defaultRoomContext.timelineRenderingType, }); @@ -118,75 +116,71 @@ describe('message', () => { const expectedContent = { "body": "> My reply\n\nhello world", "format": "org.matrix.custom.html", - "formatted_body": "
In reply to" + - " myfakeuser2" + - "
My reply
hello world", + "formatted_body": + '
In reply to' + + ' myfakeuser2' + + "
My reply
hello world", "msgtype": "m.text", "m.relates_to": { "m.in_reply_to": { - "event_id": mockReplyEvent.getId(), + event_id: mockReplyEvent.getId(), }, }, }; - expect(mockClient.sendMessage).toBeCalledWith('myfakeroom', null, expectedContent); + expect(mockClient.sendMessage).toBeCalledWith("myfakeroom", null, expectedContent); }); - it('Should scroll to bottom after sending a html message', async () => { + it("Should scroll to bottom after sending a html message", async () => { // When SettingsStore.setValue("scrollToBottomOnMessageSent", null, SettingLevel.DEVICE, true); - await sendMessage( - message, - true, - { roomContext: defaultRoomContext, mxClient: mockClient, permalinkCreator }, - ); + await sendMessage(message, true, { + roomContext: defaultRoomContext, + mxClient: mockClient, + permalinkCreator, + }); // Then - expect(spyDispatcher).toBeCalledWith( - { action: 'scroll_to_bottom', timelineRenderingType: defaultRoomContext.timelineRenderingType }, - ); + expect(spyDispatcher).toBeCalledWith({ + action: "scroll_to_bottom", + timelineRenderingType: defaultRoomContext.timelineRenderingType, + }); }); - it('Should handle emojis', async () => { + it("Should handle emojis", async () => { // When - await sendMessage( - '🎉', - false, - { roomContext: defaultRoomContext, mxClient: mockClient, permalinkCreator }, - ); + await sendMessage("🎉", false, { roomContext: defaultRoomContext, mxClient: mockClient, permalinkCreator }); // Then - expect(spyDispatcher).toBeCalledWith( - { action: 'effects.confetti' }, - ); + expect(spyDispatcher).toBeCalledWith({ action: "effects.confetti" }); }); }); - describe('editMessage', () => { + describe("editMessage", () => { const editorStateTransfer = new EditorStateTransfer(mockEvent); - it('Should cancel editing and ask for event removal when message is empty', async () => { + it("Should cancel editing and ask for event removal when message is empty", async () => { // When - const mockCreateRedactEventDialog = jest.spyOn(ConfirmRedactDialog, 'createRedactEventDialog'); + const mockCreateRedactEventDialog = jest.spyOn(ConfirmRedactDialog, "createRedactEventDialog"); const mockEvent = mkEvent({ type: "m.room.message", - room: 'myfakeroom', - user: 'myfakeuser', - content: { "msgtype": "m.text", "body": "Replying to this" }, + room: "myfakeroom", + user: "myfakeuser", + content: { msgtype: "m.text", body: "Replying to this" }, event: true, }); const replacingEvent = mkEvent({ type: "m.room.message", - room: 'myfakeroom', - user: 'myfakeuser', - content: { "msgtype": "m.text", "body": "ReplacingEvent" }, + room: "myfakeroom", + user: "myfakeuser", + content: { msgtype: "m.text", body: "ReplacingEvent" }, event: true, }); replacingEvent.setStatus(EventStatus.QUEUED); mockEvent.makeReplaced(replacingEvent); const editorStateTransfer = new EditorStateTransfer(mockEvent); - await editMessage('', { roomContext: defaultRoomContext, mxClient: mockClient, editorStateTransfer }); + await editMessage("", { roomContext: defaultRoomContext, mxClient: mockClient, editorStateTransfer }); // Then expect(mockClient.sendMessage).toBeCalledTimes(0); @@ -195,22 +189,26 @@ describe('message', () => { expect(spyDispatcher).toBeCalledTimes(0); }); - it('Should do nothing if the content is unmodified', async () => { + it("Should do nothing if the content is unmodified", async () => { // When - await editMessage( - mockEvent.getContent().body, - { roomContext: defaultRoomContext, mxClient: mockClient, editorStateTransfer }); + await editMessage(mockEvent.getContent().body, { + roomContext: defaultRoomContext, + mxClient: mockClient, + editorStateTransfer, + }); // Then expect(mockClient.sendMessage).toBeCalledTimes(0); }); - it('Should send a message when the content is modified', async () => { + it("Should send a message when the content is modified", async () => { // When const newMessage = `${mockEvent.getContent().body} new content`; - await editMessage( - newMessage, - { roomContext: defaultRoomContext, mxClient: mockClient, editorStateTransfer }); + await editMessage(newMessage, { + roomContext: defaultRoomContext, + mxClient: mockClient, + editorStateTransfer, + }); // Then const { msgtype, format } = mockEvent.getContent(); @@ -218,20 +216,20 @@ describe('message', () => { "body": ` * ${newMessage}`, "formatted_body": ` * ${newMessage}`, "m.new_content": { - "body": "Replying to this new content", - "format": "org.matrix.custom.html", - "formatted_body": "Replying to this new content", - "msgtype": "m.text", + body: "Replying to this new content", + format: "org.matrix.custom.html", + formatted_body: "Replying to this new content", + msgtype: "m.text", }, "m.relates_to": { - "event_id": mockEvent.getId(), - "rel_type": "m.replace", + event_id: mockEvent.getId(), + rel_type: "m.replace", }, msgtype, format, }; expect(mockClient.sendMessage).toBeCalledWith(mockEvent.getRoomId(), null, expectedContent); - expect(spyDispatcher).toBeCalledWith({ action: 'message_sent' }); + expect(spyDispatcher).toBeCalledWith({ action: "message_sent" }); }); }); }); diff --git a/test/components/views/settings/AddPrivilegedUsers-test.tsx b/test/components/views/settings/AddPrivilegedUsers-test.tsx new file mode 100644 index 00000000000..68acb7395db --- /dev/null +++ b/test/components/views/settings/AddPrivilegedUsers-test.tsx @@ -0,0 +1,147 @@ +/* +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 { act, fireEvent, render, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { mocked } from "jest-mock"; +import { RoomMember, EventType } from "matrix-js-sdk/src/matrix"; + +import { getMockClientWithEventEmitter, makeRoomWithStateEvents, mkEvent } from "../../../test-utils"; +import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; +import { + AddPrivilegedUsers, + getUserIdsFromCompletions, + hasLowerOrEqualLevelThanDefaultLevel, +} from "../../../../src/components/views/settings/AddPrivilegedUsers"; +import UserProvider from "../../../../src/autocomplete/UserProvider"; +import { ICompletion } from "../../../../src/autocomplete/Autocompleter"; + +jest.mock("../../../../src/autocomplete/UserProvider"); + +const completions: ICompletion[] = [ + { type: "user", completion: "user_1", completionId: "@user_1:host.local", range: { start: 1, end: 1 } }, + { type: "user", completion: "user_2", completionId: "@user_2:host.local", range: { start: 1, end: 1 } }, + { type: "user", completion: "user_without_completion_id", range: { start: 1, end: 1 } }, +]; + +describe("", () => { + const provider = mocked(UserProvider, { shallow: true }); + provider.prototype.getCompletions.mockResolvedValue(completions); + + const mockClient = getMockClientWithEventEmitter({ + // `makeRoomWithStateEvents` only work's if `getRoom` is present. + getRoom: jest.fn(), + setPowerLevel: jest.fn(), + }); + + const room = makeRoomWithStateEvents([], { roomId: "room_id", mockClient: mockClient }); + room.getMember = (userId: string) => { + const member = new RoomMember("room_id", userId); + member.powerLevel = 0; + + return member; + }; + (room.currentState.getStateEvents as unknown) = (_eventType: string, _stateKey: string) => { + return mkEvent({ + type: EventType.RoomPowerLevels, + content: {}, + user: "user_id", + }); + }; + + const getComponent = () => ( + + + + ); + + it("checks whether form submit works as intended", async () => { + const { getByTestId, queryAllByTestId } = render(getComponent()); + + // Verify that the submit button is disabled initially. + const submitButton = getByTestId("add-privileged-users-submit-button"); + expect(submitButton).toBeDisabled(); + + // Find some suggestions and select them. + const autocompleteInput = getByTestId("autocomplete-input"); + + act(() => { + fireEvent.focus(autocompleteInput); + fireEvent.change(autocompleteInput, { target: { value: "u" } }); + }); + + await waitFor(() => expect(provider.mock.instances[0].getCompletions).toHaveBeenCalledTimes(1)); + const matchOne = getByTestId("autocomplete-suggestion-item-@user_1:host.local"); + const matchTwo = getByTestId("autocomplete-suggestion-item-@user_2:host.local"); + + act(() => { + fireEvent.mouseDown(matchOne); + }); + + act(() => { + fireEvent.mouseDown(matchTwo); + }); + + // Check that `defaultUserLevel` is initially set and select a higher power level. + expect((getByTestId("power-level-option-0") as HTMLOptionElement).selected).toBeTruthy(); + expect((getByTestId("power-level-option-50") as HTMLOptionElement).selected).toBeFalsy(); + expect((getByTestId("power-level-option-100") as HTMLOptionElement).selected).toBeFalsy(); + + const powerLevelSelect = getByTestId("power-level-select-element"); + await userEvent.selectOptions(powerLevelSelect, "100"); + + expect((getByTestId("power-level-option-0") as HTMLOptionElement).selected).toBeFalsy(); + expect((getByTestId("power-level-option-50") as HTMLOptionElement).selected).toBeFalsy(); + expect((getByTestId("power-level-option-100") as HTMLOptionElement).selected).toBeTruthy(); + + // The submit button should be enabled now. + expect(submitButton).toBeEnabled(); + + // Submit the form. + act(() => { + fireEvent.submit(submitButton); + }); + + await waitFor(() => expect(mockClient.setPowerLevel).toHaveBeenCalledTimes(1)); + + // Verify that the submit button is disabled again. + expect(submitButton).toBeDisabled(); + + // Verify that previously selected items are reset. + const selectionItems = queryAllByTestId("autocomplete-selection-item", { exact: false }); + expect(selectionItems).toHaveLength(0); + + // Verify that power level select is reset to `defaultUserLevel`. + expect((getByTestId("power-level-option-0") as HTMLOptionElement).selected).toBeTruthy(); + expect((getByTestId("power-level-option-50") as HTMLOptionElement).selected).toBeFalsy(); + expect((getByTestId("power-level-option-100") as HTMLOptionElement).selected).toBeFalsy(); + }); + + it("getUserIdsFromCompletions() should map completions to user id's", () => { + expect(getUserIdsFromCompletions(completions)).toStrictEqual(["@user_1:host.local", "@user_2:host.local"]); + }); + + it.each([ + { defaultUserLevel: -50, expectation: false }, + { defaultUserLevel: 0, expectation: true }, + { defaultUserLevel: 50, expectation: true }, + ])( + "hasLowerOrEqualLevelThanDefaultLevel() should return $expectation for default level $defaultUserLevel", + ({ defaultUserLevel, expectation }) => { + expect(hasLowerOrEqualLevelThanDefaultLevel(room, completions[0], defaultUserLevel)).toBe(expectation); + }, + ); +}); diff --git a/test/components/views/settings/CryptographyPanel-test.tsx b/test/components/views/settings/CryptographyPanel-test.tsx index c46aa09f5a9..7455b2adc57 100644 --- a/test/components/views/settings/CryptographyPanel-test.tsx +++ b/test/components/views/settings/CryptographyPanel-test.tsx @@ -14,16 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ReactElement } from 'react'; -import ReactDOM from 'react-dom'; -import { MatrixClient } from 'matrix-js-sdk/src/matrix'; +import React, { ReactElement } from "react"; +import ReactDOM from "react-dom"; +import { MatrixClient } from "matrix-js-sdk/src/matrix"; -import { MatrixClientPeg } from '../../../../src/MatrixClientPeg'; -import * as TestUtils from '../../../test-utils'; -import CryptographyPanel from '../../../../src/components/views/settings/CryptographyPanel'; +import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; +import * as TestUtils from "../../../test-utils"; +import CryptographyPanel from "../../../../src/components/views/settings/CryptographyPanel"; -describe('CryptographyPanel', () => { - it('shows the session ID and key', () => { +describe("CryptographyPanel", () => { + it("shows the session ID and key", () => { const sessionId = "ABCDEFGHIJ"; const sessionKey = "AbCDeFghIJK7L/m4nOPqRSTUVW4xyzaBCDef6gHIJkl"; const sessionKeyFormatted = "AbCD eFgh IJK7 L/m4 nOPq RSTU VW4x yzaB CDef 6gHI Jkl"; @@ -45,7 +45,7 @@ describe('CryptographyPanel', () => { }); function render(component: ReactElement): HTMLDivElement { - const parentDiv = document.createElement('div'); + const parentDiv = document.createElement("div"); document.body.appendChild(parentDiv); ReactDOM.render(component, parentDiv); return parentDiv; diff --git a/test/components/views/settings/DevicesPanel-test.tsx b/test/components/views/settings/DevicesPanel-test.tsx index 81f6fb328a6..3565e59c439 100644 --- a/test/components/views/settings/DevicesPanel-test.tsx +++ b/test/components/views/settings/DevicesPanel-test.tsx @@ -13,81 +13,76 @@ 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 { fireEvent, render } from '@testing-library/react'; -import { act } from 'react-dom/test-utils'; -import { CrossSigningInfo } from 'matrix-js-sdk/src/crypto/CrossSigning'; -import { DeviceInfo } from 'matrix-js-sdk/src/crypto/deviceinfo'; -import { sleep } from 'matrix-js-sdk/src/utils'; -import { PUSHER_DEVICE_ID, PUSHER_ENABLED } from 'matrix-js-sdk/src/@types/event'; +import React from "react"; +import { fireEvent, render } from "@testing-library/react"; +import { act } from "react-dom/test-utils"; +import { CrossSigningInfo } from "matrix-js-sdk/src/crypto/CrossSigning"; +import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo"; +import { sleep } from "matrix-js-sdk/src/utils"; +import { PUSHER_DEVICE_ID, PUSHER_ENABLED } from "matrix-js-sdk/src/@types/event"; import DevicesPanel from "../../../../src/components/views/settings/DevicesPanel"; -import { - flushPromises, - getMockClientWithEventEmitter, - mkPusher, - mockClientMethodsUser, -} from "../../../test-utils"; -import MatrixClientContext from '../../../../src/contexts/MatrixClientContext'; - -describe('', () => { - const userId = '@alice:server.org'; - const device1 = { device_id: 'device_1' }; - const device2 = { device_id: 'device_2' }; - const device3 = { device_id: 'device_3' }; +import { flushPromises, getMockClientWithEventEmitter, mkPusher, mockClientMethodsUser } from "../../../test-utils"; +import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; + +describe("", () => { + const userId = "@alice:server.org"; + const device1 = { device_id: "device_1" }; + const device2 = { device_id: "device_2" }; + const device3 = { device_id: "device_3" }; const mockClient = getMockClientWithEventEmitter({ ...mockClientMethodsUser(userId), getDevices: jest.fn(), getDeviceId: jest.fn().mockReturnValue(device1.device_id), deleteMultipleDevices: jest.fn(), getStoredCrossSigningForUser: jest.fn().mockReturnValue(new CrossSigningInfo(userId, {}, {})), - getStoredDevice: jest.fn().mockReturnValue(new DeviceInfo('id')), + getStoredDevice: jest.fn().mockReturnValue(new DeviceInfo("id")), generateClientSecret: jest.fn(), getPushers: jest.fn(), setPusher: jest.fn(), }); - const getComponent = () => + const getComponent = () => ( - ; + + ); beforeEach(() => { jest.clearAllMocks(); - mockClient.getDevices - .mockReset() - .mockResolvedValue({ devices: [device1, device2, device3] }); + mockClient.getDevices.mockReset().mockResolvedValue({ devices: [device1, device2, device3] }); - mockClient.getPushers - .mockReset() - .mockResolvedValue({ - pushers: [mkPusher({ + mockClient.getPushers.mockReset().mockResolvedValue({ + pushers: [ + mkPusher({ [PUSHER_DEVICE_ID.name]: device1.device_id, [PUSHER_ENABLED.name]: true, - })], - }); + }), + ], + }); }); - it('renders device panel with devices', async () => { + it("renders device panel with devices", async () => { const { container } = render(getComponent()); await flushPromises(); expect(container).toMatchSnapshot(); }); - describe('device deletion', () => { + describe("device deletion", () => { const interactiveAuthError = { httpStatus: 401, data: { flows: [{ stages: ["m.login.password"] }] } }; - const toggleDeviceSelection = (container: HTMLElement, deviceId: string) => act(() => { - const checkbox = container.querySelector(`#device-tile-checkbox-${deviceId}`); - fireEvent.click(checkbox); - }); + const toggleDeviceSelection = (container: HTMLElement, deviceId: string) => + act(() => { + const checkbox = container.querySelector(`#device-tile-checkbox-${deviceId}`); + fireEvent.click(checkbox); + }); beforeEach(() => { mockClient.deleteMultipleDevices.mockReset(); }); - it('deletes selected devices when interactive auth is not required', async () => { + it("deletes selected devices when interactive auth is not required", async () => { mockClient.deleteMultipleDevices.mockResolvedValue({}); mockClient.getDevices .mockResolvedValueOnce({ devices: [device1, device2, device3] }) @@ -97,17 +92,17 @@ describe('', () => { const { container, getByTestId } = render(getComponent()); await flushPromises(); - expect(container.getElementsByClassName('mx_DevicesPanel_device').length).toEqual(3); + expect(container.getElementsByClassName("mx_DevicesPanel_device").length).toEqual(3); toggleDeviceSelection(container, device2.device_id); mockClient.getDevices.mockClear(); act(() => { - fireEvent.click(getByTestId('sign-out-devices-btn')); + fireEvent.click(getByTestId("sign-out-devices-btn")); }); - expect(container.getElementsByClassName('mx_Spinner').length).toBeTruthy(); + expect(container.getElementsByClassName("mx_Spinner").length).toBeTruthy(); expect(mockClient.deleteMultipleDevices).toHaveBeenCalledWith([device2.device_id], undefined); await flushPromises(); @@ -115,10 +110,10 @@ describe('', () => { // devices refreshed expect(mockClient.getDevices).toHaveBeenCalled(); // and rerendered - expect(container.getElementsByClassName('mx_DevicesPanel_device').length).toEqual(2); + expect(container.getElementsByClassName("mx_DevicesPanel_device").length).toEqual(2); }); - it('deletes selected devices when interactive auth is required', async () => { + it("deletes selected devices when interactive auth is required", async () => { mockClient.deleteMultipleDevices // require auth .mockRejectedValueOnce(interactiveAuthError) @@ -140,7 +135,7 @@ describe('', () => { toggleDeviceSelection(container, device2.device_id); act(() => { - fireEvent.click(getByTestId('sign-out-devices-btn')); + fireEvent.click(getByTestId("sign-out-devices-btn")); }); await flushPromises(); @@ -149,30 +144,34 @@ describe('', () => { expect(mockClient.deleteMultipleDevices).toHaveBeenCalledWith([device2.device_id], undefined); - const modal = document.getElementsByClassName('mx_Dialog'); + const modal = document.getElementsByClassName("mx_Dialog"); expect(modal).toMatchSnapshot(); // fill password and submit for interactive auth act(() => { - fireEvent.change(getByLabelText('Password'), { target: { value: 'topsecret' } }); - fireEvent.submit(getByLabelText('Password')); + fireEvent.change(getByLabelText("Password"), { target: { value: "topsecret" } }); + fireEvent.submit(getByLabelText("Password")); }); await flushPromises(); // called again with auth - expect(mockClient.deleteMultipleDevices).toHaveBeenCalledWith([device2.device_id], - { identifier: { - type: "m.id.user", user: userId, - }, password: "", type: "m.login.password", user: userId, - }); + expect(mockClient.deleteMultipleDevices).toHaveBeenCalledWith([device2.device_id], { + identifier: { + type: "m.id.user", + user: userId, + }, + password: "", + type: "m.login.password", + user: userId, + }); // devices refreshed expect(mockClient.getDevices).toHaveBeenCalled(); // and rerendered - expect(container.getElementsByClassName('mx_DevicesPanel_device').length).toEqual(2); + expect(container.getElementsByClassName("mx_DevicesPanel_device").length).toEqual(2); }); - it('clears loading state when interactive auth fail is cancelled', async () => { + it("clears loading state when interactive auth fail is cancelled", async () => { mockClient.deleteMultipleDevices // require auth .mockRejectedValueOnce(interactiveAuthError) @@ -194,10 +193,10 @@ describe('', () => { toggleDeviceSelection(container, device2.device_id); act(() => { - fireEvent.click(getByTestId('sign-out-devices-btn')); + fireEvent.click(getByTestId("sign-out-devices-btn")); }); - expect(container.getElementsByClassName('mx_Spinner').length).toBeTruthy(); + expect(container.getElementsByClassName("mx_Spinner").length).toBeTruthy(); await flushPromises(); // modal rendering has some weird sleeps @@ -214,7 +213,7 @@ describe('', () => { // not refreshed expect(mockClient.getDevices).not.toHaveBeenCalled(); // spinner removed - expect(container.getElementsByClassName('mx_Spinner').length).toBeFalsy(); + expect(container.getElementsByClassName("mx_Spinner").length).toBeFalsy(); }); }); }); diff --git a/test/components/views/settings/FontScalingPanel-test.tsx b/test/components/views/settings/FontScalingPanel-test.tsx index 119a26ebc15..d52dacee54c 100644 --- a/test/components/views/settings/FontScalingPanel-test.tsx +++ b/test/components/views/settings/FontScalingPanel-test.tsx @@ -14,29 +14,23 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -// eslint-disable-next-line deprecate/import -import { mount } from "enzyme"; +import React from "react"; +import { render } from "@testing-library/react"; import * as TestUtils from "../../../test-utils"; -import FontScalingPanel from '../../../../src/components/views/settings/FontScalingPanel'; +import FontScalingPanel from "../../../../src/components/views/settings/FontScalingPanel"; // Fake random strings to give a predictable snapshot -jest.mock( - 'matrix-js-sdk/src/randomstring', - () => { - return { - randomString: () => "abdefghi", - }; - }, -); +jest.mock("matrix-js-sdk/src/randomstring", () => { + return { + randomString: () => "abdefghi", + }; +}); -describe('FontScalingPanel', () => { - it('renders the font scaling UI', () => { +describe("FontScalingPanel", () => { + it("renders the font scaling UI", () => { TestUtils.stubClient(); - const wrapper = mount( - , - ); - expect(wrapper).toMatchSnapshot(); + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); }); }); diff --git a/test/components/views/settings/KeyboardShortcut-test.tsx b/test/components/views/settings/KeyboardShortcut-test.tsx index d26c0dd1e98..b12fc514c40 100644 --- a/test/components/views/settings/KeyboardShortcut-test.tsx +++ b/test/components/views/settings/KeyboardShortcut-test.tsx @@ -1,4 +1,3 @@ - /* Copyright 2022 Šimon Brandner diff --git a/test/components/views/settings/Notifications-test.tsx b/test/components/views/settings/Notifications-test.tsx index 88deaa2c0f6..6ee844d4e0f 100644 --- a/test/components/views/settings/Notifications-test.tsx +++ b/test/components/views/settings/Notifications-test.tsx @@ -12,9 +12,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -// eslint-disable-next-line deprecate/import -import { mount, ReactWrapper } from 'enzyme'; +import React from "react"; import { IPushRule, IPushRules, @@ -22,14 +20,17 @@ import { IPusher, LOCAL_NOTIFICATION_SETTINGS_PREFIX, MatrixEvent, -} from 'matrix-js-sdk/src/matrix'; -import { IThreepid, ThreepidMedium } from 'matrix-js-sdk/src/@types/threepids'; -import { act } from 'react-dom/test-utils'; - -import Notifications from '../../../../src/components/views/settings/Notifications'; + Room, + NotificationCountType, +} from "matrix-js-sdk/src/matrix"; +import { IThreepid, ThreepidMedium } from "matrix-js-sdk/src/@types/threepids"; +import { act } from "react-dom/test-utils"; +import { fireEvent, getByTestId, render, screen, waitFor } from "@testing-library/react"; + +import Notifications from "../../../../src/components/views/settings/Notifications"; import SettingsStore from "../../../../src/settings/SettingsStore"; -import { StandardActions } from '../../../../src/notifications/StandardActions'; -import { getMockClientWithEventEmitter } from '../../../test-utils'; +import { StandardActions } from "../../../../src/notifications/StandardActions"; +import { getMockClientWithEventEmitter, mkMessage } from "../../../test-utils"; // don't pollute test output with error logs from mock rejections jest.mock("matrix-js-sdk/src/logger"); @@ -45,24 +46,161 @@ const masterRule = { rule_id: RuleId.Master, }; // eslint-disable-next-line max-len -const oneToOneRule = { "conditions": [{ "kind": "room_member_count", "is": "2" }, { "kind": "event_match", "key": "type", "pattern": "m.room.message" }], "actions": ["notify", { "set_tweak": "highlight", "value": false }], "rule_id": ".m.rule.room_one_to_one", "default": true, "enabled": true } as IPushRule; +const oneToOneRule = { + conditions: [ + { kind: "room_member_count", is: "2" }, + { kind: "event_match", key: "type", pattern: "m.room.message" }, + ], + actions: ["notify", { set_tweak: "highlight", value: false }], + rule_id: ".m.rule.room_one_to_one", + default: true, + enabled: true, +} as IPushRule; // eslint-disable-next-line max-len -const encryptedOneToOneRule = { "conditions": [{ "kind": "room_member_count", "is": "2" }, { "kind": "event_match", "key": "type", "pattern": "m.room.encrypted" }], "actions": ["notify", { "set_tweak": "sound", "value": "default" }, { "set_tweak": "highlight", "value": false }], "rule_id": ".m.rule.encrypted_room_one_to_one", "default": true, "enabled": true } as IPushRule; +const encryptedOneToOneRule = { + conditions: [ + { kind: "room_member_count", is: "2" }, + { kind: "event_match", key: "type", pattern: "m.room.encrypted" }, + ], + actions: ["notify", { set_tweak: "sound", value: "default" }, { set_tweak: "highlight", value: false }], + rule_id: ".m.rule.encrypted_room_one_to_one", + default: true, + enabled: true, +} as IPushRule; // eslint-disable-next-line max-len -const encryptedGroupRule = { "conditions": [{ "kind": "event_match", "key": "type", "pattern": "m.room.encrypted" }], "actions": ["dont_notify"], "rule_id": ".m.rule.encrypted", "default": true, "enabled": true } as IPushRule; +const encryptedGroupRule = { + conditions: [{ kind: "event_match", key: "type", pattern: "m.room.encrypted" }], + actions: ["dont_notify"], + rule_id: ".m.rule.encrypted", + default: true, + enabled: true, +} as IPushRule; // 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)); - -describe('', () => { - const getComponent = () => mount(); +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) => window.setTimeout(resolve)); + +describe("", () => { + const getComponent = () => render(); // get component, wait for async data and force a render const getComponentAndWait = async () => { const component = getComponent(); await flushPromises(); - component.setProps({}); return component; }; @@ -74,7 +212,7 @@ describe('', () => { setPushRuleEnabled: jest.fn(), setPushRuleActions: jest.fn(), getRooms: jest.fn().mockReturnValue([]), - getAccountData: jest.fn().mockImplementation(eventType => { + getAccountData: jest.fn().mockImplementation((eventType) => { if (eventType.startsWith(LOCAL_NOTIFICATION_SETTINGS_PREFIX.name)) { return new MatrixEvent({ type: eventType, @@ -85,11 +223,11 @@ describe('', () => { } }), setAccountData: jest.fn(), + sendReadReceipt: jest.fn(), + supportsExperimentalThreads: jest.fn().mockReturnValue(true), }); mockClient.getPushRules.mockResolvedValue(pushRules); - const findByTestId = (component, id) => component.find(`[data-test-id="${id}"]`); - beforeEach(() => { mockClient.getPushRules.mockClear().mockResolvedValue(pushRules); mockClient.getPushers.mockClear().mockResolvedValue({ pushers: [] }); @@ -97,29 +235,29 @@ describe('', () => { mockClient.setPusher.mockClear().mockResolvedValue({}); }); - it('renders spinner while loading', () => { - const component = getComponent(); - expect(component.find('.mx_Spinner').length).toBeTruthy(); + it("renders spinner while loading", async () => { + getComponent(); + expect(screen.getByTestId("spinner")).toBeInTheDocument(); }); - it('renders error message when fetching push rules fails', async () => { + it("renders error message when fetching push rules fails", async () => { mockClient.getPushRules.mockRejectedValue({}); - const component = await getComponentAndWait(); - expect(findByTestId(component, 'error-message').length).toBeTruthy(); + await getComponentAndWait(); + expect(screen.getByTestId("error-message")).toBeInTheDocument(); }); - it('renders error message when fetching pushers fails', async () => { + it("renders error message when fetching pushers fails", async () => { mockClient.getPushers.mockRejectedValue({}); - const component = await getComponentAndWait(); - expect(findByTestId(component, 'error-message').length).toBeTruthy(); + await getComponentAndWait(); + expect(screen.getByTestId("error-message")).toBeInTheDocument(); }); - it('renders error message when fetching threepids fails', async () => { + it("renders error message when fetching threepids fails", async () => { mockClient.getThreePids.mockRejectedValue({}); - const component = await getComponentAndWait(); - expect(findByTestId(component, 'error-message').length).toBeTruthy(); + await getComponentAndWait(); + expect(screen.getByTestId("error-message")).toBeInTheDocument(); }); - describe('main notification switches', () => { - it('renders only enable notifications switch when notifications are disabled', async () => { + describe("main notification switches", () => { + it("renders only enable notifications switch when notifications are disabled", async () => { const disableNotificationsPushRules = { global: { ...pushRules.global, @@ -127,22 +265,22 @@ describe('', () => { }, } as unknown as IPushRules; mockClient.getPushRules.mockClear().mockResolvedValue(disableNotificationsPushRules); - const component = await getComponentAndWait(); + const { container } = await getComponentAndWait(); - expect(component).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); - it('renders switches correctly', async () => { - const component = await getComponentAndWait(); - - expect(findByTestId(component, 'notif-master-switch').length).toBeTruthy(); - expect(findByTestId(component, 'notif-device-switch').length).toBeTruthy(); - expect(findByTestId(component, 'notif-setting-notificationsEnabled').length).toBeTruthy(); - expect(findByTestId(component, 'notif-setting-notificationBodyEnabled').length).toBeTruthy(); - expect(findByTestId(component, 'notif-setting-audioNotificationsEnabled').length).toBeTruthy(); + it("renders switches correctly", async () => { + await getComponentAndWait(); + + expect(screen.getByTestId("notif-master-switch")).toBeInTheDocument(); + expect(screen.getByTestId("notif-device-switch")).toBeInTheDocument(); + expect(screen.getByTestId("notif-setting-notificationsEnabled")).toBeInTheDocument(); + expect(screen.getByTestId("notif-setting-notificationBodyEnabled")).toBeInTheDocument(); + expect(screen.getByTestId("notif-setting-audioNotificationsEnabled")).toBeInTheDocument(); }); - describe('email switches', () => { - const testEmail = 'tester@test.com'; + describe("email switches", () => { + const testEmail = "tester@test.com"; beforeEach(() => { mockClient.getThreePids.mockResolvedValue({ threepids: [ @@ -155,149 +293,182 @@ describe('', () => { }); }); - it('renders email switches correctly when email 3pids exist', async () => { - const component = await getComponentAndWait(); - - expect(findByTestId(component, 'notif-email-switch')).toMatchSnapshot(); + it("renders email switches correctly when email 3pids exist", async () => { + await getComponentAndWait(); + expect(screen.getByTestId("notif-email-switch")).toBeInTheDocument(); }); - it('renders email switches correctly when notifications are on for email', async () => { + it("renders email switches correctly when notifications are on for email", async () => { mockClient.getPushers.mockResolvedValue({ - pushers: [ - { kind: 'email', pushkey: testEmail } as unknown as IPusher, - ], + pushers: [{ kind: "email", pushkey: testEmail } as unknown as IPusher], }); - const component = await getComponentAndWait(); + await getComponentAndWait(); - expect(findByTestId(component, 'notif-email-switch').props().value).toEqual(true); + const emailSwitch = screen.getByTestId("notif-email-switch"); + expect(emailSwitch.querySelector('[aria-checked="true"]')).toBeInTheDocument(); }); - it('enables email notification when toggling on', async () => { - const component = await getComponentAndWait(); + it("enables email notification when toggling on", async () => { + await getComponentAndWait(); - const emailToggle = findByTestId(component, 'notif-email-switch') - .find('div[role="switch"]'); + const emailToggle = screen.getByTestId("notif-email-switch").querySelector('div[role="switch"]'); await act(async () => { - emailToggle.simulate('click'); + fireEvent.click(emailToggle); }); - expect(mockClient.setPusher).toHaveBeenCalledWith(expect.objectContaining({ - kind: "email", - app_id: "m.email", - pushkey: testEmail, - app_display_name: "Email Notifications", - device_display_name: testEmail, - append: true, - })); + expect(mockClient.setPusher).toHaveBeenCalledWith( + expect.objectContaining({ + kind: "email", + app_id: "m.email", + pushkey: testEmail, + app_display_name: "Email Notifications", + device_display_name: testEmail, + append: true, + }), + ); }); - it('displays error when pusher update fails', async () => { + it("displays error when pusher update fails", async () => { mockClient.setPusher.mockRejectedValue({}); - const component = await getComponentAndWait(); + await getComponentAndWait(); - const emailToggle = findByTestId(component, 'notif-email-switch') - .find('div[role="switch"]'); + const emailToggle = screen.getByTestId("notif-email-switch").querySelector('div[role="switch"]'); await act(async () => { - emailToggle.simulate('click'); + fireEvent.click(emailToggle); }); // force render await flushPromises(); - await component.setProps({}); - expect(findByTestId(component, 'error-message').length).toBeTruthy(); + expect(screen.getByTestId("error-message")).toBeInTheDocument(); }); - it('enables email notification when toggling off', async () => { - const testPusher = { kind: 'email', pushkey: 'tester@test.com' } as unknown as IPusher; + it("enables email notification when toggling off", async () => { + const testPusher = { kind: "email", pushkey: "tester@test.com" } as unknown as IPusher; mockClient.getPushers.mockResolvedValue({ pushers: [testPusher] }); - const component = await getComponentAndWait(); + await getComponentAndWait(); - const emailToggle = findByTestId(component, 'notif-email-switch') - .find('div[role="switch"]'); + const emailToggle = screen.getByTestId("notif-email-switch").querySelector('div[role="switch"]'); await act(async () => { - emailToggle.simulate('click'); + fireEvent.click(emailToggle); }); expect(mockClient.setPusher).toHaveBeenCalledWith({ - ...testPusher, kind: null, + ...testPusher, + kind: null, }); }); }); - it('toggles and sets settings correctly', async () => { - const component = await getComponentAndWait(); - let audioNotifsToggle: ReactWrapper; + it("toggles and sets settings correctly", async () => { + await getComponentAndWait(); + let audioNotifsToggle; const update = () => { - audioNotifsToggle = findByTestId(component, 'notif-setting-audioNotificationsEnabled') - .find('div[role="switch"]'); + audioNotifsToggle = screen + .getByTestId("notif-setting-audioNotificationsEnabled") + .querySelector('div[role="switch"]'); }; update(); - expect(audioNotifsToggle.getDOMNode().getAttribute("aria-checked")).toEqual("true"); + expect(audioNotifsToggle.getAttribute("aria-checked")).toEqual("true"); expect(SettingsStore.getValue("audioNotificationsEnabled")).toEqual(true); - act(() => { audioNotifsToggle.simulate('click'); }); + act(() => { + fireEvent.click(audioNotifsToggle); + }); update(); - expect(audioNotifsToggle.getDOMNode().getAttribute("aria-checked")).toEqual("false"); + expect(audioNotifsToggle.getAttribute("aria-checked")).toEqual("false"); expect(SettingsStore.getValue("audioNotificationsEnabled")).toEqual(false); }); }); - describe('individual notification level settings', () => { - const getCheckedRadioForRule = (ruleEl) => - ruleEl.find('input[type="radio"][checked=true]').props()['aria-label']; - it('renders categories correctly', async () => { - const component = await getComponentAndWait(); + describe("individual notification level settings", () => { + it("renders categories correctly", async () => { + await getComponentAndWait(); - expect(findByTestId(component, 'notif-section-vector_global').length).toBeTruthy(); - expect(findByTestId(component, 'notif-section-vector_mentions').length).toBeTruthy(); - expect(findByTestId(component, 'notif-section-vector_other').length).toBeTruthy(); + expect(screen.getByTestId("notif-section-vector_global")).toBeInTheDocument(); + expect(screen.getByTestId("notif-section-vector_mentions")).toBeInTheDocument(); + expect(screen.getByTestId("notif-section-vector_other")).toBeInTheDocument(); }); - it('renders radios correctly', async () => { - const component = await getComponentAndWait(); - const section = 'vector_global'; + it("renders radios correctly", async () => { + await getComponentAndWait(); + const section = "vector_global"; - const globalSection = findByTestId(component, `notif-section-${section}`); + const globalSection = screen.getByTestId(`notif-section-${section}`); // 4 notification rules with class 'global' - expect(globalSection.find('fieldset').length).toEqual(4); + expect(globalSection.querySelectorAll("fieldset").length).toEqual(4); // oneToOneRule is set to 'on' - const oneToOneRuleElement = findByTestId(component, section + oneToOneRule.rule_id); - expect(getCheckedRadioForRule(oneToOneRuleElement)).toEqual('On'); + const oneToOneRuleElement = screen.getByTestId(section + oneToOneRule.rule_id); + expect(oneToOneRuleElement.querySelector("[aria-label='On']")).toBeInTheDocument(); // encryptedOneToOneRule is set to 'loud' - const encryptedOneToOneElement = findByTestId(component, section + encryptedOneToOneRule.rule_id); - expect(getCheckedRadioForRule(encryptedOneToOneElement)).toEqual('Noisy'); + const encryptedOneToOneElement = screen.getByTestId(section + encryptedOneToOneRule.rule_id); + expect(encryptedOneToOneElement.querySelector("[aria-label='Noisy']")).toBeInTheDocument(); // encryptedGroupRule is set to 'off' - const encryptedGroupElement = findByTestId(component, section + encryptedGroupRule.rule_id); - expect(getCheckedRadioForRule(encryptedGroupElement)).toEqual('Off'); + const encryptedGroupElement = screen.getByTestId(section + encryptedGroupRule.rule_id); + expect(encryptedGroupElement.querySelector("[aria-label='Off']")).toBeInTheDocument(); }); - it('updates notification level when changed', async () => { - const component = await getComponentAndWait(); - const section = 'vector_global'; + it("updates notification level when changed", async () => { + await getComponentAndWait(); + const section = "vector_global"; // oneToOneRule is set to 'on' // and is kind: 'underride' - const oneToOneRuleElement = findByTestId(component, section + oneToOneRule.rule_id); + const oneToOneRuleElement = screen.getByTestId(section + oneToOneRule.rule_id); await act(async () => { - // toggle at 0 is 'off' - const offToggle = oneToOneRuleElement.find('input[type="radio"]').at(0); - offToggle.simulate('change'); + const offToggle = oneToOneRuleElement.querySelector('input[type="radio"]'); + fireEvent.click(offToggle); }); expect(mockClient.setPushRuleEnabled).toHaveBeenCalledWith( - 'global', 'underride', oneToOneRule.rule_id, true); + "global", + "underride", + oneToOneRule.rule_id, + true, + ); // actions for '.m.rule.room_one_to_one' state is ACTION_DONT_NOTIFY expect(mockClient.setPushRuleActions).toHaveBeenCalledWith( - 'global', 'underride', oneToOneRule.rule_id, StandardActions.ACTION_DONT_NOTIFY); + "global", + "underride", + oneToOneRule.rule_id, + StandardActions.ACTION_DONT_NOTIFY, + ); + }); + }); + + describe("clear all notifications", () => { + it("clears all notifications", async () => { + const room = new Room("room123", mockClient, "@alice:example.org"); + mockClient.getRooms.mockReset().mockReturnValue([room]); + + const message = mkMessage({ + event: true, + room: "room123", + user: "@alice:example.org", + ts: 1, + }); + room.addLiveEvents([message]); + room.setUnreadNotificationCount(NotificationCountType.Total, 1); + + const { container } = await getComponentAndWait(); + const clearNotificationEl = getByTestId(container, "clear-notifications"); + + fireEvent.click(clearNotificationEl); + + expect(clearNotificationEl.className).toContain("mx_AccessibleButton_disabled"); + expect(mockClient.sendReadReceipt).toHaveBeenCalled(); + + await waitFor(() => { + expect(clearNotificationEl.className).not.toContain("mx_AccessibleButton_disabled"); + }); }); }); }); diff --git a/test/components/views/settings/SettingsFieldset-test.tsx b/test/components/views/settings/SettingsFieldset-test.tsx index 28a784f25bb..3aafce504d8 100644 --- a/test/components/views/settings/SettingsFieldset-test.tsx +++ b/test/components/views/settings/SettingsFieldset-test.tsx @@ -12,35 +12,42 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import { renderIntoDocument } from 'react-dom/test-utils'; +import React from "react"; +import { renderIntoDocument } from "react-dom/test-utils"; -import SettingsFieldset from '../../../../src/components/views/settings/SettingsFieldset'; +import SettingsFieldset from "../../../../src/components/views/settings/SettingsFieldset"; -describe('', () => { +describe("", () => { const defaultProps = { - "legend": 'Who can read history?', + "legend": "Who can read history?", "children":
test
, - 'data-test-id': 'test', + "data-test-id": "test", }; const getComponent = (props = {}) => { const wrapper = renderIntoDocument( -
, +
+ +
, ) as HTMLDivElement; return wrapper.children[0]; }; - it('renders fieldset without description', () => { + it("renders fieldset without description", () => { expect(getComponent()).toMatchSnapshot(); }); - it('renders fieldset with plain text description', () => { - const description = 'Changes to who can read history.'; + it("renders fieldset with plain text description", () => { + const description = "Changes to who can read history."; expect(getComponent({ description })).toMatchSnapshot(); }); - it('renders fieldset with react description', () => { - const description = <>

Test

a link; + it("renders fieldset with react description", () => { + const description = ( + <> +

Test

+ a link + + ); expect(getComponent({ description })).toMatchSnapshot(); }); }); diff --git a/test/components/views/settings/ThemeChoicePanel-test.tsx b/test/components/views/settings/ThemeChoicePanel-test.tsx index e1eb47cfa0f..ce36778231b 100644 --- a/test/components/views/settings/ThemeChoicePanel-test.tsx +++ b/test/components/views/settings/ThemeChoicePanel-test.tsx @@ -14,29 +14,23 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -// eslint-disable-next-line deprecate/import -import { mount } from "enzyme"; +import React from "react"; +import { render } from "@testing-library/react"; import * as TestUtils from "../../../test-utils"; -import ThemeChoicePanel from '../../../../src/components/views/settings/ThemeChoicePanel'; +import ThemeChoicePanel from "../../../../src/components/views/settings/ThemeChoicePanel"; // Fake random strings to give a predictable snapshot -jest.mock( - 'matrix-js-sdk/src/randomstring', - () => { - return { - randomString: () => "abdefghi", - }; - }, -); +jest.mock("matrix-js-sdk/src/randomstring", () => { + return { + randomString: () => "abdefghi", + }; +}); -describe('ThemeChoicePanel', () => { - it('renders the theme choice UI', () => { +describe("ThemeChoicePanel", () => { + it("renders the theme choice UI", () => { TestUtils.stubClient(); - const wrapper = mount( - , - ); - expect(wrapper).toMatchSnapshot(); + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); }); }); diff --git a/test/components/views/settings/UiFeatureSettingWrapper-test.tsx b/test/components/views/settings/UiFeatureSettingWrapper-test.tsx index 59044577924..aeba1273ded 100644 --- a/test/components/views/settings/UiFeatureSettingWrapper-test.tsx +++ b/test/components/views/settings/UiFeatureSettingWrapper-test.tsx @@ -14,44 +14,43 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -// eslint-disable-next-line deprecate/import -import { mount } from 'enzyme'; +import React from "react"; +import { render } from "@testing-library/react"; -import SettingsStore from '../../../../src/settings/SettingsStore'; -import UiFeatureSettingWrapper from '../../../../src/components/views/settings/UiFeatureSettingWrapper'; -import { UIFeature } from '../../../../src/settings/UIFeature'; +import SettingsStore from "../../../../src/settings/SettingsStore"; +import UiFeatureSettingWrapper from "../../../../src/components/views/settings/UiFeatureSettingWrapper"; +import { UIFeature } from "../../../../src/settings/UIFeature"; -jest.mock('../../../../src/settings/SettingsStore'); +jest.mock("../../../../src/settings/SettingsStore"); -describe('', () => { +describe("", () => { const defaultProps = { uiFeature: UIFeature.Feedback, children:
test
, }; - const getComponent = (props = {}) => mount(); + const getComponent = (props = {}) => render(); beforeEach(() => { (SettingsStore.getValue as jest.Mock).mockClear().mockReturnValue(true); }); - it('renders children when setting is truthy', () => { - const component = getComponent(); + it("renders children when setting is truthy", () => { + const { asFragment } = getComponent(); - expect(component).toMatchSnapshot(); + expect(asFragment()).toMatchSnapshot(); expect(SettingsStore.getValue).toHaveBeenCalledWith(defaultProps.uiFeature); }); - it('returns null when setting is truthy but children are undefined', () => { - const component = getComponent({ children: undefined }); + it("returns null when setting is truthy but children are undefined", () => { + const { asFragment } = getComponent({ children: undefined }); - expect(component.html()).toBeNull(); + expect(asFragment()).toMatchSnapshot(); }); - it('returns null when setting is falsy', () => { + it("returns null when setting is falsy", () => { (SettingsStore.getValue as jest.Mock).mockReturnValue(false); - const component = getComponent(); + const { asFragment } = getComponent(); - expect(component.html()).toBeNull(); + expect(asFragment()).toMatchSnapshot(); }); }); diff --git a/test/components/views/settings/__snapshots__/FontScalingPanel-test.tsx.snap b/test/components/views/settings/__snapshots__/FontScalingPanel-test.tsx.snap index ef46747902a..a9752874bd2 100644 --- a/test/components/views/settings/__snapshots__/FontScalingPanel-test.tsx.snap +++ b/test/components/views/settings/__snapshots__/FontScalingPanel-test.tsx.snap @@ -1,313 +1,186 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`FontScalingPanel renders the font scaling UI 1`] = ` - +
Font size -
- -
-
-
- +
- +
Aa
- -
-
+
+
+
-
-
- 15 -
+ 15
-
+
-
+
+ - +
- -
-
-
-
- - - +
+ + +
+
- -
-
-
-
- - - +
+ + +
+
- -
-
-
-
- - - +
+ + +
+
- -
-
-
-
- - - +
+ + +
+
- -
-
-
-
- - -
+
+
+
- +
Aa
- - +