From 9494f77250cbb074af06b48e57fe917435c95ced Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Mon, 28 Aug 2023 11:32:54 -0400 Subject: [PATCH] revert --- .eslintrc.js | 187 +-- .github/workflows/cypress.yaml | 2 +- .github/workflows/i18n_check.yml | 6 +- __mocks__/languages.json | 10 +- cypress/e2e/audio-player/audio-player.spec.ts | 10 +- cypress/e2e/editing/editing.spec.ts | 4 +- cypress/e2e/login/login.spec.ts | 30 +- cypress/e2e/login/soft_logout.spec.ts | 143 ++ cypress/e2e/login/utils.ts | 49 + cypress/e2e/read-receipts/high-level.spec.ts | 292 +++- .../e2e/read-receipts/read-receipts.spec.ts | 3 +- cypress/e2e/register/email.spec.ts | 93 ++ .../general-user-settings-tab.spec.ts | 8 +- cypress/e2e/spotlight/spotlight.spec.ts | 12 +- cypress/e2e/threads/threads.spec.ts | 8 +- cypress/e2e/timeline/timeline.spec.ts | 22 +- cypress/e2e/widgets/stickers.spec.ts | 8 +- cypress/global.d.ts | 1 + cypress/plugins/docker/index.ts | 8 + cypress/plugins/index.ts | 2 + cypress/plugins/mailhog/index.ts | 91 ++ cypress/plugins/synapsedocker/index.ts | 69 +- .../templates/default/homeserver.yaml | 7 +- .../synapsedocker/templates/email/README.md | 1 + .../templates/email/homeserver.yaml | 44 + .../synapsedocker/templates/email/log.config | 50 + cypress/support/client.ts | 2 +- cypress/support/e2e.ts | 2 + cypress/support/homeserver.ts | 12 +- cypress/support/mailhog.ts | 54 + cypress/support/promise.ts | 58 + cypress/support/views.ts | 26 +- docs/settings.md | 2 +- jest.config.ts | 7 +- package.json | 37 +- res/css/_components.pcss | 8 +- res/css/components/views/utils/_Box.pcss | 27 + res/css/components/views/utils/_Flex.pcss | 23 + res/css/structures/_RightPanel.pcss | 4 - res/css/structures/_RoomStatusBar.pcss | 10 - res/css/structures/_SpaceHierarchy.pcss | 6 - res/css/structures/_SpacePanel.pcss | 18 +- res/css/structures/_SpaceRoomView.pcss | 4 - .../auth/_ConfirmSessionLockTheftView.pcss} | 20 +- .../auth/_SessionLockStolenView.pcss | 30 + res/css/views/avatars/_BaseAvatar.pcss | 50 +- .../views/avatars/_DecoratedRoomAvatar.pcss | 8 +- .../dialogs/_AddExistingToSpaceDialog.pcss | 14 +- res/css/views/dialogs/_CompoundDialog.pcss | 3 +- .../_ManageRestrictedJoinRuleDialog.pcss | 9 - .../views/dialogs/_RoomSettingsDialog.pcss | 4 + res/css/views/dialogs/_SpotlightDialog.pcss | 13 +- res/css/views/elements/_AccessibleButton.pcss | 12 +- res/css/views/elements/_FacePile.pcss | 6 +- .../elements/_GenericEventListSummary.pcss | 5 + res/css/views/elements/_LanguageDropdown.pcss | 21 + .../views/elements/_MiniAvatarUploader.pcss | 2 + res/css/views/elements/_TooltipButton.pcss | 51 - res/css/views/messages/_DateSeparator.pcss | 1 + .../views/right_panel/_RoomSummaryCard.pcss | 5 - res/css/views/right_panel/_UserInfo.pcss | 53 +- res/css/views/rooms/_EntityTile.pcss | 1 + res/css/views/rooms/_EventTile.pcss | 1 + res/css/views/rooms/_LegacyRoomHeader.pcss | 4 - res/css/views/rooms/_MemberInfo.pcss | 2 +- res/css/views/rooms/_RoomHeader.pcss | 11 +- res/css/views/rooms/_RoomPreviewCard.pcss | 7 - res/css/views/rooms/_WhoIsTypingTile.pcss | 4 - res/css/views/settings/_JoinRuleSettings.pcss | 5 - .../tabs/room/_NotificationSettingsTab.pcss | 4 + .../tabs/room/_PeopleRoomSettingsTab.pcss | 56 + res/img/element-icons/spaces.svg | 3 + res/img/feather-customised/check.svg | 2 +- res/img/feather-customised/x.svg | 2 +- .../legacy-light/css/_legacy-light.pcss | 16 +- .../css/_light-high-contrast.pcss | 16 +- res/themes/light/css/_light.pcss | 16 +- scripts/check-i18n.pl | 192 --- scripts/fix-i18n.pl | 114 -- scripts/fixup-imports.pl | 27 - sonar-project.properties | 4 +- src/@types/common.ts | 43 +- src/@types/global.d.ts | 1 + src/AddThreepid.ts | 6 +- src/AsyncWrapper.tsx | 4 +- src/ContentMessages.ts | 2 +- src/DateUtils.ts | 266 +-- src/HtmlUtils.tsx | 2 +- src/IdentityAuthClient.tsx | 6 +- src/LegacyCallHandler.tsx | 14 +- src/Lifecycle.ts | 60 +- src/Login.ts | 10 +- src/Markdown.ts | 23 +- src/MatrixClientPeg.ts | 4 +- src/Notifier.ts | 5 +- src/PosthogTrackers.ts | 2 + src/Registration.tsx | 4 +- src/RoomInvite.tsx | 3 +- src/SecurityManager.ts | 4 +- src/SlashCommands.tsx | 22 +- src/TextForEvent.tsx | 34 +- src/Unread.ts | 3 +- src/Views.ts | 6 + src/accessibility/KeyboardShortcutUtils.ts | 6 +- src/accessibility/KeyboardShortcuts.ts | 36 +- .../eventindex/DisableEventIndexDialog.tsx | 2 +- .../eventindex/ManageEventIndexDialog.tsx | 4 +- .../security/CreateKeyBackupDialog.tsx | 4 +- .../security/CreateSecretStorageDialog.tsx | 50 +- .../dialogs/security/ExportE2eKeysDialog.tsx | 15 +- .../dialogs/security/ImportE2eKeysDialog.tsx | 10 +- .../security/NewRecoveryMethodDialog.tsx | 7 +- .../security/RecoveryMethodRemovedDialog.tsx | 12 +- src/autocomplete/EmojiProvider.tsx | 6 +- src/autocomplete/NotifProvider.tsx | 2 +- src/autocomplete/RoomProvider.tsx | 2 +- src/autocomplete/UserProvider.tsx | 2 +- src/boundThreepids.ts | 3 +- src/components/structures/EmbeddedPage.tsx | 4 +- src/components/structures/HomePage.tsx | 8 +- src/components/structures/InteractiveAuth.tsx | 20 +- src/components/structures/MatrixChat.tsx | 109 +- src/components/structures/MessagePanel.tsx | 11 +- src/components/structures/RoomSearch.tsx | 9 +- src/components/structures/RoomSearchView.tsx | 4 +- src/components/structures/RoomStatusBar.tsx | 12 +- src/components/structures/RoomView.tsx | 2 +- src/components/structures/SpaceHierarchy.tsx | 32 +- src/components/structures/SpaceRoomView.tsx | 19 +- src/components/structures/TabbedView.tsx | 4 +- src/components/structures/ThreadPanel.tsx | 5 +- src/components/structures/ThreadView.tsx | 2 +- src/components/structures/TimelinePanel.tsx | 5 +- src/components/structures/UploadBar.tsx | 17 +- src/components/structures/UserMenu.tsx | 12 +- src/components/structures/ViewSource.tsx | 4 +- .../WaitingForThirdPartyRoomView.tsx | 3 +- .../auth/ConfirmSessionLockTheftView.tsx | 51 + .../structures/auth/ForgotPassword.tsx | 12 +- src/components/structures/auth/Login.tsx | 8 +- .../structures/auth/Registration.tsx | 42 +- .../structures/auth/SessionLockStolenView.tsx | 35 + .../structures/auth/SetupEncryptionBody.tsx | 25 +- src/components/structures/auth/SoftLogout.tsx | 55 +- .../auth/forgot-password/CheckEmail.tsx | 4 +- .../auth/forgot-password/VerifyEmailModal.tsx | 5 +- src/components/utils/Box.tsx | 103 ++ src/components/utils/Flex.tsx | 86 + src/components/views/auth/CountryDropdown.tsx | 54 +- src/components/views/auth/EmailField.tsx | 14 +- .../auth/InteractiveAuthEntryComponents.tsx | 17 +- src/components/views/auth/LoginWithQRFlow.tsx | 8 +- .../views/auth/PassphraseConfirmField.tsx | 8 +- src/components/views/auth/PassphraseField.tsx | 12 +- src/components/views/auth/PasswordLogin.tsx | 10 +- .../views/auth/RegistrationForm.tsx | 4 +- src/components/views/avatars/BaseAvatar.tsx | 150 +- .../views/avatars/DecoratedRoomAvatar.tsx | 7 +- src/components/views/avatars/MemberAvatar.tsx | 15 +- src/components/views/avatars/RoomAvatar.tsx | 17 +- .../views/avatars/SearchResultAvatar.tsx | 7 +- src/components/views/avatars/WidgetAvatar.tsx | 10 +- .../views/beacon/BeaconListItem.tsx | 5 +- src/components/views/beacon/BeaconMarker.tsx | 3 +- .../views/beacon/BeaconStatusTooltip.tsx | 3 +- .../views/beacon/BeaconViewDialog.tsx | 2 +- .../views/beacon/DialogOwnBeaconStatus.tsx | 10 +- .../views/beacon/OwnBeaconStatus.tsx | 6 +- .../views/beacon/RoomCallBanner.tsx | 2 +- .../views/beacon/RoomLiveShareWarning.tsx | 2 +- .../views/beacon/ShareLatestLocation.tsx | 4 +- src/components/views/beacon/displayStatus.ts | 4 +- src/components/views/beta/BetaCard.tsx | 4 +- .../views/context_menus/DeviceContextMenu.tsx | 4 +- .../context_menus/MessageContextMenu.tsx | 22 +- .../views/context_menus/RoomContextMenu.tsx | 10 +- .../context_menus/RoomGeneralContextMenu.tsx | 6 +- .../views/context_menus/SpaceContextMenu.tsx | 8 +- .../views/context_menus/WidgetContextMenu.tsx | 7 +- .../dialogs/AddExistingToSpaceDialog.tsx | 22 +- .../dialogs/AnalyticsLearnMoreDialog.tsx | 4 +- .../views/dialogs/AskInviteAnywayDialog.tsx | 2 +- .../views/dialogs/BugReportDialog.tsx | 10 +- .../views/dialogs/BulkRedactDialog.tsx | 10 +- .../CantStartVoiceMessageBroadcastDialog.tsx | 3 +- .../views/dialogs/ChangelogDialog.tsx | 2 +- .../dialogs/ConfirmAndWaitRedactDialog.tsx | 2 +- .../views/dialogs/ConfirmRedactDialog.tsx | 4 +- .../views/dialogs/ConfirmUserActionDialog.tsx | 2 +- .../views/dialogs/ConfirmWipeDeviceDialog.tsx | 5 +- .../views/dialogs/CreateRoomDialog.tsx | 13 +- .../views/dialogs/CreateSubspaceDialog.tsx | 4 +- .../views/dialogs/CryptoStoreTooNewDialog.tsx | 14 +- .../views/dialogs/DeactivateAccountDialog.tsx | 4 +- .../views/dialogs/DevtoolsDialog.tsx | 10 +- .../views/dialogs/EndPollDialog.tsx | 4 +- src/components/views/dialogs/ErrorDialog.tsx | 4 +- src/components/views/dialogs/ExportDialog.tsx | 8 +- .../views/dialogs/FeedbackDialog.tsx | 10 +- .../views/dialogs/ForwardDialog.tsx | 33 +- .../dialogs/GenericFeatureFeedbackDialog.tsx | 2 +- .../views/dialogs/IncomingSasDialog.tsx | 23 +- src/components/views/dialogs/InfoDialog.tsx | 2 +- .../dialogs/IntegrationsDisabledDialog.tsx | 4 +- .../dialogs/IntegrationsImpossibleDialog.tsx | 5 +- .../views/dialogs/InteractiveAuthDialog.tsx | 9 +- src/components/views/dialogs/InviteDialog.tsx | 43 +- .../KeySignatureUploadFailedDialog.tsx | 2 +- .../dialogs/LazyLoadingDisabledDialog.tsx | 9 +- .../views/dialogs/LazyLoadingResyncDialog.tsx | 6 +- .../views/dialogs/LeaveSpaceDialog.tsx | 7 +- src/components/views/dialogs/LogoutDialog.tsx | 12 +- .../ManageRestrictedJoinRuleDialog.tsx | 15 +- .../views/dialogs/ModuleUiDialog.tsx | 38 +- .../views/dialogs/QuestionDialog.tsx | 2 +- .../dialogs/RegistrationEmailPromptDialog.tsx | 5 +- .../views/dialogs/ReportEventDialog.tsx | 36 +- .../views/dialogs/RoomSettingsDialog.tsx | 20 +- .../views/dialogs/RoomUpgradeDialog.tsx | 10 +- .../dialogs/RoomUpgradeWarningDialog.tsx | 16 +- .../views/dialogs/ScrollableBaseModal.tsx | 3 +- .../views/dialogs/ServerOfflineDialog.tsx | 7 +- .../views/dialogs/ServerPickerDialog.tsx | 6 +- .../views/dialogs/SeshatResetDialog.tsx | 6 +- .../dialogs/SessionRestoreErrorDialog.tsx | 11 +- .../views/dialogs/SetEmailDialog.tsx | 16 +- .../dialogs/SlidingSyncOptionsDialog.tsx | 2 +- .../views/dialogs/SpacePreferencesDialog.tsx | 7 +- .../views/dialogs/StorageEvictedDialog.tsx | 6 +- src/components/views/dialogs/TermsDialog.tsx | 4 +- .../views/dialogs/TextInputDialog.tsx | 6 +- .../views/dialogs/UntrustedDeviceDialog.tsx | 2 +- .../views/dialogs/UploadConfirmDialog.tsx | 2 +- .../views/dialogs/UploadFailureDialog.tsx | 11 +- .../views/dialogs/UserSettingsDialog.tsx | 6 +- .../dialogs/WidgetOpenIDPermissionsDialog.tsx | 2 +- .../views/dialogs/devtools/BaseTool.tsx | 2 +- .../views/dialogs/devtools/Event.tsx | 6 +- .../dialogs/devtools/RoomNotifications.tsx | 33 +- .../views/dialogs/devtools/RoomState.tsx | 7 +- .../dialogs/devtools/VerificationExplorer.tsx | 6 +- .../views/dialogs/oidc/OidcLogoutDialog.tsx | 74 + .../security/AccessSecretStorageDialog.tsx | 16 +- .../ConfirmDestroyCrossSigningDialog.tsx | 7 +- .../security/CreateCrossSigningDialog.tsx | 7 +- .../security/RestoreKeyBackupDialog.tsx | 31 +- .../views/dialogs/spotlight/Filter.ts | 21 + .../dialogs/spotlight/SpotlightDialog.tsx | 143 +- .../views/directory/NetworkDropdown.tsx | 2 +- .../views/elements/AccessibleButton.tsx | 4 +- .../views/elements/AppPermission.tsx | 6 +- src/components/views/elements/AppTile.tsx | 6 +- .../views/elements/CopyableText.tsx | 2 +- .../elements/DesktopCapturerSourcePicker.tsx | 10 +- .../views/elements/DialogButtons.tsx | 2 +- src/components/views/elements/Dropdown.tsx | 2 +- .../views/elements/EditableItemList.tsx | 13 +- .../views/elements/ErrorBoundary.tsx | 12 +- src/components/views/elements/FacePile.tsx | 14 +- .../elements/GenericEventListSummary.tsx | 4 +- src/components/views/elements/ImageView.tsx | 15 +- .../views/elements/InlineSpinner.tsx | 2 +- .../views/elements/LanguageDropdown.tsx | 31 +- src/components/views/elements/LearnMore.tsx | 4 +- .../views/elements/MiniAvatarUploader.tsx | 2 +- src/components/views/elements/Pill.tsx | 10 +- .../views/elements/PollCreateDialog.tsx | 13 +- src/components/views/elements/ReplyChain.tsx | 3 +- .../views/elements/RoomFacePile.tsx | 8 +- src/components/views/elements/SSOButtons.tsx | 14 +- .../views/elements/ServerPicker.tsx | 16 +- .../views/elements/SettingsFlag.tsx | 9 +- .../elements/SpellCheckLanguagesDropdown.tsx | 13 +- src/components/views/elements/Spinner.tsx | 2 +- src/components/views/elements/TagComposer.tsx | 2 +- .../views/elements/TooltipButton.tsx | 43 - .../views/elements/UseCaseSelection.tsx | 2 +- src/components/views/emojipicker/Category.tsx | 2 +- src/components/views/emojipicker/Emoji.tsx | 9 +- .../views/emojipicker/EmojiPicker.tsx | 13 +- src/components/views/emojipicker/Preview.tsx | 5 +- .../views/emojipicker/QuickReactions.tsx | 2 +- src/components/views/emojipicker/Search.tsx | 2 +- .../views/location/EnableLiveShare.tsx | 7 +- src/components/views/location/MapError.tsx | 2 +- src/components/views/location/Marker.tsx | 3 +- .../views/location/ShareDialogButtons.tsx | 4 +- src/components/views/location/ShareType.tsx | 8 +- src/components/views/location/ZoomButtons.tsx | 4 +- .../views/location/shareLocation.ts | 28 +- src/components/views/messages/CallEvent.tsx | 15 +- .../views/messages/DateSeparator.tsx | 28 +- .../views/messages/DisambiguatedProfile.tsx | 2 +- .../views/messages/DownloadActionButton.tsx | 6 +- .../views/messages/EditHistoryMessage.tsx | 4 +- .../views/messages/EncryptionEvent.tsx | 10 +- .../views/messages/LegacyCallEvent.tsx | 8 +- src/components/views/messages/MBeaconBody.tsx | 6 +- src/components/views/messages/MFileBody.tsx | 10 +- src/components/views/messages/MImageBody.tsx | 8 +- .../messages/MKeyVerificationRequest.tsx | 4 +- src/components/views/messages/MPollBody.tsx | 12 +- .../views/messages/MPollEndBody.tsx | 3 +- .../views/messages/MessageActionBar.tsx | 22 +- .../views/messages/MessageEvent.tsx | 13 +- .../views/messages/ReactionsRow.tsx | 2 +- .../views/messages/RoomAvatarEvent.tsx | 2 +- .../views/messages/RoomPredecessorTile.tsx | 7 +- src/components/views/messages/TextualBody.tsx | 6 +- .../views/messages/TileErrorBoundary.tsx | 2 +- src/components/views/pips/WidgetPip.tsx | 6 +- .../views/polls/pollHistory/fetchPastPolls.ts | 2 +- src/components/views/right_panel/BaseCard.tsx | 4 +- .../views/right_panel/EncryptionInfo.tsx | 6 +- .../right_panel/LegacyRoomHeaderButtons.tsx | 2 +- .../views/right_panel/PinnedMessagesCard.tsx | 3 +- .../views/right_panel/RoomSummaryCard.tsx | 14 +- src/components/views/right_panel/UserInfo.tsx | 50 +- .../views/right_panel/VerificationPanel.tsx | 16 +- .../views/right_panel/WidgetCard.tsx | 2 +- .../views/room_settings/AliasSettings.tsx | 20 +- .../room_settings/RoomProfileSettings.tsx | 4 +- .../room_settings/UrlPreviewSettings.tsx | 7 +- .../views/rooms/BasicMessageComposer.tsx | 2 +- src/components/views/rooms/E2EIcon.tsx | 8 +- .../views/rooms/EditMessageComposer.tsx | 4 +- src/components/views/rooms/EntityTile.tsx | 10 +- src/components/views/rooms/EventTile.tsx | 21 +- .../views/rooms/LegacyRoomHeader.tsx | 6 +- .../views/rooms/LinkPreviewGroup.tsx | 2 +- .../views/rooms/LiveContentSummary.tsx | 2 +- src/components/views/rooms/MemberList.tsx | 9 +- src/components/views/rooms/MemberTile.tsx | 2 +- .../views/rooms/MessageComposerButtons.tsx | 7 +- .../views/rooms/MessageComposerFormatBar.tsx | 2 +- src/components/views/rooms/NewRoomIntro.tsx | 15 +- .../views/rooms/PinnedEventTile.tsx | 7 +- src/components/views/rooms/PresenceLabel.tsx | 2 +- .../views/rooms/ReadReceiptGroup.tsx | 3 +- .../views/rooms/ReadReceiptMarker.tsx | 4 +- src/components/views/rooms/ReplyTile.tsx | 2 +- .../views/rooms/RoomBreadcrumbs.tsx | 2 +- src/components/views/rooms/RoomHeader.tsx | 141 +- src/components/views/rooms/RoomList.tsx | 18 +- src/components/views/rooms/RoomListHeader.tsx | 4 +- src/components/views/rooms/RoomPreviewBar.tsx | 20 +- .../views/rooms/RoomPreviewCard.tsx | 16 +- src/components/views/rooms/RoomSublist.tsx | 2 +- src/components/views/rooms/RoomTile.tsx | 2 +- .../views/rooms/RoomTileCallSummary.tsx | 2 +- .../views/rooms/RoomUpgradeWarningBar.tsx | 10 +- src/components/views/rooms/SearchBar.tsx | 4 +- .../views/rooms/SendMessageComposer.tsx | 5 +- src/components/views/rooms/Stickerpicker.tsx | 4 +- .../views/rooms/ThirdPartyMemberInfo.tsx | 11 +- src/components/views/rooms/ThreadSummary.tsx | 3 +- .../views/rooms/VoiceRecordComposerTile.tsx | 2 +- .../views/rooms/WhoIsTypingTile.tsx | 3 +- .../DynamicImportWysiwygComposer.tsx | 2 +- .../components/EditionButtons.tsx | 4 +- .../components/FormattingButtons.tsx | 2 +- .../wysiwyg_composer/components/LinkModal.tsx | 4 +- .../components/WysiwygAutocomplete.tsx | 13 +- .../wysiwyg_composer/hooks/useEditing.ts | 2 +- .../wysiwyg_composer/hooks/useSuggestion.ts | 18 +- .../views/settings/AddPrivilegedUsers.tsx | 6 +- .../views/settings/AvatarSetting.tsx | 4 +- src/components/views/settings/BridgeTile.tsx | 4 +- .../views/settings/CrossSigningPanel.tsx | 5 +- .../views/settings/EventIndexPanel.tsx | 16 +- .../views/settings/JoinRuleSettings.tsx | 11 +- .../views/settings/LayoutSwitcher.tsx | 4 +- .../views/settings/Notifications.tsx | 13 +- .../views/settings/ProfileSettings.tsx | 4 +- .../views/settings/SecureBackupPanel.tsx | 13 +- src/components/views/settings/SetIdServer.tsx | 36 +- .../views/settings/SetIntegrationManager.tsx | 5 +- .../views/settings/SpellCheckSettings.tsx | 4 +- .../views/settings/ThemeChoicePanel.tsx | 2 +- .../views/settings/account/EmailAddresses.tsx | 36 +- .../views/settings/account/PhoneNumbers.tsx | 48 +- .../settings/devices/CurrentDeviceSection.tsx | 6 +- .../settings/devices/DeviceDetailHeading.tsx | 10 +- .../views/settings/devices/DeviceDetails.tsx | 5 +- .../devices/DeviceSecurityLearnMore.tsx | 9 +- .../settings/devices/FilteredDeviceList.tsx | 86 +- .../devices/FilteredDeviceListHeader.tsx | 24 +- .../settings/devices/LoginWithQRSection.tsx | 3 +- .../devices/OtherSessionsSectionHeading.tsx | 36 +- .../devices/SecurityRecommendations.tsx | 6 +- .../views/settings/devices/deleteDevices.tsx | 2 +- .../views/settings/devices/useOwnDevices.ts | 2 +- .../settings/discovery/EmailAddresses.tsx | 8 +- .../views/settings/discovery/PhoneNumbers.tsx | 8 +- .../NotificationPusherSettings.tsx | 3 +- .../notifications/NotificationSettings2.tsx | 6 +- .../tabs/room/AdvancedRoomSettingsTab.tsx | 4 +- .../settings/tabs/room/BridgeSettingsTab.tsx | 4 +- .../tabs/room/GeneralRoomSettingsTab.tsx | 4 +- .../tabs/room/NotificationSettingsTab.tsx | 7 +- .../tabs/room/PeopleRoomSettingsTab.tsx | 173 ++ .../tabs/room/RolesRoomSettingsTab.tsx | 19 +- .../tabs/room/SecurityRoomSettingsTab.tsx | 27 +- .../tabs/room/VoipRoomSettingsTab.tsx | 7 +- .../tabs/user/GeneralUserSettingsTab.tsx | 49 +- .../tabs/user/HelpUserSettingsTab.tsx | 28 +- .../tabs/user/LabsUserSettingsTab.tsx | 10 +- .../tabs/user/MjolnirUserSettingsTab.tsx | 19 +- .../tabs/user/SecurityUserSettingsTab.tsx | 9 +- .../settings/tabs/user/SessionManagerTab.tsx | 78 +- .../tabs/user/SidebarUserSettingsTab.tsx | 9 +- .../views/spaces/QuickSettingsButton.tsx | 6 +- .../views/spaces/QuickThemeSwitcher.tsx | 2 +- .../views/spaces/SpaceBasicSettings.tsx | 8 +- .../views/spaces/SpaceChildrenPicker.tsx | 2 +- .../views/spaces/SpaceCreateMenu.tsx | 53 +- src/components/views/spaces/SpacePanel.tsx | 19 +- .../views/spaces/SpaceSettingsGeneralTab.tsx | 2 +- .../spaces/SpaceSettingsVisibilityTab.tsx | 4 +- .../views/spaces/SpaceTreeLevel.tsx | 12 +- .../views/terms/InlineTermsAgreement.tsx | 4 +- .../views/toasts/VerificationRequestToast.tsx | 4 +- .../user-onboarding/UserOnboardingHeader.tsx | 12 +- .../verification/VerificationCancelled.tsx | 6 +- .../verification/VerificationComplete.tsx | 3 +- .../verification/VerificationShowSas.tsx | 134 +- src/components/views/voip/CallView.tsx | 6 +- src/components/views/voip/LegacyCallView.tsx | 4 +- .../LegacyCallView/LegacyCallViewHeader.tsx | 8 +- src/components/views/voip/VideoFeed.tsx | 8 +- src/createRoom.ts | 3 +- src/dispatcher/actions.ts | 5 + src/dispatcher/payloads/JoinRoomPayload.ts | 2 +- .../payloads/OpenSpotlightPayload.ts | 26 + .../payloads/SubmitAskToJoinPayload.ts | 2 +- src/editor/commands.tsx | 3 +- src/effects/effect.ts | 6 +- src/emoji.ts | 122 -- src/events/EventTileFactory.tsx | 12 +- src/events/forward/getForwardableEvent.ts | 4 +- .../location/getShareableLocationEvent.ts | 3 +- src/hooks/room/useRoomCallStatus.ts | 161 ++ src/hooks/room/useRoomThreadNotifications.ts | 67 + src/hooks/room/useTopic.ts | 17 +- src/hooks/spotlight/useDebouncedCallback.ts | 2 +- src/hooks/useGlobalNotificationState.ts | 44 + src/hooks/usePermalink.ts | 4 +- src/hooks/usePublicRoomDirectory.ts | 3 +- src/hooks/useRoomMembers.ts | 33 +- src/hooks/useSpaceResults.ts | 7 +- src/hooks/useThreepids.ts | 3 +- src/i18n/strings/ar.json | 227 +-- src/i18n/strings/az.json | 56 +- src/i18n/strings/be.json | 24 +- src/i18n/strings/bg.json | 473 +++--- src/i18n/strings/bs.json | 8 +- src/i18n/strings/ca.json | 279 ++-- src/i18n/strings/cs.json | 945 ++++++----- src/i18n/strings/cy.json | 8 +- src/i18n/strings/da.json | 126 +- src/i18n/strings/de_DE.json | 951 ++++++----- src/i18n/strings/el.json | 782 +++++---- src/i18n/strings/en_EN.json | 1207 ++++++-------- src/i18n/strings/en_US.json | 127 +- src/i18n/strings/eo.json | 663 +++++--- src/i18n/strings/es.json | 888 ++++++---- src/i18n/strings/et.json | 938 ++++++----- src/i18n/strings/eu.json | 437 +++-- src/i18n/strings/fa.json | 547 ++++--- src/i18n/strings/fi.json | 867 ++++++---- src/i18n/strings/fr.json | 940 ++++++----- src/i18n/strings/ga.json | 238 +-- src/i18n/strings/gl.json | 824 ++++++---- src/i18n/strings/he.json | 609 ++++--- src/i18n/strings/hi.json | 86 +- src/i18n/strings/hr.json | 22 +- src/i18n/strings/hu.json | 914 ++++++----- src/i18n/strings/id.json | 945 ++++++----- src/i18n/strings/is.json | 816 ++++++---- src/i18n/strings/it.json | 945 ++++++----- src/i18n/strings/ja.json | 870 ++++++---- src/i18n/strings/jbo.json | 116 +- src/i18n/strings/ka.json | 49 +- src/i18n/strings/kab.json | 449 +++--- src/i18n/strings/ko.json | 379 +++-- src/i18n/strings/lo.json | 771 +++++---- src/i18n/strings/lt.json | 633 +++++--- src/i18n/strings/lv.json | 476 +++--- src/i18n/strings/ml.json | 46 +- src/i18n/strings/mn.json | 6 +- src/i18n/strings/nb_NO.json | 427 +++-- src/i18n/strings/ne.json | 6 +- src/i18n/strings/nl.json | 832 ++++++---- src/i18n/strings/nn.json | 360 +++-- src/i18n/strings/oc.json | 161 +- src/i18n/strings/pl.json | 932 ++++++----- src/i18n/strings/pt.json | 145 +- src/i18n/strings/pt_BR.json | 680 +++++--- src/i18n/strings/ro.json | 18 +- src/i18n/strings/ru.json | 866 ++++++---- src/i18n/strings/si.json | 10 +- src/i18n/strings/sk.json | 948 ++++++----- src/i18n/strings/sl.json | 20 +- src/i18n/strings/sq.json | 908 ++++++----- src/i18n/strings/sr.json | 342 ++-- src/i18n/strings/sr_Latn.json | 18 +- src/i18n/strings/sv.json | 937 ++++++----- src/i18n/strings/ta.json | 48 +- src/i18n/strings/te.json | 40 +- src/i18n/strings/th.json | 179 ++- src/i18n/strings/tr.json | 453 +++--- src/i18n/strings/tzm.json | 75 +- src/i18n/strings/uk.json | 982 +++++++----- src/i18n/strings/vi.json | 903 +++++++---- src/i18n/strings/vls.json | 321 ++-- src/i18n/strings/zh_Hans.json | 854 ++++++---- src/i18n/strings/zh_Hant.json | 940 ++++++----- src/languageHandler.tsx | 76 +- src/modules/ProxiedModuleApi.ts | 19 +- .../VectorPushRulesDefinitions.ts | 6 +- src/phonenumber.ts | 253 +-- src/settings/Settings.tsx | 114 +- src/settings/SettingsStore.ts | 2 +- src/slash-commands/command.ts | 6 +- src/stores/OwnBeaconStore.ts | 14 +- src/stores/OwnProfileStore.ts | 1 + src/stores/RoomViewStore.tsx | 4 +- .../right-panel/RightPanelStorePhases.ts | 2 +- src/stores/room-list/MessagePreviewStore.ts | 3 +- .../previews/PollStartEventPreview.ts | 3 +- src/stores/spaces/SpaceStore.ts | 2 +- src/stores/spaces/index.ts | 11 +- src/theme.ts | 4 +- src/toasts/AnalyticsToast.tsx | 13 +- src/toasts/DesktopNotificationsToast.ts | 4 +- src/toasts/IncomingCallToast.tsx | 10 +- src/toasts/IncomingLegacyCallToast.tsx | 6 +- src/toasts/MobileGuideToast.ts | 5 +- src/toasts/ServerLimitToast.tsx | 2 +- src/toasts/SetupEncryptionToast.ts | 6 +- src/toasts/UnverifiedSessionToast.tsx | 2 +- src/toasts/UpdateToast.tsx | 6 +- src/utils/AutoDiscoveryUtils.tsx | 29 +- src/utils/ErrorUtils.tsx | 11 +- src/utils/EventRenderingUtils.ts | 13 +- src/utils/EventUtils.ts | 7 +- src/utils/FileUtils.ts | 2 +- src/utils/FormattingUtils.ts | 28 +- src/utils/MultiInviter.ts | 3 +- src/utils/PasswordScorer.ts | 7 +- src/utils/PinningUtils.ts | 3 +- src/utils/Reply.ts | 13 +- src/utils/SessionLock.ts | 261 +++ src/utils/UserInteractiveAuth.ts | 2 +- src/utils/ValidatedServerConfig.ts | 3 +- src/utils/beacon/duration.ts | 5 +- src/utils/beacon/timeline.ts | 3 +- src/utils/exportUtils/Exporter.ts | 99 +- src/utils/exportUtils/HtmlExport.tsx | 9 +- src/utils/i18n-helpers.ts | 12 +- src/utils/leave-behaviour.ts | 3 +- src/utils/location/LocationShareErrors.ts | 3 +- src/utils/location/isSelfLocation.ts | 2 +- src/utils/location/locationEventGeoUri.ts | 3 +- src/utils/location/map.ts | 7 +- src/utils/location/positionFailureMessage.ts | 3 +- src/utils/notifications.ts | 4 +- src/utils/oidc/authorize.ts | 2 +- src/utils/oidc/getDelegatedAuthAccountUrl.ts | 27 + src/utils/oidc/getOidcLogoutUrl.ts | 28 + .../components/atoms/VoiceBroadcastHeader.tsx | 2 +- .../ConfirmListenBroadcastStopCurrent.tsx | 5 +- .../hooks/useVoiceBroadcastRecording.tsx | 3 +- .../checkVoiceBroadcastPreConditions.tsx | 9 +- .../utils/showCantStartACallDialog.tsx | 3 +- src/widgets/CapabilityText.tsx | 9 +- test/DeviceListener-test.ts | 19 +- test/Reply-test.ts | 13 +- test/TextForEvent-test.ts | 45 +- test/Unread-test.ts | 3 +- test/audio/VoiceRecording-test.ts | 2 + .../components/structures/MatrixChat-test.tsx | 266 ++- .../structures/MessagePanel-test.tsx | 3 +- .../structures/PipContainer-test.tsx | 5 +- test/components/structures/RoomView-test.tsx | 2 +- .../structures/SpaceHierarchy-test.tsx | 45 +- .../components/structures/TabbedView-test.tsx | 17 +- .../structures/TimelinePanel-test.tsx | 18 +- test/components/structures/UploadBar-test.tsx | 52 + .../components/structures/ViewSource-test.tsx | 28 +- .../__snapshots__/MatrixChat-test.tsx.snap | 177 +- .../__snapshots__/MessagePanel-test.tsx.snap | 31 +- .../__snapshots__/RoomView-test.tsx.snap | 176 +- .../SpaceHierarchy-test.tsx.snap | 92 +- .../__snapshots__/UserMenu-test.tsx.snap | 23 +- .../components/structures/auth/Login-test.tsx | 10 +- .../structures/auth/Registration-test.tsx | 1 - .../views/VerificationShowSas-test.tsx | 31 + .../views/auth/CountryDropdown-test.tsx | 17 +- .../views/avatars/MemberAvatar-test.tsx | 2 +- .../views/avatars/RoomAvatar-test.tsx | 3 - .../__snapshots__/RoomAvatar-test.tsx.snap | 69 +- .../views/beacon/BeaconListItem-test.tsx | 3 +- .../__snapshots__/BeaconMarker-test.tsx.snap | 27 +- .../BeaconViewDialog-test.tsx.snap | 25 +- .../__snapshots__/DialogSidebar-test.tsx.snap | 15 +- test/components/views/beta/BetaCard-test.tsx | 11 +- .../context_menus/MessageContextMenu-test.tsx | 2 +- .../context_menus/RoomContextMenu-test.tsx | 13 + .../RoomGeneralContextMenu-test.tsx | 3 +- .../context_menus/WidgetContextMenu-test.tsx | 8 +- .../dialogs/ConfirmUserActionDialog-test.tsx | 35 + .../views/dialogs/ForwardDialog-test.tsx | 14 +- .../dialogs/MessageEditHistoryDialog-test.tsx | 2 +- .../views/dialogs/RoomSettingsDialog-test.tsx | 58 +- .../views/dialogs/SpotlightDialog-test.tsx | 25 +- .../ConfirmUserActionDialog-test.tsx.snap | 95 ++ ...nageRestrictedJoinRuleDialog-test.tsx.snap | 23 +- .../MessageEditHistoryDialog-test.tsx.snap | 14 +- .../views/elements/FacePile-test.tsx | 31 + .../views/elements/PollCreateDialog-test.tsx | 11 +- .../SpellCheckLanguagesDropdown-test.tsx | 38 + .../__snapshots__/AppTile-test.tsx.snap | 104 +- .../__snapshots__/FacePile-test.tsx.snap | 26 + .../elements/__snapshots__/Pill-test.tsx.snap | 186 +-- .../SpellCheckLanguagesDropdown-test.tsx.snap | 32 + .../views/location/LocationShareMenu-test.tsx | 3 +- .../location/LocationViewDialog-test.tsx | 5 +- .../components/views/location/Marker-test.tsx | 2 +- .../views/location/shareLocation-test.ts | 14 +- .../views/messages/CallEvent-test.tsx | 8 +- .../views/messages/DateSeparator-test.tsx | 8 +- .../views/messages/MBeaconBody-test.tsx | 2 +- .../views/messages/MImageBody-test.tsx | 26 + .../views/messages/MLocationBody-test.tsx | 3 +- .../views/messages/MPollBody-test.tsx | 7 +- .../views/messages/MPollEndBody-test.tsx | 3 +- .../views/messages/TextualBody-test.tsx | 4 +- .../__snapshots__/DateSeparator-test.tsx.snap | 8 +- .../__snapshots__/MLocationBody-test.tsx.snap | 25 +- .../__snapshots__/TextualBody-test.tsx.snap | 164 +- .../polls/pollHistory/PollHistory-test.tsx | 15 +- .../pollHistory/PollListItemEnded-test.tsx | 3 +- .../LegacyRoomHeaderButtons-test.tsx | 2 +- .../right_panel/PinnedMessagesCard-test.tsx | 2 +- .../RoomSummaryCard-test.tsx.snap | 23 +- .../__snapshots__/UserInfo-test.tsx.snap | 28 +- .../views/rooms/LegacyRoomHeader-test.tsx | 40 +- .../UnreadNotificationBadge-test.tsx | 2 +- .../views/rooms/PresenceLabel-test.tsx | 35 + .../views/rooms/RoomHeader-test.tsx | 229 ++- test/components/views/rooms/RoomTile-test.tsx | 4 +- .../views/rooms/SearchResultTile-test.tsx | 2 +- .../PinnedEventTile-test.tsx.snap | 25 +- .../RoomPreviewBar-test.tsx.snap | 115 +- .../__snapshots__/RoomTile-test.tsx.snap | 92 +- .../components/FormattingButtons-test.tsx | 4 + .../views/settings/Notifications-test.tsx | 3 +- .../settings/devices/LoginWithQR-test.tsx | 2 +- .../discovery/EmailAddresses-test.tsx | 7 +- .../settings/discovery/PhoneNumbers-test.tsx | 2 +- .../notifications/Notifications2-test.tsx | 11 +- .../tabs/room/PeopleRoomSettingsTab-test.tsx | 217 +++ .../PeopleRoomSettingsTab-test.tsx.snap | 163 ++ .../tabs/user/GeneralUserSettingsTab-test.tsx | 203 ++- .../tabs/user/LabsUserSettingsTab-test.tsx | 8 +- .../tabs/user/SessionManagerTab-test.tsx | 175 +- .../tabs/user/SidebarUserSettingsTab-test.tsx | 2 +- .../GeneralUserSettingsTab-test.tsx.snap | 175 ++ .../SessionManagerTab-test.tsx.snap | 15 + .../spaces/AddExistingToSpaceDialog-test.tsx | 23 +- .../views/spaces/QuickSettingsButton-test.tsx | 5 + .../views/spaces/SpacePanel-test.tsx | 7 +- .../views/spaces/SpaceTreeLevel-test.tsx | 19 +- .../AddExistingToSpaceDialog-test.tsx.snap | 27 +- .../QuickSettingsButton-test.tsx.snap | 15 + .../UserOnboardingPage-test.tsx | 4 +- test/components/views/voip/CallView-test.tsx | 10 +- test/editor/serialize-test.ts | 23 + test/i18n/languages.json | 5 +- test/languageHandler-test.ts | 121 -- test/{i18n-test => }/languageHandler-test.tsx | 169 +- test/modules/ProxiedModuleApi-test.ts | 105 -- test/modules/ProxiedModuleApi-test.tsx | 257 +++ ...erSupportUnstableFeatureController-test.ts | 3 +- test/setup/setupLanguage.ts | 60 +- test/setup/setupManualMocks.ts | 13 +- test/stores/OwnBeaconStore-test.ts | 18 +- .../room-list/algorithms/Algorithm-test.ts | 9 +- test/test-utils/beacon.ts | 19 +- test/test-utils/location.ts | 8 +- test/test-utils/oidc.ts | 2 +- test/test-utils/poll.ts | 8 +- test/test-utils/test-utils.ts | 3 +- test/test-utils/utilities.ts | 69 + test/utils/AutoDiscoveryUtils-test.tsx | 25 +- test/utils/DateUtils-test.ts | 244 ++- test/utils/EventUtils-test.ts | 2 +- test/utils/SessionLock-test.ts | 252 +++ .../AutoDiscoveryUtils-test.tsx.snap | 26 + test/utils/beacon/duration-test.ts | 3 +- test/utils/exportUtils/HTMLExport-test.ts | 68 +- .../utils/exportUtils/PlainTextExport-test.ts | 4 +- .../__snapshots__/HTMLExport-test.ts.snap | 8 +- test/utils/i18n-helpers-test.ts | 62 + test/utils/location/isSelfLocation-test.ts | 10 +- .../media/requestMediaPermissions-test.tsx | 3 +- test/utils/notifications-test.ts | 3 +- .../oidc/getDelegatedAuthAccountUrl-test.ts | 61 + test/utils/threepids-test.ts | 3 +- yarn.lock | 1422 ++++++++++------- 711 files changed, 29268 insertions(+), 18785 deletions(-) create mode 100644 cypress/e2e/login/soft_logout.spec.ts create mode 100644 cypress/e2e/login/utils.ts create mode 100644 cypress/e2e/register/email.spec.ts create mode 100644 cypress/plugins/mailhog/index.ts create mode 100644 cypress/plugins/synapsedocker/templates/email/README.md create mode 100644 cypress/plugins/synapsedocker/templates/email/homeserver.yaml create mode 100644 cypress/plugins/synapsedocker/templates/email/log.config create mode 100644 cypress/support/mailhog.ts create mode 100644 cypress/support/promise.ts create mode 100644 res/css/components/views/utils/_Box.pcss create mode 100644 res/css/components/views/utils/_Flex.pcss rename res/css/{voice-broadcast/atoms/_PlaybackControlButton.pcss => structures/auth/_ConfirmSessionLockTheftView.pcss} (70%) create mode 100644 res/css/structures/auth/_SessionLockStolenView.pcss create mode 100644 res/css/views/elements/_LanguageDropdown.pcss delete mode 100644 res/css/views/elements/_TooltipButton.pcss create mode 100644 res/css/views/settings/tabs/room/_PeopleRoomSettingsTab.pcss create mode 100644 res/img/element-icons/spaces.svg delete mode 100755 scripts/check-i18n.pl delete mode 100755 scripts/fix-i18n.pl delete mode 100755 scripts/fixup-imports.pl create mode 100644 src/components/structures/auth/ConfirmSessionLockTheftView.tsx create mode 100644 src/components/structures/auth/SessionLockStolenView.tsx create mode 100644 src/components/utils/Box.tsx create mode 100644 src/components/utils/Flex.tsx create mode 100644 src/components/views/dialogs/oidc/OidcLogoutDialog.tsx create mode 100644 src/components/views/dialogs/spotlight/Filter.ts delete mode 100644 src/components/views/elements/TooltipButton.tsx create mode 100644 src/components/views/settings/tabs/room/PeopleRoomSettingsTab.tsx create mode 100644 src/dispatcher/payloads/OpenSpotlightPayload.ts delete mode 100644 src/emoji.ts create mode 100644 src/hooks/room/useRoomCallStatus.ts create mode 100644 src/hooks/room/useRoomThreadNotifications.ts create mode 100644 src/hooks/useGlobalNotificationState.ts create mode 100644 src/utils/SessionLock.ts create mode 100644 src/utils/oidc/getDelegatedAuthAccountUrl.ts create mode 100644 src/utils/oidc/getOidcLogoutUrl.ts create mode 100644 test/components/structures/UploadBar-test.tsx create mode 100644 test/components/views/VerificationShowSas-test.tsx create mode 100644 test/components/views/dialogs/ConfirmUserActionDialog-test.tsx create mode 100644 test/components/views/dialogs/__snapshots__/ConfirmUserActionDialog-test.tsx.snap create mode 100644 test/components/views/elements/FacePile-test.tsx create mode 100644 test/components/views/elements/SpellCheckLanguagesDropdown-test.tsx create mode 100644 test/components/views/elements/__snapshots__/FacePile-test.tsx.snap create mode 100644 test/components/views/elements/__snapshots__/SpellCheckLanguagesDropdown-test.tsx.snap create mode 100644 test/components/views/rooms/PresenceLabel-test.tsx create mode 100644 test/components/views/settings/tabs/room/PeopleRoomSettingsTab-test.tsx create mode 100644 test/components/views/settings/tabs/room/__snapshots__/PeopleRoomSettingsTab-test.tsx.snap create mode 100644 test/components/views/spaces/__snapshots__/QuickSettingsButton-test.tsx.snap delete mode 100644 test/languageHandler-test.ts rename test/{i18n-test => }/languageHandler-test.tsx (60%) delete mode 100644 test/modules/ProxiedModuleApi-test.ts create mode 100644 test/modules/ProxiedModuleApi-test.tsx create mode 100644 test/utils/SessionLock-test.ts create mode 100644 test/utils/__snapshots__/AutoDiscoveryUtils-test.tsx.snap create mode 100644 test/utils/i18n-helpers-test.ts create mode 100644 test/utils/oidc/getDelegatedAuthAccountUrl-test.ts diff --git a/.eslintrc.js b/.eslintrc.js index f3a6e364934..4434aecfdfe 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -62,118 +62,6 @@ module.exports = { name: "matrix-js-sdk/src/index", message: "Please use matrix-js-sdk/src/matrix instead", }, - { - name: "matrix-js-sdk/src/models/typed-event-emitter", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/models/room", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/models/room-member", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/models/room-state", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/models/event", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/models/event-status", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/models/user", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/models/device", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/models/event-timeline", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/models/event-timeline-set", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/@types/partials", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/@types/event", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/client", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/models/search-result", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/models/poll", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/models/relations", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/http-api", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/@types/PushRules", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/@types/search", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/filter", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/webrtc/groupCall", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/service-types", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/sync", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/timeline-window", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/store/indexeddb", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/crypto/store/localStorage-crypto-store", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/models/thread", - message: "Please use matrix-js-sdk/src/matrix instead", - }, { name: "matrix-react-sdk", message: "Please use matrix-react-sdk/src/index instead", @@ -185,8 +73,79 @@ module.exports = { ], patterns: [ { - group: ["matrix-js-sdk/lib", "matrix-js-sdk/lib/", "matrix-js-sdk/lib/**"], - message: "Please use matrix-js-sdk/src/* instead", + group: [ + "matrix-js-sdk/src/**", + "!matrix-js-sdk/src/matrix", + "matrix-js-sdk/lib", + "matrix-js-sdk/lib/", + "matrix-js-sdk/lib/**", + // XXX: Temporarily allow these as they are not available via the main export + "!matrix-js-sdk/src/logger", + "!matrix-js-sdk/src/errors", + "!matrix-js-sdk/src/utils", + "!matrix-js-sdk/src/version-support", + "!matrix-js-sdk/src/randomstring", + "!matrix-js-sdk/src/sliding-sync", + "!matrix-js-sdk/src/browser-index", + "!matrix-js-sdk/src/feature", + "!matrix-js-sdk/src/NamespacedValue", + "!matrix-js-sdk/src/ReEmitter", + "!matrix-js-sdk/src/event-mapper", + "!matrix-js-sdk/src/interactive-auth", + "!matrix-js-sdk/src/secret-storage", + "!matrix-js-sdk/src/room-hierarchy", + "!matrix-js-sdk/src/rendezvous", + "!matrix-js-sdk/src/rendezvous/transports", + "!matrix-js-sdk/src/rendezvous/channels", + "!matrix-js-sdk/src/indexeddb-worker", + "!matrix-js-sdk/src/pushprocessor", + "!matrix-js-sdk/src/extensible_events_v1", + "!matrix-js-sdk/src/extensible_events_v1/PollStartEvent", + "!matrix-js-sdk/src/extensible_events_v1/PollResponseEvent", + "!matrix-js-sdk/src/extensible_events_v1/PollEndEvent", + "!matrix-js-sdk/src/extensible_events_v1/InvalidEventError", + "!matrix-js-sdk/src/crypto-api", + "!matrix-js-sdk/src/crypto-api/verification", + "!matrix-js-sdk/src/crypto", + "!matrix-js-sdk/src/crypto/algorithms", + "!matrix-js-sdk/src/crypto/api", + "!matrix-js-sdk/src/crypto/aes", + "!matrix-js-sdk/src/crypto/backup", + "!matrix-js-sdk/src/crypto/olmlib", + "!matrix-js-sdk/src/crypto/crypto", + "!matrix-js-sdk/src/crypto/keybackup", + "!matrix-js-sdk/src/crypto/RoomList", + "!matrix-js-sdk/src/crypto/deviceinfo", + "!matrix-js-sdk/src/crypto/key_passphrase", + "!matrix-js-sdk/src/crypto/CrossSigning", + "!matrix-js-sdk/src/crypto/recoverykey", + "!matrix-js-sdk/src/crypto/dehydration", + "!matrix-js-sdk/src/crypto/verification", + "!matrix-js-sdk/src/crypto/verification/SAS", + "!matrix-js-sdk/src/crypto/verification/QRCode", + "!matrix-js-sdk/src/crypto/verification/request", + "!matrix-js-sdk/src/crypto/verification/request/VerificationRequest", + "!matrix-js-sdk/src/common-crypto", + "!matrix-js-sdk/src/common-crypto/CryptoBackend", + "!matrix-js-sdk/src/oidc", + "!matrix-js-sdk/src/oidc/discovery", + "!matrix-js-sdk/src/oidc/authorize", + "!matrix-js-sdk/src/oidc/validate", + "!matrix-js-sdk/src/oidc/error", + "!matrix-js-sdk/src/oidc/register", + "!matrix-js-sdk/src/webrtc", + "!matrix-js-sdk/src/webrtc/call", + "!matrix-js-sdk/src/webrtc/callFeed", + "!matrix-js-sdk/src/webrtc/mediaHandler", + "!matrix-js-sdk/src/webrtc/callEventTypes", + "!matrix-js-sdk/src/webrtc/callEventHandler", + "!matrix-js-sdk/src/webrtc/groupCallEventHandler", + "!matrix-js-sdk/src/models", + "!matrix-js-sdk/src/models/read-receipt", + "!matrix-js-sdk/src/models/relations-container", + "!matrix-js-sdk/src/models/related-relations", + ], + message: "Please use matrix-js-sdk/src/matrix instead", }, ], }, diff --git a/.github/workflows/cypress.yaml b/.github/workflows/cypress.yaml index bf1a30b87c0..070dee5b5ea 100644 --- a/.github/workflows/cypress.yaml +++ b/.github/workflows/cypress.yaml @@ -163,7 +163,7 @@ jobs: echo "CYPRESS_RUST_CRYPTO=1" >> "$GITHUB_ENV" - name: Run Cypress tests - uses: cypress-io/github-action@90dff940a41c08c7c344310eac7e57eda636326a + uses: cypress-io/github-action@fa88e4afe551e64c8827a4b9e379afc63d8f691a with: working-directory: matrix-react-sdk # The built-in Electron runner seems to grind to a halt trying to run the tests, so use chrome. diff --git a/.github/workflows/i18n_check.yml b/.github/workflows/i18n_check.yml index bb8e0188826..e72f8ca7b62 100644 --- a/.github/workflows/i18n_check.yml +++ b/.github/workflows/i18n_check.yml @@ -11,8 +11,8 @@ jobs: - name: "Get modified files" id: changed_files - if: github.event_name == 'pull_request' && github.event.pull_request.user.login != 'RiotTranslateBot' - uses: tj-actions/changed-files@87697c0dca7dd44e37a2b79a79489332556ff1f3 # v37 + if: github.event_name == 'pull_request' && github.event.pull_request.user.login != 'RiotTranslateBot' && github.event.pull_request.user.login != 't3chguy' + uses: tj-actions/changed-files@1c26215f3fbd51eba03bc199e5cbabdfc3584ce3 # v38 with: files: | src/i18n/strings/* @@ -25,8 +25,8 @@ jobs: github.event.pull_request.user.login != 'RiotTranslateBot' && steps.changed_files.outputs.any_modified == 'true' run: | - echo "Only translation files modified by `yarn i18n` can be committed - other translation files will confuse weblate in unrecoverable ways." exit 1 + echo "Only translation files modified by 'yarn i18n' can be committed - other translation files will confuse weblate in unrecoverable ways." - uses: actions/setup-node@v3 with: diff --git a/__mocks__/languages.json b/__mocks__/languages.json index 36ec89561b2..35a400808b8 100644 --- a/__mocks__/languages.json +++ b/__mocks__/languages.json @@ -1,10 +1,4 @@ { - "en": { - "fileName": "en_EN.json", - "label": "English" - }, - "en-us": { - "fileName": "en_US.json", - "label": "English (US)" - } + "en": "en_EN.json", + "en-us": "en_US.json" } diff --git a/cypress/e2e/audio-player/audio-player.spec.ts b/cypress/e2e/audio-player/audio-player.spec.ts index 8f1ef8054c6..30470716c91 100644 --- a/cypress/e2e/audio-player/audio-player.spec.ts +++ b/cypress/e2e/audio-player/audio-player.spec.ts @@ -168,7 +168,8 @@ describe("Audio player", () => { it("should be correctly rendered - light theme with monospace font", () => { uploadFile("cypress/fixtures/1sec-long-name-audio-file.ogg"); - takeSnapshots("Selected EventTile of audio player (light theme, monospace font)", true); // Enable monospace + // Disabled because flaky - see https://github.com/vector-im/element-web/issues/24881 + //takeSnapshots("Selected EventTile of audio player (light theme, monospace font)", true); // Enable monospace }); it("should be correctly rendered - high contrast theme", () => { @@ -186,7 +187,8 @@ describe("Audio player", () => { uploadFile("cypress/fixtures/1sec-long-name-audio-file.ogg"); - takeSnapshots("Selected EventTile of audio player (high contrast)"); + // Disabled because flaky - see https://github.com/vector-im/element-web/issues/24881 + //takeSnapshots("Selected EventTile of audio player (high contrast)"); }); it("should be correctly rendered - dark theme", () => { @@ -254,8 +256,8 @@ describe("Audio player", () => { }); }); - // Take snapshots - takeSnapshots("Selected EventTile of audio player with a reply"); + // Disabled because flaky - see https://github.com/vector-im/element-web/issues/24881 + //takeSnapshots("Selected EventTile of audio player with a reply"); }); it("should support creating a reply chain with multiple audio files", () => { diff --git a/cypress/e2e/editing/editing.spec.ts b/cypress/e2e/editing/editing.spec.ts index dafe15c885f..b7dacf86034 100644 --- a/cypress/e2e/editing/editing.spec.ts +++ b/cypress/e2e/editing/editing.spec.ts @@ -119,7 +119,7 @@ describe("Editing", () => { // Assert that the date separator is rendered at the top cy.get("li:nth-child(1) .mx_DateSeparator").within(() => { cy.get("h2").within(() => { - cy.findByText("Today"); + cy.findByText("today").should("have.css", "text-transform", "capitalize"); }); }); @@ -184,7 +184,7 @@ describe("Editing", () => { // Assert that the date is rendered cy.get("li:nth-child(1) .mx_DateSeparator").within(() => { cy.get("h2").within(() => { - cy.findByText("Today"); + cy.findByText("today").should("have.css", "text-transform", "capitalize"); }); }); diff --git a/cypress/e2e/login/login.spec.ts b/cypress/e2e/login/login.spec.ts index 29c1f6f16bd..2bb7d4c6a00 100644 --- a/cypress/e2e/login/login.spec.ts +++ b/cypress/e2e/login/login.spec.ts @@ -17,6 +17,7 @@ limitations under the License. /// import { HomeserverInstance } from "../../plugins/utils/homeserver"; +import { doTokenRegistration } from "./utils"; describe("Login", () => { let homeserver: HomeserverInstance; @@ -93,7 +94,6 @@ describe("Login", () => { }) .then((data) => { homeserver = data; - cy.visit("/#/login"); }); }); @@ -108,33 +108,7 @@ describe("Login", () => { // If you are using ufw, try something like: // sudo ufw allow in on docker0 // - cy.findByRole("button", { name: "Edit" }).click(); - cy.findByRole("textbox", { name: "Other homeserver" }).type(homeserver.baseUrl); - cy.findByRole("button", { name: "Continue" }).click(); - // wait for the dialog to go away - cy.get(".mx_ServerPickerDialog").should("not.exist"); - - // click on "Continue with OAuth test" - cy.findByRole("button", { name: "Continue with OAuth test" }).click(); - - // wait for the Test OAuth Page to load - cy.findByText("Test OAuth page"); - - // click the submit button - cy.findByRole("button", { name: "Submit" }).click(); - - // Synapse prompts us to pick a user ID - cy.findByRole("heading", { name: "Create your account" }); - cy.findByRole("textbox", { name: "Username (required)" }).type("alice"); - - // wait for username validation to start, and complete - cy.wait(50); - cy.get("#field-username-output").should("have.value", ""); - cy.findByRole("button", { name: "Continue" }).click(); - - // Synapse prompts us to grant permission to Element - cy.findByRole("heading", { name: "Continue to your account" }); - cy.findByRole("link", { name: "Continue" }).click(); + doTokenRegistration(homeserver.baseUrl); // Eventually, we should end up at the home screen. cy.url().should("contain", "/#/home", { timeout: 30000 }); diff --git a/cypress/e2e/login/soft_logout.spec.ts b/cypress/e2e/login/soft_logout.spec.ts new file mode 100644 index 00000000000..959197b7ebe --- /dev/null +++ b/cypress/e2e/login/soft_logout.spec.ts @@ -0,0 +1,143 @@ +/* +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 { HomeserverInstance } from "../../plugins/utils/homeserver"; +import { UserCredentials } from "../../support/login"; +import { doTokenRegistration } from "./utils"; + +describe("Soft logout", () => { + let homeserver: HomeserverInstance; + + beforeEach(() => { + cy.task("startOAuthServer") + .then((oAuthServerPort: number) => { + return cy.startHomeserver({ template: "default", oAuthServerPort }); + }) + .then((data) => { + homeserver = data; + }); + }); + + afterEach(() => { + cy.stopHomeserver(homeserver); + }); + + describe("with password user", () => { + let testUserCreds: UserCredentials; + + beforeEach(() => { + cy.initTestUser(homeserver, "Alice").then((creds) => { + testUserCreds = creds; + }); + }); + + it("shows the soft-logout page when a request fails, and allows a re-login", () => { + interceptRequestsWithSoftLogout(); + cy.findByText("You're signed out"); + cy.findByPlaceholderText("Password").type(testUserCreds.password).type("{enter}"); + + // back to the welcome page + cy.url().should("contain", "/#/home", { timeout: 30000 }); + cy.findByRole("heading", { name: "Welcome Alice" }); + }); + + it("still shows the soft-logout page when the page is reloaded after a soft-logout", () => { + interceptRequestsWithSoftLogout(); + cy.findByText("You're signed out"); + cy.reload(); + cy.findByText("You're signed out"); + }); + }); + + describe("with SSO user", () => { + beforeEach(() => { + doTokenRegistration(homeserver.baseUrl); + + // Eventually, we should end up at the home screen. + cy.url().should("contain", "/#/home", { timeout: 30000 }); + cy.findByRole("heading", { name: "Welcome Alice" }); + }); + + it("shows the soft-logout page when a request fails, and allows a re-login", () => { + // there is a bug in Element which means this only actually works if there is an app reload between + // the token login and the soft-logout: https://github.com/vector-im/element-web/issues/25957 + cy.reload(); + cy.findByRole("heading", { name: "Welcome Alice" }); + + interceptRequestsWithSoftLogout(); + + cy.findByText("You're signed out"); + cy.findByRole("button", { name: "Continue with OAuth test" }).click(); + + // click the submit button + cy.findByRole("button", { name: "Submit" }).click(); + + // Synapse prompts us to grant permission to Element + cy.findByRole("heading", { name: "Continue to your account" }); + cy.findByRole("link", { name: "Continue" }).click(); + + // back to the welcome page + cy.url().should("contain", "/#/home", { timeout: 30000 }); + cy.findByRole("heading", { name: "Welcome Alice" }); + }); + }); +}); + +/** + * Intercept calls to /sync and have them fail with a soft-logout + * + * Any further requests to /sync with the same access token are blocked. + */ +function interceptRequestsWithSoftLogout(): void { + let expiredAccessToken: string | null = null; + cy.intercept( + { + pathname: "/_matrix/client/*/sync", + }, + (req) => { + const accessToken = req.headers["authorization"] as string; + + // on the first request, record the access token + if (!expiredAccessToken) { + console.log(`Soft-logout on access token ${accessToken}`); + expiredAccessToken = accessToken; + } + + // now, if the access token on this request matches the expired one, block it + if (expiredAccessToken && accessToken === expiredAccessToken) { + console.log(`Intercepting request with soft-logged-out access token`); + req.reply({ + statusCode: 401, + body: { + errcode: "M_UNKNOWN_TOKEN", + error: "Soft logout", + soft_logout: true, + }, + }); + return; + } + + // otherwise, pass through as normal + req.continue(); + }, + ); + + // do something to make the active /sync return: create a new room + cy.getClient().then((client) => { + // don't wait for this to complete: it probably won't, because of the broken sync + return client.createRoom({}); + }); +} diff --git a/cypress/e2e/login/utils.ts b/cypress/e2e/login/utils.ts new file mode 100644 index 00000000000..39d87b42756 --- /dev/null +++ b/cypress/e2e/login/utils.ts @@ -0,0 +1,49 @@ +/* +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. +*/ + +/** Visit the login page, choose to log in with "OAuth test", register a new account, and redirect back to Element + */ +export function doTokenRegistration(homeserverUrl: string) { + cy.visit("/#/login"); + + cy.findByRole("button", { name: "Edit" }).click(); + cy.findByRole("textbox", { name: "Other homeserver" }).type(homeserverUrl); + cy.findByRole("button", { name: "Continue" }).click(); + // wait for the dialog to go away + cy.get(".mx_ServerPickerDialog").should("not.exist"); + + // click on "Continue with OAuth test" + cy.findByRole("button", { name: "Continue with OAuth test" }).click(); + + // wait for the Test OAuth Page to load + cy.findByText("Test OAuth page"); + + // click the submit button + cy.findByRole("button", { name: "Submit" }).click(); + + // Synapse prompts us to pick a user ID + cy.findByRole("heading", { name: "Create your account" }); + cy.findByRole("textbox", { name: "Username (required)" }).type("alice"); + + // wait for username validation to start, and complete + cy.wait(50); + cy.get("#field-username-output").should("have.value", ""); + cy.findByRole("button", { name: "Continue" }).click(); + + // Synapse prompts us to grant permission to Element + cy.findByRole("heading", { name: "Continue to your account" }); + cy.findByRole("link", { name: "Continue" }).click(); +} diff --git a/cypress/e2e/read-receipts/high-level.spec.ts b/cypress/e2e/read-receipts/high-level.spec.ts index 7af0daeb9e4..d7b27242c85 100644 --- a/cypress/e2e/read-receipts/high-level.spec.ts +++ b/cypress/e2e/read-receipts/high-level.spec.ts @@ -68,11 +68,15 @@ describe("Read receipts", () => { cy.stopHomeserver(homeserver); }); - abstract class MessageSpec { + abstract class MessageContentSpec { public abstract getContent(room: Room): Promise>; } - type Message = string | MessageSpec; + abstract class BotActionSpec { + public abstract performAction(cli: MatrixClient, room: Room): Promise; + } + + type Message = string | MessageContentSpec | BotActionSpec; function goTo(room: string) { cy.viewRoomByName(room); @@ -95,24 +99,39 @@ describe("Read receipts", () => { cy.get(".mx_ThreadView_timelinePanelWrapper", { log: false }).should("have.length", 1); } - /** - * Sends messages into given room as a bot - * @param room - the name of the room to send messages into - * @param messages - the list of messages to send, these can be strings or implementations of MessageSpace like `editOf` - */ - function receiveMessages(room: string, messages: Message[]) { + function sendMessageAsClient(cli: MatrixClient, room: string, messages: Message[]) { findRoomByName(room).then(async ({ roomId }) => { - const room = bot.getRoom(roomId); + const room = cli.getRoom(roomId); for (const message of messages) { if (typeof message === "string") { - await bot.sendTextMessage(roomId, message); + await cli.sendTextMessage(roomId, message); + } else if (message instanceof MessageContentSpec) { + await cli.sendMessage(roomId, await message.getContent(room)); } else { - await bot.sendMessage(roomId, await message.getContent(room)); + await message.performAction(cli, room); } } }); } + /** + * Sends messages into given room as a bot + * @param room - the name of the room to send messages into + * @param messages - the list of messages to send, these can be strings or implementations of MessageSpec like `editOf` + */ + function receiveMessages(room: string, messages: Message[]) { + sendMessageAsClient(bot, room, messages); + } + + /** + * Sends messages into given room as the currently logged-in user + * @param room - the name of the room to send messages into + * @param messages - the list of messages to send, these can be strings or implementations of MessageSpec like `editOf` + */ + function sendMessages(room: string, messages: Message[]) { + cy.getClient().then((cli) => sendMessageAsClient(cli, room, messages)); + } + /** * Utility to find a MatrixEvent by its body content * @param room - the room to search for the event in @@ -140,12 +159,12 @@ describe("Read receipts", () => { } /** - * MessageSpec to send an edit into a room + * MessageContentSpec to send an edit into a room * @param originalMessage - the body of the message to edit * @param newMessage - the message body to send in the edit */ - function editOf(originalMessage: string, newMessage: string): MessageSpec { - return new (class extends MessageSpec { + function editOf(originalMessage: string, newMessage: string): MessageContentSpec { + return new (class extends MessageContentSpec { public async getContent(room: Room): Promise> { const ev = await getMessage(room, originalMessage, true); @@ -163,12 +182,12 @@ describe("Read receipts", () => { } /** - * MessageSpec to send a reply into a room + * MessageContentSpec to send a reply into a room * @param targetMessage - the body of the message to reply to * @param newMessage - the message body to send into the reply */ - function replyTo(targetMessage: string, newMessage: string): MessageSpec { - return new (class extends MessageSpec { + function replyTo(targetMessage: string, newMessage: string): MessageContentSpec { + return new (class extends MessageContentSpec { public async getContent(room: Room): Promise> { const ev = await getMessage(room, targetMessage); @@ -186,12 +205,12 @@ describe("Read receipts", () => { } /** - * MessageSpec to send a threaded response into a room + * MessageContentSpec to send a threaded response into a room * @param rootMessage - the body of the thread root message to send a response to * @param newMessage - the message body to send into the thread response */ - function threadedOff(rootMessage: string, newMessage: string): MessageSpec { - return new (class extends MessageSpec { + function threadedOff(rootMessage: string, newMessage: string): MessageContentSpec { + return new (class extends MessageContentSpec { public async getContent(room: Room): Promise> { const ev = await getMessage(room, rootMessage); @@ -208,6 +227,53 @@ describe("Read receipts", () => { })(); } + /** + * BotActionSpec to send a reaction to an existing event into a room + * @param targetMessage - the body of the message to send a reaction to + * @param reaction - the key of the reaction to send into the room + */ + function reactionTo(targetMessage: string, reaction: string): BotActionSpec { + return new (class extends BotActionSpec { + public async performAction(cli: MatrixClient, room: Room): Promise { + const ev = await getMessage(room, targetMessage, true); + const threadId = !ev.isThreadRoot ? ev.threadRootId : undefined; + await cli.sendEvent(room.roomId, threadId ?? null, "m.reaction", { + "m.relates_to": { + rel_type: "m.annotation", + event_id: ev.getId(), + key: reaction, + }, + }); + } + })(); + } + + /** + * BotActionSpec to send a custom event + * @param eventType - the type of the event to send + * @param content - the event content to send + */ + function customEvent(eventType: string, content: Record): BotActionSpec { + return new (class extends BotActionSpec { + public async performAction(cli: MatrixClient, room: Room): Promise { + await cli.sendEvent(room.roomId, null, eventType, content); + } + })(); + } + + /** + * BotActionSpec to send a redaction into a room + * @param targetMessage - the body of the message to send a redaction to + */ + function redactionOf(targetMessage: string): BotActionSpec { + return new (class extends BotActionSpec { + public async performAction(cli: MatrixClient, room: Room): Promise { + const ev = await getMessage(room, targetMessage, true); + await cli.redactEvent(room.roomId, ev.threadRootId, ev.getId()); + } + })(); + } + function getRoomListTile(room: string) { return cy.findByRole("treeitem", { name: new RegExp("^" + room), log: false }); } @@ -246,7 +312,7 @@ describe("Read receipts", () => { cy.log("Open thread list"); cy.findByTestId("threadsButton", { log: false }).then(($button) => { if ($button?.attr("aria-current") !== "true") { - $button.trigger("click"); + cy.findByTestId("threadsButton", { log: false }).click(); } }); @@ -296,7 +362,7 @@ describe("Read receipts", () => { describe("new messages", () => { describe("in the main timeline", () => { - it("Sending a message makes a room unread", () => { + it("Receiving a message makes a room unread", () => { goTo(room1); assertRead(room2); @@ -323,7 +389,7 @@ describe("Read receipts", () => { markAsRead(room2); assertRead(room2); }); - it("Sending a new message after marking as read makes it unread", () => { + it("Receiving a new message after marking as read makes it unread", () => { goTo(room1); assertRead(room2); receiveMessages(room2, ["Msg1"]); @@ -356,6 +422,16 @@ describe("Read receipts", () => { saveAndReload(); assertRead(room2); }); + it.skip("Sending a message from a different client marks room as read", () => { + goTo(room1); + assertRead(room2); + + receiveMessages(room2, ["Msg1"]); + assertUnread(room2, 1); + + sendMessages(room2, ["Msg2"]); + assertRead(room2); + }); }); describe("in threads", () => { @@ -376,7 +452,7 @@ describe("Read receipts", () => { // Given a thread exists goTo(room1); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]); - assertUnread(room2, 1); + assertUnread(room2, 2); goTo(room2); openThread("Msg1"); @@ -386,7 +462,7 @@ describe("Read receipts", () => { // Given a thread exists goTo(room1); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); - assertUnread(room2, 2); // (Sanity) + assertUnread(room2, 3); // (Sanity) // When I read the main timeline goTo(room2); @@ -405,7 +481,7 @@ describe("Read receipts", () => { receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), "Msg2", threadedOff("Msg2", "Resp2")]); assertUnread(room2, 4); goTo(room2); - assertUnread(room2, 4); + assertUnread(room2, 2); openThread("Msg1"); assertUnread(room2, 1); @@ -486,7 +562,7 @@ describe("Read receipts", () => { // Given a thread exists goTo(room1); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); - assertUnread(room2, 2); // (Sanity) + assertUnread(room2, 3); // (Sanity) // When I read the main timeline goTo(room2); @@ -527,13 +603,13 @@ describe("Read receipts", () => { // Given a thread exists goTo(room1); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]); - assertUnread(room2, 1); // (Sanity) + assertUnread(room2, 2); // (Sanity) // When I read the main timeline goTo(room2); // Then room does appear unread - assertUnread(room2, 2); + assertUnread(room2, 1); assertUnreadThread("Msg1"); }); it.skip("Reading a thread root within the thread view marks it as read in the main timeline", () => {}); @@ -625,7 +701,7 @@ describe("Read receipts", () => { goTo(room1); receiveMessages(room2, ["Msg1", replyTo("Msg1", "Reply1")]); - assertUnread(room2, 1); + assertUnread(room2, 2); goTo(room2); assertRead(room2); @@ -642,7 +718,7 @@ describe("Read receipts", () => { goTo(room1); receiveMessages(room2, ["Msg1", replyTo("Msg1", "Reply1")]); - assertUnread(room2, 1); + assertUnread(room2, 2); markAsRead(room2); // When an edit appears in the room @@ -693,7 +769,7 @@ describe("Read receipts", () => { it("An edit of a threaded message makes the room unread", () => { goTo(room1); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]); - assertUnread(room2, 1); + assertUnread(room2, 2); goTo(room2); openThread("Msg1"); @@ -706,7 +782,7 @@ describe("Read receipts", () => { it("Reading an edit of a threaded message makes the room read", () => { goTo(room1); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]); - assertUnread(room2, 1); + assertUnread(room2, 2); goTo(room2); openThread("Msg1"); @@ -723,7 +799,7 @@ describe("Read receipts", () => { it("Marking a room as read after an edit in a thread makes it read", () => { goTo(room1); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), editOf("Resp1", "Edit1")]); - assertUnread(room2, 2); + assertUnread(room2, 3); // TODO: the edit counts as a message! markAsRead(room2); assertRead(room2); @@ -731,13 +807,13 @@ describe("Read receipts", () => { it.skip("Editing a thread message after marking as read makes the room unread", () => { goTo(room1); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]); - assertUnread(room2, 1); + assertUnread(room2, 2); markAsRead(room2); assertRead(room2); receiveMessages(room2, [editOf("Resp1", "Edit1")]); - assertUnread(room2, 1); + assertUnread(room2, 1); // TODO: should this edit make us unread? }); it("A room with an edited threaded message is still unread after restart", () => { goTo(room1); @@ -764,7 +840,7 @@ describe("Read receipts", () => { it("An edit of a thread root makes the room unread", () => { goTo(room1); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]); - assertUnread(room2, 1); + assertUnread(room2, 2); goTo(room2); openThread("Msg1"); @@ -801,24 +877,78 @@ describe("Read receipts", () => { }); describe("reactions", () => { - // Justification for this section: edits an reactions are similar, so we - // might choose to miss this section, but I have included it because - // edits replace the content of the original event in our code and - // reactions don't, so it seems possible that bugs could creep in that - // affect only one or the other. - describe("in the main timeline", () => { - it.skip("Reacting to a message makes a room unread", () => {}); - it.skip("Reading a reaction makes the room read", () => {}); - it.skip("Marking a room as read after a reaction makes it read", () => {}); - it.skip("Reacting to a message after marking as read makes the room unread", () => {}); - it.skip("A room with a reaction is still unread after restart", () => {}); - it.skip("A room where all reactions are read is still read after restart", () => {}); + it("Receiving a reaction to a message does not make a room unread", () => { + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1", "Msg2"]); + assertUnread(room2, 2); + + // When I read the main timeline + goTo(room2); + assertRead(room2); + + goTo(room1); + receiveMessages(room2, [reactionTo("Msg2", "🪿")]); + assertRead(room2); + }); + it("Reacting to a message after marking as read does not make the room unread", () => { + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1", "Msg2"]); + assertUnread(room2, 2); + + markAsRead(room2); + assertRead(room2); + + receiveMessages(room2, [reactionTo("Msg2", "🪿")]); + assertRead(room2); + }); + it("A room with an unread reaction is still read after restart", () => { + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1", "Msg2"]); + assertUnread(room2, 2); + + markAsRead(room2); + assertRead(room2); + + receiveMessages(room2, [reactionTo("Msg2", "🪿")]); + assertRead(room2); + + saveAndReload(); + assertRead(room2); + }); + it("A room where all reactions are read is still read after restart", () => { + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1", "Msg2", reactionTo("Msg2", "🪿")]); + assertUnread(room2, 2); + + markAsRead(room2); + assertRead(room2); + + saveAndReload(); + assertRead(room2); + }); }); describe("in threads", () => { - it.skip("A reaction to a threaded message makes the room unread", () => {}); - it.skip("Reading a reaction to a threaded message makes the room read", () => {}); + it("A reaction to a threaded message does not make the room unread", () => { + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Reply1")]); + assertUnread(room2, 2); + + goTo(room2); + openThread("Msg1"); + assertRead(room2); + + goTo(room1); + receiveMessages(room2, [reactionTo("Reply1", "🪿")]); + + assertRead(room2); + }); it.skip("Marking a room as read after a reaction in a thread makes it read", () => {}); it.skip("Reacting to a thread message after marking as read makes the room unread", () => {}); it.skip("A room with a reaction to a threaded message is still unread after restart", () => {}); @@ -826,7 +956,21 @@ describe("Read receipts", () => { }); describe("thread roots", () => { - it.skip("A reaction to a thread root makes the room unread", () => {}); + it("A reaction to a thread root does not make the room unread", () => { + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Reply1")]); + assertUnread(room2, 2); + + goTo(room2); + openThread("Msg1"); + assertRead(room2); + + goTo(room1); + receiveMessages(room2, [reactionTo("Msg1", "🪿")]); + + assertRead(room2); + }); it.skip("Reading a reaction to a thread root makes the room read", () => {}); it.skip("Marking a room as read after a reaction to a thread root makes it read", () => {}); it.skip("Reacting to a thread root after marking as read makes the room unread but not the thread", () => {}); @@ -835,9 +979,20 @@ describe("Read receipts", () => { describe("redactions", () => { describe("in the main timeline", () => { - // One of the following two must be right: - it.skip("Redacting the message pointed to by my receipt leaves the room read", () => {}); - it.skip("Redacting a message after it was read makes the room unread", () => {}); + it("Redacting the message pointed to by my receipt leaves the room read", () => { + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1", "Msg2"]); + assertUnread(room2, 2); + + // When I read the main timeline + goTo(room2); + assertRead(room2); + + goTo(room1); + receiveMessages(room2, [redactionOf("Msg2")]); + assertRead(room2); + }); it.skip("Reading an unread room after a redaction of the latest message makes it read", () => {}); it.skip("Reading an unread room after a redaction of an older message makes it read", () => {}); @@ -949,8 +1104,31 @@ describe("Read receipts", () => { }); describe("Ignored events", () => { - it.skip("If all events after receipt are unimportant, the room is read", () => {}); - it.skip("Sending an important event after unimportant ones makes the room unread", () => {}); + it("If all events after receipt are unimportant, the room is read", () => { + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1", "Msg2"]); + assertUnread(room2, 2); + + markAsRead(room2); + + receiveMessages(room2, [customEvent("org.custom.event", { body: "foobar" })]); + assertRead(room2); + }); + it("Sending an important event after unimportant ones makes the room unread", () => { + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1", "Msg2"]); + assertUnread(room2, 2); + + markAsRead(room2); + + receiveMessages(room2, [customEvent("org.custom.event", { body: "foobar" })]); + assertRead(room2); + + receiveMessages(room2, ["Hello"]); + assertUnread(room2, 1); + }); it.skip("A receipt for the last unimportant event makes the room read, even if all are unimportant", () => {}); }); diff --git a/cypress/e2e/read-receipts/read-receipts.spec.ts b/cypress/e2e/read-receipts/read-receipts.spec.ts index a08862d4409..e298f7fa987 100644 --- a/cypress/e2e/read-receipts/read-receipts.spec.ts +++ b/cypress/e2e/read-receipts/read-receipts.spec.ts @@ -16,8 +16,7 @@ limitations under the License. /// -import type { MatrixClient, MatrixEvent, ISendEventResponse } from "matrix-js-sdk/src/matrix"; -import type { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts"; +import type { MatrixClient, MatrixEvent, ISendEventResponse, ReceiptType } from "matrix-js-sdk/src/matrix"; import { HomeserverInstance } from "../../plugins/utils/homeserver"; describe("Read receipts", () => { diff --git a/cypress/e2e/register/email.spec.ts b/cypress/e2e/register/email.spec.ts new file mode 100644 index 00000000000..988cee9ff21 --- /dev/null +++ b/cypress/e2e/register/email.spec.ts @@ -0,0 +1,93 @@ +/* +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 { HomeserverInstance } from "../../plugins/utils/homeserver"; +import { Mailhog } from "../../support/mailhog"; + +describe("Email Registration", () => { + let homeserver: HomeserverInstance; + let mailhog: Mailhog; + + beforeEach(() => { + cy.startMailhog().then((_mailhog) => { + mailhog = _mailhog; + cy.startHomeserver({ + template: "email", + variables: { + SMTP_HOST: "{{HOST_DOCKER_INTERNAL}}", // This will get replaced in synapseStart + SMTP_PORT: _mailhog.instance.smtpPort, + }, + }).then((_homeserver) => { + homeserver = _homeserver; + + cy.intercept( + { method: "GET", pathname: "/config.json" }, + { + body: { + default_server_config: { + "m.homeserver": { + base_url: homeserver.baseUrl, + }, + "m.identity_server": { + base_url: "https://server.invalid", + }, + }, + }, + }, + ); + cy.visit("/#/register"); + cy.injectAxe(); + }); + }); + }); + + afterEach(() => { + cy.stopHomeserver(homeserver); + cy.stopMailhog(mailhog); + }); + + it("registers an account and lands on the use case selection screen", () => { + cy.findByRole("textbox", { name: "Username" }).should("be.visible"); + // Hide the server text as it contains the randomly allocated Homeserver port + const percyCSS = ".mx_ServerPicker_server { visibility: hidden !important; }"; + + cy.findByRole("textbox", { name: "Username" }).type("alice"); + cy.findByPlaceholderText("Password").type("totally a great password"); + cy.findByPlaceholderText("Confirm password").type("totally a great password"); + cy.findByPlaceholderText("Email").type("alice@email.com"); + cy.findByRole("button", { name: "Register" }).click(); + + cy.findByText("Check your email to continue").should("be.visible"); + cy.percySnapshot("Registration check your email", { percyCSS }); + cy.checkA11y(); + + cy.findByText("An error was encountered when sending the email").should("not.exist"); + + cy.waitForPromise(async () => { + const messages = await mailhog.api.messages(); + expect(messages.items).to.have.length(1); + expect(messages.items[0].to).to.eq("alice@email.com"); + const [link] = messages.items[0].text.match(/http.+/); + return link; + }).as("emailLink"); + + cy.get("@emailLink").then((link) => cy.request(link)); + + cy.get(".mx_UseCaseSelection_skip", { timeout: 30000 }).should("exist"); + }); +}); diff --git a/cypress/e2e/settings/general-user-settings-tab.spec.ts b/cypress/e2e/settings/general-user-settings-tab.spec.ts index 2879d6d9301..725caf2038e 100644 --- a/cypress/e2e/settings/general-user-settings-tab.spec.ts +++ b/cypress/e2e/settings/general-user-settings-tab.spec.ts @@ -133,10 +133,12 @@ describe("General user settings tab", () => { cy.findByRole("button", { name: "Language Dropdown" }).click(); // Assert that the default option is rendered and highlighted - cy.findByRole("option", { name: /Bahasa Indonesia/ }) + cy.findByRole("option", { name: /Albanian/ }) .should("be.visible") .should("have.class", "mx_Dropdown_option_highlight"); + cy.findByRole("option", { name: /Deutsch/ }).should("be.visible"); + // Click again to close the dropdown cy.findByRole("button", { name: "Language Dropdown" }).click(); @@ -230,7 +232,7 @@ describe("General user settings tab", () => { cy.closeDialog(); // Assert the avatar's initial characters are set - cy.get(".mx_UserMenu .mx_BaseAvatar_initial").findByText("A").should("exist"); // Alice - cy.get(".mx_RoomView_wrapper .mx_BaseAvatar_initial").findByText("A").should("exist"); // Alice + cy.get(".mx_UserMenu .mx_BaseAvatar").findByText("A").should("exist"); // Alice + cy.get(".mx_RoomView_wrapper .mx_BaseAvatar").findByText("A").should("exist"); // Alice }); }); diff --git a/cypress/e2e/spotlight/spotlight.spec.ts b/cypress/e2e/spotlight/spotlight.spec.ts index b85e1103984..d7ce14eff9e 100644 --- a/cypress/e2e/spotlight/spotlight.spec.ts +++ b/cypress/e2e/spotlight/spotlight.spec.ts @@ -218,20 +218,20 @@ describe("Spotlight", () => { cy.openSpotlightDialog().within(() => { cy.wait(1000); // wait for the dialog to settle, otherwise our keypresses might race with an update - // initially, publicrooms should be highlighted (because there are no other suggestions) - cy.get("#mx_SpotlightDialog_button_explorePublicRooms").should("have.attr", "aria-selected", "true"); + // initially, public spaces should be highlighted (because there are no other suggestions) + cy.get("#mx_SpotlightDialog_button_explorePublicSpaces").should("have.attr", "aria-selected", "true"); - // hitting enter should enable the publicrooms filter + // hitting enter should enable the public rooms filter cy.spotlightSearch().type("{enter}"); - cy.get(".mx_SpotlightDialog_filter").should("contain", "Public rooms"); + cy.get(".mx_SpotlightDialog_filter").should("contain", "Public spaces"); cy.spotlightSearch().type("{backspace}"); cy.get(".mx_SpotlightDialog_filter").should("not.exist"); cy.spotlightSearch().type("{downArrow}"); cy.spotlightSearch().type("{downArrow}"); - cy.get("#mx_SpotlightDialog_button_startChat").should("have.attr", "aria-selected", "true"); + cy.get("#mx_SpotlightDialog_button_explorePublicRooms").should("have.attr", "aria-selected", "true"); cy.spotlightSearch().type("{enter}"); - cy.get(".mx_SpotlightDialog_filter").should("contain", "People"); + cy.get(".mx_SpotlightDialog_filter").should("contain", "Public rooms"); cy.spotlightSearch().type("{backspace}"); cy.get(".mx_SpotlightDialog_filter").should("not.exist"); }); diff --git a/cypress/e2e/threads/threads.spec.ts b/cypress/e2e/threads/threads.spec.ts index 35b6410d5b8..78fc6c63272 100644 --- a/cypress/e2e/threads/threads.spec.ts +++ b/cypress/e2e/threads/threads.spec.ts @@ -98,7 +98,7 @@ describe("Threads", () => { // Wait until the both messages are read cy.get(".mx_ThreadView .mx_EventTile_last[data-layout=group]").within(() => { cy.get(".mx_EventTile_line .mx_MTextBody").findByText(MessageLong).should("exist"); - cy.get(".mx_ReadReceiptGroup .mx_BaseAvatar_image").should("be.visible"); + cy.get(".mx_ReadReceiptGroup .mx_BaseAvatar").should("be.visible"); // Make sure the CSS style for spacing is applied to mx_EventTile_line on group/modern layout cy.get(".mx_EventTile_line").should("have.css", "padding-inline-start", ThreadViewGroupSpacingStart); @@ -118,7 +118,7 @@ describe("Threads", () => { cy.get(".mx_EventTile_line .mx_MTextBody").findByText(MessageLong).should("exist"); // Make sure the avatar inside ReadReceiptGroup is visible on the group layout - cy.get(".mx_ReadReceiptGroup .mx_BaseAvatar_image").should("be.visible"); + cy.get(".mx_ReadReceiptGroup .mx_BaseAvatar").should("be.visible"); }); // Enable the bubble layout @@ -127,12 +127,12 @@ describe("Threads", () => { cy.get(".mx_ThreadView .mx_EventTile[data-layout='bubble'].mx_EventTile_last").within(() => { // TODO: remove this after fixing the issue of ReadReceiptGroup being hidden on the bubble layout // See: https://github.com/vector-im/element-web/issues/23569 - cy.get(".mx_ReadReceiptGroup .mx_BaseAvatar_image").should("exist"); + cy.get(".mx_ReadReceiptGroup .mx_BaseAvatar").should("exist"); // Make sure the avatar inside ReadReceiptGroup is visible on bubble layout // TODO: enable this after fixing the issue of ReadReceiptGroup being hidden on the bubble layout // See: https://github.com/vector-im/element-web/issues/23569 - // cy.get(".mx_ReadReceiptGroup .mx_BaseAvatar_image").should("be.visible"); + // cy.get(".mx_ReadReceiptGroup .mx_BaseAvatar").should("be.visible"); }); // Re-enable the group layout diff --git a/cypress/e2e/timeline/timeline.spec.ts b/cypress/e2e/timeline/timeline.spec.ts index 42e23a2bc49..3d83a61b5ac 100644 --- a/cypress/e2e/timeline/timeline.spec.ts +++ b/cypress/e2e/timeline/timeline.spec.ts @@ -45,7 +45,7 @@ const expectDisplayName = (e: JQuery, displayName: string): void => const expectAvatar = (e: JQuery, avatarUrl: string): void => { cy.all([cy.window({ log: false }), cy.getClient()]).then(([win, cli]) => { const size = AVATAR_SIZE * win.devicePixelRatio; - expect(e.find(".mx_BaseAvatar_image").attr("src")).to.equal( + expect(e.find(".mx_BaseAvatar img").attr("src")).to.equal( // eslint-disable-next-line no-restricted-properties cli.mxcUrlToHttp(avatarUrl, size, size, AVATAR_RESIZE_METHOD), ); @@ -197,10 +197,10 @@ describe("Timeline", () => { cy.get(".mx_GenericEventListSummary").within(() => { // Click "expand" link button - cy.findByRole("button", { name: "expand" }).click(); + cy.findByRole("button", { name: "Expand" }).click(); // Assert that the "expand" link button worked - cy.findByRole("button", { name: "collapse" }).should("exist"); + cy.findByRole("button", { name: "Collapse" }).should("exist"); }); cy.get(".mx_MainSplit").percySnapshotElement("Expanded GELS on IRC layout", { percyCSS }); @@ -224,10 +224,10 @@ describe("Timeline", () => { cy.get(".mx_GenericEventListSummary").within(() => { // Click "expand" link button - cy.findByRole("button", { name: "expand" }).click(); + cy.findByRole("button", { name: "Expand" }).click(); // Assert that the "expand" link button worked - cy.findByRole("button", { name: "collapse" }).should("exist"); + cy.findByRole("button", { name: "Collapse" }).should("exist"); }); cy.get(".mx_MainSplit").percySnapshotElement("Expanded GELS on modern layout", { percyCSS }); @@ -247,10 +247,10 @@ describe("Timeline", () => { cy.get(".mx_GenericEventListSummary").within(() => { // Click "expand" link button - cy.findByRole("button", { name: "expand" }).click(); + cy.findByRole("button", { name: "Expand" }).click(); // Assert that the "expand" link button worked - cy.findByRole("button", { name: "collapse" }).should("exist"); + cy.findByRole("button", { name: "Collapse" }).should("exist"); }); // Make sure spacer is not visible on bubble layout @@ -270,10 +270,10 @@ describe("Timeline", () => { .realHover() .findByRole("toolbar", { name: "Message Actions" }) .should("be.visible"); - cy.findByRole("button", { name: "collapse" }).click(); + cy.findByRole("button", { name: "Collapse" }).click(); // Assert that "collapse" link button worked - cy.findByRole("button", { name: "expand" }).should("exist"); + cy.findByRole("button", { name: "Expand" }).should("exist"); }); // Save snapshot of collapsed generic event list summary on bubble layout @@ -292,7 +292,7 @@ describe("Timeline", () => { }); // Click "expand" link button - cy.get(".mx_GenericEventListSummary").findByRole("button", { name: "expand" }).click(); + cy.get(".mx_GenericEventListSummary").findByRole("button", { name: "Expand" }).click(); // Check the event line has margin instead of inset property // cf. _EventTile.pcss @@ -388,7 +388,7 @@ describe("Timeline", () => { // 2. Alignment of expanded GELS and messages // Click "expand" link button - cy.get(".mx_GenericEventListSummary").findByRole("button", { name: "expand" }).click(); + cy.get(".mx_GenericEventListSummary").findByRole("button", { name: "Expand" }).click(); // Check inline start spacing of info line on expanded GELS cy.get(".mx_EventTile[data-layout=irc].mx_EventTile_info:first-of-type .mx_EventTile_line") // See: _EventTile.pcss diff --git a/cypress/e2e/widgets/stickers.spec.ts b/cypress/e2e/widgets/stickers.spec.ts index 1a172055f97..d3e08f8405c 100644 --- a/cypress/e2e/widgets/stickers.spec.ts +++ b/cypress/e2e/widgets/stickers.spec.ts @@ -86,10 +86,10 @@ function expectTimelineSticker(roomId: string) { // Make sure it's in the right room cy.get(".mx_EventTile_sticker > a").should("have.attr", "href").and("include", `/${roomId}/`); - // Make sure the image points at the sticker image - cy.get(`img[alt="${STICKER_NAME}"]`) - .should("have.attr", "src") - .and("match", /thumbnail\/somewhere\?/); + // Make sure the image points at the sticker image. We will briefly show it + // using the thumbnail URL, but as soon as that fails, we will switch to the + // download URL. + cy.get(`img[alt="${STICKER_NAME}"][src*="download/somewhere"]`).should("exist"); } describe("Stickers", () => { diff --git a/cypress/global.d.ts b/cypress/global.d.ts index da5b3b8cd71..f8caad1f89e 100644 --- a/cypress/global.d.ts +++ b/cypress/global.d.ts @@ -17,6 +17,7 @@ limitations under the License. import "../src/@types/global"; import "../src/@types/svg"; import "../src/@types/raw-loader"; +// eslint-disable-next-line no-restricted-imports import "matrix-js-sdk/src/@types/global"; import type { MatrixClient, diff --git a/cypress/plugins/docker/index.ts b/cypress/plugins/docker/index.ts index 66bab0b8532..4c2da5f6457 100644 --- a/cypress/plugins/docker/index.ts +++ b/cypress/plugins/docker/index.ts @@ -156,6 +156,14 @@ export function isPodman(): Promise { }); } +/** + * Supply the right hostname to use to talk to the host machine. On Docker this + * is "host.docker.internal" and on Podman this is "host.containers.internal". + */ +export async function hostContainerName() { + return (await isPodman()) ? "host.containers.internal" : "host.docker.internal"; +} + /** * @type {Cypress.PluginConfig} */ diff --git a/cypress/plugins/index.ts b/cypress/plugins/index.ts index cb6d819dcd3..412057cf544 100644 --- a/cypress/plugins/index.ts +++ b/cypress/plugins/index.ts @@ -26,6 +26,7 @@ import { webserver } from "./webserver"; import { docker } from "./docker"; import { log } from "./log"; import { oAuthServer } from "./oauth_server"; +import { mailhogDocker } from "./mailhog"; /** * @type {Cypress.PluginConfig} @@ -41,4 +42,5 @@ export default function (on: PluginEvents, config: PluginConfigOptions) { installLogsPrinter(on, { // printLogsToConsole: "always", }); + mailhogDocker(on, config); } diff --git a/cypress/plugins/mailhog/index.ts b/cypress/plugins/mailhog/index.ts new file mode 100644 index 00000000000..a156e939818 --- /dev/null +++ b/cypress/plugins/mailhog/index.ts @@ -0,0 +1,91 @@ +/* +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 PluginEvents = Cypress.PluginEvents; +import PluginConfigOptions = Cypress.PluginConfigOptions; +import { getFreePort } from "../utils/port"; +import { dockerIp, dockerRun, dockerStop } from "../docker"; + +// A cypress plugins to add command to manage an instance of Mailhog in Docker + +export interface Instance { + host: string; + smtpPort: number; + httpPort: number; + containerId: string; +} + +const instances = new Map(); + +// Start a synapse instance: the template must be the name of +// one of the templates in the cypress/plugins/synapsedocker/templates +// directory +async function mailhogStart(): Promise { + const smtpPort = await getFreePort(); + const httpPort = await getFreePort(); + + console.log(`Starting mailhog...`); + + const containerId = await dockerRun({ + image: "mailhog/mailhog:latest", + containerName: `react-sdk-cypress-mailhog`, + params: ["--rm", "-p", `${smtpPort}:1025/tcp`, "-p", `${httpPort}:8025/tcp`], + }); + + console.log(`Started mailhog on ports smtp=${smtpPort} http=${httpPort}.`); + + const host = await dockerIp({ containerId }); + const instance: Instance = { smtpPort, httpPort, containerId, host }; + instances.set(containerId, instance); + return instance; +} + +async function mailhogStop(id: string): Promise { + const synCfg = instances.get(id); + + if (!synCfg) throw new Error("Unknown mailhog ID"); + + await dockerStop({ + containerId: id, + }); + + instances.delete(id); + + console.log(`Stopped mailhog id ${id}.`); + // cypress deliberately fails if you return 'undefined', so + // return null to signal all is well, and we've handled the task. + return null; +} + +/** + * @type {Cypress.PluginConfig} + */ +export function mailhogDocker(on: PluginEvents, config: PluginConfigOptions) { + on("task", { + mailhogStart, + mailhogStop, + }); + + on("after:spec", async (spec) => { + // Cleans up any remaining instances after a spec run + for (const synId of instances.keys()) { + console.warn(`Cleaning up synapse ID ${synId} after ${spec.name}`); + await mailhogStop(synId); + } + }); +} diff --git a/cypress/plugins/synapsedocker/index.ts b/cypress/plugins/synapsedocker/index.ts index 9773849e030..7c278610cc1 100644 --- a/cypress/plugins/synapsedocker/index.ts +++ b/cypress/plugins/synapsedocker/index.ts @@ -24,7 +24,7 @@ import * as fse from "fs-extra"; import PluginEvents = Cypress.PluginEvents; import PluginConfigOptions = Cypress.PluginConfigOptions; import { getFreePort } from "../utils/port"; -import { dockerExec, dockerLogs, dockerRun, dockerStop } from "../docker"; +import { dockerExec, dockerLogs, dockerRun, dockerStop, hostContainerName, isPodman } from "../docker"; import { HomeserverConfig, HomeserverInstance } from "../utils/homeserver"; import { StartHomeserverOpts } from "../../support/homeserver"; @@ -58,21 +58,41 @@ async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise ${outputHomeserver}`); + let hsYaml = await fse.readFile(templateHomeserver, "utf8"); hsYaml = hsYaml.replace(/{{REGISTRATION_SECRET}}/g, registrationSecret); hsYaml = hsYaml.replace(/{{MACAROON_SECRET_KEY}}/g, macaroonSecret); hsYaml = hsYaml.replace(/{{FORM_SECRET}}/g, formSecret); hsYaml = hsYaml.replace(/{{PUBLIC_BASEURL}}/g, baseUrl); hsYaml = hsYaml.replace(/{{OAUTH_SERVER_PORT}}/g, opts.oAuthServerPort?.toString()); - await fse.writeFile(path.join(tempDir, "homeserver.yaml"), hsYaml); + hsYaml = hsYaml.replace(/{{HOST_DOCKER_INTERNAL}}/g, await hostContainerName()); + if (opts.variables) { + let fetchedHostContainer = null; + for (const key in opts.variables) { + let value = String(opts.variables[key]); + + if (value === "{{HOST_DOCKER_INTERNAL}}") { + if (!fetchedHostContainer) { + fetchedHostContainer = await hostContainerName(); + } + value = fetchedHostContainer; + } + + hsYaml = hsYaml.replace(new RegExp("%" + key + "%", "g"), value); + } + } + + await fse.writeFile(outputHomeserver, hsYaml); // now generate a signing key (we could use synapse's config generation for // this, or we could just do this...) // NB. This assumes the homeserver.yaml specifies the key in this location const signingKey = randB64Bytes(32); - console.log(`Gen ${path.join(templateDir, "localhost.signing.key")}`); - await fse.writeFile(path.join(tempDir, "localhost.signing.key"), `ed25519 x ${signingKey}`); + const outputSigningKey = path.join(tempDir, "localhost.signing.key"); + console.log(`Gen -> ${outputSigningKey}`); + await fse.writeFile(outputSigningKey, `ed25519 x ${signingKey}`); return { port, @@ -82,27 +102,38 @@ async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise { const synCfg = await cfgDirFromTemplate(opts); console.log(`Starting synapse with config dir ${synCfg.configDir}...`); + const dockerSynapseParams = ["--rm", "-v", `${synCfg.configDir}:/data`, "-p", `${synCfg.port}:8008/tcp`]; + + if (await isPodman()) { + // Make host.containers.internal work to allow Synapse to talk to the + // test OIDC server. + dockerSynapseParams.push("--network"); + dockerSynapseParams.push("slirp4netns:allow_host_loopback=true"); + } else { + // Make host.docker.internal work to allow Synapse to talk to the test + // OIDC server. + dockerSynapseParams.push("--add-host"); + dockerSynapseParams.push("host.docker.internal:host-gateway"); + } + const synapseId = await dockerRun({ image: "matrixdotorg/synapse:develop", containerName: `react-sdk-cypress-synapse`, - params: [ - "--rm", - "-v", - `${synCfg.configDir}:/data`, - "-p", - `${synCfg.port}:8008/tcp`, - // make host.docker.internal work to allow Synapse to talk to the test OIDC server - "--add-host", - "host.docker.internal:host-gateway", - ], + params: dockerSynapseParams, cmd: ["run"], }); diff --git a/cypress/plugins/synapsedocker/templates/default/homeserver.yaml b/cypress/plugins/synapsedocker/templates/default/homeserver.yaml index a866d4b5be6..e51ac1918ff 100644 --- a/cypress/plugins/synapsedocker/templates/default/homeserver.yaml +++ b/cypress/plugins/synapsedocker/templates/default/homeserver.yaml @@ -81,9 +81,10 @@ oidc_providers: issuer: "http://localhost:{{OAUTH_SERVER_PORT}}/oauth" authorization_endpoint: "http://localhost:{{OAUTH_SERVER_PORT}}/oauth/auth.html" # the token endpoint receives requests from synapse, rather than the webapp, so needs to escape the docker container. - # Hence, host.docker.internal rather than localhost. - token_endpoint: "http://host.docker.internal:{{OAUTH_SERVER_PORT}}/oauth/token" - userinfo_endpoint: "http://host.docker.internal:{{OAUTH_SERVER_PORT}}/oauth/userinfo" + # Hence, HOST_DOCKER_INTERNAL rather than localhost. This is set to + # host.docker.internal on Docker and host.containers.internal on Podman. + token_endpoint: "http://{{HOST_DOCKER_INTERNAL}}:{{OAUTH_SERVER_PORT}}/oauth/token" + userinfo_endpoint: "http://{{HOST_DOCKER_INTERNAL}}:{{OAUTH_SERVER_PORT}}/oauth/userinfo" client_id: "synapse" discover: false scopes: ["profile"] diff --git a/cypress/plugins/synapsedocker/templates/email/README.md b/cypress/plugins/synapsedocker/templates/email/README.md new file mode 100644 index 00000000000..40c23ba0be4 --- /dev/null +++ b/cypress/plugins/synapsedocker/templates/email/README.md @@ -0,0 +1 @@ +A synapse configured to require an email for registration diff --git a/cypress/plugins/synapsedocker/templates/email/homeserver.yaml b/cypress/plugins/synapsedocker/templates/email/homeserver.yaml new file mode 100644 index 00000000000..fc20641ab40 --- /dev/null +++ b/cypress/plugins/synapsedocker/templates/email/homeserver.yaml @@ -0,0 +1,44 @@ +server_name: "localhost" +pid_file: /data/homeserver.pid +public_baseurl: "{{PUBLIC_BASEURL}}" +listeners: + - port: 8008 + tls: false + bind_addresses: ["::"] + type: http + x_forwarded: true + + resources: + - names: [client] + compress: false + +database: + name: "sqlite3" + args: + database: ":memory:" + +log_config: "/data/log.config" + +media_store_path: "/data/media_store" +uploads_path: "/data/uploads" +enable_registration: true +registrations_require_3pid: + - email +registration_shared_secret: "{{REGISTRATION_SECRET}}" +report_stats: false +macaroon_secret_key: "{{MACAROON_SECRET_KEY}}" +form_secret: "{{FORM_SECRET}}" +signing_key_path: "/data/localhost.signing.key" + +trusted_key_servers: + - server_name: "matrix.org" +suppress_key_server_warning: true + +ui_auth: + session_timeout: "300s" + +email: + smtp_host: "%SMTP_HOST%" + smtp_port: %SMTP_PORT% + notif_from: "Your Friendly %(app)s homeserver " + app_name: my_branded_matrix_server diff --git a/cypress/plugins/synapsedocker/templates/email/log.config b/cypress/plugins/synapsedocker/templates/email/log.config new file mode 100644 index 00000000000..ac232762da3 --- /dev/null +++ b/cypress/plugins/synapsedocker/templates/email/log.config @@ -0,0 +1,50 @@ +# Log configuration for Synapse. +# +# This is a YAML file containing a standard Python logging configuration +# dictionary. See [1] for details on the valid settings. +# +# Synapse also supports structured logging for machine readable logs which can +# be ingested by ELK stacks. See [2] for details. +# +# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema +# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html + +version: 1 + +formatters: + precise: + format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' + +handlers: + # A handler that writes logs to stderr. Unused by default, but can be used + # instead of "buffer" and "file" in the logger handlers. + console: + class: logging.StreamHandler + formatter: precise + +loggers: + synapse.storage.SQL: + # beware: increasing this to DEBUG will make synapse log sensitive + # information such as access tokens. + level: INFO + + twisted: + # We send the twisted logging directly to the file handler, + # to work around https://github.com/matrix-org/synapse/issues/3471 + # when using "buffer" logger. Use "console" to log to stderr instead. + handlers: [console] + propagate: false + +root: + level: INFO + + # Write logs to the `buffer` handler, which will buffer them together in memory, + # then write them to a file. + # + # Replace "buffer" with "console" to log to stderr instead. (Note that you'll + # also need to update the configuration for the `twisted` logger above, in + # this case.) + # + handlers: [console] + +disable_existing_loggers: false diff --git a/cypress/support/client.ts b/cypress/support/client.ts index ad3c2e239f0..44bc1487af3 100644 --- a/cypress/support/client.ts +++ b/cypress/support/client.ts @@ -26,8 +26,8 @@ import type { UploadOpts, ICreateRoomOpts, ISendEventResponse, + ReceiptType, } from "matrix-js-sdk/src/matrix"; -import type { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts"; import Chainable = Cypress.Chainable; import { UserCredentials } from "./login"; diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index 2ff0197ba65..11eae401f9e 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -40,6 +40,8 @@ import "./network"; import "./composer"; import "./proxy"; import "./axe"; +import "./mailhog"; +import "./promise"; installLogsCollector({ // specify the types of logs to collect (and report to the node console at the end of the test) diff --git a/cypress/support/homeserver.ts b/cypress/support/homeserver.ts index 15fce60350b..2e8a309a65e 100644 --- a/cypress/support/homeserver.ts +++ b/cypress/support/homeserver.ts @@ -28,6 +28,9 @@ export interface StartHomeserverOpts { /** Port of an OAuth server to configure the homeserver to use */ oAuthServerPort?: number; + + /** Additional variables to inject into the configuration template **/ + variables?: Record; } declare global { @@ -36,15 +39,22 @@ declare global { interface Chainable { /** * Start a homeserver instance with a given config template. + * * @param opts: either the template path (within cypress/plugins/{homeserver}docker/template/), or * an options object + * + * If any of opts.variables has the special value + * '{{HOST_DOCKER_INTERNAL}}', it will be replaced by + * 'host.docker.interal' if we are on Docker, or + * 'host.containers.internal' on Podman. */ startHomeserver(opts: string | StartHomeserverOpts): Chainable; /** * Custom command wrapping task:{homeserver}Stop whilst preventing uncaught exceptions * for if Homeserver stopping races with the app's background sync loop. - * @param homeserver the homeserver instance returned by start{Homeserver} + * + * @param homeserver the homeserver instance returned by {homeserver}Start (e.g. synapseStart). */ stopHomeserver(homeserver: HomeserverInstance): Chainable; diff --git a/cypress/support/mailhog.ts b/cypress/support/mailhog.ts new file mode 100644 index 00000000000..86efc5e0f66 --- /dev/null +++ b/cypress/support/mailhog.ts @@ -0,0 +1,54 @@ +/* +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 mailhog from "mailhog"; + +import Chainable = Cypress.Chainable; +import { Instance } from "../plugins/mailhog"; + +export interface Mailhog { + api: mailhog.API; + instance: Instance; +} + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Cypress { + interface Chainable { + startMailhog(): Chainable; + stopMailhog(instance: Mailhog): Chainable; + } + } +} + +Cypress.Commands.add("startMailhog", (): Chainable => { + return cy.task("mailhogStart", { log: false }).then((x) => { + Cypress.log({ name: "startHomeserver", message: `Started mailhog instance ${x.containerId}` }); + return { + api: mailhog({ + host: "localhost", + port: x.httpPort, + }), + instance: x, + }; + }); +}); + +Cypress.Commands.add("stopMailhog", (mailhog: Mailhog): Chainable => { + return cy.task("mailhogStop", mailhog.instance.containerId); +}); diff --git a/cypress/support/promise.ts b/cypress/support/promise.ts new file mode 100644 index 00000000000..4baaf75e8ea --- /dev/null +++ b/cypress/support/promise.ts @@ -0,0 +1,58 @@ +/* +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 Chainable = Cypress.Chainable; + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Cypress { + interface Chainable { + /** + * Utility wrapper around promises to help control flow in tests + * Calls `fn` function `tries` times, with a sleep of `interval` between calls. + * Ensure you do not rely on any effects of calling any `cy.*` functions within the body of `fn` + * as the calls will not happen until after waitForPromise returns. + * @param fn the function to retry + * @param tries the number of tries to call it + * @param interval the time interval between tries + */ + waitForPromise(fn: () => Promise, tries?: number, interval?: number): Chainable; + } + } +} + +function waitForPromise(fn: () => Promise, tries = 10, interval = 1000): Chainable { + return cy.then( + () => + new Cypress.Promise(async (resolve, reject) => { + for (let i = 0; i < tries; i++) { + try { + const v = await fn(); + resolve(v); + } catch { + await new Cypress.Promise((resolve) => setTimeout(resolve, interval)); + } + } + reject(); + }), + ); +} + +Cypress.Commands.add("waitForPromise", waitForPromise); + +export {}; diff --git a/cypress/support/views.ts b/cypress/support/views.ts index f31c2d7bf2a..c610af5f8b7 100644 --- a/cypress/support/views.ts +++ b/cypress/support/views.ts @@ -23,11 +23,11 @@ declare global { namespace Cypress { interface Chainable { /** - * Opens the given room by name. The room must be visible in the room list. - * It uses a start-anchored regexp to accommodate for room tiles for unread rooms containing additional - * context in their aria labels, e.g. "Room name 3 unread messages." + * Opens the given room by name. The room must be visible in the + * room list, but the room list may be folded horizontally, and the + * room may contain unread messages. * - * @param name The room name to find and click on/open. + * @param name The exact room name to find and click on/open. */ viewRoomByName(name: string): Chainable>; @@ -65,10 +65,20 @@ declare global { } Cypress.Commands.add("viewRoomByName", (name: string): Chainable> => { - return cy - .findByRole("treeitem", { name: new RegExp("^" + name) }) - .should("have.class", "mx_RoomTile") - .click(); + // We look for the room inside the room list, which is a tree called Rooms. + // + // There are 3 cases: + // - the room list is folded: + // then the aria-label on the room tile is the name (with nothing extra) + // - the room list is unfolder and the room has messages: + // then the aria-label contains the unread count, but the title of the + // div inside the titleContainer equals the room name + // - the room list is unfolded and the room has no messages: + // then the aria-label is the name and so is the title of a div + // + // So by matching EITHER title=name OR aria-label=name we find this exact + // room in all three cases. + return cy.findByRole("tree", { name: "Rooms" }).find(`[title="${name}"],[aria-label="${name}"]`).first().click(); }); Cypress.Commands.add("viewRoomById", (id: string): void => { diff --git a/docs/settings.md b/docs/settings.md index b8a575ff5ae..3f0636d3801 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -174,7 +174,7 @@ An example of a watcher in action would be: class MyComponent extends React.Component { settingWatcherRef = null; - componentWillMount() { + componentDidMount() { const callback = (settingName, roomId, level, newValAtLevel, newVal) => { this.setState({ color: newVal }); }; diff --git a/jest.config.ts b/jest.config.ts index d7f00f194eb..58bec7684e8 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -36,7 +36,12 @@ const config: Config = { "RecorderWorklet": "/__mocks__/empty.js", }, transformIgnorePatterns: ["/node_modules/(?!matrix-js-sdk).+$"], - collectCoverageFrom: ["/src/**/*.{js,ts,tsx}"], + collectCoverageFrom: [ + "/src/**/*.{js,ts,tsx}", + // getSessionLock is piped into a different JS context via stringification, and the coverage functionality is + // not available in that contest. So, turn off coverage instrumentation for it. + "!/src/utils/SessionLock.ts", + ], coverageReporters: ["text-summary", "lcov"], testResultsProcessor: "@casualbot/jest-sonar-reporter", }; diff --git a/package.json b/package.json index 9dfbdbf53bf..4ba9c8bb149 100644 --- a/package.json +++ b/package.json @@ -55,18 +55,20 @@ "coverage": "yarn test --coverage" }, "resolutions": { - "@types/react-dom": "17.0.19", - "@types/react": "17.0.58" + "@types/react-dom": "17.0.20", + "@types/react": "17.0.65" }, "dependencies": { "@babel/runtime": "^7.12.5", - "@matrix-org/analytics-events": "^0.6.0", + "@matrix-org/analytics-events": "^0.7.0", + "@matrix-org/emojibase-bindings": "^1.1.2", "@matrix-org/matrix-wysiwyg": "^2.4.1", - "@matrix-org/react-sdk-module-api": "^1.0.0", + "@matrix-org/react-sdk-module-api": "^2.1.0", + "@matrix-org/spec": "^1.7.0", "@sentry/browser": "^7.0.0", "@sentry/tracing": "^7.0.0", "@testing-library/react-hooks": "^8.0.1", - "@vector-im/compound-design-tokens": "^0.0.3", + "@vector-im/compound-design-tokens": "^0.0.4", "@vector-im/compound-web": "^0.2.3", "await-lock": "^2.1.0", "blurhash": "^1.1.3", @@ -75,8 +77,6 @@ "counterpart": "^0.18.6", "diff-dom": "^4.2.2", "diff-match-patch": "^1.0.5", - "emojibase": "15.0.0", - "emojibase-data": "15.0.0", "emojibase-regex": "15.0.0", "escape-html": "^1.0.3", "file-saver": "^2.0.5", @@ -121,6 +121,7 @@ "sanitize-html": "2.11.0", "tar-js": "^0.3.0", "ua-parser-js": "^1.0.2", + "uuid": "^9.0.0", "what-input": "^5.2.10", "zxcvbn": "^4.4.2" }, @@ -146,7 +147,7 @@ "@percy/cli": "^1.11.0", "@percy/cypress": "^3.1.2", "@testing-library/cypress": "^9.0.0", - "@testing-library/jest-dom": "^5.16.5", + "@testing-library/jest-dom": "^6.0.0", "@testing-library/react": "^12.1.5", "@testing-library/user-event": "^14.4.3", "@types/commonmark": "^0.27.4", @@ -158,17 +159,18 @@ "@types/fs-extra": "^11.0.0", "@types/geojson": "^7946.0.8", "@types/glob-to-regexp": "^0.4.1", - "@types/jest": "29.2.6", + "@types/jest": "29.5.3", "@types/katex": "^0.16.0", "@types/lodash": "^4.14.168", "@types/modernizr": "^3.5.3", "@types/node": "^16", "@types/node-fetch": "^2.6.2", "@types/pako": "^2.0.0", + "@types/prettier": "^2.7.0", "@types/qrcode": "^1.3.5", - "@types/react": "17.0.58", + "@types/react": "17.0.65", "@types/react-beautiful-dnd": "^13.0.0", - "@types/react-dom": "17.0.19", + "@types/react-dom": "17.0.20", "@types/react-transition-group": "^4.4.0", "@types/sanitize-html": "2.9.0", "@types/sdp-transform": "^2.4.6", @@ -190,7 +192,7 @@ "cypress-terminal-report": "^5.3.2", "eslint": "8.45.0", "eslint-config-google": "^0.14.0", - "eslint-config-prettier": "^8.5.0", + "eslint-config-prettier": "^9.0.0", "eslint-plugin-deprecate": "^0.7.0", "eslint-plugin-import": "^2.25.4", "eslint-plugin-jest": "^27.2.1", @@ -202,14 +204,15 @@ "express": "^4.18.2", "fetch-mock-jest": "^1.5.1", "fs-extra": "^11.0.0", - "jest": "29.3.1", - "jest-canvas-mock": "2.4.0", - "jest-environment-jsdom": "^29.2.2", - "jest-mock": "^29.2.2", + "jest": "^29.6.2", + "jest-canvas-mock": "^2.5.2", + "jest-environment-jsdom": "^29.6.2", + "jest-mock": "^29.6.2", "jest-raw-loader": "^1.0.1", "jsqr": "^1.4.0", + "mailhog": "^4.16.0", "matrix-mock-request": "^2.5.0", - "matrix-web-i18n": "^1.4.0", + "matrix-web-i18n": "^2.1.0", "mocha-junit-reporter": "^2.2.0", "node-fetch": "2", "postcss-scss": "^4.0.4", diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 37ce247a366..3b46ee5fda9 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -52,6 +52,8 @@ @import "./components/views/settings/shared/_SettingsSubsectionHeading.pcss"; @import "./components/views/spaces/_QuickThemeSwitcher.pcss"; @import "./components/views/typography/_Caption.pcss"; +@import "./components/views/utils/_Box.pcss"; +@import "./components/views/utils/_Flex.pcss"; @import "./compound/_Icon.pcss"; @import "./compound/_SuccessDialog.pcss"; @import "./structures/_AutoHideScrollbar.pcss"; @@ -88,8 +90,10 @@ @import "./structures/_UserMenu.pcss"; @import "./structures/_ViewSource.pcss"; @import "./structures/auth/_CompleteSecurity.pcss"; +@import "./structures/auth/_ConfirmSessionLockTheftView.pcss"; @import "./structures/auth/_Login.pcss"; @import "./structures/auth/_Registration.pcss"; +@import "./structures/auth/_SessionLockStolenView.pcss"; @import "./structures/auth/_SetupEncryptionBody.pcss"; @import "./views/audio_messages/_AudioPlayer.pcss"; @import "./views/audio_messages/_PlayPauseButton.pcss"; @@ -188,6 +192,7 @@ @import "./views/elements/_InteractiveTooltip.pcss"; @import "./views/elements/_InviteReason.pcss"; @import "./views/elements/_LabelledCheckbox.pcss"; +@import "./views/elements/_LanguageDropdown.pcss"; @import "./views/elements/_MiniAvatarUploader.pcss"; @import "./views/elements/_Pill.pcss"; @import "./views/elements/_PowerSelector.pcss"; @@ -210,7 +215,6 @@ @import "./views/elements/_TextWithTooltip.pcss"; @import "./views/elements/_ToggleSwitch.pcss"; @import "./views/elements/_Tooltip.pcss"; -@import "./views/elements/_TooltipButton.pcss"; @import "./views/elements/_UseCaseSelection.pcss"; @import "./views/elements/_UseCaseSelectionButton.pcss"; @import "./views/elements/_Validation.pcss"; @@ -341,6 +345,7 @@ @import "./views/settings/tabs/_SettingsSection.pcss"; @import "./views/settings/tabs/_SettingsTab.pcss"; @import "./views/settings/tabs/room/_NotificationSettingsTab.pcss"; +@import "./views/settings/tabs/room/_PeopleRoomSettingsTab.pcss"; @import "./views/settings/tabs/room/_RolesRoomSettingsTab.pcss"; @import "./views/settings/tabs/room/_SecurityRoomSettingsTab.pcss"; @import "./views/settings/tabs/user/_AppearanceUserSettingsTab.pcss"; @@ -379,7 +384,6 @@ @import "./views/voip/_LegacyCallViewSidebar.pcss"; @import "./views/voip/_VideoFeed.pcss"; @import "./voice-broadcast/atoms/_LiveBadge.pcss"; -@import "./voice-broadcast/atoms/_PlaybackControlButton.pcss"; @import "./voice-broadcast/atoms/_VoiceBroadcastControl.pcss"; @import "./voice-broadcast/atoms/_VoiceBroadcastHeader.pcss"; @import "./voice-broadcast/atoms/_VoiceBroadcastRecordingConnectionError.pcss"; diff --git a/res/css/components/views/utils/_Box.pcss b/res/css/components/views/utils/_Box.pcss new file mode 100644 index 00000000000..a8ab7e94557 --- /dev/null +++ b/res/css/components/views/utils/_Box.pcss @@ -0,0 +1,27 @@ +/* +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. +*/ + +.mx_Box--flex { + flex: var(--mx-box-flex, unset); +} + +.mx_Box--shrink { + flex-shrink: var(--mx-box-shrink, unset); +} + +.mx_Box--grow { + flex-grow: var(--mx-box-grow, unset); +} diff --git a/res/css/components/views/utils/_Flex.pcss b/res/css/components/views/utils/_Flex.pcss new file mode 100644 index 00000000000..f9cdc7e3cc4 --- /dev/null +++ b/res/css/components/views/utils/_Flex.pcss @@ -0,0 +1,23 @@ +/* +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. +*/ + +.mx_Flex { + display: var(--mx-flex-display, unset); + flex-direction: var(--mx-flex-direction, unset); + align-items: var(--mx-flex-align, unset); + justify-content: var(--mx-flex-justify, unset); + gap: var(--mx-flex-gap, unset); +} diff --git a/res/css/structures/_RightPanel.pcss b/res/css/structures/_RightPanel.pcss index 7d39968c11a..7649ce25721 100644 --- a/res/css/structures/_RightPanel.pcss +++ b/res/css/structures/_RightPanel.pcss @@ -111,8 +111,4 @@ limitations under the License. margin-right: 8px; vertical-align: middle; } - - .mx_BaseAvatar_image { - border-radius: 8px; - } } diff --git a/res/css/structures/_RoomStatusBar.pcss b/res/css/structures/_RoomStatusBar.pcss index e046e5f7fd7..d0bfa9f7f5d 100644 --- a/res/css/structures/_RoomStatusBar.pcss +++ b/res/css/structures/_RoomStatusBar.pcss @@ -25,16 +25,6 @@ limitations under the License. text-align: left; } -.mx_RoomStatusBar_typingIndicatorAvatars .mx_BaseAvatar_image { - margin-right: -12px; - border: 1px solid $background; -} - -.mx_RoomStatusBar_typingIndicatorAvatars .mx_BaseAvatar_initial { - padding-left: 1px; - padding-top: 1px; -} - .mx_RoomStatusBar_typingIndicatorRemaining { display: inline-block; color: #acacac; diff --git a/res/css/structures/_SpaceHierarchy.pcss b/res/css/structures/_SpaceHierarchy.pcss index 81498af176a..ca129fdbac9 100644 --- a/res/css/structures/_SpaceHierarchy.pcss +++ b/res/css/structures/_SpaceHierarchy.pcss @@ -108,12 +108,6 @@ limitations under the License. } } - .mx_SpaceHierarchy_subspace { - .mx_BaseAvatar_image { - border-radius: 8px; - } - } - .mx_SpaceHierarchy_subspace_toggle { position: absolute; left: -1px; diff --git a/res/css/structures/_SpacePanel.pcss b/res/css/structures/_SpacePanel.pcss index 76c328fa568..02f6f50363d 100644 --- a/res/css/structures/_SpacePanel.pcss +++ b/res/css/structures/_SpacePanel.pcss @@ -232,10 +232,6 @@ limitations under the License. transform: rotate(45deg); } - .mx_BaseAvatar_image { - border-radius: 8px; - } - .mx_SpaceButton_menuButton { width: 20px; min-width: 20px; /* yay flex */ @@ -269,19 +265,6 @@ limitations under the License. min-width: 0; flex-grow: 1; - .mx_BaseAvatar:not(.mx_UserMenu_userAvatar_BaseAvatar) .mx_BaseAvatar_initial { - color: $secondary-content; - border-radius: 8px; - background-color: $panel-actions; - font-size: $font-15px !important; /* override inline style */ - font-weight: var(--cpd-font-weight-semibold); - line-height: $font-18px; - - & + .mx_BaseAvatar_image { - visibility: hidden; - } - } - .mx_SpaceTreeLevel { // Indent subspaces padding-left: 16px; @@ -290,6 +273,7 @@ limitations under the License. .mx_SpaceButton_avatarWrapper { position: relative; + line-height: 0; } .mx_SpacePanel_badgeContainer { diff --git a/res/css/structures/_SpaceRoomView.pcss b/res/css/structures/_SpaceRoomView.pcss index f1bf0cf2141..dff60c3fb5d 100644 --- a/res/css/structures/_SpaceRoomView.pcss +++ b/res/css/structures/_SpaceRoomView.pcss @@ -143,10 +143,6 @@ limitations under the License. .mx_BaseAvatar { width: 80px; - - .mx_BaseAvatar_image { - border-radius: 12px; - } } } diff --git a/res/css/voice-broadcast/atoms/_PlaybackControlButton.pcss b/res/css/structures/auth/_ConfirmSessionLockTheftView.pcss similarity index 70% rename from res/css/voice-broadcast/atoms/_PlaybackControlButton.pcss rename to res/css/structures/auth/_ConfirmSessionLockTheftView.pcss index fd2c3ad73c5..14b92daaa81 100644 --- a/res/css/voice-broadcast/atoms/_PlaybackControlButton.pcss +++ b/res/css/structures/auth/_ConfirmSessionLockTheftView.pcss @@ -1,5 +1,5 @@ /* -Copyright 2022 The Matrix.org Foundation C.I.C. +Copyright 2019-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. @@ -14,13 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_BroadcastPlaybackControlButton { - align-items: center; - background-color: $background; - border-radius: 50%; +.mx_ConfirmSessionLockTheftView { + width: 100%; + height: 100%; display: flex; - height: 32px; + align-items: center; justify-content: center; - margin-bottom: $spacing-8; - width: 32px; +} + +.mx_ConfirmSessionLockTheftView_body { + display: flex; + flex-direction: column; + max-width: 400px; + align-items: center; } diff --git a/res/css/structures/auth/_SessionLockStolenView.pcss b/res/css/structures/auth/_SessionLockStolenView.pcss new file mode 100644 index 00000000000..e9ab0d95ffa --- /dev/null +++ b/res/css/structures/auth/_SessionLockStolenView.pcss @@ -0,0 +1,30 @@ +/* +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. +*/ + +.mx_SessionLockStolenView { + h1 { + font-weight: var(--cpd-font-weight-semibold); + font-size: $font-32px; + text-align: center; + } + + h2 { + margin: 0; + font-weight: 500; + font-size: $font-24px; + text-align: center; + } +} diff --git a/res/css/views/avatars/_BaseAvatar.pcss b/res/css/views/avatars/_BaseAvatar.pcss index 70c41d0b251..52fd8452d3e 100644 --- a/res/css/views/avatars/_BaseAvatar.pcss +++ b/res/css/views/avatars/_BaseAvatar.pcss @@ -14,57 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_BaseAvatar { - position: relative; - /* In at least Firefox, the case of relative positioned inline elements */ - /* (such as mx_BaseAvatar) with absolute positioned children (such as */ - /* mx_BaseAvatar_initial) is a dark corner full of spider webs. It will give */ - /* different results during full reflow of the page vs. incremental reflow */ - /* of small portions. While that's surely a browser bug, we can avoid it by */ - /* using `inline-block` instead of the default `inline`. */ - /* https://github.com/vector-im/element-web/issues/5594 */ - /* https://bugzilla.mozilla.org/show_bug.cgi?id=1535053 */ - /* https://bugzilla.mozilla.org/show_bug.cgi?id=255139 */ - display: inline-block; - user-select: none; - - &.mx_RoomAvatar_isSpaceRoom { - &.mx_BaseAvatar_image, - .mx_BaseAvatar_image { - border-radius: 8px; - } - } -} - -.mx_BaseAvatar_initial { - position: absolute; - left: 0; - color: $avatar-initial-color; - text-align: center; - speak: none; - pointer-events: none; - font-weight: normal; -} - -.mx_BaseAvatar_image { - object-fit: cover; - aspect-ratio: 1; - border-radius: 125px; - vertical-align: top; - background-color: $background; -} - /* Percy screenshot test specific CSS */ @media only percy { /* Stick the default room avatar colour, so it doesn't cause a false diff on the screenshot */ - .mx_BaseAvatar_initial { + .mx_BaseAvatar { background-color: var(--percy-color-avatar) !important; - border-radius: 125px; - } - .mx_RoomAvatar_isSpaceRoom .mx_BaseAvatar_initial { - border-radius: 8px; - } - .mx_BaseAvatar_initial + .mx_BaseAvatar_image { - visibility: hidden; + color: white !important; } } diff --git a/res/css/views/avatars/_DecoratedRoomAvatar.pcss b/res/css/views/avatars/_DecoratedRoomAvatar.pcss index a0770c3ca0b..2d430981a35 100644 --- a/res/css/views/avatars/_DecoratedRoomAvatar.pcss +++ b/res/css/views/avatars/_DecoratedRoomAvatar.pcss @@ -18,6 +18,7 @@ limitations under the License. .mx_ExtraTile { position: relative; contain: content; + line-height: 0; &.mx_DecoratedRoomAvatar_cutout .mx_BaseAvatar { mask-image: url("$(res)/img/element-icons/roomlist/decorated-avatar-mask.svg"); @@ -29,10 +30,9 @@ limitations under the License. .mx_DecoratedRoomAvatar_icon { position: absolute; /* the following percentage based sizings are to match the scalable svg mask for the cutout */ - bottom: -6.25%; - right: -6.25%; - margin: 12.5%; - width: 25%; + bottom: 6.25%; // 2px for a 32x32 avatar + right: 6.25%; + width: 25%; // 8px for a 32x32 avatar height: 25%; border-radius: 50%; } diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.pcss b/res/css/views/dialogs/_AddExistingToSpaceDialog.pcss index 7866bac1c11..25e75911670 100644 --- a/res/css/views/dialogs/_AddExistingToSpaceDialog.pcss +++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.pcss @@ -157,12 +157,6 @@ limitations under the License. .mx_SubspaceSelector { display: flex; - .mx_BaseAvatar_image { - border-radius: 8px; - margin: 0; - vertical-align: unset; - } - .mx_BaseAvatar { display: inline-flex; margin: auto 16px auto 5px; @@ -228,16 +222,10 @@ limitations under the License. display: flex; margin-top: 12px; - .mx_DecoratedRoomAvatar, /* we can't target .mx_BaseAvatar here as it'll break the decorated avatar styling */ - .mx_BaseAvatar.mx_RoomAvatar_isSpaceRoom { + .mx_DecoratedRoomAvatar, /* we can't target .mx_BaseAvatar here as it'll break the decorated avatar styling */ { margin-right: 12px; } - img.mx_RoomAvatar_isSpaceRoom, - .mx_RoomAvatar_isSpaceRoom img { - border-radius: 8px; - } - .mx_AddExistingToSpace_entry_name { font-size: $font-15px; line-height: 30px; diff --git a/res/css/views/dialogs/_CompoundDialog.pcss b/res/css/views/dialogs/_CompoundDialog.pcss index 6777b4f81d4..70ba1f8c10c 100644 --- a/res/css/views/dialogs/_CompoundDialog.pcss +++ b/res/css/views/dialogs/_CompoundDialog.pcss @@ -58,12 +58,13 @@ limitations under the License. display: flex; flex-direction: column; min-height: 0; - max-height: 100%; + flex: 1; } .mx_CompoundDialog_content { overflow: auto; padding: 8px 32px; + flex: 1; } .mx_CompoundDialog_footer { diff --git a/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.pcss b/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.pcss index 1082e500055..501d7a2aaf6 100644 --- a/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.pcss +++ b/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.pcss @@ -66,11 +66,6 @@ limitations under the License. flex-grow: 1; } - img.mx_RoomAvatar_isSpaceRoom, - .mx_RoomAvatar_isSpaceRoom img { - border-radius: 4px; - } - .mx_ManageRestrictedJoinRuleDialog_entry_name { margin: 0 8px; font-size: $font-15px; @@ -98,10 +93,6 @@ limitations under the License. .mx_BaseAvatar { margin-right: 12px; } - - .mx_BaseAvatar_image { - border-radius: 8px; - } } .mx_ManageRestrictedJoinRuleDialog_section_info { diff --git a/res/css/views/dialogs/_RoomSettingsDialog.pcss b/res/css/views/dialogs/_RoomSettingsDialog.pcss index 3c2cfa3e1a9..78feacce114 100644 --- a/res/css/views/dialogs/_RoomSettingsDialog.pcss +++ b/res/css/views/dialogs/_RoomSettingsDialog.pcss @@ -53,6 +53,10 @@ limitations under the License. mask-image: url("$(res)/img/element-icons/room/message-bar/emoji.svg"); } +.mx_RoomSettingsDialog_peopleIcon::before { + mask-image: url("$(res)/img/element-icons/group-members.svg"); +} + .mx_RoomSettingsDialog .mx_Dialog_title { -ms-text-overflow: ellipsis; text-overflow: ellipsis; diff --git a/res/css/views/dialogs/_SpotlightDialog.pcss b/res/css/views/dialogs/_SpotlightDialog.pcss index 4e811ecef10..5c2f5b67174 100644 --- a/res/css/views/dialogs/_SpotlightDialog.pcss +++ b/res/css/views/dialogs/_SpotlightDialog.pcss @@ -97,7 +97,11 @@ limitations under the License. } &.mx_SpotlightDialog_filterPublicRooms::before { - mask-image: url("$(res)/img/element-icons/roomlist/explore.svg"); + mask-image: url("$(res)/img/element-icons/roomlist/hash-circle.svg"); + } + + &.mx_SpotlightDialog_filterPublicSpaces::before { + mask-image: url("$(res)/img/element-icons/spaces.svg"); } .mx_SpotlightDialog_filter--close { @@ -408,6 +412,7 @@ limitations under the License. .mx_SpotlightDialog_startChat, .mx_SpotlightDialog_joinRoomAlias, .mx_SpotlightDialog_explorePublicRooms, + .mx_SpotlightDialog_explorePublicSpaces, .mx_SpotlightDialog_startGroupChat { padding-left: $spacing-32; position: relative; @@ -436,7 +441,11 @@ limitations under the License. } .mx_SpotlightDialog_explorePublicRooms::before { - mask-image: url("$(res)/img/element-icons/roomlist/explore.svg"); + mask-image: url("$(res)/img/element-icons/roomlist/hash-circle.svg"); + } + + .mx_SpotlightDialog_explorePublicSpaces::before { + mask-image: url("$(res)/img/element-icons/spaces.svg"); } .mx_SpotlightDialog_startGroupChat::before { diff --git a/res/css/views/elements/_AccessibleButton.pcss b/res/css/views/elements/_AccessibleButton.pcss index 35a5287fa9e..172d8fc053f 100644 --- a/res/css/views/elements/_AccessibleButton.pcss +++ b/res/css/views/elements/_AccessibleButton.pcss @@ -20,6 +20,8 @@ limitations under the License. &.mx_AccessibleButton_disabled { cursor: not-allowed; + &.mx_AccessibleButton_kind_icon_primary, + &.mx_AccessibleButton_kind_icon_primary_outline, &.mx_AccessibleButton_kind_primary, &.mx_AccessibleButton_kind_primary_outline, &.mx_AccessibleButton_kind_primary_sm, @@ -80,29 +82,37 @@ limitations under the License. } } - &.mx_AccessibleButton_kind_icon { + &.mx_AccessibleButton_kind_icon, + &.mx_AccessibleButton_kind_icon_primary, + &.mx_AccessibleButton_kind_icon_primary_outline { padding: 0; height: 32px; width: 32px; } } + &.mx_AccessibleButton_kind_icon_primary, + &.mx_AccessibleButton_kind_icon_primary_outline, &.mx_AccessibleButton_kind_primary, &.mx_AccessibleButton_kind_primary_outline, &.mx_AccessibleButton_kind_secondary { font-weight: var(--cpd-font-weight-semibold); } + &.mx_AccessibleButton_kind_icon_primary, + &.mx_AccessibleButton_kind_icon_primary_outline, &.mx_AccessibleButton_kind_primary, &.mx_AccessibleButton_kind_primary_outline { border: 1px solid $accent; } + &.mx_AccessibleButton_kind_icon_primary, &.mx_AccessibleButton_kind_primary { color: $button-primary-fg-color; background-color: $accent; } + &.mx_AccessibleButton_kind_icon_primary_outline, &.mx_AccessibleButton_kind_primary_outline { color: $accent; } diff --git a/res/css/views/elements/_FacePile.pcss b/res/css/views/elements/_FacePile.pcss index 03b736a73e8..2976873b1aa 100644 --- a/res/css/views/elements/_FacePile.pcss +++ b/res/css/views/elements/_FacePile.pcss @@ -29,14 +29,10 @@ limitations under the License. margin-right: -8px; } - .mx_BaseAvatar_image { + .mx_BaseAvatar { border: 1px solid var(--facepile-background, $background); } - .mx_BaseAvatar_initial { - margin: 1px; /* to offset the border on the image */ - } - .mx_FacePile_more { position: relative; border-radius: 100%; diff --git a/res/css/views/elements/_GenericEventListSummary.pcss b/res/css/views/elements/_GenericEventListSummary.pcss index cfb62d0d37b..f05a15b44d2 100644 --- a/res/css/views/elements/_GenericEventListSummary.pcss +++ b/res/css/views/elements/_GenericEventListSummary.pcss @@ -31,6 +31,11 @@ limitations under the License. } } + .mx_GenericEventListSummary_toggle { + // We reuse a title cased translation + text-transform: lowercase; + } + &[data-layout="irc"], &[data-layout="group"] { .mx_GenericEventListSummary_toggle { diff --git a/res/css/views/elements/_LanguageDropdown.pcss b/res/css/views/elements/_LanguageDropdown.pcss new file mode 100644 index 00000000000..60f406af73a --- /dev/null +++ b/res/css/views/elements/_LanguageDropdown.pcss @@ -0,0 +1,21 @@ +/* +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. +*/ + +.mx_LanguageDropdown { + .mx_Dropdown_option > div { + text-transform: capitalize; + } +} diff --git a/res/css/views/elements/_MiniAvatarUploader.pcss b/res/css/views/elements/_MiniAvatarUploader.pcss index b28886a103b..01616cd3dde 100644 --- a/res/css/views/elements/_MiniAvatarUploader.pcss +++ b/res/css/views/elements/_MiniAvatarUploader.pcss @@ -43,6 +43,8 @@ limitations under the License. border-radius: 50%; z-index: 1; + line-height: 0; + .mx_MiniAvatarUploader_cameraIcon { height: 100%; width: 100%; diff --git a/res/css/views/elements/_TooltipButton.pcss b/res/css/views/elements/_TooltipButton.pcss deleted file mode 100644 index cc91c6d112c..00000000000 --- a/res/css/views/elements/_TooltipButton.pcss +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright 2017 New Vector Ltd. -Copyright 2019 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_TooltipButton { - display: inline-block; - width: 11px; - height: 11px; - margin-left: 5px; - - border: 2px solid $neutral-badge-color; - border-radius: 20px; - color: $neutral-badge-color; - - transition: opacity 0.2s ease-in; - opacity: 0.6; - - line-height: $font-11px; - text-align: center; - - cursor: pointer; -} - -.mx_TooltipButton:hover { - opacity: 1; -} - -.mx_TooltipButton_container { - position: relative; - top: -18px; - left: 4px; -} - -.mx_TooltipButton_helpText { - width: 400px; - text-align: start; - line-height: 17px !important; -} diff --git a/res/css/views/messages/_DateSeparator.pcss b/res/css/views/messages/_DateSeparator.pcss index 0a25cccaafd..52d263f6888 100644 --- a/res/css/views/messages/_DateSeparator.pcss +++ b/res/css/views/messages/_DateSeparator.pcss @@ -40,6 +40,7 @@ limitations under the License. font-size: inherit; font-weight: inherit; color: inherit; + text-transform: capitalize; } .mx_DateSeparator_jumpToDateMenu { diff --git a/res/css/views/right_panel/_RoomSummaryCard.pcss b/res/css/views/right_panel/_RoomSummaryCard.pcss index 4bd5f2ee0ff..0c11cab73b4 100644 --- a/res/css/views/right_panel/_RoomSummaryCard.pcss +++ b/res/css/views/right_panel/_RoomSummaryCard.pcss @@ -123,11 +123,6 @@ limitations under the License. text-overflow: ellipsis; overflow: hidden; - .mx_BaseAvatar_image { - vertical-align: top; - margin-right: 12px; - } - span { color: $primary-content; } diff --git a/res/css/views/right_panel/_UserInfo.pcss b/res/css/views/right_panel/_UserInfo.pcss index 12d74915a2d..c0b8f16c6b6 100644 --- a/res/css/views/right_panel/_UserInfo.pcss +++ b/res/css/views/right_panel/_UserInfo.pcss @@ -95,49 +95,14 @@ limitations under the License. .mx_UserInfo_avatar_transition { max-width: 30vh; + aspect-ratio: 1 / 1; margin: 0 auto; transition: 0.5s; - .mx_UserInfo_avatar_transition_child { - /* use padding-top instead of height to make this element square, - as the % in padding is a % of the width (including margin, - that's why we had to put the margin to center on a parent div), - and not a % of the parent height. */ - padding-top: 100%; - position: relative; - - .mx_BaseAvatar, - .mx_BaseAvatar_initial, - .mx_BaseAvatar_image { - border-radius: 100%; - position: absolute; - top: 0; - left: 0; - width: 100% !important; - height: 100% !important; - } - - .mx_BaseAvatar { - &.mx_BaseAvatar_image { - cursor: zoom-in; - } - - .mx_BaseAvatar_initial { - z-index: 1; - display: flex; - align-items: center; - justify-content: center; - - /* override the calculated sizes so that the letter isn't HUGE */ - font-size: 6rem !important; - width: 100% !important; - transition: font-size 0.5s; - - & + .mx_BaseAvatar_image { - cursor: default; - } - } - } + .mx_BaseAvatar, + .mx_BaseAvatar img { + width: 100%; + height: 100%; } } } @@ -285,14 +250,6 @@ limitations under the License. max-width: 72px; margin: 0 auto; } - - .mx_UserInfo_avatar_transition_child { - .mx_BaseAvatar { - .mx_BaseAvatar_initial { - font-size: 40px !important; /* override the other override because here the avatar is smaller */ - } - } - } } } } diff --git a/res/css/views/rooms/_EntityTile.pcss b/res/css/views/rooms/_EntityTile.pcss index a2ce91037d9..9632946bd59 100644 --- a/res/css/views/rooms/_EntityTile.pcss +++ b/res/css/views/rooms/_EntityTile.pcss @@ -67,6 +67,7 @@ limitations under the License. padding-top: 4px; padding-bottom: 4px; position: relative; + line-height: 0; } .mx_EntityTile_name { diff --git a/res/css/views/rooms/_EventTile.pcss b/res/css/views/rooms/_EventTile.pcss index 7d68dc98d9e..8b8d370ac75 100644 --- a/res/css/views/rooms/_EventTile.pcss +++ b/res/css/views/rooms/_EventTile.pcss @@ -571,6 +571,7 @@ $left-gutter: 64px; .mx_EventTile_avatar, .mx_EventTile_e2eIcon { + line-height: 1; margin: $spacing-block-start 0 $spacing-block-end; } diff --git a/res/css/views/rooms/_LegacyRoomHeader.pcss b/res/css/views/rooms/_LegacyRoomHeader.pcss index 17f1dfec912..1e36e0887b7 100644 --- a/res/css/views/rooms/_LegacyRoomHeader.pcss +++ b/res/css/views/rooms/_LegacyRoomHeader.pcss @@ -163,10 +163,6 @@ limitations under the License. cursor: pointer; } -.mx_LegacyRoomHeader_avatar .mx_BaseAvatar_image { - object-fit: cover; -} - .mx_LegacyRoomHeader_button { cursor: pointer; flex: 0 0 auto; diff --git a/res/css/views/rooms/_MemberInfo.pcss b/res/css/views/rooms/_MemberInfo.pcss index c963c7ed28d..f309d37e6d9 100644 --- a/res/css/views/rooms/_MemberInfo.pcss +++ b/res/css/views/rooms/_MemberInfo.pcss @@ -96,7 +96,7 @@ limitations under the License. display: block; } - .mx_BaseAvatar.mx_BaseAvatar_image { + .mx_BaseAvatar img { cursor: zoom-in; } } diff --git a/res/css/views/rooms/_RoomHeader.pcss b/res/css/views/rooms/_RoomHeader.pcss index 3937e10584d..e5456494642 100644 --- a/res/css/views/rooms/_RoomHeader.pcss +++ b/res/css/views/rooms/_RoomHeader.pcss @@ -15,17 +15,14 @@ limitations under the License. */ .mx_RoomHeader { - display: flex; - align-items: center; height: 64px; - gap: var(--cpd-space-3x); padding: 0 var(--cpd-space-3x); border-bottom: 1px solid $separator; background-color: $background; +} - &:hover { - cursor: pointer; - } +.mx_RoomHeader_info { + cursor: pointer; } .mx_RoomHeader_topic { @@ -39,7 +36,7 @@ limitations under the License. word-break: break-all; text-overflow: ellipsis; - transition: all var(--transition-standard) ease; + transition: all var(--transition-standard) ease 0.1s; } .mx_RoomHeader:hover .mx_RoomHeader_topic { diff --git a/res/css/views/rooms/_RoomPreviewCard.pcss b/res/css/views/rooms/_RoomPreviewCard.pcss index 087e71bdd73..241b6f37ff1 100644 --- a/res/css/views/rooms/_RoomPreviewCard.pcss +++ b/res/css/views/rooms/_RoomPreviewCard.pcss @@ -70,13 +70,6 @@ limitations under the License. display: flex; align-items: center; - .mx_RoomAvatar_isSpaceRoom { - &.mx_BaseAvatar_image, - .mx_BaseAvatar_image { - border-radius: 12px; - } - } - .mx_RoomPreviewCard_video { width: 50px; height: 50px; diff --git a/res/css/views/rooms/_WhoIsTypingTile.pcss b/res/css/views/rooms/_WhoIsTypingTile.pcss index be07862f75c..b981526e58f 100644 --- a/res/css/views/rooms/_WhoIsTypingTile.pcss +++ b/res/css/views/rooms/_WhoIsTypingTile.pcss @@ -31,10 +31,6 @@ limitations under the License. margin-left: -12px; } -.mx_WhoIsTypingTile_avatars .mx_BaseAvatar_initial { - padding-top: 1px; -} - .mx_WhoIsTypingTile_avatars .mx_BaseAvatar { border: 1px solid $background; border-radius: 40px; diff --git a/res/css/views/settings/_JoinRuleSettings.pcss b/res/css/views/settings/_JoinRuleSettings.pcss index de06580ea7e..62debe28a13 100644 --- a/res/css/views/settings/_JoinRuleSettings.pcss +++ b/res/css/views/settings/_JoinRuleSettings.pcss @@ -39,11 +39,6 @@ limitations under the License. color: $secondary-content; display: inline-block; - img.mx_RoomAvatar_isSpaceRoom, - .mx_RoomAvatar_isSpaceRoom img { - border-radius: 8px; - } - .mx_BaseAvatar { margin-right: 8px; } diff --git a/res/css/views/settings/tabs/room/_NotificationSettingsTab.pcss b/res/css/views/settings/tabs/room/_NotificationSettingsTab.pcss index fead9e430a1..92fc9d599ed 100644 --- a/res/css/views/settings/tabs/room/_NotificationSettingsTab.pcss +++ b/res/css/views/settings/tabs/room/_NotificationSettingsTab.pcss @@ -69,3 +69,7 @@ limitations under the License. mask-image: url("$(res)/img/element-icons/roomlist/notifications-off.svg"); } } + +input[type="file"].mx_NotificationSound_soundUpload { + display: none; +} diff --git a/res/css/views/settings/tabs/room/_PeopleRoomSettingsTab.pcss b/res/css/views/settings/tabs/room/_PeopleRoomSettingsTab.pcss new file mode 100644 index 00000000000..0b9c5c00a28 --- /dev/null +++ b/res/css/views/settings/tabs/room/_PeopleRoomSettingsTab.pcss @@ -0,0 +1,56 @@ +/* +Copyright 2023 Nordeck IT + Consulting GmbH + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_PeopleRoomSettingsTab_knock { + display: flex; + margin-top: var(--cpd-space-2x); +} + +.mx_PeopleRoomSettingsTab_content { + flex-grow: 1; + margin: 0 var(--cpd-space-4x); +} + +.mx_PeopleRoomSettingsTab_name { + font-weight: var(--cpd-font-weight-semibold); +} + +.mx_PeopleRoomSettingsTab_timestamp { + color: $secondary-content; + margin-left: var(--cpd-space-1x); +} + +.mx_PeopleRoomSettingsTab_userId { + color: $secondary-content; + display: block; + font-size: var(--cpd-font-size-body-sm); +} + +.mx_PeopleRoomSettingsTab_seeMoreOrLess { + margin: var(--cpd-space-3x) 0 0; +} + +.mx_PeopleRoomSettingsTab_action { + flex-shrink: 0; + + + .mx_PeopleRoomSettingsTab_action { + margin-left: var(--cpd-space-3x); + } +} + +.mx_PeopleRoomSettingsTab_paragraph { + margin: 0; +} diff --git a/res/img/element-icons/spaces.svg b/res/img/element-icons/spaces.svg new file mode 100644 index 00000000000..7183b4eca9a --- /dev/null +++ b/res/img/element-icons/spaces.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/feather-customised/check.svg b/res/img/feather-customised/check.svg index 5c600f8649e..85cd1965117 100644 --- a/res/img/feather-customised/check.svg +++ b/res/img/feather-customised/check.svg @@ -1,3 +1,3 @@ - + diff --git a/res/img/feather-customised/x.svg b/res/img/feather-customised/x.svg index 5468caa8aa1..a4f6c4a81a7 100644 --- a/res/img/feather-customised/x.svg +++ b/res/img/feather-customised/x.svg @@ -1,4 +1,4 @@ - + diff --git a/res/themes/legacy-light/css/_legacy-light.pcss b/res/themes/legacy-light/css/_legacy-light.pcss index ba18532d737..a48301009c3 100644 --- a/res/themes/legacy-light/css/_legacy-light.pcss +++ b/res/themes/legacy-light/css/_legacy-light.pcss @@ -186,14 +186,14 @@ $call-background: #15191e; $call-primary-content: #ffffff; $call-light-quaternary-content: #c1c6cd; -$username-variant1-color: var(--cpd-color-blue-900); -$username-variant2-color: var(--cpd-color-fuchsia-900); -$username-variant3-color: var(--cpd-color-green-900); -$username-variant4-color: var(--cpd-color-pink-900); -$username-variant5-color: var(--cpd-color-orange-900); -$username-variant6-color: var(--cpd-color-cyan-900); -$username-variant7-color: var(--cpd-color-purple-900); -$username-variant8-color: var(--cpd-color-lime-900); +$username-variant1-color: var(--cpd-color-blue-1200); +$username-variant2-color: var(--cpd-color-fuchsia-1200); +$username-variant3-color: var(--cpd-color-green-1200); +$username-variant4-color: var(--cpd-color-pink-1200); +$username-variant5-color: var(--cpd-color-orange-1200); +$username-variant6-color: var(--cpd-color-cyan-1200); +$username-variant7-color: var(--cpd-color-purple-1200); +$username-variant8-color: var(--cpd-color-lime-1200); /** * Creating a `semantic` color scale. This will not be needed with the new diff --git a/res/themes/light-high-contrast/css/_light-high-contrast.pcss b/res/themes/light-high-contrast/css/_light-high-contrast.pcss index f82284089ba..1562db5eab5 100644 --- a/res/themes/light-high-contrast/css/_light-high-contrast.pcss +++ b/res/themes/light-high-contrast/css/_light-high-contrast.pcss @@ -8,14 +8,14 @@ $tertiary-content: var(--cpd-color-gray-800); $quaternary-content: var(--cpd-color-gray-600); $quinary-content: var(--cpd-color-gray-400); -$username-variant1-color: var(--cpd-color-blue-900); -$username-variant2-color: var(--cpd-color-fuchsia-900); -$username-variant3-color: var(--cpd-color-green-900); -$username-variant4-color: var(--cpd-color-pink-900); -$username-variant5-color: var(--cpd-color-orange-900); -$username-variant6-color: var(--cpd-color-cyan-900); -$username-variant7-color: var(--cpd-color-purple-900); -$username-variant8-color: var(--cpd-color-lime-900); +$username-variant1-color: var(--cpd-color-blue-1200); +$username-variant2-color: var(--cpd-color-fuchsia-1200); +$username-variant3-color: var(--cpd-color-green-1200); +$username-variant4-color: var(--cpd-color-pink-1200); +$username-variant5-color: var(--cpd-color-orange-1200); +$username-variant6-color: var(--cpd-color-cyan-1200); +$username-variant7-color: var(--cpd-color-purple-1200); +$username-variant8-color: var(--cpd-color-lime-1200); $accent-alt: $links; $input-border-color: $secondary-content; diff --git a/res/themes/light/css/_light.pcss b/res/themes/light/css/_light.pcss index f40a56ddbb9..e4428ac1810 100644 --- a/res/themes/light/css/_light.pcss +++ b/res/themes/light/css/_light.pcss @@ -36,14 +36,14 @@ $alert: var(--cpd-color-text-critical-primary); $links: var(--cpd-color-text-link-external); $link-external: var(--cpd-color-text-link-external); -$username-variant1-color: var(--cpd-color-blue-900); -$username-variant2-color: var(--cpd-color-fuchsia-900); -$username-variant3-color: var(--cpd-color-green-900); -$username-variant4-color: var(--cpd-color-pink-900); -$username-variant5-color: var(--cpd-color-orange-900); -$username-variant6-color: var(--cpd-color-cyan-900); -$username-variant7-color: var(--cpd-color-purple-900); -$username-variant8-color: var(--cpd-color-lime-900); +$username-variant1-color: var(--cpd-color-blue-1200); +$username-variant2-color: var(--cpd-color-fuchsia-1200); +$username-variant3-color: var(--cpd-color-green-1200); +$username-variant4-color: var(--cpd-color-pink-1200); +$username-variant5-color: var(--cpd-color-orange-1200); +$username-variant6-color: var(--cpd-color-cyan-1200); +$username-variant7-color: var(--cpd-color-purple-1200); +$username-variant8-color: var(--cpd-color-lime-1200); /* ******************** */ /** diff --git a/scripts/check-i18n.pl b/scripts/check-i18n.pl deleted file mode 100755 index fa11bc52924..00000000000 --- a/scripts/check-i18n.pl +++ /dev/null @@ -1,192 +0,0 @@ -#!/usr/bin/perl - -use strict; -use warnings; -use Cwd 'abs_path'; - -# script which checks how out of sync the i18ns are drifting - -# example i18n format: -# "%(oneUser)sleft": "%(oneUser)sleft", - -$|=1; - -$0 =~ /^(.*\/)/; -my $i18ndir = abs_path($1."/../src/i18n/strings"); -my $srcdir = abs_path($1."/../src"); - -my $en = read_i18n($i18ndir."/en_EN.json"); - -my $src_strings = read_src_strings($srcdir); -my $src = {}; - -print "Checking strings in src\n"; -foreach my $tuple (@$src_strings) { - my ($s, $file) = (@$tuple); - $src->{$s} = $file; - if (!$en->{$s}) { - if ($en->{$s . '.'}) { - printf ("%50s %24s\t%s\n", $file, "en_EN has fullstop!", $s); - } - else { - $s =~ /^(.*)\.?$/; - if ($en->{$1}) { - printf ("%50s %24s\t%s\n", $file, "en_EN lacks fullstop!", $s); - } - else { - printf ("%50s %24s\t%s\n", $file, "Translation missing!", $s); - } - } - } -} - -print "\nChecking en_EN\n"; -my $count = 0; -my $remaining_src = {}; -foreach (keys %$src) { $remaining_src->{$_}++ }; - -foreach my $k (sort keys %$en) { - # crappy heuristic to ignore country codes for now... - next if ($k =~ /^(..|..-..)$/); - - if ($en->{$k} ne $k) { - printf ("%50s %24s\t%s\n", "en_EN", "en_EN is not symmetrical", $k); - } - - if (!$src->{$k}) { - if ($src->{$k. '.'}) { - printf ("%50s %24s\t%s\n", $src->{$k. '.'}, "src has fullstop!", $k); - } - else { - $k =~ /^(.*)\.?$/; - if ($src->{$1}) { - printf ("%50s %24s\t%s\n", $src->{$1}, "src lacks fullstop!", $k); - } - else { - printf ("%50s %24s\t%s\n", '???', "Not present in src?", $k); - } - } - } - else { - $count++; - delete $remaining_src->{$k}; - } -} -printf ("$count/" . (scalar keys %$src) . " strings found in src are present in en_EN\n"); -foreach (keys %$remaining_src) { - print "missing: $_\n"; -} - -opendir(DIR, $i18ndir) || die $!; -my @files = readdir(DIR); -closedir(DIR); -foreach my $lang (grep { -f "$i18ndir/$_" && !/(basefile|en_EN)\.json/ } @files) { - print "\nChecking $lang\n"; - - my $map = read_i18n($i18ndir."/".$lang); - my $count = 0; - - my $remaining_en = {}; - foreach (keys %$en) { $remaining_en->{$_}++ }; - - foreach my $k (sort keys %$map) { - { - no warnings 'uninitialized'; - my $vars = {}; - while ($k =~ /%\((.*?)\)s/g) { - $vars->{$1}++; - } - while ($map->{$k} =~ /%\((.*?)\)s/g) { - $vars->{$1}--; - } - foreach my $var (keys %$vars) { - if ($vars->{$var} != 0) { - printf ("%10s %24s\t%s\n", $lang, "Broken var ($var)s", $k); - } - } - } - - if ($en->{$k}) { - if ($map->{$k} eq $k) { - printf ("%10s %24s\t%s\n", $lang, "Untranslated string?", $k); - } - $count++; - delete $remaining_en->{$k}; - } - else { - if ($en->{$k . "."}) { - printf ("%10s %24s\t%s\n", $lang, "en_EN has fullstop!", $k); - next; - } - - $k =~ /^(.*)\.?$/; - if ($en->{$1}) { - printf ("%10s %24s\t%s\n", $lang, "en_EN lacks fullstop!", $k); - next; - } - - printf ("%10s %24s\t%s\n", $lang, "Not present in en_EN", $k); - } - } - - if (scalar keys %$remaining_en < 100) { - foreach (keys %$remaining_en) { - printf ("%10s %24s\t%s\n", $lang, "Not yet translated", $_); - } - } - - printf ("$count/" . (scalar keys %$en) . " strings translated\n"); -} - -sub read_i18n { - my $path = shift; - my $map = {}; - $path =~ /.*\/(.*)$/; - my $lang = $1; - - open(FILE, "<", $path) || die $!; - while() { - if ($_ =~ m/^(\s+)"(.*?)"(: *)"(.*?)"(,?)$/) { - my ($indent, $src, $colon, $dst, $comma) = ($1, $2, $3, $4, $5); - $src =~ s/\\"/"/g; - $dst =~ s/\\"/"/g; - - if ($map->{$src}) { - printf ("%10s %24s\t%s\n", $lang, "Duplicate translation!", $src); - } - $map->{$src} = $dst; - } - } - close(FILE); - - return $map; -} - -sub read_src_strings { - my $path = shift; - - use File::Find; - use File::Slurp; - - my $strings = []; - - my @files; - find( sub { push @files, $File::Find::name if (-f $_ && /\.jsx?$/) }, $path ); - foreach my $file (@files) { - my $src = read_file($file); - $src =~ s/'\s*\+\s*'//g; - $src =~ s/"\s*\+\s*"//g; - - $file =~ s/^.*\/src/src/; - while ($src =~ /_t(?:Jsx)?\(\s*'(.*?[^\\])'/sg) { - my $s = $1; - $s =~ s/\\'/'/g; - push @$strings, [$s, $file]; - } - while ($src =~ /_t(?:Jsx)?\(\s*"(.*?[^\\])"/sg) { - push @$strings, [$1, $file]; - } - } - - return $strings; -} \ No newline at end of file diff --git a/scripts/fix-i18n.pl b/scripts/fix-i18n.pl deleted file mode 100755 index def352463d4..00000000000 --- a/scripts/fix-i18n.pl +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/perl -ni - -use strict; -use warnings; - -# script which synchronises i18n strings to include punctuation. -# i've cherry-picked ones which seem to have diverged between the different translations -# from TextForEvent, causing missing events all over the place - -BEGIN { -$::fixups = [split(/\n/, < 0 ? ('../' x $depth) : './'; - -s/= require\(['"]matrix-react-sdk\/lib\/(.*?)['"]\)/= require('$prefix$1')/; -s/= require\(['"]matrix-react-sdk['"]\)/= require('${prefix}index')/; - -s/^(import .* from )['"]matrix-react-sdk\/lib\/(.*?)['"]/$1'$prefix$2'/; -s/^(import .* from )['"]matrix-react-sdk['"]/$1'${prefix}index'/; diff --git a/sonar-project.properties b/sonar-project.properties index a8d8f0cf860..b127fee1e98 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -8,7 +8,9 @@ sonar.sources=src,res sonar.tests=test,cypress sonar.exclusions=__mocks__,docs +sonar.cpd.exclusions=src/i18n/strings/*.json sonar.typescript.tsconfigPath=./tsconfig.json sonar.javascript.lcov.reportPaths=coverage/lcov.info -sonar.coverage.exclusions=test/**/*,cypress/**/*,src/components/views/dialogs/devtools/**/* +# instrumentation is disabled on SessionLock +sonar.coverage.exclusions=test/**/*,cypress/**/*,src/components/views/dialogs/devtools/**/*,src/utils/SessionLock.ts sonar.testExecutionReportPaths=coverage/jest-sonar-report.xml diff --git a/src/@types/common.ts b/src/@types/common.ts index 4ea9cff802c..dd42c2078f2 100644 --- a/src/@types/common.ts +++ b/src/@types/common.ts @@ -23,22 +23,41 @@ export type Writeable = { -readonly [P in keyof T]: T[P] }; export type ComponentClass = keyof JSX.IntrinsicElements | JSXElementConstructor; -// Utility type for string dot notation for accessing nested object properties -// Based on https://stackoverflow.com/a/58436959 -type Join = K extends string | number +/** + * Utility type for string dot notation for accessing nested object properties. + * Based on https://stackoverflow.com/a/58436959 + * @example + * { + * "a": { + * "b": { + * "c": "value" + * }, + * "d": "foobar" + * } + * } + * will yield a type of `"a.b.c" | "a.d"` with Separator="." + * @typeParam Target the target type to generate leaf keys for + * @typeParam Separator the separator to use between key segments when accessing nested objects + * @typeParam LeafType the type which leaves of this object extend, used to determine when to stop recursion + * @typeParam MaxDepth the maximum depth to recurse to + * @returns a union type representing all dot (Separator) string notation keys which can access a Leaf (of LeafType) + */ +export type Leaves = [ + MaxDepth, +] extends [never] + ? never + : Target extends LeafType + ? "" + : { + [K in keyof Target]-?: Join, Separator>; + }[keyof Target]; +type Prev = [never, 0, 1, 2, 3, ...0[]]; +type Join = K extends string | number ? P extends string | number - ? `${K}${"" extends P ? "" : "."}${P}` + ? `${K}${"" extends P ? "" : S}${P}` : never : never; -type Prev = [never, 0, 1, 2, 3, ...0[]]; - -export type Leaves = [D] extends [never] - ? never - : T extends object - ? { [K in keyof T]-?: Join> }[keyof T] - : ""; - export type RecursivePartial = { [P in keyof T]?: T[P] extends (infer U)[] ? RecursivePartial[] diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index d8f01cd4be5..39e1eacbc87 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +// eslint-disable-next-line no-restricted-imports import "matrix-js-sdk/src/@types/global"; // load matrix-js-sdk's type extensions first import "@types/modernizr"; diff --git a/src/AddThreepid.ts b/src/AddThreepid.ts index bc9958cb6df..0d527ada5a7 100644 --- a/src/AddThreepid.ts +++ b/src/AddThreepid.ts @@ -24,8 +24,8 @@ import { MatrixClient, MatrixError, HTTPError, + IThreepid, } from "matrix-js-sdk/src/matrix"; -import { IThreepid } from "matrix-js-sdk/src/@types/threepids"; import Modal from "./Modal"; import { _t, UserFriendlyError } from "./languageHandler"; @@ -226,7 +226,7 @@ export default class AddThreepid { [SSOAuthEntry.PHASE_POSTAUTH]: { title: _t("Confirm adding email"), body: _t("Click the button below to confirm adding this email address."), - continueText: _t("Confirm"), + continueText: _t("action|confirm"), continueKind: "primary", }, }; @@ -329,7 +329,7 @@ export default class AddThreepid { [SSOAuthEntry.PHASE_POSTAUTH]: { title: _t("Confirm adding phone number"), body: _t("Click the button below to confirm adding this phone number."), - continueText: _t("Confirm"), + continueText: _t("action|confirm"), continueKind: "primary", }, }; diff --git a/src/AsyncWrapper.tsx b/src/AsyncWrapper.tsx index 1173ca3fd06..901699a3597 100644 --- a/src/AsyncWrapper.tsx +++ b/src/AsyncWrapper.tsx @@ -77,10 +77,10 @@ export default class AsyncWrapper extends React.Component { return ; } else if (this.state.error) { return ( - + {_t("Unable to load! Check your network connectivity and try again.")} diff --git a/src/ContentMessages.ts b/src/ContentMessages.ts index 14208e0d9e6..a5a5ffb137f 100644 --- a/src/ContentMessages.ts +++ b/src/ContentMessages.ts @@ -536,7 +536,7 @@ export default class ContentMessages { replyToEvent: MatrixEvent | undefined, promBefore?: Promise, ): Promise { - const fileName = file.name || _t("Attachment"); + const fileName = file.name || _t("common|attachment"); const content: Omit & { info: Partial } = { body: fileName, info: { diff --git a/src/DateUtils.ts b/src/DateUtils.ts index e743b3feead..78b390a4aa9 100644 --- a/src/DateUtils.ts +++ b/src/DateUtils.ts @@ -18,95 +18,121 @@ limitations under the License. import { Optional } from "matrix-events-sdk"; -import { _t } from "./languageHandler"; +import { _t, getUserLanguage } from "./languageHandler"; -function getDaysArray(): string[] { - return [_t("Sun"), _t("Mon"), _t("Tue"), _t("Wed"), _t("Thu"), _t("Fri"), _t("Sat")]; -} +export const MINUTE_MS = 60000; +export const HOUR_MS = MINUTE_MS * 60; +export const DAY_MS = HOUR_MS * 24; -function getMonthsArray(): string[] { - return [ - _t("Jan"), - _t("Feb"), - _t("Mar"), - _t("Apr"), - _t("May"), - _t("Jun"), - _t("Jul"), - _t("Aug"), - _t("Sep"), - _t("Oct"), - _t("Nov"), - _t("Dec"), - ]; +/** + * Returns array of 7 weekday names, from Sunday to Saturday, internationalised to the user's language. + * @param weekday - format desired "short" | "long" | "narrow" + */ +export function getDaysArray(weekday: Intl.DateTimeFormatOptions["weekday"] = "short"): string[] { + const sunday = 1672574400000; // 2023-01-01 12:00 UTC + const { format } = new Intl.DateTimeFormat(getUserLanguage(), { weekday, timeZone: "UTC" }); + return [...Array(7).keys()].map((day) => format(sunday + day * DAY_MS)); } -function pad(n: number): string { - return (n < 10 ? "0" : "") + n; +/** + * Returns array of 12 month names, from January to December, internationalised to the user's language. + * @param month - format desired "numeric" | "2-digit" | "long" | "short" | "narrow" + */ +export function getMonthsArray(month: Intl.DateTimeFormatOptions["month"] = "short"): string[] { + const { format } = new Intl.DateTimeFormat(getUserLanguage(), { month, timeZone: "UTC" }); + return [...Array(12).keys()].map((m) => format(Date.UTC(2021, m))); } -function twelveHourTime(date: Date, showSeconds = false): string { - let hours = date.getHours() % 12; - const minutes = pad(date.getMinutes()); - const ampm = date.getHours() >= 12 ? _t("PM") : _t("AM"); - hours = hours ? hours : 12; // convert 0 -> 12 - if (showSeconds) { - const seconds = pad(date.getSeconds()); - return `${hours}:${minutes}:${seconds}${ampm}`; - } - return `${hours}:${minutes}${ampm}`; +// XXX: Ideally we could just specify `hour12: boolean` but it has issues on Chrome in the `en` locale +// https://support.google.com/chrome/thread/29828561?hl=en +function getTwelveHourOptions(showTwelveHour: boolean): Intl.DateTimeFormatOptions { + return { + hourCycle: showTwelveHour ? "h12" : "h23", + }; } -export function formatDate(date: Date, showTwelveHour = false): string { +/** + * Formats a given date to a date & time string. + * + * The output format depends on how far away the given date is from now. + * Will use the browser's default time zone. + * If the date is today it will return a time string excluding seconds. See {@formatTime}. + * If the date is within the last 6 days it will return the name of the weekday along with the time string excluding seconds. + * If the date is within the same year then it will return the weekday, month and day of the month along with the time string excluding seconds. + * Otherwise, it will return a string representing the full date & time in a human friendly manner. See {@formatFullDate}. + * @param date - date object to format + * @param showTwelveHour - whether to use 12-hour rather than 24-hour time. Defaults to `false` (24 hour mode). + * Overrides the default from the locale, whether `true` or `false`. + * @param locale - the locale string to use, in BCP 47 format, defaulting to user's selected application locale + */ +export function formatDate(date: Date, showTwelveHour = false, locale?: string): string { + const _locale = locale ?? getUserLanguage(); const now = new Date(); - const days = getDaysArray(); - const months = getMonthsArray(); if (date.toDateString() === now.toDateString()) { - return formatTime(date, showTwelveHour); - } else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) { - // TODO: use standard date localize function provided in counterpart - return _t("%(weekDayName)s %(time)s", { - weekDayName: days[date.getDay()], - time: formatTime(date, showTwelveHour), - }); + return formatTime(date, showTwelveHour, _locale); + } else if (now.getTime() - date.getTime() < 6 * DAY_MS) { + // Time is within the last 6 days (or in the future) + return new Intl.DateTimeFormat(_locale, { + ...getTwelveHourOptions(showTwelveHour), + weekday: "short", + hour: "numeric", + minute: "2-digit", + }).format(date); } else if (now.getFullYear() === date.getFullYear()) { - // TODO: use standard date localize function provided in counterpart - return _t("%(weekDayName)s, %(monthName)s %(day)s %(time)s", { - weekDayName: days[date.getDay()], - monthName: months[date.getMonth()], - day: date.getDate(), - time: formatTime(date, showTwelveHour), - }); + return new Intl.DateTimeFormat(_locale, { + ...getTwelveHourOptions(showTwelveHour), + weekday: "short", + month: "short", + day: "numeric", + hour: "numeric", + minute: "2-digit", + }).format(date); } - return formatFullDate(date, showTwelveHour); + return formatFullDate(date, showTwelveHour, false, _locale); } -export function formatFullDateNoTime(date: Date): string { - const days = getDaysArray(); - const months = getMonthsArray(); - return _t("%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s", { - weekDayName: days[date.getDay()], - monthName: months[date.getMonth()], - day: date.getDate(), - fullYear: date.getFullYear(), - }); +/** + * Formats a given date to a human-friendly string with short weekday. + * Will use the browser's default time zone. + * @example "Thu, 17 Nov 2022" in en-GB locale + * @param date - date object to format + * @param locale - the locale string to use, in BCP 47 format, defaulting to user's selected application locale + */ +export function formatFullDateNoTime(date: Date, locale?: string): string { + return new Intl.DateTimeFormat(locale ?? getUserLanguage(), { + weekday: "short", + month: "short", + day: "numeric", + year: "numeric", + }).format(date); } -export function formatFullDate(date: Date, showTwelveHour = false, showSeconds = true): string { - const days = getDaysArray(); - const months = getMonthsArray(); - return _t("%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s", { - weekDayName: days[date.getDay()], - monthName: months[date.getMonth()], - day: date.getDate(), - fullYear: date.getFullYear(), - time: showSeconds ? formatFullTime(date, showTwelveHour) : formatTime(date, showTwelveHour), - }); +/** + * Formats a given date to a date & time string, optionally including seconds. + * Will use the browser's default time zone. + * @example "Thu, 17 Nov 2022, 4:58:32 pm" in en-GB locale with showTwelveHour=true and showSeconds=true + * @param date - date object to format + * @param showTwelveHour - whether to use 12-hour rather than 24-hour time. Defaults to `false` (24 hour mode). + * Overrides the default from the locale, whether `true` or `false`. + * @param showSeconds - whether to include seconds in the time portion of the string + * @param locale - the locale string to use, in BCP 47 format, defaulting to user's selected application locale + */ +export function formatFullDate(date: Date, showTwelveHour = false, showSeconds = true, locale?: string): string { + return new Intl.DateTimeFormat(locale ?? getUserLanguage(), { + ...getTwelveHourOptions(showTwelveHour), + weekday: "short", + month: "short", + day: "numeric", + year: "numeric", + hour: "numeric", + minute: "2-digit", + second: showSeconds ? "2-digit" : undefined, + }).format(date); } /** * Formats dates to be compatible with attributes of a ``. Dates - * should be formatted like "2020-06-23" (formatted according to ISO8601) + * should be formatted like "2020-06-23" (formatted according to ISO8601). * * @param date The date to format. * @returns The date string in ISO8601 format ready to be used with an `` @@ -115,22 +141,44 @@ export function formatDateForInput(date: Date): string { const year = `${date.getFullYear()}`.padStart(4, "0"); const month = `${date.getMonth() + 1}`.padStart(2, "0"); const day = `${date.getDate()}`.padStart(2, "0"); - const dateInputValue = `${year}-${month}-${day}`; - return dateInputValue; + return `${year}-${month}-${day}`; } -export function formatFullTime(date: Date, showTwelveHour = false): string { - if (showTwelveHour) { - return twelveHourTime(date, true); - } - return pad(date.getHours()) + ":" + pad(date.getMinutes()) + ":" + pad(date.getSeconds()); +/** + * Formats a given date to a time string including seconds. + * Will use the browser's default time zone. + * @example "4:58:32 PM" in en-GB locale with showTwelveHour=true + * @example "16:58:32" in en-GB locale with showTwelveHour=false + * @param date - date object to format + * @param showTwelveHour - whether to use 12-hour rather than 24-hour time. Defaults to `false` (24 hour mode). + * Overrides the default from the locale, whether `true` or `false`. + * @param locale - the locale string to use, in BCP 47 format, defaulting to user's selected application locale + */ +export function formatFullTime(date: Date, showTwelveHour = false, locale?: string): string { + return new Intl.DateTimeFormat(locale ?? getUserLanguage(), { + ...getTwelveHourOptions(showTwelveHour), + hour: "numeric", + minute: "2-digit", + second: "2-digit", + }).format(date); } -export function formatTime(date: Date, showTwelveHour = false): string { - if (showTwelveHour) { - return twelveHourTime(date); - } - return pad(date.getHours()) + ":" + pad(date.getMinutes()); +/** + * Formats a given date to a time string excluding seconds. + * Will use the browser's default time zone. + * @example "4:58 PM" in en-GB locale with showTwelveHour=true + * @example "16:58" in en-GB locale with showTwelveHour=false + * @param date - date object to format + * @param showTwelveHour - whether to use 12-hour rather than 24-hour time. Defaults to `false` (24 hour mode). + * Overrides the default from the locale, whether `true` or `false`. + * @param locale - the locale string to use, in BCP 47 format, defaulting to user's selected application locale + */ +export function formatTime(date: Date, showTwelveHour = false, locale?: string): string { + return new Intl.DateTimeFormat(locale ?? getUserLanguage(), { + ...getTwelveHourOptions(showTwelveHour), + hour: "numeric", + minute: "2-digit", + }).format(date); } export function formatSeconds(inSeconds: number): string { @@ -183,9 +231,8 @@ export function formatTimeLeft(inSeconds: number): string { }); } -const MILLIS_IN_DAY = 86400000; function withinPast24Hours(prevDate: Date, nextDate: Date): boolean { - return Math.abs(prevDate.getTime() - nextDate.getTime()) <= MILLIS_IN_DAY; + return Math.abs(prevDate.getTime() - nextDate.getTime()) <= DAY_MS; } function withinCurrentDay(prevDate: Date, nextDate: Date): boolean { @@ -210,15 +257,15 @@ export function wantsDateSeparator(prevEventDate: Optional, nextEventDate: } export function formatFullDateNoDay(date: Date): string { + const locale = getUserLanguage(); return _t("%(date)s at %(time)s", { - date: date.toLocaleDateString().replace(/\//g, "-"), - time: date.toLocaleTimeString().replace(/:/g, "-"), + date: date.toLocaleDateString(locale).replace(/\//g, "-"), + time: date.toLocaleTimeString(locale).replace(/:/g, "-"), }); } /** - * Returns an ISO date string without textual description of the date (ie: no "Wednesday" or - * similar) + * Returns an ISO date string without textual description of the date (ie: no "Wednesday" or similar) * @param date The date to format. * @returns The date string in ISO format. */ @@ -226,12 +273,23 @@ export function formatFullDateNoDayISO(date: Date): string { return date.toISOString(); } -export function formatFullDateNoDayNoTime(date: Date): string { - return date.getFullYear() + "/" + pad(date.getMonth() + 1) + "/" + pad(date.getDate()); +/** + * Formats a given date to a string. + * Will use the browser's default time zone. + * @example 17/11/2022 in en-GB locale + * @param date - date object to format + * @param locale - the locale string to use, in BCP 47 format, defaulting to user's selected application locale + */ +export function formatFullDateNoDayNoTime(date: Date, locale?: string): string { + return new Intl.DateTimeFormat(locale ?? getUserLanguage(), { + year: "numeric", + month: "numeric", + day: "numeric", + }).format(date); } export function formatRelativeTime(date: Date, showTwelveHour = false): string { - const now = new Date(Date.now()); + const now = new Date(); if (withinCurrentDay(date, now)) { return formatTime(date, showTwelveHour); } else { @@ -245,15 +303,11 @@ export function formatRelativeTime(date: Date, showTwelveHour = false): string { } } -const MINUTE_MS = 60000; -const HOUR_MS = MINUTE_MS * 60; -const DAY_MS = HOUR_MS * 24; - /** - * Formats duration in ms to human readable string - * Returns value in biggest possible unit (day, hour, min, second) + * Formats duration in ms to human-readable string + * Returns value in the biggest possible unit (day, hour, min, second) * Rounds values up until unit threshold - * ie. 23:13:57 -> 23h, 24:13:57 -> 1d, 44:56:56 -> 2d + * i.e. 23:13:57 -> 23h, 24:13:57 -> 1d, 44:56:56 -> 2d */ export function formatDuration(durationMs: number): string { if (durationMs >= DAY_MS) { @@ -269,9 +323,9 @@ export function formatDuration(durationMs: number): string { } /** - * Formats duration in ms to human readable string + * Formats duration in ms to human-readable string * Returns precise value down to the nearest second - * ie. 23:13:57 -> 23h 13m 57s, 44:56:56 -> 1d 20h 56m 56s + * i.e. 23:13:57 -> 23h 13m 57s, 44:56:56 -> 1d 20h 56m 56s */ export function formatPreciseDuration(durationMs: number): string { const days = Math.floor(durationMs / DAY_MS); @@ -293,13 +347,13 @@ export function formatPreciseDuration(durationMs: number): string { /** * Formats a timestamp to a short date - * (eg 25/12/22 in uk locale) - * localised by system locale + * Similar to {@formatFullDateNoDayNoTime} but with 2-digit on day, month, year. + * @example 25/12/22 in en-GB locale * @param timestamp - epoch timestamp + * @param locale - the locale string to use, in BCP 47 format, defaulting to user's selected application locale * @returns {string} formattedDate */ -export const formatLocalDateShort = (timestamp: number): string => - new Intl.DateTimeFormat( - undefined, // locales - { day: "2-digit", month: "2-digit", year: "2-digit" }, - ).format(timestamp); +export const formatLocalDateShort = (timestamp: number, locale?: string): string => + new Intl.DateTimeFormat(locale ?? getUserLanguage(), { day: "2-digit", month: "2-digit", year: "2-digit" }).format( + timestamp, + ); diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 04c79bc44ae..538c626b839 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -29,6 +29,7 @@ import { Optional } from "matrix-events-sdk"; import _Linkify from "linkify-react"; import escapeHtml from "escape-html"; import GraphemeSplitter from "graphemer"; +import { getEmojiFromUnicode } from "@matrix-org/emojibase-bindings"; import { _linkifyElement, @@ -39,7 +40,6 @@ import { import { IExtendedSanitizeOptions } from "./@types/sanitize-html"; import SettingsStore from "./settings/SettingsStore"; import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks"; -import { getEmojiFromUnicode } from "./emoji"; import { mediaFromMxc } from "./customisations/Media"; import { stripHTMLReply, stripPlainReply } from "./utils/Reply"; import { PERMITTED_URL_SCHEMES } from "./utils/UrlUtils"; diff --git a/src/IdentityAuthClient.tsx b/src/IdentityAuthClient.tsx index 44f0aaf2e6b..a596156addf 100644 --- a/src/IdentityAuthClient.tsx +++ b/src/IdentityAuthClient.tsx @@ -141,9 +141,7 @@ export default class IdentityAuthClient {

{_t( - "This action requires accessing the default identity server " + - " to validate an email address or phone number, " + - "but the server does not have any terms of service.", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.", {}, { server: () => {abbreviateUrl(identityServerUrl)}, @@ -153,7 +151,7 @@ export default class IdentityAuthClient {

{_t("Only continue if you trust the owner of the server.")}

), - button: _t("Trust"), + button: _t("action|trust"), }); const [confirmed] = await finished; if (confirmed) { diff --git a/src/LegacyCallHandler.tsx b/src/LegacyCallHandler.tsx index 18ae6b8b8e6..675756b462d 100644 --- a/src/LegacyCallHandler.tsx +++ b/src/LegacyCallHandler.tsx @@ -823,19 +823,14 @@ export default class LegacyCallHandler extends EventEmitter {

{_t( - "Please ask the administrator of your homeserver " + - "(%(homeserverDomain)s) to configure a TURN server in " + - "order for calls to work reliably.", + "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.", { homeserverDomain: cli.getDomain() }, { code: (sub: string) => {sub} }, )}

{_t( - "Alternatively, you can try to use the public server at " + - ", but this will not be as reliable, and " + - "it will share your IP address with that server. You can also manage " + - "this in Settings.", + "Alternatively, you can try to use the public server at , but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.", undefined, { server: () => {new URL(FALLBACK_ICE_SERVER).pathname} }, )} @@ -845,7 +840,7 @@ export default class LegacyCallHandler extends EventEmitter { button: _t("Try using %(server)s", { server: new URL(FALLBACK_ICE_SERVER).pathname, }), - cancelButton: _t("OK"), + cancelButton: _t("action|ok"), onFinished: (allow) => { SettingsStore.setValue("fallbackICEServerAllowed", null, SettingLevel.DEVICE, allow); cli.setFallbackICEServerAllowed(!!allow); @@ -865,8 +860,7 @@ export default class LegacyCallHandler extends EventEmitter { description = (

{_t( - "Call failed because microphone could not be accessed. " + - "Check that a microphone is plugged in and set up correctly.", + "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.", )}
); diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 719b0a45fe0..34c27c6a21a 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -18,12 +18,11 @@ limitations under the License. */ import { ReactNode } from "react"; -import { createClient, MatrixClient } from "matrix-js-sdk/src/matrix"; +import { createClient, MatrixClient, SSOAction } from "matrix-js-sdk/src/matrix"; import { InvalidStoreError } from "matrix-js-sdk/src/errors"; import { decryptAES, encryptAES, IEncryptedPayload } from "matrix-js-sdk/src/crypto/aes"; import { QueryDict } from "matrix-js-sdk/src/utils"; import { logger } from "matrix-js-sdk/src/logger"; -import { SSOAction } from "matrix-js-sdk/src/@types/auth"; import { MINIMUM_MATRIX_VERSION } from "matrix-js-sdk/src/version-support"; import { IMatrixClientCreds, MatrixClientPeg } from "./MatrixClientPeg"; @@ -83,6 +82,41 @@ dis.register((payload) => { } }); +/** + * This is set to true by {@link #onSessionLockStolen}. + * + * It is used in various of the async functions to prevent races where we initialise a client after the lock is stolen. + */ +let sessionLockStolen = false; + +// this is exposed solely for unit tests. +// ts-prune-ignore-next +export function setSessionLockNotStolen(): void { + sessionLockStolen = false; +} + +/** + * Handle the session lock being stolen. Stops any active Matrix Client, and aborts any ongoing client initialisation. + */ +export async function onSessionLockStolen(): Promise { + sessionLockStolen = true; + stopMatrixClient(); +} + +/** + * Check if we still hold the session lock. + * + * If not, raises a {@link SessionLockStolenError}. + */ +function checkSessionLock(): void { + if (sessionLockStolen) { + throw new SessionLockStolenError("session lock has been released"); + } +} + +/** Error type raised by various functions in the Lifecycle workflow if session lock is stolen during execution */ +class SessionLockStolenError extends Error {} + interface ILoadSessionOpts { enableGuest?: boolean; guestHsUrl?: string; @@ -154,6 +188,9 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise if (success) { return true; } + if (sessionLockStolen) { + return false; + } if (enableGuest && guestHsUrl) { return registerAsGuest(guestHsUrl, guestIsUrl, defaultDeviceDisplayName); @@ -167,6 +204,12 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise // need to show the general failure dialog. Instead, just go back to welcome. return false; } + + // likewise, if the session lock has been stolen while we've been trying to start + if (sessionLockStolen) { + return false; + } + return handleLoadSessionFailure(e); } } @@ -306,8 +349,7 @@ export function attemptTokenLogin( logger.warn("Cannot log in with token: can't determine HS URL to use"); onFailedDelegatedAuthLogin( _t( - "We asked the browser to remember which homeserver you use to let you sign in, " + - "but unfortunately your browser has forgotten it. Go to the sign in page and try again.", + "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.", ), ); return Promise.resolve(false); @@ -366,7 +408,7 @@ async function onFailedDelegatedAuthLogin(description: string | ReactNode, tryAg Modal.createDialog(ErrorDialog, { title: _t("We couldn't log you in"), description, - button: _t("Try again"), + button: _t("action|try_again"), // if we have a tryAgain callback, call it the primary 'try again' button was clicked in the dialog onFinished: tryAgain ? (shouldTryAgain?: boolean) => shouldTryAgain && tryAgain() : undefined, }); @@ -614,7 +656,7 @@ async function checkServerVersions(): Promise { brand: SdkConfig.get().brand, }, ), - acceptLabel: _t("OK"), + acceptLabel: _t("action|ok"), onAccept: () => { ToastStore.sharedInstance().dismissToast(toastKey); }, @@ -721,6 +763,7 @@ export async function hydrateSession(credentials: IMatrixClientCreds): Promise { + checkSessionLock(); credentials.guest = Boolean(credentials.guest); const softLogout = isSoftLogout(); @@ -751,6 +794,8 @@ async function doSetLoggedIn(credentials: IMatrixClientCreds, clearStorageEnable await abortLogin(); } + // check the session lock just before creating the new client + checkSessionLock(); MatrixClientPeg.replaceUsingCreds(credentials); const client = MatrixClientPeg.safeGet(); @@ -783,6 +828,7 @@ async function doSetLoggedIn(credentials: IMatrixClientCreds, clearStorageEnable } else { logger.warn("No local storage available: can't persist session!"); } + checkSessionLock(); dis.fire(Action.OnLoggedIn); await startMatrixClient(client, /*startSyncing=*/ !softLogout); @@ -978,6 +1024,8 @@ async function startMatrixClient(client: MatrixClient, startSyncing = true): Pro await MatrixClientPeg.assign(); } + checkSessionLock(); + // Run the migrations after the MatrixClientPeg has been assigned SettingsStore.runMigrations(); diff --git a/src/Login.ts b/src/Login.ts index d918aebc4b2..5ca4e9e5a25 100644 --- a/src/Login.ts +++ b/src/Login.ts @@ -15,9 +15,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { createClient, MatrixClient } from "matrix-js-sdk/src/matrix"; +import { + createClient, + MatrixClient, + LoginFlow, + DELEGATED_OIDC_COMPATIBILITY, + ILoginFlow, + LoginRequest, +} from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; -import { DELEGATED_OIDC_COMPATIBILITY, ILoginFlow, LoginFlow, LoginRequest } from "matrix-js-sdk/src/@types/auth"; import { IMatrixClientCreds } from "./MatrixClientPeg"; import SecurityCustomisations from "./customisations/Security"; diff --git a/src/Markdown.ts b/src/Markdown.ts index 709c34f31b8..0d33e58b6f0 100644 --- a/src/Markdown.ts +++ b/src/Markdown.ts @@ -112,6 +112,10 @@ const innerNodeLiteral = (node: commonmark.Node): string => { return literal; }; +const emptyItemWithNoSiblings = (node: commonmark.Node): boolean => { + return !node.prev && !node.next && !node.firstChild; +}; + /** * Class that wraps commonmark, adding the ability to see whether * a given message actually uses any markdown syntax or whether @@ -242,13 +246,30 @@ export default class Markdown { public isPlainText(): boolean { const walker = this.parsed.walker(); - let ev: commonmark.NodeWalkingStep | null; + while ((ev = walker.next())) { const node = ev.node; + if (TEXT_NODES.indexOf(node.type) > -1) { // definitely text continue; + } else if (node.type == "list" || node.type == "item") { + // Special handling for inputs like `+`, `*`, `-` and `2021.` which + // would otherwise be treated as a list of a single empty item. + // See https://github.com/vector-im/element-web/issues/7631 + if (node.type == "list" && node.firstChild && emptyItemWithNoSiblings(node.firstChild)) { + // A list with a single empty item is treated as plain text. + continue; + } + + if (node.type == "item" && emptyItemWithNoSiblings(node)) { + // An empty list item with no sibling items is treated as plain text. + continue; + } + + // Everything else is actual lists and therefore not plaintext. + return false; } else if (node.type == "html_inline" || node.type == "html_block") { // if it's an allowed html tag, we need to render it and therefore // we will need to use HTML. If it's not allowed, it's not HTML since diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 22c2dcb45c9..517d264597e 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -26,8 +26,8 @@ import { EventTimelineSet, IStartClientOpts, MatrixClient, + MemoryStore, } from "matrix-js-sdk/src/matrix"; -import { MemoryStore } from "matrix-js-sdk/src/store/memory"; import * as utils from "matrix-js-sdk/src/utils"; import { verificationMethods } from "matrix-js-sdk/src/crypto"; import { SHOW_QR_CODE_METHOD } from "matrix-js-sdk/src/crypto/verification/QRCode"; @@ -212,7 +212,7 @@ class MatrixClientPegClass implements IMatrixClientPeg { description: _t( "This may be caused by having the app open in multiple tabs or due to clearing browser data.", ), - button: _t("Reload"), + button: _t("action|reload"), }); const [reload] = await finished; if (!reload) return; diff --git a/src/Notifier.ts b/src/Notifier.ts index 839be8c83a6..2a7e7daf6ab 100644 --- a/src/Notifier.ts +++ b/src/Notifier.ts @@ -27,9 +27,9 @@ import { SyncState, SyncStateData, IRoomTimelineData, + M_LOCATION, } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; -import { M_LOCATION } from "matrix-js-sdk/src/@types/location"; import { PermissionChanged as PermissionChangedEvent } from "@matrix-org/analytics-events/types/typescript/PermissionChanged"; import { MatrixClientPeg } from "./MatrixClientPeg"; @@ -299,8 +299,7 @@ class NotifierClass { const description = result === "denied" ? _t( - "%(brand)s does not have permission to send you notifications - " + - "please check your browser settings", + "%(brand)s does not have permission to send you notifications - please check your browser settings", { brand }, ) : _t("%(brand)s was not given permission to send notifications - please try again", { diff --git a/src/PosthogTrackers.ts b/src/PosthogTrackers.ts index c03a20216c1..14bd8426de6 100644 --- a/src/PosthogTrackers.ts +++ b/src/PosthogTrackers.ts @@ -27,6 +27,7 @@ export type InteractionName = InteractionEvent["name"]; const notLoggedInMap: Record, ScreenName> = { [Views.LOADING]: "Loading", + [Views.CONFIRM_LOCK_THEFT]: "ConfirmStartup", [Views.WELCOME]: "Welcome", [Views.LOGIN]: "Login", [Views.REGISTER]: "Register", @@ -35,6 +36,7 @@ const notLoggedInMap: Record, ScreenName> = { [Views.COMPLETE_SECURITY]: "CompleteSecurity", [Views.E2E_SETUP]: "E2ESetup", [Views.SOFT_LOGOUT]: "SoftLogout", + [Views.LOCK_STOLEN]: "SessionLockStolen", }; const loggedInPageTypeMap: Record = { diff --git a/src/Registration.tsx b/src/Registration.tsx index f96aef22519..b9cebf35a93 100644 --- a/src/Registration.tsx +++ b/src/Registration.tsx @@ -53,11 +53,11 @@ export async function startAnyRegistrationFlow( const modal = Modal.createDialog(QuestionDialog, { hasCancelButton: true, quitOnly: true, - title: SettingsStore.getValue(UIFeature.Registration) ? _t("Sign In or Create Account") : _t("Sign In"), + title: SettingsStore.getValue(UIFeature.Registration) ? _t("Sign In or Create Account") : _t("action|sign_in"), description: SettingsStore.getValue(UIFeature.Registration) ? _t("Use your account or create a new one to continue.") : _t("Use your account to continue."), - button: _t("Sign In"), + button: _t("action|sign_in"), extraButtons: SettingsStore.getValue(UIFeature.Registration) ? [
diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index 16cd300b7a5..4283840d7e1 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -75,8 +75,8 @@ async function confirmToDismiss(): Promise { title: _t("Cancel entering passphrase?"), description: _t("Are you sure you want to cancel entering passphrase?"), danger: false, - button: _t("Go Back"), - cancelButton: _t("Cancel"), + button: _t("action|go_back"), + cancelButton: _t("action|cancel"), }).finished; return !sure; } diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 0a3d2ba8b17..8cb2f781d02 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -18,10 +18,8 @@ limitations under the License. */ import * as React from "react"; -import { User, IContent, Direction } from "matrix-js-sdk/src/matrix"; -import * as ContentHelpers from "matrix-js-sdk/src/content-helpers"; +import { User, IContent, Direction, ContentHelpers, MRoomTopicEventContent } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; -import { MRoomTopicEventContent } from "matrix-js-sdk/src/@types/topic"; import dis from "./dispatcher/dispatcher"; import { _t, _td, UserFriendlyError } from "./languageHandler"; @@ -193,8 +191,7 @@ export const Commands = [ const unixTimestamp = Date.parse(args); if (!unixTimestamp) { throw new UserFriendlyError( - "We were unable to understand the given date (%(inputDate)s). " + - "Try using the format YYYY-MM-DD.", + "We were unable to understand the given date (%(inputDate)s). Try using the format YYYY-MM-DD.", { inputDate: args, cause: undefined }, ); } @@ -402,16 +399,14 @@ export const Commands = [ description: (

{_t( - "Use an identity server to invite by email. " + - "Click continue to use the default identity server " + - "(%(defaultIdentityServerName)s) or manage in Settings.", + "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.", { defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl), }, )}

), - button: _t("Continue"), + button: _t("action|continue"), }); prom = finished.then(([useDefault]) => { @@ -461,7 +456,7 @@ export const Commands = [ new Command({ command: "part", args: "[]", - description: _td("Leave room"), + description: _td("action|leave_room"), analyticsName: "Part", isEnabled: (cli) => !isCurrentLocalRoom(cli), runFn: function (cli, roomId, threadId, args) { @@ -718,9 +713,7 @@ export const Commands = [ if (device.getFingerprint() !== fingerprint) { const fprint = device.getFingerprint(); throw new UserFriendlyError( - "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session" + - ' %(deviceId)s is "%(fprint)s" which does not match the provided key ' + - '"%(fingerprint)s". This could mean your communications are being intercepted!', + 'WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session %(deviceId)s is "%(fprint)s" which does not match the provided key "%(fingerprint)s". This could mean your communications are being intercepted!', { fprint, userId, @@ -740,8 +733,7 @@ export const Commands = [

{_t( - "The signing key you provided matches the signing key you received " + - "from %(userId)s's session %(deviceId)s. Session marked as verified.", + "The signing key you provided matches the signing key you received from %(userId)s's session %(deviceId)s. Session marked as verified.", { userId, deviceId }, )}

diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx index 93896cd0aa5..a1876839144 100644 --- a/src/TextForEvent.tsx +++ b/src/TextForEvent.tsx @@ -23,10 +23,11 @@ import { JoinRule, EventType, MsgType, + M_POLL_START, + M_POLL_END, } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { removeDirectionOverrideChars } from "matrix-js-sdk/src/utils"; -import { M_POLL_START, M_POLL_END } from "matrix-js-sdk/src/@types/polls"; import { PollStartEvent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent"; import { _t } from "./languageHandler"; @@ -482,17 +483,14 @@ function textForHistoryVisibilityEvent(event: MatrixEvent): (() => string) | nul case HistoryVisibility.Invited: return () => _t( - "%(senderName)s made future room history visible to all room members, " + - "from the point they are invited.", + "%(senderName)s made future room history visible to all room members, from the point they are invited.", { senderName }, ); case HistoryVisibility.Joined: return () => - _t( - "%(senderName)s made future room history visible to all room members, " + - "from the point they joined.", - { senderName }, - ); + _t("%(senderName)s made future room history visible to all room members, from the point they joined.", { + senderName, + }); case HistoryVisibility.Shared: return () => _t("%(senderName)s made future room history visible to all room members.", { senderName }); case HistoryVisibility.WorldReadable: @@ -810,33 +808,31 @@ function textForMjolnirEvent(event: MatrixEvent): (() => string) | null { if (USER_RULE_TYPES.includes(event.getType())) { return () => _t( - "%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching " + - "%(newGlob)s for %(reason)s", + "%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching %(newGlob)s for %(reason)s", { senderName, oldGlob: prevEntity, newGlob: entity, reason }, ); } else if (ROOM_RULE_TYPES.includes(event.getType())) { return () => _t( - "%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching " + - "%(newGlob)s for %(reason)s", + "%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching %(newGlob)s for %(reason)s", { senderName, oldGlob: prevEntity, newGlob: entity, reason }, ); } else if (SERVER_RULE_TYPES.includes(event.getType())) { return () => _t( - "%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching " + - "%(newGlob)s for %(reason)s", + "%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching %(newGlob)s for %(reason)s", { senderName, oldGlob: prevEntity, newGlob: entity, reason }, ); } // Unknown type. We'll say something but we shouldn't end up here. return () => - _t( - "%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s " + - "for %(reason)s", - { senderName, oldGlob: prevEntity, newGlob: entity, reason }, - ); + _t("%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s", { + senderName, + oldGlob: prevEntity, + newGlob: entity, + reason, + }); } export function textForLocationEvent(event: MatrixEvent): () => string { diff --git a/src/Unread.ts b/src/Unread.ts index 6185407a9b4..45f4f1fb820 100644 --- a/src/Unread.ts +++ b/src/Unread.ts @@ -14,9 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { M_BEACON } from "matrix-js-sdk/src/@types/beacon"; +import { M_BEACON, Room, Thread, MatrixEvent, EventType, MatrixClient } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; -import { Room, Thread, MatrixEvent, EventType, MatrixClient } from "matrix-js-sdk/src/matrix"; import shouldHideEvent from "./shouldHideEvent"; import { haveRendererForEvent } from "./events/EventTileFactory"; diff --git a/src/Views.ts b/src/Views.ts index 447dbeab84d..4c7f002d507 100644 --- a/src/Views.ts +++ b/src/Views.ts @@ -20,6 +20,9 @@ enum Views { // trying to re-animate a matrix client or register as a guest. LOADING, + // Another tab holds the lock. + CONFIRM_LOCK_THEFT, + // we are showing the welcome view WELCOME, @@ -48,6 +51,9 @@ enum Views { // We are logged out (invalid token) but have our local state again. The user // should log back in to rehydrate the client. SOFT_LOGOUT, + + // Another instance of the application has started up. We just show an error page. + LOCK_STOLEN, } export default Views; diff --git a/src/accessibility/KeyboardShortcutUtils.ts b/src/accessibility/KeyboardShortcutUtils.ts index acbf14d7565..8b6ac184f97 100644 --- a/src/accessibility/KeyboardShortcutUtils.ts +++ b/src/accessibility/KeyboardShortcutUtils.ts @@ -25,9 +25,9 @@ import { IKeyboardShortcuts, KeyBindingAction, KEYBOARD_SHORTCUTS, + KeyboardShortcutSetting, MAC_ONLY_SHORTCUTS, } from "./KeyboardShortcuts"; -import { IBaseSetting } from "../settings/Settings"; /** * This function gets the keyboard shortcuts that should be presented in the UI @@ -115,7 +115,7 @@ export const getKeyboardShortcuts = (): IKeyboardShortcuts => { export const getKeyboardShortcutsForUI = (): IKeyboardShortcuts => { const entries = [...Object.entries(getUIOnlyShortcuts()), ...Object.entries(getKeyboardShortcuts())] as [ KeyBindingAction, - IBaseSetting, + KeyboardShortcutSetting, ][]; return entries.reduce((acc, [key, value]) => { @@ -130,5 +130,5 @@ export const getKeyboardShortcutValue = (name: KeyBindingAction): KeyCombo | und export const getKeyboardShortcutDisplayName = (name: KeyBindingAction): string | undefined => { const keyboardShortcutDisplayName = getKeyboardShortcutsForUI()[name]?.displayName; - return keyboardShortcutDisplayName && _t(keyboardShortcutDisplayName as string); + return keyboardShortcutDisplayName && _t(keyboardShortcutDisplayName); }; diff --git a/src/accessibility/KeyboardShortcuts.ts b/src/accessibility/KeyboardShortcuts.ts index bcd720ee21b..db13ea79eb6 100644 --- a/src/accessibility/KeyboardShortcuts.ts +++ b/src/accessibility/KeyboardShortcuts.ts @@ -16,7 +16,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { _td } from "../languageHandler"; +import { _td, TranslationKey } from "../languageHandler"; import { IS_MAC, Key } from "../Keyboard"; import { IBaseSetting } from "../settings/Settings"; import { KeyCombo } from "../KeyBindingsManager"; @@ -154,13 +154,15 @@ export enum KeyBindingAction { ToggleHiddenEventVisibility = "KeyBinding.toggleHiddenEventVisibility", } -type KeyboardShortcutSetting = Omit, "supportedLevels">; +export type KeyboardShortcutSetting = Omit, "supportedLevels" | "displayName"> & { + displayName?: TranslationKey; +}; // TODO: We should figure out what to do with the keyboard shortcuts that are not handled by KeybindingManager export type IKeyboardShortcuts = Partial>; export interface ICategory { - categoryLabel?: string; + categoryLabel?: TranslationKey; // TODO: We should figure out what to do with the keyboard shortcuts that are not handled by KeybindingManager settingNames: KeyBindingAction[]; } @@ -179,18 +181,18 @@ export enum CategoryName { // Meta-key representing the digits [0-9] often found at the top of standard keyboard layouts export const DIGITS = "digits"; -export const ALTERNATE_KEY_NAME: Record = { - [Key.PAGE_UP]: _td("Page Up"), - [Key.PAGE_DOWN]: _td("Page Down"), - [Key.ESCAPE]: _td("Esc"), - [Key.ENTER]: _td("Enter"), - [Key.SPACE]: _td("Space"), - [Key.HOME]: _td("Home"), - [Key.END]: _td("End"), - [Key.ALT]: _td("Alt"), - [Key.CONTROL]: _td("Ctrl"), - [Key.SHIFT]: _td("Shift"), - [DIGITS]: _td("[number]"), +export const ALTERNATE_KEY_NAME: Record = { + [Key.PAGE_UP]: _td("keyboard|page_up"), + [Key.PAGE_DOWN]: _td("keyboard|page_down"), + [Key.ESCAPE]: _td("keyboard|escape"), + [Key.ENTER]: _td("keyboard|enter"), + [Key.SPACE]: _td("keyboard|space"), + [Key.HOME]: _td("keyboard|home"), + [Key.END]: _td("keyboard|end"), + [Key.ALT]: _td("keyboard|alt"), + [Key.CONTROL]: _td("keyboard|control"), + [Key.SHIFT]: _td("keyboard|shift"), + [DIGITS]: _td("keyboard|number"), }; export const KEY_ICON: Record = { [Key.ARROW_UP]: "↑", @@ -231,7 +233,7 @@ export const CATEGORIES: Record = { settingNames: [KeyBindingAction.ToggleMicInCall, KeyBindingAction.ToggleWebcamInCall], }, [CategoryName.ROOM]: { - categoryLabel: _td("Room"), + categoryLabel: _td("common|room"), settingNames: [ KeyBindingAction.SearchInRoom, KeyBindingAction.UploadFile, @@ -301,7 +303,7 @@ export const CATEGORIES: Record = { ], }, [CategoryName.LABS]: { - categoryLabel: _td("Labs"), + categoryLabel: _td("common|labs"), settingNames: [KeyBindingAction.ToggleHiddenEventVisibility], }, }; diff --git a/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.tsx b/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.tsx index a1e9485e027..dbedac0db04 100644 --- a/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.tsx +++ b/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.tsx @@ -62,7 +62,7 @@ export default class DisableEventIndexDialog extends React.Component :
} {eventIndexingSettings} diff --git a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx index 2a02945621f..88e69032a26 100644 --- a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx @@ -131,7 +131,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent

{_t("Your keys are being backed up (the first backup could take a few minutes).")}

- +
); } @@ -154,7 +154,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent

{_t("Unable to create key backup")}

{_t( - "Safeguard against losing access to encrypted messages & data by " + - "backing up encryption keys on your server.", + "Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.", )}

@@ -569,7 +567,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent @@ -588,7 +586,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent

{_t( - "Upgrade this session to allow it to verify other sessions, " + - "granting them access to encrypted messages and marking them " + - "as trusted for other users.", + "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.", )}

{authPrompt}
@@ -625,7 +621,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent
@@ -637,8 +633,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent

{_t( - "Enter a Security Phrase only you know, as it's used to safeguard your data. " + - "To be secure, you shouldn't re-use your account password.", + "Enter a Security Phrase only you know, as it's used to safeguard your data. To be secure, you shouldn't re-use your account password.", )}

@@ -659,13 +654,13 @@ export default class CreateSecretStorageDialog extends React.PureComponent @@ -717,13 +712,13 @@ export default class CreateSecretStorageDialog extends React.PureComponent{passPhraseMatch}
@@ -735,7 +730,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent

{_t( - "Store your Security Key somewhere safe, like a password manager or a safe, " + - "as it's used to safeguard your encrypted data.", + "Store your Security Key somewhere safe, like a password manager or a safe, as it's used to safeguard your encrypted data.", )}

@@ -769,7 +763,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent - {_t("Download")} + {_t("action|download")} {_t("%(downloadButton)s or %(copyButton)s", { @@ -783,7 +777,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent - {this.state.copied ? _t("Copied!") : _t("Copy")} + {this.state.copied ? _t("Copied!") : _t("action|copy")}
@@ -806,7 +800,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent

{_t("Your keys are now being backed up from this device.")}

this.props.onFinished(true)} hasCancel={false} /> @@ -820,7 +814,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent{_t("Unable to query secret storage status")}

{_t("You can also set up Secure Backup & manage your keys in Settings.")}

@@ -895,7 +889,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent{_t("Unable to set up secret storage")}

{_t( - "This process allows you to export the keys for messages " + - "you have received in encrypted rooms to a local file. You " + - "will then be able to import the file into another Matrix " + - "client in the future, so that client will also be able to " + - "decrypt these messages.", + "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.", )}

{_t( - "The exported file will allow anyone who can read it to decrypt " + - "any encrypted messages that you can see, so you should be " + - "careful to keep it secure. To help with this, you should enter " + - "a unique passphrase below, which will only be used to encrypt the " + - "exported data. " + - "It will only be possible to import the data by using the same passphrase.", + "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a unique passphrase below, which will only be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.", )}

{this.state.errStr}
@@ -229,7 +220,7 @@ export default class ExportE2eKeysDialog extends React.Component disabled={disableForm} />
diff --git a/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx index e9bf268cbae..b65e104170d 100644 --- a/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx +++ b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx @@ -146,16 +146,12 @@ export default class ImportE2eKeysDialog extends React.Component

{_t( - "This process allows you to import encryption keys " + - "that you had previously exported from another Matrix " + - "client. You will then be able to decrypt any " + - "messages that the other client could decrypt.", + "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.", )}

{_t( - "The export file will be protected with a passphrase. " + - "You should enter the passphrase here, to decrypt the file.", + "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.", )}

{this.state.errStr}
@@ -195,7 +191,7 @@ export default class ImportE2eKeysDialog extends React.Component disabled={!this.state.enableSubmit || disableForm} />
diff --git a/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx b/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx index a7aa464b8d1..db18b4e54d9 100644 --- a/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx +++ b/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx @@ -62,10 +62,7 @@ export default class NewRecoveryMethodDialog extends React.PureComponent const hackWarning = (

{_t( - "If you didn't set the new recovery method, an " + - "attacker may be trying to access your account. " + - "Change your account password and set a new recovery " + - "method immediately in Settings.", + "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.", )}

); @@ -78,7 +75,7 @@ export default class NewRecoveryMethodDialog extends React.PureComponent

{_t("This session is encrypting history using the new recovery method.")}

{hackWarning}

{_t( - "This session has detected that your Security Phrase and key " + - "for Secure Messages have been removed.", + "This session has detected that your Security Phrase and key for Secure Messages have been removed.", )}

{_t( - "If you did this accidentally, you can setup Secure Messages on " + - "this session which will re-encrypt this session's message " + - "history with a new recovery method.", + "If you did this accidentally, you can setup Secure Messages on this session which will re-encrypt this session's message history with a new recovery method.", )}

{_t( - "If you didn't remove the recovery method, an " + - "attacker may be trying to access your account. " + - "Change your account password and set a new recovery " + - "method immediately in Settings.", + "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.", )}

; public nameMatcher: QueryMatcher; - private readonly recentlyUsed: IEmoji[]; + private readonly recentlyUsed: Emoji[]; private emotes: Map = new Map(); private emotesPromise?: Promise>; public constructor(room: Room, renderingType?: TimelineRenderingType) { diff --git a/src/autocomplete/NotifProvider.tsx b/src/autocomplete/NotifProvider.tsx index 6d23c3694dd..8bad2c87187 100644 --- a/src/autocomplete/NotifProvider.tsx +++ b/src/autocomplete/NotifProvider.tsx @@ -56,7 +56,7 @@ export default class NotifProvider extends AutocompleteProvider { suffix: " ", component: ( - + ), range: range!, diff --git a/src/autocomplete/RoomProvider.tsx b/src/autocomplete/RoomProvider.tsx index c60c901f7c1..3d7a8ba1c1b 100644 --- a/src/autocomplete/RoomProvider.tsx +++ b/src/autocomplete/RoomProvider.tsx @@ -122,7 +122,7 @@ export default class RoomProvider extends AutocompleteProvider { href: makeRoomPermalink(this.room.client, room.displayedAlias), component: ( - + ), range: range!, diff --git a/src/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx index a934e9309a7..519c65344e3 100644 --- a/src/autocomplete/UserProvider.tsx +++ b/src/autocomplete/UserProvider.tsx @@ -135,7 +135,7 @@ export default class UserProvider extends AutocompleteProvider { href: makeUserPermalink(user.userId), component: ( - + ), range: range!, diff --git a/src/boundThreepids.ts b/src/boundThreepids.ts index 42a81876fb2..e8fee803bec 100644 --- a/src/boundThreepids.ts +++ b/src/boundThreepids.ts @@ -14,8 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { IThreepid, ThreepidMedium } from "matrix-js-sdk/src/@types/threepids"; -import { MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix"; +import { IThreepid, ThreepidMedium, MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix"; import IdentityAuthClient from "./IdentityAuthClient"; diff --git a/src/components/structures/EmbeddedPage.tsx b/src/components/structures/EmbeddedPage.tsx index 7ee30cb3ec6..6b063db9985 100644 --- a/src/components/structures/EmbeddedPage.tsx +++ b/src/components/structures/EmbeddedPage.tsx @@ -21,7 +21,7 @@ import sanitizeHtml from "sanitize-html"; import classnames from "classnames"; import { logger } from "matrix-js-sdk/src/logger"; -import { _t } from "../../languageHandler"; +import { _t, TranslationKey } from "../../languageHandler"; import dis from "../../dispatcher/dispatcher"; import { MatrixClientPeg } from "../../MatrixClientPeg"; import MatrixClientContext from "../../contexts/MatrixClientContext"; @@ -56,7 +56,7 @@ export default class EmbeddedPage extends React.PureComponent { }; } - private translate(s: string): string { + private translate(s: TranslationKey): string { return sanitizeHtml(_t(s)); } diff --git a/src/components/structures/HomePage.tsx b/src/components/structures/HomePage.tsx index 115da051188..f16f4113361 100644 --- a/src/components/structures/HomePage.tsx +++ b/src/components/structures/HomePage.tsx @@ -59,7 +59,7 @@ const getOwnProfile = ( avatarUrl?: string; } => ({ displayName: OwnProfileStore.instance.displayName || userId, - avatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(AVATAR_SIZE) ?? undefined, + avatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(parseInt(AVATAR_SIZE, 10)) ?? undefined, }); const UserWelcomeTop: React.FC = () => { @@ -84,9 +84,7 @@ const UserWelcomeTop: React.FC = () => { idName={userId} name={ownProfile.displayName} url={ownProfile.avatarUrl} - width={AVATAR_SIZE} - height={AVATAR_SIZE} - resizeMethod="crop" + size={AVATAR_SIZE + "px"} /> @@ -106,7 +104,7 @@ const HomePage: React.FC = ({ justRegistered = false }) => { } let introSection: JSX.Element; - if (justRegistered || !OwnProfileStore.instance.getHttpAvatarUrl(AVATAR_SIZE)) { + if (justRegistered || !OwnProfileStore.instance.getHttpAvatarUrl(parseInt(AVATAR_SIZE, 10))) { introSection = ; } else { const brandingConfig = SdkConfig.getObject("branding"); diff --git a/src/components/structures/InteractiveAuth.tsx b/src/components/structures/InteractiveAuth.tsx index 67c765760d5..d35d403e5f0 100644 --- a/src/components/structures/InteractiveAuth.tsx +++ b/src/components/structures/InteractiveAuth.tsx @@ -35,8 +35,8 @@ type InteractiveAuthCallbackSuccess = ( success: true, response: T, extra?: { emailSid?: string; clientSecret?: string }, -) => void; -type InteractiveAuthCallbackFailure = (success: false, response: IAuthData | Error) => void; +) => Promise; +type InteractiveAuthCallbackFailure = (success: false, response: IAuthData | Error) => Promise; export type InteractiveAuthCallback = InteractiveAuthCallbackSuccess & InteractiveAuthCallbackFailure; export interface InteractiveAuthProps { @@ -141,15 +141,15 @@ export default class InteractiveAuthComponent extends React.Component { + .then(async (result) => { const extra = { emailSid: this.authLogic.getEmailSid(), clientSecret: this.authLogic.getClientSecret(), }; - this.props.onAuthFinished(true, result, extra); + await this.props.onAuthFinished(true, result, extra); }) - .catch((error) => { - this.props.onAuthFinished(false, error); + .catch(async (error) => { + await this.props.onAuthFinished(false, error); logger.error("Error during user-interactive auth:", error); if (this.unmounted) { return; @@ -251,12 +251,12 @@ export default class InteractiveAuthComponent extends React.Component { - this.props.onAuthFinished(false, ERROR_USER_CANCELLED); + private onStageCancel = async (): Promise => { + await this.props.onAuthFinished(false, ERROR_USER_CANCELLED); }; - private onAuthStageFailed = (e: Error): void => { - this.props.onAuthFinished(false, e); + private onAuthStageFailed = async (e: Error): Promise => { + await this.props.onAuthFinished(false, e); }; private setEmailSid = (sid: string): void => { diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 49981b781a9..401dfc712fa 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -140,12 +140,16 @@ import { SdkContextClass, SDKContext } from "../../contexts/SDKContext"; import { viewUserDeviceSettings } from "../../actions/handlers/viewUserDeviceSettings"; import { cleanUpBroadcasts, VoiceBroadcastResumer } from "../../voice-broadcast"; import GenericToast from "../views/toasts/GenericToast"; -import RovingSpotlightDialog, { Filter } from "../views/dialogs/spotlight/SpotlightDialog"; +import RovingSpotlightDialog from "../views/dialogs/spotlight/SpotlightDialog"; import { findDMForUser } from "../../utils/dm/findDMForUser"; import { Linkify } from "../../HtmlUtils"; import { NotificationColor } from "../../stores/notifications/NotificationColor"; import { UserTab } from "../views/dialogs/UserTab"; import { shouldSkipSetupEncryption } from "../../utils/crypto/shouldSkipSetupEncryption"; +import { Filter } from "../views/dialogs/spotlight/Filter"; +import { checkSessionLockFree, getSessionLock } from "../../utils/SessionLock"; +import { SessionLockStolenView } from "./auth/SessionLockStolenView"; +import { ConfirmSessionLockTheftView } from "./auth/ConfirmSessionLockTheftView"; // legacy export export { default as Views } from "../../Views"; @@ -176,8 +180,6 @@ interface IProps { initialScreenAfterLogin?: IScreen; // displayname, if any, to set on the device when logging in/registering. defaultDeviceDisplayName?: string; - // A function that makes a registration URL - makeRegistrationUrl: (params: QueryDict) => string; } interface IState { @@ -308,11 +310,23 @@ export default class MatrixChat extends React.PureComponent { initSentry(SdkConfig.get("sentry")); + if (!checkSessionLockFree()) { + // another instance holds the lock; confirm its theft before proceeding + setTimeout(() => this.setState({ view: Views.CONFIRM_LOCK_THEFT }), 0); + } else { + this.startInitSession(); + } + } + + /** + * Kick off a call to {@link initSession}, and handle any errors + */ + private startInitSession = (): void => { this.initSession().catch((err) => { // TODO: show an error screen, rather than a spinner of doom logger.error("Error initialising Matrix session", err); }); - } + }; /** * Do what we can to establish a Matrix session. @@ -325,6 +339,13 @@ export default class MatrixChat extends React.PureComponent { * * If all else fails, present a login screen. */ private async initSession(): Promise { + // The Rust Crypto SDK will break if two Element instances try to use the same datastore at once, so + // make sure we are the only Element instance in town (on this browser/domain). + if (!(await getSessionLock(() => this.onSessionLockStolen()))) { + // we failed to get the lock. onSessionLockStolen should already have been called, so nothing left to do. + return; + } + // If the user was soft-logged-out, we want to make the SoftLogout component responsible for doing any // token auth (rather than Lifecycle.attemptDelegatedAuthLogin), since SoftLogout knows about submitting the // device ID and preserving the session. @@ -379,6 +400,18 @@ export default class MatrixChat extends React.PureComponent { } } + private async onSessionLockStolen(): Promise { + // switch to the LockStolenView. We deliberately do this immediately, rather than going through the dispatcher, + // because there can be a substantial queue in the dispatcher, and some of the events in it might require an + // active MatrixClient. + await new Promise((resolve) => { + this.setState({ view: Views.LOCK_STOLEN }, resolve); + }); + + // now we can tell the Lifecycle routines to abort any active startup, and to stop the active client. + await Lifecycle.onSessionLockStolen(); + } + private async postLoginSetup(): Promise { const cli = MatrixClientPeg.safeGet(); const cryptoEnabled = cli.isCryptoEnabled(); @@ -483,12 +516,10 @@ export default class MatrixChat extends React.PureComponent { const waitText = _t("Wait!"); const scamText = _t( - "If someone told you to copy/paste something here, " + "there is a high likelihood you're being scammed!", + "If someone told you to copy/paste something here, there is a high likelihood you're being scammed!", ); const devText = _t( - "If you know what you're doing, Element is open-source, " + - "be sure to check out our GitHub (https://github.com/vector-im/element-web/) " + - "and contribute!", + "If you know what you're doing, Element is open-source, be sure to check out our GitHub (https://github.com/vector-im/element-web/) and contribute!", ); global.mx_rage_logger.bypassRageshake( @@ -577,6 +608,11 @@ export default class MatrixChat extends React.PureComponent { } private onAction = (payload: ActionPayload): void => { + // once the session lock has been stolen, don't try to do anything. + if (this.state.view === Views.LOCK_STOLEN) { + return; + } + // Start the onboarding process for certain actions if (MatrixClientPeg.get()?.isGuest() && ONBOARDING_FLOW_STARTERS.includes(payload.action)) { // This will cause `payload` to be dispatched later, once a @@ -900,6 +936,18 @@ export default class MatrixChat extends React.PureComponent { break; } + case Action.OpenSpotlight: + Modal.createDialog( + RovingSpotlightDialog, + { + initialText: payload.initialText, + initialFilter: payload.initialFilter, + }, + "mx_SpotlightDialog_wrapper", + false, + true, + ); + break; } }; @@ -1155,8 +1203,7 @@ export default class MatrixChat extends React.PureComponent { {" " /* Whitespace, otherwise the sentences get smashed together */} {_t( - "You are the only person here. " + - "If you leave, no one will be able to join in the future, including you.", + "You are the only person here. If you leave, no one will be able to join in the future, including you.", )} , ); @@ -1188,7 +1235,7 @@ export default class MatrixChat extends React.PureComponent { const isSpace = roomToLeave?.isSpaceRoom(); Modal.createDialog(QuestionDialog, { - title: isSpace ? _t("Leave space") : _t("Leave room"), + title: isSpace ? _t("Leave space") : _t("action|leave_room"), description: ( {isSpace @@ -1201,7 +1248,7 @@ export default class MatrixChat extends React.PureComponent { {warnings} ), - button: _t("Leave"), + button: _t("action|leave"), onFinished: async (shouldLeave) => { if (shouldLeave) { await leaveRoomBehaviour(cli, roomId); @@ -1390,7 +1437,7 @@ export default class MatrixChat extends React.PureComponent { title: userNotice.title, props: { description: {userNotice.description}, - acceptLabel: _t("OK"), + acceptLabel: _t("action|ok"), onAccept: () => { ToastStore.sharedInstance().dismissToast(key); localStorage.setItem(key, "1"); @@ -1582,15 +1629,14 @@ export default class MatrixChat extends React.PureComponent {

{" "} {_t( - "To continue using the %(homeserverDomain)s homeserver " + - "you must review and agree to our terms and conditions.", + "To continue using the %(homeserverDomain)s homeserver you must review and agree to our terms and conditions.", { homeserverDomain: cli.getDomain() }, )}

), button: _t("Review terms and conditions"), - cancelButton: _t("Dismiss"), + cancelButton: _t("action|dismiss"), onFinished: (confirmed) => { if (confirmed) { const wnd = window.open(consentUri, "_blank")!; @@ -1632,13 +1678,7 @@ export default class MatrixChat extends React.PureComponent { Modal.createDialog(ErrorDialog, { title: _t("Old cryptography data detected"), description: _t( - "Data from an older version of %(brand)s has been detected. " + - "This will have caused end-to-end cryptography to malfunction " + - "in the older version. End-to-end encrypted messages exchanged " + - "recently whilst using the older version may not be decryptable " + - "in this version. This may also cause messages exchanged with this " + - "version to fail. If you experience problems, log out and back in " + - "again. To retain message history, export and re-import your keys.", + "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.", { brand: SdkConfig.get().brand }, ), }); @@ -1989,7 +2029,7 @@ export default class MatrixChat extends React.PureComponent { this.subTitleStatus = ""; if (state === SyncState.Error) { - this.subTitleStatus += `[${_t("Offline")}] `; + this.subTitleStatus += `[${_t("common|offline")}] `; } if (numUnreadRooms > 0) { this.subTitleStatus += `[${numUnreadRooms}]`; @@ -2004,13 +2044,6 @@ export default class MatrixChat extends React.PureComponent { this.setState({ serverConfig }); }; - private makeRegistrationUrl = (params: QueryDict): string => { - if (this.props.startingFragmentQueryParams?.referrer) { - params.referrer = this.props.startingFragmentQueryParams.referrer; - } - return this.props.makeRegistrationUrl(params); - }; - /** * After registration or login, we run various post-auth steps before entering the app * proper, such setting up cross-signing or verifying the new session. @@ -2057,6 +2090,15 @@ export default class MatrixChat extends React.PureComponent { ); + } else if (this.state.view === Views.CONFIRM_LOCK_THEFT) { + view = ( + { + this.setState({ view: Views.LOADING }); + this.startInitSession(); + }} + /> + ); } else if (this.state.view === Views.COMPLETE_SECURITY) { view = ; } else if (this.state.view === Views.E2E_SETUP) { @@ -2104,7 +2146,7 @@ export default class MatrixChat extends React.PureComponent {
- {_t("Logout")} + {_t("action|logout")}
@@ -2121,7 +2163,6 @@ export default class MatrixChat extends React.PureComponent { idSid={this.state.register_id_sid} email={email} brand={this.props.config.brand} - makeRegistrationUrl={this.makeRegistrationUrl} onLoggedIn={this.onRegisterFlowComplete} onLoginClick={this.onLoginClick} onServerConfigChange={this.onServerConfigChange} @@ -2164,6 +2205,8 @@ export default class MatrixChat extends React.PureComponent { ); } else if (this.state.view === Views.USE_CASE_SELECTION) { view = => this.onShowPostLoginScreen(useCase)} />; + } else if (this.state.view === Views.LOCK_STOLEN) { + view = ; } else { logger.error(`Unknown view ${this.state.view}`); return null; diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 957377f4776..0760d316b75 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -17,9 +17,16 @@ limitations under the License. import React, { createRef, ReactNode, TransitionEvent } from "react"; import ReactDOM from "react-dom"; import classNames from "classnames"; -import { Room, MatrixClient, RoomStateEvent, EventStatus, MatrixEvent, EventType } from "matrix-js-sdk/src/matrix"; +import { + Room, + MatrixClient, + RoomStateEvent, + EventStatus, + MatrixEvent, + EventType, + M_BEACON_INFO, +} from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; -import { M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon"; import { isSupportedReceiptType } from "matrix-js-sdk/src/utils"; import { Optional } from "matrix-events-sdk"; diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx index a387a2e0d54..33644676410 100644 --- a/src/components/structures/RoomSearch.tsx +++ b/src/components/structures/RoomSearch.tsx @@ -22,9 +22,8 @@ import defaultDispatcher from "../../dispatcher/dispatcher"; import { ActionPayload } from "../../dispatcher/payloads"; import { IS_MAC, Key } from "../../Keyboard"; import { _t } from "../../languageHandler"; -import Modal from "../../Modal"; -import SpotlightDialog from "../views/dialogs/spotlight/SpotlightDialog"; import AccessibleButton from "../views/elements/AccessibleButton"; +import { Action } from "../../dispatcher/actions"; interface IProps { isMinimized: boolean; @@ -44,7 +43,7 @@ export default class RoomSearch extends React.PureComponent { } private openSpotlight(): void { - Modal.createDialog(SpotlightDialog, {}, "mx_SpotlightDialog_wrapper", false, true); + defaultDispatcher.fire(Action.OpenSpotlight); } private onAction = (payload: ActionPayload): void => { @@ -73,7 +72,9 @@ export default class RoomSearch extends React.PureComponent { return ( {icon} - {!this.props.isMinimized &&
{_t("Search")}
} + {!this.props.isMinimized && ( +
{_t("action|search")}
+ )} {shortcutPrompt}
); diff --git a/src/components/structures/RoomSearchView.tsx b/src/components/structures/RoomSearchView.tsx index 1b3d8651948..bb701ed5754 100644 --- a/src/components/structures/RoomSearchView.tsx +++ b/src/components/structures/RoomSearchView.tsx @@ -199,7 +199,7 @@ export const RoomSearchView = forwardRef( if (!results?.results?.length) { ret.push(
  • -

    {_t("No results")}

    +

    {_t("common|no_results")}

  • , ); } else { @@ -256,7 +256,7 @@ export const RoomSearchView = forwardRef( ret.push(
  • - {_t("Room")}: {room.name} + {_t("common|room")}: {room.name}

  • , ); diff --git a/src/components/structures/RoomStatusBar.tsx b/src/components/structures/RoomStatusBar.tsx index 98039b1abcf..58c8225ab98 100644 --- a/src/components/structures/RoomStatusBar.tsx +++ b/src/components/structures/RoomStatusBar.tsx @@ -207,8 +207,7 @@ export default class RoomStatusBar extends React.PureComponent { } if (consentError) { title = _t( - "You can't send any messages until you review and agree to " + - "our terms and conditions.", + "You can't send any messages until you review and agree to our terms and conditions.", {}, { consentLink: (sub) => ( @@ -224,16 +223,13 @@ export default class RoomStatusBar extends React.PureComponent { resourceLimitError.data.admin_contact, { "monthly_active_user": _td( - "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. " + - "Please contact your service administrator to continue using the service.", + "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.", ), "hs_disabled": _td( - "Your message wasn't sent because this homeserver has been blocked by its administrator. " + - "Please contact your service administrator to continue using the service.", + "Your message wasn't sent because this homeserver has been blocked by its administrator. Please contact your service administrator to continue using the service.", ), "": _td( - "Your message wasn't sent because this homeserver has exceeded a resource limit. " + - "Please contact your service administrator to continue using the service.", + "Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.", ), }, ); diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index fe4d0c957c8..e8521aee24a 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -286,7 +286,7 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement { if (room.isError) { const buttons = ( - {_t("Retry")} + {_t("action|retry")} ); diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx index 4640ee9295f..b2524063d1e 100644 --- a/src/components/structures/SpaceHierarchy.tsx +++ b/src/components/structures/SpaceHierarchy.tsx @@ -38,9 +38,10 @@ import { RoomType, GuestAccess, HistoryVisibility, + HierarchyRelation, + HierarchyRoom, } from "matrix-js-sdk/src/matrix"; import { RoomHierarchy } from "matrix-js-sdk/src/room-hierarchy"; -import { IHierarchyRelation, IHierarchyRoom } from "matrix-js-sdk/src/@types/spaces"; import classNames from "classnames"; import { sortBy, uniqBy } from "lodash"; import { logger } from "matrix-js-sdk/src/logger"; @@ -85,7 +86,7 @@ interface IProps { } interface ITileProps { - room: IHierarchyRoom; + room: HierarchyRoom; suggested?: boolean; selected?: boolean; numChildRooms?: number; @@ -162,13 +163,13 @@ const Tile: React.FC = ({ onFocus={onFocus} tabIndex={isActive ? 0 : -1} > - {_t("View")} + {_t("action|view")} ); } else { button = ( - {_t("Join")} + {_t("action|join")} ); } @@ -193,15 +194,14 @@ const Tile: React.FC = ({ let avatar: ReactElement; if (joinedRoom) { - avatar = ; + avatar = ; } else { avatar = ( ); } @@ -429,8 +429,8 @@ export const joinRoom = async (cli: MatrixClient, hierarchy: RoomHierarchy, room }; interface IHierarchyLevelProps { - root: IHierarchyRoom; - roomSet: Set; + root: HierarchyRoom; + roomSet: Set; hierarchy: RoomHierarchy; parents: Set; selectedMap?: Map>; @@ -439,7 +439,7 @@ interface IHierarchyLevelProps { onToggleClick?(parentId: string, childId: string): void; } -export const toLocalRoom = (cli: MatrixClient, room: IHierarchyRoom, hierarchy: RoomHierarchy): IHierarchyRoom => { +export const toLocalRoom = (cli: MatrixClient, room: HierarchyRoom, hierarchy: RoomHierarchy): HierarchyRoom => { const history = cli.getRoomUpgradeHistory( room.room_id, true, @@ -497,14 +497,14 @@ export const HierarchyLevel: React.FC = ({ }); const [subspaces, childRooms] = sortedChildren.reduce( - (result, ev: IHierarchyRelation) => { + (result, ev: HierarchyRelation) => { const room = hierarchy.roomMap.get(ev.state_key); if (room && roomSet.has(room)) { result[room.room_type === RoomType.Space ? 0 : 1].push(toLocalRoom(cli, room, hierarchy)); } return result; }, - [[] as IHierarchyRoom[], [] as IHierarchyRoom[]], + [[] as HierarchyRoom[], [] as HierarchyRoom[]], ); const newParents = new Set(parents).add(root.room_id); @@ -564,12 +564,12 @@ export const useRoomHierarchy = ( space: Room, ): { loading: boolean; - rooms?: IHierarchyRoom[]; + rooms?: HierarchyRoom[]; hierarchy?: RoomHierarchy; error?: Error; loadMore(pageSize?: number): Promise; } => { - const [rooms, setRooms] = useState([]); + const [rooms, setRooms] = useState([]); const [hierarchy, setHierarchy] = useState(); const [error, setError] = useState(); @@ -715,7 +715,7 @@ const ManageButtons: React.FC = ({ hierarchy, selected, set kind="danger_outline" disabled={disabled} > - {removing ? _t("Removing…") : _t("Remove")} + {removing ? _t("Removing…") : _t("action|remove")} + )}
    diff --git a/src/components/structures/WaitingForThirdPartyRoomView.tsx b/src/components/structures/WaitingForThirdPartyRoomView.tsx index 8b1fe716f9d..418199d5d91 100644 --- a/src/components/structures/WaitingForThirdPartyRoomView.tsx +++ b/src/components/structures/WaitingForThirdPartyRoomView.tsx @@ -75,8 +75,7 @@ export const WaitingForThirdPartyRoomView: React.FC = ({ roomView, resize className="mx_cryptoEvent mx_cryptoEvent_icon" title={_t("Waiting for users to join %(brand)s", { brand })} subtitle={_t( - "Once invited users have joined %(brand)s, " + - "you will be able to chat and the room will be end-to-end encrypted", + "Once invited users have joined %(brand)s, you will be able to chat and the room will be end-to-end encrypted", { brand }, )} /> diff --git a/src/components/structures/auth/ConfirmSessionLockTheftView.tsx b/src/components/structures/auth/ConfirmSessionLockTheftView.tsx new file mode 100644 index 00000000000..36d612d21bf --- /dev/null +++ b/src/components/structures/auth/ConfirmSessionLockTheftView.tsx @@ -0,0 +1,51 @@ +/* +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 { _t } from "../../../languageHandler"; +import SdkConfig from "../../../SdkConfig"; +import AccessibleButton from "../../views/elements/AccessibleButton"; + +interface Props { + /** Callback which the view will call if the user confirms they want to use this window */ + onConfirm: () => void; +} + +/** + * Component shown by {@link MatrixChat} when another session is already active in the same browser and we need to + * confirm if we should steal its lock + */ +export function ConfirmSessionLockTheftView(props: Props): JSX.Element { + const brand = SdkConfig.get().brand; + + return ( +
    +
    +

    + {_t( + '%(brand)s is open in another window. Click "%(label)s" to use %(brand)s here and disconnect the other window.', + { brand, label: _t("action|continue") }, + )} +

    + + + {_t("action|continue")} + +
    +
    + ); +} diff --git a/src/components/structures/auth/ForgotPassword.tsx b/src/components/structures/auth/ForgotPassword.tsx index 14d58f372fa..f3026bbec61 100644 --- a/src/components/structures/auth/ForgotPassword.tsx +++ b/src/components/structures/auth/ForgotPassword.tsx @@ -354,19 +354,17 @@ export default class ForgotPassword extends React.Component {

    {_t( - "Signing out your devices will delete the message encryption keys stored on them, " + - "making encrypted chat history unreadable.", + "Signing out your devices will delete the message encryption keys stored on them, making encrypted chat history unreadable.", )}

    {_t( - "If you want to retain access to your chat history in encrypted rooms, set up Key Backup " + - "or export your message keys from one of your other devices before proceeding.", + "If you want to retain access to your chat history in encrypted rooms, set up Key Backup or export your message keys from one of your other devices before proceeding.", )}

    ), - button: _t("Continue"), + button: _t("action|continue"), }); const [confirmed] = await finished; return !!confirmed; @@ -443,9 +441,7 @@ export default class ForgotPassword extends React.Component { {this.state.logoutDevices ? (

    {_t( - "You have been logged out of all devices and will no longer receive " + - "push notifications. To re-enable notifications, sign in again on each " + - "device.", + "You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device.", )}

    ) : null} diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx index 241c2dcc719..7f3a326b58a 100644 --- a/src/components/structures/auth/Login.tsx +++ b/src/components/structures/auth/Login.tsx @@ -17,7 +17,7 @@ limitations under the License. import React, { ReactNode } from "react"; import classNames from "classnames"; import { logger } from "matrix-js-sdk/src/logger"; -import { ISSOFlow, SSOAction } from "matrix-js-sdk/src/@types/auth"; +import { SSOFlow, SSOAction } from "matrix-js-sdk/src/matrix"; import { _t, _td, UserFriendlyError } from "../../../languageHandler"; import Login, { ClientLoginFlow, OidcNativeFlow } from "../../../Login"; @@ -481,13 +481,13 @@ export default class LoginComponent extends React.PureComponent ); }} > - {_t("Continue")} + {_t("action|continue")} ); }; private renderSsoStep = (loginType: "cas" | "sso"): JSX.Element => { - const flow = this.state.flows?.find((flow) => flow.type === "m.login." + loginType) as ISSOFlow; + const flow = this.state.flows?.find((flow) => flow.type === "m.login." + loginType) as SSOFlow; return (

    - {_t("Sign in")} + {_t("action|sign_in")} {loader}

    {errorTextSection} diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index 2a8f89a5cdb..88725be2662 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -24,12 +24,13 @@ import { IRegisterRequestParams, IRequestTokenResponse, MatrixClient, + SSOFlow, + SSOAction, + RegisterResponse, } from "matrix-js-sdk/src/matrix"; import React, { Fragment, ReactNode } from "react"; import classNames from "classnames"; import { logger } from "matrix-js-sdk/src/logger"; -import { ISSOFlow, SSOAction } from "matrix-js-sdk/src/@types/auth"; -import { RegisterResponse } from "matrix-js-sdk/src/@types/registration"; import { _t } from "../../../languageHandler"; import { adminContactStrings, messageForResourceLimitError, resourceLimitStrings } from "../../../utils/ErrorUtils"; @@ -73,15 +74,7 @@ interface IProps { // - The user's password, if available and applicable (may be cached in memory // for a short time so the user is not required to re-enter their password // for operations like uploading cross-signing keys). - onLoggedIn(params: IMatrixClientCreds, password: string): void; - makeRegistrationUrl(params: { - /* eslint-disable camelcase */ - client_secret: string; - hs_url: string; - is_url?: string; - session_id: string; - /* eslint-enable camelcase */ - }): string; + onLoggedIn(params: IMatrixClientCreds, password: string): Promise; // registration shouldn't know or care how login is done. onLoginClick(): void; onServerConfigChange(config: ValidatedServerConfig): void; @@ -129,7 +122,7 @@ interface IState { differentLoggedInUserId?: string; // the SSO flow definition, this is fetched from /login as that's the only // place it is exposed. - ssoFlow?: ISSOFlow; + ssoFlow?: SSOFlow; } export default class Registration extends React.Component { @@ -227,11 +220,11 @@ export default class Registration extends React.Component { this.loginLogic.setHomeserverUrl(hsUrl); this.loginLogic.setIdentityServerUrl(isUrl); - let ssoFlow: ISSOFlow | undefined; + let ssoFlow: SSOFlow | undefined; try { const loginFlows = await this.loginLogic.getFlows(); if (serverConfig !== this.latestServerConfig) return; // discard, serverConfig changed from under us - ssoFlow = loginFlows.find((f) => f.type === "m.login.sso" || f.type === "m.login.cas") as ISSOFlow; + ssoFlow = loginFlows.find((f) => f.type === "m.login.sso" || f.type === "m.login.cas") as SSOFlow; } catch (e) { if (serverConfig !== this.latestServerConfig) return; // discard, serverConfig changed from under us logger.error("Failed to get login flows to check for SSO support", e); @@ -302,17 +295,7 @@ export default class Registration extends React.Component { sessionId: string, ): Promise => { if (!this.state.matrixClient) throw new Error("Matrix client has not yet been loaded"); - return this.state.matrixClient.requestRegisterEmailToken( - emailAddress, - clientSecret, - sendAttempt, - this.props.makeRegistrationUrl({ - client_secret: clientSecret, - hs_url: this.state.matrixClient.getHomeserverUrl(), - is_url: this.state.matrixClient.getIdentityServerUrl(), - session_id: sessionId, - }), - ); + return this.state.matrixClient.requestRegisterEmailToken(emailAddress, clientSecret, sendAttempt); }; private onUIAuthFinished: InteractiveAuthCallback = async (success, response): Promise => { @@ -401,9 +384,7 @@ export default class Registration extends React.Component { const hasAccessToken = Boolean(accessToken); debuglog("Registration: ui auth finished:", { hasEmail, hasAccessToken }); // don’t log in if we found a session for a different user - if (!hasEmail && hasAccessToken && !newState.differentLoggedInUserId) { - // we'll only try logging in if we either have no email to verify at all or we're the client that verified - // the email, not the client that started the registration flow + if (hasAccessToken && !newState.differentLoggedInUserId) { await this.props.onLoggedIn( { userId, @@ -628,7 +609,7 @@ export default class Registration extends React.Component { if (this.state.doingUIAuth) { goBack = ( - {_t("Go back")} + {_t("action|go_back")} ); } @@ -641,8 +622,7 @@ export default class Registration extends React.Component {

    {_t( - "Your new account (%(newAccountId)s) is registered, but you're already " + - "logged into a different account (%(loggedInUserId)s).", + "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).", { newAccountId: this.state.registeredUsername, loggedInUserId: this.state.differentLoggedInUserId, diff --git a/src/components/structures/auth/SessionLockStolenView.tsx b/src/components/structures/auth/SessionLockStolenView.tsx new file mode 100644 index 00000000000..0c0bc018499 --- /dev/null +++ b/src/components/structures/auth/SessionLockStolenView.tsx @@ -0,0 +1,35 @@ +/* +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 SplashPage from "../SplashPage"; +import { _t } from "../../../languageHandler"; +import SdkConfig from "../../../SdkConfig"; + +/** + * Component shown by {@link MatrixChat} when another session is started in the same browser. + */ +export function SessionLockStolenView(): JSX.Element { + const brand = SdkConfig.get().brand; + + return ( + +

    {_t("common|error")}

    +

    {_t("%(brand)s has been opened in another tab.", { brand })}

    + + ); +} diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index 1de7cc7d83f..f8781cdb0f5 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -161,10 +161,7 @@ export default class SetupEncryptionBody extends React.Component

    {_t( - "It looks like you don't have a Security Key or any other devices you can " + - "verify against. This device will not be able to access old encrypted messages. " + - "In order to verify your identity on this device, you'll need to reset " + - "your verification keys.", + "It looks like you don't have a Security Key or any other devices you can verify against. This device will not be able to access old encrypted messages. In order to verify your identity on this device, you'll need to reset your verification keys.", )}

    @@ -234,8 +231,7 @@ export default class SetupEncryptionBody extends React.Component message = (

    {_t( - "Your new device is now verified. It has access to your " + - "encrypted messages, and other users will see it as trusted.", + "Your new device is now verified. It has access to your encrypted messages, and other users will see it as trusted.", )}

    ); @@ -248,7 +244,7 @@ export default class SetupEncryptionBody extends React.Component {message}
    - {_t("Done")} + {_t("action|done")}
    @@ -258,8 +254,7 @@ export default class SetupEncryptionBody extends React.Component

    {_t( - "Without verifying, you won't have access to all your messages " + - "and may appear as untrusted to others.", + "Without verifying, you won't have access to all your messages and may appear as untrusted to others.", )}

    @@ -267,7 +262,7 @@ export default class SetupEncryptionBody extends React.Component {_t("I'll verify later")} - {_t("Go Back")} + {_t("action|go_back")}
    @@ -277,16 +272,12 @@ export default class SetupEncryptionBody extends React.Component

    {_t( - "Resetting your verification keys cannot be undone. After resetting, " + - "you won't have access to old encrypted messages, and any friends who " + - "have previously verified you will see security warnings until you " + - "re-verify with them.", + "Resetting your verification keys cannot be undone. After resetting, you won't have access to old encrypted messages, and any friends who have previously verified you will see security warnings until you re-verify with them.", )}

    {_t( - "Please only proceed if you're sure you've lost all of your other " + - "devices and your Security Key.", + "Please only proceed if you're sure you've lost all of your other devices and your Security Key.", )}

    @@ -295,7 +286,7 @@ export default class SetupEncryptionBody extends React.Component {_t("Proceed with reset")} - {_t("Go Back")} + {_t("action|go_back")}
    diff --git a/src/components/structures/auth/SoftLogout.tsx b/src/components/structures/auth/SoftLogout.tsx index 4ff18492ef7..57a39c74426 100644 --- a/src/components/structures/auth/SoftLogout.tsx +++ b/src/components/structures/auth/SoftLogout.tsx @@ -17,8 +17,7 @@ limitations under the License. import React, { ChangeEvent, SyntheticEvent } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import { Optional } from "matrix-events-sdk"; -import { ISSOFlow, LoginFlow, SSOAction } from "matrix-js-sdk/src/@types/auth"; -import { MatrixError } from "matrix-js-sdk/src/matrix"; +import { LoginFlow, MatrixError, SSOAction, SSOFlow } from "matrix-js-sdk/src/matrix"; import { _t } from "../../../languageHandler"; import dis from "../../../dispatcher/dispatcher"; @@ -64,7 +63,6 @@ interface IProps { interface IState { loginView: LoginView; - keyBackupNeeded: boolean; busy: boolean; password: string; errorText: string; @@ -77,7 +75,6 @@ export default class SoftLogout extends React.Component { this.state = { loginView: LoginView.Loading, - keyBackupNeeded: true, // assume we do while we figure it out (see componentDidMount) busy: false, password: "", errorText: "", @@ -93,13 +90,6 @@ export default class SoftLogout extends React.Component { } this.initLogin(); - - const cli = MatrixClientPeg.safeGet(); - if (cli.isCryptoEnabled()) { - cli.countSessionsNeedingBackup().then((remaining) => { - this.setState({ keyBackupNeeded: remaining > 0 }); - }); - } } private onClearAll = (): void => { @@ -235,7 +225,7 @@ export default class SoftLogout extends React.Component { {error} { type="submit" disabled={this.state.busy} > - {_t("Sign In")} + {_t("action|sign_in")} {_t("Forgotten your password?")} @@ -257,7 +247,7 @@ export default class SoftLogout extends React.Component { private renderSsoForm(introText: Optional): JSX.Element { const loginType = this.state.loginView === LoginView.CAS ? "cas" : "sso"; - const flow = this.state.flows.find((flow) => flow.type === "m.login." + loginType) as ISSOFlow; + const flow = this.state.flows.find((flow) => flow.type === "m.login." + loginType) as SSOFlow; return (
    @@ -279,42 +269,22 @@ export default class SoftLogout extends React.Component { return ; } - let introText: string | null = null; // null is translated to something area specific in this function - if (this.state.keyBackupNeeded) { - introText = _t( - "Regain access to your account and recover encryption keys stored in this session. " + - "Without them, you won't be able to read all of your secure messages in any session.", - ); - } - if (this.state.loginView === LoginView.Password) { - if (!introText) { - introText = _t("Enter your password to sign in and regain access to your account."); - } // else we already have a message and should use it (key backup warning) - - return this.renderPasswordForm(introText); + return this.renderPasswordForm(_t("Enter your password to sign in and regain access to your account.")); } if (this.state.loginView === LoginView.SSO || this.state.loginView === LoginView.CAS) { - if (!introText) { - introText = _t("Sign in and regain access to your account."); - } // else we already have a message and should use it (key backup warning) - - return this.renderSsoForm(introText); + return this.renderSsoForm(_t("Sign in and regain access to your account.")); } if (this.state.loginView === LoginView.PasswordWithSocialSignOn) { - if (!introText) { - introText = _t("Sign in and regain access to your account."); - } - // We render both forms with no intro/error to ensure the layout looks reasonably // okay enough. // // Note: "mx_AuthBody_centered" text taken from registration page. return ( <> -

    {introText}

    +

    {_t("Sign in and regain access to your account.")}

    {this.renderSsoForm(null)}

    {_t("%(ssoButtons)s Or %(usernamePassword)s", { @@ -330,10 +300,7 @@ export default class SoftLogout extends React.Component { // Default: assume unsupported/error return (

    - {_t( - "You cannot sign in to your account. Please contact your " + - "homeserver admin for more information.", - )} + {_t("You cannot sign in to your account. Please contact your homeserver admin for more information.")}

    ); } @@ -345,15 +312,13 @@ export default class SoftLogout extends React.Component {

    {_t("You're signed out")}

    -

    {_t("Sign in")}

    +

    {_t("action|sign_in")}

    {this.renderSignInSection()}

    {_t("Clear personal data")}

    {_t( - "Warning: your personal data (including encryption keys) is still stored " + - "in this session. Clear it if you're finished using this session, or want to sign " + - "in to another account.", + "Warning: your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.", )}

    diff --git a/src/components/structures/auth/forgot-password/CheckEmail.tsx b/src/components/structures/auth/forgot-password/CheckEmail.tsx index 7e4679a1dcf..086a71d4c8b 100644 --- a/src/components/structures/auth/forgot-password/CheckEmail.tsx +++ b/src/components/structures/auth/forgot-password/CheckEmail.tsx @@ -66,7 +66,7 @@ export const CheckEmail: React.FC = ({

    {errorText && } - +
    {_t("Did not receive it?")} = ({ aria-describedby={tooltipVisible ? tooltipId : undefined} > - {_t("Resend")} + {_t("action|resend")} = ({

    {_t("Verify your email to continue")}

    {_t( - "We need to know it’s you before resetting your password. " + - "Click the link in the email we just sent to %(email)s", + "We need to know it’s you before resetting your password. Click the link in the email we just sent to %(email)s", { email, }, @@ -74,7 +73,7 @@ export const VerifyEmailModal: React.FC = ({ aria-describedby={tooltipVisible ? tooltipId : undefined} > - {_t("Resend")} + {_t("action|resend")} void; + /** + * The flex space to use + * @default null + */ + flex?: string | null; + /** + * The flex shrink factor + * @default null + */ + shrink?: string | null; + /** + * The flex grow factor + * @default null + */ + grow?: string | null; +}; + +/** + * Set or remove a CSS property + * @param ref the reference + * @param name the CSS property name + * @param value the CSS property value + */ +function addOrRemoveProperty( + ref: React.MutableRefObject, + name: string, + value?: string | null, +): void { + const style = ref.current!.style; + if (value) { + style.setProperty(name, value); + } else { + style.removeProperty(name); + } +} + +/** + * A flex child helper + */ +export function Box({ + as = "div", + flex = null, + shrink = null, + grow = null, + className, + children, + ...props +}: React.PropsWithChildren): JSX.Element { + const ref = useRef(); + + useEffect(() => { + addOrRemoveProperty(ref, `--mx-box-flex`, flex); + addOrRemoveProperty(ref, `--mx-box-shrink`, shrink); + addOrRemoveProperty(ref, `--mx-box-grow`, grow); + }, [flex, grow, shrink]); + + return React.createElement( + as, + { + ...props, + className: classNames("mx_Box", className, { + "mx_Box--flex": !!flex, + "mx_Box--shrink": !!shrink, + "mx_Box--grow": !!grow, + }), + ref, + }, + children, + ); +} diff --git a/src/components/utils/Flex.tsx b/src/components/utils/Flex.tsx new file mode 100644 index 00000000000..20802f4f022 --- /dev/null +++ b/src/components/utils/Flex.tsx @@ -0,0 +1,86 @@ +/* +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 classNames from "classnames"; +import React, { useEffect, useRef } from "react"; + +type FlexProps = { + /** + * The type of the HTML element + * @default div + */ + as?: string; + /** + * The CSS class name. + */ + className?: string; + /** + * The type of flex container + * @default flex + */ + display?: "flex" | "inline-flex"; + /** + * The flow direction of the flex children + * @default row + */ + direction?: "row" | "column" | "row-reverse" | "column-reverse"; + /** + * The alingment of the flex children + * @default start + */ + align?: "start" | "center" | "end" | "baseline" | "stretch"; + /** + * The justification of the flex children + * @default start + */ + justify?: "start" | "center" | "end" | "between"; + /** + * The spacing between the flex children, expressed with the CSS unit + * @default 0 + */ + gap?: string; + /** + * the on click event callback + */ + onClick?: (e: React.MouseEvent) => void; +}; + +/** + * A flexbox container helper + */ +export function Flex({ + as = "div", + display = "flex", + direction = "row", + align = "start", + justify = "start", + gap = "0", + className, + children, + ...props +}: React.PropsWithChildren): JSX.Element { + const ref = useRef(); + + useEffect(() => { + ref.current!.style.setProperty(`--mx-flex-display`, display); + ref.current!.style.setProperty(`--mx-flex-direction`, direction); + ref.current!.style.setProperty(`--mx-flex-align`, align); + ref.current!.style.setProperty(`--mx-flex-justify`, justify); + ref.current!.style.setProperty(`--mx-flex-gap`, gap); + }, [align, direction, display, gap, justify]); + + return React.createElement(as, { ...props, className: classNames("mx_Flex", className), ref }, children); +} diff --git a/src/components/views/auth/CountryDropdown.tsx b/src/components/views/auth/CountryDropdown.tsx index a1b428de680..9946e72dad4 100644 --- a/src/components/views/auth/CountryDropdown.tsx +++ b/src/components/views/auth/CountryDropdown.tsx @@ -18,16 +18,15 @@ import React, { ReactElement } from "react"; import { COUNTRIES, getEmojiFlag, PhoneNumberCountryDefinition } from "../../../phonenumber"; import SdkConfig from "../../../SdkConfig"; -import { _t } from "../../../languageHandler"; +import { _t, getUserLanguage } from "../../../languageHandler"; import Dropdown from "../elements/Dropdown"; import { NonEmptyArray } from "../../../@types/common"; -const COUNTRIES_BY_ISO2: Record = {}; -for (const c of COUNTRIES) { - COUNTRIES_BY_ISO2[c.iso2] = c; +interface InternationalisedCountry extends PhoneNumberCountryDefinition { + name: string; // already translated to the user's locale } -function countryMatchesSearchQuery(query: string, country: PhoneNumberCountryDefinition): boolean { +function countryMatchesSearchQuery(query: string, country: InternationalisedCountry): boolean { // Remove '+' if present (when searching for a prefix) if (query[0] === "+") { query = query.slice(1); @@ -41,7 +40,7 @@ function countryMatchesSearchQuery(query: string, country: PhoneNumberCountryDef interface IProps { value?: string; - onOptionChange: (country: PhoneNumberCountryDefinition) => void; + onOptionChange: (country: InternationalisedCountry) => void; isSmall: boolean; // if isSmall, show +44 in the selected value showPrefix: boolean; className?: string; @@ -53,15 +52,25 @@ interface IState { } export default class CountryDropdown extends React.Component { - private readonly defaultCountry: PhoneNumberCountryDefinition; + private readonly defaultCountry: InternationalisedCountry; + private readonly countries: InternationalisedCountry[]; + private readonly countryMap: Map; public constructor(props: IProps) { super(props); - let defaultCountry: PhoneNumberCountryDefinition | undefined; + const displayNames = new Intl.DisplayNames([getUserLanguage()], { type: "region" }); + + this.countries = COUNTRIES.map((c) => ({ + name: displayNames.of(c.iso2) ?? c.iso2, + ...c, + })); + this.countryMap = new Map(this.countries.map((c) => [c.iso2, c])); + + let defaultCountry: InternationalisedCountry | undefined; const defaultCountryCode = SdkConfig.get("default_country_code"); if (defaultCountryCode) { - const country = COUNTRIES.find((c) => c.iso2 === defaultCountryCode.toUpperCase()); + const country = this.countries.find((c) => c.iso2 === defaultCountryCode.toUpperCase()); if (country) defaultCountry = country; } @@ -69,9 +78,8 @@ export default class CountryDropdown extends React.Component { try { const locale = new Intl.Locale(navigator.language ?? navigator.languages[0]); const code = locale.region ?? locale.language ?? locale.baseName; - const displayNames = new Intl.DisplayNames(["en"], { type: "region" }); - const displayName = displayNames.of(code)?.toUpperCase(); - defaultCountry = COUNTRIES.find( + const displayName = displayNames.of(code)!.toUpperCase(); + defaultCountry = this.countries.find( (c) => c.iso2 === code.toUpperCase() || c.name.toUpperCase() === displayName, ); } catch (e) { @@ -79,7 +87,7 @@ export default class CountryDropdown extends React.Component { } } - this.defaultCountry = defaultCountry ?? COUNTRIES[0]; + this.defaultCountry = defaultCountry ?? this.countries[0]; this.state = { searchQuery: "", }; @@ -101,7 +109,7 @@ export default class CountryDropdown extends React.Component { }; private onOptionChange = (iso2: string): void => { - this.props.onOptionChange(COUNTRIES_BY_ISO2[iso2]); + this.props.onOptionChange(this.countryMap.get(iso2)!); }; private flagImgForIso2(iso2: string): React.ReactNode { @@ -112,9 +120,9 @@ export default class CountryDropdown extends React.Component { if (!this.props.isSmall) { return undefined; } - let countryPrefix; + let countryPrefix: string | undefined; if (this.props.showPrefix) { - countryPrefix = "+" + COUNTRIES_BY_ISO2[iso2].prefix; + countryPrefix = "+" + this.countryMap.get(iso2)!.prefix; } return ( @@ -125,26 +133,28 @@ export default class CountryDropdown extends React.Component { }; public render(): React.ReactNode { - let displayedCountries; + let displayedCountries: InternationalisedCountry[]; if (this.state.searchQuery) { - displayedCountries = COUNTRIES.filter(countryMatchesSearchQuery.bind(this, this.state.searchQuery)); - if (this.state.searchQuery.length == 2 && COUNTRIES_BY_ISO2[this.state.searchQuery.toUpperCase()]) { + displayedCountries = this.countries.filter((country) => + countryMatchesSearchQuery(this.state.searchQuery, country), + ); + if (this.state.searchQuery.length == 2 && this.countryMap.has(this.state.searchQuery.toUpperCase())) { // exact ISO2 country name match: make the first result the matches ISO2 - const matched = COUNTRIES_BY_ISO2[this.state.searchQuery.toUpperCase()]; + const matched = this.countryMap.get(this.state.searchQuery.toUpperCase())!; displayedCountries = displayedCountries.filter((c) => { return c.iso2 != matched.iso2; }); displayedCountries.unshift(matched); } } else { - displayedCountries = COUNTRIES; + displayedCountries = this.countries; } const options = displayedCountries.map((country) => { return (

    {this.flagImgForIso2(country.iso2)} - {_t(country.name)} (+{country.prefix}) + {country.name} (+{country.prefix})
    ); }) as NonEmptyArray; diff --git a/src/components/views/auth/EmailField.tsx b/src/components/views/auth/EmailField.tsx index 849f38fea7b..16fa73771ce 100644 --- a/src/components/views/auth/EmailField.tsx +++ b/src/components/views/auth/EmailField.tsx @@ -17,7 +17,7 @@ limitations under the License. import React, { PureComponent, RefCallback, RefObject } from "react"; import Field, { IInputProps } from "../elements/Field"; -import { _t, _td } from "../../../languageHandler"; +import { _t, _td, TranslationKey } from "../../../languageHandler"; import withValidation, { IFieldState, IValidationResult } from "../elements/Validation"; import * as Email from "../../../email"; @@ -27,9 +27,9 @@ interface IProps extends Omit { value: string; autoFocus?: boolean; - label?: string; - labelRequired?: string; - labelInvalid?: string; + label: TranslationKey; + labelRequired: TranslationKey; + labelInvalid: TranslationKey; // When present, completely overrides the default validation rules. validationRules?: (fieldState: IFieldState) => Promise; @@ -50,12 +50,12 @@ class EmailField extends PureComponent { { key: "required", test: ({ value, allowEmpty }) => allowEmpty || !!value, - invalid: () => _t(this.props.labelRequired!), + invalid: () => _t(this.props.labelRequired), }, { key: "email", test: ({ value }) => !value || Email.looksValid(value), - invalid: () => _t(this.props.labelInvalid!), + invalid: () => _t(this.props.labelInvalid), }, ], }); @@ -80,7 +80,7 @@ class EmailField extends PureComponent { id={this.props.id} ref={this.props.fieldRef} type="text" - label={_t(this.props.label!)} + label={_t(this.props.label)} value={this.props.value} autoFocus={this.props.autoFocus} onChange={this.props.onChange} diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index add3638fcb4..50af045d5f3 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -153,7 +153,7 @@ export class PasswordAuthEntry extends React.Component ); } @@ -175,7 +175,7 @@ export class PasswordAuthEntry extends React.Component - {_t("Accept")} + {_t("action|accept")}
    ); } @@ -509,7 +508,7 @@ export class EmailIdentityAuthEntry extends React.Component< a: (text: string) => ( - {_t("Continue")} + {_t("action|continue")} ); } @@ -869,7 +868,7 @@ export class SSOAuthEntry extends React.Component - {_t("Cancel")} + {_t("action|cancel")} ); if (this.state.phase === SSOAuthEntry.PHASE_PREAUTH) { @@ -881,7 +880,7 @@ export class SSOAuthEntry extends React.Component - {this.props.continueText || _t("Confirm")} + {this.props.continueText || _t("action|confirm")} ); } diff --git a/src/components/views/auth/LoginWithQRFlow.tsx b/src/components/views/auth/LoginWithQRFlow.tsx index e5d1d94a32e..acb2fc5a2f8 100644 --- a/src/components/views/auth/LoginWithQRFlow.tsx +++ b/src/components/views/auth/LoginWithQRFlow.tsx @@ -54,7 +54,7 @@ export default class LoginWithQRFlow extends React.Component { private cancelButton = (): JSX.Element => ( - {_t("Cancel")} + {_t("action|cancel")} ); @@ -124,7 +124,7 @@ export default class LoginWithQRFlow extends React.Component { kind="primary" onClick={this.handleClick(Click.TryAgain)} > - {_t("Try again")} + {_t("action|try_again")} {this.cancelButton()} @@ -156,7 +156,7 @@ export default class LoginWithQRFlow extends React.Component { kind="primary_outline" onClick={this.handleClick(Click.Decline)} > - {_t("Cancel")} + {_t("action|cancel")} { buttons = this.cancelButton(); break; case Phase.Verifying: - title = _t("Success"); + title = _t("common|success"); centreTitle = true; main = this.simpleSpinner(_t("Completing set up of your new device")); break; diff --git a/src/components/views/auth/PassphraseConfirmField.tsx b/src/components/views/auth/PassphraseConfirmField.tsx index b314c3f838a..ebb273ff550 100644 --- a/src/components/views/auth/PassphraseConfirmField.tsx +++ b/src/components/views/auth/PassphraseConfirmField.tsx @@ -18,7 +18,7 @@ import React, { PureComponent, RefCallback, RefObject } from "react"; import Field, { IInputProps } from "../elements/Field"; import withValidation, { IFieldState, IValidationResult } from "../elements/Validation"; -import { _t, _td } from "../../../languageHandler"; +import { _t, _td, TranslationKey } from "../../../languageHandler"; interface IProps extends Omit { id?: string; @@ -27,9 +27,9 @@ interface IProps extends Omit { value: string; password: string; // The password we're confirming - label: string; - labelRequired: string; - labelInvalid: string; + label: TranslationKey; + labelRequired: TranslationKey; + labelInvalid: TranslationKey; onChange(ev: React.FormEvent): void; onValidate?(result: IValidationResult): void; diff --git a/src/components/views/auth/PassphraseField.tsx b/src/components/views/auth/PassphraseField.tsx index a40ba0bb07d..4efd06e4e43 100644 --- a/src/components/views/auth/PassphraseField.tsx +++ b/src/components/views/auth/PassphraseField.tsx @@ -20,7 +20,7 @@ import zxcvbn from "zxcvbn"; import SdkConfig from "../../../SdkConfig"; import withValidation, { IFieldState, IValidationResult } from "../elements/Validation"; -import { _t, _td } from "../../../languageHandler"; +import { _t, _td, TranslationKey } from "../../../languageHandler"; import Field, { IInputProps } from "../elements/Field"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; @@ -34,10 +34,10 @@ interface IProps extends Omit { // Additional strings such as a username used to catch bad passwords userInputs?: string[]; - label: string; - labelEnterPassword: string; - labelStrongPassword: string; - labelAllowedButUnsafe: string; + label: TranslationKey; + labelEnterPassword: TranslationKey; + labelStrongPassword: TranslationKey; + labelAllowedButUnsafe: TranslationKey; onChange(ev: React.FormEvent): void; onValidate?(result: IValidationResult): void; @@ -45,7 +45,7 @@ interface IProps extends Omit { class PassphraseField extends PureComponent { public static defaultProps = { - label: _td("Password"), + label: _td("common|password"), labelEnterPassword: _td("Enter password"), labelStrongPassword: _td("Nice, strong password!"), labelAllowedButUnsafe: _td("Password is allowed, but unsafe"), diff --git a/src/components/views/auth/PasswordLogin.tsx b/src/components/views/auth/PasswordLogin.tsx index bbab2242f49..33c62a8df02 100644 --- a/src/components/views/auth/PasswordLogin.tsx +++ b/src/components/views/auth/PasswordLogin.tsx @@ -308,8 +308,8 @@ export default class PasswordLogin extends React.PureComponent { autoComplete="username" key="username_input" type="text" - label={_t("Username")} - placeholder={_t("Username").toLocaleLowerCase()} + label={_t("common|username")} + placeholder={_t("common|username").toLocaleLowerCase()} value={this.props.username} onChange={this.onUsernameChanged} onBlur={this.onUsernameBlur} @@ -404,7 +404,7 @@ export default class PasswordLogin extends React.PureComponent { disabled={this.props.busy} >
    ); diff --git a/src/components/views/beacon/RoomLiveShareWarning.tsx b/src/components/views/beacon/RoomLiveShareWarning.tsx index ace873cc1ad..541e2329d0c 100644 --- a/src/components/views/beacon/RoomLiveShareWarning.tsx +++ b/src/components/views/beacon/RoomLiveShareWarning.tsx @@ -111,7 +111,7 @@ const RoomLiveShareWarningInner: React.FC = ({ l element="button" disabled={stoppingInProgress} > - {hasError ? _t("Retry") : _t("Stop")} + {hasError ? _t("action|retry") : _t("action|stop")}
    {hasLocationPublishError && ( = ({ latestLocationState }) => { diff --git a/src/components/views/beacon/displayStatus.ts b/src/components/views/beacon/displayStatus.ts index 48260cb672f..a2cd4b662bb 100644 --- a/src/components/views/beacon/displayStatus.ts +++ b/src/components/views/beacon/displayStatus.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { BeaconLocationState } from "matrix-js-sdk/src/content-helpers"; +import { ContentHelpers } from "matrix-js-sdk/src/matrix"; export enum BeaconDisplayStatus { Loading = "Loading", @@ -24,7 +24,7 @@ export enum BeaconDisplayStatus { } export const getBeaconDisplayStatus = ( isLive: boolean, - latestLocationState?: BeaconLocationState, + latestLocationState?: ContentHelpers.BeaconLocationState, error?: Error, waitingToStart?: boolean, ): BeaconDisplayStatus => { diff --git a/src/components/views/beta/BetaCard.tsx b/src/components/views/beta/BetaCard.tsx index 7fbf68b2908..a604aee1ec5 100644 --- a/src/components/views/beta/BetaCard.tsx +++ b/src/components/views/beta/BetaCard.tsx @@ -61,12 +61,12 @@ export const BetaPill: React.FC = ({ } onClick={onClick} > - {_t("Beta")} + {_t("common|beta")} ); } - return {_t("Beta")}; + return {_t("common|beta")}; }; const BetaCard: React.FC = ({ title: titleOverride, featureId }) => { diff --git a/src/components/views/context_menus/DeviceContextMenu.tsx b/src/components/views/context_menus/DeviceContextMenu.tsx index 16ac988f6fb..81c33b56efa 100644 --- a/src/components/views/context_menus/DeviceContextMenu.tsx +++ b/src/components/views/context_menus/DeviceContextMenu.tsx @@ -19,9 +19,9 @@ import React, { useEffect, useState } from "react"; import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../MediaDeviceHandler"; import IconizedContextMenu, { IconizedContextMenuOptionList, IconizedContextMenuRadio } from "./IconizedContextMenu"; import { IProps as IContextMenuProps } from "../../structures/ContextMenu"; -import { _t, _td } from "../../../languageHandler"; +import { _t, _td, TranslationKey } from "../../../languageHandler"; -const SECTION_NAMES: Record = { +const SECTION_NAMES: Record = { [MediaDeviceKindEnum.AudioInput]: _td("Input devices"), [MediaDeviceKindEnum.AudioOutput]: _td("Output devices"), [MediaDeviceKindEnum.VideoInput]: _td("Cameras"), diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index 012119c3c6a..f92f66ceff6 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -26,8 +26,8 @@ import { RelationType, Relations, Thread, + M_POLL_START, } from "matrix-js-sdk/src/matrix"; -import { M_POLL_START } from "matrix-js-sdk/src/@types/polls"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import dis from "../../../dispatcher/dispatcher"; @@ -425,7 +425,7 @@ export default class MessageContextMenu extends React.Component redactButton = ( ); @@ -456,7 +456,7 @@ export default class MessageContextMenu extends React.Component forwardButton = ( ); @@ -467,7 +467,7 @@ export default class MessageContextMenu extends React.Component pinButton = ( ); @@ -499,7 +499,7 @@ export default class MessageContextMenu extends React.Component quoteButton = ( ); @@ -600,7 +600,7 @@ export default class MessageContextMenu extends React.Component copyButton = ( @@ -631,7 +631,7 @@ export default class MessageContextMenu extends React.Component editButton = ( ); @@ -642,7 +642,7 @@ export default class MessageContextMenu extends React.Component replyButton = ( ); @@ -664,7 +664,7 @@ export default class MessageContextMenu extends React.Component reactButton = ( diff --git a/src/components/views/context_menus/RoomContextMenu.tsx b/src/components/views/context_menus/RoomContextMenu.tsx index 883b86f3dae..cf3704ffa06 100644 --- a/src/components/views/context_menus/RoomContextMenu.tsx +++ b/src/components/views/context_menus/RoomContextMenu.tsx @@ -105,7 +105,7 @@ const RoomContextMenu: React.FC = ({ room, onFinished, ...props }) => { leaveOption = ( @@ -136,7 +136,7 @@ const RoomContextMenu: React.FC = ({ room, onFinished, ...props }) => { inviteOption = ( ); @@ -186,7 +186,7 @@ const RoomContextMenu: React.FC = ({ room, onFinished, ...props }) => { iconClassName = "mx_RoomTile_iconNotificationsMentionsKeywords"; break; case RoomNotifState.Mute: - notificationLabel = _t("Mute"); + notificationLabel = _t("common|mute"); iconClassName = "mx_RoomTile_iconNotificationsNone"; break; } @@ -228,7 +228,7 @@ const RoomContextMenu: React.FC = ({ room, onFinished, ...props }) => { onFinished(); PosthogTrackers.trackInteraction("WebRoomHeaderContextMenuPeopleItem", ev); }} - label={_t("People")} + label={_t("common|people")} iconClassName="mx_RoomTile_iconPeople" > {room.getJoinedMemberCount()} @@ -390,7 +390,7 @@ const RoomContextMenu: React.FC = ({ room, onFinished, ...props }) => { onFinished(); PosthogTrackers.trackInteraction("WebRoomHeaderContextMenuSettingsItem", ev); }} - label={_t("Settings")} + label={_t("common|settings")} iconClassName="mx_RoomTile_iconSettings" /> diff --git a/src/components/views/context_menus/RoomGeneralContextMenu.tsx b/src/components/views/context_menus/RoomGeneralContextMenu.tsx index 965a34c8983..eba32eca06a 100644 --- a/src/components/views/context_menus/RoomGeneralContextMenu.tsx +++ b/src/components/views/context_menus/RoomGeneralContextMenu.tsx @@ -138,7 +138,7 @@ export const RoomGeneralContextMenu: React.FC = ({ }), onPostInviteClick, )} - label={_t("Invite")} + label={_t("action|invite")} iconClassName="mx_RoomGeneralContextMenu_iconInvite" /> ); @@ -172,7 +172,7 @@ export const RoomGeneralContextMenu: React.FC = ({ }), onPostSettingsClick, )} - label={_t("Settings")} + label={_t("common|settings")} iconClassName="mx_RoomGeneralContextMenu_iconSettings" /> ); @@ -205,7 +205,7 @@ export const RoomGeneralContextMenu: React.FC = ({ }), onPostLeaveClick, )} - label={_t("Leave")} + label={_t("action|leave")} className="mx_IconizedContextMenu_option_red" iconClassName="mx_RoomGeneralContextMenu_iconSignOut" /> diff --git a/src/components/views/context_menus/SpaceContextMenu.tsx b/src/components/views/context_menus/SpaceContextMenu.tsx index 811a88ca209..d9262270200 100644 --- a/src/components/views/context_menus/SpaceContextMenu.tsx +++ b/src/components/views/context_menus/SpaceContextMenu.tsx @@ -69,7 +69,7 @@ const SpaceContextMenu: React.FC = ({ space, hideHeader, onFinished, ... data-testid="invite-option" className="mx_SpacePanel_contextMenu_inviteButton" iconClassName="mx_SpacePanel_iconInvite" - label={_t("Invite")} + label={_t("action|invite")} onClick={onInviteClick} /> ); @@ -90,7 +90,7 @@ const SpaceContextMenu: React.FC = ({ space, hideHeader, onFinished, ... ); @@ -173,13 +173,13 @@ const SpaceContextMenu: React.FC = ({ space, hideHeader, onFinished, ... newRoomSection = ( <>
    - {_t("Add")} + {_t("action|add")}
    {canAddRooms && ( )} diff --git a/src/components/views/context_menus/WidgetContextMenu.tsx b/src/components/views/context_menus/WidgetContextMenu.tsx index 83fde0722cd..2525f3d2b28 100644 --- a/src/components/views/context_menus/WidgetContextMenu.tsx +++ b/src/components/views/context_menus/WidgetContextMenu.tsx @@ -159,7 +159,7 @@ export const WidgetContextMenu: React.FC = ({ onFinished(); }; - editButton = ; + editButton = ; } let snapshotButton: JSX.Element | undefined; @@ -192,8 +192,7 @@ export const WidgetContextMenu: React.FC = ({ Modal.createDialog(QuestionDialog, { title: _t("Delete Widget"), description: _t( - "Deleting a widget removes it for all users in this room." + - " Are you sure you want to delete this widget?", + "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?", ), button: _t("Delete widget"), onFinished: (confirmed) => { @@ -209,7 +208,7 @@ export const WidgetContextMenu: React.FC = ({ deleteButton = ( ); } diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index e2d4481e0b2..7d6c10839db 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -20,7 +20,7 @@ import { Room, EventType } from "matrix-js-sdk/src/matrix"; import { sleep } from "matrix-js-sdk/src/utils"; import { logger } from "matrix-js-sdk/src/logger"; -import { _t, _td } from "../../../languageHandler"; +import { _t, _td, TranslationKey } from "../../../languageHandler"; import BaseDialog from "./BaseDialog"; import Dropdown from "../elements/Dropdown"; import SearchBox from "../../structures/SearchBox"; @@ -62,9 +62,9 @@ export const Entry: React.FC<{ return (
    diff --git a/src/components/views/dialogs/IntegrationsImpossibleDialog.tsx b/src/components/views/dialogs/IntegrationsImpossibleDialog.tsx index 5b158402e87..6eee4e73c8b 100644 --- a/src/components/views/dialogs/IntegrationsImpossibleDialog.tsx +++ b/src/components/views/dialogs/IntegrationsImpossibleDialog.tsx @@ -43,14 +43,13 @@ export default class IntegrationsImpossibleDialog extends React.Component

    {_t( - "Your %(brand)s doesn't allow you to use an integration manager to do this. " + - "Please contact an admin.", + "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.", { brand }, )}

    diff --git a/src/components/views/dialogs/InteractiveAuthDialog.tsx b/src/components/views/dialogs/InteractiveAuthDialog.tsx index 4fbb3456f65..fbb57148939 100644 --- a/src/components/views/dialogs/InteractiveAuthDialog.tsx +++ b/src/components/views/dialogs/InteractiveAuthDialog.tsx @@ -17,9 +17,8 @@ limitations under the License. */ import React from "react"; -import { MatrixClient } from "matrix-js-sdk/src/matrix"; +import { MatrixClient, UIAResponse } from "matrix-js-sdk/src/matrix"; import { AuthType } from "matrix-js-sdk/src/interactive-auth"; -import { UIAResponse } from "matrix-js-sdk/src/@types/uia"; import { _t } from "../../../languageHandler"; import AccessibleButton from "../elements/AccessibleButton"; @@ -106,7 +105,7 @@ export default class InteractiveAuthDialog extends React.Component extends React.Component = (success, result): void => { + private onAuthFinished: InteractiveAuthCallback = async (success, result): Promise => { if (success) { this.props.onFinished(true, result); } else { @@ -172,7 +171,7 @@ export default class InteractiveAuthDialog extends React.Component{this.state.authError.message || this.state.authError.toString()}
    - {_t("Dismiss")} + {_t("action|dismiss")} ); diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index 67818a308ad..d37c834f50d 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -126,7 +126,7 @@ class DMUserTile extends React.PureComponent { }; public render(): React.ReactNode { - const avatarSize = 20; + const avatarSize = "20px"; const avatar = ; let closeButton; @@ -135,7 +135,7 @@ class DMUserTile extends React.PureComponent { {_t("Remove")} @@ -233,20 +233,21 @@ class DMRoomTile extends React.PureComponent { timestamp = {humanTs}; } - const avatarSize = 36; + const avatarSize = "36px"; const avatar = (this.props.member as ThreepidMember).isEmail ? ( ) : ( ); @@ -944,7 +945,7 @@ export default class InviteDialog extends React.PureComponent (kind === "recents" ? m.lastActive : undefined); - let sectionName = kind === "recents" ? _t("Recent Conversations") : _t("Suggestions"); + let sectionName = kind === "recents" ? _t("Recent Conversations") : _t("common|suggestions"); if (this.props.kind === InviteKind.Invite) { - sectionName = kind === "recents" ? _t("Recently Direct Messaged") : _t("Suggestions"); + sectionName = kind === "recents" ? _t("Recently Direct Messaged") : _t("common|suggestions"); } // Mix in the server results if we have any, but only if we're searching. We track the additional @@ -1038,7 +1039,7 @@ export default class InviteDialog extends React.PureComponent

    {sectionName}

    -

    {_t("No results")}

    +

    {_t("common|no_results")}

    ); } @@ -1107,7 +1108,7 @@ export default class InviteDialog extends React.PureComponent 0) } autoComplete="off" - placeholder={hasPlaceholder ? _t("Search") : undefined} + placeholder={hasPlaceholder ? _t("action|search") : undefined} data-testid="invite-dialog-input" /> ); @@ -1133,9 +1134,7 @@ export default class InviteDialog extends React.PureComponent {_t( - "Use an identity server to invite by email. " + - "Use the default (%(defaultIdentityServerName)s) " + - "or manage in Settings.", + "Use an identity server to invite by email. Use the default (%(defaultIdentityServerName)s) or manage in Settings.", { defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl), }, @@ -1158,7 +1157,7 @@ export default class InviteDialog extends React.PureComponent {_t( - "Use an identity server to invite by email. " + "Manage in Settings.", + "Use an identity server to invite by email. Manage in Settings.", {}, { settings: (sub) => ( @@ -1349,23 +1348,21 @@ export default class InviteDialog extends React.PureComponent) or share this space.", + "Invite someone using their name, email address, username (like ) or share this space.", ); } else { helpTextUntranslated = _td( - "Invite someone using their name, username " + "(like ) or share this space.", + "Invite someone using their name, username (like ) or share this space.", ); } } else { if (identityServersEnabled) { helpTextUntranslated = _td( - "Invite someone using their name, email address, username " + - "(like ) or share this room.", + "Invite someone using their name, email address, username (like ) or share this room.", ); } else { helpTextUntranslated = _td( - "Invite someone using their name, username " + "(like ) or share this room.", + "Invite someone using their name, username (like ) or share this room.", ); } } @@ -1392,7 +1389,7 @@ export default class InviteDialog extends React.PureComponent - {_t("Cancel")} + {_t("action|cancel")}
    = ({ failures, source, co body = (
    {text} - +
    ); } diff --git a/src/components/views/dialogs/LazyLoadingDisabledDialog.tsx b/src/components/views/dialogs/LazyLoadingDisabledDialog.tsx index e1d5374fb3e..cd69a6ce72f 100644 --- a/src/components/views/dialogs/LazyLoadingDisabledDialog.tsx +++ b/src/components/views/dialogs/LazyLoadingDisabledDialog.tsx @@ -29,19 +29,14 @@ interface IProps { const LazyLoadingDisabledDialog: React.FC = (props) => { const brand = SdkConfig.get().brand; const description1 = _t( - "You've previously used %(brand)s on %(host)s with lazy loading of members enabled. " + - "In this version lazy loading is disabled. " + - "As the local cache is not compatible between these two settings, " + - "%(brand)s needs to resync your account.", + "You've previously used %(brand)s on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, %(brand)s needs to resync your account.", { brand, host: props.host, }, ); const description2 = _t( - "If the other version of %(brand)s is still open in another tab, " + - "please close it as using %(brand)s on the same host with both " + - "lazy loading enabled and disabled simultaneously will cause issues.", + "If the other version of %(brand)s is still open in another tab, please close it as using %(brand)s on the same host with both lazy loading enabled and disabled simultaneously will cause issues.", { brand, }, diff --git a/src/components/views/dialogs/LazyLoadingResyncDialog.tsx b/src/components/views/dialogs/LazyLoadingResyncDialog.tsx index c5bd2e0227c..2caa4e6fce8 100644 --- a/src/components/views/dialogs/LazyLoadingResyncDialog.tsx +++ b/src/components/views/dialogs/LazyLoadingResyncDialog.tsx @@ -28,9 +28,7 @@ interface IProps { const LazyLoadingResyncDialog: React.FC = (props) => { const brand = SdkConfig.get().brand; const description = _t( - "%(brand)s now uses 3-5x less memory, by only loading information " + - "about other users when needed. Please wait whilst we resynchronise " + - "with the server!", + "%(brand)s now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!", { brand }, ); @@ -39,7 +37,7 @@ const LazyLoadingResyncDialog: React.FC = (props) => { hasCancelButton={false} title={_t("Updating %(brand)s", { brand })} description={
    {description}
    } - button={_t("OK")} + button={_t("action|ok")} onFinished={props.onFinished} /> ); diff --git a/src/components/views/dialogs/LeaveSpaceDialog.tsx b/src/components/views/dialogs/LeaveSpaceDialog.tsx index cc31183414b..a8a9aca4d8e 100644 --- a/src/components/views/dialogs/LeaveSpaceDialog.tsx +++ b/src/components/views/dialogs/LeaveSpaceDialog.tsx @@ -63,15 +63,12 @@ const LeaveSpaceDialog: React.FC = ({ space, onFinished }) => { let onlyAdminWarning; if (isOnlyAdmin(space)) { - onlyAdminWarning = _t( - "You're the only admin of this space. " + "Leaving it will mean no one has control over it.", - ); + onlyAdminWarning = _t("You're the only admin of this space. Leaving it will mean no one has control over it."); } else { const numChildrenOnlyAdminIn = roomsToLeave.filter(isOnlyAdmin).length; if (numChildrenOnlyAdminIn > 0) { onlyAdminWarning = _t( - "You're the only admin of some of the rooms or spaces you wish to leave. " + - "Leaving them will leave them without any admins.", + "You're the only admin of some of the rooms or spaces you wish to leave. Leaving them will leave them without any admins.", ); } } diff --git a/src/components/views/dialogs/LogoutDialog.tsx b/src/components/views/dialogs/LogoutDialog.tsx index 9830cf59922..2086a33b8af 100644 --- a/src/components/views/dialogs/LogoutDialog.tsx +++ b/src/components/views/dialogs/LogoutDialog.tsx @@ -138,16 +138,12 @@ export default class LogoutDialog extends React.Component {

    {_t( - "Encrypted messages are secured with end-to-end encryption. " + - "Only you and the recipient(s) have the keys to read these messages.", + "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.", )}

    {_t( - "When you sign out, these keys will be deleted from this device, " + - "which means you won't be able to read encrypted messages unless you " + - "have the keys for them on your other devices, or backed them up to the " + - "server.", + "When you sign out, these keys will be deleted from this device, which means you won't be able to read encrypted messages unless you have the keys for them on your other devices, or backed them up to the server.", )}

    {_t("Back up your keys before signing out to avoid losing them.")}

    @@ -206,9 +202,9 @@ export default class LogoutDialog extends React.Component { return ( ); diff --git a/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx b/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx index d29b9e8bafb..f01ff6d362a 100644 --- a/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx +++ b/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx @@ -54,11 +54,7 @@ const Entry: React.FC<{
    { let buttons; if (this.props.totalFiles === 1 && this.props.badFiles.length === 1) { message = _t( - "This file is too large to upload. " + - "The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.", + "This file is too large to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.", { limit: fileSize(this.props.contentMessages.getUploadLimit()), sizeOfThisFile: fileSize(this.props.badFiles[0].size), @@ -61,7 +60,7 @@ export default class UploadFailureDialog extends React.Component { ); buttons = ( { ); } else if (this.props.totalFiles === this.props.badFiles.length) { message = _t( - "These files are too large to upload. " + "The file size limit is %(limit)s.", + "These files are too large to upload. The file size limit is %(limit)s.", { limit: fileSize(this.props.contentMessages.getUploadLimit()), }, @@ -79,7 +78,7 @@ export default class UploadFailureDialog extends React.Component { ); buttons = ( { ); } else { message = _t( - "Some files are too large to be uploaded. " + "The file size limit is %(limit)s.", + "Some files are too large to be uploaded. The file size limit is %(limit)s.", { limit: fileSize(this.props.contentMessages.getUploadLimit()), }, diff --git a/src/components/views/dialogs/UserSettingsDialog.tsx b/src/components/views/dialogs/UserSettingsDialog.tsx index c185da1f2df..e35bed48395 100644 --- a/src/components/views/dialogs/UserSettingsDialog.tsx +++ b/src/components/views/dialogs/UserSettingsDialog.tsx @@ -86,7 +86,7 @@ export default class UserSettingsDialog extends React.Component tabs.push( new Tab( UserTab.Appearance, - _td("Appearance"), + _td("common|appearance"), "mx_UserSettingsDialog_appearanceIcon", , "UserSettingsAppearance", @@ -168,7 +168,7 @@ export default class UserSettingsDialog extends React.Component tabs.push( new Tab( UserTab.Labs, - _td("Labs"), + _td("common|labs"), "mx_UserSettingsDialog_labsIcon", , "UserSettingsLabs", @@ -205,7 +205,7 @@ export default class UserSettingsDialog extends React.Component className="mx_UserSettingsDialog" hasCancel={true} onFinished={this.props.onFinished} - title={_t("Settings")} + title={_t("common|settings")} >
    > = ({
    {children}
    {extraButton} - + {actionButton}
    diff --git a/src/components/views/dialogs/devtools/Event.tsx b/src/components/views/dialogs/devtools/Event.tsx index 16d76752eb1..f0a89fd6a4d 100644 --- a/src/components/views/dialogs/devtools/Event.tsx +++ b/src/components/views/dialogs/devtools/Event.tsx @@ -18,7 +18,7 @@ limitations under the License. import React, { ChangeEvent, ReactNode, useContext, useMemo, useRef, useState } from "react"; import { IContent, MatrixEvent } from "matrix-js-sdk/src/matrix"; -import { _t, _td } from "../../../../languageHandler"; +import { _t, _td, TranslationKey } from "../../../../languageHandler"; import Field from "../../elements/Field"; import BaseTool, { DevtoolsContext, IDevtoolsProps } from "./BaseTool"; import MatrixClientContext from "../../../../contexts/MatrixClientContext"; @@ -37,7 +37,7 @@ interface IEventEditorProps extends Pick { interface IFieldDef { id: string; - label: string; // _td + label: TranslationKey; default?: string; } @@ -161,7 +161,7 @@ export const EventViewer: React.FC = ({ mxEvent, onBack, Editor, e }; return ( - + {stringify(mxEvent.event)} ); diff --git a/src/components/views/dialogs/devtools/RoomNotifications.tsx b/src/components/views/dialogs/devtools/RoomNotifications.tsx index cbb0f3c0a6a..34e9a29c6b0 100644 --- a/src/components/views/dialogs/devtools/RoomNotifications.tsx +++ b/src/components/views/dialogs/devtools/RoomNotifications.tsx @@ -14,9 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { NotificationCountType, Room, Thread } from "matrix-js-sdk/src/matrix"; +import { NotificationCountType, Room, Thread, ReceiptType } from "matrix-js-sdk/src/matrix"; import React, { useContext } from "react"; -import { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts"; import { ReadReceipt } from "matrix-js-sdk/src/models/read-receipt"; import MatrixClientContext from "../../../../contexts/MatrixClientContext"; @@ -76,16 +75,26 @@ export default function RoomNotifications({ onBack }: IDevtoolsProps): JSX.Eleme

    {_t("Room status")}

    • - {_t( - "Room unread status: %(status)s, count: %(count)s", - { - status: humanReadableNotificationColor(color), - count, - }, - { - strong: (sub) => {sub}, - }, - )} + {count > 0 + ? _t( + "Room unread status: %(status)s, count: %(count)s", + { + status: humanReadableNotificationColor(color), + count, + }, + { + strong: (sub) => {sub}, + }, + ) + : _t( + "Room unread status: %(status)s", + { + status: humanReadableNotificationColor(color), + }, + { + strong: (sub) => {sub}, + }, + )}
    • {_t( diff --git a/src/components/views/dialogs/devtools/RoomState.tsx b/src/components/views/dialogs/devtools/RoomState.tsx index e84799c513c..9dbbf89fbce 100644 --- a/src/components/views/dialogs/devtools/RoomState.tsx +++ b/src/components/views/dialogs/devtools/RoomState.tsx @@ -95,6 +95,11 @@ const RoomStateHistory: React.FC<{ const StateEventButton: React.FC = ({ label, onClick }) => { const trimmed = label.trim(); + let content = label; + if (!trimmed) { + content = label.length > 0 ? _t("<%(count)s spaces>", { count: label.length }) : _t(""); + } + return ( ); }; diff --git a/src/components/views/dialogs/devtools/VerificationExplorer.tsx b/src/components/views/dialogs/devtools/VerificationExplorer.tsx index aaaf02dea4d..171bfd01769 100644 --- a/src/components/views/dialogs/devtools/VerificationExplorer.tsx +++ b/src/components/views/dialogs/devtools/VerificationExplorer.tsx @@ -21,16 +21,16 @@ import { VerificationPhase as Phase, VerificationRequestEvent } from "matrix-js- import { CryptoEvent } from "matrix-js-sdk/src/crypto"; import { useTypedEventEmitter, useTypedEventEmitterState } from "../../../../hooks/useEventEmitter"; -import { _t, _td } from "../../../../languageHandler"; +import { _t, _td, TranslationKey } from "../../../../languageHandler"; import MatrixClientContext from "../../../../contexts/MatrixClientContext"; import BaseTool, { DevtoolsContext, IDevtoolsProps } from "./BaseTool"; import { Tool } from "../DevtoolsDialog"; -const PHASE_MAP: Record = { +const PHASE_MAP: Record = { [Phase.Unsent]: _td("Unsent"), [Phase.Requested]: _td("Requested"), [Phase.Ready]: _td("Ready"), - [Phase.Done]: _td("Done"), + [Phase.Done]: _td("action|done"), [Phase.Started]: _td("Started"), [Phase.Cancelled]: _td("Cancelled"), }; diff --git a/src/components/views/dialogs/oidc/OidcLogoutDialog.tsx b/src/components/views/dialogs/oidc/OidcLogoutDialog.tsx new file mode 100644 index 00000000000..b15051ba52b --- /dev/null +++ b/src/components/views/dialogs/oidc/OidcLogoutDialog.tsx @@ -0,0 +1,74 @@ +/* +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, { useState } from "react"; + +import { _t } from "../../../../languageHandler"; +import BaseDialog from "../BaseDialog"; +import { getOidcLogoutUrl } from "../../../../utils/oidc/getOidcLogoutUrl"; +import AccessibleButton from "../../elements/AccessibleButton"; + +export interface OidcLogoutDialogProps { + delegatedAuthAccountUrl: string; + deviceId: string; + onFinished(ok?: boolean): void; +} + +/** + * Handle logout of OIDC sessions other than the current session + * - ask for user confirmation to open the delegated auth provider + * - open the auth provider in a new tab + * - wait for the user to return and close the modal, we assume the user has completed sign out of the session in auth provider UI + * and trigger a refresh of the session list + */ +export const OidcLogoutDialog: React.FC = ({ + delegatedAuthAccountUrl, + deviceId, + onFinished, +}) => { + const [hasOpenedLogoutLink, setHasOpenedLogoutLink] = useState(false); + const logoutUrl = getOidcLogoutUrl(delegatedAuthAccountUrl, deviceId); + + return ( + +
      + {_t("You will be redirected to your server's authentication provider to complete sign out.")} +
      +
      + {hasOpenedLogoutLink ? ( + onFinished(true)}> + {_t("action|close")} + + ) : ( + <> + onFinished(false)}> + {_t("action|cancel")} + + setHasOpenedLogoutLink(true)} + kind="primary" + href={logoutUrl} + target="_blank" + > + {_t("action|continue")} + + + )} +
      +
      + ); +}; diff --git a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx index 2ee052d2a2a..0bead58e7df 100644 --- a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx +++ b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx @@ -305,12 +305,11 @@ export default class AccessSecretStorageDialog extends React.PureComponent{_t("Only do this if you have no other device to complete verification with.")}

      {_t( - "If you reset everything, you will restart with no trusted sessions, no trusted users, and " + - "might not be able to see past messages.", + "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.", )}

      {"\uD83D\uDC4E "} {_t( - "Unable to access secret storage. " + - "Please verify that you entered the correct Security Phrase.", + "Unable to access secret storage. Please verify that you entered the correct Security Phrase.", )} ); @@ -368,7 +366,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {keyStatus} - {_t("Upload")} + {_t("action|upload")} {recoveryKeyFeedback}

      {_t( - "Deleting cross-signing keys is permanent. " + - "Anyone you have verified with will see security alerts. " + - "You almost certainly don't want to do this, unless " + - "you've lost every device you can cross-sign from.", + "Deleting cross-signing keys is permanent. Anyone you have verified with will see security alerts. You almost certainly don't want to do this, unless you've lost every device you can cross-sign from.", )}

      @@ -55,7 +52,7 @@ export default class ConfirmDestroyCrossSigningDialog extends React.Component diff --git a/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx index ba54bc28f5d..0a55fc6de4e 100644 --- a/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx +++ b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx @@ -16,9 +16,8 @@ limitations under the License. */ import React from "react"; -import { CrossSigningKeys, AuthDict, MatrixError, UIAFlow } from "matrix-js-sdk/src/matrix"; +import { CrossSigningKeys, AuthDict, MatrixError, UIAFlow, UIAResponse } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; -import { UIAResponse } from "matrix-js-sdk/src/@types/uia"; import { MatrixClientPeg } from "../../../../MatrixClientPeg"; import { _t } from "../../../../languageHandler"; @@ -121,7 +120,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent{_t("Unable to set up keys")}

      diff --git a/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx b/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx index fdf558e8ea8..34399a25bbf 100644 --- a/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx +++ b/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx @@ -338,7 +338,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent ); } else if (this.state.loadError) { - title = _t("Error"); + title = _t("common|error"); content = _t("Unable to load backup status"); } else if (this.state.restoreError) { if ( @@ -351,8 +351,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent

      {_t( - "Backup could not be decrypted with this Security Key: " + - "please verify that you entered the correct Security Key.", + "Backup could not be decrypted with this Security Key: please verify that you entered the correct Security Key.", )}

      @@ -363,19 +362,18 @@ export default class RestoreKeyBackupDialog extends React.PureComponent

      {_t( - "Backup could not be decrypted with this Security Phrase: " + - "please verify that you entered the correct Security Phrase.", + "Backup could not be decrypted with this Security Phrase: please verify that you entered the correct Security Phrase.", )}

      ); } } else { - title = _t("Error"); + title = _t("common|error"); content = _t("Unable to restore backup"); } } else if (this.state.backupInfo === null) { - title = _t("Error"); + title = _t("common|error"); content = _t("No backup found!"); } else if (this.state.recoverInfo) { title = _t("Keys restored"); @@ -398,7 +396,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {failedToDecrypt}

      {_t( - "Access your secure message history and set up secure " + - "messaging by entering your Security Phrase.", + "Access your secure message history and set up secure messaging by entering your Security Phrase.", )}

      @@ -432,7 +429,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {_t( - "If you've forgotten your Security Phrase you can " + - "use your Security Key or " + - "set up new recovery options", + "If you've forgotten your Security Phrase you can use your Security Key or set up new recovery options", {}, { button1: (s) => ( @@ -493,8 +488,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent

      {_t( - "Access your secure message history and set up secure " + - "messaging by entering your Security Key.", + "Access your secure message history and set up secure messaging by entering your Security Key.", )}

      @@ -507,7 +501,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {keyStatus} {_t( - "If you've forgotten your Security Key you can " + - "", + "If you've forgotten your Security Key you can ", {}, { button: (s) => ( diff --git a/src/components/views/dialogs/spotlight/Filter.ts b/src/components/views/dialogs/spotlight/Filter.ts new file mode 100644 index 00000000000..11cdb6dc6ba --- /dev/null +++ b/src/components/views/dialogs/spotlight/Filter.ts @@ -0,0 +1,21 @@ +/* +Copyright 2021 - 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. +*/ + +export enum Filter { + People, + PublicRooms, + PublicSpaces, +} diff --git a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx index 8ccffaf1b84..758ff320924 100644 --- a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx +++ b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx @@ -17,8 +17,14 @@ limitations under the License. import { WebSearch as WebSearchEvent } from "@matrix-org/analytics-events/types/typescript/WebSearch"; import classNames from "classnames"; import { capitalize, sum } from "lodash"; -import { IHierarchyRoom } from "matrix-js-sdk/src/@types/spaces"; -import { IPublicRoomsChunkRoom, MatrixClient, RoomMember, RoomType, Room } from "matrix-js-sdk/src/matrix"; +import { + IPublicRoomsChunkRoom, + MatrixClient, + RoomMember, + RoomType, + Room, + HierarchyRoom, +} from "matrix-js-sdk/src/matrix"; import { normalize } from "matrix-js-sdk/src/utils"; import React, { ChangeEvent, RefObject, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import sanitizeHtml from "sanitize-html"; @@ -65,7 +71,6 @@ import DecoratedRoomAvatar from "../../avatars/DecoratedRoomAvatar"; import { SearchResultAvatar } from "../../avatars/SearchResultAvatar"; import { NetworkDropdown } from "../../directory/NetworkDropdown"; import AccessibleButton, { ButtonEvent } from "../../elements/AccessibleButton"; -import LabelledCheckbox from "../../elements/LabelledCheckbox"; import Spinner from "../../elements/Spinner"; import NotificationBadge from "../../rooms/NotificationBadge"; import BaseDialog from "../BaseDialog"; @@ -79,10 +84,11 @@ import RoomAvatar from "../../avatars/RoomAvatar"; import { useFeatureEnabled } from "../../../../hooks/useSettings"; import { filterBoolean } from "../../../../utils/arrays"; import { transformSearchTerm } from "../../../../utils/SearchInput"; +import { Filter } from "./Filter"; const MAX_RECENT_SEARCHES = 10; const SECTION_LIMIT = 50; // only show 50 results per section for performance reasons -const AVATAR_SIZE = 24; +const AVATAR_SIZE = "24px"; interface IProps { initialText?: string; @@ -94,11 +100,11 @@ function refIsForRecentlyViewed(ref?: RefObject): boolean { return ref?.current?.id?.startsWith("mx_SpotlightDialog_button_recentlyViewed_") === true; } -function getRoomTypes(showRooms: boolean, showSpaces: boolean): Set { +function getRoomTypes(filter: Filter | null): Set { const roomTypes = new Set(); - if (showRooms) roomTypes.add(null); - if (showSpaces) roomTypes.add(RoomType.Space); + if (filter === Filter.PublicRooms) roomTypes.add(null); + if (filter === Filter.PublicSpaces) roomTypes.add(RoomType.Space); return roomTypes; } @@ -108,20 +114,17 @@ enum Section { Rooms, Spaces, Suggestions, - PublicRooms, -} - -export enum Filter { - People, - PublicRooms, + PublicRoomsAndSpaces, } function filterToLabel(filter: Filter): string { switch (filter) { case Filter.People: - return _t("People"); + return _t("common|people"); case Filter.PublicRooms: return _t("Public rooms"); + case Filter.PublicSpaces: + return _t("Public spaces"); } } @@ -162,8 +165,8 @@ const isMemberResult = (result: any): result is IMemberResult => !!result?.membe const toPublicRoomResult = (publicRoom: IPublicRoomsChunkRoom): IPublicRoomResult => ({ publicRoom, - section: Section.PublicRooms, - filter: [Filter.PublicRooms], + section: Section.PublicRoomsAndSpaces, + filter: [Filter.PublicRooms, Filter.PublicSpaces], query: filterBoolean([ publicRoom.room_id.toLowerCase(), publicRoom.canonical_alias?.toLowerCase(), @@ -302,7 +305,16 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n const [inviteLinkCopied, setInviteLinkCopied] = useState(false); const trimmedQuery = useMemo(() => query.trim(), [query]); - const exploringPublicSpacesEnabled = useFeatureEnabled("feature_exploring_public_spaces"); + const [supportsSpaceFiltering, setSupportsSpaceFiltering] = useState(true); // assume it does until we find out it doesn't + useEffect(() => { + cli.isVersionSupported("v1.4") + .then((supported) => { + return supported || cli.doesServerSupportUnstableFeature("org.matrix.msc3827.stable"); + }) + .then((supported) => { + setSupportsSpaceFiltering(supported); + }); + }, [cli]); const { loading: publicRoomsLoading, @@ -313,21 +325,23 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n search: searchPublicRooms, error: publicRoomsError, } = usePublicRoomDirectory(); - const [showRooms, setShowRooms] = useState(true); - const [showSpaces, setShowSpaces] = useState(false); const { loading: peopleLoading, users: userDirectorySearchResults, search: searchPeople } = useUserDirectory(); const { loading: profileLoading, profile, search: searchProfileInfo } = useProfileInfo(); const searchParams: [IDirectoryOpts] = useMemo( () => [ { query: trimmedQuery, - roomTypes: getRoomTypes(showRooms, showSpaces), + roomTypes: getRoomTypes(filter), limit: SECTION_LIMIT, }, ], - [trimmedQuery, showRooms, showSpaces], + [trimmedQuery, filter], + ); + useDebouncedCallback( + filter === Filter.PublicRooms || filter === Filter.PublicSpaces, + searchPublicRooms, + searchParams, ); - useDebouncedCallback(filter === Filter.PublicRooms, searchPublicRooms, searchParams); useDebouncedCallback(filter === Filter.People, searchPeople, searchParams); useDebouncedCallback(filter === Filter.People, searchProfileInfo, searchParams); @@ -397,7 +411,7 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n [Section.Rooms]: [], [Section.Spaces]: [], [Section.Suggestions]: [], - [Section.PublicRooms]: [], + [Section.PublicRoomsAndSpaces]: [], }; // Group results in their respective sections @@ -430,7 +444,7 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n results[entry.section].push(entry); }); - } else if (filter === Filter.PublicRooms) { + } else if (filter === Filter.PublicRooms || filter === Filter.PublicSpaces) { // return all results for public rooms if no query is given possibleResults.forEach((entry) => { if (isPublicRoomResult(entry)) { @@ -543,6 +557,15 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n {trimmedQuery ? _t('Use "%(query)s" to search', { query }) : _t("Search for")}
      + {filter !== Filter.PublicSpaces && supportsSpaceFiltering && ( + + )} {filter !== Filter.PublicRooms && (
      ); @@ -763,22 +781,18 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n } let publicRoomsSection: JSX.Element | undefined; - if (filter === Filter.PublicRooms) { + if (filter === Filter.PublicRooms || filter === Filter.PublicSpaces) { let content: JSX.Element | JSX.Element[]; - if (!showRooms && !showSpaces) { + if (publicRoomsError) { content = (
      - {_t("You cannot search for rooms that are neither a room nor a space")} -
      - ); - } else if (publicRoomsError) { - content = ( -
      - {_t("Failed to query public rooms")} + {filter === Filter.PublicRooms + ? _t("Failed to query public rooms") + : _t("Failed to query public spaces")}
      ); } else { - content = results[Section.PublicRooms].slice(0, SECTION_LIMIT).map(resultMapper); + content = results[Section.PublicRoomsAndSpaces].slice(0, SECTION_LIMIT).map(resultMapper); } publicRoomsSection = ( @@ -788,22 +802,8 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n aria-labelledby="mx_SpotlightDialog_section_publicRooms" >
      -

      {_t("Suggestions")}

      +

      {_t("common|suggestions")}

      - {exploringPublicSpacesEnabled && ( - <> - - - - )}
      @@ -825,7 +825,7 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n
      {spaceResults.slice(0, SECTION_LIMIT).map( - (room: IHierarchyRoom): JSX.Element => ( + (room: HierarchyRoom): JSX.Element => (
      ); - } else if (trimmedQuery && filter === Filter.PublicRooms) { + } else if (trimmedQuery && (filter === Filter.PublicRooms || filter === Filter.PublicSpaces)) { hiddenResultsSection = (

      {_t("Some results may be hidden")}

      - {_t( - "If you can't find the room you're looking for, " + - "ask for an invite or create a new room.", - )} + {_t("If you can't find the room you're looking for, ask for an invite or create a new room.")}
      - {_t("Continue")} + {_t("action|continue")}
      diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx index 7badebd7051..a25fc464292 100644 --- a/src/components/views/elements/AppTile.tsx +++ b/src/components/views/elements/AppTile.tsx @@ -526,7 +526,7 @@ export default class AppTile extends React.Component { return ( - + {name} {title ? titleSpacer : ""} @@ -620,7 +620,7 @@ export default class AppTile extends React.Component { const loadingElement = (
      - +
      ); @@ -785,7 +785,7 @@ export default class AppTile extends React.Component { {this.state.hasContextMenuOptions && ( = ({ children, getTextToCopy, border = true
      {children} { + private getTab(type: TabId, label: TranslationKey): Tab { const sources = this.state.sources .filter((source) => source.id.startsWith(type)) .map((source) => { @@ -154,8 +154,8 @@ export default class DesktopCapturerSourcePicker extends React.Component> = [ - this.getTab("screen", _t("Share entire screen")), - this.getTab("window", _t("Application window")), + this.getTab("screen", _td("Share entire screen")), + this.getTab("window", _td("Application window")), ]; return ( @@ -166,7 +166,7 @@ export default class DesktopCapturerSourcePicker extends React.Component { className={this.props.cancelButtonClass} disabled={this.props.disabled} > - {this.props.cancelButton || _t("Cancel")} + {this.props.cancelButton || _t("action|cancel")} ); } diff --git a/src/components/views/elements/Dropdown.tsx b/src/components/views/elements/Dropdown.tsx index 305cee51967..0a5786a1cb2 100644 --- a/src/components/views/elements/Dropdown.tsx +++ b/src/components/views/elements/Dropdown.tsx @@ -320,7 +320,7 @@ export default class Dropdown extends React.Component { if (!options?.length) { return [
    • - {_t("No results")} + {_t("common|no_results")}
    • , ]; } diff --git a/src/components/views/elements/EditableItemList.tsx b/src/components/views/elements/EditableItemList.tsx index 87576afcf89..de30fb3f91f 100644 --- a/src/components/views/elements/EditableItemList.tsx +++ b/src/components/views/elements/EditableItemList.tsx @@ -67,14 +67,14 @@ export class EditableItem extends React.Component { kind="primary_sm" className="mx_EditableItem_confirmBtn" > - {_t("Yes")} + {_t("action|yes")} - {_t("No")} + {_t("action|no")} ); @@ -82,7 +82,12 @@ export class EditableItem extends React.Component { return (
      -
      +
      {this.props.value}
      ); @@ -142,7 +147,7 @@ export default class EditableItemList

      extends React.PureComponent - {_t("Add")} + {_t("action|add")} ); diff --git a/src/components/views/elements/ErrorBoundary.tsx b/src/components/views/elements/ErrorBoundary.tsx index dca7d4562f6..4ecb8a17ee1 100644 --- a/src/components/views/elements/ErrorBoundary.tsx +++ b/src/components/views/elements/ErrorBoundary.tsx @@ -85,8 +85,7 @@ export default class ErrorBoundary extends React.PureComponent {

      {_t( - "Please create a new issue " + - "on GitHub so that we can investigate this bug.", + "Please create a new issue on GitHub so that we can investigate this bug.", {}, { newIssueLink: (sub) => { @@ -101,15 +100,10 @@ export default class ErrorBoundary extends React.PureComponent {

      {_t( - "If you've submitted a bug via GitHub, debug logs can help " + - "us track down the problem. ", + "If you've submitted a bug via GitHub, debug logs can help us track down the problem. ", )} {_t( - "Debug logs contain application " + - "usage data including your username, the IDs or aliases of " + - "the rooms you have visited, which UI elements you " + - "last interacted with, and the usernames of other users. " + - "They do not contain messages.", + "Debug logs contain application usage data including your username, the IDs or aliases of the rooms you have visited, which UI elements you last interacted with, and the usernames of other users. They do not contain messages.", )}

      diff --git a/src/components/views/elements/FacePile.tsx b/src/components/views/elements/FacePile.tsx index 6a03f7cbbe8..6c31614c06e 100644 --- a/src/components/views/elements/FacePile.tsx +++ b/src/components/views/elements/FacePile.tsx @@ -23,25 +23,19 @@ import TextWithTooltip from "./TextWithTooltip"; interface IProps extends HTMLAttributes { members: RoomMember[]; - faceSize: number; + size: string; overflow: boolean; tooltip?: ReactNode; children?: ReactNode; } -const FacePile: FC = ({ members, faceSize, overflow, tooltip, children, ...props }) => { +const FacePile: FC = ({ members, size, overflow, tooltip, children, ...props }) => { const faces = members.map( tooltip - ? (m) => + ? (m) => : (m) => ( - + ), ); diff --git a/src/components/views/elements/GenericEventListSummary.tsx b/src/components/views/elements/GenericEventListSummary.tsx index f9668c2ca46..79dda584169 100644 --- a/src/components/views/elements/GenericEventListSummary.tsx +++ b/src/components/views/elements/GenericEventListSummary.tsx @@ -103,7 +103,7 @@ const GenericEventListSummary: React.FC = ({ }), (member) => member.getMxcAvatarUrl(), ); - const avatars = uniqueMembers.map((m) => ); + const avatars = uniqueMembers.map((m) => ); body = (
      @@ -130,7 +130,7 @@ const GenericEventListSummary: React.FC = ({ onClick={toggleExpanded} aria-expanded={expanded} > - {expanded ? _t("collapse") : _t("expand")} + {expanded ? _t("action|collapse") : _t("action|expand")} {body} diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 7162d7ad804..d66014b7ed6 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -477,8 +477,7 @@ export default class ImageView extends React.Component { ); @@ -504,7 +503,7 @@ export default class ImageView extends React.Component { contextMenuButton = ( { const zoomOutButton = ( ); const zoomInButton = ( ); @@ -531,7 +530,7 @@ export default class ImageView extends React.Component { if (this.props.mxEvent?.getContent()) { title = (
      - {presentableTextForFile(this.props.mxEvent?.getContent(), _t("Image"), true)} + {presentableTextForFile(this.props.mxEvent?.getContent(), _t("common|image"), true)}
      ); } @@ -565,13 +564,13 @@ export default class ImageView extends React.Component { /> {contextMenuButton} {this.renderContextMenu()} diff --git a/src/components/views/elements/InlineSpinner.tsx b/src/components/views/elements/InlineSpinner.tsx index 144463c3245..866fc4b1d86 100644 --- a/src/components/views/elements/InlineSpinner.tsx +++ b/src/components/views/elements/InlineSpinner.tsx @@ -36,7 +36,7 @@ export default class InlineSpinner extends React.PureComponent {
      {this.props.children}
      diff --git a/src/components/views/elements/LanguageDropdown.tsx b/src/components/views/elements/LanguageDropdown.tsx index 5de1ffe7853..ff27b112834 100644 --- a/src/components/views/elements/LanguageDropdown.tsx +++ b/src/components/views/elements/LanguageDropdown.tsx @@ -16,6 +16,7 @@ limitations under the License. */ import React, { ReactElement } from "react"; +import classNames from "classnames"; import * as languageHandler from "../../../languageHandler"; import SettingsStore from "../../../settings/SettingsStore"; @@ -24,9 +25,10 @@ import Spinner from "./Spinner"; import Dropdown from "./Dropdown"; import { NonEmptyArray } from "../../../@types/common"; -type Languages = Awaited>; +type Languages = Awaited>; function languageMatchesSearchQuery(query: string, language: Languages[0]): boolean { + if (language.labelInTargetLanguage.toUpperCase().includes(query.toUpperCase())) return true; if (language.label.toUpperCase().includes(query.toUpperCase())) return true; if (language.value.toUpperCase() === query.toUpperCase()) return true; return false; @@ -56,23 +58,30 @@ export default class LanguageDropdown extends React.Component { public componentDidMount(): void { languageHandler - .getAllLanguagesFromJson() + .getAllLanguagesWithLabels() .then((langs) => { langs.sort(function (a, b) { - if (a.label < b.label) return -1; - if (a.label > b.label) return 1; + if (a.labelInTargetLanguage < b.labelInTargetLanguage) return -1; + if (a.labelInTargetLanguage > b.labelInTargetLanguage) return 1; return 0; }); this.setState({ langs }); }) .catch(() => { - this.setState({ langs: [{ value: "en", label: "English" }] }); + this.setState({ + langs: [ + { + value: "en", + label: "English", + labelInTargetLanguage: "English", + }, + ], + }); }); if (!this.props.value) { - // If no value is given, we start with the first - // country selected, but our parent component - // doesn't know this, therefore we do this. + // If no value is given, we start with the first country selected, + // but our parent component doesn't know this, therefore we do this. const language = languageHandler.getUserLanguage(); this.props.onOptionChange(language); } @@ -89,7 +98,7 @@ export default class LanguageDropdown extends React.Component { return ; } - let displayedLanguages: Awaited>; + let displayedLanguages: Awaited>; if (this.state.searchQuery) { displayedLanguages = this.state.langs.filter((lang) => { return languageMatchesSearchQuery(this.state.searchQuery, lang); @@ -99,7 +108,7 @@ export default class LanguageDropdown extends React.Component { } const options = displayedLanguages.map((language) => { - return
      {language.label}
      ; + return
      {language.labelInTargetLanguage}
      ; }) as NonEmptyArray; // default value here too, otherwise we need to handle null / undefined @@ -116,7 +125,7 @@ export default class LanguageDropdown extends React.Component { return ( = ({ title, description, ...rest }) => Modal.createDialog(InfoDialog, { title, description, - button: _t("Got it"), + button: _t("action|got_it"), hasCloseButton: true, }); }; return ( - {_t("Learn more")} + {_t("action|learn_more")} ); }; diff --git a/src/components/views/elements/MiniAvatarUploader.tsx b/src/components/views/elements/MiniAvatarUploader.tsx index dd02d3238d6..35414c87824 100644 --- a/src/components/views/elements/MiniAvatarUploader.tsx +++ b/src/components/views/elements/MiniAvatarUploader.tsx @@ -26,7 +26,7 @@ import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; import AccessibleButton from "./AccessibleButton"; import Spinner from "./Spinner"; -export const AVATAR_SIZE = 52; +export const AVATAR_SIZE = "52px"; interface IProps { hasAvatar: boolean; diff --git a/src/components/views/elements/Pill.tsx b/src/components/views/elements/Pill.tsx index 7e96a21b6f0..5770145b788 100644 --- a/src/components/views/elements/Pill.tsx +++ b/src/components/views/elements/Pill.tsx @@ -44,7 +44,7 @@ export const pillRoomNotifLen = (): number => { return "@room".length; }; -const linkIcon = ; +const linkIcon = ; const PillRoomAvatar: React.FC<{ shouldShowPillAvatar: boolean; @@ -55,7 +55,7 @@ const PillRoomAvatar: React.FC<{ } if (room) { - return
      diff --git a/src/components/views/location/Marker.tsx b/src/components/views/location/Marker.tsx index c38f43b0d37..5045d87a03b 100644 --- a/src/components/views/location/Marker.tsx +++ b/src/components/views/location/Marker.tsx @@ -78,8 +78,7 @@ const Marker = React.forwardRef(({ id, roomMember, useMem {roomMember ? ( = ({ onBack, onCancel, displayBack }) @@ -44,7 +44,7 @@ const ShareDialogButtons: React.FC = ({ onBack, onCancel, displayBack }) diff --git a/src/components/views/location/ShareType.tsx b/src/components/views/location/ShareType.tsx index d17bcdc8ce3..4e99c286227 100644 --- a/src/components/views/location/ShareType.tsx +++ b/src/components/views/location/ShareType.tsx @@ -31,8 +31,8 @@ const UserAvatar: React.FC = () => { const userId = matrixClient.getSafeUserId(); const displayName = OwnProfileStore.instance.displayName ?? undefined; // 40 - 2px border - const avatarSize = 36; - const avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize) ?? undefined; + const avatarSize = "36px"; + const avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(parseInt(avatarSize, 10)) ?? undefined; return (
      @@ -40,9 +40,7 @@ const UserAvatar: React.FC = () => { idName={userId} name={displayName} url={avatarUrl} - width={avatarSize} - height={avatarSize} - resizeMethod="crop" + size={avatarSize} className="mx_UserMenu_userAvatar_BaseAvatar" />
      diff --git a/src/components/views/location/ZoomButtons.tsx b/src/components/views/location/ZoomButtons.tsx index 461cdad3cdb..970835cffd4 100644 --- a/src/components/views/location/ZoomButtons.tsx +++ b/src/components/views/location/ZoomButtons.tsx @@ -40,7 +40,7 @@ const ZoomButtons: React.FC = ({ map }) => { @@ -48,7 +48,7 @@ const ZoomButtons: React.FC = ({ map }) => { diff --git a/src/components/views/location/shareLocation.ts b/src/components/views/location/shareLocation.ts index a0c7b6febc3..dd3a0ce9d0a 100644 --- a/src/components/views/location/shareLocation.ts +++ b/src/components/views/location/shareLocation.ts @@ -14,10 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixClient, IContent, IEventRelation, MatrixError, THREAD_RELATION_TYPE } from "matrix-js-sdk/src/matrix"; -import { makeLocationContent, makeBeaconInfoContent } from "matrix-js-sdk/src/content-helpers"; +import { + MatrixClient, + IContent, + IEventRelation, + MatrixError, + THREAD_RELATION_TYPE, + ContentHelpers, + LocationAssetType, +} from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; -import { LocationAssetType } from "matrix-js-sdk/src/@types/location"; import { _t } from "../../../languageHandler"; import Modal from "../../../Modal"; @@ -57,7 +63,7 @@ const getPermissionsErrorParams = ( const modalParams = { title: _t("You don't have permission to share locations"), description: _t("You need to have the right permissions in order to share locations in this room."), - button: _t("OK"), + button: _t("action|ok"), hasCancelButton: false, onFinished: () => {}, // NOOP }; @@ -80,8 +86,8 @@ const getDefaultErrorParams = ( description: _t("%(brand)s could not send your location. Please try again later.", { brand: SdkConfig.get().brand, }), - button: _t("Try again"), - cancelButton: _t("Cancel"), + button: _t("action|try_again"), + cancelButton: _t("action|cancel"), onFinished: (tryAgain: boolean) => { if (tryAgain) { openMenu(); @@ -109,7 +115,7 @@ export const shareLiveLocation = try { await OwnBeaconStore.instance.createLiveBeacon( roomId, - makeBeaconInfoContent( + ContentHelpers.makeBeaconInfoContent( timeout ?? DEFAULT_LIVE_DURATION, true /* isLive */, description, @@ -134,7 +140,13 @@ export const shareLocation = try { const threadId = (relation?.rel_type === THREAD_RELATION_TYPE.name && relation?.event_id) || null; const assetType = shareType === LocationShareType.Pin ? LocationAssetType.Pin : LocationAssetType.Self; - const content = makeLocationContent(undefined, uri, timestamp, undefined, assetType) as IContent; + const content = ContentHelpers.makeLocationContent( + undefined, + uri, + timestamp, + undefined, + assetType, + ) as IContent; await doMaybeLocalRoomAction( roomId, (actualRoomId: string) => client.sendMessage(actualRoomId, threadId, content), diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index 067bffce30f..575f19c3a24 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -62,8 +62,7 @@ const ActiveCallEvent = forwardRef( member={mxEvent.sender} fallbackUserId={mxEvent.getSender()} viewUserOnClick - width={24} - height={24} + size="24px" />
      @@ -76,7 +75,7 @@ const ActiveCallEvent = forwardRef( active={false} participantCount={participatingMembers.length} /> - +
      {call && } (({ mxE const [buttonText, buttonKind, onButtonClick] = useMemo(() => { switch (connectionState) { case ConnectionState.Disconnected: - return [_t("Join"), "primary", connect]; + return [_t("action|join"), "primary", connect]; case ConnectionState.Connecting: - return [_t("Join"), "primary", null]; + return [_t("action|join"), "primary", null]; case ConnectionState.Connected: - return [_t("Leave"), "danger", disconnect]; + return [_t("action|leave"), "danger", disconnect]; case ConnectionState.Disconnecting: - return [_t("Leave"), "danger", null]; + return [_t("action|leave"), "danger", null]; } }, [connectionState, connect, disconnect]); @@ -189,7 +188,7 @@ export const CallEvent = forwardRef(({ mxEvent }, ref) => { mxEvent={mxEvent} call={null} participatingMembers={[]} - buttonText={_t("Join")} + buttonText={_t("action|join")} buttonKind="primary" onButtonClick={null} /> diff --git a/src/components/views/messages/DateSeparator.tsx b/src/components/views/messages/DateSeparator.tsx index 0e6815ed36a..4175b7d0218 100644 --- a/src/components/views/messages/DateSeparator.tsx +++ b/src/components/views/messages/DateSeparator.tsx @@ -19,8 +19,8 @@ import React from "react"; import { Direction, ConnectionError, MatrixError, HTTPError } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; -import { _t } from "../../../languageHandler"; -import { formatFullDateNoDay, formatFullDateNoTime } from "../../../DateUtils"; +import { _t, getUserLanguage } from "../../../languageHandler"; +import { formatFullDateNoDay, formatFullDateNoTime, getDaysArray } from "../../../DateUtils"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import dispatcher from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; @@ -40,10 +40,6 @@ import JumpToDatePicker from "./JumpToDatePicker"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { SdkContextClass } from "../../../contexts/SDKContext"; -function getDaysArray(): string[] { - return [_t("Sunday"), _t("Monday"), _t("Tuesday"), _t("Wednesday"), _t("Thursday"), _t("Friday"), _t("Saturday")]; -} - interface IProps { roomId: string; ts: number; @@ -105,15 +101,16 @@ export default class DateSeparator extends React.Component { const today = new Date(); const yesterday = new Date(); - const days = getDaysArray(); + const days = getDaysArray("long"); yesterday.setDate(today.getDate() - 1); + const relativeTimeFormat = new Intl.RelativeTimeFormat(getUserLanguage(), { style: "long", numeric: "auto" }); if (date.toDateString() === today.toDateString()) { - return _t("Today"); + return relativeTimeFormat.format(0, "day"); // Today } else if (date.toDateString() === yesterday.toDateString()) { - return _t("Yesterday"); + return relativeTimeFormat.format(-1, "day"); // Yesterday } else if (today.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) { - return days[date.getDay()]; + return days[date.getDay()]; // Sunday-Saturday } else { return formatFullDateNoTime(date); } @@ -170,16 +167,12 @@ export default class DateSeparator extends React.Component { let submitDebugLogsContent: JSX.Element = <>; if (err instanceof ConnectionError) { friendlyErrorMessage = _t( - "A network error occurred while trying to find and jump to the given date. " + - "Your homeserver might be down or there was just a temporary problem with " + - "your internet connection. Please try again. If this continues, please " + - "contact your homeserver administrator.", + "A network error occurred while trying to find and jump to the given date. Your homeserver might be down or there was just a temporary problem with your internet connection. Please try again. If this continues, please contact your homeserver administrator.", ); } else if (err instanceof MatrixError) { if (err?.errcode === "M_NOT_FOUND") { friendlyErrorMessage = _t( - "We were unable to find an event looking forwards from %(dateString)s. " + - "Try choosing an earlier date.", + "We were unable to find an event looking forwards from %(dateString)s. Try choosing an earlier date.", { dateString: formatFullDateNoDay(new Date(unixTimestamp)) }, ); } else { @@ -195,8 +188,7 @@ export default class DateSeparator extends React.Component { submitDebugLogsContent = (

      {_t( - "Please submit debug logs to help us " + - "track down the problem.", + "Please submit debug logs to help us track down the problem.", {}, { debugLogsLink: (sub) => ( diff --git a/src/components/views/messages/DisambiguatedProfile.tsx b/src/components/views/messages/DisambiguatedProfile.tsx index 20788b71cbf..6810723c6e3 100644 --- a/src/components/views/messages/DisambiguatedProfile.tsx +++ b/src/components/views/messages/DisambiguatedProfile.tsx @@ -40,7 +40,7 @@ export default class DisambiguatedProfile extends React.Component { let colorClass: string | undefined; if (colored) { - colorClass = getUserNameColorClass(fallbackName); + colorClass = getUserNameColorClass(mxid ?? ""); } let mxidElement; diff --git a/src/components/views/messages/DownloadActionButton.tsx b/src/components/views/messages/DownloadActionButton.tsx index 8e974c1e15e..5123ed63f2a 100644 --- a/src/components/views/messages/DownloadActionButton.tsx +++ b/src/components/views/messages/DownloadActionButton.tsx @@ -22,7 +22,7 @@ import { Icon as DownloadIcon } from "../../../../res/img/download.svg"; import { MediaEventHelper } from "../../../utils/MediaEventHelper"; import { RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex"; import Spinner from "../elements/Spinner"; -import { _t, _td } from "../../../languageHandler"; +import { _t, _td, TranslationKey } from "../../../languageHandler"; import { FileDownloader } from "../../../utils/FileDownloader"; interface IProps { @@ -37,7 +37,7 @@ interface IProps { interface IState { loading: boolean; blob?: Blob; - tooltip: string; + tooltip: TranslationKey; } export default class DownloadActionButton extends React.PureComponent { @@ -95,7 +95,7 @@ export default class DownloadActionButton extends React.PureComponent diff --git a/src/components/views/messages/EditHistoryMessage.tsx b/src/components/views/messages/EditHistoryMessage.tsx index f49db88d7c1..49e0f1f7de4 100644 --- a/src/components/views/messages/EditHistoryMessage.tsx +++ b/src/components/views/messages/EditHistoryMessage.tsx @@ -137,13 +137,13 @@ export default class EditHistoryMessage extends React.PureComponent{_t("Remove")}; + redactButton = {_t("action|remove")}; } let viewSourceButton: JSX.Element | undefined; if (SettingsStore.getValue("developerMode")) { viewSourceButton = ( - {_t("View Source")} + {_t("action|view_source")} ); } diff --git a/src/components/views/messages/EncryptionEvent.tsx b/src/components/views/messages/EncryptionEvent.tsx index bcd6136ec90..b9566f65306 100644 --- a/src/components/views/messages/EncryptionEvent.tsx +++ b/src/components/views/messages/EncryptionEvent.tsx @@ -53,23 +53,21 @@ const EncryptionEvent = forwardRef(({ mxEvent, timestamp } else if (dmPartner) { const displayName = room?.getMember(dmPartner)?.rawDisplayName || dmPartner; subtitle = _t( - "Messages here are end-to-end encrypted. " + - "Verify %(displayName)s in their profile - tap on their profile picture.", + "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their profile picture.", { displayName }, ); } else if (room && isLocalRoom(room)) { subtitle = _t("Messages in this chat will be end-to-end encrypted."); } else { subtitle = _t( - "Messages in this room are end-to-end encrypted. " + - "When people join, you can verify them in their profile, just tap on their profile picture.", + "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their profile picture.", ); } return ( @@ -80,7 +78,7 @@ const EncryptionEvent = forwardRef(({ mxEvent, timestamp return ( diff --git a/src/components/views/messages/LegacyCallEvent.tsx b/src/components/views/messages/LegacyCallEvent.tsx index 9516d035779..c56d424d950 100644 --- a/src/components/views/messages/LegacyCallEvent.tsx +++ b/src/components/views/messages/LegacyCallEvent.tsx @@ -137,14 +137,14 @@ export default class LegacyCallEvent extends React.PureComponent onClick={this.props.callEventGrouper.rejectCall} kind="danger" > - {_t("Decline")} + {_t("action|decline")} - {_t("Accept")} + {_t("action|accept")} {this.props.timestamp}

      @@ -234,7 +234,7 @@ export default class LegacyCallEvent extends React.PureComponent kind={InfoTooltipKind.Warning} /> {_t("Connection failed")} - {this.renderCallBackButton(_t("Retry"))} + {this.renderCallBackButton(_t("action|retry"))} {this.props.timestamp}
      ); @@ -290,7 +290,7 @@ export default class LegacyCallEvent extends React.PureComponent
      {silenceIcon}
      - +
      {sender}
      diff --git a/src/components/views/messages/MBeaconBody.tsx b/src/components/views/messages/MBeaconBody.tsx index c8e7f3b17e3..eeed20d5673 100644 --- a/src/components/views/messages/MBeaconBody.tsx +++ b/src/components/views/messages/MBeaconBody.tsx @@ -23,10 +23,10 @@ import { MatrixClient, RelationType, IRedactOpts, + ContentHelpers, + M_BEACON, } from "matrix-js-sdk/src/matrix"; -import { BeaconLocationState } from "matrix-js-sdk/src/content-helpers"; import { randomString } from "matrix-js-sdk/src/randomstring"; -import { M_BEACON } from "matrix-js-sdk/src/@types/beacon"; import classNames from "classnames"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; @@ -51,7 +51,7 @@ const useBeaconState = ( ): { beacon?: Beacon; description?: string; - latestLocationState?: BeaconLocationState; + latestLocationState?: ContentHelpers.BeaconLocationState; isLive?: boolean; waitingToStart?: boolean; } => { diff --git a/src/components/views/messages/MFileBody.tsx b/src/components/views/messages/MFileBody.tsx index 1fc0004fa1c..f430b0f9bdd 100644 --- a/src/components/views/messages/MFileBody.tsx +++ b/src/components/views/messages/MFileBody.tsx @@ -133,7 +133,7 @@ export default class MFileBody extends React.Component { } private get fileName(): string { - return this.content.body && this.content.body.length > 0 ? this.content.body : _t("Attachment"); + return this.content.body && this.content.body.length > 0 ? this.content.body : _t("common|attachment"); } private get linkText(): string { @@ -173,7 +173,7 @@ export default class MFileBody extends React.Component { } catch (err) { logger.warn("Unable to decrypt attachment: ", err); Modal.createDialog(ErrorDialog, { - title: _t("Error"), + title: _t("common|error"), description: _t("Error decrypting attachment"), }); } @@ -205,9 +205,9 @@ export default class MFileBody extends React.Component { placeholder = ( - + - {presentableTextForFile(this.content, _t("Attachment"), true, true)} + {presentableTextForFile(this.content, _t("common|attachment"), true, true)} @@ -284,7 +284,7 @@ export default class MFileBody extends React.Component { */}