diff --git a/.eslintrc.js b/.eslintrc.js index e95f4834e9..f168a87a06 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -117,10 +117,6 @@ module.exports = { "!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", - "!matrix-js-sdk/src/crypto/keybackup", - "!matrix-js-sdk/src/crypto/deviceinfo", - "!matrix-js-sdk/src/crypto/dehydration", "!matrix-js-sdk/src/oidc", "!matrix-js-sdk/src/oidc/discovery", "!matrix-js-sdk/src/oidc/authorize", diff --git a/.github/workflows/end-to-end-tests.yaml b/.github/workflows/end-to-end-tests.yaml index 0abc35d85d..1784dafe0e 100644 --- a/.github/workflows/end-to-end-tests.yaml +++ b/.github/workflows/end-to-end-tests.yaml @@ -69,7 +69,6 @@ jobs: VERSION: "${{ steps.layered_build.outputs.VERSION }}" run: | yarn build - echo $VERSION > webapp/version - name: Upload Artifact uses: actions/upload-artifact@v4 diff --git a/.stylelintrc.js b/.stylelintrc.js index dc8ae6376b..fa36402ff1 100644 --- a/.stylelintrc.js +++ b/.stylelintrc.js @@ -1,7 +1,7 @@ module.exports = { extends: ["stylelint-config-standard"], customSyntax: "postcss-scss", - plugins: ["stylelint-scss"], + plugins: ["stylelint-scss", "stylelint-value-no-unknown-custom-properties"], rules: { "comment-empty-line-before": null, "declaration-empty-line-before": null, @@ -46,5 +46,33 @@ module.exports = { "number-max-precision": null, "no-invalid-double-slash-comments": true, "media-feature-range-notation": null, + "csstools/value-no-unknown-custom-properties": [ + true, + { + importFrom: [ + { from: "res/css/_common.pcss", type: "css" }, + { from: "res/themes/light/css/_light.pcss", type: "css" }, + // Right now our styles share vars all over the place, this is not ideal but acceptable for now + { from: "res/css/views/rooms/_EventTile.pcss", type: "css" }, + { from: "res/css/views/rooms/_IRCLayout.pcss", type: "css" }, + { from: "res/css/views/rooms/_EventBubbleTile.pcss", type: "css" }, + { from: "res/css/views/rooms/_ReadReceiptGroup.pcss", type: "css" }, + { from: "res/css/views/rooms/_EditMessageComposer.pcss", type: "css" }, + { from: "res/css/views/right_panel/_BaseCard.pcss", type: "css" }, + { from: "res/css/views/messages/_MessageTimestamp.pcss", type: "css" }, + { from: "res/css/views/messages/_EventTileBubble.pcss", type: "css" }, + { from: "res/css/views/messages/_MessageActionBar.pcss", type: "css" }, + { from: "res/css/views/voip/LegacyCallView/_LegacyCallViewButtons.pcss", type: "css" }, + { from: "res/css/views/elements/_ToggleSwitch.pcss", type: "css" }, + { from: "res/css/views/settings/tabs/_SettingsTab.pcss", type: "css" }, + { from: "res/css/structures/_RoomView.pcss", type: "css" }, + // Compound vars + "node_modules/@vector-im/compound-design-tokens/assets/web/css/cpd-common-base.css", + "node_modules/@vector-im/compound-design-tokens/assets/web/css/cpd-common-semantic.css", + "node_modules/@vector-im/compound-design-tokens/assets/web/css/cpd-theme-light-base-mq.css", + "node_modules/@vector-im/compound-design-tokens/assets/web/css/cpd-theme-light-semantic-mq.css", + ], + }, + ], }, }; diff --git a/package.json b/package.json index 1bd9dea0ac..e01c670965 100644 --- a/package.json +++ b/package.json @@ -85,8 +85,8 @@ "@matrix-org/react-sdk-module-api": "^2.4.0", "@matrix-org/spec": "^1.7.0", "@sentry/browser": "^8.0.0", - "@vector-im/compound-design-tokens": "^1.8.0", - "@vector-im/compound-web": "^7.1.0", + "@vector-im/compound-design-tokens": "^2.0.1", + "@vector-im/compound-web": "^7.3.0", "@vector-im/matrix-wysiwyg": "2.37.13", "@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4", @@ -276,6 +276,7 @@ "stylelint": "^16.1.0", "stylelint-config-standard": "^36.0.0", "stylelint-scss": "^6.0.0", + "stylelint-value-no-unknown-custom-properties": "^6.0.1", "terser-webpack-plugin": "^5.3.9", "ts-node": "^10.9.1", "ts-prune": "^0.10.3", @@ -286,6 +287,7 @@ "webpack-bundle-analyzer": "^4.8.0", "webpack-cli": "^5.0.0", "webpack-dev-server": "^5.0.0", + "webpack-version-file-plugin": "^0.5.0", "yaml": "^2.3.3" }, "@casualbot/jest-sonar-reporter": { diff --git a/playwright/plugins/homeserver/synapse/index.ts b/playwright/plugins/homeserver/synapse/index.ts index bc8403b350..ea95a6bbdc 100644 --- a/playwright/plugins/homeserver/synapse/index.ts +++ b/playwright/plugins/homeserver/synapse/index.ts @@ -20,7 +20,7 @@ import { randB64Bytes } from "../../utils/rand"; // Docker tag to use for synapse docker image. // We target a specific digest as every now and then a Synapse update will break our CI. // This digest is updated by the playwright-image-updates.yaml workflow periodically. -const DOCKER_TAG = "develop@sha256:27e36370a0422d275b54d0292c8bf87e925dc004d0fe5b10dbee7ea4ffd27289"; +const DOCKER_TAG = "develop@sha256:d947f40999b060ad4856c0af741b8619fa131430a29922606e374fdba532082b"; async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise> { const templateDir = path.join(__dirname, "templates", opts.template); diff --git a/playwright/snapshots/polls/polls.spec.ts/Polls-Timeline-tile-no-votes-linux.png b/playwright/snapshots/polls/polls.spec.ts/Polls-Timeline-tile-no-votes-linux.png index 7e1195c82d..1ade373ba8 100644 Binary files a/playwright/snapshots/polls/polls.spec.ts/Polls-Timeline-tile-no-votes-linux.png and b/playwright/snapshots/polls/polls.spec.ts/Polls-Timeline-tile-no-votes-linux.png differ diff --git a/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-no-results-linux.png b/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-no-results-linux.png index 458c6469f1..82779d4cc9 100644 Binary files a/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-no-results-linux.png and b/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-no-results-linux.png differ diff --git a/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-one-result-linux.png b/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-one-result-linux.png index 7bfa3229ed..666dbe689a 100644 Binary files a/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-one-result-linux.png and b/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-one-result-linux.png differ diff --git a/playwright/snapshots/user-menu/user-menu.spec.ts/user-menu-linux.png b/playwright/snapshots/user-menu/user-menu.spec.ts/user-menu-linux.png index 7e04b4404d..edc0ad17ad 100644 Binary files a/playwright/snapshots/user-menu/user-menu.spec.ts/user-menu-linux.png and b/playwright/snapshots/user-menu/user-menu.spec.ts/user-menu-linux.png differ diff --git a/res/css/components/views/elements/_AppPermission.pcss b/res/css/components/views/elements/_AppPermission.pcss index 25db241f73..0891d25221 100644 --- a/res/css/components/views/elements/_AppPermission.pcss +++ b/res/css/components/views/elements/_AppPermission.pcss @@ -11,7 +11,8 @@ Please see LICENSE files in the repository root for full details. font-size: $font-12px; width: 100%; /* make mx_AppPermission fill width of mx_AppTileBody so that scroll bar appears on the edge */ overflow-y: scroll; - .mx_AppPermission_bolder { + .mx_AppPermission_bolder, + .mx_AppPermission_content_bolder { font-weight: var(--cpd-font-weight-semibold); } .mx_AppPermission_content { @@ -21,10 +22,6 @@ Please see LICENSE files in the repository root for full details. margin-block: 12px; } - .mx_AppPermission_content_bolder { - font-weight: var(--font-semi-bold); - } - .mx_TextWithTooltip_target--helpIcon { display: inline-block; height: $font-14px; /* align with characters on the same line */ diff --git a/res/css/structures/_RightPanel.pcss b/res/css/structures/_RightPanel.pcss index 57196985a9..0f796dbc96 100644 --- a/res/css/structures/_RightPanel.pcss +++ b/res/css/structures/_RightPanel.pcss @@ -27,7 +27,7 @@ Please see LICENSE files in the repository root for full details. /** Fixme - factor this out with the main header **/ .mx_RightPanel_threadsButton::before { - mask-image: url("$(res)/img/element-icons/room/thread.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/threads-solid.svg"); } .mx_RightPanel_notifsButton::before { @@ -36,7 +36,7 @@ Please see LICENSE files in the repository root for full details. } .mx_RightPanel_roomSummaryButton::before { - mask-image: url("$(res)/img/element-icons/room/room-summary.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/info-solid.svg"); mask-position: center; } diff --git a/res/css/structures/_RoomView.pcss b/res/css/structures/_RoomView.pcss index eaa02cd2d2..65ea555ce1 100644 --- a/res/css/structures/_RoomView.pcss +++ b/res/css/structures/_RoomView.pcss @@ -207,62 +207,3 @@ Please see LICENSE files in the repository root for full details. min-height: 42px; } } - -@keyframes mx_Indicator_pulse { - 0% { - transform: scale(0.95); - } - - 70% { - transform: scale(1); - } - - 100% { - transform: scale(0.95); - } -} - -@keyframes mx_Indicator_pulse_shadow { - 0% { - opacity: 0.7; - } - - 70% { - transform: scale(2.2); - opacity: 0; - } - - 100% { - opacity: 0; - } -} - -.mx_Indicator { - position: absolute; - right: -3px; - top: -3px; - width: var(--RoomHeader-indicator-dot-size); - height: var(--RoomHeader-indicator-dot-size); - border-radius: 50%; - transform: scale(1); - background: var(--RoomHeader-indicator-pulseColor); - box-shadow: 0 0 0 0 var(--RoomHeader-indicator-pulseColor); - animation: mx_Indicator_pulse 2s infinite; - animation-iteration-count: 1; - - &::after { - content: ""; - position: absolute; - width: inherit; - height: inherit; - top: 0; - left: 0; - transform: scale(1); - transform-origin: center center; - animation-name: mx_Indicator_pulse_shadow; - animation-duration: inherit; - animation-iteration-count: inherit; - border-radius: 50%; - background: inherit; - } -} diff --git a/res/css/structures/_SpacePanel.pcss b/res/css/structures/_SpacePanel.pcss index fdaa930686..7ea717554b 100644 --- a/res/css/structures/_SpacePanel.pcss +++ b/res/css/structures/_SpacePanel.pcss @@ -211,11 +211,11 @@ Please see LICENSE files in the repository root for full details. } &.mx_SpaceButton_favourites .mx_SpaceButton_icon::before { - mask-image: url("$(res)/img/element-icons/roomlist/favorite.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/favourite-solid.svg"); } &.mx_SpaceButton_people .mx_SpaceButton_icon::before { - mask-image: url("$(res)/img/element-icons/room/members.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/user-profile-solid.svg"); } &.mx_SpaceButton_orphans .mx_SpaceButton_icon::before { @@ -426,11 +426,11 @@ Please see LICENSE files in the repository root for full details. } .mx_SpacePanel_iconLeave::before { - mask-image: url("$(res)/img/element-icons/leave.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/leave.svg"); } .mx_SpacePanel_iconMembers::before { - mask-image: url("$(res)/img/element-icons/room/members.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/user-profile-solid.svg"); } .mx_SpacePanel_iconPlus::before { diff --git a/res/css/structures/_SpaceRoomView.pcss b/res/css/structures/_SpaceRoomView.pcss index 7e55743200..d756d51d65 100644 --- a/res/css/structures/_SpaceRoomView.pcss +++ b/res/css/structures/_SpaceRoomView.pcss @@ -39,7 +39,7 @@ Please see LICENSE files in the repository root for full details. } &:hover { - border-color: var(--cpd-color-bg-interactive-primary-rest); + border-color: var(--cpd-color-bg-action-primary-rest); &::before { background-color: var(--cpd-color-icon-primary); @@ -248,7 +248,7 @@ Please see LICENSE files in the repository root for full details. } .mx_SpaceRoomView_privateScope_justMeButton::before { - mask-image: url("$(res)/img/element-icons/room/members.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/user-profile-solid.svg"); } .mx_SpaceRoomView_privateScope_meAndMyTeammatesButton::before { diff --git a/res/css/structures/_UserMenu.pcss b/res/css/structures/_UserMenu.pcss index 7cf3845027..741a4e90dc 100644 --- a/res/css/structures/_UserMenu.pcss +++ b/res/css/structures/_UserMenu.pcss @@ -197,7 +197,7 @@ Please see LICENSE files in the repository root for full details. } .mx_UserMenu_iconSignOut::before { - mask-image: url("$(res)/img/element-icons/leave.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/leave.svg"); } .mx_UserMenu_iconQr::before { diff --git a/res/css/views/audio_messages/_PlaybackContainer.pcss b/res/css/views/audio_messages/_PlaybackContainer.pcss index e02533037b..f1dc1d1ec8 100644 --- a/res/css/views/audio_messages/_PlaybackContainer.pcss +++ b/res/css/views/audio_messages/_PlaybackContainer.pcss @@ -28,10 +28,11 @@ Please see LICENSE files in the repository root for full details. /* Waveforms are present in live recording only */ .mx_Waveform { + /* default, overridden in JS */ + --barHeight: 1; .mx_Waveform_bar { background-color: $quaternary-content; height: 100%; - /* Variable set by a JS component */ transform: scaleY(max(0.05, var(--barHeight))); &.mx_Waveform_bar_100pct { diff --git a/res/css/views/audio_messages/_SeekBar.pcss b/res/css/views/audio_messages/_SeekBar.pcss index 47cce4b47a..fb781811f1 100644 --- a/res/css/views/audio_messages/_SeekBar.pcss +++ b/res/css/views/audio_messages/_SeekBar.pcss @@ -12,6 +12,9 @@ Please see LICENSE files in the repository root for full details. /* * https://css-tricks.com/styling-cross-browser-compatible-range-inputs-css/ */ .mx_SeekBar { + /* default, overridden in JS */ + --fillTo: 1; + /* Dev note: we deliberately do not have the -ms-track (and friends) selectors because we don't */ /* need to support IE. */ diff --git a/res/css/views/context_menus/_MessageContextMenu.pcss b/res/css/views/context_menus/_MessageContextMenu.pcss index e06782ebe9..a73dab9982 100644 --- a/res/css/views/context_menus/_MessageContextMenu.pcss +++ b/res/css/views/context_menus/_MessageContextMenu.pcss @@ -33,7 +33,7 @@ Please see LICENSE files in the repository root for full details. } .mx_MessageContextMenu_iconLink::before { - mask-image: url("$(res)/img/element-icons/link.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/link.svg"); } .mx_MessageContextMenu_iconPermalink::before { @@ -53,7 +53,7 @@ Please see LICENSE files in the repository root for full details. } .mx_MessageContextMenu_iconForward::before { - mask-image: url("$(res)/img/element-icons/message/fwd.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/forward.svg"); } .mx_MessageContextMenu_iconRedact::before { @@ -96,7 +96,7 @@ Please see LICENSE files in the repository root for full details. } .mx_MessageContextMenu_iconReplyInThread::before { - mask-image: url("$(res)/img/element-icons/message/thread.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/threads.svg"); } .mx_MessageContextMenu_iconReact::before { diff --git a/res/css/views/context_menus/_RoomGeneralContextMenu.pcss b/res/css/views/context_menus/_RoomGeneralContextMenu.pcss index 90602538f0..0eb51420bb 100644 --- a/res/css/views/context_menus/_RoomGeneralContextMenu.pcss +++ b/res/css/views/context_menus/_RoomGeneralContextMenu.pcss @@ -1,5 +1,5 @@ .mx_RoomGeneralContextMenu_iconStar::before { - mask-image: url("$(res)/img/element-icons/roomlist/favorite.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/favourite-solid.svg"); } .mx_RoomGeneralContextMenu_iconArrowDown::before { @@ -31,7 +31,7 @@ } .mx_RoomGeneralContextMenu_iconPeople::before { - mask-image: url("$(res)/img/element-icons/room/members.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/user-profile-solid.svg"); } .mx_RoomGeneralContextMenu_iconFiles::before { @@ -43,7 +43,7 @@ } .mx_RoomGeneralContextMenu_iconWidgets::before { - mask-image: url("$(res)/img/element-icons/room/apps.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/extensions-solid.svg"); } .mx_RoomGeneralContextMenu_iconSettings::before { @@ -51,7 +51,7 @@ } .mx_RoomGeneralContextMenu_iconExport::before { - mask-image: url("$(res)/img/element-icons/export.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/export-archive.svg"); } .mx_RoomGeneralContextMenu_iconDeveloperTools::before { @@ -59,7 +59,7 @@ } .mx_RoomGeneralContextMenu_iconCopyLink::before { - mask-image: url("$(res)/img/element-icons/link.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/link.svg"); } .mx_RoomGeneralContextMenu_iconInvite::before { @@ -67,5 +67,5 @@ } .mx_RoomGeneralContextMenu_iconSignOut::before { - mask-image: url("$(res)/img/element-icons/leave.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/leave.svg"); } diff --git a/res/css/views/dialogs/_ConfirmSpaceUserActionDialog.pcss b/res/css/views/dialogs/_ConfirmSpaceUserActionDialog.pcss index 7c1828183a..3b91eddc8b 100644 --- a/res/css/views/dialogs/_ConfirmSpaceUserActionDialog.pcss +++ b/res/css/views/dialogs/_ConfirmSpaceUserActionDialog.pcss @@ -51,7 +51,7 @@ Please see LICENSE files in the repository root for full details. background-color: $secondary-content; mask-repeat: no-repeat; mask-size: contain; - mask-image: url("$(res)/img/element-icons/room/room-summary.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/info-solid.svg"); mask-position: center; } } diff --git a/res/css/views/dialogs/_LeaveSpaceDialog.pcss b/res/css/views/dialogs/_LeaveSpaceDialog.pcss index b332942f75..b3e3878276 100644 --- a/res/css/views/dialogs/_LeaveSpaceDialog.pcss +++ b/res/css/views/dialogs/_LeaveSpaceDialog.pcss @@ -45,7 +45,7 @@ Please see LICENSE files in the repository root for full details. background-color: $secondary-content; mask-repeat: no-repeat; mask-size: contain; - mask-image: url("$(res)/img/element-icons/room/room-summary.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/info-solid.svg"); mask-position: center; } } diff --git a/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.pcss b/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.pcss index f6635b9791..a6b9fe0304 100644 --- a/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.pcss +++ b/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.pcss @@ -108,7 +108,7 @@ Please see LICENSE files in the repository root for full details. background-color: $secondary-content; mask-repeat: no-repeat; mask-size: contain; - mask-image: url("$(res)/img/element-icons/room/room-summary.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/info-solid.svg"); mask-position: center; } } diff --git a/res/css/views/dialogs/_SpotlightDialog.pcss b/res/css/views/dialogs/_SpotlightDialog.pcss index eff7bd0e12..c4f94bfde9 100644 --- a/res/css/views/dialogs/_SpotlightDialog.pcss +++ b/res/css/views/dialogs/_SpotlightDialog.pcss @@ -93,7 +93,7 @@ Please see LICENSE files in the repository root for full details. } &.mx_SpotlightDialog_filterPeople::before { - mask-image: url("$(res)/img/element-icons/room/members.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/user-profile-solid.svg"); } &.mx_SpotlightDialog_filterPublicRooms::before { @@ -400,7 +400,7 @@ Please see LICENSE files in the repository root for full details. } .mx_SpotlightDialog_inviteLink .mx_AccessibleButton::before { - mask-image: url("$(res)/img/element-icons/link.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/link.svg"); } .mx_SpotlightDialog_createRoom .mx_AccessibleButton::before { @@ -432,7 +432,7 @@ Please see LICENSE files in the repository root for full details. } .mx_SpotlightDialog_startChat::before { - mask-image: url("$(res)/img/element-icons/room/members.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/user-profile-solid.svg"); } .mx_SpotlightDialog_joinRoomAlias::before { @@ -512,11 +512,11 @@ Please see LICENSE files in the repository root for full details. } &.mx_SpotlightDialog_metaspaceResult_favourites-space { - mask-image: url("$(res)/img/element-icons/roomlist/favorite.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/favourite-solid.svg"); } &.mx_SpotlightDialog_metaspaceResult_people-space { - mask-image: url("$(res)/img/element-icons/room/members.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/user-profile-solid.svg"); } &.mx_SpotlightDialog_metaspaceResult_orphans-space { diff --git a/res/css/views/elements/_InfoTooltip.pcss b/res/css/views/elements/_InfoTooltip.pcss index 0329f6a63b..dcec1410f1 100644 --- a/res/css/views/elements/_InfoTooltip.pcss +++ b/res/css/views/elements/_InfoTooltip.pcss @@ -25,7 +25,7 @@ Please see LICENSE files in the repository root for full details. } .mx_InfoTooltip_icon_info::before { - mask-image: url("$(res)/img/element-icons/info.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/info.svg"); } .mx_InfoTooltip_icon_warning::before { diff --git a/res/css/views/elements/_ProgressBar.pcss b/res/css/views/elements/_ProgressBar.pcss index 8900b7d985..062770f77f 100644 --- a/res/css/views/elements/_ProgressBar.pcss +++ b/res/css/views/elements/_ProgressBar.pcss @@ -16,16 +16,7 @@ progress.mx_ProgressBar { @mixin ProgressBarBorderRadius 6px; @mixin ProgressBarColour var(--cpd-color-icon-accent-tertiary); @mixin ProgressBarBgColour $progressbar-bg-color; - ::-webkit-progress-value { + &::-webkit-progress-value { transition: width 1s; } - ::-moz-progress-bar { - transition: padding-bottom 1s; - padding-bottom: var(--value); - transform-origin: 0 0; - transform: rotate(-90deg) translateX(-15px); - padding-left: 15px; - - height: 0; - } } diff --git a/res/css/views/emojipicker/_EmojiPicker.pcss b/res/css/views/emojipicker/_EmojiPicker.pcss index cd7b2bb769..d4ae92172d 100644 --- a/res/css/views/emojipicker/_EmojiPicker.pcss +++ b/res/css/views/emojipicker/_EmojiPicker.pcss @@ -111,7 +111,7 @@ Please see LICENSE files in the repository root for full details. border-radius: 4px 0; &::placeholder { - color: var(--cpd-color-text-placeholder); + color: var(--cpd-color-text-secondary); } } diff --git a/res/css/views/messages/_MessageActionBar.pcss b/res/css/views/messages/_MessageActionBar.pcss index 3768bfb021..cdfc3693d5 100644 --- a/res/css/views/messages/_MessageActionBar.pcss +++ b/res/css/views/messages/_MessageActionBar.pcss @@ -108,12 +108,16 @@ Please see LICENSE files in the repository root for full details. color: var(--cpd-color-icon-primary); } + &.mx_MessageActionBar_threadButton { + --MessageActionBar-icon-size: 20px; + } + &.mx_MessageActionBar_retryButton { --MessageActionBar-icon-size: 16px; } &.mx_MessageActionBar_downloadButton { - --MessageActionBar-icon-size: 14px; + --MessageActionBar-icon-size: 20px; &.mx_MessageActionBar_downloadSpinnerButton { svg { diff --git a/res/css/views/right_panel/_ThreadPanel.pcss b/res/css/views/right_panel/_ThreadPanel.pcss index 93efded304..1f9d1e0562 100644 --- a/res/css/views/right_panel/_ThreadPanel.pcss +++ b/res/css/views/right_panel/_ThreadPanel.pcss @@ -165,7 +165,7 @@ Please see LICENSE files in the repository root for full details. } .mx_ThreadPanel_copyLinkToThread::before { - mask-image: url("$(res)/img/element-icons/link.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/link.svg"); } .mx_ContextualMenu_wrapper { diff --git a/res/css/views/rooms/_BasicMessageComposer.pcss b/res/css/views/rooms/_BasicMessageComposer.pcss index e34c991d89..499ce870ec 100644 --- a/res/css/views/rooms/_BasicMessageComposer.pcss +++ b/res/css/views/rooms/_BasicMessageComposer.pcss @@ -7,6 +7,11 @@ Please see LICENSE files in the repository root for full details. */ .mx_BasicMessageComposer { + /* These are set in Javascript */ + --avatar-letter: ""; + --avatar-background: unset; + --placeholder: ""; + position: relative; .mx_BasicMessageComposer_inputEmpty > :first-child::before { diff --git a/res/css/views/rooms/_EventBubbleTile.pcss b/res/css/views/rooms/_EventBubbleTile.pcss index 3a42cde9bb..7b1af0c771 100644 --- a/res/css/views/rooms/_EventBubbleTile.pcss +++ b/res/css/views/rooms/_EventBubbleTile.pcss @@ -334,7 +334,6 @@ Please see LICENSE files in the repository root for full details. .mx_MImageBody { width: 100%; - height: 100%; .mx_MImageBody_thumbnail.mx_MImageBody_thumbnail--blurhash { position: unset; diff --git a/res/css/views/rooms/_EventTile.pcss b/res/css/views/rooms/_EventTile.pcss index 311e059166..d405381db1 100644 --- a/res/css/views/rooms/_EventTile.pcss +++ b/res/css/views/rooms/_EventTile.pcss @@ -1017,16 +1017,6 @@ $left-gutter: 64px; visibility: visible; } -/* Inverse of the above to *disable* the animation on any indicators. This approach */ -/* is less pretty, but is easier to target because otherwise we need to define the */ -/* animation for when it's shown which means duplicating the style definition in */ -/* multiple places. */ -.mx_EventTile:not(:hover):not(.mx_EventTile_actionBarFocused):not([data-whatinput="keyboard"] :focus-within) { - &:not(:focus-visible:focus-within) .mx_MessageActionBar .mx_Indicator { - animation: none; - } -} - .mx_EventTile[data-shape="ThreadsList"], .mx_EventTile[data-shape="Notification"] { --topOffset: $spacing-12; diff --git a/res/css/views/rooms/_RoomList.pcss b/res/css/views/rooms/_RoomList.pcss index 4ceba9a20a..97b1e76cef 100644 --- a/res/css/views/rooms/_RoomList.pcss +++ b/res/css/views/rooms/_RoomList.pcss @@ -29,7 +29,7 @@ Please see LICENSE files in the repository root for full details. mask-image: url("$(res)/img/element-icons/roomlist/dialpad.svg"); } .mx_RoomList_iconStartChat::before { - mask-image: url("$(res)/img/element-icons/roomlist/member-plus.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/user-add-solid.svg"); } .mx_RoomList_iconInvite::before { mask-image: url("$(res)/img/element-icons/room/share.svg"); diff --git a/res/css/views/rooms/_RoomListHeader.pcss b/res/css/views/rooms/_RoomListHeader.pcss index 6fbd2a38db..fa0e0b24eb 100644 --- a/res/css/views/rooms/_RoomListHeader.pcss +++ b/res/css/views/rooms/_RoomListHeader.pcss @@ -92,7 +92,7 @@ Please see LICENSE files in the repository root for full details. mask-image: url("$(res)/img/element-icons/room/invite.svg"); } .mx_RoomListHeader_iconStartChat::before { - mask-image: url("$(res)/img/element-icons/roomlist/member-plus.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/user-add-solid.svg"); } .mx_RoomListHeader_iconNewRoom::before { mask-image: url("$(res)/img/element-icons/roomlist/hash-plus.svg"); diff --git a/res/css/views/rooms/_RoomPreviewCard.pcss b/res/css/views/rooms/_RoomPreviewCard.pcss index 6b070de27f..f96b705cc2 100644 --- a/res/css/views/rooms/_RoomPreviewCard.pcss +++ b/res/css/views/rooms/_RoomPreviewCard.pcss @@ -34,7 +34,7 @@ Please see LICENSE files in the repository root for full details. mask-repeat: no-repeat; mask-position: center; mask-size: contain; - mask-image: url("$(res)/img/element-icons/room/room-summary.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/info-solid.svg"); background-color: $secondary-content; } } diff --git a/res/css/views/rooms/_RoomTile.pcss b/res/css/views/rooms/_RoomTile.pcss index 1550fc84fa..53f9c10f1b 100644 --- a/res/css/views/rooms/_RoomTile.pcss +++ b/res/css/views/rooms/_RoomTile.pcss @@ -182,7 +182,7 @@ Please see LICENSE files in the repository root for full details. .mx_RoomTile_contextMenu { .mx_RoomTile_iconStar::before { - mask-image: url("$(res)/img/element-icons/roomlist/favorite.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/favourite-solid.svg"); } .mx_RoomTile_iconArrowDown::before { @@ -206,7 +206,7 @@ Please see LICENSE files in the repository root for full details. } .mx_RoomTile_iconPeople::before { - mask-image: url("$(res)/img/element-icons/room/members.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/user-profile-solid.svg"); } .mx_RoomTile_iconFiles::before { @@ -218,7 +218,7 @@ Please see LICENSE files in the repository root for full details. } .mx_RoomTile_iconWidgets::before { - mask-image: url("$(res)/img/element-icons/room/apps.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/extensions-solid.svg"); } .mx_RoomTile_iconSettings::before { @@ -226,11 +226,11 @@ Please see LICENSE files in the repository root for full details. } .mx_RoomTile_iconExport::before { - mask-image: url("$(res)/img/element-icons/export.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/export-archive.svg"); } .mx_RoomTile_iconCopyLink::before { - mask-image: url("$(res)/img/element-icons/link.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/link.svg"); } .mx_RoomTile_iconInvite::before { @@ -238,6 +238,6 @@ Please see LICENSE files in the repository root for full details. } .mx_RoomTile_iconSignOut::before { - mask-image: url("$(res)/img/element-icons/leave.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/leave.svg"); } } diff --git a/res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss b/res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss index dfe05afa3e..34c2a4d626 100644 --- a/res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss +++ b/res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss @@ -7,6 +7,11 @@ Please see LICENSE files in the repository root for full details. */ .mx_WysiwygComposer_Editor_container { + /* These are set in Javascript */ + --avatar-letter: ""; + --avatar-background: unset; + --placeholder: ""; + @keyframes visualbell { from { background-color: $visual-bell-bg-color; @@ -155,7 +160,7 @@ Please see LICENSE files in the repository root for full details. display: inline-block; pointer-events: none; white-space: nowrap; - color: var(--cpd-color-text-placeholder); + color: var(--cpd-color-text-secondary); } } diff --git a/res/css/views/spaces/_SpacePublicShare.pcss b/res/css/views/spaces/_SpacePublicShare.pcss index 58cf3659ae..ddda97b493 100644 --- a/res/css/views/spaces/_SpacePublicShare.pcss +++ b/res/css/views/spaces/_SpacePublicShare.pcss @@ -11,7 +11,7 @@ Please see LICENSE files in the repository root for full details. @mixin SpacePillButton; &.mx_SpacePublicShare_shareButton::before { - mask-image: url("$(res)/img/element-icons/link.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/link.svg"); } &.mx_SpacePublicShare_inviteButton::before { diff --git a/res/fonts/Open_Sans/LICENSE.txt b/res/fonts/Open_Sans/LICENSE.txt index 9df79cc97d..75b52484ea 100755 --- a/res/fonts/Open_Sans/LICENSE.txt +++ b/res/fonts/Open_Sans/LICENSE.txt @@ -1,224 +1,3 @@ -<<<<<<<< HEAD:res/jitsi_external_api.min.js.LICENSE.txt - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - 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. - - - -Note: - -This project was originally contributed to the community under the MIT license and with the following notice: - -The MIT License (MIT) - -Copyright (c) 2013 ESTOS GmbH -Copyright (c) 2013 BlueJimp SARL - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -======== Apache License Version 2.0, January 2004 @@ -421,4 +200,3 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI 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. ->>>>>>>> repomerge/t3chguy/repomerge:res/fonts/Open_Sans/LICENSE.txt diff --git a/res/img/element-icons/export.svg b/res/img/element-icons/export.svg deleted file mode 100644 index 038866c294..0000000000 --- a/res/img/element-icons/export.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/res/img/element-icons/info.svg b/res/img/element-icons/info.svg deleted file mode 100644 index b5769074ab..0000000000 --- a/res/img/element-icons/info.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/res/img/element-icons/leave.svg b/res/img/element-icons/leave.svg deleted file mode 100644 index 773e27d4ce..0000000000 --- a/res/img/element-icons/leave.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/res/img/element-icons/link.svg b/res/img/element-icons/link.svg deleted file mode 100644 index 07dbdc0ccc..0000000000 --- a/res/img/element-icons/link.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/element-icons/location.svg b/res/img/element-icons/location.svg deleted file mode 100644 index fc8337a43b..0000000000 --- a/res/img/element-icons/location.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/element-icons/message/fwd.svg b/res/img/element-icons/message/fwd.svg deleted file mode 100644 index 8bcc70d092..0000000000 --- a/res/img/element-icons/message/fwd.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/element-icons/message/thread.svg b/res/img/element-icons/message/thread.svg deleted file mode 100644 index dc23d8c14a..0000000000 --- a/res/img/element-icons/message/thread.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/res/img/element-icons/room/apps.svg b/res/img/element-icons/room/apps.svg deleted file mode 100644 index c90704752c..0000000000 --- a/res/img/element-icons/room/apps.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/res/img/element-icons/room/members.svg b/res/img/element-icons/room/members.svg deleted file mode 100644 index 50aa0aa466..0000000000 --- a/res/img/element-icons/room/members.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/res/img/element-icons/room/message-bar/reply.svg b/res/img/element-icons/room/message-bar/reply.svg deleted file mode 100644 index c32848a0b0..0000000000 --- a/res/img/element-icons/room/message-bar/reply.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/res/img/element-icons/room/room-summary.svg b/res/img/element-icons/room/room-summary.svg deleted file mode 100644 index b6ac258b18..0000000000 --- a/res/img/element-icons/room/room-summary.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/element-icons/room/thread.svg b/res/img/element-icons/room/thread.svg deleted file mode 100644 index d1b8b35c91..0000000000 --- a/res/img/element-icons/room/thread.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/res/img/element-icons/roomlist/favorite.svg b/res/img/element-icons/roomlist/favorite.svg deleted file mode 100644 index c601b69808..0000000000 --- a/res/img/element-icons/roomlist/favorite.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/element-icons/roomlist/member-plus.svg b/res/img/element-icons/roomlist/member-plus.svg deleted file mode 100644 index 71269b54ca..0000000000 --- a/res/img/element-icons/roomlist/member-plus.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/themes/dark/css/_dark.pcss b/res/themes/dark/css/_dark.pcss index 56630763fe..8b0673f692 100644 --- a/res/themes/dark/css/_dark.pcss +++ b/res/themes/dark/css/_dark.pcss @@ -136,7 +136,7 @@ $input-border-color: rgba(231, 231, 231, 0.2); $input-darker-bg-color: #181b21; $input-darker-fg-color: #61708b; $input-lighter-bg-color: #f2f5f8; -$input-placeholder: var(--cpd-color-text-placeholder); +$input-placeholder: var(--cpd-color-text-secondary); /* ******************** */ /* Dialog */ diff --git a/res/themes/legacy-dark/css/_legacy-dark.pcss b/res/themes/legacy-dark/css/_legacy-dark.pcss index c6840f5b90..45bb1870f1 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.pcss +++ b/res/themes/legacy-dark/css/_legacy-dark.pcss @@ -54,7 +54,7 @@ $input-border-color: #e7e7e7; $input-darker-bg-color: #181b21; $input-darker-fg-color: #61708b; $input-lighter-bg-color: #f2f5f8; -$input-placeholder: var(--cpd-color-text-placeholder); +$input-placeholder: var(--cpd-color-text-secondary); $resend-button-divider-color: $muted-fg-color; diff --git a/res/themes/legacy-light/css/_legacy-light.pcss b/res/themes/legacy-light/css/_legacy-light.pcss index 398cf0e1f1..76e0eec588 100644 --- a/res/themes/legacy-light/css/_legacy-light.pcss +++ b/res/themes/legacy-light/css/_legacy-light.pcss @@ -81,7 +81,7 @@ $strong-input-border-color: #c7c7c7; /* used for UserSettings EditableText */ $input-underline-color: rgba(151, 151, 151, 0.5); $input-fg-color: rgba(74, 74, 74, 0.9); -$input-placeholder: var(--cpd-color-text-placeholder); +$input-placeholder: var(--cpd-color-text-secondary); /* scrollbars */ $scrollbar-thumb-color: rgba(0, 0, 0, 0.2); /* context menus */ diff --git a/res/themes/light/css/_light.pcss b/res/themes/light/css/_light.pcss index d649b6b38d..5f278c6f16 100644 --- a/res/themes/light/css/_light.pcss +++ b/res/themes/light/css/_light.pcss @@ -184,7 +184,7 @@ $input-darker-fg-color: #9fa9ba; $input-lighter-bg-color: $secondary-accent-color; $input-underline-color: rgba(151, 151, 151, 0.5); $input-fg-color: rgba(74, 74, 74, 0.9); -$input-placeholder: var(--cpd-color-text-placeholder); +$input-placeholder: var(--cpd-color-text-secondary); /* ******************** */ /* Dialog */ diff --git a/scripts/docker-package.sh b/scripts/docker-package.sh index 12f207d4b0..0920587117 100755 --- a/scripts/docker-package.sh +++ b/scripts/docker-package.sh @@ -17,4 +17,3 @@ fi DIST_VERSION=$("$DIR"/normalize-version.sh "$DIST_VERSION") VERSION=$DIST_VERSION yarn build -echo "$DIST_VERSION" > /src/webapp/version diff --git a/scripts/package.sh b/scripts/package.sh index 6a8bf2b9bd..9937dd20d3 100755 --- a/scripts/package.sh +++ b/scripts/package.sh @@ -21,8 +21,6 @@ cp -r webapp element-$version # Just in case you have a local config, remove it before packaging rm element-$version/config.json || true -$(dirname $0)/normalize-version.sh ${version} > element-$version/version - # GNU/BSD compatibility workaround tar_perms=(--owner=0 --group=0) && [ "$(uname)" == "Darwin" ] && tar_perms=(--uid=0 --gid=0) tar "${tar_perms[@]}" -chvzf dist/element-$version.tar.gz element-$version diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index 02e26729d2..4f47cd7eac 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -46,6 +46,7 @@ import SettingsStore, { CallbackFn } from "./settings/SettingsStore"; import { UIFeature } from "./settings/UIFeature"; import { isBulkUnverifiedDeviceReminderSnoozed } from "./utils/device/snoozeBulkUnverifiedDeviceReminder"; import { getUserDeviceIds } from "./utils/crypto/deviceInfo"; +import { asyncSomeParallel } from "./utils/arrays.ts"; const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000; @@ -240,13 +241,16 @@ export default class DeviceListener { return this.keyBackupInfo; } - private shouldShowSetupEncryptionToast(): boolean { + private async shouldShowSetupEncryptionToast(): Promise { // If we're in the middle of a secret storage operation, we're likely // modifying the state involved here, so don't add new toasts to setup. if (isSecretStorageBeingAccessed()) return false; // Show setup toasts once the user is in at least one encrypted room. const cli = this.client; - return cli?.getRooms().some((r) => cli.isRoomEncrypted(r.roomId)) ?? false; + const cryptoApi = cli?.getCrypto(); + if (!cli || !cryptoApi) return false; + + return await asyncSomeParallel(cli.getRooms(), ({ roomId }) => cryptoApi.isEncryptionEnabledInRoom(roomId)); } private recheck(): void { @@ -283,7 +287,7 @@ export default class DeviceListener { hideSetupEncryptionToast(); this.checkKeyBackupStatus(); - } else if (this.shouldShowSetupEncryptionToast()) { + } else if (await this.shouldShowSetupEncryptionToast()) { // make sure our keys are finished downloading await crypto.getUserDeviceInfo([cli.getSafeUserId()]); diff --git a/src/Searching.ts b/src/Searching.ts index 85483eb23c..252d4378ad 100644 --- a/src/Searching.ts +++ b/src/Searching.ts @@ -596,7 +596,7 @@ async function combinedPagination( return result; } -function eventIndexSearch( +async function eventIndexSearch( client: MatrixClient, term: string, roomId?: string, @@ -605,7 +605,7 @@ function eventIndexSearch( let searchPromise: Promise; if (roomId !== undefined) { - if (client.isRoomEncrypted(roomId)) { + if (await client.getCrypto()?.isEncryptionEnabledInRoom(roomId)) { // The search is for a single encrypted room, use our local // search method. searchPromise = localSearchProcess(client, term, roomId); diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index cf8d40acc8..f97dff786f 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -186,6 +186,15 @@ export async function withSecretStorageKeyCache(func: () => Promise): Prom } } +export interface AccessSecretStorageOpts { + /** Reset secret storage even if it's already set up. */ + forceReset?: boolean; + /** Create new cross-signing keys. Only applicable if `forceReset` is `true`. */ + resetCrossSigning?: boolean; + /** The cached account password, if available. */ + accountPassword?: string; +} + /** * This helper should be used whenever you need to access secret storage. It * ensures that secret storage (and also cross-signing since they each depend on @@ -205,14 +214,17 @@ export async function withSecretStorageKeyCache(func: () => Promise): Prom * * @param {Function} [func] An operation to perform once secret storage has been * bootstrapped. Optional. - * @param {bool} [forceReset] Reset secret storage even if it's already set up + * @param [opts] The options to use when accessing secret storage. */ -export async function accessSecretStorage(func = async (): Promise => {}, forceReset = false): Promise { - await withSecretStorageKeyCache(() => doAccessSecretStorage(func, forceReset)); +export async function accessSecretStorage( + func = async (): Promise => {}, + opts: AccessSecretStorageOpts = {}, +): Promise { + await withSecretStorageKeyCache(() => doAccessSecretStorage(func, opts)); } /** Helper for {@link #accessSecretStorage} */ -async function doAccessSecretStorage(func: () => Promise, forceReset: boolean): Promise { +async function doAccessSecretStorage(func: () => Promise, opts: AccessSecretStorageOpts): Promise { try { const cli = MatrixClientPeg.safeGet(); const crypto = cli.getCrypto(); @@ -221,7 +233,7 @@ async function doAccessSecretStorage(func: () => Promise, forceReset: bool } let createNew = false; - if (forceReset) { + if (opts.forceReset) { logger.debug("accessSecretStorage: resetting 4S"); createNew = true; } else if (!(await cli.secretStorage.hasKey())) { @@ -234,9 +246,7 @@ async function doAccessSecretStorage(func: () => Promise, forceReset: bool // passphrase creation. const { finished } = Modal.createDialog( lazy(() => import("./async-components/views/dialogs/security/CreateSecretStorageDialog")), - { - forceReset, - }, + opts, undefined, /* priority = */ false, /* static = */ true, diff --git a/src/SlidingSyncManager.ts b/src/SlidingSyncManager.ts index e3ca77f988..11872d059e 100644 --- a/src/SlidingSyncManager.ts +++ b/src/SlidingSyncManager.ts @@ -229,7 +229,7 @@ export class SlidingSyncManager { subscriptions.delete(roomId); } const room = this.client?.getRoom(roomId); - let shouldLazyLoad = !this.client?.isRoomEncrypted(roomId); + let shouldLazyLoad = !(await this.client?.getCrypto()?.isEncryptionEnabledInRoom(roomId)); if (!room) { // default to safety: request all state if we can't work it out. This can happen if you // refresh the app whilst viewing a room: we call setRoomVisible before we know anything diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx index 1e87b5b826..1258bde2ca 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx @@ -58,6 +58,7 @@ interface IProps { hasCancel?: boolean; accountPassword?: string; forceReset?: boolean; + resetCrossSigning?: boolean; onFinished(ok?: boolean): void; } @@ -91,6 +92,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent = { hasCancel: true, forceReset: false, + resetCrossSigning: false, }; private recoveryKey?: GeneratedSecretStorageKey; private recoveryKeyNode = createRef(); @@ -270,7 +272,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent => { const cli = MatrixClientPeg.safeGet(); const crypto = cli.getCrypto()!; - const { forceReset } = this.props; + const { forceReset, resetCrossSigning } = this.props; let backupInfo; // First, unless we know we want to do a reset, we see if there is an existing key backup @@ -292,12 +294,28 @@ export default class CreateSecretStorageDialog extends React.PureComponent this.recoveryKey!, - setupNewKeyBackup: true, setupNewSecretStorage: true, }); + if (resetCrossSigning) { + logger.log("Resetting cross signing"); + await crypto.bootstrapCrossSigning({ + authUploadDeviceSigningKeys: this.doBootstrapUIAuth, + setupNewCrossSigning: true, + }); + } + logger.log("Resetting key backup"); + await crypto.resetKeyBackup(); } else { // For password authentication users after 2020-09, this cross-signing // step will be a no-op since it is now setup during registration or login diff --git a/src/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx index 18c93d0cd0..a8f50ceccb 100644 --- a/src/autocomplete/UserProvider.tsx +++ b/src/autocomplete/UserProvider.tsx @@ -37,7 +37,7 @@ const USER_REGEX = /\B@\S*/g; // used when you hit 'tab' - we allow some separator chars at the beginning // to allow you to tab-complete /mat into /(matthew) -const FORCED_USER_REGEX = /[^/,:; \t\n]\S*/g; +const FORCED_USER_REGEX = /[^/,.():; \t\n]\S*/g; export default class UserProvider extends AutocompleteProvider { public matcher: QueryMatcher; diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index afd444c952..e51dd96647 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -427,7 +427,7 @@ export default class MatrixChat extends React.PureComponent { } } else if ( (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) && - !shouldSkipSetupEncryption(cli) + !(await shouldSkipSetupEncryption(cli)) ) { // if cross-signing is not yet set up, do so now if possible. this.setStateForNewView({ view: Views.E2E_SETUP }); diff --git a/src/components/views/dialogs/ReportEventDialog.tsx b/src/components/views/dialogs/ReportEventDialog.tsx index 75b9977dc4..3234c2be35 100644 --- a/src/components/views/dialogs/ReportEventDialog.tsx +++ b/src/components/views/dialogs/ReportEventDialog.tsx @@ -43,6 +43,10 @@ interface IState { // If we know it, the nature of the abuse, as specified by MSC3215. nature?: ExtendedNature; ignoreUserToo: boolean; // if true, user will be ignored/blocked on submit + /* + * Whether the room is encrypted. + */ + isRoomEncrypted: boolean; } const MODERATED_BY_STATE_EVENT_TYPE = [ @@ -188,9 +192,20 @@ export default class ReportEventDialog extends React.Component { // If specified, the nature of the abuse, as specified by MSC3215. nature: undefined, ignoreUserToo: false, // default false, for now. Could easily be argued as default true + isRoomEncrypted: false, // async, will be set later }; } + public componentDidMount = async (): Promise => { + const crypto = MatrixClientPeg.safeGet().getCrypto(); + const roomId = this.props.mxEvent.getRoomId(); + if (!crypto || !roomId) return; + + this.setState({ + isRoomEncrypted: await crypto.isEncryptionEnabledInRoom(roomId), + }); + }; + private onIgnoreUserTooChanged = (newVal: boolean): void => { this.setState({ ignoreUserToo: newVal }); }; @@ -319,7 +334,6 @@ export default class ReportEventDialog extends React.Component { if (this.moderation) { // Display report-to-moderator dialog. // We let the user pick a nature. - const client = MatrixClientPeg.safeGet(); const homeServerName = SdkConfig.get("validated_server_config")!.hsName; let subtitle: string; switch (this.state.nature) { @@ -336,7 +350,7 @@ export default class ReportEventDialog extends React.Component { subtitle = _t("report_content|nature_spam"); break; case NonStandardValue.Admin: - if (client.isRoomEncrypted(this.props.mxEvent.getRoomId()!)) { + if (this.state.isRoomEncrypted) { subtitle = _t("report_content|nature_nonstandard_admin_encrypted", { homeserver: homeServerName, }); diff --git a/src/components/views/dialogs/devtools/RoomNotifications.tsx b/src/components/views/dialogs/devtools/RoomNotifications.tsx index c54e695006..1bcff78487 100644 --- a/src/components/views/dialogs/devtools/RoomNotifications.tsx +++ b/src/components/views/dialogs/devtools/RoomNotifications.tsx @@ -17,6 +17,7 @@ import { determineUnreadState } from "../../../../RoomNotifs"; import { humanReadableNotificationLevel } from "../../../../stores/notifications/NotificationLevel"; import { doesRoomOrThreadHaveUnreadMessages } from "../../../../Unread"; import BaseTool, { DevtoolsContext, IDevtoolsProps } from "./BaseTool"; +import { useIsEncrypted } from "../../../../hooks/useIsEncrypted.ts"; function UserReadUpTo({ target }: { target: ReadReceipt }): JSX.Element { const cli = useContext(MatrixClientContext); @@ -59,6 +60,7 @@ function UserReadUpTo({ target }: { target: ReadReceipt }): JSX.Elemen export default function RoomNotifications({ onBack }: IDevtoolsProps): JSX.Element { const { room } = useContext(DevtoolsContext); const cli = useContext(MatrixClientContext); + const isRoomEncrypted = useIsEncrypted(cli, room); const { level, count } = determineUnreadState(room, undefined, false); const [notificationState] = useNotificationState(room); @@ -93,9 +95,7 @@ export default function RoomNotifications({ onBack }: IDevtoolsProps): JSX.Eleme
  • {_t( - cli.isRoomEncrypted(room.roomId!) - ? _td("devtools|room_encrypted") - : _td("devtools|room_not_encrypted"), + isRoomEncrypted ? _td("devtools|room_encrypted") : _td("devtools|room_not_encrypted"), {}, { strong: (sub) => {sub}, diff --git a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx index 7361e3982d..d9c97261dd 100644 --- a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx +++ b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx @@ -19,7 +19,6 @@ import AccessibleButton, { ButtonEvent } from "../../elements/AccessibleButton"; import { _t } from "../../../../languageHandler"; import { accessSecretStorage } from "../../../../SecurityManager"; import Modal from "../../../../Modal"; -import InteractiveAuthDialog from "../InteractiveAuthDialog"; import DialogButtons from "../../elements/DialogButtons"; import BaseDialog from "../BaseDialog"; import { chromeFileInputFix } from "../../../../utils/BrowserWorkarounds"; @@ -226,28 +225,14 @@ export default class AccessSecretStorageDialog extends React.PureComponent => { - // Now reset cross-signing so everything Just Worksâ„¢ again. - const cli = MatrixClientPeg.safeGet(); - await cli.getCrypto()?.bootstrapCrossSigning({ - authUploadDeviceSigningKeys: async (makeRequest): Promise => { - const { finished } = Modal.createDialog(InteractiveAuthDialog, { - title: _t("encryption|bootstrap_title"), - matrixClient: cli, - makeRequest, - }); - const [confirmed] = await finished; - if (!confirmed) { - throw new Error("Cross-signing key upload auth canceled"); - } - }, - setupNewCrossSigning: true, - }); - - // Now we can indicate that the user is done pressing buttons, finally. - // Upstream flows will detect the new secret storage, key backup, etc and use it. - this.props.onFinished({}); - }, true); + await accessSecretStorage( + async (): Promise => { + // Now we can indicate that the user is done pressing buttons, finally. + // Upstream flows will detect the new secret storage, key backup, etc and use it. + this.props.onFinished({}); + }, + { forceReset: true, resetCrossSigning: true }, + ); } catch (e) { logger.error(e); this.props.onFinished(false); diff --git a/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx b/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx index af84feb848..ec85e72ac9 100644 --- a/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx +++ b/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx @@ -109,7 +109,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { this.props.onFinished(false); - accessSecretStorage(async (): Promise => {}, /* forceReset = */ true); + accessSecretStorage(async (): Promise => {}, { forceReset: true }); }; /** diff --git a/src/components/views/elements/MiniAvatarUploader.tsx b/src/components/views/elements/MiniAvatarUploader.tsx index 8bbca5b309..cf5a239814 100644 --- a/src/components/views/elements/MiniAvatarUploader.tsx +++ b/src/components/views/elements/MiniAvatarUploader.tsx @@ -17,6 +17,7 @@ import { useTimeout } from "../../../hooks/useTimeout"; import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; import AccessibleButton from "./AccessibleButton"; import Spinner from "./Spinner"; +import { getFileChanged } from "../settings/AvatarSetting.tsx"; export const AVATAR_SIZE = "52px"; @@ -72,11 +73,12 @@ const MiniAvatarUploader: React.FC = ({ onClick?.(ev); }} onChange={async (ev): Promise => { - if (!ev.target.files?.length) return; setBusy(true); - const file = ev.target.files[0]; - const { content_uri: uri } = await cli.uploadContent(file); - await setAvatarUrl(uri); + const file = getFileChanged(ev); + if (file) { + const { content_uri: uri } = await cli.uploadContent(file); + await setAvatarUrl(uri); + } setBusy(false); }} accept="image/*" diff --git a/src/components/views/location/MapFallback.tsx b/src/components/views/location/MapFallback.tsx index cb1a579764..101a5d8066 100644 --- a/src/components/views/location/MapFallback.tsx +++ b/src/components/views/location/MapFallback.tsx @@ -8,8 +8,8 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import classNames from "classnames"; +import LocationMarkerIcon from "@vector-im/compound-design-tokens/assets/web/icons/location-pin-solid"; -import { Icon as LocationMarkerIcon } from "../../../../res/img/element-icons/location.svg"; import { Icon as MapFallbackImage } from "../../../../res/img/location/map.svg"; import Spinner from "../elements/Spinner"; diff --git a/src/components/views/location/Marker.tsx b/src/components/views/location/Marker.tsx index 93a5c28831..58e1ce30fb 100644 --- a/src/components/views/location/Marker.tsx +++ b/src/components/views/location/Marker.tsx @@ -9,8 +9,8 @@ Please see LICENSE files in the repository root for full details. import React, { ReactNode, useState } from "react"; import classNames from "classnames"; import { RoomMember } from "matrix-js-sdk/src/matrix"; +import LocationIcon from "@vector-im/compound-design-tokens/assets/web/icons/location-pin-solid"; -import { Icon as LocationIcon } from "../../../../res/img/element-icons/location.svg"; import { getUserNameColorClass } from "../../../utils/FormattingUtils"; import MemberAvatar from "../avatars/MemberAvatar"; diff --git a/src/components/views/location/ShareType.tsx b/src/components/views/location/ShareType.tsx index f580d4638d..0aa31b7bd4 100644 --- a/src/components/views/location/ShareType.tsx +++ b/src/components/views/location/ShareType.tsx @@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import React, { HTMLAttributes, useContext } from "react"; +import LocationIcon from "@vector-im/compound-design-tokens/assets/web/icons/location-pin-solid"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { _t } from "../../../languageHandler"; @@ -14,7 +15,6 @@ import { OwnProfileStore } from "../../../stores/OwnProfileStore"; import BaseAvatar from "../avatars/BaseAvatar"; import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; import Heading from "../typography/Heading"; -import { Icon as LocationIcon } from "../../../../res/img/element-icons/location.svg"; import { LocationShareType } from "./shareLocation"; import StyledLiveBeaconIcon from "../beacon/StyledLiveBeaconIcon"; diff --git a/src/components/views/messages/EncryptionEvent.tsx b/src/components/views/messages/EncryptionEvent.tsx index e721662cb5..bc6680d300 100644 --- a/src/components/views/messages/EncryptionEvent.tsx +++ b/src/components/views/messages/EncryptionEvent.tsx @@ -6,18 +6,18 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { forwardRef, useContext } from "react"; +import React, { forwardRef } from "react"; import { MatrixEvent } from "matrix-js-sdk/src/matrix"; import type { RoomEncryptionEventContent } from "matrix-js-sdk/src/types"; import { _t } from "../../../languageHandler"; -import { MatrixClientPeg } from "../../../MatrixClientPeg"; import EventTileBubble from "./EventTileBubble"; -import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; import DMRoomMap from "../../../utils/DMRoomMap"; import { objectHasDiff } from "../../../utils/objects"; import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom"; import { MEGOLM_ENCRYPTION_ALGORITHM } from "../../../utils/crypto"; +import { useIsEncrypted } from "../../../hooks/useIsEncrypted.ts"; interface IProps { mxEvent: MatrixEvent; @@ -25,9 +25,9 @@ interface IProps { } const EncryptionEvent = forwardRef(({ mxEvent, timestamp }, ref) => { - const cli = useContext(MatrixClientContext); + const cli = useMatrixClientContext(); const roomId = mxEvent.getRoomId()!; - const isRoomEncrypted = MatrixClientPeg.safeGet().isRoomEncrypted(roomId); + const isRoomEncrypted = useIsEncrypted(cli, cli.getRoom(roomId) || undefined); const prevContent = mxEvent.getPrevContent() as RoomEncryptionEventContent; const content = mxEvent.getContent(); diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx index 0776c51437..fdd0200429 100644 --- a/src/components/views/messages/MessageActionBar.tsx +++ b/src/components/views/messages/MessageActionBar.tsx @@ -28,11 +28,11 @@ import { ReplyIcon, DeleteIcon, RestartIcon, + ThreadsIcon, } from "@vector-im/compound-design-tokens/assets/web/icons"; import { Icon as EditIcon } from "../../../../res/img/element-icons/room/message-bar/edit.svg"; import { Icon as EmojiIcon } from "../../../../res/img/element-icons/room/message-bar/emoji.svg"; -import { Icon as ThreadIcon } from "../../../../res/img/element-icons/message/thread.svg"; import { Icon as ExpandMessageIcon } from "../../../../res/img/element-icons/expand-message.svg"; import { Icon as CollapseMessageIcon } from "../../../../res/img/element-icons/collapse-message.svg"; import type { Relations } from "matrix-js-sdk/src/matrix"; @@ -243,7 +243,7 @@ const ReplyInThreadButton: React.FC = ({ mxEvent }) => { onContextMenu={onClick} placement="left" > - + ); }; diff --git a/src/components/views/rooms/EventTile/EventTileThreadToolbar.tsx b/src/components/views/rooms/EventTile/EventTileThreadToolbar.tsx index 603bc9953e..ad1a6ce9a6 100644 --- a/src/components/views/rooms/EventTile/EventTileThreadToolbar.tsx +++ b/src/components/views/rooms/EventTile/EventTileThreadToolbar.tsx @@ -7,11 +7,11 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; +import LinkIcon from "@vector-im/compound-design-tokens/assets/web/icons/link"; import { RovingAccessibleButton } from "../../../../accessibility/RovingTabIndex"; import Toolbar from "../../../../accessibility/Toolbar"; import { _t } from "../../../../languageHandler"; -import { Icon as LinkIcon } from "../../../../../res/img/element-icons/link.svg"; import { Icon as ViewInRoomIcon } from "../../../../../res/img/element-icons/view-in-room.svg"; import { ButtonEvent } from "../../elements/AccessibleButton"; diff --git a/src/components/views/settings/AvatarSetting.tsx b/src/components/views/settings/AvatarSetting.tsx index eaeabc641b..b6ce541590 100644 --- a/src/components/views/settings/AvatarSetting.tsx +++ b/src/components/views/settings/AvatarSetting.tsx @@ -19,6 +19,8 @@ import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; import { useId } from "../../../utils/useId"; import AccessibleButton from "../elements/AccessibleButton"; import BaseAvatar from "../avatars/BaseAvatar"; +import Modal from "../../../Modal.tsx"; +import ErrorDialog from "../dialogs/ErrorDialog.tsx"; interface MenuProps { trigger: ReactNode; @@ -103,6 +105,18 @@ interface IProps { placeholderName: string; } +export function getFileChanged(e: React.ChangeEvent): File | null { + if (!e.target.files?.length) return null; + const file = e.target.files[0]; + if (file.type.startsWith("image/")) return file; + + Modal.createDialog(ErrorDialog, { + title: _t("upload_failed_title"), + description: _t("upload_file|not_image"), + }); + return null; +} + /** * Component for setting or removing an avatar on something (eg. a user or a room) */ @@ -139,7 +153,10 @@ const AvatarSetting: React.FC = ({ const onFileChanged = useCallback( (e: React.ChangeEvent) => { - if (e.target.files) onChange?.(e.target.files[0]); + const file = getFileChanged(e); + if (file) { + onChange?.(file); + } }, [onChange], ); diff --git a/src/components/views/settings/SecureBackupPanel.tsx b/src/components/views/settings/SecureBackupPanel.tsx index db165eb115..06c67c7d0b 100644 --- a/src/components/views/settings/SecureBackupPanel.tsx +++ b/src/components/views/settings/SecureBackupPanel.tsx @@ -209,7 +209,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { private resetSecretStorage = async (): Promise => { this.setState({ error: false }); try { - await accessSecretStorage(async (): Promise => {}, /* forceReset = */ true); + await accessSecretStorage(async (): Promise => {}, { forceReset: true }); } catch (e) { logger.error("Error resetting secret storage", e); if (this.unmounted) return; diff --git a/src/components/views/settings/tabs/user/SidebarUserSettingsTab.tsx b/src/components/views/settings/tabs/user/SidebarUserSettingsTab.tsx index 41ebbaf669..0971ece699 100644 --- a/src/components/views/settings/tabs/user/SidebarUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/SidebarUserSettingsTab.tsx @@ -7,10 +7,13 @@ Please see LICENSE files in the repository root for full details. */ import React, { ChangeEvent, useMemo } from "react"; -import { VideoCallSolidIcon, HomeSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { + VideoCallSolidIcon, + HomeSolidIcon, + UserProfileSolidIcon, + FavouriteSolidIcon, +} from "@vector-im/compound-design-tokens/assets/web/icons"; -import { Icon as FavoriteIcon } from "../../../../../../res/img/element-icons/roomlist/favorite.svg"; -import { Icon as MembersIcon } from "../../../../../../res/img/element-icons/room/members.svg"; import { Icon as HashCircleIcon } from "../../../../../../res/img/element-icons/roomlist/hash-circle.svg"; import { _t } from "../../../../../languageHandler"; import SettingsStore from "../../../../../settings/SettingsStore"; @@ -112,7 +115,7 @@ const SidebarUserSettingsTab: React.FC = () => { className="mx_SidebarUserSettingsTab_checkbox" > - + {_t("common|favourites")} @@ -126,7 +129,7 @@ const SidebarUserSettingsTab: React.FC = () => { className="mx_SidebarUserSettingsTab_checkbox" > - + {_t("common|people")} diff --git a/src/components/views/spaces/QuickSettingsButton.tsx b/src/components/views/spaces/QuickSettingsButton.tsx index c21e0a71e2..161290fca8 100644 --- a/src/components/views/spaces/QuickSettingsButton.tsx +++ b/src/components/views/spaces/QuickSettingsButton.tsx @@ -8,7 +8,11 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import classNames from "classnames"; -import EllipsisIcon from "@vector-im/compound-design-tokens/assets/web/icons/overflow-horizontal"; +import { + OverflowHorizontalIcon, + UserProfileSolidIcon, + FavouriteSolidIcon, +} from "@vector-im/compound-design-tokens/assets/web/icons"; import { _t } from "../../../languageHandler"; import ContextMenu, { alwaysAboveRightOf, ChevronFace, useContextMenu } from "../../structures/ContextMenu"; @@ -22,8 +26,6 @@ import { Action } from "../../../dispatcher/actions"; import { UserTab } from "../dialogs/UserTab"; import QuickThemeSwitcher from "./QuickThemeSwitcher"; import { Icon as PinUprightIcon } from "../../../../res/img/element-icons/room/pin-upright.svg"; -import { Icon as MembersIcon } from "../../../../res/img/element-icons/room/members.svg"; -import { Icon as FavoriteIcon } from "../../../../res/img/element-icons/roomlist/favorite.svg"; import Modal from "../../../Modal"; import DevtoolsDialog from "../dialogs/DevtoolsDialog"; import { SdkContextClass } from "../../../contexts/SDKContext"; @@ -89,7 +91,7 @@ const QuickSettingsButton: React.FC<{ checked={!!favouritesEnabled} onChange={onMetaSpaceChangeFactory(MetaSpace.Favourites, "WebQuickSettingsPinToSidebarCheckbox")} > - + {_t("common|favourites")} - + {_t("common|people")} - + {_t("quick_settings|sidebar_settings")} diff --git a/src/editor/autocomplete.ts b/src/editor/autocomplete.ts index 28f86ddf77..542a2bbea5 100644 --- a/src/editor/autocomplete.ts +++ b/src/editor/autocomplete.ts @@ -10,11 +10,12 @@ import { KeyboardEvent } from "react"; import { Part, CommandPartCreator, PartCreator } from "./parts"; import DocumentPosition from "./position"; -import { ICompletion } from "../autocomplete/Autocompleter"; +import { ICompletion, ISelectionRange } from "../autocomplete/Autocompleter"; import Autocomplete from "../components/views/rooms/Autocomplete"; export interface ICallback { replaceParts?: Part[]; + range?: ISelectionRange; close?: boolean; } @@ -82,6 +83,7 @@ export default class AutocompleteWrapperModel { this.updateCallback({ replaceParts: this.partForCompletion(completion), close: true, + range: completion.range, }); } diff --git a/src/editor/model.ts b/src/editor/model.ts index 67b19a3999..efe294cd21 100644 --- a/src/editor/model.ts +++ b/src/editor/model.ts @@ -250,14 +250,24 @@ export default class EditorModel { return Promise.resolve(); } - private onAutoComplete = ({ replaceParts, close }: ICallback): void => { + private onAutoComplete = ({ replaceParts, close, range }: ICallback): void => { let pos: DocumentPosition | undefined; if (replaceParts) { const autoCompletePartIdx = this.autoCompletePartIdx || 0; - this._parts.splice(autoCompletePartIdx, this.autoCompletePartCount, ...replaceParts); + + this.replaceRange( + new DocumentPosition(autoCompletePartIdx, range?.start ?? 0), + new DocumentPosition( + autoCompletePartIdx + this.autoCompletePartCount - 1, + range?.end ?? this.parts[autoCompletePartIdx + this.autoCompletePartCount - 1].text.length, + ), + replaceParts, + ); + this.autoCompletePartCount = replaceParts.length; const lastPart = replaceParts[replaceParts.length - 1]; - const lastPartIndex = autoCompletePartIdx + replaceParts.length - 1; + // `replaceRange` merges adjacent parts so we need to find it in the new parts list + const lastPartIndex = this.parts.indexOf(lastPart); pos = new DocumentPosition(lastPartIndex, lastPart.text.length); } if (close) { diff --git a/src/hooks/useEncryptionStatus.ts b/src/hooks/useEncryptionStatus.ts index 30417f7821..686f68f25e 100644 --- a/src/hooks/useEncryptionStatus.ts +++ b/src/hooks/useEncryptionStatus.ts @@ -6,21 +6,40 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; -import { useEffect, useState } from "react"; +import { CryptoEvent, MatrixClient, Room, RoomStateEvent } from "matrix-js-sdk/src/matrix"; +import { useEffect, useMemo, useState } from "react"; +import { throttle } from "lodash"; import { E2EStatus, shieldStatusForRoom } from "../utils/ShieldUtils"; +import { useTypedEventEmitter } from "./useEventEmitter"; export function useEncryptionStatus(client: MatrixClient, room: Room): E2EStatus | null { const [e2eStatus, setE2eStatus] = useState(null); - useEffect(() => { - if (client.getCrypto()) { - shieldStatusForRoom(client, room).then((e2eStatus) => { - setE2eStatus(e2eStatus); - }); - } - }, [client, room]); + const updateEncryptionStatus = useMemo( + () => + throttle( + () => { + if (client.getCrypto()) { + shieldStatusForRoom(client, room).then((e2eStatus) => { + setE2eStatus(e2eStatus); + }); + } + }, + 250, + { leading: true, trailing: true }, + ), + [client, room], + ); + + useEffect(updateEncryptionStatus, [updateEncryptionStatus]); + + // shieldStatusForRoom depends on the room membership, each member's trust + // status for each member, and each member's devices, so we update the + // status whenever any of those changes. + useTypedEventEmitter(room, RoomStateEvent.Members, updateEncryptionStatus); + useTypedEventEmitter(client, CryptoEvent.UserTrustStatusChanged, updateEncryptionStatus); + useTypedEventEmitter(client, CryptoEvent.DevicesUpdated, updateEncryptionStatus); return e2eStatus; } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 4a524db97c..3b4765b0ad 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -3742,6 +3742,7 @@ "error_files_too_large": "These files are too large to upload. The file size limit is %(limit)s.", "error_some_files_too_large": "Some files are too large to be uploaded. The file size limit is %(limit)s.", "error_title": "Upload Error", + "not_image": "The file you have chosen is not a valid image file.", "title": "Upload files", "title_progress": "Upload files (%(current)s of %(total)s)", "upload_all_button": "Upload all", diff --git a/src/stores/SetupEncryptionStore.ts b/src/stores/SetupEncryptionStore.ts index 5422f68d7b..2fb9c6a9ca 100644 --- a/src/stores/SetupEncryptionStore.ts +++ b/src/stores/SetupEncryptionStore.ts @@ -19,9 +19,6 @@ import { Device, SecretStorage } from "matrix-js-sdk/src/matrix"; import { MatrixClientPeg } from "../MatrixClientPeg"; import { AccessCancelledError, accessSecretStorage } from "../SecurityManager"; -import Modal from "../Modal"; -import InteractiveAuthDialog from "../components/views/dialogs/InteractiveAuthDialog"; -import { _t } from "../languageHandler"; import { SdkContextClass } from "../contexts/SDKContext"; import { asyncSome } from "../utils/arrays"; import { initialiseDehydration } from "../utils/device/dehydration"; @@ -230,42 +227,16 @@ export class SetupEncryptionStore extends EventEmitter { // secret storage key if they had one. Start by resetting // secret storage and setting up a new recovery key, then // create new cross-signing keys once that succeeds. - await accessSecretStorage(async (): Promise => { - const cli = MatrixClientPeg.safeGet(); - await cli.getCrypto()?.bootstrapCrossSigning({ - authUploadDeviceSigningKeys: async (makeRequest): Promise => { - const cachedPassword = SdkContextClass.instance.accountPasswordStore.getPassword(); - - if (cachedPassword) { - await makeRequest({ - type: "m.login.password", - identifier: { - type: "m.id.user", - user: cli.getSafeUserId(), - }, - user: cli.getSafeUserId(), - password: cachedPassword, - }); - return; - } - - const { finished } = Modal.createDialog(InteractiveAuthDialog, { - title: _t("encryption|bootstrap_title"), - matrixClient: cli, - makeRequest, - }); - const [confirmed] = await finished; - if (!confirmed) { - throw new Error("Cross-signing key upload auth canceled"); - } - }, - setupNewCrossSigning: true, - }); - - await initialiseDehydration(true); - - this.phase = Phase.Finished; - }, true); + await accessSecretStorage( + async (): Promise => { + this.phase = Phase.Finished; + }, + { + forceReset: true, + resetCrossSigning: true, + accountPassword: SdkContextClass.instance.accountPasswordStore.getPassword(), + }, + ); } catch (e) { logger.error("Error resetting cross-signing", e); this.phase = Phase.Intro; diff --git a/src/utils/arrays.ts b/src/utils/arrays.ts index a7f97766b4..82b54e9df6 100644 --- a/src/utils/arrays.ts +++ b/src/utils/arrays.ts @@ -328,6 +328,28 @@ export async function asyncSome(values: Iterable, predicate: (value: T) => return false; } +/** + * Async version of Array.some that runs all promises in parallel. + * @param values + * @param predicate + */ +export async function asyncSomeParallel( + values: Array, + predicate: (value: T) => Promise, +): Promise { + try { + return await Promise.any( + values.map((value) => + predicate(value).then((result) => (result ? Promise.resolve(true) : Promise.reject(false))), + ), + ); + } catch (e) { + // If the array is empty or all the promises are false, Promise.any will reject an AggregateError + if (e instanceof AggregateError) return false; + throw e; + } +} + /** * Async version of Array.filter. * @param values diff --git a/src/utils/crypto/shouldSkipSetupEncryption.ts b/src/utils/crypto/shouldSkipSetupEncryption.ts index 51d7a9303c..d4dbb27d1b 100644 --- a/src/utils/crypto/shouldSkipSetupEncryption.ts +++ b/src/utils/crypto/shouldSkipSetupEncryption.ts @@ -9,6 +9,7 @@ Please see LICENSE files in the repository root for full details. import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { shouldForceDisableEncryption } from "./shouldForceDisableEncryption"; +import { asyncSomeParallel } from "../arrays.ts"; /** * If encryption is force disabled AND the user is not in any encrypted rooms @@ -16,7 +17,13 @@ import { shouldForceDisableEncryption } from "./shouldForceDisableEncryption"; * @param client * @returns {boolean} true when we can skip settings up encryption */ -export const shouldSkipSetupEncryption = (client: MatrixClient): boolean => { +export const shouldSkipSetupEncryption = async (client: MatrixClient): Promise => { const isEncryptionForceDisabled = shouldForceDisableEncryption(client); - return isEncryptionForceDisabled && !client.getRooms().some((r) => client.isRoomEncrypted(r.roomId)); + const crypto = client.getCrypto(); + if (!crypto) return true; + + return ( + isEncryptionForceDisabled && + !(await asyncSomeParallel(client.getRooms(), ({ roomId }) => crypto.isEncryptionEnabledInRoom(roomId))) + ); }; diff --git a/src/utils/notifications.ts b/src/utils/notifications.ts index a131c3e55b..30d2948380 100644 --- a/src/utils/notifications.ts +++ b/src/utils/notifications.ts @@ -151,10 +151,7 @@ export async function setMarkedUnreadState(room: Room, client: MatrixClient, unr const currentState = getMarkedUnreadState(room); if (Boolean(currentState) !== unread) { - // Assuming MSC2867 passes FCP with no changes, we should update to start writing - // the flag to the stable prefix (or both) and then ultimately use only the - // stable prefix. - await client.setRoomAccountData(room.roomId, MARKED_UNREAD_TYPE_UNSTABLE, { unread }); + await client.setRoomAccountData(room.roomId, MARKED_UNREAD_TYPE_STABLE, { unread }); } } diff --git a/test/test-utils/client.ts b/test/test-utils/client.ts index 0a5798d8a1..7842afbfe5 100644 --- a/test/test-utils/client.ts +++ b/test/test-utils/client.ts @@ -162,6 +162,7 @@ export const mockClientMethodsCrypto = (): Partial< getVersion: jest.fn().mockReturnValue("Version 0"), getOwnDeviceKeys: jest.fn().mockReturnValue(new Promise(() => {})), getCrossSigningKeyId: jest.fn(), + isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(false), }), }); diff --git a/test/unit-tests/DeviceListener-test.ts b/test/unit-tests/DeviceListener-test.ts index 0862c6b385..906826e456 100644 --- a/test/unit-tests/DeviceListener-test.ts +++ b/test/unit-tests/DeviceListener-test.ts @@ -95,6 +95,7 @@ describe("DeviceListener", () => { }, }), getSessionBackupPrivateKey: jest.fn(), + isEncryptionEnabledInRoom: jest.fn(), } as unknown as Mocked; mockClient = getMockClientWithEventEmitter({ isGuest: jest.fn(), @@ -105,7 +106,6 @@ describe("DeviceListener", () => { isVersionSupported: jest.fn().mockResolvedValue(true), isInitialSyncComplete: jest.fn().mockReturnValue(true), waitForClientWellKnown: jest.fn(), - isRoomEncrypted: jest.fn(), getClientWellKnown: jest.fn(), getDeviceId: jest.fn().mockReturnValue(deviceId), setAccountData: jest.fn(), @@ -292,7 +292,7 @@ describe("DeviceListener", () => { mockCrypto!.isCrossSigningReady.mockResolvedValue(false); mockCrypto!.isSecretStorageReady.mockResolvedValue(false); mockClient!.getRooms.mockReturnValue(rooms); - mockClient!.isRoomEncrypted.mockReturnValue(true); + jest.spyOn(mockClient.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true); }); it("hides setup encryption toast when cross signing and secret storage are ready", async () => { @@ -317,7 +317,7 @@ describe("DeviceListener", () => { }); it("does not show any toasts when no rooms are encrypted", async () => { - mockClient!.isRoomEncrypted.mockReturnValue(false); + jest.spyOn(mockClient.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(false); await createAndStart(); expect(SetupEncryptionToast.showToast).not.toHaveBeenCalled(); diff --git a/test/unit-tests/SecurityManager-test.ts b/test/unit-tests/SecurityManager-test.ts index 63143d4644..574549d8b2 100644 --- a/test/unit-tests/SecurityManager-test.ts +++ b/test/unit-tests/SecurityManager-test.ts @@ -68,7 +68,7 @@ describe("SecurityManager", () => { stubClient(); const func = jest.fn(); - accessSecretStorage(func, true); + accessSecretStorage(func, { forceReset: true }); expect(spy).toHaveBeenCalledTimes(1); await expect(spy.mock.lastCall![0]).resolves.toEqual(expect.objectContaining({ __test: true })); diff --git a/test/unit-tests/components/structures/MatrixChat-test.tsx b/test/unit-tests/components/structures/MatrixChat-test.tsx index 16106ee0d2..b3766bfc89 100644 --- a/test/unit-tests/components/structures/MatrixChat-test.tsx +++ b/test/unit-tests/components/structures/MatrixChat-test.tsx @@ -146,7 +146,6 @@ describe("", () => { matrixRTC: createStubMatrixRTC(), getDehydratedDevice: jest.fn(), whoami: jest.fn(), - isRoomEncrypted: jest.fn(), logout: jest.fn(), getDeviceId: jest.fn(), getKeyBackupVersion: jest.fn().mockResolvedValue(null), @@ -1011,6 +1010,7 @@ describe("", () => { userHasCrossSigningKeys: jest.fn().mockResolvedValue(false), // This needs to not finish immediately because we need to test the screen appears bootstrapCrossSigning: jest.fn().mockImplementation(() => bootstrapDeferred.promise), + isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(false), }; loginClient.getCrypto.mockReturnValue(mockCrypto as any); }); @@ -1058,9 +1058,11 @@ describe("", () => { }, }); - loginClient.isRoomEncrypted.mockImplementation((roomId) => { - return roomId === encryptedRoom.roomId; - }); + jest.spyOn(loginClient.getCrypto()!, "isEncryptionEnabledInRoom").mockImplementation( + async (roomId) => { + return roomId === encryptedRoom.roomId; + }, + ); }); it("should go straight to logged in view when user is not in any encrypted rooms", async () => { diff --git a/test/unit-tests/components/structures/MessagePanel-test.tsx b/test/unit-tests/components/structures/MessagePanel-test.tsx index 037a57bb06..cf44716ba9 100644 --- a/test/unit-tests/components/structures/MessagePanel-test.tsx +++ b/test/unit-tests/components/structures/MessagePanel-test.tsx @@ -23,6 +23,7 @@ import { createTestClient, getMockClientWithEventEmitter, makeBeaconInfoEvent, + mockClientMethodsCrypto, mockClientMethodsEvents, mockClientMethodsUser, } from "../../../test-utils"; @@ -42,6 +43,7 @@ describe("MessagePanel", function () { const client = getMockClientWithEventEmitter({ ...mockClientMethodsUser(userId), ...mockClientMethodsEvents(), + ...mockClientMethodsCrypto(), getAccountData: jest.fn(), isUserIgnored: jest.fn().mockReturnValue(false), isRoomEncrypted: jest.fn().mockReturnValue(false), diff --git a/test/unit-tests/components/structures/RoomView-test.tsx b/test/unit-tests/components/structures/RoomView-test.tsx index 02bed8cf4f..f30db3d80e 100644 --- a/test/unit-tests/components/structures/RoomView-test.tsx +++ b/test/unit-tests/components/structures/RoomView-test.tsx @@ -21,6 +21,7 @@ import { SearchResult, IEvent, } from "matrix-js-sdk/src/matrix"; +import { CryptoApi, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api"; import { KnownMembership } from "matrix-js-sdk/src/types"; import { fireEvent, render, screen, RenderResult, waitForElementToBeRemoved, waitFor } from "jest-matrix-react"; import userEvent from "@testing-library/user-event"; @@ -72,6 +73,7 @@ describe("RoomView", () => { let rooms: Map; let roomCount = 0; let stores: SdkContextClass; + let crypto: CryptoApi; // mute some noise filterConsole("RVS update", "does not have an m.room.create event", "Current version: 1", "Version capability"); @@ -97,6 +99,7 @@ describe("RoomView", () => { stores.rightPanelStore.useUnitTestClient(cli); jest.spyOn(VoipUserMapper.sharedInstance(), "getVirtualRoomForRoom").mockResolvedValue(undefined); + crypto = cli.getCrypto()!; jest.spyOn(cli, "getCrypto").mockReturnValue(undefined); }); @@ -341,7 +344,13 @@ describe("RoomView", () => { describe("that is encrypted", () => { beforeEach(() => { + // Not all the calls to cli.isRoomEncrypted are migrated, so we need to mock both. mocked(cli.isRoomEncrypted).mockReturnValue(true); + jest.spyOn(cli, "getCrypto").mockReturnValue(crypto); + jest.spyOn(cli.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true); + jest.spyOn(cli.getCrypto()!, "getUserVerificationStatus").mockResolvedValue( + new UserVerificationStatus(false, true, false), + ); localRoom.encrypted = true; localRoom.currentState.setStateEvents([ new MatrixEvent({ @@ -360,7 +369,7 @@ describe("RoomView", () => { it("should match the snapshot", async () => { const { container } = await renderRoomView(); - expect(container).toMatchSnapshot(); + await waitFor(() => expect(container).toMatchSnapshot()); }); }); }); diff --git a/test/unit-tests/components/views/beacon/__snapshots__/BeaconViewDialog-test.tsx.snap b/test/unit-tests/components/views/beacon/__snapshots__/BeaconViewDialog-test.tsx.snap index a87001baae..1bf8cba6bb 100644 --- a/test/unit-tests/components/views/beacon/__snapshots__/BeaconViewDialog-test.tsx.snap +++ b/test/unit-tests/components/views/beacon/__snapshots__/BeaconViewDialog-test.tsx.snap @@ -8,9 +8,18 @@ exports[` renders a fallback when there are no locations 1`]
    -
    + fill="currentColor" + height="1em" + viewBox="0 0 24 24" + width="1em" + xmlns="http://www.w3.org/2000/svg" + > + + diff --git a/test/unit-tests/components/views/context_menus/RoomGeneralContextMenu-test.tsx b/test/unit-tests/components/views/context_menus/RoomGeneralContextMenu-test.tsx index 10de3996e6..9fc32dda29 100644 --- a/test/unit-tests/components/views/context_menus/RoomGeneralContextMenu-test.tsx +++ b/test/unit-tests/components/views/context_menus/RoomGeneralContextMenu-test.tsx @@ -150,7 +150,7 @@ describe("RoomGeneralContextMenu", () => { await sleep(0); - expect(mockClient.setRoomAccountData).toHaveBeenCalledWith(ROOM_ID, "com.famedly.marked_unread", { + expect(mockClient.setRoomAccountData).toHaveBeenCalledWith(ROOM_ID, "m.marked_unread", { unread: true, }); expect(onFinished).toHaveBeenCalled(); diff --git a/test/unit-tests/components/views/dialogs/AccessSecretStorageDialog-test.tsx b/test/unit-tests/components/views/dialogs/AccessSecretStorageDialog-test.tsx index 30e1151d53..f5b0b1e074 100644 --- a/test/unit-tests/components/views/dialogs/AccessSecretStorageDialog-test.tsx +++ b/test/unit-tests/components/views/dialogs/AccessSecretStorageDialog-test.tsx @@ -122,4 +122,34 @@ describe("AccessSecretStorageDialog", () => { expect(screen.getByPlaceholderText("Security Phrase")).toHaveFocus(); }); + + it("Can reset secret storage", async () => { + jest.spyOn(mockClient.secretStorage, "checkKey").mockResolvedValue(true); + + const onFinished = jest.fn(); + const checkPrivateKey = jest.fn().mockResolvedValue(true); + renderComponent({ onFinished, checkPrivateKey }); + + await userEvent.click(screen.getByText("Reset all"), { delay: null }); + + // It will prompt the user to confirm resetting + expect(screen.getByText("Reset everything")).toBeInTheDocument(); + await userEvent.click(screen.getByText("Reset"), { delay: null }); + + // Then it will prompt the user to create a key/passphrase + await screen.findByText("Set up Secure Backup"); + document.execCommand = jest.fn().mockReturnValue(true); + jest.spyOn(mockClient.getCrypto()!, "createRecoveryKeyFromPassphrase").mockResolvedValue({ + privateKey: new Uint8Array(), + encodedPrivateKey: securityKey, + }); + screen.getByRole("button", { name: "Continue" }).click(); + + await screen.findByText(/Save your Security Key/); + screen.getByRole("button", { name: "Copy" }).click(); + await screen.findByText("Copied!"); + screen.getByRole("button", { name: "Continue" }).click(); + + await screen.findByText("Secure Backup successful"); + }); }); diff --git a/test/unit-tests/components/views/dialogs/security/CreateSecretStorageDialog-test.tsx b/test/unit-tests/components/views/dialogs/security/CreateSecretStorageDialog-test.tsx index b9d0514148..fa1d74955d 100644 --- a/test/unit-tests/components/views/dialogs/security/CreateSecretStorageDialog-test.tsx +++ b/test/unit-tests/components/views/dialogs/security/CreateSecretStorageDialog-test.tsx @@ -97,4 +97,38 @@ describe("CreateSecretStorageDialog", () => { await screen.findByText("Your keys are now being backed up from this device."); }); }); + + it("resets keys in the right order when resetting secret storage and cross-signing", async () => { + const result = renderComponent({ forceReset: true, resetCrossSigning: true }); + + await result.findByText(/Set up Secure Backup/); + jest.spyOn(mockClient.getCrypto()!, "createRecoveryKeyFromPassphrase").mockResolvedValue({ + privateKey: new Uint8Array(), + encodedPrivateKey: "abcd efgh ijkl", + }); + result.getByRole("button", { name: "Continue" }).click(); + + await result.findByText(/Save your Security Key/); + result.getByRole("button", { name: "Copy" }).click(); + + // Resetting should reset secret storage, cross signing, and key + // backup. We make sure that all three are reset, and done in the + // right order. + const resetFunctionCallLog: string[] = []; + jest.spyOn(mockClient.getCrypto()!, "bootstrapSecretStorage").mockImplementation(async () => { + resetFunctionCallLog.push("bootstrapSecretStorage"); + }); + jest.spyOn(mockClient.getCrypto()!, "bootstrapCrossSigning").mockImplementation(async () => { + resetFunctionCallLog.push("bootstrapCrossSigning"); + }); + jest.spyOn(mockClient.getCrypto()!, "resetKeyBackup").mockImplementation(async () => { + resetFunctionCallLog.push("resetKeyBackup"); + }); + + result.getByRole("button", { name: "Continue" }).click(); + + await result.findByText("Your keys are now being backed up from this device."); + + expect(resetFunctionCallLog).toEqual(["bootstrapSecretStorage", "bootstrapCrossSigning", "resetKeyBackup"]); + }); }); diff --git a/test/unit-tests/components/views/elements/MiniAvatarUploader-test.tsx b/test/unit-tests/components/views/elements/MiniAvatarUploader-test.tsx new file mode 100644 index 0000000000..cf6ed6ae62 --- /dev/null +++ b/test/unit-tests/components/views/elements/MiniAvatarUploader-test.tsx @@ -0,0 +1,40 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import React from "react"; +import { render } from "jest-matrix-react"; +import userEvent from "@testing-library/user-event"; +import { mocked } from "jest-mock"; + +import MiniAvatarUploader from "../../../../../src/components/views/elements/MiniAvatarUploader.tsx"; +import { stubClient, withClientContextRenderOptions } from "../../../../test-utils"; + +const BASE64_GIF = "R0lGODlhAQABAAAAACw="; +const AVATAR_FILE = new File([Uint8Array.from(atob(BASE64_GIF), (c) => c.charCodeAt(0))], "avatar.gif", { + type: "image/gif", +}); + +describe("", () => { + it("calls setAvatarUrl when a file is uploaded", async () => { + const cli = stubClient(); + mocked(cli.uploadContent).mockResolvedValue({ content_uri: "mxc://example.com/1234" }); + + const setAvatarUrl = jest.fn(); + const user = userEvent.setup(); + + const { container, findByText } = render( + , + withClientContextRenderOptions(cli), + ); + + await findByText("Upload"); + await user.upload(container.querySelector("input")!, AVATAR_FILE); + + expect(cli.uploadContent).toHaveBeenCalledWith(AVATAR_FILE); + expect(setAvatarUrl).toHaveBeenCalledWith("mxc://example.com/1234"); + }); +}); diff --git a/test/unit-tests/components/views/location/__snapshots__/LocationViewDialog-test.tsx.snap b/test/unit-tests/components/views/location/__snapshots__/LocationViewDialog-test.tsx.snap index edd05cc260..36152bc0f4 100644 --- a/test/unit-tests/components/views/location/__snapshots__/LocationViewDialog-test.tsx.snap +++ b/test/unit-tests/components/views/location/__snapshots__/LocationViewDialog-test.tsx.snap @@ -13,9 +13,18 @@ exports[` renders map correctly 1`] = `
    -
    + fill="currentColor" + height="1em" + viewBox="0 0 24 24" + width="1em" + xmlns="http://www.w3.org/2000/svg" + > + +
    diff --git a/test/unit-tests/components/views/location/__snapshots__/Marker-test.tsx.snap b/test/unit-tests/components/views/location/__snapshots__/Marker-test.tsx.snap index e7fce5e5a2..635119d55c 100644 --- a/test/unit-tests/components/views/location/__snapshots__/Marker-test.tsx.snap +++ b/test/unit-tests/components/views/location/__snapshots__/Marker-test.tsx.snap @@ -9,9 +9,18 @@ exports[` renders with location icon when no room member 1`] = `
    -
    + fill="currentColor" + height="1em" + viewBox="0 0 24 24" + width="1em" + xmlns="http://www.w3.org/2000/svg" + > + +
    diff --git a/test/unit-tests/components/views/location/__snapshots__/SmartMarker-test.tsx.snap b/test/unit-tests/components/views/location/__snapshots__/SmartMarker-test.tsx.snap index 1e043c9db8..f2b3e4cc8b 100644 --- a/test/unit-tests/components/views/location/__snapshots__/SmartMarker-test.tsx.snap +++ b/test/unit-tests/components/views/location/__snapshots__/SmartMarker-test.tsx.snap @@ -9,9 +9,18 @@ exports[` creates a marker on mount 1`] = `
    -
    + fill="currentColor" + height="1em" + viewBox="0 0 24 24" + width="1em" + xmlns="http://www.w3.org/2000/svg" + > + +
    @@ -27,9 +36,18 @@ exports[` removes marker on unmount 1`] = `
    -
    + fill="currentColor" + height="1em" + viewBox="0 0 24 24" + width="1em" + xmlns="http://www.w3.org/2000/svg" + > + +
    diff --git a/test/unit-tests/components/views/messages/EncryptionEvent-test.tsx b/test/unit-tests/components/views/messages/EncryptionEvent-test.tsx index 3a78ef55e8..ca5f3d04b9 100644 --- a/test/unit-tests/components/views/messages/EncryptionEvent-test.tsx +++ b/test/unit-tests/components/views/messages/EncryptionEvent-test.tsx @@ -10,6 +10,7 @@ import React from "react"; import { mocked } from "jest-mock"; import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; import { render, screen } from "jest-matrix-react"; +import { waitFor } from "@testing-library/dom"; import EncryptionEvent from "../../../../../src/components/views/messages/EncryptionEvent"; import { createTestClient, mkMessage } from "../../../../test-utils"; @@ -55,17 +56,19 @@ describe("EncryptionEvent", () => { describe("for an encrypted room", () => { beforeEach(() => { event.event.content!.algorithm = algorithm; - mocked(client.isRoomEncrypted).mockReturnValue(true); + jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true); const room = new Room(roomId, client, client.getUserId()!); mocked(client.getRoom).mockReturnValue(room); }); - it("should show the expected texts", () => { + it("should show the expected texts", async () => { renderEncryptionEvent(client, event); - checkTexts( - "Encryption enabled", - "Messages in this room are end-to-end encrypted. " + - "When people join, you can verify them in their profile, just tap on their profile picture.", + await waitFor(() => + checkTexts( + "Encryption enabled", + "Messages in this room are end-to-end encrypted. " + + "When people join, you can verify them in their profile, just tap on their profile picture.", + ), ); }); @@ -76,9 +79,9 @@ describe("EncryptionEvent", () => { }); }); - it("should show the expected texts", () => { + it("should show the expected texts", async () => { renderEncryptionEvent(client, event); - checkTexts("Encryption enabled", "Some encryption parameters have been changed."); + await waitFor(() => checkTexts("Encryption enabled", "Some encryption parameters have been changed.")); }); }); @@ -87,36 +90,38 @@ describe("EncryptionEvent", () => { event.event.content!.algorithm = "unknown"; }); - it("should show the expected texts", () => { + it("should show the expected texts", async () => { renderEncryptionEvent(client, event); - checkTexts("Encryption enabled", "Ignored attempt to disable encryption"); + await waitFor(() => checkTexts("Encryption enabled", "Ignored attempt to disable encryption")); }); }); }); describe("for an unencrypted room", () => { beforeEach(() => { - mocked(client.isRoomEncrypted).mockReturnValue(false); + jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(false); renderEncryptionEvent(client, event); }); - it("should show the expected texts", () => { - expect(client.isRoomEncrypted).toHaveBeenCalledWith(roomId); - checkTexts("Encryption not enabled", "The encryption used by this room isn't supported."); + it("should show the expected texts", async () => { + expect(client.getCrypto()!.isEncryptionEnabledInRoom).toHaveBeenCalledWith(roomId); + await waitFor(() => + checkTexts("Encryption not enabled", "The encryption used by this room isn't supported."), + ); }); }); describe("for an encrypted local room", () => { beforeEach(() => { event.event.content!.algorithm = algorithm; - mocked(client.isRoomEncrypted).mockReturnValue(true); + jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true); const localRoom = new LocalRoom(roomId, client, client.getUserId()!); mocked(client.getRoom).mockReturnValue(localRoom); renderEncryptionEvent(client, event); }); it("should show the expected texts", () => { - expect(client.isRoomEncrypted).toHaveBeenCalledWith(roomId); + expect(client.getCrypto()!.isEncryptionEnabledInRoom).toHaveBeenCalledWith(roomId); checkTexts("Encryption enabled", "Messages in this chat will be end-to-end encrypted."); }); }); diff --git a/test/unit-tests/components/views/messages/__snapshots__/MLocationBody-test.tsx.snap b/test/unit-tests/components/views/messages/__snapshots__/MLocationBody-test.tsx.snap index 5a61ada30f..7b919b5326 100644 --- a/test/unit-tests/components/views/messages/__snapshots__/MLocationBody-test.tsx.snap +++ b/test/unit-tests/components/views/messages/__snapshots__/MLocationBody-test.tsx.snap @@ -49,9 +49,18 @@ exports[`MLocationBody without error renders map correctly 1`] =
    -
    + fill="currentColor" + height="1em" + viewBox="0 0 24 24" + width="1em" + xmlns="http://www.w3.org/2000/svg" + > + +
    diff --git a/test/unit-tests/components/views/rooms/EventTile/__snapshots__/EventTileThreadToolbar-test.tsx.snap b/test/unit-tests/components/views/rooms/EventTile/__snapshots__/EventTileThreadToolbar-test.tsx.snap index 133531b447..4597bd83bc 100644 --- a/test/unit-tests/components/views/rooms/EventTile/__snapshots__/EventTileThreadToolbar-test.tsx.snap +++ b/test/unit-tests/components/views/rooms/EventTile/__snapshots__/EventTileThreadToolbar-test.tsx.snap @@ -22,7 +22,17 @@ exports[`EventTileThreadToolbar renders 1`] = ` role="button" tabindex="-1" > -
    + + +
    diff --git a/test/unit-tests/components/views/rooms/RoomHeader-test.tsx b/test/unit-tests/components/views/rooms/RoomHeader-test.tsx index a7e556e452..1be9c77713 100644 --- a/test/unit-tests/components/views/rooms/RoomHeader-test.tsx +++ b/test/unit-tests/components/views/rooms/RoomHeader-test.tsx @@ -8,9 +8,19 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import { CallType, MatrixCall } from "matrix-js-sdk/src/webrtc/call"; -import { EventType, JoinRule, MatrixEvent, PendingEventOrdering, Room, RoomMember } from "matrix-js-sdk/src/matrix"; -import { KnownMembership } from "matrix-js-sdk/src/types"; import { + EventType, + JoinRule, + MatrixEvent, + PendingEventOrdering, + Room, + RoomStateEvent, + RoomMember, +} from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; +import { CryptoEvent, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api"; +import { + act, createEvent, fireEvent, getAllByLabelText, @@ -632,6 +642,52 @@ describe("RoomHeader", () => { expect(asFragment()).toMatchSnapshot(); }); + + it("updates the icon when the encryption status changes", async () => { + // The room starts verified + jest.spyOn(ShieldUtils, "shieldStatusForRoom").mockResolvedValue(ShieldUtils.E2EStatus.Verified); + render(, getWrapper()); + await waitFor(() => expect(getByLabelText(document.body, "Verified")).toBeInTheDocument()); + + // A new member joins, and the room becomes unverified + jest.spyOn(ShieldUtils, "shieldStatusForRoom").mockResolvedValue(ShieldUtils.E2EStatus.Warning); + act(() => { + room.emit( + RoomStateEvent.Members, + new MatrixEvent({ + event_id: "$event_id", + type: EventType.RoomMember, + state_key: "@alice:example.org", + content: { + membership: "join", + }, + room_id: ROOM_ID, + sender: "@alice:example.org", + }), + room.currentState, + new RoomMember(room.roomId, "@alice:example.org"), + ); + }); + await waitFor(() => expect(getByLabelText(document.body, "Untrusted")).toBeInTheDocument()); + + // The user becomes verified + jest.spyOn(ShieldUtils, "shieldStatusForRoom").mockResolvedValue(ShieldUtils.E2EStatus.Verified); + act(() => { + MatrixClientPeg.get()!.emit( + CryptoEvent.UserTrustStatusChanged, + "@alice:example.org", + new UserVerificationStatus(true, true, true, false), + ); + }); + await waitFor(() => expect(getByLabelText(document.body, "Verified")).toBeInTheDocument()); + + // An unverified device is added + jest.spyOn(ShieldUtils, "shieldStatusForRoom").mockResolvedValue(ShieldUtils.E2EStatus.Warning); + act(() => { + MatrixClientPeg.get()!.emit(CryptoEvent.DevicesUpdated, ["@alice:example.org"], false); + }); + await waitFor(() => expect(getByLabelText(document.body, "Untrusted")).toBeInTheDocument()); + }); }); it("renders additionalButtons", async () => { diff --git a/test/unit-tests/components/views/settings/AvatarSetting-test.tsx b/test/unit-tests/components/views/settings/AvatarSetting-test.tsx index e3e2b1cf96..1b88c416bc 100644 --- a/test/unit-tests/components/views/settings/AvatarSetting-test.tsx +++ b/test/unit-tests/components/views/settings/AvatarSetting-test.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ import React from "react"; -import { render, screen } from "jest-matrix-react"; +import { render, screen, fireEvent } from "jest-matrix-react"; import userEvent from "@testing-library/user-event"; import AvatarSetting from "../../../../../src/components/views/settings/AvatarSetting"; @@ -16,6 +16,9 @@ const BASE64_GIF = "R0lGODlhAQABAAAAACw="; const AVATAR_FILE = new File([Uint8Array.from(atob(BASE64_GIF), (c) => c.charCodeAt(0))], "avatar.gif", { type: "image/gif", }); +const GENERIC_FILE = new File([Uint8Array.from(atob(BASE64_GIF), (c) => c.charCodeAt(0))], "not-avatar.doc", { + type: "application/msword", +}); describe("", () => { beforeEach(() => { @@ -70,4 +73,45 @@ describe("", () => { expect(onChange).toHaveBeenCalledWith(AVATAR_FILE); }); + + it("should noop when selecting no file", async () => { + const onChange = jest.fn(); + + render( + , + ); + + const fileInput = screen.getByAltText("Upload"); + // Can't use userEvent.upload here as it doesn't support uploading invalid files + fireEvent.change(fileInput, { target: { files: [] } }); + + expect(onChange).not.toHaveBeenCalled(); + }); + + it("should show error if user tries to use non-image file", async () => { + const onChange = jest.fn(); + + render( + , + ); + + const fileInput = screen.getByAltText("Upload"); + // Can't use userEvent.upload here as it doesn't support uploading invalid files + fireEvent.change(fileInput, { target: { files: [GENERIC_FILE] } }); + + expect(onChange).not.toHaveBeenCalled(); + await expect(screen.findByRole("heading", { name: "Upload Failed" })).resolves.toBeInTheDocument(); + }); }); diff --git a/test/unit-tests/components/views/settings/tabs/user/__snapshots__/SidebarUserSettingsTab-test.tsx.snap b/test/unit-tests/components/views/settings/tabs/user/__snapshots__/SidebarUserSettingsTab-test.tsx.snap index 145210fa7b..cd87bbb165 100644 --- a/test/unit-tests/components/views/settings/tabs/user/__snapshots__/SidebarUserSettingsTab-test.tsx.snap +++ b/test/unit-tests/components/views/settings/tabs/user/__snapshots__/SidebarUserSettingsTab-test.tsx.snap @@ -135,7 +135,17 @@ exports[` renders sidebar settings with guest spa url
    -
    + + + Favourites
    renders sidebar settings with guest spa url
    -
    + + + + People
    renders sidebar settings without guest spa u
    -
    + + + Favourites
    renders sidebar settings without guest spa u
    -
    + + + + People
    { await setupEncryptionStore.resetConfirm(); - expect(mocked(accessSecretStorage)).toHaveBeenCalledWith(expect.any(Function), true); - expect(makeRequest).toHaveBeenCalledWith({ - identifier: { - type: "m.id.user", - user: "@userId:matrix.org", - }, - password: cachedPassword, - type: "m.login.password", - user: "@userId:matrix.org", + expect(mocked(accessSecretStorage)).toHaveBeenCalledWith(expect.any(Function), { + accountPassword: cachedPassword, + forceReset: true, + resetCrossSigning: true, }); }); }); diff --git a/test/unit-tests/stores/notifications/RoomNotificationState-test.ts b/test/unit-tests/stores/notifications/RoomNotificationState-test.ts index 5ebbe3f1ad..396bb06ec6 100644 --- a/test/unit-tests/stores/notifications/RoomNotificationState-test.ts +++ b/test/unit-tests/stores/notifications/RoomNotificationState-test.ts @@ -91,7 +91,7 @@ describe("RoomNotificationState", () => { const listener = jest.fn(); roomNotifState.addListener(NotificationStateEvents.Update, listener); const accountDataEvent = { - getType: () => "com.famedly.marked_unread", + getType: () => "m.marked_unread", getContent: () => { return { unread: true }; }, diff --git a/test/unit-tests/utils/arrays-test.ts b/test/unit-tests/utils/arrays-test.ts index 9bd8f15510..7e440d3ee5 100644 --- a/test/unit-tests/utils/arrays-test.ts +++ b/test/unit-tests/utils/arrays-test.ts @@ -23,6 +23,7 @@ import { concat, asyncEvery, asyncSome, + asyncSomeParallel, asyncFilter, } from "../../../src/utils/arrays"; @@ -462,6 +463,25 @@ describe("arrays", () => { }); }); + describe("asyncSomeParallel", () => { + it("when called with an empty array, it should return false", async () => { + expect(await asyncSomeParallel([], jest.fn().mockResolvedValue(true))).toBe(false); + }); + + it("when all the predicates return false", async () => { + expect(await asyncSomeParallel([1, 2, 3], jest.fn().mockResolvedValue(false))).toBe(false); + }); + + it("when all the predicates return true", async () => { + expect(await asyncSomeParallel([1, 2, 3], jest.fn().mockResolvedValue(true))).toBe(true); + }); + + it("when one of the predicate return true", async () => { + const predicate = jest.fn().mockImplementation((value) => Promise.resolve(value === 2)); + expect(await asyncSomeParallel([1, 2, 3], predicate)).toBe(true); + }); + }); + describe("asyncFilter", () => { it("when called with an empty array, it should return an empty array", async () => { expect(await asyncFilter([], jest.fn().mockResolvedValue(true))).toEqual([]); diff --git a/test/unit-tests/utils/notifications-test.ts b/test/unit-tests/utils/notifications-test.ts index 67948ed217..8e33575fec 100644 --- a/test/unit-tests/utils/notifications-test.ts +++ b/test/unit-tests/utils/notifications-test.ts @@ -270,7 +270,7 @@ describe("notifications", () => { // set true, no existing event it("sets unread flag if event doesn't exist", async () => { await setMarkedUnreadState(room, client, true); - expect(client.setRoomAccountData).toHaveBeenCalledWith(ROOM_ID, "com.famedly.marked_unread", { + expect(client.setRoomAccountData).toHaveBeenCalledWith(ROOM_ID, "m.marked_unread", { unread: true, }); }); @@ -287,7 +287,7 @@ describe("notifications", () => { .fn() .mockReturnValue({ getContent: jest.fn().mockReturnValue({ unread: false }) }); await setMarkedUnreadState(room, client, true); - expect(client.setRoomAccountData).toHaveBeenCalledWith(ROOM_ID, "com.famedly.marked_unread", { + expect(client.setRoomAccountData).toHaveBeenCalledWith(ROOM_ID, "m.marked_unread", { unread: true, }); }); @@ -316,7 +316,7 @@ describe("notifications", () => { .fn() .mockReturnValue({ getContent: jest.fn().mockReturnValue({ unread: true }) }); await setMarkedUnreadState(room, client, false); - expect(client.setRoomAccountData).toHaveBeenCalledWith(ROOM_ID, "com.famedly.marked_unread", { + expect(client.setRoomAccountData).toHaveBeenCalledWith(ROOM_ID, "m.marked_unread", { unread: false, }); }); diff --git a/webpack.config.js b/webpack.config.js index d35fb8e0d2..a664d7ea81 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -9,6 +9,7 @@ const TerserPlugin = require("terser-webpack-plugin"); const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); const HtmlWebpackInjectPreload = require("@principalstudio/html-webpack-inject-preload"); const CopyWebpackPlugin = require("copy-webpack-plugin"); +const VersionFilePlugin = require("webpack-version-file-plugin"); // Environment variables // RIOT_OG_IMAGE_URL: specifies the URL to the image which should be used for the opengraph logo. @@ -19,11 +20,6 @@ dotenv.config(); let ogImageUrl = process.env.RIOT_OG_IMAGE_URL; if (!ogImageUrl) ogImageUrl = "https://app.element.io/themes/element/img/logos/opengraph.png"; -if (!process.env.VERSION) { - console.warn("Unset VERSION variable - this may affect build output"); - process.env.VERSION = "!!UNSET!!"; -} - const cssThemes = { // CSS themes "theme-legacy-light": "./res/themes/legacy-light/css/legacy-light.pcss", @@ -97,6 +93,14 @@ module.exports = (env, argv) => { const devMode = nodeEnv !== "production"; const enableMinification = !devMode && !process.env.CI_PACKAGE; + let VERSION = process.env.VERSION; + if (!VERSION) { + VERSION = require("./package.json").version; + if (devMode) { + VERSION += "-dev"; + } + } + const development = {}; if (devMode) { // Embedded source maps for dev builds, can't use eval-source-map due to CSP @@ -651,8 +655,6 @@ module.exports = (env, argv) => { }, }), - new webpack.EnvironmentPlugin(["VERSION"]), - new CopyWebpackPlugin({ patterns: [ "res/apple-app-site-association", @@ -677,6 +679,15 @@ module.exports = (env, argv) => { Buffer: ["buffer", "Buffer"], process: "process/browser", }), + + // We bake the version in so the app knows its version immediately + new webpack.DefinePlugin({ "process.env.VERSION": JSON.stringify(VERSION) }), + // But we also write it to a file which gets polled for update detection + new VersionFilePlugin({ + outputFile: "version", + templateString: "<%= extras.VERSION %>", + extras: { VERSION }, + }), ].filter(Boolean), output: { diff --git a/yarn.lock b/yarn.lock index e8cd5406a8..c47cf82029 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2006,9 +2006,9 @@ integrity sha512-S7lOrndAK9/8qOtaTq/WhttJC/o4GAzdfK0MUPpo8ApzsJEC0QjtwrkC3KBXdFP1cD1MXi/mlKR7aaoVMKgs6Q== "@matrix-org/react-sdk-module-api@^2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@matrix-org/react-sdk-module-api/-/react-sdk-module-api-2.4.0.tgz#5e4552acbe728141f42c1d54d75dcb4efea9301c" - integrity sha512-cPb+YaqllfJkRX0ofcG/0YdHxCvcMAvUbdNMO2olpGL8vwbBP6mHdhbZ87z9pgsRIVOqfFuLUE3WeW0hxWrklQ== + version "2.5.0" + resolved "https://registry.yarnpkg.com/@matrix-org/react-sdk-module-api/-/react-sdk-module-api-2.5.0.tgz#df774d0ae0c327fbd40f8994bbb13ed35e26c337" + integrity sha512-l/SmiO47gPIRd6YJJGj+B6qbxyypJF6SEsfYr7j9rSW6E85ZYCqf+TpMM2LmfwZRADyKfCVkaJbbBZYpoD02VA== dependencies: "@babel/runtime" "^7.17.9" @@ -3414,15 +3414,15 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@vector-im/compound-design-tokens@^1.8.0": - version "1.9.1" - resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-1.9.1.tgz#644dc7ca5ca251fd476af2a7c075e9d740c08871" - integrity sha512-zjI+PhoNLNrJrLU8whEGjzCuxdqIz6tM0ARYBMS8AG1vC+NlGak6Y21TWnzHT3VINNhnF+PiQ9lFWsU65GydOg== +"@vector-im/compound-design-tokens@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-2.0.1.tgz#add14494caab16cdbe98f2bdabe726908739def4" + integrity sha512-4nkPcrPII+sejispn+UkWZYFN7LecN39e4WGBupdceiMq0NJrfXrnVtJ9/6BDLgSqHInb6R/IWQkIbPbzfqRMg== -"@vector-im/compound-web@^7.1.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-7.2.0.tgz#0ec4a598e5755cc4b3e83fbc232a4986a12bf808" - integrity sha512-wOT2kSo936FSBG1CsZ1vmHLwTTWBq9OBBfq76sM95rUawRSQCCWnjFMLTiacRvxBHucZaSNsfhpJH3oZcrOexw== +"@vector-im/compound-web@^7.3.0": + version "7.3.0" + resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-7.3.0.tgz#9594113ac50bff4794715104a30a60c52d15517d" + integrity sha512-gDppQUtpk5LvNHUg+Zlv9qzo1iBAag0s3g8Ec0qS5q4zGBKG6ruXXrNUKg1aK8rpbo2hYQsGaHM6dD8NqLoq3Q== dependencies: "@floating-ui/react" "^0.26.24" "@radix-ui/react-context-menu" "^2.2.1" @@ -3934,6 +3934,11 @@ astral-regex@^2.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== +async@^3.2.3: + version "3.2.6" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" + integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -4332,7 +4337,7 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: +chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -4745,9 +4750,9 @@ cronstrue@^2.41.0: integrity sha512-ULYhWIonJzlScCCQrPUG5uMXzXxSixty4djud9SS37DoNxDdkeRocxzHuAo4ImRBUK+mAuU5X9TSwEDccnnuPg== cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + version "7.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.5.tgz#910aac880ff5243da96b728bc6521a5f6c2f2f82" + integrity sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug== dependencies: path-key "^3.1.0" shebang-command "^2.0.0" @@ -5365,6 +5370,13 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== +ejs@^3.1.8: + version "3.1.10" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b" + integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA== + dependencies: + jake "^10.8.5" + electron-to-chromium@^1.5.41: version "1.5.56" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.56.tgz#3213f369efc3a41091c3b2c05bc0f406108ac1df" @@ -6188,6 +6200,13 @@ file@^0.2.2: resolved "https://registry.yarnpkg.com/file/-/file-0.2.2.tgz#c3dfd8f8cf3535ae455c2b423c2e52635d76b4d3" integrity sha512-gwabMtChzdnpDJdPEpz8Vr/PX0pU85KailuPV71Zw/un5yJVKvzukhB3qf6O3lnTwIe5CxlMYLh3jOK3w5xrLA== +filelist@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" + integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q== + dependencies: + minimatch "^5.0.1" + filesize@10.1.6: version "10.1.6" resolved "https://registry.yarnpkg.com/filesize/-/filesize-10.1.6.tgz#31194da825ac58689c0bce3948f33ce83aabd361" @@ -6342,6 +6361,11 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== +fs@latest: + version "0.0.1-security" + resolved "https://registry.yarnpkg.com/fs/-/fs-0.0.1-security.tgz#8a7bd37186b6dddf3813f23858b57ecaaf5e41d4" + integrity sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w== + fsevents@2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" @@ -7362,6 +7386,16 @@ jackspeak@^4.0.1: dependencies: "@isaacs/cliui" "^8.0.2" +jake@^10.8.5: + version "10.9.2" + resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.2.tgz#6ae487e6a69afec3a5e167628996b59f35ae2b7f" + integrity sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA== + dependencies: + async "^3.2.3" + chalk "^4.0.2" + filelist "^1.0.4" + minimatch "^3.1.2" + jest-canvas-mock@^2.5.2: version "2.5.2" resolved "https://registry.yarnpkg.com/jest-canvas-mock/-/jest-canvas-mock-2.5.2.tgz#7e21ebd75e05ab41c890497f6ba8a77f915d2ad6" @@ -8331,7 +8365,7 @@ matrix-events-sdk@0.0.1: jwt-decode "^4.0.0" loglevel "^1.7.1" matrix-events-sdk "0.0.1" - matrix-widget-api "^1.8.2" + matrix-widget-api "^1.10.0" oidc-client-ts "^3.0.1" p-retry "4" sdp-transform "^2.14.1" @@ -8519,6 +8553,13 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" +minimatch@^5.0.1: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + minimatch@^8.0.2: version "8.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-8.0.4.tgz#847c1b25c014d4e9a7f68aaf63dedd668a626229" @@ -10344,7 +10385,7 @@ resolve.exports@^2.0.0: resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800" integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg== -resolve@^1.1.7, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.20.0, resolve@^1.22.4: +resolve@^1.1.7, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.20.0, resolve@^1.22.4, resolve@^1.22.8: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== @@ -11086,6 +11127,14 @@ stylelint-scss@^6.0.0: postcss-selector-parser "^6.1.2" postcss-value-parser "^4.2.0" +stylelint-value-no-unknown-custom-properties@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/stylelint-value-no-unknown-custom-properties/-/stylelint-value-no-unknown-custom-properties-6.0.1.tgz#526cc20344f4fc5e33231152767a432b6ed8f957" + integrity sha512-N60PTdaTknB35j6D4FhW0GL2LlBRV++bRpXMMldWMQZ240yFQaoltzlLY4lXXs7Z0J5mNUYZQ/gjyVtU2DhCMA== + dependencies: + postcss-value-parser "^4.2.0" + resolve "^1.22.8" + stylelint@^16.1.0: version "16.10.0" resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-16.10.0.tgz#452b42a5d82f2ad910954eb2ba2b3a2ec583cd75" @@ -11566,6 +11615,11 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +underscore@^1.13.6: + version "1.13.7" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.7.tgz#970e33963af9a7dda228f17ebe8399e5fbe63a10" + integrity sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g== + undici-types@~5.26.4: version "5.26.5" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" @@ -11942,6 +11996,15 @@ webpack-sources@^3.2.3: resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== +webpack-version-file-plugin@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/webpack-version-file-plugin/-/webpack-version-file-plugin-0.5.0.tgz#1711baafc06da373f3bb95de86cad831e00217e1" + integrity sha512-Ef5gGkD3OPtXU794XNt6JNzIv1dYmTqN3SfY25qRNg6/auOXGF4XBpPnisO9mJTUbIgBFcSEiV74uXJlrL0xfg== + dependencies: + ejs "^3.1.8" + fs latest + underscore "^1.13.6" + webpack-virtual-modules@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz#362f14738a56dae107937ab98ea7062e8bdd3b6c"