From a3defa6cf7ad9e368924e5a611ea8764f1865fdb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 Feb 2023 10:31:10 +1300 Subject: [PATCH 01/44] Update dependency rimraf to v4 (#10234) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 17b40b5789..9008f464fc 100644 --- a/package.json +++ b/package.json @@ -211,7 +211,7 @@ "postcss-scss": "^4.0.4", "prettier": "2.8.0", "raw-loader": "^4.0.2", - "rimraf": "^3.0.2", + "rimraf": "^4.0.0", "stylelint": "^14.9.1", "stylelint-config-prettier": "^9.0.4", "stylelint-config-standard": "^29.0.0", diff --git a/yarn.lock b/yarn.lock index 4ec4f4ccca..fde6e1b488 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7763,6 +7763,11 @@ rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" +rimraf@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-4.1.2.tgz#20dfbc98083bdfaa28b01183162885ef213dbf7c" + integrity sha512-BlIbgFryTbw3Dz6hyoWFhKk+unCcHMSkZGrTFVAx2WmttdBSonsdtRlwiuTbDqTKr+UlXIUqJVS4QT5tUzGENQ== + rrweb-snapshot@^1.1.14: version "1.1.14" resolved "https://registry.yarnpkg.com/rrweb-snapshot/-/rrweb-snapshot-1.1.14.tgz#9d4d9be54a28a893373428ee4393ec7e5bd83fcc" From 12dd799301c2633b3d780408d16166fb2b0fc5fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Telaty=C5=84ski?= <7t3chguy@gmail.com> Date: Mon, 27 Feb 2023 09:15:27 +0000 Subject: [PATCH 02/44] Fix double translation issue (#10240 * Fix double translation issue * Remove some redundant string concatenations --- cypress/e2e/crypto/decryption-failure.spec.ts | 2 +- src/AddThreepid.ts | 5 ++--- src/LegacyCallHandler.tsx | 2 +- .../views/dialogs/eventindex/ManageEventIndexDialog.tsx | 8 +++----- src/components/structures/TimelinePanel.tsx | 4 +--- src/components/structures/auth/Login.tsx | 2 +- src/components/views/auth/CaptchaForm.tsx | 2 +- src/components/views/dialogs/StorageEvictedDialog.tsx | 2 +- .../views/dialogs/security/RestoreKeyBackupDialog.tsx | 4 ++-- src/components/views/location/LocationPicker.tsx | 2 +- src/components/views/rooms/NewRoomIntro.tsx | 6 +++--- src/components/views/rooms/RoomPreviewBar.tsx | 2 +- src/components/views/settings/EventIndexPanel.tsx | 2 +- src/components/views/settings/SecureBackupPanel.tsx | 2 +- src/components/views/settings/SetIdServer.tsx | 4 ++-- src/components/views/settings/account/EmailAddresses.tsx | 2 +- .../views/settings/discovery/EmailAddresses.tsx | 2 +- src/utils/leave-behaviour.ts | 4 ++-- src/utils/location/findMapStyleUrl.ts | 4 +--- src/utils/location/map.ts | 5 +---- test/components/views/elements/EventListSummary-test.tsx | 4 +--- 21 files changed, 29 insertions(+), 41 deletions(-) diff --git a/cypress/e2e/crypto/decryption-failure.spec.ts b/cypress/e2e/crypto/decryption-failure.spec.ts index 20f748494a..15b437d621 100644 --- a/cypress/e2e/crypto/decryption-failure.spec.ts +++ b/cypress/e2e/crypto/decryption-failure.spec.ts @@ -164,7 +164,7 @@ describe("Decryption Failure Bar", () => { cy.contains(".mx_DecryptionFailureBar_button", "Resend key requests").should("not.exist"); cy.get(".mx_DecryptionFailureBar").percySnapshotElement( - "DecryptionFailureBar prompts user to open another device, " + "without Resend Key Requests button", + "DecryptionFailureBar prompts user to open another device, without Resend Key Requests button", { widths: [320, 640], }, diff --git a/src/AddThreepid.ts b/src/AddThreepid.ts index b1ee5795d0..db74eaad31 100644 --- a/src/AddThreepid.ts +++ b/src/AddThreepid.ts @@ -213,8 +213,7 @@ export default class AddThreepid { [SSOAuthEntry.PHASE_PREAUTH]: { title: _t("Use Single Sign On to continue"), body: _t( - "Confirm adding this email address by using " + - "Single Sign On to prove your identity.", + "Confirm adding this email address by using Single Sign On to prove your identity.", ), continueText: _t("Single Sign On"), continueKind: "primary", @@ -333,7 +332,7 @@ export default class AddThreepid { [SSOAuthEntry.PHASE_PREAUTH]: { title: _t("Use Single Sign On to continue"), body: _t( - "Confirm adding this phone number by using " + "Single Sign On to prove your identity.", + "Confirm adding this phone number by using Single Sign On to prove your identity.", ), continueText: _t("Single Sign On"), continueKind: "primary", diff --git a/src/LegacyCallHandler.tsx b/src/LegacyCallHandler.tsx index b5358b4930..90bded231e 100644 --- a/src/LegacyCallHandler.tsx +++ b/src/LegacyCallHandler.tsx @@ -737,7 +737,7 @@ export default class LegacyCallHandler extends EventEmitter { ); if (!stats) { logger.debug( - "Call statistics are undefined. The call has " + "probably failed before a peerConn was established", + "Call statistics are undefined. The call has probably failed before a peerConn was established", ); return; } diff --git a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx index 5393ae3fc6..5513165e69 100644 --- a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx +++ b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx @@ -157,11 +157,9 @@ export default class ManageEventIndexDialog extends React.Component - {_t( - "%(brand)s is securely caching encrypted messages locally for them " + - "to appear in search results:", - { brand }, - )} + {_t("%(brand)s is securely caching encrypted messages locally for them to appear in search results:", { + brand, + })}
{crawlerState}
diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index da17ae36e1..8e6833954f 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -1473,9 +1473,7 @@ class TimelinePanel extends React.Component { "do not have permission to view the message in question.", ); } else { - description = _t( - "Tried to load a specific point in this room's timeline, but was " + "unable to find it.", - ); + description = _t("Tried to load a specific point in this room's timeline, but was unable to find it."); } Modal.createDialog(ErrorDialog, { diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx index c2c7a0d913..97f3866f96 100644 --- a/src/components/structures/auth/Login.tsx +++ b/src/components/structures/auth/Login.tsx @@ -454,7 +454,7 @@ export default class LoginComponent extends React.PureComponent } let errorText: ReactNode = - _t("There was a problem communicating with the homeserver, " + "please try again later.") + + _t("There was a problem communicating with the homeserver, please try again later.") + (errCode ? " (" + errCode + ")" : ""); if (err instanceof ConnectionError) { diff --git a/src/components/views/auth/CaptchaForm.tsx b/src/components/views/auth/CaptchaForm.tsx index 651921b233..a34038f000 100644 --- a/src/components/views/auth/CaptchaForm.tsx +++ b/src/components/views/auth/CaptchaForm.tsx @@ -91,7 +91,7 @@ export default class CaptchaForm extends React.Component { )}

- {_t("Your browser likely removed this data when running low on " + "disk space.")} {logRequest} + {_t("Your browser likely removed this data when running low on disk space.")} {logRequest}

{_t( - "Warning: you should only set up key backup " + "from a trusted computer.", + "Warning: you should only set up key backup from a trusted computer.", {}, { b: (sub) => {sub} }, )} @@ -480,7 +480,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent

{_t( - "Warning: you should only set up key backup " + "from a trusted computer.", + "Warning: you should only set up key backup from a trusted computer.", {}, { b: (sub) => {sub} }, )} diff --git a/src/components/views/location/LocationPicker.tsx b/src/components/views/location/LocationPicker.tsx index 53c010c809..503d4d8688 100644 --- a/src/components/views/location/LocationPicker.tsx +++ b/src/components/views/location/LocationPicker.tsx @@ -93,7 +93,7 @@ class LocationPicker extends React.Component { this.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, ); this.setState({ error: LocationShareError.MapStyleUrlNotReachable }); diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx index fba24be3f2..f8ab6f4c18 100644 --- a/src/components/views/rooms/NewRoomIntro.tsx +++ b/src/components/views/rooms/NewRoomIntro.tsx @@ -23,7 +23,7 @@ import { User } from "matrix-js-sdk/src/models/user"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import RoomContext from "../../../contexts/RoomContext"; import DMRoomMap from "../../../utils/DMRoomMap"; -import { _t } from "../../../languageHandler"; +import { _t, _td } from "../../../languageHandler"; import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; import MiniAvatarUploader, { AVATAR_SIZE } from "../elements/MiniAvatarUploader"; import RoomAvatar from "../avatars/RoomAvatar"; @@ -55,11 +55,11 @@ const NewRoomIntro: React.FC = () => { let body: JSX.Element; if (dmPartner) { - let introMessage = _t("This is the beginning of your direct message history with ."); + let introMessage = _td("This is the beginning of your direct message history with ."); let caption: string | undefined; if (isLocalRoom) { - introMessage = _t("Send your first message to invite to chat"); + introMessage = _td("Send your first message to invite to chat"); } else if (room.getJoinedMemberCount() + room.getInvitedMemberCount() === 2) { caption = _t("Only the two of you are in this conversation, unless either of you invites anyone to join."); } diff --git a/src/components/views/rooms/RoomPreviewBar.tsx b/src/components/views/rooms/RoomPreviewBar.tsx index f9359bc5a5..a968bd3794 100644 --- a/src/components/views/rooms/RoomPreviewBar.tsx +++ b/src/components/views/rooms/RoomPreviewBar.tsx @@ -435,7 +435,7 @@ export default class RoomPreviewBar extends React.Component { } subTitle = _t( - "Link this email with your account in Settings to receive invites " + "directly in %(brand)s.", + "Link this email with your account in Settings to receive invites directly in %(brand)s.", { brand }, ); primaryActionLabel = _t("Join the discussion"); diff --git a/src/components/views/settings/EventIndexPanel.tsx b/src/components/views/settings/EventIndexPanel.tsx index c5e617355d..8a1d93c98f 100644 --- a/src/components/views/settings/EventIndexPanel.tsx +++ b/src/components/views/settings/EventIndexPanel.tsx @@ -177,7 +177,7 @@ export default class EventIndexPanel extends React.Component<{}, IState> { eventIndexingSettings = (

- {_t("Securely cache encrypted messages locally for them to " + "appear in search results.")} + {_t("Securely cache encrypted messages locally for them to appear in search results.")}
diff --git a/src/components/views/settings/SecureBackupPanel.tsx b/src/components/views/settings/SecureBackupPanel.tsx index cc5dde1cb0..98c241fd0f 100644 --- a/src/components/views/settings/SecureBackupPanel.tsx +++ b/src/components/views/settings/SecureBackupPanel.tsx @@ -183,7 +183,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { Modal.createDialog(QuestionDialog, { title: _t("Delete Backup"), description: _t( - "Are you sure? You will lose your encrypted messages if your " + "keys are not backed up properly.", + "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.", ), button: _t("Delete Backup"), danger: true, diff --git a/src/components/views/settings/SetIdServer.tsx b/src/components/views/settings/SetIdServer.tsx index 43f59c8ee1..feb1bc0f79 100644 --- a/src/components/views/settings/SetIdServer.tsx +++ b/src/components/views/settings/SetIdServer.tsx @@ -192,7 +192,7 @@ export default class SetIdServer extends React.Component { const [confirmed] = await this.showServerChangeWarning({ title: _t("Change identity server"), unboundMessage: _t( - "Disconnect from the identity server and " + "connect to instead?", + "Disconnect from the identity server and connect to instead?", {}, { current: (sub) => {abbreviateUrl(currentClientIdServer)}, @@ -330,7 +330,7 @@ export default class SetIdServer extends React.Component {

{_t( - "You are still sharing your personal data on the identity " + "server .", + "You are still sharing your personal data on the identity server .", {}, messageElements, )} diff --git a/src/components/views/settings/account/EmailAddresses.tsx b/src/components/views/settings/account/EmailAddresses.tsx index 83e137a21a..0d5405d93f 100644 --- a/src/components/views/settings/account/EmailAddresses.tsx +++ b/src/components/views/settings/account/EmailAddresses.tsx @@ -223,7 +223,7 @@ export default class EmailAddresses extends React.Component { Modal.createDialog(ErrorDialog, { title: _t("Your email address hasn't been verified yet"), description: _t( - "Click the link in the email you received to verify " + "and then click continue again.", + "Click the link in the email you received to verify and then click continue again.", ), }); } else { diff --git a/src/components/views/settings/discovery/EmailAddresses.tsx b/src/components/views/settings/discovery/EmailAddresses.tsx index 9f2e123ac3..18cf49a162 100644 --- a/src/components/views/settings/discovery/EmailAddresses.tsx +++ b/src/components/views/settings/discovery/EmailAddresses.tsx @@ -184,7 +184,7 @@ export class EmailAddress extends React.Component !!r[1]); if (errors.length > 0) { - const messages = []; + const messages: ReactNode[] = []; for (const roomErr of errors) { const err = roomErr[1]; // [0] is the roomId let message = _t("Unexpected server error trying to leave the room"); diff --git a/src/utils/location/findMapStyleUrl.ts b/src/utils/location/findMapStyleUrl.ts index 0653d65cf2..02ff401b14 100644 --- a/src/utils/location/findMapStyleUrl.ts +++ b/src/utils/location/findMapStyleUrl.ts @@ -29,9 +29,7 @@ export function findMapStyleUrl(): string { 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/map.ts b/src/utils/location/map.ts index 34d3d01478..b0434d76cd 100644 --- a/src/utils/location/map.ts +++ b/src/utils/location/map.ts @@ -50,10 +50,7 @@ export const createMap = (interactive: boolean, bodyId: string, onError?: (error map.addControl(new maplibregl.AttributionControl(), "top-right"); map.on("error", (e) => { - logger.error( - "Failed to load map: check map_style_url in config.json has a " + "valid URL and API key", - e.error, - ); + logger.error("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)); }); diff --git a/test/components/views/elements/EventListSummary-test.tsx b/test/components/views/elements/EventListSummary-test.tsx index 4ecb963ee0..81fc59e2aa 100644 --- a/test/components/views/elements/EventListSummary-test.tsx +++ b/test/components/views/elements/EventListSummary-test.tsx @@ -567,9 +567,7 @@ describe("EventListSummary", function () { const summary = wrapper.find(".mx_GenericEventListSummary_summary"); const summaryText = summary.text(); - expect(summaryText).toBe( - "user_1 and one other rejected their invitations and " + "had their invitations withdrawn", - ); + expect(summaryText).toBe("user_1 and one other rejected their invitations and had their invitations withdrawn"); }); it("handles invitation plurals correctly when there are multiple invites", function () { From c22971e5423e83610890dc8c433acf16a7fa909e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Telaty=C5=84ski?= <7t3chguy@gmail.com> Date: Mon, 27 Feb 2023 09:16:49 +0000 Subject: [PATCH 03/44] Improve percy snapshot stability (#10239) --- cypress/e2e/polls/polls.spec.ts | 4 ++-- cypress/e2e/timeline/timeline.spec.ts | 14 ++++++-------- cypress/support/percy.ts | 4 +++- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/cypress/e2e/polls/polls.spec.ts b/cypress/e2e/polls/polls.spec.ts index 07a14533c7..b2537c2cbe 100644 --- a/cypress/e2e/polls/polls.spec.ts +++ b/cypress/e2e/polls/polls.spec.ts @@ -20,7 +20,7 @@ import { HomeserverInstance } from "../../plugins/utils/homeserver"; import { MatrixClient } from "../../global"; import Chainable = Cypress.Chainable; -const hideTimestampCSS = ".mx_MessageTimestamp { visibility: hidden !important; }"; +const hidePercyCSS = ".mx_MessageTimestamp, .mx_RoomView_myReadMarker { visibility: hidden !important; }"; describe("Polls", () => { let homeserver: HomeserverInstance; @@ -133,7 +133,7 @@ describe("Polls", () => { .as("pollId"); cy.get("@pollId").then((pollId) => { - getPollTile(pollId).percySnapshotElement("Polls Timeline tile - no votes", { percyCSS: hideTimestampCSS }); + getPollTile(pollId).percySnapshotElement("Polls Timeline tile - no votes", { percyCSS: hidePercyCSS }); // Bot votes 'Maybe' in the poll botVoteForOption(bot, roomId, pollId, pollParams.options[2]); diff --git a/cypress/e2e/timeline/timeline.spec.ts b/cypress/e2e/timeline/timeline.spec.ts index aa5a94a6dd..246f4e3d77 100644 --- a/cypress/e2e/timeline/timeline.spec.ts +++ b/cypress/e2e/timeline/timeline.spec.ts @@ -185,9 +185,8 @@ describe("Timeline", () => { .should("have.css", "margin-inline-start", "104px") .should("have.css", "inset-inline-start", "0px"); - // Exclude timestamp from snapshot - const percyCSS = - ".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp { visibility: hidden !important; }"; + // Exclude timestamp and read marker from snapshot + const percyCSS = ".mx_MessageTimestamp, .mx_RoomView_myReadMarker { visibility: hidden !important; }"; cy.get(".mx_MainSplit").percySnapshotElement("Event line with inline start margin on IRC layout", { percyCSS, }); @@ -213,8 +212,8 @@ describe("Timeline", () => { // Click timestamp to highlight hidden event line cy.get(".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp").click(); - // Exclude timestamp from snapshot - const percyCSS = ".mx_RoomView_body .mx_EventTile .mx_MessageTimestamp { visibility: hidden !important; }"; + // Exclude timestamp and read marker from snapshot + const percyCSS = ".mx_MessageTimestamp, .mx_RoomView_myReadMarker { visibility: hidden !important; }"; // should not add inline start padding to a hidden event line on IRC layout cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC); @@ -336,9 +335,8 @@ describe("Timeline", () => { cy.checkA11y(); - // Exclude timestamp from snapshot - const percyCSS = - ".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp { visibility: hidden !important; }"; + // Exclude timestamp and read marker from snapshot + const percyCSS = ".mx_MessageTimestamp, .mx_RoomView_myReadMarker { visibility: hidden !important; }"; cy.get(".mx_EventTile_last").percySnapshotElement("URL Preview", { percyCSS, widths: [800, 400], diff --git a/cypress/support/percy.ts b/cypress/support/percy.ts index b0f5c9f7c7..9183d5ebf6 100644 --- a/cypress/support/percy.ts +++ b/cypress/support/percy.ts @@ -41,7 +41,9 @@ declare global { Cypress.Commands.add("percySnapshotElement", { prevSubject: "element" }, (subject, name, options) => { if (!options?.allowSpinners) { // Await spinners to vanish - cy.get(".mx_Spinner").should("not.exist"); + cy.get(".mx_Spinner", { log: false }).should("not.exist"); + // But like really no more spinners please + cy.get(".mx_Spinner", { log: false }).should("not.exist"); } cy.percySnapshot(name, { domTransformation: (documentClone) => scope(documentClone, subject.selector), From 62cd0f1beb31097505c82e6b61b5cb46211b813a Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 27 Feb 2023 09:34:02 +0000 Subject: [PATCH 04/44] Use the room avatar as a placeholder in calls (#10231) * Use the room avatar as a placeholder in calls Rather than the image for the user we're in a call with. This makes it work correctly with virtual rooms easily since we'll get the avatar for the correct room. * Prettier * TS strict errors * More TS strict fixes * More strict TS * Prettier * Even more TS strict * more stricter --- src/components/views/voip/LegacyCallView.tsx | 32 +++++++-- src/components/views/voip/VideoFeed.tsx | 9 ++- test/components/views/voip/VideoFeed-test.tsx | 69 +++++++++++++++++++ 3 files changed, 102 insertions(+), 8 deletions(-) create mode 100644 test/components/views/voip/VideoFeed-test.tsx diff --git a/src/components/views/voip/LegacyCallView.tsx b/src/components/views/voip/LegacyCallView.tsx index 1e8b4621f9..2d989d296a 100644 --- a/src/components/views/voip/LegacyCallView.tsx +++ b/src/components/views/voip/LegacyCallView.tsx @@ -430,7 +430,8 @@ export default class LegacyCallView extends React.Component { const { pipMode, call, onResize } = this.props; const { isLocalOnHold, isRemoteOnHold, sidebarShown, primaryFeed, secondaryFeed, sidebarFeeds } = this.state; - const callRoom = MatrixClientPeg.get().getRoom(call.roomId) ?? undefined; + const callRoomId = LegacyCallHandler.instance.roomIdForCall(call); + const callRoom = (callRoomId ? MatrixClientPeg.get().getRoom(callRoomId) : undefined) ?? undefined; const avatarSize = pipMode ? 76 : 160; const transfereeCall = LegacyCallHandler.instance.getTransfereeForCallId(call.callId); const isOnHold = isLocalOnHold || isRemoteOnHold; @@ -527,23 +528,44 @@ export default class LegacyCallView extends React.Component {

); } else if (pipMode) { + // We've already checked that we have feeds so we cast away the optional when passing the feed return (
- +
); } else if (secondaryFeed) { return (
- + {secondaryFeedElement}
); } else { return (
- - {sidebarShown && } + + {sidebarShown && ( + + )}
); } diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index 35fb851f4a..503c53ec66 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -23,7 +23,9 @@ import { logger } from "matrix-js-sdk/src/logger"; import { SDPStreamMetadataPurpose } from "matrix-js-sdk/src/webrtc/callEventTypes"; import SettingsStore from "../../../settings/SettingsStore"; -import MemberAvatar from "../avatars/MemberAvatar"; +import LegacyCallHandler from "../../../LegacyCallHandler"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import RoomAvatar from "../avatars/RoomAvatar"; interface IProps { call: MatrixCall; @@ -197,7 +199,8 @@ export default class VideoFeed extends React.PureComponent { let content; if (this.state.videoMuted) { - const member = this.props.feed.getMember(); + const callRoomId = LegacyCallHandler.instance.roomIdForCall(this.props.call); + const callRoom = (callRoomId ? MatrixClientPeg.get().getRoom(callRoomId) : undefined) ?? undefined; let avatarSize; if (pipMode && primary) avatarSize = 76; @@ -205,7 +208,7 @@ export default class VideoFeed extends React.PureComponent { else if (!pipMode && primary) avatarSize = 160; else; // TBD - content = ; + content = ; } else { const videoClasses = classnames("mx_VideoFeed_video", { mx_VideoFeed_video_mirror: diff --git a/test/components/views/voip/VideoFeed-test.tsx b/test/components/views/voip/VideoFeed-test.tsx new file mode 100644 index 0000000000..85152437e0 --- /dev/null +++ b/test/components/views/voip/VideoFeed-test.tsx @@ -0,0 +1,69 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed"; +import { MatrixCall } from "matrix-js-sdk/src/webrtc/call"; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { Room } from "matrix-js-sdk/src/models/room"; + +import * as AvatarModule from "../../../../src/Avatar"; +import VideoFeed from "../../../../src/components/views/voip/VideoFeed"; +import { stubClient, useMockedCalls } from "../../../test-utils"; +import LegacyCallHandler from "../../../../src/LegacyCallHandler"; +import DMRoomMap from "../../../../src/utils/DMRoomMap"; + +const FAKE_AVATAR_URL = "http://fakeurl.dummy/fake.png"; + +describe("VideoFeed", () => { + useMockedCalls(); + + let client: MatrixClient; + + beforeAll(() => { + client = stubClient(); + (AvatarModule as any).avatarUrlForRoom = jest.fn().mockReturnValue(FAKE_AVATAR_URL); + + const dmRoomMap = new DMRoomMap(client); + jest.spyOn(dmRoomMap, "getUserIdForRoomId"); + jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap); + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + it("Displays the room avatar when no video is available", () => { + window.mxLegacyCallHandler = { + roomIdForCall: jest.fn().mockReturnValue("!this:room.here"), + } as unknown as LegacyCallHandler; + + const mockCall = { + room: new Room("!room:example.com", client, client.getSafeUserId()), + }; + + const feed = { + isAudioMuted: jest.fn().mockReturnValue(false), + isVideoMuted: jest.fn().mockReturnValue(true), + addListener: jest.fn(), + removeListener: jest.fn(), + }; + render(); + const avatarImg = screen.getByRole("img"); + expect(avatarImg).toHaveAttribute("src", FAKE_AVATAR_URL); + }); +}); From 24b8bcac885ac8663c502da641dbe75c674a7368 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 Feb 2023 10:12:42 +0000 Subject: [PATCH 05/44] Update dependency stylelint to v15 (#10242) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 99 +++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 80 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 9008f464fc..2b52f19eea 100644 --- a/package.json +++ b/package.json @@ -212,7 +212,7 @@ "prettier": "2.8.0", "raw-loader": "^4.0.2", "rimraf": "^4.0.0", - "stylelint": "^14.9.1", + "stylelint": "^15.0.0", "stylelint-config-prettier": "^9.0.4", "stylelint-config-standard": "^29.0.0", "stylelint-scss": "^4.2.0", diff --git a/yarn.lock b/yarn.lock index fde6e1b488..7b359b50df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1182,10 +1182,25 @@ resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== -"@csstools/selector-specificity@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz#1bfafe4b7ed0f3e4105837e056e0a89b108ebe36" - integrity sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg== +"@csstools/css-parser-algorithms@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.0.1.tgz#ff02629c7c95d1f4f8ea84d5ef1173461610535e" + integrity sha512-B9/8PmOtU6nBiibJg0glnNktQDZ3rZnGn/7UmDfrm2vMtrdlXO3p7ErE95N0up80IRk9YEtB5jyj/TmQ1WH3dw== + +"@csstools/css-tokenizer@^2.0.1": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-2.1.0.tgz#fee4de3d444db3ce9007f3af6474af8ba3e4b930" + integrity sha512-dtqFyoJBHUxGi9zPZdpCKP1xk8tq6KPHJ/NY4qWXiYo6IcSGwzk3L8x2XzZbbyOyBs9xQARoGveU2AsgLj6D2A== + +"@csstools/media-query-list-parser@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-2.0.1.tgz#d85a366811563a5d002755ed10e5212a1613c91d" + integrity sha512-X2/OuzEbjaxhzm97UJ+95GrMeT29d1Ib+Pu+paGLuRWZnWRK9sI9r3ikmKXPWGA1C4y4JEdBEFpp9jEqCvLeRA== + +"@csstools/selector-specificity@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.1.1.tgz#c9c61d9fe5ca5ac664e1153bb0aa0eba1c6d6308" + integrity sha512-jwx+WCqszn53YHOfvFMJJRd/B2GqkCBt+1MJSG6o5/s8+ytHMvDZXsJgUEWLk12UnLd7HYKac4BYU5i/Ron1Cw== "@cypress/request@^2.88.10": version "2.88.10" @@ -3454,7 +3469,7 @@ core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== -cosmiconfig@^7.0.0, cosmiconfig@^7.1.0: +cosmiconfig@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== @@ -3465,6 +3480,16 @@ cosmiconfig@^7.0.0, cosmiconfig@^7.1.0: path-type "^4.0.0" yaml "^1.10.0" +cosmiconfig@^8.0.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.1.0.tgz#947e174c796483ccf0a48476c24e4fefb7e1aea8" + integrity sha512-0tLZ9URlPGU7JsKq0DQOQ3FoRsYX8xDZ7xMiATQfaiGMz7EHowNkbU9u1coAOmnh9p/1ySpm0RB3JNWRXM5GCg== + dependencies: + import-fresh "^3.2.1" + js-yaml "^4.1.0" + parse-json "^5.0.0" + path-type "^4.0.0" + counterpart@^0.18.6: version "0.18.6" resolved "https://registry.yarnpkg.com/counterpart/-/counterpart-0.18.6.tgz#cf6b60d8ef99a4b44b8bf6445fa99b4bd1b2f9dd" @@ -3525,6 +3550,14 @@ css-select@^5.1.0: domutils "^3.0.1" nth-check "^2.0.1" +css-tree@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.3.1.tgz#10264ce1e5442e8572fc82fbe490644ff54b5c20" + integrity sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw== + dependencies: + mdn-data "2.0.30" + source-map-js "^1.0.1" + css-what@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" @@ -5163,7 +5196,7 @@ ieee754@^1.1.12, ieee754@^1.1.13: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ignore@^5.2.0: +ignore@^5.2.0, ignore@^5.2.4: version "5.2.4" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== @@ -6548,6 +6581,11 @@ md5@^2.3.0: crypt "0.0.2" is-buffer "~1.1.6" +mdn-data@2.0.30: + version "2.0.30" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc" + integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA== + mdurl@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" @@ -7159,7 +7197,7 @@ postcss-scss@^4.0.4: resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-4.0.6.tgz#5d62a574b950a6ae12f2aa89b60d63d9e4432bfd" integrity sha512-rLDPhJY4z/i4nVFZ27j9GqLxj1pwxE80eAzUNRMXtcpipFYIeowerzBgG3yJhMtObGEXidtIgbUpQ3eLDsf5OQ== -postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.6: +postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.6: version "6.0.11" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz#2e41dc39b7ad74046e1615185185cd0b17d0c8dc" integrity sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g== @@ -7172,7 +7210,7 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@^8.3.11, postcss@^8.4.19: +postcss@^8.3.11: version "8.4.19" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.19.tgz#61178e2add236b17351897c8bcc0b4c8ecab56fc" integrity sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA== @@ -7181,6 +7219,15 @@ postcss@^8.3.11, postcss@^8.4.19: picocolors "^1.0.0" source-map-js "^1.0.2" +postcss@^8.4.21: + version "8.4.21" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4" + integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg== + dependencies: + nanoid "^3.3.4" + picocolors "^1.0.0" + source-map-js "^1.0.2" + posthog-js@1.36.0: version "1.36.0" resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.36.0.tgz#cbefa031a1e7ee6ff25dae29b8aa77bd741adbba" @@ -7967,7 +8014,7 @@ slice-ansi@^4.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" -source-map-js@^1.0.2: +source-map-js@^1.0.1, source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== @@ -8188,16 +8235,20 @@ stylelint-scss@^4.2.0: postcss-selector-parser "^6.0.6" postcss-value-parser "^4.1.0" -stylelint@^14.9.1: - version "14.15.0" - resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-14.15.0.tgz#4df55078e734869f81f6b85bbec2d56a4b478ece" - integrity sha512-JOgDAo5QRsqiOZPZO+B9rKJvBm64S0xasbuRPAbPs6/vQDgDCnZLIiw6XcAS6GQKk9k1sBWR6rmH3Mfj8OknKg== +stylelint@^15.0.0: + version "15.2.0" + resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-15.2.0.tgz#e906eb59df83bde075d148623216f298f9ceb03a" + integrity sha512-wjg5OLn8zQwjlj5cYUgyQpMWKzct42AG5dYlqkHRJQJqsystFFn3onqEc263KH4xfEI0W3lZCnlIhFfS64uwSA== dependencies: - "@csstools/selector-specificity" "^2.0.2" + "@csstools/css-parser-algorithms" "^2.0.1" + "@csstools/css-tokenizer" "^2.0.1" + "@csstools/media-query-list-parser" "^2.0.1" + "@csstools/selector-specificity" "^2.1.1" balanced-match "^2.0.0" colord "^2.9.3" - cosmiconfig "^7.1.0" + cosmiconfig "^8.0.0" css-functions-list "^3.1.0" + css-tree "^2.3.1" debug "^4.3.4" fast-glob "^3.2.12" fastest-levenshtein "^1.0.16" @@ -8206,7 +8257,7 @@ stylelint@^14.9.1: globby "^11.1.0" globjoin "^0.1.4" html-tags "^3.2.0" - ignore "^5.2.0" + ignore "^5.2.4" import-lazy "^4.0.0" imurmurhash "^0.1.4" is-plain-object "^5.0.0" @@ -8216,11 +8267,11 @@ stylelint@^14.9.1: micromatch "^4.0.5" normalize-path "^3.0.0" picocolors "^1.0.0" - postcss "^8.4.19" + postcss "^8.4.21" postcss-media-query-parser "^0.2.3" postcss-resolve-nested-selector "^0.1.1" postcss-safe-parser "^6.0.0" - postcss-selector-parser "^6.0.10" + postcss-selector-parser "^6.0.11" postcss-value-parser "^4.2.0" resolve-from "^5.0.0" string-width "^4.2.3" @@ -8230,7 +8281,7 @@ stylelint@^14.9.1: svg-tags "^1.0.0" table "^6.8.1" v8-compile-cache "^2.3.0" - write-file-atomic "^4.0.2" + write-file-atomic "^5.0.0" supercluster@^7.1.5: version "7.1.5" @@ -8865,7 +8916,7 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== -write-file-atomic@^4.0.1, write-file-atomic@^4.0.2: +write-file-atomic@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd" integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== @@ -8873,6 +8924,14 @@ write-file-atomic@^4.0.1, write-file-atomic@^4.0.2: imurmurhash "^0.1.4" signal-exit "^3.0.7" +write-file-atomic@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-5.0.0.tgz#54303f117e109bf3d540261125c8ea5a7320fab0" + integrity sha512-R7NYMnHSlV42K54lwY9lvW6MnSm1HSJqZL3xiSgi9E7//FYaI74r2G0rd+/X6VAMkHEdzxQaU5HUOXWUz5kA/w== + dependencies: + imurmurhash "^0.1.4" + signal-exit "^3.0.7" + ws@^8.0.0: version "8.12.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.12.0.tgz#485074cc392689da78e1828a9ff23585e06cddd8" From b9f61da7e6ff2dac7a87b676c4071e40524279fd Mon Sep 17 00:00:00 2001 From: Germain Date: Mon, 27 Feb 2023 16:27:13 +0000 Subject: [PATCH 06/44] Add EventTileThreadToolbar tests (#10243) --- .../EventTile/EventTileThreadToolbar-test.tsx | 53 +++++++++++++++++++ .../EventTileThreadToolbar-test.tsx.snap | 29 ++++++++++ 2 files changed, 82 insertions(+) create mode 100644 test/components/views/rooms/EventTile/EventTileThreadToolbar-test.tsx create mode 100644 test/components/views/rooms/EventTile/__snapshots__/EventTileThreadToolbar-test.tsx.snap diff --git a/test/components/views/rooms/EventTile/EventTileThreadToolbar-test.tsx b/test/components/views/rooms/EventTile/EventTileThreadToolbar-test.tsx new file mode 100644 index 0000000000..8c9bff2f1c --- /dev/null +++ b/test/components/views/rooms/EventTile/EventTileThreadToolbar-test.tsx @@ -0,0 +1,53 @@ +/* +Copyright 2023 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 { getByLabelText, render, RenderResult } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import React from "react"; +import { ComponentProps } from "react"; + +import { EventTileThreadToolbar } from "../../../../../src/components/views/rooms/EventTile/EventTileThreadToolbar"; + +describe("EventTileThreadToolbar", () => { + const viewInRoom = jest.fn(); + const copyLink = jest.fn(); + + function renderComponent(props: Partial> = {}): RenderResult { + return render(); + } + + afterEach(() => { + jest.resetAllMocks(); + }); + + it("renders", () => { + const { asFragment } = renderComponent(); + expect(asFragment()).toMatchSnapshot(); + }); + + it("calls the right callbacks", async () => { + const { container } = renderComponent(); + + const copyBtn = getByLabelText(container, "Copy link to thread"); + const viewInRoomBtn = getByLabelText(container, "View in room"); + + await userEvent.click(copyBtn); + expect(copyLink).toHaveBeenCalledTimes(1); + + await userEvent.click(viewInRoomBtn); + expect(viewInRoom).toHaveBeenCalledTimes(1); + }); +}); diff --git a/test/components/views/rooms/EventTile/__snapshots__/EventTileThreadToolbar-test.tsx.snap b/test/components/views/rooms/EventTile/__snapshots__/EventTileThreadToolbar-test.tsx.snap new file mode 100644 index 0000000000..133531b447 --- /dev/null +++ b/test/components/views/rooms/EventTile/__snapshots__/EventTileThreadToolbar-test.tsx.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EventTileThreadToolbar renders 1`] = ` + + ); }; diff --git a/src/components/views/dialogs/polls/fetchPastPolls.ts b/src/components/views/dialogs/polls/fetchPastPolls.ts index 1d045d3d07..e97755874d 100644 --- a/src/components/views/dialogs/polls/fetchPastPolls.ts +++ b/src/components/views/dialogs/polls/fetchPastPolls.ts @@ -14,41 +14,82 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { M_POLL_START } from "matrix-js-sdk/src/@types/polls"; import { MatrixClient } from "matrix-js-sdk/src/client"; -import { EventTimeline, EventTimelineSet, Room } from "matrix-js-sdk/src/matrix"; +import { Direction, EventTimeline, EventTimelineSet, Room } from "matrix-js-sdk/src/matrix"; import { Filter, IFilterDefinition } from "matrix-js-sdk/src/filter"; import { logger } from "matrix-js-sdk/src/logger"; -/** - * Page timeline backwards until either: - * - event older than endOfHistoryPeriodTimestamp is encountered - * - end of timeline is reached - * @param timelineSet - timelineset to page - * @param matrixClient - client - * @param endOfHistoryPeriodTimestamp - epoch timestamp to fetch until - * @returns void - */ -const pagePolls = async ( - timelineSet: EventTimelineSet, - matrixClient: MatrixClient, - endOfHistoryPeriodTimestamp: number, -): Promise => { - const liveTimeline = timelineSet.getLiveTimeline(); - const events = liveTimeline.getEvents(); - const oldestEventTimestamp = events[0]?.getTs() || Date.now(); - const hasMorePages = !!liveTimeline.getPaginationToken(EventTimeline.BACKWARDS); - - if (!hasMorePages || oldestEventTimestamp <= endOfHistoryPeriodTimestamp) { +const getOldestEventTimestamp = (timelineSet?: EventTimelineSet): number | undefined => { + if (!timelineSet) { return; } + const liveTimeline = timelineSet?.getLiveTimeline(); + const events = liveTimeline.getEvents(); + return events[0]?.getTs(); +}; + +/** + * Page backwards in timeline history + * @param timelineSet - timelineset to page + * @param matrixClient - client + * @param canPageBackward - whether the timeline has more pages + * @param oldestEventTimestamp - server ts of the oldest encountered event + */ +const pagePollHistory = async ( + timelineSet: EventTimelineSet, + matrixClient: MatrixClient, +): Promise<{ + oldestEventTimestamp?: number; + canPageBackward: boolean; +}> => { + if (!timelineSet) { + return { canPageBackward: false }; + } + + const liveTimeline = timelineSet.getLiveTimeline(); + await matrixClient.paginateEventTimeline(liveTimeline, { backwards: true, }); - return pagePolls(timelineSet, matrixClient, endOfHistoryPeriodTimestamp); + return { + oldestEventTimestamp: getOldestEventTimestamp(timelineSet), + canPageBackward: !!liveTimeline.getPaginationToken(EventTimeline.BACKWARDS), + }; +}; + +/** + * Page timeline backwards until either: + * - event older than timestamp is encountered + * - end of timeline is reached + * @param timelineSet - timeline set to page + * @param matrixClient - client + * @param timestamp - epoch timestamp to page until + * @param canPageBackward - whether the timeline has more pages + * @param oldestEventTimestamp - server ts of the oldest encountered event + */ +const fetchHistoryUntilTimestamp = async ( + timelineSet: EventTimelineSet | undefined, + matrixClient: MatrixClient, + timestamp: number, + canPageBackward: boolean, + oldestEventTimestamp?: number, +): Promise => { + if (!timelineSet || !canPageBackward || (oldestEventTimestamp && oldestEventTimestamp < timestamp)) { + return; + } + const result = await pagePollHistory(timelineSet, matrixClient); + + return fetchHistoryUntilTimestamp( + timelineSet, + matrixClient, + timestamp, + result.canPageBackward, + result.oldestEventTimestamp, + ); }; const ONE_DAY_MS = 60000 * 60 * 24; @@ -57,35 +98,73 @@ const ONE_DAY_MS = 60000 * 60 * 24; * @param timelineSet - timelineset to page * @param matrixClient - client * @param historyPeriodDays - number of days of history to fetch, from current day - * @returns isLoading - true while fetching history + * @returns isLoading - true while fetching + * @returns oldestEventTimestamp - timestamp of oldest encountered poll, undefined when no polls found in timeline so far + * @returns loadMorePolls - function to page timeline backwards, undefined when timeline cannot be paged backwards + * @returns loadTimelineHistory - loads timeline history for the given history period */ const useTimelineHistory = ( - timelineSet: EventTimelineSet | null, + timelineSet: EventTimelineSet | undefined, matrixClient: MatrixClient, historyPeriodDays: number, -): { isLoading: boolean } => { +): { + isLoading: boolean; + oldestEventTimestamp?: number; + loadTimelineHistory: () => Promise; + loadMorePolls?: () => Promise; +} => { const [isLoading, setIsLoading] = useState(true); + const [oldestEventTimestamp, setOldestEventTimestamp] = useState(undefined); + const [canPageBackward, setCanPageBackward] = useState(false); - useEffect(() => { + const loadTimelineHistory = useCallback(async () => { + const endOfHistoryPeriodTimestamp = Date.now() - ONE_DAY_MS * historyPeriodDays; + setIsLoading(true); + try { + const liveTimeline = timelineSet?.getLiveTimeline(); + const canPageBackward = !!liveTimeline?.getPaginationToken(Direction.Backward); + const oldestEventTimestamp = getOldestEventTimestamp(timelineSet); + + await fetchHistoryUntilTimestamp( + timelineSet, + matrixClient, + endOfHistoryPeriodTimestamp, + canPageBackward, + oldestEventTimestamp, + ); + + setCanPageBackward(!!timelineSet?.getLiveTimeline()?.getPaginationToken(EventTimeline.BACKWARDS)); + setOldestEventTimestamp(getOldestEventTimestamp(timelineSet)); + } catch (error) { + logger.error("Failed to fetch room polls history", error); + } finally { + setIsLoading(false); + } + }, [historyPeriodDays, timelineSet, matrixClient]); + + const loadMorePolls = useCallback(async () => { if (!timelineSet) { return; } - const endOfHistoryPeriodTimestamp = Date.now() - ONE_DAY_MS * historyPeriodDays; + setIsLoading(true); + try { + const result = await pagePollHistory(timelineSet, matrixClient); - const doFetchHistory = async (): Promise => { - setIsLoading(true); - try { - await pagePolls(timelineSet, matrixClient, endOfHistoryPeriodTimestamp); - } catch (error) { - logger.error("Failed to fetch room polls history", error); - } finally { - setIsLoading(false); - } - }; - doFetchHistory(); - }, [timelineSet, historyPeriodDays, matrixClient]); + setCanPageBackward(result.canPageBackward); + setOldestEventTimestamp(result.oldestEventTimestamp); + } catch (error) { + logger.error("Failed to fetch room polls history", error); + } finally { + setIsLoading(false); + } + }, [timelineSet, matrixClient]); - return { isLoading }; + return { + isLoading, + oldestEventTimestamp, + loadTimelineHistory, + loadMorePolls: canPageBackward ? loadMorePolls : undefined, + }; }; const filterDefinition: IFilterDefinition = { @@ -97,18 +176,24 @@ const filterDefinition: IFilterDefinition = { }; /** - * Fetch poll start events in the last N days of room history + * Fetches poll start events in the last N days of room history * @param room - room to fetch history for * @param matrixClient - client * @param historyPeriodDays - number of days of history to fetch, from current day * @returns isLoading - true while fetching history + * @returns oldestEventTimestamp - timestamp of oldest encountered poll, undefined when no polls found in timeline so far + * @returns loadMorePolls - function to page timeline backwards, undefined when timeline cannot be paged backwards */ export const useFetchPastPolls = ( room: Room, matrixClient: MatrixClient, historyPeriodDays = 30, -): { isLoading: boolean } => { - const [timelineSet, setTimelineSet] = useState(null); +): { + isLoading: boolean; + oldestEventTimestamp?: number; + loadMorePolls?: () => Promise; +} => { + const [timelineSet, setTimelineSet] = useState(undefined); useEffect(() => { const filter = new Filter(matrixClient.getSafeUserId()); @@ -123,7 +208,15 @@ export const useFetchPastPolls = ( getFilteredTimelineSet(); }, [room, matrixClient]); - const { isLoading } = useTimelineHistory(timelineSet, matrixClient, historyPeriodDays); + const { isLoading, oldestEventTimestamp, loadMorePolls, loadTimelineHistory } = useTimelineHistory( + timelineSet, + matrixClient, + historyPeriodDays, + ); - return { isLoading }; + useEffect(() => { + loadTimelineHistory(); + }, [loadTimelineHistory]); + + return { isLoading, oldestEventTimestamp, loadMorePolls }; }; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a2cacaf4c9..055f3c1d07 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -3143,8 +3143,15 @@ "Active polls": "Active polls", "Past polls": "Past polls", "Loading polls": "Loading polls", + "Load more polls": "Load more polls", "There are no active polls in this room": "There are no active polls in this room", "There are no past polls in this room": "There are no past polls in this room", + "There are no active polls. Load more polls to view polls for previous months": "There are no active polls. Load more polls to view polls for previous months", + "There are no past polls. Load more polls to view polls for previous months": "There are no past polls. Load more polls to view polls for previous months", + "There are no active polls for the past %(count)s days. Load more polls to view polls for previous months|other": "There are no active polls for the past %(count)s days. Load more polls to view polls for previous months", + "There are no active polls for the past %(count)s days. Load more polls to view polls for previous months|one": "There are no active polls for the past day. Load more polls to view polls for previous months", + "There are no past polls for the past %(count)s days. Load more polls to view polls for previous months|other": "There are no past polls for the past %(count)s days. Load more polls to view polls for previous months", + "There are no past polls for the past %(count)s days. Load more polls to view polls for previous months|one": "There are no past polls for the past day. Load more polls to view polls for previous months", "View poll": "View poll", "Send custom account data event": "Send custom account data event", "Send custom room account data event": "Send custom room account data event", diff --git a/test/components/views/dialogs/polls/PollHistoryDialog-test.tsx b/test/components/views/dialogs/polls/PollHistoryDialog-test.tsx index f1da27141c..d113d73ac5 100644 --- a/test/components/views/dialogs/polls/PollHistoryDialog-test.tsx +++ b/test/components/views/dialogs/polls/PollHistoryDialog-test.tsx @@ -176,12 +176,20 @@ describe("", () => { expect(mockClient.paginateEventTimeline).toHaveBeenCalledTimes(1); }); - it("displays loader and list while paging timeline", async () => { + it("renders a no polls message when there are no active polls in the room", async () => { + const { getByText } = getComponent(); + await flushPromises(); + + expect(getByText("There are no active polls in this room")).toBeTruthy(); + }); + + it("renders a no polls message and a load more button when not at end of timeline", async () => { const timelineSet = room.getOrCreateFilteredTimelineSet(expectedFilter); const liveTimeline = timelineSet.getLiveTimeline(); - const tenDaysAgoTs = now - 60000 * 60 * 24 * 10; + const fourtyDaysAgoTs = now - 60000 * 60 * 24 * 40; + const pollStart = makePollStartEvent("Question?", userId, undefined, { ts: fourtyDaysAgoTs, id: "1" }); - jest.spyOn(liveTimeline, "getEvents").mockReset().mockReturnValue([]); + jest.spyOn(liveTimeline, "getEvents").mockReset().mockReturnValueOnce([]).mockReturnValueOnce([pollStart]); // mock three pages of timeline history jest.spyOn(liveTimeline, "getPaginationToken") @@ -189,57 +197,24 @@ describe("", () => { .mockReturnValueOnce("test-pagination-token-2") .mockReturnValueOnce("test-pagination-token-3"); - // reference to pagination resolve, so we can assert between pages - let resolvePagination1: (value: boolean) => void | undefined; - let resolvePagination2: (value: boolean) => void | undefined; - mockClient.paginateEventTimeline - .mockImplementationOnce(async (_p) => { - const pollStart = makePollStartEvent("Question?", userId, undefined, { ts: now, id: "1" }); - jest.spyOn(liveTimeline, "getEvents").mockReturnValue([pollStart]); - room.processPollEvents([pollStart]); - return new Promise((resolve) => (resolvePagination1 = resolve)); - }) - .mockImplementationOnce(async (_p) => { - const pollStart = makePollStartEvent("Older question?", userId, undefined, { - ts: tenDaysAgoTs, - id: "2", - }); - jest.spyOn(liveTimeline, "getEvents").mockReturnValue([pollStart]); - room.processPollEvents([pollStart]); - return new Promise((resolve) => (resolvePagination2 = resolve)); - }); - - const { getByText, queryByText } = getComponent(); - + const { getByText } = getComponent(); await flushPromises(); expect(mockClient.paginateEventTimeline).toHaveBeenCalledTimes(1); - resolvePagination1!(true); + expect(getByText("There are no active polls. Load more polls to view polls for previous months")).toBeTruthy(); + + fireEvent.click(getByText("Load more polls")); + + // paged again + expect(mockClient.paginateEventTimeline).toHaveBeenCalledTimes(2); + // load more polls button still in UI, with loader + expect(getByText("Load more polls")).toMatchSnapshot(); + await flushPromises(); - // first page has results, display immediately - expect(getByText("Question?")).toBeInTheDocument(); - // but we are still fetching history, diaply loader - expect(getByText("Loading polls")).toBeInTheDocument(); - - resolvePagination2!(true); - await flushPromises(); - - // additional results addeds - expect(getByText("Older question?")).toBeInTheDocument(); - expect(getByText("Question?")).toBeInTheDocument(); - // finished paging - expect(queryByText("Loading polls")).not.toBeInTheDocument(); - - expect(mockClient.paginateEventTimeline).toHaveBeenCalledTimes(3); - }); - - it("renders a no polls message when there are no active polls in the room", async () => { - const { getByText } = getComponent(); - await flushPromises(); - - expect(getByText("There are no active polls in this room")).toBeTruthy(); + // no more spinner + expect(getByText("Load more polls")).toMatchSnapshot(); }); it("renders a no past polls message when there are no past polls in the room", async () => { diff --git a/test/components/views/dialogs/polls/__snapshots__/PollHistoryDialog-test.tsx.snap b/test/components/views/dialogs/polls/__snapshots__/PollHistoryDialog-test.tsx.snap index 76c76484fb..9c08631c2a 100644 --- a/test/components/views/dialogs/polls/__snapshots__/PollHistoryDialog-test.tsx.snap +++ b/test/components/views/dialogs/polls/__snapshots__/PollHistoryDialog-test.tsx.snap @@ -168,3 +168,32 @@ exports[` renders a list of active polls when there are pol />
`; + +exports[` renders a no polls message and a load more button when not at end of timeline 1`] = ` +