);
-
- /**
- * Created by {@link createRef}, or {@link useRef} when passed `null`.
-@@ -941,7 +941,7 @@ declare namespace React {
- context: unknown;
-
- // Keep in sync with constructor signature of JSXElementConstructor and ComponentClass.
-- constructor(props: P);
-+ constructor(props: P, context?: unknown);
-
- // We MUST keep setState() as a unified signature because it allows proper checking of the method return type.
- // See: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment-351013257
-@@ -1113,7 +1113,7 @@ declare namespace React {
- */
- interface ComponentClass extends StaticLifecycle
{
- // constructor signature must match React.Component
-- new(props: P): Component
;
-+ new(props: P, context?: any): Component
;
- /**
- * Ignored by React.
- * @deprecated Only kept in types for backwards compatibility. Will be removed in a future major release.
diff --git a/res/apple-app-site-association b/res/apple-app-site-association
index 94869effabb..0235e6ada1e 100644
--- a/res/apple-app-site-association
+++ b/res/apple-app-site-association
@@ -5,8 +5,7 @@
"appIDs": [
"7J4U792NQT.im.vector.app",
"7J4U792NQT.io.element.elementx",
- "7J4U792NQT.io.element.elementx.nightly",
- "7J4U792NQT.io.element.elementx.pr"
+ "7J4U792NQT.io.element.elementx.nightly"
],
"components": [
{
@@ -28,8 +27,7 @@
"apps": [
"7J4U792NQT.im.vector.app",
"7J4U792NQT.io.element.elementx",
- "7J4U792NQT.io.element.elementx.nightly",
- "7J4U792NQT.io.element.elementx.pr"
+ "7J4U792NQT.io.element.elementx.nightly"
]
}
}
diff --git a/res/css/_common.pcss b/res/css/_common.pcss
index 18753d98ea8..83b6f41c52a 100644
--- a/res/css/_common.pcss
+++ b/res/css/_common.pcss
@@ -624,6 +624,7 @@ legend {
.mx_Dialog
button:not(
.mx_EncryptionUserSettingsTab button,
+ .mx_EncryptionCard button,
.mx_UserProfileSettings button,
.mx_ShareDialog button,
.mx_UnpinAllDialog button,
@@ -631,6 +632,7 @@ legend {
.mx_Dialog_nonDialogButton,
.mx_AccessibleButton,
.mx_IdentityServerPicker button,
+ .mx_AccessSecretStorageDialog button,
[class|="maplibregl"]
),
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton, .mx_AccessibleButton),
diff --git a/res/css/_components.pcss b/res/css/_components.pcss
index 277aa8cd3ab..57e14e0814b 100644
--- a/res/css/_components.pcss
+++ b/res/css/_components.pcss
@@ -53,8 +53,6 @@
@import "./components/views/settings/shared/_SettingsSubsectionHeading.pcss";
@import "./components/views/spaces/_QuickThemeSwitcher.pcss";
@import "./components/views/typography/_Caption.pcss";
-@import "./components/views/utils/_Box.pcss";
-@import "./components/views/utils/_Flex.pcss";
@import "./compound/_Icon.pcss";
@import "./compound/_SuccessDialog.pcss";
@import "./structures/_AutoHideScrollbar.pcss";
@@ -99,7 +97,6 @@
@import "./structures/auth/_Registration.pcss";
@import "./structures/auth/_SessionLockStolenView.pcss";
@import "./structures/auth/_SetupEncryptionBody.pcss";
-@import "./views/audio_messages/_AudioPlayer.pcss";
@import "./views/audio_messages/_PlayPauseButton.pcss";
@import "./views/audio_messages/_PlaybackContainer.pcss";
@import "./views/audio_messages/_SeekBar.pcss";
@@ -133,6 +130,7 @@
@import "./views/dialogs/_BugReportDialog.pcss";
@import "./views/dialogs/_ChangelogDialog.pcss";
@import "./views/dialogs/_CompoundDialog.pcss";
+@import "./views/dialogs/_ConfirmKeyStorageOffDialog.pcss";
@import "./views/dialogs/_ConfirmSpaceUserActionDialog.pcss";
@import "./views/dialogs/_ConfirmUserActionDialog.pcss";
@import "./views/dialogs/_CreateRoomDialog.pcss";
@@ -146,6 +144,7 @@
@import "./views/dialogs/_GenericFeatureFeedbackDialog.pcss";
@import "./views/dialogs/_IncomingSasDialog.pcss";
@import "./views/dialogs/_InviteDialog.pcss";
+@import "./views/dialogs/_InviteProgressBody.pcss";
@import "./views/dialogs/_JoinRuleDropdown.pcss";
@import "./views/dialogs/_LeaveSpaceDialog.pcss";
@import "./views/dialogs/_LocationViewDialog.pcss";
@@ -178,7 +177,6 @@
@import "./views/dialogs/_WidgetCapabilitiesPromptDialog.pcss";
@import "./views/dialogs/security/_AccessSecretStorageDialog.pcss";
@import "./views/dialogs/security/_CreateCrossSigningDialog.pcss";
-@import "./views/dialogs/security/_CreateKeyBackupDialog.pcss";
@import "./views/dialogs/security/_CreateSecretStorageDialog.pcss";
@import "./views/dialogs/security/_KeyBackupFailedDialog.pcss";
@import "./views/dialogs/security/_RestoreKeyBackupDialog.pcss";
@@ -282,6 +280,7 @@
@import "./views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss";
@import "./views/rooms/RoomListPanel/_RoomListSearch.pcss";
@import "./views/rooms/RoomListPanel/_RoomListSecondaryFilters.pcss";
+@import "./views/rooms/RoomListPanel/_RoomListSkeleton.pcss";
@import "./views/rooms/_AppsDrawer.pcss";
@import "./views/rooms/_Autocomplete.pcss";
@import "./views/rooms/_AuxPanel.pcss";
diff --git a/res/css/_font-sizes.pcss b/res/css/_font-sizes.pcss
index 528cc3c4621..98ebf28af0d 100644
--- a/res/css/_font-sizes.pcss
+++ b/res/css/_font-sizes.pcss
@@ -12,31 +12,39 @@ Please see LICENSE files in the repository root for full details.
* These are defined in `rem` so that they scale with the `font-size` of the root element (which is adjustable via the
* "Font size" setting). They exist to make the job of converting designs (which tend to be based in pixels) into CSS
* easier.
+ */
+
+/*
+ * These variables are now *deprecated* and should not be used in new code; instead Compound typographic tokens
+ * should be used. Direct equivalents for these old font size tokens are listed below; where no equivalent exists,
+ * that suggests that the design is using a non-standard font size and should be updated.
*
+ * In fact, modern Figma designs should actually use a named Typography style such as "Web/font/heading/sm/semibold",
+ * translates directly to `font: var(--cpd-font-heading-sm-semibold)`.
*/
$font-1px: 0.0625rem;
$font-8px: 0.5rem;
$font-9px: 0.5625rem;
$font-10px: 0.625rem;
$font-10-4px: 0.6275rem;
-$font-11px: 0.6875rem;
+$font-11px: 0.6875rem; /* Compound equivalent: --cpd-font-size-body-xs */
$font-12px: 0.75rem;
-$font-13px: 0.8125rem;
+$font-13px: 0.8125rem; /* Compound equivalent: --cpd-font-size-body-sm */
$font-14px: 0.875rem;
-$font-15px: 0.9375rem;
+$font-15px: 0.9375rem; /* Compound equivalent: --cpd-font-size-body-md */
$font-16px: 1rem;
-$font-17px: 1.0625rem;
+$font-17px: 1.0625rem; /* Compound equivalent: --cpd-font-size-body-lg */
$font-18px: 1.125rem;
-$font-20px: 1.25rem;
+$font-20px: 1.25rem; /* Compound equivalent: --cpd-font-size-heading-sm */
$font-22px: 1.375rem;
$font-23px: 1.4375rem;
-$font-24px: 1.5rem;
+$font-24px: 1.5rem; /* Compound equivalent: --cpd-font-size-heading-md */
$font-25px: 1.5625rem;
$font-26px: 1.625rem;
-$font-28px: 1.75rem;
+$font-28px: 1.75rem; /* Compound equivalent: --cpd-font-size-heading-lg */
$font-29px: 1.8125rem;
$font-30px: 1.875rem;
-$font-32px: 2rem;
+$font-32px: 2rem; /* Compound equivalent: --cpd-font-size-heading-xl */
$font-34px: 2.125rem;
$font-35px: 2.1875rem;
$font-39px: 2.4375rem;
diff --git a/res/css/components/views/dialogs/polls/_PollListItem.pcss b/res/css/components/views/dialogs/polls/_PollListItem.pcss
index 6cb46a21d2d..cd24c759376 100644
--- a/res/css/components/views/dialogs/polls/_PollListItem.pcss
+++ b/res/css/components/views/dialogs/polls/_PollListItem.pcss
@@ -15,7 +15,7 @@ Please see LICENSE files in the repository root for full details.
display: grid;
justify-content: left;
align-items: center;
- grid-gap: $spacing-8;
+ gap: $spacing-8;
grid-template-columns: auto auto auto;
grid-template-rows: auto;
cursor: pointer;
diff --git a/res/css/components/views/dialogs/polls/_PollListItemEnded.pcss b/res/css/components/views/dialogs/polls/_PollListItemEnded.pcss
index 772b47c9a49..2eb7a185ac4 100644
--- a/res/css/components/views/dialogs/polls/_PollListItemEnded.pcss
+++ b/res/css/components/views/dialogs/polls/_PollListItemEnded.pcss
@@ -22,7 +22,7 @@ Please see LICENSE files in the repository root for full details.
display: grid;
justify-content: left;
align-items: center;
- grid-gap: $spacing-8;
+ gap: $spacing-8;
grid-template-columns: min-content 1fr min-content;
grid-template-rows: auto;
}
@@ -47,7 +47,7 @@ Please see LICENSE files in the repository root for full details.
.mx_PollListItemEnded_answers {
display: grid;
- grid-gap: $spacing-8;
+ gap: $spacing-8;
margin-top: $spacing-12;
}
diff --git a/res/css/components/views/settings/devices/_DeviceDetailHeading.pcss b/res/css/components/views/settings/devices/_DeviceDetailHeading.pcss
index 789efa9e7f8..82b19b9ff13 100644
--- a/res/css/components/views/settings/devices/_DeviceDetailHeading.pcss
+++ b/res/css/components/views/settings/devices/_DeviceDetailHeading.pcss
@@ -19,7 +19,7 @@ Please see LICENSE files in the repository root for full details.
.mx_DeviceDetailHeading_renameForm {
display: grid;
- grid-gap: $spacing-16;
+ gap: $spacing-16;
justify-content: left;
grid-template-columns: 100%;
}
diff --git a/res/css/components/views/settings/devices/_DeviceDetails.pcss b/res/css/components/views/settings/devices/_DeviceDetails.pcss
index d3635710f3a..4b311d1c7c2 100644
--- a/res/css/components/views/settings/devices/_DeviceDetails.pcss
+++ b/res/css/components/views/settings/devices/_DeviceDetails.pcss
@@ -23,7 +23,7 @@ Please see LICENSE files in the repository root for full details.
border-bottom: 1px solid $quinary-content;
display: grid;
- grid-gap: $spacing-24;
+ gap: $spacing-24;
justify-content: left;
grid-template-columns: 100%;
diff --git a/res/css/components/views/settings/devices/_DeviceTile.pcss b/res/css/components/views/settings/devices/_DeviceTile.pcss
index e4096329d6a..07ee70792d8 100644
--- a/res/css/components/views/settings/devices/_DeviceTile.pcss
+++ b/res/css/components/views/settings/devices/_DeviceTile.pcss
@@ -35,7 +35,7 @@ Please see LICENSE files in the repository root for full details.
.mx_DeviceTile_actions {
display: grid;
- grid-gap: $spacing-8;
+ gap: $spacing-8;
grid-auto-flow: column;
margin-left: $spacing-8;
}
diff --git a/res/css/components/views/settings/devices/_FilteredDeviceList.pcss b/res/css/components/views/settings/devices/_FilteredDeviceList.pcss
index aac5986280e..06f5a80b651 100644
--- a/res/css/components/views/settings/devices/_FilteredDeviceList.pcss
+++ b/res/css/components/views/settings/devices/_FilteredDeviceList.pcss
@@ -15,7 +15,7 @@ Please see LICENSE files in the repository root for full details.
.mx_FilteredDeviceList_list {
list-style-type: none;
display: grid;
- grid-gap: $spacing-16;
+ gap: $spacing-16;
margin: 0;
padding: 0 $spacing-16;
}
diff --git a/res/css/components/views/settings/shared/_SettingsSubsection.pcss b/res/css/components/views/settings/shared/_SettingsSubsection.pcss
index 0d03a12b1db..3b22c679c5e 100644
--- a/res/css/components/views/settings/shared/_SettingsSubsection.pcss
+++ b/res/css/components/views/settings/shared/_SettingsSubsection.pcss
@@ -39,7 +39,7 @@ Please see LICENSE files in the repository root for full details.
.mx_SettingsSubsection_content {
width: 100%;
display: grid;
- grid-gap: $spacing-8;
+ gap: $spacing-8;
/* setting minwidth 0 makes columns definitely sized fixing horizontal overflow */
grid-template-columns: minmax(0, 1fr);
justify-items: flex-start;
diff --git a/res/css/shared.pcss b/res/css/shared.pcss
new file mode 100644
index 00000000000..42f83936668
--- /dev/null
+++ b/res/css/shared.pcss
@@ -0,0 +1,9 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+@import url("@vector-im/compound-design-tokens/assets/web/css/compound-design-tokens.css") layer(compound);
+@import url("@vector-im/compound-web/dist/style.css");
diff --git a/res/css/structures/ErrorView.pcss b/res/css/structures/ErrorView.pcss
index ddc510e1882..805456bdbbc 100644
--- a/res/css/structures/ErrorView.pcss
+++ b/res/css/structures/ErrorView.pcss
@@ -50,7 +50,7 @@ Please see LICENSE files in the repository root for full details.
color: var(--cpd-color-text-secondary);
}
- .mx_Flex {
+ .mx_ErrorView_flexContainer {
margin: 0 auto;
max-width: max-content;
flex-wrap: wrap;
diff --git a/res/css/structures/_LeftPanel.pcss b/res/css/structures/_LeftPanel.pcss
index 7b8a6e273b1..97e9f20e3f3 100644
--- a/res/css/structures/_LeftPanel.pcss
+++ b/res/css/structures/_LeftPanel.pcss
@@ -244,3 +244,11 @@ Please see LICENSE files in the repository root for full details.
}
}
}
+
+.mx_LeftPanel_newRoomList {
+ /* Thew new rooms list is not designed to be collapsed to just icons. */
+ /* 224 + 68(spaces bar) was deemed by design to be a good minimum for the left panel. */
+ --collapsedWidth: 224px;
+ /* Important to force the color on ED titlebar until we remove the old room list */
+ background-color: var(--cpd-color-bg-canvas-default) !important;
+}
diff --git a/res/css/structures/_SpacePanel.pcss b/res/css/structures/_SpacePanel.pcss
index 844f48bcc98..1a29d0eaa5f 100644
--- a/res/css/structures/_SpacePanel.pcss
+++ b/res/css/structures/_SpacePanel.pcss
@@ -30,6 +30,11 @@ Please see LICENSE files in the repository root for full details.
width: 68px;
}
+ &.newUi {
+ background-color: var(--cpd-color-bg-canvas-default);
+ border-right: 1px solid var(--cpd-color-bg-subtle-primary);
+ }
+
.mx_SpacePanel_toggleCollapse {
position: absolute;
width: 18px;
@@ -400,6 +405,11 @@ Please see LICENSE files in the repository root for full details.
}
}
+ &.newUi .mx_UserMenu {
+ margin-top: var(--cpd-space-4x);
+ border-bottom: none;
+ }
+
/* elecord, rpc */
.mx_UserRPC {
padding-bottom: 12px;
diff --git a/res/css/structures/_ThreadsActivityCentre.pcss b/res/css/structures/_ThreadsActivityCentre.pcss
index a1472108ac9..f26139da64c 100644
--- a/res/css/structures/_ThreadsActivityCentre.pcss
+++ b/res/css/structures/_ThreadsActivityCentre.pcss
@@ -49,12 +49,12 @@
&:hover,
&:hover .mx_ThreadsActivityCentreButton_Icon {
background-color: $quaternary-content;
- color: $primary-content;
+ fill: $primary-content;
}
}
& .mx_ThreadsActivityCentreButton_Icon {
- color: $secondary-content;
+ fill: $secondary-content;
}
}
diff --git a/res/css/structures/_ToastContainer.pcss b/res/css/structures/_ToastContainer.pcss
index 3f36e100b18..37584ffffca 100644
--- a/res/css/structures/_ToastContainer.pcss
+++ b/res/css/structures/_ToastContainer.pcss
@@ -79,6 +79,11 @@ Please see LICENSE files in the repository root for full details.
background-color: $primary-content;
}
+ &.mx_Toast_icon_key_storage::after {
+ mask-image: url("@vector-im/compound-design-tokens/icons/settings-solid.svg");
+ background-color: $primary-content;
+ }
+
&.mx_Toast_icon_labs::after {
mask-image: url("$(res)/img/element-icons/flask.svg");
background-color: $secondary-content;
diff --git a/res/css/views/audio_messages/_AudioPlayer.pcss b/res/css/views/audio_messages/_AudioPlayer.pcss
deleted file mode 100644
index 51e97611f5a..00000000000
--- a/res/css/views/audio_messages/_AudioPlayer.pcss
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
-Copyright 2024 New Vector Ltd.
-Copyright 2021 The Matrix.org Foundation C.I.C.
-
-SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
-Please see LICENSE files in the repository root for full details.
-*/
-
-.mx_MediaBody.mx_AudioPlayer_container {
- padding: 16px 12px 12px 12px;
-
- .mx_AudioPlayer_primaryContainer {
- display: flex;
-
- .mx_PlayPauseButton {
- margin-right: 8px;
- }
-
- .mx_AudioPlayer_mediaInfo {
- flex: 1;
- overflow: hidden; /* makes the ellipsis on the file name work */
-
- & > * {
- display: block;
- }
-
- .mx_AudioPlayer_mediaName {
- color: $primary-content;
- font-size: $font-15px;
- line-height: $font-15px;
- text-overflow: ellipsis;
- white-space: nowrap;
- overflow: hidden;
- padding-bottom: 4px; /* mimics the line-height differences in the Figma */
- }
-
- .mx_AudioPlayer_byline {
- font-size: $font-12px;
- line-height: $font-12px;
- }
- }
- }
-
- .mx_AudioPlayer_seek {
- display: flex;
- align-items: center;
-
- .mx_SeekBar {
- flex: 1;
- }
-
- .mx_Clock {
- min-width: $font-42px; /* for flexbox */
- padding-left: $spacing-4; /* isolate from seek bar */
- text-align: justify;
- white-space: nowrap;
- }
- }
-}
diff --git a/res/css/views/dialogs/_ConfirmKeyStorageOffDialog.pcss b/res/css/views/dialogs/_ConfirmKeyStorageOffDialog.pcss
new file mode 100644
index 00000000000..5ac53c7b706
--- /dev/null
+++ b/res/css/views/dialogs/_ConfirmKeyStorageOffDialog.pcss
@@ -0,0 +1,16 @@
+/*
+Copyright 2025 New Vector Ltd.
+
+SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+Please see LICENSE files in the repository root for full details.
+*/
+
+.mx_ConfirmKeyStorageOffDialog {
+ .mx_Dialog_border {
+ width: 600px;
+ }
+
+ .mx_EncryptionCard {
+ text-align: center;
+ }
+}
diff --git a/res/css/views/dialogs/_InviteDialog.pcss b/res/css/views/dialogs/_InviteDialog.pcss
index 70a8cdc6087..0f952049cf5 100644
--- a/res/css/views/dialogs/_InviteDialog.pcss
+++ b/res/css/views/dialogs/_InviteDialog.pcss
@@ -63,17 +63,6 @@ Please see LICENSE files in the repository root for full details.
height: 25px;
line-height: $font-25px;
}
-
- .mx_InviteDialog_buttonAndSpinner {
- .mx_Spinner {
- /* Width and height are required to trick the layout engine. */
- width: 20px;
- height: 20px;
- margin-inline-start: 5px;
- display: inline-block;
- vertical-align: middle;
- }
- }
}
.mx_InviteDialog_section {
@@ -218,6 +207,10 @@ Please see LICENSE files in the repository root for full details.
flex-direction: column;
flex-grow: 1;
overflow: hidden;
+
+ .mx_InviteProgressBody {
+ margin-top: var(--cpd-space-12x);
+ }
}
.mx_InviteDialog_transfer {
diff --git a/res/css/views/dialogs/_InviteProgressBody.pcss b/res/css/views/dialogs/_InviteProgressBody.pcss
new file mode 100644
index 00000000000..e3069a133c0
--- /dev/null
+++ b/res/css/views/dialogs/_InviteProgressBody.pcss
@@ -0,0 +1,16 @@
+/*
+Copyright 2025 New Vector Ltd.
+
+SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+Please see LICENSE files in the repository root for full details.
+*/
+
+.mx_InviteProgressBody {
+ text-align: center;
+ font: var(--cpd-font-body-lg-regular);
+
+ h1 {
+ color: var(--cpd-color-text-primary);
+ font: var(--cpd-font-heading-sm-semibold);
+ }
+}
diff --git a/res/css/views/dialogs/_SettingsDialog.pcss b/res/css/views/dialogs/_SettingsDialog.pcss
index 186a82c0f5d..2b65bff63ba 100644
--- a/res/css/views/dialogs/_SettingsDialog.pcss
+++ b/res/css/views/dialogs/_SettingsDialog.pcss
@@ -30,4 +30,28 @@ Please see LICENSE files in the repository root for full details.
/* colliding harshly with the dialog when scrolled down. */
padding-bottom: 100px;
}
+
+ .mx_SettingsDialog_tabLabelsAlert::after {
+ display: inline-block;
+ content: "";
+ width: 8px;
+ height: 8px;
+ background-color: var(--cpd-color-icon-critical-primary);
+ clip-path: circle(4px);
+ position: absolute;
+ right: var(--cpd-space-4x);
+ }
+}
+
+/* On narrow viewports, the tab labels are hidden, so we need to shift the indicator so it isn't over the tab icon. */
+@media (max-width: 1024px) {
+ .mx_UserSettingsDialog,
+ .mx_RoomSettingsDialog,
+ .mx_SpaceSettingsDialog,
+ .mx_SpacePreferencesDialog {
+ .mx_SettingsDialog_tabLabelsAlert::after {
+ right: var(--cpd-space-1x);
+ top: var(--cpd-space-1x);
+ }
+ }
}
diff --git a/res/css/views/dialogs/_SpotlightDialog.pcss b/res/css/views/dialogs/_SpotlightDialog.pcss
index d00acd6786d..592431c2f19 100644
--- a/res/css/views/dialogs/_SpotlightDialog.pcss
+++ b/res/css/views/dialogs/_SpotlightDialog.pcss
@@ -412,7 +412,8 @@ Please see LICENSE files in the repository root for full details.
.mx_SpotlightDialog_joinRoomAlias,
.mx_SpotlightDialog_explorePublicRooms,
.mx_SpotlightDialog_explorePublicSpaces,
- .mx_SpotlightDialog_startGroupChat {
+ .mx_SpotlightDialog_startGroupChat,
+ .mx_SpotlightDialog_searchMessages {
padding-left: $spacing-32;
position: relative;
@@ -451,22 +452,14 @@ Please see LICENSE files in the repository root for full details.
mask-image: url("$(res)/img/element-icons/group-members.svg");
}
+ .mx_SpotlightDialog_searchMessages::before {
+ mask-image: url("$(res)/img/element-icons/room/search-inset.svg");
+ }
+
.mx_SpotlightDialog_otherSearches_messageSearchText {
font-size: $font-15px;
line-height: $font-24px;
}
-
- .mx_SpotlightDialog_otherSearches_messageSearchIcon {
- display: inline-block;
- width: 24px;
- height: 24px;
- background-color: $secondary-content;
- vertical-align: text-bottom;
- mask-repeat: no-repeat;
- mask-position: center;
- mask-size: contain;
- mask-image: url("$(res)/img/element-icons/room/search-inset.svg");
- }
}
.mx_SpotlightDialog_result_details {
diff --git a/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss b/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss
index 16962ad15e5..3de035ab305 100644
--- a/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss
+++ b/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss
@@ -7,32 +7,11 @@ Please see LICENSE files in the repository root for full details.
*/
.mx_AccessSecretStorageDialog {
- .mx_AccessSecretStorageDialog_titleWithIcon {
- &::before {
- content: "";
- display: inline-block;
- width: 24px;
- height: 24px;
- margin-inline-end: $spacing-8;
- position: relative;
- top: 5px;
- background-color: $primary-content;
- }
-
- &.mx_AccessSecretStorageDialog_resetBadge::before {
- /* The image isn't capable of masking, so we use a background instead. */
- background-image: url("@vector-im/compound-design-tokens/icons/error-solid.svg");
- background-size: 24px;
- background-color: transparent;
- }
-
- &.mx_AccessSecretStorageDialog_secureBackupTitle::before {
- mask-image: url("$(res)/img/feather-customised/secure-backup.svg");
- }
-
- &.mx_AccessSecretStorageDialog_securePhraseTitle::before {
- mask-image: url("$(res)/img/feather-customised/secure-phrase.svg");
- }
+ &.mx_EncryptionCard {
+ /* override some styles that we don't need */
+ border: 0px none;
+ box-shadow: none;
+ padding: 0px;
}
.mx_AccessSecretStorageDialog_primaryContainer {
@@ -47,19 +26,19 @@ Please see LICENSE files in the repository root for full details.
}
.mx_AccessSecretStorageDialog_recoveryKeyEntry {
- display: flex;
- align-items: center;
-
- .mx_AccessSecretStorageDialog_recoveryKeyEntry_textInput {
- flex-grow: 1;
- }
-
- .mx_AccessSecretStorageDialog_recoveryKeyEntry_entryControlSeparatorText {
- margin: $spacing-16;
- }
-
- .mx_AccessSecretStorageDialog_recoveryKeyEntry_fileInput {
- display: none;
+ /*
+ * Be specific here to avoid "margin: 9px" from _common.pcss
+ */
+ :not(.mx_textinput):not(.mx_Field):not(.mx_no_textinput) {
+ input {
+ /*
+ * From figma: https://www.figma.com/design/ZodBLtGnKmRTGJo5SGLnH3/ER-137--Excluding-Insecure-Devices?node-id=102-43729&t=QmewENUd7f6Tmw9U-1
+ */
+ width: 448px;
+ height: 70px;
+ margin: 0px;
+ border: 1px solid;
+ }
}
}
@@ -89,54 +68,14 @@ Please see LICENSE files in the repository root for full details.
color: $alert;
&::before {
- mask-image: url("@vector-im/compound-design-tokens/icons/close.svg");
+ mask-image: url("@vector-im/compound-design-tokens/icons/error-solid.svg");
background-color: $alert;
}
}
}
+ }
- .mx_Dialog_buttons {
- $spacingStart: $spacing-24; /* 16px icon + 8px padding */
-
- text-align: initial;
- display: flex;
- flex-flow: column;
- gap: 14px;
-
- .mx_Dialog_buttons_additive {
- float: none;
-
- .mx_AccessSecretStorageDialog_reset {
- position: relative;
- padding-inline-start: $spacingStart;
- /* To avoid bold styling inherent with elements */
- font-weight: inherit;
-
- &::before {
- content: "";
- display: inline-block;
- position: absolute;
- height: 16px;
- width: 16px;
- left: 0;
- top: 2px; /* alignment */
- background-image: url("@vector-im/compound-design-tokens/icons/error-solid.svg");
- background-size: contain;
-
- /* elecord, temp colour fix */
- filter: invert(1) brightness(0.7);
- }
-
- .mx_AccessSecretStorageDialog_reset_link {
- color: $alert;
- }
- }
- }
-
- .mx_Dialog_buttons_row {
- gap: $spacing-16; /* TODO: needs normalization */
- padding-inline-start: $spacingStart;
- }
- }
+ .mx_EncryptionCard_buttons {
+ margin-top: var(--cpd-space-20x);
}
}
diff --git a/res/css/views/dialogs/security/_CreateKeyBackupDialog.pcss b/res/css/views/dialogs/security/_CreateKeyBackupDialog.pcss
deleted file mode 100644
index 9bd85398818..00000000000
--- a/res/css/views/dialogs/security/_CreateKeyBackupDialog.pcss
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
-Copyright 2018-2024 New Vector Ltd.
-
-SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
-Please see LICENSE files in the repository root for full details.
-*/
-
-.mx_CreateKeyBackupDialog .mx_Dialog_title {
- /* TODO: Consider setting this for all dialog titles. */
- margin-bottom: 1em;
-}
-
-.mx_CreateKeyBackupDialog_primaryContainer {
- /* FIXME: plinth colour in new theme(s). background-color: $accent; */
- padding: 20px;
-}
-
-.mx_CreateKeyBackupDialog_primaryContainer::after {
- content: "";
- clear: both;
- display: block;
-}
-
-.mx_CreateKeyBackupDialog_passPhraseContainer {
- display: flex;
- align-items: flex-start;
-}
-
-.mx_CreateKeyBackupDialog_passPhraseInput {
- flex: none;
- width: 250px;
- border: 1px solid $accent;
- border-radius: 5px;
- padding: 10px;
- margin-bottom: 1em;
-}
-
-.mx_CreateKeyBackupDialog_passPhraseMatch {
- margin-left: 20px;
-}
-
-.mx_CreateKeyBackupDialog_recoveryKeyHeader {
- margin-bottom: 1em;
-}
-
-.mx_CreateKeyBackupDialog_recoveryKeyContainer {
- display: flex;
-}
-
-.mx_CreateKeyBackupDialog_recoveryKey {
- width: 262px;
- padding: 20px;
- color: $info-plinth-fg-color;
- background-color: $info-plinth-bg-color;
- margin-right: 12px;
-}
-
-.mx_CreateKeyBackupDialog_recoveryKeyButtons {
- flex: 1;
- display: flex;
- align-items: center;
-}
-
-.mx_CreateKeyBackupDialog_recoveryKeyButtons button {
- flex: 1;
- white-space: nowrap;
-}
-
-.mx_CreateKeyBackupDialog {
- details .mx_AccessibleButton {
- margin: 1em 0; /* emulate paragraph spacing because we can't put this button in a paragraph due to HTML rules */
- }
-}
diff --git a/res/css/views/elements/_Pill.pcss b/res/css/views/elements/_Pill.pcss
index d692f812a4e..4b3fd3bb685 100644
--- a/res/css/views/elements/_Pill.pcss
+++ b/res/css/views/elements/_Pill.pcss
@@ -11,8 +11,7 @@ Please see LICENSE files in the repository root for full details.
line-height: $font-17px;
border-radius: $font-16px;
vertical-align: text-top;
- display: inline-flex;
- align-items: center;
+ display: inline-block;
box-sizing: border-box;
max-width: 100%;
overflow: hidden;
@@ -57,6 +56,8 @@ Please see LICENSE files in the repository root for full details.
margin-inline-start: -0.3em; /* Otherwise the gap is too large */
margin-inline-end: 0.2em;
min-width: $font-16px; /* ensure the avatar is not compressed */
+ user-select: text;
+ vertical-align: -2.5px;
}
.mx_Pill_text {
diff --git a/res/css/views/messages/_MPollBody.pcss b/res/css/views/messages/_MPollBody.pcss
index 33144083ea9..9889bb81bb2 100644
--- a/res/css/views/messages/_MPollBody.pcss
+++ b/res/css/views/messages/_MPollBody.pcss
@@ -60,7 +60,7 @@ Please see LICENSE files in the repository root for full details.
.mx_MPollBody_allOptions {
display: grid;
- grid-gap: $spacing-16;
+ gap: $spacing-16;
margin-bottom: $spacing-8;
max-width: 550px;
}
diff --git a/res/css/views/polls/pollHistory/_PollHistoryList.pcss b/res/css/views/polls/pollHistory/_PollHistoryList.pcss
index 95d54192f91..b7325451786 100644
--- a/res/css/views/polls/pollHistory/_PollHistoryList.pcss
+++ b/res/css/views/polls/pollHistory/_PollHistoryList.pcss
@@ -21,12 +21,12 @@ Please see LICENSE files in the repository root for full details.
flex: 1 1 0;
align-content: flex-start;
display: grid;
- grid-gap: $spacing-20;
+ gap: $spacing-20;
padding-right: $spacing-64;
margin: $spacing-32 0;
&.mx_PollHistoryList_list_ENDED {
- grid-gap: $spacing-32;
+ gap: $spacing-32;
}
}
diff --git a/res/css/views/right_panel/_ExtensionsCard.pcss b/res/css/views/right_panel/_ExtensionsCard.pcss
index 0dbfc056cd2..c98fa3e9dcf 100644
--- a/res/css/views/right_panel/_ExtensionsCard.pcss
+++ b/res/css/views/right_panel/_ExtensionsCard.pcss
@@ -7,12 +7,11 @@ Please see LICENSE files in the repository root for full details.
*/
.mx_ExtensionsCard {
- --cpd-separator-inset: var(--cpd-space-4x);
- --cpd-separator-spacing: var(--cpd-space-4x);
-
+ --cpd-separator-spacing: var(--cpd-space-6x);
+ --AddExtension-overlap: -76px;
.mx_AutoHideScrollbar {
padding: 0 var(--cpd-space-4x);
- margin-top: var(--cpd-space-3x);
+ margin-top: var(--cpd-space-6x);
box-sizing: border-box;
/* Styling for the "Add extensions" button */
@@ -128,6 +127,11 @@ Please see LICENSE files in the repository root for full details.
.mx_EmptyState::before {
/* Overlap the Add extensions button */
- top: -76px;
+ top: var(--AddExtension-overlap);
+ }
+
+ .mx_EmptyState {
+ /* Stop empty state scrolling */
+ height: calc(100% + var(--AddExtension-overlap));
}
}
diff --git a/res/css/views/right_panel/_RoomSummaryCard.pcss b/res/css/views/right_panel/_RoomSummaryCard.pcss
index 8ddf7aec108..7e6ef2b4dc2 100644
--- a/res/css/views/right_panel/_RoomSummaryCard.pcss
+++ b/res/css/views/right_panel/_RoomSummaryCard.pcss
@@ -32,7 +32,7 @@ Please see LICENSE files in the repository root for full details.
padding: 0 12px;
color: var(--cpd-color-text-secondary);
- .mx_Box {
+ .mx_RoomSummaryCard_topic_box {
width: 100%;
}
diff --git a/res/css/views/right_panel/_UserInfo.pcss b/res/css/views/right_panel/_UserInfo.pcss
index 3030b93c030..2b6480bd841 100644
--- a/res/css/views/right_panel/_UserInfo.pcss
+++ b/res/css/views/right_panel/_UserInfo.pcss
@@ -108,11 +108,6 @@ Please see LICENSE files in the repository root for full details.
margin: 0;
font-size: $font-20px;
line-height: $font-25px;
-
- /* E2E icon wrapper */
- .mx_Flex > span {
- display: inline-block;
- }
}
.mx_UserInfo_profile_name {
diff --git a/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss b/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss
index 1e61bf7f3c5..06ffe532d7c 100644
--- a/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss
+++ b/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss
@@ -7,79 +7,74 @@
/**
* The RoomListItemView has the following structure:
- * button----------------------------------------|
- * | <-12px-> container--------------------------|
- * | | room avatar <-12px-> content-----|
- * | | | room_name |
- * | | | ----------| <-- border
- * |---------------------------------------------|
+ * button--------------------------------------------------|
+ * | <-12px-> container------------------------------------|
+ * | | room avatar <-8px-> content----------------|
+ * | | | room_name <- 20px ->|
+ * | | | --------------------| <-- border
+ * |-------------------------------------------------------|
*/
.mx_RoomListItemView {
- all: unset;
+ /* Remove button default style */
+ background: unset;
+ border: none;
+ padding: 0;
+ text-align: unset;
+
cursor: pointer;
+ height: 48px;
+ width: 100%;
- &:hover {
- background-color: var(--cpd-color-bg-action-secondary-hovered);
- }
+ padding-left: var(--cpd-space-3x);
+ font: var(--cpd-font-body-md-regular);
- .mx_RoomListItemView_container {
- padding-left: var(--cpd-space-2x);
- font: var(--cpd-font-body-md-regular);
+ .mx_RoomListItemView_content {
height: 100%;
+ flex: 1;
+ /* The border is only under the room name and the future hover menu */
+ border-bottom: var(--cpd-border-width-0-5) solid var(--cpd-color-bg-subtle-secondary);
+ box-sizing: border-box;
+ min-width: 0;
+ padding-right: var(--cpd-space-5x);
- .mx_RoomListItemView_content {
- height: 100%;
- flex: 1;
- /* The border is only under the room name and the future hover menu */
- border-bottom: var(--cpd-border-width-0-5) solid var(--cpd-color-bg-subtle-secondary);
- box-sizing: border-box;
+ .mx_RoomListItemView_text {
min-width: 0;
+ }
- .mx_RoomListItemView_text {
- min-width: 0;
- }
-
- .mx_RoomListItemView_roomName {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
+ .mx_RoomListItemView_roomName {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
- .mx_RoomListItemView_messagePreview {
- font: var(--cpd-font-body-sm-regular);
- color: var(--cpd-color-text-secondary);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
+ .mx_RoomListItemView_messagePreview {
+ font: var(--cpd-font-body-sm-regular);
+ color: var(--cpd-color-text-secondary);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
}
}
-.mx_RoomListItemView_menu_open {
+.mx_RoomListItemView_hover {
background-color: var(--cpd-color-bg-action-secondary-hovered);
+}
- .mx_RoomListItemView_content {
- padding-right: var(--cpd-space-1-5x);
- }
+.mx_RoomListItemView_menu_open .mx_RoomListItemView_content {
+ /**
+ * The figma uses 16px padding (--cpd-space-4x) but due to https://github.com/element-hq/compound-web/issues/331
+ * the icon size of the menu is 18px instead of 20px with a different internal padding
+ * We need to use 18px to align the icon with the others icons
+ * 18px is not available in compound spacing
+ */
+ padding-right: 18px;
}
.mx_RoomListItemView_selected {
background-color: var(--cpd-color-bg-action-secondary-pressed);
}
-.mx_RoomListItemView_notification_decoration {
- .mx_RoomListItemView_content {
- padding-right: var(--cpd-space-2x);
- }
-}
-
-.mx_RoomListItemView_empty {
- .mx_RoomListItemView_content {
- padding-right: var(--cpd-space-3x);
- }
-}
-
.mx_RoomListItemView_bold .mx_RoomListItemView_roomName {
font: var(--cpd-font-body-md-semibold);
}
diff --git a/res/css/views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss b/res/css/views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss
index ac85782bbd0..f8fc31ae124 100644
--- a/res/css/views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss
+++ b/res/css/views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss
@@ -6,7 +6,32 @@
*/
.mx_RoomListPrimaryFilters {
- margin: unset;
- list-style-type: none;
- padding: var(--cpd-space-2x) var(--cpd-space-3x);
+ padding: var(--cpd-space-2x) var(--cpd-space-4x) var(--cpd-space-2x) var(--cpd-space-3x);
+
+ .mx_RoomListPrimaryFilters_wrapping {
+ display: none;
+ }
+
+ ul {
+ margin: unset;
+ padding: unset;
+ list-style-type: none;
+ /**
+ * The InteractionObserver needs the height to be set to work properly.
+ */
+ height: 100%;
+ flex: 1;
+ }
+
+ .mx_RoomListPrimaryFilters_IconButton {
+ svg {
+ transition: transform 0.1s linear;
+ }
+ }
+
+ .mx_RoomListPrimaryFilters_IconButton[aria-expanded="true"] {
+ svg {
+ transform: rotate(180deg);
+ }
+ }
}
diff --git a/res/css/views/rooms/RoomListPanel/_RoomListSearch.pcss b/res/css/views/rooms/RoomListPanel/_RoomListSearch.pcss
index 8a97086df8e..472badc3ad8 100644
--- a/res/css/views/rooms/RoomListPanel/_RoomListSearch.pcss
+++ b/res/css/views/rooms/RoomListPanel/_RoomListSearch.pcss
@@ -12,15 +12,16 @@
border-bottom: var(--cpd-border-width-1) solid var(--cpd-color-bg-subtle-primary);
padding: 0 var(--cpd-space-3x);
- svg {
- fill: var(--cpd-color-icon-secondary);
- }
-
.mx_RoomListSearch_search {
/* The search button should take all the remaining space */
flex: 1;
font: var(--cpd-font-body-md-regular);
color: var(--cpd-color-text-secondary);
+ min-width: 0;
+
+ svg {
+ fill: var(--cpd-color-icon-secondary);
+ }
span {
flex: 1;
@@ -28,12 +29,17 @@
kbd {
font-family: inherit;
}
- }
- }
- .mx_RoomListSearch_button:hover {
- svg {
- fill: var(--cpd-color-icon-primary);
+ /* Shrink and truncate the search text */
+ white-space: nowrap;
+ overflow: hidden;
+ .mx_RoomListSearch_search_text {
+ min-width: 0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ text-align: start;
+ }
}
}
}
diff --git a/res/css/views/rooms/RoomListPanel/_RoomListSecondaryFilters.pcss b/res/css/views/rooms/RoomListPanel/_RoomListSecondaryFilters.pcss
index c94fb54007d..0fa8dc12aef 100644
--- a/res/css/views/rooms/RoomListPanel/_RoomListSecondaryFilters.pcss
+++ b/res/css/views/rooms/RoomListPanel/_RoomListSecondaryFilters.pcss
@@ -10,16 +10,3 @@
margin: var(--cpd-space-2x);
margin-left: var(--cpd-space-1x);
}
-
-.mx_RoomListSecondaryFilters_roomOptionsButton {
- /* Size the button appropriately (should this be in em, maybe,
- * so it gets bigger with font size? These values taken from the figma.
- */
- width: 28px;
- height: 28px;
- margin-left: auto;
-
- svg {
- color: var(--cpd-color-icon-primary);
- }
-}
diff --git a/res/css/views/rooms/RoomListPanel/_RoomListSkeleton.pcss b/res/css/views/rooms/RoomListPanel/_RoomListSkeleton.pcss
new file mode 100644
index 00000000000..2e644cbba1e
--- /dev/null
+++ b/res/css/views/rooms/RoomListPanel/_RoomListSkeleton.pcss
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+.mx_RoomListSkeleton {
+ position: relative;
+ margin-left: 4px;
+ height: 100%;
+
+ &::before {
+ background-color: var(--cpd-color-bg-subtle-secondary);
+ width: 100%;
+ height: 100%;
+
+ content: "";
+ position: absolute;
+ mask-repeat: repeat-y;
+ mask-size: auto 96px;
+ mask-image: url("$(res)/img/element-icons/roomlist/room-list-item-skeleton.svg");
+ }
+}
diff --git a/res/css/views/rooms/_E2EIcon.pcss b/res/css/views/rooms/_E2EIcon.pcss
index f3aaf8a8833..7dd0aa476dd 100644
--- a/res/css/views/rooms/_E2EIcon.pcss
+++ b/res/css/views/rooms/_E2EIcon.pcss
@@ -55,6 +55,13 @@ Please see LICENSE files in the repository root for full details.
background-color: var(--cpd-color-icon-tertiary);
}
+.mx_E2EIcon_verified,
+.mx_E2EIcon_warning {
+ .mx_E2EIcon_normal::after {
+ background-color: white;
+ }
+}
+
.mx_E2EIcon_verified::after {
mask-image: url("$(res)/img/e2e/verified.svg");
background-color: $e2e-verified-color;
diff --git a/res/css/views/rooms/_RoomHeader.pcss b/res/css/views/rooms/_RoomHeader.pcss
index 320a482c74f..99f1f366ff7 100644
--- a/res/css/views/rooms/_RoomHeader.pcss
+++ b/res/css/views/rooms/_RoomHeader.pcss
@@ -106,5 +106,5 @@ Please see LICENSE files in the repository root for full details.
}
.mx_RoomHeader .mx_RoomHeader_toggled {
- color: var(--cpd-color-icon-accent-primary);
+ fill: var(--cpd-color-icon-accent-primary);
}
diff --git a/res/css/views/rooms/_RoomSublist.pcss b/res/css/views/rooms/_RoomSublist.pcss
index 7b0da5608ec..3361bce4bbf 100644
--- a/res/css/views/rooms/_RoomSublist.pcss
+++ b/res/css/views/rooms/_RoomSublist.pcss
@@ -404,8 +404,7 @@ Please see LICENSE files in the repository root for full details.
height: 240px;
&::before {
- background: $roomsublist-skeleton-ui-bg;
-
+ background-color: var(--cpd-color-bg-subtle-secondary);
width: 100%;
height: 100%;
diff --git a/res/css/views/settings/_JoinRuleSettings.pcss b/res/css/views/settings/_JoinRuleSettings.pcss
index 485434f0da5..fcb21fca968 100644
--- a/res/css/views/settings/_JoinRuleSettings.pcss
+++ b/res/css/views/settings/_JoinRuleSettings.pcss
@@ -53,6 +53,14 @@ Please see LICENSE files in the repository root for full details.
display: block;
}
+ &.mx_StyledRadioButton_disabled {
+ opacity: 0.5;
+ }
+
+ &.mx_StyledRadioButton_disabled + span {
+ opacity: 0.5;
+ }
+
& + span {
display: inline-block;
margin-left: 34px;
@@ -71,3 +79,7 @@ Please see LICENSE files in the repository root for full details.
font: var(--cpd-font-body-md-regular);
margin-top: var(--cpd-space-2x);
}
+
+.mx_JoinRuleSettings_recommended {
+ color: $accent-1000;
+}
diff --git a/res/css/views/settings/_NotificationSettings2.pcss b/res/css/views/settings/_NotificationSettings2.pcss
index d579c22b95b..285282c89c5 100644
--- a/res/css/views/settings/_NotificationSettings2.pcss
+++ b/res/css/views/settings/_NotificationSettings2.pcss
@@ -30,7 +30,7 @@ Please see LICENSE files in the repository root for full details.
.mx_SettingsSubsection_content {
margin-top: 12px;
- grid-gap: 12px;
+ gap: 12px;
justify-items: stretch;
justify-content: stretch;
}
@@ -40,7 +40,7 @@ Please see LICENSE files in the repository root for full details.
}
.mx_NotificationSettings2_flags {
- grid-gap: 4px;
+ gap: 4px;
}
.mx_StyledRadioButton_content {
diff --git a/res/css/views/settings/_Notifications.pcss b/res/css/views/settings/_Notifications.pcss
index e4e450fd581..a97d7529d5c 100644
--- a/res/css/views/settings/_Notifications.pcss
+++ b/res/css/views/settings/_Notifications.pcss
@@ -11,7 +11,7 @@ Please see LICENSE files in the repository root for full details.
display: grid;
grid-template-columns: auto repeat(3, 62px);
place-items: center center;
- grid-gap: 8px;
+ gap: 8px;
/* Override StyledRadioButton default styles */
.mx_StyledRadioButton {
diff --git a/res/css/views/settings/_SettingsHeader.pcss b/res/css/views/settings/_SettingsHeader.pcss
index a705deda6cf..e1c470214f9 100644
--- a/res/css/views/settings/_SettingsHeader.pcss
+++ b/res/css/views/settings/_SettingsHeader.pcss
@@ -16,4 +16,13 @@
font: var(--cpd-font-body-sm-medium);
color: var(--cpd-color-text-action-accent);
}
+
+ &.mx_SettingsHeader_recommended::after {
+ display: inline-block;
+ content: "";
+ width: 8px;
+ height: 8px;
+ background-color: var(--cpd-color-icon-critical-primary);
+ clip-path: circle(4px);
+ }
}
diff --git a/res/css/views/settings/encryption/_ChangeRecoveryKey.pcss b/res/css/views/settings/encryption/_ChangeRecoveryKey.pcss
index ceacb22c270..872decadc86 100644
--- a/res/css/views/settings/encryption/_ChangeRecoveryKey.pcss
+++ b/res/css/views/settings/encryption/_ChangeRecoveryKey.pcss
@@ -68,5 +68,28 @@
display: flex;
flex-direction: column;
gap: var(--cpd-space-8x);
+
+ .mx_KeyForm_password {
+ > input[name="recoveryKey"] {
+ /*
+ * From figma https://www.figma.com/design/qTWRfItpO3RdCjnTKPu4mL/Settings?node-id=375-77506&t=d82NdRBDoKsUe1C9-4
+ */
+ height: 70px;
+ padding: var(--cpd-space-3x) var(--cpd-space-3x) var(--cpd-space-3x) var(--cpd-space-4x);
+ border: var(--cpd-border-width-1) solid;
+ border-radius: 8px;
+ margin: 0px;
+ }
+
+ > button {
+ /*
+ * See figma https://www.figma.com/design/qTWRfItpO3RdCjnTKPu4mL/Settings?node-id=375-77506&t=d82NdRBDoKsUe1C9-4
+ * Avoid stretching the hide/show symbol to the height of the input, and centre it vertically.
+ */
+ height: 24.5px;
+ padding: var(--cpd-space-1x);
+ align-self: center;
+ }
+ }
}
}
diff --git a/res/css/views/settings/tabs/_SettingsSection.pcss b/res/css/views/settings/tabs/_SettingsSection.pcss
index 997343190dc..ce3c9266c39 100644
--- a/res/css/views/settings/tabs/_SettingsSection.pcss
+++ b/res/css/views/settings/tabs/_SettingsSection.pcss
@@ -34,7 +34,7 @@ Please see LICENSE files in the repository root for full details.
.mx_SettingsSection_subSections {
display: grid;
grid-template-columns: minmax(0, 1fr);
- grid-gap: $spacing-32;
+ gap: $spacing-32;
padding: $spacing-16 0;
}
diff --git a/res/css/views/settings/tabs/_SettingsTab.pcss b/res/css/views/settings/tabs/_SettingsTab.pcss
index e0abf08e83b..99690d56577 100644
--- a/res/css/views/settings/tabs/_SettingsTab.pcss
+++ b/res/css/views/settings/tabs/_SettingsTab.pcss
@@ -84,7 +84,7 @@ Please see LICENSE files in the repository root for full details.
.mx_SettingsTab_sections {
display: grid;
grid-template-columns: 1fr;
- grid-gap: $spacing-32;
+ gap: $spacing-32;
padding-bottom: $spacing-16;
}
diff --git a/res/css/views/settings/tabs/user/_KeyboardUserSettingsTab.pcss b/res/css/views/settings/tabs/user/_KeyboardUserSettingsTab.pcss
index b3251f3e3c1..44452be7446 100644
--- a/res/css/views/settings/tabs/user/_KeyboardUserSettingsTab.pcss
+++ b/res/css/views/settings/tabs/user/_KeyboardUserSettingsTab.pcss
@@ -12,7 +12,7 @@ Please see LICENSE files in the repository root for full details.
padding: 0;
width: 100%;
display: grid;
- grid-gap: $spacing-4;
+ gap: $spacing-4;
}
.mx_KeyboardShortcut_shortcutRow,
diff --git a/res/css/views/verification/_VerificationShowSas.pcss b/res/css/views/verification/_VerificationShowSas.pcss
index 1a24519cbf8..9e4d1f138b3 100644
--- a/res/css/views/verification/_VerificationShowSas.pcss
+++ b/res/css/views/verification/_VerificationShowSas.pcss
@@ -47,7 +47,7 @@ Please see LICENSE files in the repository root for full details.
.mx_VerificationShowSas_emojiSas_label {
font-size: $font-12px;
- word-break: break-all;
+ word-break: break-word;
}
.mx_VerificationShowSas_emojiSas_break {
diff --git a/res/img/element-icons/roomlist/room-list-item-skeleton.svg b/res/img/element-icons/roomlist/room-list-item-skeleton.svg
new file mode 100644
index 00000000000..adf56e4ed8a
--- /dev/null
+++ b/res/img/element-icons/roomlist/room-list-item-skeleton.svg
@@ -0,0 +1,14 @@
+
diff --git a/res/themes/light-high-contrast/css/_light-high-contrast.pcss b/res/themes/light-high-contrast/css/_light-high-contrast.pcss
index 213c6414401..94774bc5b8b 100644
--- a/res/themes/light-high-contrast/css/_light-high-contrast.pcss
+++ b/res/themes/light-high-contrast/css/_light-high-contrast.pcss
@@ -163,7 +163,8 @@ $accent-1400: var(--cpd-color-green-1400);
&.mx_SpotlightDialog_startChat::before,
&.mx_SpotlightDialog_joinRoomAlias::before,
&.mx_SpotlightDialog_explorePublicRooms::before,
- &.mx_SpotlightDialog_startGroupChat::before {
+ &.mx_SpotlightDialog_startGroupChat::before,
+ &.mx_SpotlightDialog_searchMessages::before {
background-color: $background !important;
}
diff --git a/res/welcome.html b/res/welcome.html
index ef2d43bd8ff..9fdd60a7c02 100644
--- a/res/welcome.html
+++ b/res/welcome.html
@@ -3,7 +3,7 @@
* voodoo where we have to set display: none by default
*/
- h1::after {
+ .mx_Header_title::after {
content: "!";
}
diff --git a/scripts/gitrm.sh b/scripts/gitrm.sh
old mode 100644
new mode 100755
index 926ea38f635..3f8bc96f126
--- a/scripts/gitrm.sh
+++ b/scripts/gitrm.sh
@@ -122,3 +122,6 @@ rm .github/workflows/update-topics.yaml
rm -rf __mocks__/
rm knip.ts
rm developer_guide.md
+
+git rm --cached -r .storybook/
+rm -rf .storybook/
diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts
index 344059fee4c..289bda3f494 100644
--- a/src/@types/global.d.ts
+++ b/src/@types/global.d.ts
@@ -68,9 +68,17 @@ type ElectronChannel =
| "openDesktopCapturerSourcePicker"
| "userAccessToken"
| "homeserverUrl"
- | "serverSupportedVersions";
+ | "serverSupportedVersions"
+ | "showToast";
declare global {
+ // use `number` as the return type in all cases for globalThis.set{Interval,Timeout},
+ // so we don't accidentally use the methods on NodeJS.Timeout - they only exist in a subset of environments.
+ // The overload for clear{Interval,Timeout} is resolved as expected.
+ // We use `ReturnType` in the code to be agnostic of if this definition gets loaded.
+ function setInterval(handler: TimerHandler, timeout: number, ...arguments: any[]): number;
+ function setTimeout(handler: TimerHandler, timeout: number, ...arguments: any[]): number;
+
interface Window {
mxSendRageshake: (text: string, withLogs?: boolean) => void;
matrixLogger: typeof logger;
@@ -82,19 +90,10 @@ declare global {
mxMatrixClientPeg: IMatrixClientPeg;
mxReactSdkConfig: DeepReadonly;
- // Needed for Safari, unknown to TypeScript
- webkitAudioContext: typeof AudioContext;
-
// https://docs.microsoft.com/en-us/previous-versions/hh772328(v=vs.85)
// we only ever check for its existence, so we can ignore its actual type
MSStream?: unknown;
- // https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1029#issuecomment-869224737
- // https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas
- OffscreenCanvas?: {
- new (width: number, height: number): OffscreenCanvas;
- };
-
mxContentMessages: ContentMessages;
mxToastStore: ToastStore;
mxDeviceListener: DeviceListener;
@@ -137,8 +136,20 @@ declare global {
}
interface Electron {
+ // Legacy
on(channel: ElectronChannel, listener: (event: Event, ...args: any[]) => void): void;
send(channel: ElectronChannel, ...args: any[]): void;
+ // Initialisation
+ initialise(): Promise<{
+ protocol: string;
+ sessionId: string;
+ supportsBadgeOverlay: boolean;
+ config: IConfigOptions;
+ supportedSettings: Record;
+ }>;
+ // Settings
+ setSettingValue(settingName: string, value: any): Promise;
+ getSettingValue(settingName: string): Promise;
}
interface DesktopCapturerSource {
@@ -156,31 +167,10 @@ declare global {
fetchWindowIcons?: boolean;
}
- interface Document {
- // Safari & IE11 only have this prefixed: we used prefixed versions
- // previously so let's continue to support them for now
- webkitExitFullscreen(): Promise;
- msExitFullscreen(): Promise;
- readonly webkitFullscreenElement: Element | null;
- readonly msFullscreenElement: Element | null;
- }
-
- interface Navigator {
- userLanguage?: string;
- }
-
interface StorageEstimate {
usageDetails?: { [key: string]: number };
}
- interface Element {
- // Safari & IE11 only have this prefixed: we used prefixed versions
- // previously so let's continue to support them for now
- webkitRequestFullScreen(options?: FullscreenOptions): Promise;
- msRequestFullscreen(options?: FullscreenOptions): Promise;
- // scrollIntoView(arg?: boolean | _ScrollIntoViewOptions): void;
- }
-
// https://github.com/microsoft/TypeScript/issues/28308#issuecomment-650802278
interface AudioWorkletProcessor {
readonly port: MessagePort;
@@ -239,11 +229,4 @@ declare global {
var mx_rage_store: IndexedDBLogStore;
}
-// add method which is missing from the node typing
-declare module "url" {
- interface Url {
- format(): string;
- }
-}
-
/* eslint-enable @typescript-eslint/naming-convention */
diff --git a/src/@types/invite-rules.ts b/src/@types/invite-rules.ts
new file mode 100644
index 00000000000..bc72a5e9222
--- /dev/null
+++ b/src/@types/invite-rules.ts
@@ -0,0 +1,29 @@
+/*
+Copyright 2025 New Vector Ltd.
+
+SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+Please see LICENSE files in the repository root for full details.
+*/
+
+export const INVITE_RULES_ACCOUNT_DATA_TYPE = "org.matrix.msc4155.invite_permission_config";
+
+export interface InviteConfigAccountData {
+ allowed_users?: string[];
+ blocked_users?: string[];
+ ignored_users?: string[];
+ allowed_servers?: string[];
+ blocked_servers?: string[];
+ ignored_servers?: string[];
+}
+
+/**
+ * Computed values based on MSC4155. Currently Element Web only supports
+ * blocking all invites.
+ */
+export interface ComputedInviteConfig extends Record {
+ /**
+ * Are all invites blocked. This is only about blocking all invites,
+ * but this being false may still block invites through other rules.
+ */
+ allBlocked: boolean;
+}
diff --git a/src/@types/matrix-js-sdk.d.ts b/src/@types/matrix-js-sdk.d.ts
index c81c5377bfc..ad75ca95f05 100644
--- a/src/@types/matrix-js-sdk.d.ts
+++ b/src/@types/matrix-js-sdk.d.ts
@@ -15,6 +15,7 @@ import type { EmptyObject } from "matrix-js-sdk/src/matrix";
import type { DeviceClientInformation } from "../utils/device/types.ts";
import type { UserWidget } from "../utils/WidgetUtils-types.ts";
import { type MediaPreviewConfig } from "./media_preview.ts";
+import { type INVITE_RULES_ACCOUNT_DATA_TYPE, type InviteConfigAccountData } from "./invite-rules.ts";
// Extend Matrix JS SDK types via Typescript declaration merging to support unspecced event fields and types
declare module "matrix-js-sdk/src/types" {
@@ -60,7 +61,6 @@ declare module "matrix-js-sdk/src/types" {
};
};
}
-
export interface AccountDataEvents {
// Analytics account data event
"im.vector.analytics": {
@@ -89,7 +89,12 @@ declare module "matrix-js-sdk/src/types" {
accepted: string[];
};
+ // MSC4155: Invite filtering
+ [INVITE_RULES_ACCOUNT_DATA_TYPE]: InviteConfigAccountData;
"io.element.msc4278.media_preview_config": MediaPreviewConfig;
+
+ // Indicate whether recovery is enabled or disabled
+ "io.element.recovery": { enabled: boolean };
}
export interface AudioContent {
diff --git a/src/AddThreepid.ts b/src/AddThreepid.ts
index 7d2c4cefb5f..877a0c5c4a1 100644
--- a/src/AddThreepid.ts
+++ b/src/AddThreepid.ts
@@ -79,6 +79,8 @@ export default class AddThreepid {
} catch (err) {
if (err instanceof MatrixError && err.errcode === "M_THREEPID_IN_USE") {
throw new UserFriendlyError("settings|general|email_address_in_use", { cause: err });
+ } else if (err instanceof MatrixError && err.errcode === "M_THREEPID_MEDIUM_NOT_SUPPORTED") {
+ throw new UserFriendlyError("settings|general|email_adding_unsupported_by_hs", { cause: err });
}
// Otherwise, just blurt out the same error
throw err;
@@ -121,6 +123,8 @@ export default class AddThreepid {
* @param {string} phoneCountry The ISO 2 letter code of the country to resolve phoneNumber in
* @param {string} phoneNumber The national or international formatted phone number to add
* @return {Promise} Resolves when the text message has been sent. Then call haveMsisdnToken().
+ *
+ * @throws {UserFriendlyError} An appropriate user-friendly error if the verification code could not be sent.
*/
public async addMsisdn(phoneCountry: string, phoneNumber: string): Promise {
try {
@@ -136,6 +140,10 @@ export default class AddThreepid {
} catch (err) {
if (err instanceof MatrixError && err.errcode === "M_THREEPID_IN_USE") {
throw new UserFriendlyError("settings|general|msisdn_in_use", { cause: err });
+ } else if (err instanceof MatrixError && err.errcode === "M_THREEPID_MEDIUM_NOT_SUPPORTED") {
+ throw new UserFriendlyError("settings|general|msisdn_adding_unsupported_by_hs", { cause: err });
+ } else if (err instanceof MatrixError && err.errcode === "M_INVALID_PARAM") {
+ throw new UserFriendlyError("settings|general|invalid_phone_number", { cause: err });
}
// Otherwise, just blurt out the same error
throw err;
diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts
index c7b7825fe68..05dd437f942 100644
--- a/src/BasePlatform.ts
+++ b/src/BasePlatform.ts
@@ -72,7 +72,7 @@ export default abstract class BasePlatform {
protected _favicon?: Favicon;
protected constructor() {
- dis.register(this.onAction);
+ dis.register(this.onAction.bind(this));
this.startUpdateCheck = this.startUpdateCheck.bind(this);
}
@@ -85,14 +85,14 @@ export default abstract class BasePlatform {
*/
public abstract getDefaultDeviceDisplayName(): string;
- protected onAction = (payload: ActionPayload): void => {
+ protected onAction(payload: ActionPayload): void {
switch (payload.action) {
case "on_client_not_viable":
case Action.OnLoggedOut:
this.setNotificationCount(0);
break;
}
- };
+ }
// Used primarily for Analytics
public abstract getHumanReadableName(): string;
@@ -477,6 +477,8 @@ export default abstract class BasePlatform {
// The redirect URL has to exactly match that registered at the OIDC server, so
// ensure that the fragment part of the URL is empty.
url.hash = "";
+ // Set no_universal_links=true to prevent the callback being handled by Element X installed on macOS Apple Silicon
+ url.searchParams.set("no_universal_links", "true");
return url;
}
@@ -494,15 +496,12 @@ export default abstract class BasePlatform {
}
private updateFavicon(): void {
- let bgColor = "#d00";
- let notif: string | number = this.notificationCount;
+ const notif: string | number = this.notificationCount;
if (this.errorDidOccur) {
- notif = notif || "×";
- bgColor = "#f00";
+ this.favicon.badge(notif || "×", { bgColor: "#f00" });
}
-
- this.favicon.badge(notif, { bgColor });
+ this.favicon.badge(notif);
}
/**
diff --git a/src/ContentMessages.ts b/src/ContentMessages.ts
index c5e34d71307..54aaea3ae19 100644
--- a/src/ContentMessages.ts
+++ b/src/ContentMessages.ts
@@ -63,6 +63,7 @@ import { blobIsAnimated } from "./utils/Image.ts";
const PHYS_HIDPI = [0x00, 0x00, 0x16, 0x25, 0x00, 0x00, 0x16, 0x25, 0x01];
export class UploadCanceledError extends Error {}
+export class UploadFailedError extends Error {}
interface IMediaConfig {
"m.upload.size"?: number;
@@ -355,12 +356,19 @@ export async function uploadFile(
// Pass the encrypted data as a Blob to the uploader.
const blob = new Blob([encryptResult.data]);
- const { content_uri: url } = await matrixClient.uploadContent(blob, {
- progressHandler,
- abortController,
- includeFilename: false,
- type: "application/octet-stream",
- });
+ let url: string;
+ try {
+ ({ content_uri: url } = await matrixClient.uploadContent(blob, {
+ progressHandler,
+ abortController,
+ includeFilename: false,
+ type: "application/octet-stream",
+ }));
+ } catch (e) {
+ if (abortController.signal.aborted) throw new UploadCanceledError();
+ console.error("Failed to upload file", e);
+ throw new UploadFailedError();
+ }
if (abortController.signal.aborted) throw new UploadCanceledError();
// If the attachment is encrypted then bundle the URL along with the information
@@ -372,7 +380,14 @@ export async function uploadFile(
} as EncryptedFile,
};
} else {
- const { content_uri: url } = await matrixClient.uploadContent(file, { progressHandler, abortController });
+ let url: string;
+ try {
+ ({ content_uri: url } = await matrixClient.uploadContent(file, { progressHandler, abortController }));
+ } catch (e) {
+ if (abortController.signal.aborted) throw new UploadCanceledError();
+ console.error("Failed to upload file", e);
+ throw new UploadFailedError();
+ }
if (abortController.signal.aborted) throw new UploadCanceledError();
// If the attachment isn't encrypted then include the URL directly.
return { url };
@@ -570,7 +585,7 @@ export default class ContentMessages {
const imageInfo = await infoForImageFile(matrixClient, roomId, file);
Object.assign(content.info, imageInfo);
} catch (e) {
- if (e instanceof HTTPError) {
+ if (e instanceof UploadFailedError) {
// re-throw to main upload error handler
throw e;
}
diff --git a/src/CreateCrossSigning.ts b/src/CreateCrossSigning.ts
index db9bc3e3fea..5f6f3e48aae 100644
--- a/src/CreateCrossSigning.ts
+++ b/src/CreateCrossSigning.ts
@@ -38,10 +38,10 @@ export async function createCrossSigning(cli: MatrixClient): Promise {
export async function uiAuthCallback(
matrixClient: MatrixClient,
- makeRequest: (authData: AuthDict) => Promise,
+ makeRequest: (authData: AuthDict | null) => Promise,
): Promise {
try {
- await makeRequest({});
+ await makeRequest(null);
} catch (error) {
if (!(error instanceof MatrixError) || !error.data || !error.data.flows) {
// Not a UIA response
@@ -64,7 +64,7 @@ export async function uiAuthCallback(
};
const { finished } = Modal.createDialog(InteractiveAuthDialog, {
- title: _t("encryption|bootstrap_title"),
+ title: "",
matrixClient,
makeRequest,
aestheticsForStagePhases: {
diff --git a/src/DateUtils.ts b/src/DateUtils.ts
index e788ca09bfd..a5e22c08910 100644
--- a/src/DateUtils.ts
+++ b/src/DateUtils.ts
@@ -13,6 +13,8 @@ import { type Optional } from "matrix-events-sdk";
import { _t, getUserLanguage } from "./languageHandler";
import { getUserTimezone } from "./TimezoneHandler";
+export { formatSeconds } from "./shared-components/utils/DateUtils";
+
export const MINUTE_MS = 60000;
export const HOUR_MS = MINUTE_MS * 60;
export const DAY_MS = HOUR_MS * 24;
@@ -180,31 +182,6 @@ export function formatTime(date: Date, showTwelveHour = false, locale?: string):
}).format(date);
}
-export function formatSeconds(inSeconds: number): string {
- const isNegative = inSeconds < 0;
- inSeconds = Math.abs(inSeconds);
-
- const hours = Math.floor(inSeconds / (60 * 60))
- .toFixed(0)
- .padStart(2, "0");
- const minutes = Math.floor((inSeconds % (60 * 60)) / 60)
- .toFixed(0)
- .padStart(2, "0");
- const seconds = Math.floor((inSeconds % (60 * 60)) % 60)
- .toFixed(0)
- .padStart(2, "0");
-
- let output = "";
- if (hours !== "00") output += `${hours}:`;
- output += `${minutes}:${seconds}`;
-
- if (isNegative) {
- output = "-" + output;
- }
-
- return output;
-}
-
export function formatTimeLeft(inSeconds: number): string {
const hours = Math.floor(inSeconds / (60 * 60)).toFixed(0);
const minutes = Math.floor((inSeconds % (60 * 60)) / 60).toFixed(0);
diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts
index 751e71dd9f6..4fc4e9cf20f 100644
--- a/src/DeviceListener.ts
+++ b/src/DeviceListener.ts
@@ -15,7 +15,7 @@ import {
type SyncState,
ClientStoppedError,
} from "matrix-js-sdk/src/matrix";
-import { logger as baseLogger, LogSpan } from "matrix-js-sdk/src/logger";
+import { logger as baseLogger, type BaseLogger, LogSpan } from "matrix-js-sdk/src/logger";
import { CryptoEvent, type KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
import { type CryptoSessionStateChange } from "@matrix-org/analytics-events/types/typescript/CryptoSessionStateChange";
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
@@ -57,6 +57,11 @@ const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
*/
export const BACKUP_DISABLED_ACCOUNT_DATA_KEY = "m.org.matrix.custom.backup_disabled";
+/**
+ * Account data key to indicate whether the user has chosen to enable or disable recovery.
+ */
+export const RECOVERY_ACCOUNT_DATA_KEY = "io.element.recovery";
+
const logger = baseLogger.getChild("DeviceListener:");
export default class DeviceListener {
@@ -97,6 +102,7 @@ export default class DeviceListener {
this.client.on(CryptoEvent.DevicesUpdated, this.onDevicesUpdated);
this.client.on(CryptoEvent.UserTrustStatusChanged, this.onUserTrustStatusChanged);
this.client.on(CryptoEvent.KeysChanged, this.onCrossSingingKeysChanged);
+ this.client.on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatusChanged);
this.client.on(ClientEvent.AccountData, this.onAccountData);
this.client.on(ClientEvent.Sync, this.onSync);
this.client.on(RoomStateEvent.Events, this.onRoomStateEvents);
@@ -132,7 +138,7 @@ export default class DeviceListener {
this.dismissedThisDeviceToast = false;
this.keyBackupInfo = null;
this.keyBackupFetchedAt = null;
- this.keyBackupStatusChecked = false;
+ this.cachedKeyBackupUploadActive = undefined;
this.ourDeviceIdsAtStart = null;
this.displayingToastsForDeviceIds = new Set();
this.client = undefined;
@@ -157,6 +163,20 @@ export default class DeviceListener {
this.recheck();
}
+ /**
+ * Set the account data "m.org.matrix.custom.backup_disabled" to { "disabled": true }.
+ */
+ public async recordKeyBackupDisabled(): Promise {
+ await this.client?.setAccountData(BACKUP_DISABLED_ACCOUNT_DATA_KEY, { disabled: true });
+ }
+
+ /**
+ * Set the account data to indicate that recovery is disabled
+ */
+ public async recordRecoveryDisabled(): Promise {
+ await this.client?.setAccountData(RECOVERY_ACCOUNT_DATA_KEY, { enabled: false });
+ }
+
private async ensureDeviceIdsAtStartPopulated(): Promise {
if (this.ourDeviceIdsAtStart === null) {
this.ourDeviceIdsAtStart = await this.getDeviceIds();
@@ -192,6 +212,12 @@ export default class DeviceListener {
this.recheck();
};
+ private onKeyBackupStatusChanged = (): void => {
+ logger.info("Backup status changed");
+ this.cachedKeyBackupUploadActive = undefined;
+ this.recheck();
+ };
+
private onCrossSingingKeysChanged = (): void => {
this.recheck();
};
@@ -201,11 +227,14 @@ export default class DeviceListener {
// * migrated SSSS to symmetric
// * uploaded keys to secret storage
// * completed secret storage creation
+ // * disabled key backup
// which result in account data changes affecting checks below.
if (
ev.getType().startsWith("m.secret_storage.") ||
ev.getType().startsWith("m.cross_signing.") ||
- ev.getType() === "m.megolm_backup.v1"
+ ev.getType() === "m.megolm_backup.v1" ||
+ ev.getType() === BACKUP_DISABLED_ACCOUNT_DATA_KEY ||
+ ev.getType() === RECOVERY_ACCOUNT_DATA_KEY
) {
this.recheck();
}
@@ -285,6 +314,7 @@ export default class DeviceListener {
private async doRecheck(): Promise {
if (!this.running || !this.client) return; // we have been stopped
const logSpan = new LogSpan(logger, "check_" + secureRandomString(4));
+ logSpan.debug("starting recheck...");
const cli = this.client;
@@ -317,6 +347,9 @@ export default class DeviceListener {
crossSigningStatus.privateKeysCachedLocally.userSigningKey;
const defaultKeyId = await cli.secretStorage.getDefaultKeyId();
+ const recoveryDisabled = await this.recheckRecoveryDisabled(cli);
+
+ const recoveryIsOk = secretStorageReady || recoveryDisabled;
const isCurrentDeviceTrusted =
crossSigningReady &&
@@ -324,7 +357,15 @@ export default class DeviceListener {
(await crypto.getDeviceVerificationStatus(cli.getSafeUserId(), cli.deviceId!))?.crossSigningVerified,
);
- const allSystemsReady = crossSigningReady && secretStorageReady && allCrossSigningSecretsCached;
+ const keyBackupUploadActive = await this.isKeyBackupUploadActive(logSpan);
+ const backupDisabled = await this.recheckBackupDisabled(cli);
+
+ // We warn if key backup upload is turned off and we have not explicitly
+ // said we are OK with that.
+ const keyBackupIsOk = keyBackupUploadActive || backupDisabled;
+
+ const allSystemsReady = isCurrentDeviceTrusted && allCrossSigningSecretsCached && keyBackupIsOk && recoveryIsOk;
+
await this.reportCryptoSessionStateToAnalytics(cli);
if (this.dismissedThisDeviceToast || allSystemsReady) {
@@ -336,13 +377,8 @@ export default class DeviceListener {
// make sure our keys are finished downloading
await crypto.getUserDeviceInfo([cli.getSafeUserId()]);
- if (!crossSigningReady) {
- // This account is legacy and doesn't have cross-signing set up at all.
- // Prompt the user to set it up.
- logSpan.info("Cross-signing not ready: showing SET_UP_ENCRYPTION toast");
- showSetupEncryptionToast(SetupKind.SET_UP_ENCRYPTION);
- } else if (!isCurrentDeviceTrusted) {
- // cross signing is ready but the current device is not trusted: prompt the user to verify
+ if (!isCurrentDeviceTrusted) {
+ // the current device is not trusted: prompt the user to verify
logSpan.info("Current device not verified: showing VERIFY_THIS_SESSION toast");
showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION);
} else if (!allCrossSigningSecretsCached) {
@@ -353,26 +389,35 @@ export default class DeviceListener {
crossSigningStatus.privateKeysCachedLocally,
);
showSetupEncryptionToast(SetupKind.KEY_STORAGE_OUT_OF_SYNC);
+ } else if (!keyBackupIsOk) {
+ logSpan.info("Key backup upload is unexpectedly turned off: showing TURN_ON_KEY_STORAGE toast");
+ showSetupEncryptionToast(SetupKind.TURN_ON_KEY_STORAGE);
} else if (defaultKeyId === null) {
- // the user just hasn't set up 4S yet: prompt them to do so (unless they've explicitly said no to key storage)
- const disabledEvent = cli.getAccountData(BACKUP_DISABLED_ACCOUNT_DATA_KEY);
- if (!disabledEvent?.getContent().disabled) {
+ // The user just hasn't set up 4S yet: if they have key
+ // backup, prompt them to turn on recovery too. (If not, they
+ // have explicitly opted out, so don't hassle them.)
+ if (recoveryDisabled) {
+ logSpan.info("Recovery disabled: no toast needed");
+ hideSetupEncryptionToast();
+ } else if (keyBackupUploadActive) {
logSpan.info("No default 4S key: showing SET_UP_RECOVERY toast");
showSetupEncryptionToast(SetupKind.SET_UP_RECOVERY);
} else {
logSpan.info("No default 4S key but backup disabled: no toast needed");
+ hideSetupEncryptionToast();
}
} else {
- // some other condition... yikes! Show the 'set up encryption' toast: this is what we previously did
- // in 'other' situations. Possibly we should consider prompting for a full reset in this case?
- logSpan.warn("Couldn't match encryption state to a known case: showing 'setup encryption' prompt", {
+ // If we get here, then we are verified, have key backup, and
+ // 4S, but crypto.isSecretStorageReady returned false, which
+ // means that 4S doesn't have all the secrets.
+ logSpan.warn("4S is missing secrets", {
crossSigningReady,
secretStorageReady,
allCrossSigningSecretsCached,
isCurrentDeviceTrusted,
defaultKeyId,
});
- showSetupEncryptionToast(SetupKind.SET_UP_ENCRYPTION);
+ showSetupEncryptionToast(SetupKind.KEY_STORAGE_OUT_OF_SYNC_STORE);
}
} else {
logSpan.info("Not yet ready, but shouldShowSetupEncryptionToast==false");
@@ -443,6 +488,30 @@ export default class DeviceListener {
this.displayingToastsForDeviceIds = newUnverifiedDeviceIds;
}
+ /**
+ * Fetch the account data for `backup_disabled`. If this is the first time,
+ * fetch it from the server (in case the initial sync has not finished).
+ * Otherwise, fetch it from the store as normal.
+ */
+ private async recheckBackupDisabled(cli: MatrixClient): Promise {
+ const backupDisabled = await cli.getAccountDataFromServer(BACKUP_DISABLED_ACCOUNT_DATA_KEY);
+ return !!backupDisabled?.disabled;
+ }
+
+ /**
+ * Check whether the user has disabled recovery. If this is the first time,
+ * fetch it from the server (in case the initial sync has not finished).
+ * Otherwise, fetch it from the store as normal.
+ */
+ private async recheckRecoveryDisabled(cli: MatrixClient): Promise {
+ const recoveryStatus = await cli.getAccountDataFromServer(RECOVERY_ACCOUNT_DATA_KEY);
+ // Recovery is disabled only if the `enabled` flag is set to `false`.
+ // If it is missing, or set to any other value, we consider it as
+ // not-disabled, and will prompt the user to create recovery (if
+ // missing).
+ return recoveryStatus?.enabled === false;
+ }
+
/**
* Reports current recovery state to analytics.
* Checks if the session is verified and if the recovery is correctly set up (i.e all secrets known locally and in 4S).
@@ -512,18 +581,43 @@ export default class DeviceListener {
* trigger an auto-rageshake).
*/
private checkKeyBackupStatus = async (): Promise => {
- if (this.keyBackupStatusChecked || !this.client) {
- return;
+ if (!(await this.isKeyBackupUploadActive(logger))) {
+ dis.dispatch({ action: Action.ReportKeyBackupNotEnabled });
}
- const activeKeyBackupVersion = await this.client.getCrypto()?.getActiveSessionBackupVersion();
- // if key backup is enabled, no need to check this ever again (XXX: why only when it is enabled?)
- this.keyBackupStatusChecked = !!activeKeyBackupVersion;
+ };
- if (!activeKeyBackupVersion) {
- dis.dispatch({ action: Action.ReportKeyBackupNotEnabled });
+ /**
+ * Is key backup enabled? Use a cached answer if we have one.
+ */
+ private isKeyBackupUploadActive = async (logger: BaseLogger): Promise => {
+ if (!this.client) {
+ // To preserve existing behaviour, if there is no client, we
+ // pretend key backup upload is on.
+ //
+ // Someone looking to improve this code could try throwing an error
+ // here since we don't expect client to be undefined.
+ return true;
+ }
+
+ const crypto = this.client.getCrypto();
+ if (!crypto) {
+ // If there is no crypto, there is no key backup
+ return false;
+ }
+
+ // If we've already cached the answer, return it.
+ if (this.cachedKeyBackupUploadActive !== undefined) {
+ return this.cachedKeyBackupUploadActive;
}
+
+ // Fetch the answer and cache it
+ const activeKeyBackupVersion = await crypto.getActiveSessionBackupVersion();
+ this.cachedKeyBackupUploadActive = !!activeKeyBackupVersion;
+ logger.debug(`Key backup upload is ${this.cachedKeyBackupUploadActive ? "active" : "inactive"}`);
+
+ return this.cachedKeyBackupUploadActive;
};
- private keyBackupStatusChecked = false;
+ private cachedKeyBackupUploadActive: boolean | undefined = undefined;
private onRecordClientInformationSettingChange: CallbackFn = (
_originalSettingName,
diff --git a/src/IConfigOptions.ts b/src/IConfigOptions.ts
index 8b88a18075c..952d35e88d1 100644
--- a/src/IConfigOptions.ts
+++ b/src/IConfigOptions.ts
@@ -81,6 +81,7 @@ export interface IConfigOptions {
};
mobile_guide_toast?: boolean;
+ mobile_guide_app_variant?: "element" | "element-classic" | "element-pro";
default_theme?: "light" | "dark" | string; // custom themes are strings
default_country_code?: string; // ISO 3166 alpha2 country code
diff --git a/src/Keyboard.ts b/src/Keyboard.ts
index de7ab059c6f..5a7e7b59f10 100644
--- a/src/Keyboard.ts
+++ b/src/Keyboard.ts
@@ -79,3 +79,12 @@ export function isOnlyCtrlOrCmdKeyEvent(ev: React.KeyboardEvent | KeyboardEvent)
return ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
}
}
+
+/**
+ * Checks if the given keyboard event is a modified key event (i.e., if any modifier keys are active).
+ * @param ev The keyboard event to check
+ * @returns True if the event is a modified key event, false otherwise
+ */
+export function isModifiedKeyEvent(ev: React.KeyboardEvent | KeyboardEvent): boolean {
+ return ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey;
+}
diff --git a/src/LegacyCallHandler.tsx b/src/LegacyCallHandler.tsx
index 97cb4785120..e618fa62a01 100644
--- a/src/LegacyCallHandler.tsx
+++ b/src/LegacyCallHandler.tsx
@@ -112,6 +112,7 @@ export enum LegacyCallHandlerEvent {
CallsChanged = "calls_changed",
CallChangeRoom = "call_change_room",
SilencedCallsChanged = "silenced_calls_changed",
+ ShownSidebarsChanged = "shown_sidebars_changed",
CallState = "call_state",
ProtocolSupport = "protocol_support",
}
@@ -120,6 +121,7 @@ type EventEmitterMap = {
[LegacyCallHandlerEvent.CallsChanged]: (calls: Map) => void;
[LegacyCallHandlerEvent.CallChangeRoom]: (call: MatrixCall) => void;
[LegacyCallHandlerEvent.SilencedCallsChanged]: (calls: Set) => void;
+ [LegacyCallHandlerEvent.ShownSidebarsChanged]: (sidebarsShown: Map) => void;
[LegacyCallHandlerEvent.CallState]: (mappedRoomId: string | null, status: CallState) => void;
[LegacyCallHandlerEvent.ProtocolSupport]: () => void;
};
@@ -144,6 +146,8 @@ export default class LegacyCallHandler extends TypedEventEmitter(); // callIds
+ private shownSidebars = new Map(); // callId (call) -> sidebar show
+
private backgroundAudio = new BackgroundAudio();
private playingSources: Record = {}; // Record them for stopping
@@ -240,6 +244,15 @@ export default class LegacyCallHandler extends TypedEventEmitter {
try {
const protocols = await MatrixClientPeg.safeGet().getThirdpartyProtocols();
@@ -679,7 +692,7 @@ export default class LegacyCallHandler extends TypedEventEmitter {
- SettingsStore.setValue("fallbackICEServerAllowed", null, SettingLevel.DEVICE, allow);
- },
},
undefined,
true,
);
+
+ finished.then(([allow]) => {
+ SettingsStore.setValue("fallbackICEServerAllowed", null, SettingLevel.DEVICE, allow);
+ });
}
private showMediaCaptureError(call: MatrixCall): void {
diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts
index c1559886b47..dcd4ae9c004 100644
--- a/src/Lifecycle.ts
+++ b/src/Lifecycle.ts
@@ -469,12 +469,15 @@ type TryAgainFunction = () => void;
* @param tryAgain OPTIONAL function to call on try again button from error dialog
*/
function onFailedDelegatedAuthLogin(description: string | ReactNode, tryAgain?: TryAgainFunction): void {
- Modal.createDialog(ErrorDialog, {
+ const { finished } = Modal.createDialog(ErrorDialog, {
title: _t("auth|oidc|error_title"),
description,
button: _t("action|try_again"),
+ });
+
+ finished.then(([shouldTryAgain]) => {
// if we have a tryAgain callback, call it the primary 'try again' button was clicked in the dialog
- onFinished: tryAgain ? (shouldTryAgain?: boolean) => shouldTryAgain && tryAgain() : undefined,
+ if (shouldTryAgain) tryAgain?.();
});
}
@@ -618,6 +621,9 @@ export async function restoreSessionFromStorage(opts?: { ignoreGuest?: boolean }
await getStoredSessionVars();
if (hasAccessToken && !accessToken) {
+ logger.warn(
+ "restoreSessionFromStorage: storage indicates we should have an access token, but we do not. Displaying StorageEvictedDialog",
+ );
await abortLogin();
}
@@ -654,7 +660,7 @@ export async function restoreSessionFromStorage(opts?: { ignoreGuest?: boolean }
freshLogin: freshLogin,
},
false,
- false,
+ freshLogin,
);
return true;
} else {
@@ -820,6 +826,7 @@ async function doSetLoggedIn(
// crypto store, we'll be generally confused when handling encrypted data.
// Show a modal recommending a full reset of storage.
if (results.dataInLocalStorage && results.cryptoInited && !results.dataInCryptoStore) {
+ logger.warn("doSetLoggedIn: StorageManager consistency check failed; displaying StorageEvictedDialog.");
await abortLogin();
}
@@ -1112,7 +1119,9 @@ export async function onLoggedOut(): Promise {
* @param {object} opts Options for how to clear storage.
* @returns {Promise} promise which resolves once the stores have been cleared
*/
-async function clearStorage(opts?: { deleteEverything?: boolean }): Promise {
+export async function clearStorage(opts?: { deleteEverything?: boolean }): Promise {
+ logger.info(`Clearing storage, deleteEverything=${opts?.deleteEverything}`);
+
if (window.localStorage) {
// get the currently defined device language, if set, so we can restore it later
const language = SettingsStore.getValueAt(SettingLevel.DEVICE, "language", null, true, true);
diff --git a/src/Modal.tsx b/src/Modal.tsx
index ba5c6702bdd..e2873783ea3 100644
--- a/src/Modal.tsx
+++ b/src/Modal.tsx
@@ -10,7 +10,6 @@ Please see LICENSE files in the repository root for full details.
import React, { StrictMode } from "react";
import { createRoot, type Root } from "react-dom/client";
import classNames from "classnames";
-import { type IDeferred, defer } from "matrix-js-sdk/src/utils";
import { TypedEventEmitter } from "matrix-js-sdk/src/matrix";
import { Glass, TooltipProvider } from "@vector-im/compound-web";
@@ -30,12 +29,25 @@ export type ComponentType =
}>
| React.ComponentType;
-// Generic type which returns the props of the Modal component with the onFinished being optional.
+/**
+ * The parameter types of the `onFinished` callback property exposed by the component which forms the
+ * body of the dialog.
+ *
+ * @typeParam C - The type of the React component which forms the body of the dialog.
+ */
+type OnFinishedParams = Parameters["onFinished"]>;
+
+/**
+ * The properties exposed by the `props` argument to {@link Modal.createDialog}: the same as
+ * those exposed by the underlying component, with the exception of `onFinished`, which is provided by
+ * `createDialog`.
+ *
+ * @typeParam C - The type of the React component which forms the body of the dialog.
+ */
export type ComponentProps = Defaultize<
Omit, "onFinished">,
C["defaultProps"]
-> &
- Partial, "onFinished">>;
+>;
export interface IModal {
elem: React.ReactNode;
@@ -43,15 +55,44 @@ export interface IModal {
beforeClosePromise?: Promise;
closeReason?: ModalCloseReason;
onBeforeClose?(reason?: ModalCloseReason): Promise;
- onFinished: ComponentProps["onFinished"];
- close(...args: Parameters["onFinished"]>): void;
+
+ /**
+ * Run the {@link deferred} with the given arguments, and close this modal.
+ *
+ * This method is passed as the `onFinished` callback to the underlying component,
+ * as well as being returned by {@link Modal.createDialog} to the caller.
+ */
+ close(...args: OnFinishedParams | []): void;
+
hidden?: boolean;
- deferred?: IDeferred["onFinished"]>>;
+
+ /** A deferred to resolve when the dialog closes, with the results as provided by
+ * the call to {@link close} (normally from the `onFinished` callback).
+ */
+ deferred?: PromiseWithResolvers | []>;
}
+/** The result of {@link Modal.createDialog}.
+ *
+ * @typeParam C - The type of the React component which forms the body of the dialog.
+ */
export interface IHandle {
- finished: Promise["onFinished"]>>;
- close(...args: Parameters["onFinished"]>): void;
+ /**
+ * A promise which will resolve when the dialog closes.
+ *
+ * If the dialog body component calls the `onFinished` property, or the caller calls {@link close},
+ * the promise resolves with an array holding the arguments to that call.
+ *
+ * If the dialog is closed by clicking in the background, the promise resolves with an empty array.
+ */
+ finished: Promise | []>;
+
+ /**
+ * A function which, if called, will close the dialog.
+ *
+ * @param args - Arguments to return to {@link finished}.
+ */
+ close(...args: OnFinishedParams): void;
}
interface IOptions {
@@ -164,7 +205,6 @@ export class ModalManager extends TypedEventEmitter["finished"];
} {
const modal = {
- onFinished: props?.onFinished,
onBeforeClose: options?.onBeforeClose,
className,
@@ -196,8 +235,7 @@ export class ModalManager extends TypedEventEmitter;
- // never call this from onFinished() otherwise it will loop
- const [closeDialog, onFinishedProm] = this.getCloseFn(modal, props);
+ const [closeDialog, onFinishedProm] = this.getCloseFn(modal);
// don't attempt to reuse the same AsyncWrapper for different dialogs,
// otherwise we'll get confused.
@@ -214,13 +252,10 @@ export class ModalManager extends TypedEventEmitter(
- modal: IModal,
- props?: ComponentProps,
- ): [IHandle["close"], IHandle["finished"]] {
- modal.deferred = defer["onFinished"]>>();
+ private getCloseFn(modal: IModal): [IHandle["close"], IHandle["finished"]] {
+ modal.deferred = Promise.withResolvers | []>();
return [
- async (...args: Parameters["onFinished"]>): Promise => {
+ async (...args: OnFinishedParams): Promise => {
if (modal.beforeClosePromise) {
await modal.beforeClosePromise;
} else if (modal.onBeforeClose) {
@@ -232,7 +267,6 @@ export class ModalManager extends TypedEventEmitter= 0) {
this.modals.splice(i, 1);
@@ -280,7 +314,8 @@ export class ModalManager extends TypedEventEmitter import('./MyComponent'))`
*
- * @param props properties to pass to the displayed component. (We will also pass an 'onFinished' property.)
+ * @param props properties to pass to the displayed component. (We will also pass an `onFinished` property; when
+ * called, that property will close the dialog and return the results to the caller via {@link IHandle.finished}.)
*
* @param className CSS class to apply to the modal wrapper
*
@@ -295,7 +330,7 @@ export class ModalManager extends TypedEventEmitter(
component: C,
diff --git a/src/Registration.tsx b/src/Registration.tsx
index ea0264fab35..22eb6e15ff5 100644
--- a/src/Registration.tsx
+++ b/src/Registration.tsx
@@ -62,14 +62,14 @@ export async function startAnyRegistrationFlow(
,
]
: [],
- onFinished: (proceed) => {
- if (proceed) {
- dis.dispatch({ action: "start_login", screenAfterLogin: options.screen_after });
- } else if (options.go_home_on_cancel) {
- dis.dispatch({ action: Action.ViewHomePage });
- } else if (options.go_welcome_on_cancel) {
- dis.dispatch({ action: "view_welcome_page" });
- }
- },
+ });
+ modal.finished.then(([proceed]) => {
+ if (proceed) {
+ dis.dispatch({ action: "start_login", screenAfterLogin: options.screen_after });
+ } else if (options.go_home_on_cancel) {
+ dis.dispatch({ action: Action.ViewHomePage });
+ } else if (options.go_welcome_on_cancel) {
+ dis.dispatch({ action: "view_welcome_page" });
+ }
});
}
diff --git a/src/RoomAliasCache.ts b/src/RoomAliasCache.ts
index fd611a7559f..ff57a70e6c0 100644
--- a/src/RoomAliasCache.ts
+++ b/src/RoomAliasCache.ts
@@ -6,6 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
+type CacheResult = { roomId: string; viaServers: string[] };
+
/**
* This is meant to be a cache of room alias to room ID so that moving between
* rooms happens smoothly (for example using browser back / forward buttons).
@@ -16,12 +18,12 @@ Please see LICENSE files in the repository root for full details.
* A similar thing could also be achieved via `pushState` with a state object,
* but keeping it separate like this seems easier in case we do want to extend.
*/
-const aliasToIDMap = new Map();
+const cache = new Map();
-export function storeRoomAliasInCache(alias: string, id: string): void {
- aliasToIDMap.set(alias, id);
+export function storeRoomAliasInCache(alias: string, roomId: string, viaServers: string[]): void {
+ cache.set(alias, { roomId, viaServers });
}
-export function getCachedRoomIDForAlias(alias: string): string | undefined {
- return aliasToIDMap.get(alias);
+export function getCachedRoomIdForAlias(alias: string): CacheResult | undefined {
+ return cache.get(alias);
}
diff --git a/src/RoomInvite.tsx b/src/RoomInvite.tsx
index a02530a1cf8..feefdf72444 100644
--- a/src/RoomInvite.tsx
+++ b/src/RoomInvite.tsx
@@ -7,10 +7,9 @@ Please see LICENSE files in the repository root for full details.
*/
import React, { type ComponentProps } from "react";
-import { type Room, type MatrixEvent, type MatrixClient, type User, EventType } from "matrix-js-sdk/src/matrix";
-import { logger } from "matrix-js-sdk/src/logger";
+import { EventType, type MatrixClient, type MatrixEvent, type Room, type User } from "matrix-js-sdk/src/matrix";
-import MultiInviter, { type CompletionStates } from "./utils/MultiInviter";
+import MultiInviter, { type CompletionStates, type MultiInviterOptions } from "./utils/MultiInviter";
import Modal from "./Modal";
import { _t } from "./languageHandler";
import InviteDialog from "./components/views/dialogs/InviteDialog";
@@ -26,22 +25,24 @@ export interface IInviteResult {
}
/**
- * Invites multiple addresses to a room
- * Simpler interface to utils/MultiInviter but with
- * no option to cancel.
+ * Invites multiple addresses to a room.
+ *
+ * Simpler interface to {@link MultiInviter}.
+ *
+ * Any failures are returned via the `states` in the result.
*
* @param {string} roomId The ID of the room to invite to
* @param {string[]} addresses Array of strings of addresses to invite. May be matrix IDs or 3pids.
- * @param {function} progressCallback optional callback, fired after each invite.
+ * @param options Options object.
* @returns {Promise} Promise
*/
export async function inviteMultipleToRoom(
client: MatrixClient,
roomId: string,
addresses: string[],
- progressCallback?: () => void,
+ options: MultiInviterOptions = {},
): Promise {
- const inviter = new MultiInviter(client, roomId, progressCallback);
+ const inviter = new MultiInviter(client, roomId, options);
return { states: await inviter.invite(addresses), inviter };
}
@@ -89,26 +90,6 @@ export function isValid3pidInvite(event: MatrixEvent): boolean {
return true;
}
-export function inviteUsersToRoom(
- client: MatrixClient,
- roomId: string,
- userIds: string[],
- progressCallback?: () => void,
-): Promise {
- return inviteMultipleToRoom(client, roomId, userIds, progressCallback)
- .then((result) => {
- const room = client.getRoom(roomId)!;
- showAnyInviteErrors(result.states, room, result.inviter);
- })
- .catch((err) => {
- logger.error(err.stack);
- Modal.createDialog(ErrorDialog, {
- title: _t("invite|failed_title"),
- description: err?.message ?? _t("invite|failed_generic"),
- });
- });
-}
-
export function showAnyInviteErrors(
states: CompletionStates,
room: Room,
diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts
index b497c57a10c..ecf895fd797 100644
--- a/src/SecurityManager.ts
+++ b/src/SecurityManager.ts
@@ -19,7 +19,6 @@ import AccessSecretStorageDialog, {
type KeyParams,
} from "./components/views/dialogs/security/AccessSecretStorageDialog";
import { ModuleRunner } from "./modules/ModuleRunner";
-import QuestionDialog from "./components/views/dialogs/QuestionDialog";
import InteractiveAuthDialog from "./components/views/dialogs/InteractiveAuthDialog";
// This stores the secret storage private keys in memory for the JS SDK. This is
@@ -50,17 +49,6 @@ export class AccessCancelledError extends Error {
}
}
-async function confirmToDismiss(): Promise {
- const [sure] = await Modal.createDialog(QuestionDialog, {
- title: _t("encryption|cancel_entering_passphrase_title"),
- description: _t("encryption|cancel_entering_passphrase_description"),
- danger: false,
- button: _t("action|go_back"),
- cancelButton: _t("action|cancel"),
- }).finished;
- return !sure;
-}
-
function makeInputToKey(
keyInfo: SecretStorage.SecretStorageKeyDescription,
): (keyParams: KeyParams) => Promise {
@@ -134,17 +122,6 @@ async function getSecretStorageKey(
return MatrixClientPeg.safeGet().secretStorage.checkKey(key, keyInfo);
},
},
- /* className= */ undefined,
- /* isPriorityModal= */ false,
- /* isStaticModal= */ false,
- /* options= */ {
- onBeforeClose: async (reason): Promise => {
- if (reason === "backgroundClick") {
- return confirmToDismiss();
- }
- return true;
- },
- },
);
const [keyParams] = await finished;
if (!keyParams) {
@@ -199,10 +176,11 @@ export async function withSecretStorageKeyCache(func: () => Promise): Prom
}
export interface AccessSecretStorageOpts {
- /** Reset secret storage even if it's already set up. */
+ /**
+ * Reset secret storage even if it's already set up.
+ * @deprecated send the user to the Encryption settings tab to reset secret storage
+ */
forceReset?: boolean;
- /** Create new cross-signing keys. Only applicable if `forceReset` is `true`. */
- resetCrossSigning?: boolean;
}
/**
@@ -212,8 +190,8 @@ export interface AccessSecretStorageOpts {
* provided function.
*
* Bootstrapping secret storage may take one of these paths:
- * 1. Create secret storage from a passphrase and store cross-signing keys
- * in secret storage.
+ * 1. (Only if `opts.forceReset` is set) create secret storage from a passphrase
+ * and store cross-signing keys in secret storage.
* 2. Access existing secret storage by requesting passphrase and accessing
* cross-signing keys as needed.
* 3. All keys are loaded and there's nothing to do.
@@ -222,6 +200,8 @@ export interface AccessSecretStorageOpts {
* to ensure the user is prompted only once for their secret storage
* passphrase. The cache is then cleared once the provided function completes.
*
+ * Throws an error if secret storage is not set up (and `opts.forceReset` is not set)
+ *
* @param {Function} [func] An operation to perform once secret storage has been
* bootstrapped. Optional.
* @param [opts] The options to use when accessing secret storage.
@@ -242,16 +222,8 @@ async function doAccessSecretStorage(func: () => Promise, opts: AccessSecr
throw new Error("End-to-end encryption is disabled - unable to access secret storage.");
}
- let createNew = false;
if (opts.forceReset) {
logger.debug("accessSecretStorage: resetting 4S");
- createNew = true;
- } else if (!(await cli.secretStorage.hasKey())) {
- logger.debug("accessSecretStorage: no 4S key configured, creating a new one");
- createNew = true;
- }
-
- if (createNew) {
// This dialog calls bootstrap itself after guiding the user through
// passphrase creation.
const { finished } = Modal.createDialog(
@@ -274,6 +246,9 @@ async function doAccessSecretStorage(func: () => Promise, opts: AccessSecr
if (!confirmed) {
throw new Error("Secret storage creation canceled");
}
+ } else if (!(await cli.secretStorage.hasKey())) {
+ logger.debug("accessSecretStorage: no 4S key configured");
+ throw new Error("Secret storage has not been created yet.");
} else {
logger.debug("accessSecretStorage: bootstrapCrossSigning");
await crypto.bootstrapCrossSigning({
diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx
index afbfeeca03e..f0d9085507b 100644
--- a/src/SlashCommands.tsx
+++ b/src/SlashCommands.tsx
@@ -60,6 +60,7 @@ import { deop, op } from "./slash-commands/op";
import { CommandCategories } from "./slash-commands/interface";
import { Command } from "./slash-commands/command";
import { goto, join } from "./slash-commands/join";
+import { manuallyVerifyDevice } from "./components/views/dialogs/ManualDeviceKeyVerificationDialog";
export { CommandCategories, Command };
@@ -147,7 +148,7 @@ export const Commands = [
command: "upgraderoom",
args: "",
description: _td("slash_command|upgraderoom"),
- isEnabled: (cli) => !isCurrentLocalRoom(cli) && SettingsStore.getValue("developerMode"),
+ isEnabled: (cli) => !isCurrentLocalRoom(cli),
runFn: function (cli, roomId, threadId, args) {
if (args) {
const room = cli.getRoom(roomId);
@@ -663,6 +664,36 @@ export const Commands = [
category: CommandCategories.admin,
renderingTypes: [TimelineRenderingType.Room],
}),
+ new Command({
+ command: "verify",
+ args: " ",
+ description: _td("slash_command|verify"),
+ runFn: function (cli, _roomId, _threadId, args) {
+ if (args) {
+ const matches = args.match(/^(\S+) +(\S+)$/);
+ if (matches) {
+ const deviceId = matches[1];
+ const fingerprint = matches[2];
+
+ const { finished } = Modal.createDialog(QuestionDialog, {
+ title: _t("slash_command|manual_device_verification_confirm_title"),
+ description: _t("slash_command|manual_device_verification_confirm_description"),
+ button: _t("action|verify"),
+ danger: true,
+ });
+
+ return success(
+ finished.then(([confirmed]) => {
+ if (confirmed) manuallyVerifyDevice(cli, deviceId, fingerprint);
+ }),
+ );
+ }
+ }
+ return reject(this.getUsage());
+ },
+ category: CommandCategories.advanced,
+ renderingTypes: [TimelineRenderingType.Room],
+ }),
new Command({
command: "discardsession",
description: _td("slash_command|discardsession"),
diff --git a/src/SlidingSyncManager.ts b/src/SlidingSyncManager.ts
index 88b839312d1..815e438da75 100644
--- a/src/SlidingSyncManager.ts
+++ b/src/SlidingSyncManager.ts
@@ -49,7 +49,7 @@ import {
SlidingSyncState,
} from "matrix-js-sdk/src/sliding-sync";
import { logger } from "matrix-js-sdk/src/logger";
-import { defer, sleep } from "matrix-js-sdk/src/utils";
+import { sleep } from "matrix-js-sdk/src/utils";
// how long to long poll for
const SLIDING_SYNC_TIMEOUT_MS = 20 * 1000;
@@ -184,7 +184,7 @@ export class SlidingSyncManager {
public slidingSync?: SlidingSync;
private client?: MatrixClient;
- private configureDefer = defer();
+ private configureDefer = Promise.withResolvers();
public static get instance(): SlidingSyncManager {
return SlidingSyncManager.internalInstance;
diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx
index b63e5b2a00e..60ada112e05 100644
--- a/src/TextForEvent.tsx
+++ b/src/TextForEvent.tsx
@@ -36,9 +36,9 @@ import { RoomSettingsTab } from "./components/views/dialogs/RoomSettingsDialog";
import AccessibleButton from "./components/views/elements/AccessibleButton";
import RightPanelStore from "./stores/right-panel/RightPanelStore";
import { highlightEvent, isLocationEvent } from "./utils/EventUtils";
-import { ElementCall } from "./models/Call";
import { getSenderName } from "./utils/event/getSenderName";
import PosthogTrackers from "./PosthogTrackers.ts";
+import { ElementCallEventType } from "./call-types.ts";
function getRoomMemberDisplayname(client: MatrixClient, event: MatrixEvent, userId = event.getSender()): string {
const roomId = event.getRoomId();
@@ -572,8 +572,11 @@ function textForPinnedEvent(event: MatrixEvent, client: MatrixClient, allowJSX:
const senderName = getSenderName(event);
const roomId = event.getRoomId()!;
- const pinned = event.getContent<{ pinned: string[] }>().pinned ?? [];
- const previouslyPinned: string[] = event.getPrevContent().pinned ?? [];
+ const content = event.getContent<{ pinned: string[] }>();
+ const prevContent = event.getPrevContent();
+
+ const pinned = Array.isArray(content.pinned) ? content.pinned : [];
+ const previouslyPinned: string[] = Array.isArray(prevContent.pinned) ? prevContent.pinned : [];
const newlyPinned = pinned.filter((item) => previouslyPinned.indexOf(item) < 0);
const newlyUnpinned = previouslyPinned.filter((item) => pinned.indexOf(item) < 0);
@@ -922,7 +925,7 @@ for (const evType of ALL_RULE_TYPES) {
}
// Add both stable and unstable m.call events
-for (const evType of ElementCall.CALL_EVENT_TYPE.names) {
+for (const evType of ElementCallEventType.names) {
stateHandlers[evType] = textForCallEvent;
}
diff --git a/src/Unread.ts b/src/Unread.ts
index e8f4769e25f..d6a80a8f97e 100644
--- a/src/Unread.ts
+++ b/src/Unread.ts
@@ -39,7 +39,12 @@ export function eventTriggersUnreadCount(client: MatrixClient, ev: MatrixEvent):
}
if (ev.isRedacted()) return false;
- return haveRendererForEvent(ev, client, false /* hidden messages should never trigger unread counts anyways */);
+ try {
+ return haveRendererForEvent(ev, client, false /* hidden messages should never trigger unread counts anyways */);
+ } catch (e) {
+ console.warn("Error determining if event should trigger unread count", e);
+ return false; // If we can't determine if the event should trigger an unread count, assume it does not.
+ }
}
export function doesRoomHaveUnreadMessages(room: Room, includeThreads: boolean): boolean {
diff --git a/src/WorkerManager.ts b/src/WorkerManager.ts
index a8a95ca727d..1d403be88c2 100644
--- a/src/WorkerManager.ts
+++ b/src/WorkerManager.ts
@@ -6,14 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
-import { defer, type IDeferred } from "matrix-js-sdk/src/utils";
-
import { type WorkerPayload } from "./workers/worker";
export class WorkerManager {
private readonly worker: Worker;
private seq = 0;
- private pendingDeferredMap = new Map>();
+ private pendingDeferredMap = new Map>();
public constructor(worker: Worker) {
this.worker = worker;
@@ -30,7 +28,7 @@ export class WorkerManager {
public call(request: Request): Promise {
const seq = this.seq++;
- const deferred = defer();
+ const deferred = Promise.withResolvers();
this.pendingDeferredMap.set(seq, deferred);
this.worker.postMessage({ seq, ...request });
return deferred.promise;
diff --git a/src/accessibility/KeyboardShortcuts.ts b/src/accessibility/KeyboardShortcuts.ts
index da0097f4b27..21ea5abb02f 100644
--- a/src/accessibility/KeyboardShortcuts.ts
+++ b/src/accessibility/KeyboardShortcuts.ts
@@ -8,7 +8,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
-import { _td, type TranslationKey } from "../languageHandler";
+// Import i18n.tsx instead of languageHandler to avoid circular deps
+import { _td, type TranslationKey } from "../shared-components/utils/i18n";
import { IS_MAC, IS_ELECTRON, Key } from "../Keyboard";
import { type IBaseSetting } from "../settings/Settings";
import { type KeyCombo } from "../KeyBindingsManager";
@@ -145,6 +146,7 @@ export enum KeyBindingAction {
ArrowDown = "KeyBinding.arrowDown",
Tab = "KeyBinding.tab",
Comma = "KeyBinding.comma",
+ Save = "KeyBinding.save",
/** Toggle visibility of hidden events */
ToggleHiddenEventVisibility = "KeyBinding.toggleHiddenEventVisibility",
@@ -268,6 +270,7 @@ export const CATEGORIES: Record = {
KeyBindingAction.ArrowRight,
KeyBindingAction.ArrowDown,
KeyBindingAction.Comma,
+ KeyBindingAction.Save,
],
},
[CategoryName.NAVIGATION]: {
@@ -521,7 +524,8 @@ export const KEYBOARD_SHORTCUTS: IKeyboardShortcuts = {
[KeyBindingAction.GoToHome]: {
default: {
ctrlKey: true,
- altKey: true,
+ altKey: !IS_MAC,
+ shiftKey: IS_MAC,
key: Key.H,
},
displayName: _td("keyboard|go_home_view"),
@@ -586,7 +590,7 @@ export const KEYBOARD_SHORTCUTS: IKeyboardShortcuts = {
default: {
ctrlKey: true,
shiftKey: true,
- key: Key.H,
+ key: Key.J,
},
displayName: _td("keyboard|toggle_hidden_events"),
},
@@ -619,6 +623,13 @@ export const KEYBOARD_SHORTCUTS: IKeyboardShortcuts = {
},
displayName: _td("keyboard|composer_redo"),
},
+ [KeyBindingAction.Save]: {
+ default: {
+ key: Key.S,
+ ctrlOrCmdKey: true,
+ },
+ displayName: _td("keyboard|save"),
+ },
[KeyBindingAction.PreviousVisitedRoomOrSpace]: {
default: {
metaKey: IS_MAC,
diff --git a/src/async-components/structures/ErrorView.tsx b/src/async-components/structures/ErrorView.tsx
index 0f9a61781ed..5ce2a900a1c 100644
--- a/src/async-components/structures/ErrorView.tsx
+++ b/src/async-components/structures/ErrorView.tsx
@@ -10,7 +10,7 @@ import { Text, Heading, Button, Separator } from "@vector-im/compound-web";
import PopOutIcon from "@vector-im/compound-design-tokens/assets/web/icons/pop-out";
import SdkConfig from "../../SdkConfig";
-import { Flex } from "../../components/utils/Flex";
+import { Flex } from "../../shared-components/utils/Flex";
import { _t } from "../../languageHandler";
import { Icon as AppleIcon } from "../../../res/themes/element/img/compound/apple.svg";
import { Icon as MicrosoftIcon } from "../../../res/themes/element/img/compound/microsoft.svg";
@@ -58,7 +58,7 @@ const MobileAppLinks: React.FC<{
googlePlayUrl?: string;
fdroidUrl?: string;
}> = ({ appleAppStoreUrl, googlePlayUrl, fdroidUrl }) => (
-
+
{appleAppStoreUrl && (
@@ -84,7 +84,7 @@ const DesktopAppLinks: React.FC<{
linuxUrl?: string;
}> = ({ macOsUrl, win64Url, win64ArmUrl, linuxUrl }) => {
return (
-
+
{macOsUrl && (