Merge branch 'release-v3.34.0'

This commit is contained in:
RiotRobot 2021-11-08 17:51:49 +00:00
commit 712f5630e3
217 changed files with 6762 additions and 2677 deletions

View file

@ -10,7 +10,7 @@ on:
jobs:
end-to-end:
runs-on: ubuntu-latest
env:
env:
PR_NUMBER: ${{github.event.number}}
container: vectorim/element-web-ci-e2etests-env:latest
steps:

View file

@ -0,0 +1,15 @@
name: Notify element-web
on:
push:
branches: [develop]
jobs:
notify-element-web:
runs-on: ubuntu-latest
environment: develop
steps:
- name: Notify element-web repo that a new SDK build is on develop
uses: peter-evans/repository-dispatch@v1
with:
token: ${{ secrets.ELEMENT_WEB_NOTIFY_TOKEN }}
repository: vector-im/element-web
event-type: element-web-notify

View file

@ -11,7 +11,8 @@ module.exports = {
"length-zero-no-unit": null,
"rule-empty-line-before": null,
"color-hex-length": null,
"max-empty-lines": null,
"max-empty-lines": 1,
"no-eol-whitespace": true,
"number-no-trailing-zeros": null,
"number-leading-zero": null,
"selector-list-comma-newline-after": null,

View file

@ -1,3 +1,60 @@
Changes in [3.34.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.34.0) (2021-11-08)
=====================================================================================================
## ✨ Features
* Improve the look of tooltips ([\#7049](https://github.com/matrix-org/matrix-react-sdk/pull/7049)). Contributed by @SimonBrandner.
* Improve the look of the spinner ([\#6083](https://github.com/matrix-org/matrix-react-sdk/pull/6083)). Contributed by @SimonBrandner.
* Polls: Creation form & start event ([\#7001](https://github.com/matrix-org/matrix-react-sdk/pull/7001)).
* Show a gray shield when encrypted by deleted session ([\#6119](https://github.com/matrix-org/matrix-react-sdk/pull/6119)). Contributed by @SimonBrandner.
* Silence some widgets for better screen reader presentation. ([\#7057](https://github.com/matrix-org/matrix-react-sdk/pull/7057)). Contributed by @ndarilek.
* Make message separator more accessible. ([\#7056](https://github.com/matrix-org/matrix-react-sdk/pull/7056)). Contributed by @ndarilek.
* Give each room directory entry the `listitem` role to correspond with the containing `list`. ([\#7035](https://github.com/matrix-org/matrix-react-sdk/pull/7035)). Contributed by @ndarilek.
* Implement RequiresClient capability for widgets ([\#7005](https://github.com/matrix-org/matrix-react-sdk/pull/7005)). Fixes vector-im/element-web#15744 and vector-im/element-web#15744.
* Respect the system high contrast setting when using system theme ([\#7043](https://github.com/matrix-org/matrix-react-sdk/pull/7043)).
* Remove redundant duplicate mimetype field which doesn't conform to spec ([\#7045](https://github.com/matrix-org/matrix-react-sdk/pull/7045)). Fixes vector-im/element-web#17145 and vector-im/element-web#17145.
* Make join button on space hierarchy action in the background ([\#7041](https://github.com/matrix-org/matrix-react-sdk/pull/7041)). Fixes vector-im/element-web#17388 and vector-im/element-web#17388.
* Add a high contrast theme (a variant of the light theme) ([\#7036](https://github.com/matrix-org/matrix-react-sdk/pull/7036)).
* Improve timeline message for restricted join rule changes ([\#6984](https://github.com/matrix-org/matrix-react-sdk/pull/6984)). Fixes vector-im/element-web#18980 and vector-im/element-web#18980.
* Improve the appearance of the font size slider ([\#7038](https://github.com/matrix-org/matrix-react-sdk/pull/7038)).
* Improve RovingTabIndex & Room List filtering performance ([\#6987](https://github.com/matrix-org/matrix-react-sdk/pull/6987)). Fixes vector-im/element-web#17864 and vector-im/element-web#17864.
* Remove outdated Spaces restricted rooms warning ([\#6927](https://github.com/matrix-org/matrix-react-sdk/pull/6927)).
* Make /msg <message> param optional for more flexibility ([\#7028](https://github.com/matrix-org/matrix-react-sdk/pull/7028)). Fixes vector-im/element-web#19481 and vector-im/element-web#19481.
* Add decoration to space hierarchy for tiles which have already been j… ([\#6969](https://github.com/matrix-org/matrix-react-sdk/pull/6969)). Fixes vector-im/element-web#18755 and vector-im/element-web#18755.
* Add insert link button to the format bar ([\#5879](https://github.com/matrix-org/matrix-react-sdk/pull/5879)). Contributed by @SimonBrandner.
* Improve visibility of font size chooser ([\#6988](https://github.com/matrix-org/matrix-react-sdk/pull/6988)).
* Soften border-radius on selected/hovered messages ([\#6525](https://github.com/matrix-org/matrix-react-sdk/pull/6525)). Fixes vector-im/element-web#18108. Contributed by @SimonBrandner.
* Add a developer mode flag and use it for accessing space timelines ([\#6994](https://github.com/matrix-org/matrix-react-sdk/pull/6994)). Fixes vector-im/element-web#19416 and vector-im/element-web#19416.
* Position toggle switch more clearly ([\#6914](https://github.com/matrix-org/matrix-react-sdk/pull/6914)). Contributed by @CicadaCinema.
* Validate email address in forgot password dialog ([\#6983](https://github.com/matrix-org/matrix-react-sdk/pull/6983)). Fixes vector-im/element-web#9978 and vector-im/element-web#9978. Contributed by @psrpinto.
* Handle and i18n M_THREEPID_IN_USE during registration ([\#6986](https://github.com/matrix-org/matrix-react-sdk/pull/6986)). Fixes vector-im/element-web#13767 and vector-im/element-web#13767.
* For space invite previews, use room summary API to get the right member count ([\#6982](https://github.com/matrix-org/matrix-react-sdk/pull/6982)). Fixes vector-im/element-web#19123 and vector-im/element-web#19123.
* Simplify Space Panel notification badge layout ([\#6977](https://github.com/matrix-org/matrix-react-sdk/pull/6977)). Fixes vector-im/element-web#18527 and vector-im/element-web#18527.
* Use prettier hsName during 3pid registration where possible ([\#6980](https://github.com/matrix-org/matrix-react-sdk/pull/6980)). Fixes vector-im/element-web#19162 and vector-im/element-web#19162.
## 🐛 Bug Fixes
* Add a condition to only activate the resizer which belongs to the clicked handle ([\#7055](https://github.com/matrix-org/matrix-react-sdk/pull/7055)). Fixes vector-im/element-web#19521 and vector-im/element-web#19521.
* Restore composer focus after event edit ([\#7065](https://github.com/matrix-org/matrix-react-sdk/pull/7065)). Fixes vector-im/element-web#19469 and vector-im/element-web#19469.
* Don't apply message bubble visual style to media messages ([\#7040](https://github.com/matrix-org/matrix-react-sdk/pull/7040)).
* Handle no selected screen when screen-sharing ([\#7018](https://github.com/matrix-org/matrix-react-sdk/pull/7018)). Fixes vector-im/element-web#19460 and vector-im/element-web#19460. Contributed by @SimonBrandner.
* Add history entry before completing emoji ([\#7007](https://github.com/matrix-org/matrix-react-sdk/pull/7007)). Fixes vector-im/element-web#19177 and vector-im/element-web#19177. Contributed by @RafaelGoncalves8.
* Add padding between controls on edit form in message bubbles ([\#7039](https://github.com/matrix-org/matrix-react-sdk/pull/7039)).
* Respect the roomState right container request for the Jitsi widget ([\#7033](https://github.com/matrix-org/matrix-react-sdk/pull/7033)). Fixes vector-im/element-web#16552 and vector-im/element-web#16552.
* Fix cannot read length of undefined for room upgrades ([\#7037](https://github.com/matrix-org/matrix-react-sdk/pull/7037)). Fixes vector-im/element-web#19509 and vector-im/element-web#19509.
* Cleanup re-dispatching around timelines and composers ([\#7023](https://github.com/matrix-org/matrix-react-sdk/pull/7023)). Fixes vector-im/element-web#19491 and vector-im/element-web#19491. Contributed by @SimonBrandner.
* Fix removing a room from a Space and interaction with `m.space.parent` ([\#6944](https://github.com/matrix-org/matrix-react-sdk/pull/6944)). Fixes vector-im/element-web#19363 and vector-im/element-web#19363.
* Fix recent css regression ([\#7022](https://github.com/matrix-org/matrix-react-sdk/pull/7022)). Fixes vector-im/element-web#19470 and vector-im/element-web#19470. Contributed by @CicadaCinema.
* Fix ModalManager reRender racing with itself ([\#7027](https://github.com/matrix-org/matrix-react-sdk/pull/7027)). Fixes vector-im/element-web#19489 and vector-im/element-web#19489.
* Fix fullscreening a call while connecting ([\#7019](https://github.com/matrix-org/matrix-react-sdk/pull/7019)). Fixes vector-im/element-web#19309 and vector-im/element-web#19309. Contributed by @SimonBrandner.
* Allow scrolling right in reply-quoted code block ([\#7024](https://github.com/matrix-org/matrix-react-sdk/pull/7024)). Fixes vector-im/element-web#19487 and vector-im/element-web#19487. Contributed by @SimonBrandner.
* Fix dark theme codeblock colors ([\#6384](https://github.com/matrix-org/matrix-react-sdk/pull/6384)). Fixes vector-im/element-web#17998. Contributed by @SimonBrandner.
* Show passphrase input label ([\#6992](https://github.com/matrix-org/matrix-react-sdk/pull/6992)). Fixes vector-im/element-web#19428 and vector-im/element-web#19428. Contributed by @RafaelGoncalves8.
* Always render disabled settings as disabled ([\#7014](https://github.com/matrix-org/matrix-react-sdk/pull/7014)).
* Make "Security Phrase" placeholder look consistent cross-browser ([\#6870](https://github.com/matrix-org/matrix-react-sdk/pull/6870)). Fixes vector-im/element-web#19006 and vector-im/element-web#19006. Contributed by @neer17.
* Fix direction override characters breaking member event text direction ([\#6999](https://github.com/matrix-org/matrix-react-sdk/pull/6999)).
* Remove redundant text in verification dialogs ([\#6993](https://github.com/matrix-org/matrix-react-sdk/pull/6993)). Fixes vector-im/element-web#19290 and vector-im/element-web#19290. Contributed by @RafaelGoncalves8.
* Fix space panel name overflowing ([\#6995](https://github.com/matrix-org/matrix-react-sdk/pull/6995)). Fixes vector-im/element-web#19455 and vector-im/element-web#19455.
* Fix conflicting CSS on syntax highlighted blocks ([\#6991](https://github.com/matrix-org/matrix-react-sdk/pull/6991)). Fixes vector-im/element-web#19445 and vector-im/element-web#19445.
Changes in [3.33.0](https://github.com/vector-im/element-desktop/releases/tag/v3.33.0) (2021-10-25)
===================================================================================================

View file

@ -11,7 +11,7 @@ a 'skin'. A skin provides:
* The containing application
* Zero or more 'modules' containing non-UI functionality
As of Aug 2018, the only skin that exists is `vector-im/element-web`; it and
As of Aug 2018, the only skin that exists is [`vector-im/element-web`](https://github.com/vector-im/element-web/); it and
`matrix-org/matrix-react-sdk` should effectively
be considered as a single project (for instance, matrix-react-sdk bugs
are currently filed against vector-im/element-web rather than this project).
@ -138,7 +138,7 @@ guide](https://classic.yarnpkg.com/docs/install) if you do not have it
already. This project has not yet been migrated to Yarn 2, so please ensure
`yarn --version` shows a version from the 1.x series.
`matrix-react-sdk` depends on `matrix-js-sdk`. To make use of changes in the
`matrix-react-sdk` depends on [`matrix-js-sdk`](https://github.com/matrix-org/matrix-js-sdk). To make use of changes in the
latter and to ensure tests run against the develop branch of `matrix-js-sdk`,
you should set up `matrix-js-sdk`:
@ -175,4 +175,4 @@ yarn test
## End-to-End tests
Make sure you've got your Element development server running (by doing `yarn start` in element-web), and then in this project, run `yarn run e2etests`.
See `test/end-to-end-tests/README.md` for more information.
See [`test/end-to-end-tests/README.md`](https://github.com/matrix-org/matrix-react-sdk/blob/develop/test/end-to-end-tests/README.md) for more information.

View file

@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
"version": "3.33.0",
"version": "3.34.0",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
@ -83,8 +83,8 @@
"katex": "^0.12.0",
"linkifyjs": "^2.1.9",
"lodash": "^4.17.20",
"matrix-js-sdk": "15.0.0",
"matrix-widget-api": "^0.1.0-beta.16",
"matrix-js-sdk": "15.1.0",
"matrix-widget-api": "^0.1.0-beta.17",
"minimist": "^1.2.5",
"opus-recorder": "^8.0.3",
"pako": "^2.0.3",
@ -154,7 +154,7 @@
"@typescript-eslint/eslint-plugin": "^4.17.0",
"@typescript-eslint/parser": "^4.17.0",
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.1",
"allchange": "^1.0.3",
"allchange": "^1.0.5",
"babel-jest": "^26.6.3",
"chokidar": "^3.5.1",
"concurrently": "^5.3.0",

View file

@ -22,10 +22,13 @@ limitations under the License.
$hover-transition: 0.08s cubic-bezier(.46, .03, .52, .96); // quadratic
$EventTile_e2e_state_indicator_width: 4px;
$selected-message-border-width: 4px;
$MessageTimestamp_width: 46px; /* 8 + 30 (avatar) + 8 */
$MessageTimestamp_width_hover: calc($MessageTimestamp_width - 2 * $EventTile_e2e_state_indicator_width);
$MessageTimestamp_width_hover: calc($MessageTimestamp_width - 2 * $selected-message-border-width);
$slider-dot-size: 1em;
$slider-selection-dot-size: 2.4em;
:root {
font-size: 10px;
@ -401,7 +404,10 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
* We should go through and have one consistent set of styles for buttons throughout the app.
* For now, I am duplicating the selectors here for mx_Dialog and mx_DialogButtons.
*/
.mx_Dialog button, .mx_Dialog input[type="submit"], .mx_Dialog_buttons button, .mx_Dialog_buttons input[type="submit"] {
.mx_Dialog button:not(.mx_Dialog_nonDialogButton),
.mx_Dialog input[type="submit"],
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton),
.mx_Dialog_buttons input[type="submit"] {
@mixin mx_DialogButton;
margin-left: 0px;
margin-right: 8px;
@ -414,36 +420,52 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
font-family: inherit;
}
.mx_Dialog button:last-child {
.mx_Dialog button:not(.mx_Dialog_nonDialogButton):last-child {
margin-right: 0px;
}
.mx_Dialog button:hover, .mx_Dialog input[type="submit"]:hover, .mx_Dialog_buttons button:hover, .mx_Dialog_buttons input[type="submit"]:hover {
.mx_Dialog button:not(.mx_Dialog_nonDialogButton):hover,
.mx_Dialog input[type="submit"]:hover,
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):hover,
.mx_Dialog_buttons input[type="submit"]:hover {
@mixin mx_DialogButton_hover;
}
.mx_Dialog button:focus, .mx_Dialog input[type="submit"]:focus, .mx_Dialog_buttons button:focus, .mx_Dialog_buttons input[type="submit"]:focus {
.mx_Dialog button:not(.mx_Dialog_nonDialogButton):focus,
.mx_Dialog input[type="submit"]:focus,
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):focus,
.mx_Dialog_buttons input[type="submit"]:focus {
filter: brightness($focus-brightness);
}
.mx_Dialog button.mx_Dialog_primary, .mx_Dialog input[type="submit"].mx_Dialog_primary, .mx_Dialog_buttons button.mx_Dialog_primary, .mx_Dialog_buttons input[type="submit"].mx_Dialog_primary {
.mx_Dialog button.mx_Dialog_primary,
.mx_Dialog input[type="submit"].mx_Dialog_primary,
.mx_Dialog_buttons button.mx_Dialog_primary,
.mx_Dialog_buttons input[type="submit"].mx_Dialog_primary {
color: $accent-fg-color;
background-color: $accent-color;
min-width: 156px;
}
.mx_Dialog button.danger, .mx_Dialog input[type="submit"].danger, .mx_Dialog_buttons button.danger, .mx_Dialog_buttons input[type="submit"].danger {
.mx_Dialog button.danger,
.mx_Dialog input[type="submit"].danger,
.mx_Dialog_buttons button.danger,
.mx_Dialog_buttons input[type="submit"].danger {
background-color: $warning-color;
border: solid 1px $warning-color;
color: $accent-fg-color;
}
.mx_Dialog button.warning, .mx_Dialog input[type="submit"].warning {
.mx_Dialog button.warning,
.mx_Dialog input[type="submit"].warning {
border: solid 1px $warning-color;
color: $warning-color;
}
.mx_Dialog button:disabled, .mx_Dialog input[type="submit"]:disabled, .mx_Dialog_buttons button:disabled, .mx_Dialog_buttons input[type="submit"]:disabled {
.mx_Dialog button:not(.mx_Dialog_nonDialogButton):disabled,
.mx_Dialog input[type="submit"]:disabled,
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):disabled,
.mx_Dialog_buttons input[type="submit"]:disabled {
background-color: $light-fg-color;
border: solid 1px $light-fg-color;
opacity: 0.7;

View file

@ -74,6 +74,7 @@
@import "./views/dialogs/_ChangelogDialog.scss";
@import "./views/dialogs/_ChatCreateOrReuseChatDialog.scss";
@import "./views/dialogs/_CommunityPrototypeInviteDialog.scss";
@import "./views/dialogs/_CompoundDialog.scss";
@import "./views/dialogs/_ConfirmSpaceUserActionDialog.scss";
@import "./views/dialogs/_ConfirmUserActionDialog.scss";
@import "./views/dialogs/_CreateCommunityPrototypeDialog.scss";
@ -99,6 +100,7 @@
@import "./views/dialogs/_MessageEditHistoryDialog.scss";
@import "./views/dialogs/_ModalWidgetDialog.scss";
@import "./views/dialogs/_NewSessionReviewDialog.scss";
@import "./views/dialogs/_PollCreateDialog.scss";
@import "./views/dialogs/_RegistrationEmailPromptDialog.scss";
@import "./views/dialogs/_RoomSettingsDialog.scss";
@import "./views/dialogs/_RoomSettingsDialogBridges.scss";
@ -200,10 +202,10 @@
@import "./views/right_panel/_EncryptionInfo.scss";
@import "./views/right_panel/_PinnedMessagesCard.scss";
@import "./views/right_panel/_RoomSummaryCard.scss";
@import "./views/right_panel/_ThreadPanel.scss";
@import "./views/right_panel/_UserInfo.scss";
@import "./views/right_panel/_VerificationPanel.scss";
@import "./views/right_panel/_WidgetCard.scss";
@import "./views/right_panel/_ThreadPanel.scss";
@import "./views/room_settings/_AliasSettings.scss";
@import "./views/rooms/_AppsDrawer.scss";
@import "./views/rooms/_Autocomplete.scss";
@ -248,6 +250,7 @@
@import "./views/settings/_DevicesPanel.scss";
@import "./views/settings/_E2eAdvancedPanel.scss";
@import "./views/settings/_EmailAddresses.scss";
@import "./views/settings/_FontScalingPanel.scss";
@import "./views/settings/_IntegrationManager.scss";
@import "./views/settings/_JoinRuleSettings.scss";
@import "./views/settings/_LayoutSwitcher.scss";
@ -258,6 +261,7 @@
@import "./views/settings/_SetIdServer.scss";
@import "./views/settings/_SetIntegrationManager.scss";
@import "./views/settings/_SpellCheckLanguages.scss";
@import "./views/settings/_ThemeChoicePanel.scss";
@import "./views/settings/_UpdateCheckButton.scss";
@import "./views/settings/tabs/_SettingsTab.scss";
@import "./views/settings/tabs/room/_GeneralRoomSettingsTab.scss";

View file

@ -116,3 +116,11 @@ limitations under the License.
border-top: 8px solid $menu-bg-color;
border-right: 8px solid transparent;
}
.mx_ContextualMenu_rightAligned {
transform: translateX(-100%);
}
.mx_ContextualMenu_bottomAligned {
transform: translateY(-100%);
}

View file

@ -34,4 +34,3 @@ limitations under the License.
.mx_CreateRoom_description {
width: 330px;
}

View file

@ -43,8 +43,6 @@ $roomListCollapsedWidth: 68px;
}
}
.mx_LeftPanel {
background-color: $roomlist-bg-color;
// TODO decrease this once Spaces launches as it'll no longer need to include the 56px Community Panel

View file

@ -121,7 +121,7 @@ limitations under the License.
vertical-align: text-top;
margin-right: 2px;
content: "";
mask: url('$(res)/img/feather-customised/user.svg');
mask: url("$(res)/img/feather-customised/user.svg");
mask-repeat: no-repeat;
mask-position: center;
// scale it down and make the size slightly bigger (16 instead of 14px)
@ -132,7 +132,8 @@ limitations under the License.
}
}
.mx_RoomDirectory_join, .mx_RoomDirectory_preview {
.mx_RoomDirectory_join,
.mx_RoomDirectory_preview {
align-self: center;
white-space: nowrap;
}
@ -220,3 +221,7 @@ limitations under the License.
margin-top: 5px;
}
}
.mx_RoomDirectory_listItem {
display: contents;
}

View file

@ -32,7 +32,6 @@ limitations under the License.
position: relative;
}
@keyframes mx_RoomView_fileDropTarget_animation {
from {
opacity: 0;
@ -112,7 +111,6 @@ limitations under the License.
max-width: 1920px !important;
}
.mx_RoomView .mx_MainSplit {
flex: 1 1 0;
}

View file

@ -203,7 +203,8 @@ limitations under the License.
grid-row: 1;
grid-column: 2;
.mx_InfoTooltip {
.mx_InfoTooltip,
.mx_SpaceHierarchy_roomTile_joined {
display: inline;
margin-left: 12px;
color: $tertiary-content;
@ -222,6 +223,25 @@ limitations under the License.
}
}
}
.mx_SpaceHierarchy_roomTile_joined {
position: relative;
padding-left: 16px;
&::before {
content: '';
width: 20px;
height: 20px;
top: -2px;
left: -4px;
position: absolute;
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
background-color: $accent-color;
mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg');
}
}
}
.mx_SpaceHierarchy_roomTile_info {
@ -268,6 +288,11 @@ limitations under the License.
visibility: visible;
}
}
&.mx_SpaceHierarchy_joining .mx_AccessibleButton {
visibility: visible;
padding: 4px 18px;
}
}
li.mx_SpaceHierarchy_roomTileWrapper {

View file

@ -144,13 +144,7 @@ $activeBorderColor: $secondary-content;
align-items: center;
border-radius: 12px;
padding: 4px;
}
&:not(.mx_SpaceButton_narrow) {
.mx_SpaceButton_selectionWrapper {
width: 100%;
overflow: hidden;
}
width: 100%;
}
.mx_SpaceButton_name {
@ -227,7 +221,7 @@ $activeBorderColor: $secondary-content;
height: 20px;
margin-top: auto;
margin-bottom: auto;
display: none;
visibility: hidden;
position: relative;
&::before {
@ -246,67 +240,45 @@ $activeBorderColor: $secondary-content;
}
}
.mx_SpaceButton_avatarWrapper {
position: relative;
}
.mx_SpacePanel_badgeContainer {
// Create a flexbox to make aligning dot badges easier
display: flex;
align-items: center;
position: absolute;
right: -3px;
top: -3px;
.mx_NotificationBadge {
margin: 0 2px; // centering
background-clip: padding-box;
}
.mx_NotificationBadge_dot {
// make the smaller dot occupy the same width for centering
margin: 0 7px;
margin: 0 -1px 0 0;
border: 3px solid $groupFilterPanel-bg-color;
}
.mx_NotificationBadge_2char,
.mx_NotificationBadge_3char {
margin: -5px -5px 0 0;
border: 2px solid $groupFilterPanel-bg-color;
}
}
&.collapsed {
.mx_SpaceButton {
.mx_SpacePanel_badgeContainer {
position: absolute;
right: 0;
top: 0;
.mx_NotificationBadge {
background-clip: padding-box;
}
.mx_NotificationBadge_dot {
margin: 0 -1px 0 0;
border: 3px solid $groupFilterPanel-bg-color;
}
.mx_NotificationBadge_2char,
.mx_NotificationBadge_3char {
margin: -5px -5px 0 0;
border: 2px solid $groupFilterPanel-bg-color;
}
}
&.mx_SpaceButton_active .mx_SpacePanel_badgeContainer {
// when we draw the selection border we move the relative bounds of our parent
// so update our position within the bounds of the parent to maintain position overall
right: -3px;
top: -3px;
}
}
.mx_SpaceButton_narrow .mx_SpaceButton_menuButton {
display: none;
}
&:not(.collapsed) {
.mx_SpaceButton:hover,
.mx_SpaceButton:focus-within,
.mx_SpaceButton_hasMenuOpen {
&:not(.mx_SpaceButton_invite) {
// Hide the badge container on hover because it'll be a menu button
.mx_SpacePanel_badgeContainer {
display: none;
}
.mx_SpaceButton_menuButton {
display: block;
}
}
.mx_SpaceButton:hover,
.mx_SpaceButton:focus-within,
.mx_SpaceButton_hasMenuOpen {
&:not(.mx_SpaceButton_invite) .mx_SpaceButton_menuButton {
visibility: visible;
}
}
@ -376,7 +348,6 @@ $activeBorderColor: $secondary-content;
}
}
.mx_SpacePanel_sharePublicSpace {
margin: 0;
}

View file

@ -380,45 +380,6 @@ $SpaceRoomViewInnerWidth: 428px;
}
}
.mx_SpaceRoomView_betaWarning {
padding: 12px 12px 12px 54px;
position: relative;
font-size: $font-15px;
line-height: $font-24px;
width: 432px;
border-radius: 8px;
background-color: $info-plinth-bg-color;
color: $secondary-content;
box-sizing: border-box;
> h3 {
font-weight: $font-semi-bold;
font-size: inherit;
line-height: inherit;
margin: 0;
}
> p {
font-size: inherit;
line-height: inherit;
margin: 0;
}
&::before {
mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
mask-position: center;
mask-repeat: no-repeat;
mask-size: contain;
content: '';
width: 20px;
height: 20px;
position: absolute;
top: 14px;
left: 14px;
background-color: $secondary-content;
}
}
.mx_SpaceRoomView_inviteTeammates {
// XXX remove this when spaces leaves Beta
.mx_SpaceRoomView_inviteTeammates_betaDisclaimer {
@ -511,10 +472,11 @@ $SpaceRoomViewInnerWidth: 428px;
mask-image: url("$(res)/img/element-icons/lock.svg");
}
.mx_AccessibleButton_kind_link {
.mx_SpaceRoomView_info_memberCount {
color: inherit;
position: relative;
padding-left: 16px;
padding: 0 0 0 16px;
font-size: $font-15px;
&::before {
content: "·"; // visual separator

View file

@ -0,0 +1,87 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// --------------------------------------------------------------------------------
// DEV NOTE: This stylesheet covers dialogs listed by the compound, including
// over multiple React components. The actual inner contents of the dialog should
// be in their respective stylesheets.
// --------------------------------------------------------------------------------
// Override legacy/default styles for dialogs
.mx_Dialog_wrapper.mx_CompoundDialog > .mx_Dialog {
padding: 0; // we'll manage it ourselves
color: $primary-content;
}
.mx_CompoundDialog {
.mx_CompoundDialog_header {
padding: 32px 32px 16px 32px;
h1 {
display: inline-block;
font-weight: 600;
font-size: $font-24px;
margin: 0; // managed by header class
}
.mx_CompoundDialog_cancelButton {
mask: url('$(res)/img/feather-customised/cancel.svg');
mask-repeat: no-repeat;
mask-position: center;
mask-size: cover;
width: 20px;
height: 20px;
background-color: $dialog-close-fg-color;
cursor: pointer;
// Align with middle of title, 34px from right edge
position: absolute;
top: 34px;
right: 34px;
}
}
.mx_CompoundDialog_content {
overflow: auto;
padding: 8px 32px;
}
.mx_CompoundDialog_footer {
padding: 20px 32px;
text-align: right;
position: absolute;
bottom: 0;
left: 0;
right: 0;
.mx_AccessibleButton {
margin-left: 24px;
}
}
}
.mx_ScrollableBaseDialog {
width: 544px; // fixed
height: 516px; // fixed
.mx_CompoundDialog_content {
height: 349px; // dialogHeight - header - footer
}
.mx_CompoundDialog_footer {
box-shadow: 0px -4px 4px rgba(0, 0, 0, 0.05); // hardcoded colour for both themes
}
}

View file

@ -64,4 +64,3 @@ limitations under the License.
mask-size: contain;
}
}

View file

@ -64,4 +64,3 @@ limitations under the License.
padding: 0 8px;
}
}

View file

@ -0,0 +1,70 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_PollCreateDialog {
h2 {
font-weight: 600;
font-size: $font-15px;
line-height: $font-24px;
margin-top: 0;
margin-bottom: 8px;
&:nth-child(n + 2) {
margin-top: 20px;
}
}
.mx_PollCreateDialog_option {
display: flex;
align-items: center;
margin-top: 11px;
margin-bottom: 16px; // 11px from the top will collapse, so this creates a 16px gap between options
.mx_Field {
flex: 1;
margin: 0;
}
.mx_PollCreateDialog_removeOption {
margin-left: 12px;
width: 20px;
height: 20px;
border-radius: 50%;
background-color: $quinary-content;
cursor: pointer;
position: relative;
&::before {
content: "";
mask: url('$(res)/img/element-icons/x-8px.svg');
mask-repeat: no-repeat;
mask-position: center;
mask-size: cover;
width: 8px;
height: 8px;
position: absolute;
top: 6px;
left: 6px;
background-color: $secondary-content;
}
}
}
.mx_PollCreateDialog_addOption {
padding: 0;
margin-bottom: 40px; // arbitrary to create scrollable area under the poll
}
}

View file

@ -58,4 +58,3 @@ limitations under the License.
mask-size: 36px;
mask-position: center;
}

View file

@ -50,4 +50,3 @@ limitations under the License.
vertical-align: middle;
}
}

View file

@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_WidgetCapabilitiesPromptDialog {
.text-muted {
font-size: $font-12px;
@ -55,7 +54,6 @@ limitations under the License.
width: $font-32px;
height: $font-15px;
&.mx_ToggleSwitch_on > .mx_ToggleSwitch_ball {
left: calc(100% - $font-15px);
}

View file

@ -70,7 +70,6 @@ limitations under the License.
width: 300px;
border: 1px solid $accent-color;
border-radius: 5px;
padding: 10px;
}
.mx_AccessSecretStorageDialog_recoveryKeyEntry {

View file

@ -30,11 +30,13 @@ limitations under the License.
align-items: center;
justify-content: center;
font-size: $font-14px;
border: none; // override default <button /> styles
}
.mx_AccessibleButton_kind_primary {
color: $button-primary-fg-color;
background-color: $button-primary-bg-color;
border: 1px solid $button-primary-bg-color; // account for size loss of no border
font-weight: 600;
}
@ -115,3 +117,43 @@ limitations under the License.
.mx_AccessibleButton_kind_link_sm.mx_AccessibleButton_disabled {
opacity: 0.4;
}
.mx_AccessibleButton_hasKind.mx_AccessibleButton_kind_confirm_sm {
background-color: $button-primary-bg-color;
&::before {
mask-image: url('$(res)/img/feather-customised/check.svg');
}
}
.mx_AccessibleButton_hasKind.mx_AccessibleButton_kind_cancel_sm {
background-color: $button-danger-bg-color;
&::before {
mask-image: url('$(res)/img/feather-customised/x.svg');
}
}
.mx_AccessibleButton_kind_confirm_sm,
.mx_AccessibleButton_kind_cancel_sm {
padding: 0px;
width: 16px;
height: 16px;
border-radius: 100%;
position: relative;
display: block;
&::before {
content: "";
display: block;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: #ffffff;
mask-repeat: no-repeat;
mask-position: center;
mask-size: 80%;
}
}

View file

@ -130,4 +130,3 @@ input.mx_Dropdown_option:focus {
margin-left: 5px;
margin-bottom: 5px;
}

View file

@ -61,4 +61,3 @@ limitations under the License.
.mx_EditableItemList_label {
margin-bottom: 5px;
}

View file

@ -89,7 +89,8 @@ limitations under the License.
}
.mx_Field input:placeholder-shown:focus::placeholder,
.mx_Field textarea:placeholder-shown:focus::placeholder {
.mx_Field textarea:placeholder-shown:focus::placeholder,
.mx_Field.mx_Field_placeholderIsHint input::placeholder {
transition: color 0.25s ease-in 0.1s;
color: $greyed-fg-color;
}

View file

@ -24,5 +24,5 @@ limitations under the License.
}
.mx_InlineSpinner_icon {
display: inline-block;
display: inline-block !important; // Override regular mx_Spinner_icon
}

View file

@ -54,22 +54,30 @@ limitations under the License.
.mx_Slider_selectionDot {
position: absolute;
width: 1.1em;
height: 1.1em;
width: $slider-selection-dot-size;
height: $slider-selection-dot-size;
background-color: $slider-selection-color;
border-radius: 50%;
box-shadow: 0 0 6px lightgrey;
z-index: 10;
}
.mx_Slider_selectionText {
color: $muted-fg-color;
font-size: $font-14px;
position: relative;
text-align: center;
top: 30px;
width: 100%;
}
.mx_Slider_selection > hr {
margin: 0;
border: 0.2em solid $slider-selection-color;
}
.mx_Slider_dot {
height: 1em;
width: 1em;
height: $slider-dot-size;
width: $slider-dot-size;
border-radius: 50%;
background-color: $slider-background-color;
z-index: 0;

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -37,8 +38,28 @@ limitations under the License.
}
.mx_Spinner_icon {
background-color: $primary-content;
mask: url('$(res)/img/spinner.svg');
mask-size: contain;
animation: 1.1s steps(12, end) infinite spin;
background-color: $quinary-content;
mask: url('$(res)/img/spinner/spinner-background.svg');
mask-size: 100%;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
&::before {
background-color: $secondary-content;
mask: url('$(res)/img/spinner/spinner-foreground.svg');
mask-size: 100%;
width: 100%;
height: 100%;
content: "";
display: flex;
animation: 1s linear spin infinite;
}
}

View file

@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_Checkbox {
$size: $font-16px;
$border-size: $font-1-5px;
@ -49,22 +48,20 @@ limitations under the License.
box-sizing: border-box;
border-radius: $border-radius;
img {
.mx_Checkbox_checkmark {
display: none;
height: 100%;
width: 100%;
filter: invert(100%);
mask-image: url('$(res)/img/feather-customised/check.svg');
mask-position: center;
mask-size: 100%;
mask-repeat: no-repeat;
}
}
&:checked + label > .mx_Checkbox_background {
background: $accent-color;
border-color: $accent-color;
img {
display: block;
}
&:checked + label > .mx_Checkbox_background .mx_Checkbox_checkmark {
display: block;
}
& + label > *:not(.mx_Checkbox_background) {
@ -76,11 +73,6 @@ limitations under the License.
cursor: not-allowed;
}
&:checked:disabled + label > .mx_Checkbox_background {
background-color: $accent-color;
border-color: $accent-color;
}
&.focus-visible {
& + label .mx_Checkbox_background {
@mixin unreal-focus;
@ -88,3 +80,25 @@ limitations under the License.
}
}
}
.mx_Checkbox.mx_Checkbox_kind_solid input[type=checkbox] {
& + label > .mx_Checkbox_background .mx_Checkbox_checkmark {
background: #ffffff;
}
&:checked + label > .mx_Checkbox_background {
background: $accent-color;
border-color: $accent-color;
}
}
.mx_Checkbox.mx_Checkbox_kind_outline input[type=checkbox] {
& + label > .mx_Checkbox_background .mx_Checkbox_checkmark {
background: $accent-color;
}
&:checked + label > .mx_Checkbox_background {
background: transparent;
border-color: $accent-color;
}
}

View file

@ -52,7 +52,6 @@ limitations under the License.
display: none;
position: fixed;
border-radius: 8px;
box-shadow: 4px 4px 12px 0 $menu-box-shadow-color;
z-index: 6000; // Higher than context menu so tooltips can be used everywhere
padding: 10px;
pointer-events: none;
@ -63,7 +62,7 @@ limitations under the License.
word-break: break-word;
margin-right: 50px;
background-color: $inverted-bg-color;
background-color: #21262C; // Same on both themes
color: $accent-fg-color;
border: 0;
text-align: center;

View file

@ -49,4 +49,3 @@ limitations under the License.
text-align: start;
line-height: 17px !important;
}

View file

@ -34,4 +34,3 @@ limitations under the License.
}
}
}

View file

@ -39,7 +39,6 @@ limitations under the License.
background-color: $notice-primary-color;
}
.mx_cryptoEvent_state, .mx_cryptoEvent_buttons {
grid-column: 3;
grid-row: 1 / 3;

View file

@ -83,7 +83,7 @@ limitations under the License.
}
.mx_RoomSummaryCard_e2ee_warning {
background-color: #ff4b55;
background-color: #ff5b55;
&::before {
mask-image: url('$(res)/img/e2e/warning.svg');
}

View file

@ -14,25 +14,28 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_ThreadPanel {
display: flex;
flex-direction: column;
padding-right: 0;
.mx_BaseCard_header {
padding: 6px 0;
padding: 6px 8px 6px 0;
.mx_BaseCard_close,
.mx_BaseCard_back {
margin-top: 15px;
}
.mx_BaseCard_close {
margin-top: 15px;
right: -8px;
}
}
.mx_AccessibleButton.mx_BaseCard_back {
display: none;
}
&__header {
width: calc(100% - 40px);
.mx_ThreadPanel__header {
width: calc(100% - 60px);
margin-left: 30px;
display: flex;
flex: 1;
justify-content: space-between;
@ -100,11 +103,39 @@ limitations under the License.
}
}
.mx_ThreadPanel_button {
width: 20px;
height: 20px;
margin-top: -3px;
margin-bottom: auto;
position: relative;
&::before {
top: 2px;
left: 2px;
content: '';
width: 16px;
height: 16px;
position: absolute;
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
background: $primary-content;
}
&.mx_ThreadPanel_OptionsButton::before {
mask-image: url('$(res)/img/element-icons/context-menu.svg');
}
}
.mx_AutoHideScrollbar {
border-radius: 8px;
}
.mx_RoomView_messageListWrapper {
background-color: $background;
border-radius: 8px;
padding-top: 8px;
padding-bottom: 12px;
padding: 8px;
border-radius: inherit;
}
.mx_ScrollPanel {
@ -117,18 +148,7 @@ limitations under the License.
// Account for scrollbar when hovering
width: calc(100% - 3px);
margin: 0 2px;
.mx_MessageTimestamp {
// We need to add !important here due to some enormous selectors overriding it anyways
// See: _EventTile.scss:241
left: unset !important;
right: 0 !important;
top: 16px;
}
.mx_EventTile_line.mx_EventTile_line {
position: unset;
}
padding-top: 0;
.mx_ThreadInfo {
position: relative;
@ -149,4 +169,21 @@ limitations under the License.
display: none;
}
}
.mx_MessageComposer {
background-color: $background;
border-radius: 8px;
margin-top: 8px;
width: calc(100% - 8px);
padding: 0 8px;
box-sizing: border-box;
}
}
.mx_ThreadPanel_viewInRoom::before {
mask-image: url('$(res)/img/element-icons/view-in-room.svg');
}
.mx_ThreadPanel_copyLinkToThread::before {
mask-image: url('$(res)/img/element-icons/link.svg');
}

View file

@ -223,7 +223,6 @@ limitations under the License.
display: flex;
margin: 8px 0;
&.mx_UserInfo_device_verified {
.mx_UserInfo_device_trusted {
color: $accent-color;
@ -267,7 +266,6 @@ limitations under the License.
margin: 16px 0 8px;
}
.mx_VerificationShowSas {
.mx_AccessibleButton + .mx_AccessibleButton {
margin: 8px 0; // space between buttons

View file

@ -23,7 +23,6 @@ limitations under the License.
}
}
.mx_UserInfo {
.mx_EncryptionPanel_cancel {
mask: url('$(res)/img/feather-customised/cancel.svg');

View file

@ -365,7 +365,6 @@ $MinWidth: 240px;
to { opacity: 1; }
}
.mx_AppLoading iframe {
display: none;
}

View file

@ -24,7 +24,6 @@ limitations under the License.
margin: -7px -10px -5px -10px;
overflow: visible !important; // override mx_EventTile_content
.mx_BasicMessageComposer_input {
border-radius: 4px;
border: solid 1px $primary-hairline-color;

View file

@ -92,6 +92,10 @@ limitations under the License.
&[data-self=false] {
.mx_EventTile_line {
border-bottom-right-radius: var(--cornerRadius);
.mx_MImageBody .mx_MImageBody_thumbnail {
border-bottom-right-radius: var(--cornerRadius);
}
}
.mx_EventTile_avatar {
left: -34px;
@ -106,12 +110,16 @@ limitations under the License.
}
&[data-self=true] {
.mx_EventTile_line {
border-bottom-left-radius: var(--cornerRadius);
float: right;
border-bottom-left-radius: var(--cornerRadius);
> a {
left: auto;
right: -68px;
}
.mx_MImageBody .mx_MImageBody_thumbnail {
border-bottom-left-radius: var(--cornerRadius);
}
}
.mx_ThreadInfo {
@ -147,33 +155,62 @@ limitations under the License.
.mx_EventTile_line {
position: relative;
padding: var(--gutterSize);
border-top-left-radius: var(--cornerRadius);
border-top-right-radius: var(--cornerRadius);
background: var(--backgroundColor);
display: flex;
gap: 5px;
margin: 0 -12px 0 -9px;
border-top-left-radius: var(--cornerRadius);
border-top-right-radius: var(--cornerRadius);
> a {
position: absolute;
padding: 10px 20px;
top: 0;
left: -68px;
}
//noinspection CssReplaceWithShorthandSafely
.mx_MImageBody .mx_MImageBody_thumbnail {
// Note: This is intentionally not compressed because the browser gets confused
// when it is all combined. We're effectively unsetting the border radius then
// setting the two corners we care about manually.
border-radius: unset;
border-top-left-radius: var(--cornerRadius);
border-top-right-radius: var(--cornerRadius);
}
}
.mx_EventTile_line:not(.mx_EventTile_mediaLine) {
padding: var(--gutterSize);
background: var(--backgroundColor);
}
&.mx_EventTile_continuation[data-self=false] .mx_EventTile_line {
border-top-left-radius: 0;
.mx_MImageBody .mx_MImageBody_thumbnail {
border-top-left-radius: 0;
}
}
&.mx_EventTile_lastInSection[data-self=false] .mx_EventTile_line {
border-bottom-left-radius: var(--cornerRadius);
.mx_MImageBody .mx_MImageBody_thumbnail {
border-bottom-left-radius: var(--cornerRadius);
}
}
&.mx_EventTile_continuation[data-self=true] .mx_EventTile_line {
border-top-right-radius: 0;
.mx_MImageBody .mx_MImageBody_thumbnail {
border-top-right-radius: 0;
}
}
&.mx_EventTile_lastInSection[data-self=true] .mx_EventTile_line {
border-bottom-right-radius: var(--cornerRadius);
.mx_MImageBody .mx_MImageBody_thumbnail {
border-bottom-right-radius: var(--cornerRadius);
}
}
.mx_EventTile_avatar {
@ -232,7 +269,7 @@ limitations under the License.
.mx_EditMessageComposer_buttons {
position: static;
padding: 0;
margin: 0;
margin: 8px 0 0;
background: transparent;
}
@ -263,7 +300,6 @@ limitations under the License.
}
}
.mx_EventTile_readAvatars {
position: absolute;
right: -110px;

View file

@ -16,7 +16,6 @@ limitations under the License.
*/
$left-gutter: 64px;
$hover-select-border: 4px;
.mx_EventTile:not([data-layout=bubble]) {
max-width: 100%;
@ -25,6 +24,11 @@ $hover-select-border: 4px;
font-size: $font-14px;
position: relative;
.mx_ThreadInfo {
margin-right: 110px;
margin-left: 64px;
}
&.mx_EventTile_info {
padding-top: 1px;
}
@ -110,15 +114,15 @@ $hover-select-border: 4px;
}
&.mx_EventTile_selected > div > a > .mx_MessageTimestamp {
left: calc(-$hover-select-border);
left: calc(-$selected-message-border-width);
}
/* this is used for the tile for the event which is selected via the URL.
* TODO: ultimately we probably want some transition on here.
*/
&.mx_EventTile_selected > .mx_EventTile_line {
border-left: $accent-color 4px solid;
padding-left: calc($left-gutter - $hover-select-border);
border-left: $accent-color $selected-message-border-width solid;
padding-left: calc($left-gutter - $selected-message-border-width);
background-color: $event-selected-color;
}
@ -132,7 +136,7 @@ $hover-select-border: 4px;
}
&.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line {
padding-left: calc($left-gutter + 18px - $hover-select-border);
padding-left: calc($left-gutter + 18px - $selected-message-border-width);
}
&.mx_EventTile:hover .mx_EventTile_line,
@ -208,28 +212,34 @@ $hover-select-border: 4px;
overflow-y: hidden;
}
&.mx_EventTile_selected .mx_EventTile_line,
&:hover .mx_EventTile_line {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
&:hover.mx_EventTile_verified .mx_EventTile_line,
&:hover.mx_EventTile_unverified .mx_EventTile_line,
&:hover.mx_EventTile_unknown .mx_EventTile_line {
padding-left: calc($left-gutter - $hover-select-border);
padding-left: calc($left-gutter - $selected-message-border-width);
}
&:hover.mx_EventTile_verified .mx_EventTile_line {
border-left: $e2e-verified-color $EventTile_e2e_state_indicator_width solid;
border-left: $e2e-verified-color $selected-message-border-width solid;
}
&:hover.mx_EventTile_unverified .mx_EventTile_line {
border-left: $e2e-unverified-color $EventTile_e2e_state_indicator_width solid;
border-left: $e2e-unverified-color $selected-message-border-width solid;
}
&:hover.mx_EventTile_unknown .mx_EventTile_line {
border-left: $e2e-unknown-color $EventTile_e2e_state_indicator_width solid;
border-left: $e2e-unknown-color $selected-message-border-width solid;
}
&:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line,
&:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line,
&:hover.mx_EventTile_unknown.mx_EventTile_info .mx_EventTile_line {
padding-left: calc($left-gutter + 18px - $hover-select-border);
padding-left: calc($left-gutter + 18px - $selected-message-border-width);
}
/* End to end encryption stuff */
@ -241,7 +251,7 @@ $hover-select-border: 4px;
&:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp,
&:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp,
&:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp {
left: calc(-$hover-select-border);
left: calc(-$selected-message-border-width);
}
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
@ -391,7 +401,6 @@ $hover-select-border: 4px;
cursor: pointer;
}
.mx_EventTile_e2eIcon {
position: relative;
width: 14px;
@ -423,7 +432,7 @@ $hover-select-border: 4px;
}
}
.mx_EventTile_e2eIcon_undecryptable, .mx_EventTile_e2eIcon_unverified {
.mx_EventTile_e2eIcon_warning {
&::after {
mask-image: url('$(res)/img/e2e/warning.svg');
background-color: $notice-primary-color;
@ -431,23 +440,7 @@ $hover-select-border: 4px;
opacity: 1;
}
.mx_EventTile_e2eIcon_unknown {
&::after {
mask-image: url('$(res)/img/e2e/warning.svg');
background-color: $notice-primary-color;
}
opacity: 1;
}
.mx_EventTile_e2eIcon_unencrypted {
&::after {
mask-image: url('$(res)/img/e2e/warning.svg');
background-color: $notice-primary-color;
}
opacity: 1;
}
.mx_EventTile_e2eIcon_unauthenticated {
.mx_EventTile_e2eIcon_normal {
&::after {
mask-image: url('$(res)/img/e2e/normal.svg');
background-color: $composer-e2e-icon-color;
@ -476,7 +469,7 @@ $hover-select-border: 4px;
pre, code {
font-family: $monospace-font-family !important;
background-color: $header-panel-bg-color;
background-color: $codeblock-background-color;
}
pre code > * {
@ -571,7 +564,6 @@ $hover-select-border: 4px;
color: inherit;
}
/* Make h1 and h2 the same size as h3. */
.mx_EventTile_content .markdown-body h1,
.mx_EventTile_content .markdown-body h2 {
@ -603,7 +595,6 @@ $hover-select-border: 4px;
/* end of overrides */
.mx_EventTile_keyRequestInfo {
font-size: $font-12px;
}
@ -688,7 +679,7 @@ $hover-select-border: 4px;
justify-content: flex-start;
clear: both;
&:hover, &-active {
&:hover {
cursor: pointer;
border: 1px solid $quinary-content;
padding-top: 7px;
@ -721,25 +712,14 @@ $hover-select-border: 4px;
}
}
.mx_ThreadView {
display: flex;
flex-direction: column;
.mx_ScrollPanel {
margin-top: 20px;
.mx_RoomView_MessageList {
padding: 0;
}
}
.mx_EventTile_senderDetails {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 6px;
a {
flex: 1;
@ -772,14 +752,26 @@ $hover-select-border: 4px;
width: 100%;
display: flex;
flex-direction: column;
margin-top: 0;
padding-bottom: 5px;
margin-bottom: 5px;
padding-top: 0;
.mx_MessageTimestamp {
left: auto;
right: 0;
right: 2px !important;
top: 1px !important;
}
.mx_ReactionsRow {
order: 999;
padding-left: 0;
padding-right: 0;
margin-left: 36px;
margin-right: 50px;
}
}
.mx_EventTile_content {
margin-left: 36px;
margin-right: 50px;
}
.mx_MessageComposer_sendMessage {

View file

@ -247,7 +247,6 @@ limitations under the License.
}
}
.mx_MessageComposer_upload::before {
mask-image: url('$(res)/img/element-icons/room/composer/attach.svg');
}
@ -391,6 +390,12 @@ limitations under the License.
padding: 0 0 0 25px;
}
&:not(.mx_MessageComposer_e2eStatus) {
.mx_MessageComposer_wrapper {
padding: 0;
}
}
.mx_MessageComposer_button:last-child {
margin-right: 0;
}

View file

@ -16,12 +16,13 @@ limitations under the License.
.mx_MessageComposerFormatBar {
display: none;
width: calc(26px * 5);
height: 24px;
width: calc(32px * 6);
height: 32px;
position: absolute;
cursor: pointer;
border-radius: 4px;
background-color: $message-action-bar-bg-color;
border-radius: 8px;
background-color: $background;
border: 1px solid $input-border-color;
user-select: none;
// equal to z-index of mx_ReplyPreview and mx_RoomView_statusArea (1000)
// but as it appears after them in the DOM, will appear on top.
@ -35,32 +36,19 @@ limitations under the License.
white-space: nowrap;
display: inline-block;
position: relative;
border: 1px solid $message-action-bar-border-color;
margin-left: -1px;
margin: 2px;
&:hover {
border-color: $message-action-bar-hover-border-color;
background: $roomlist-button-bg-color;
border-radius: 6px;
z-index: 1;
}
&:first-child {
border-radius: 3px 0 0 3px;
}
&:last-child {
border-radius: 0 3px 3px 0;
}
&:only-child {
border-radius: 3px;
}
}
.mx_MessageComposerFormatBar_button {
width: 27px;
height: 24px;
width: 28px;
height: 28px;
box-sizing: border-box;
background: none;
vertical-align: middle;
}
@ -73,7 +61,11 @@ limitations under the License.
width: 100%;
mask-repeat: no-repeat;
mask-position: center;
background-color: $message-action-bar-fg-color;
background-color: $secondary-content;
}
.mx_MessageComposerFormatBar_button:hover::after {
background-color: $primary-content;
}
.mx_MessageComposerFormatBar_buttonIconBold::after {
@ -95,6 +87,11 @@ limitations under the License.
.mx_MessageComposerFormatBar_buttonIconCode::after {
mask-image: url('$(res)/img/element-icons/room/format-bar/code.svg');
}
.mx_MessageComposerFormatBar_buttonIconInsertLink::after {
mask-image: url('$(res)/img/element-icons/link.svg');
mask-size: 18px;
}
}
.mx_MessageComposerFormatBar_buttonTooltip {

View file

@ -52,4 +52,3 @@ limitations under the License.
}
}
}

View file

@ -78,7 +78,8 @@ limitations under the License.
// Hack to cut content in <pre> tags too
.mx_EventTile_pre_container > pre {
overflow: hidden;
overflow-x: scroll;
overflow-y: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;

View file

@ -68,4 +68,3 @@ limitations under the License.
cursor: pointer;
}
}

View file

@ -47,4 +47,3 @@ limitations under the License.
}
}
}

View file

@ -15,42 +15,81 @@ limitations under the License.
*/
.mx_DevicesPanel {
table-layout: fixed;
// Normally the panel is 880px, however this can easily overflow the container.
// TODO: Fix the table to not be squishy
width: auto;
max-width: 880px;
border-spacing: 10px;
hr {
opacity: 0.2;
border: none;
border-bottom: 1px solid $primary-content;
}
}
.mx_DevicesPanel_header {
font-weight: bold;
display: flex;
align-items: center;
margin-block: 10px;
.mx_DevicesPanel_header_title {
font-size: $font-18px;
font-weight: 600;
color: $primary-content;
}
.mx_DevicesPanel_selectButton {
padding-top: 9px;
}
.mx_E2EIcon {
width: 24px;
height: 24px;
margin-left: 0;
margin-right: 5px;
}
}
.mx_DevicesPanel_header .mx_DevicesPanel_deviceButtons {
height: 48px; // make this tall so the table doesn't move down when the delete button appears
width: 20%;
.mx_DevicesPanel_deleteButton {
margin-top: 10px;
}
.mx_DevicesPanel_header th {
padding: 0px;
text-align: left;
vertical-align: middle;
.mx_DevicesPanel_device {
display: flex;
align-items: flex-start;
margin-block: 10px;
min-height: 35px;
}
.mx_DevicesPanel_header .mx_DevicesPanel_deviceName {
width: 50%;
.mx_DevicesPanel_icon, .mx_DevicesPanel_checkbox {
margin-left: 9px;
margin-top: 2px;
}
.mx_DevicesPanel_header .mx_DevicesPanel_deviceLastSeen {
width: 30%;
.mx_DevicesPanel_deviceInfo {
flex-grow: 1;
}
.mx_DevicesPanel_device td {
vertical-align: baseline;
padding: 0px;
.mx_DevicesPanel_deviceName {
color: $primary-content;
}
.mx_DevicesPanel_myDevice {
font-weight: bold;
.mx_DevicesPanel_lastSeen {
font-size: $font-12px;
}
.mx_DevicesPanel_deviceButtons {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 9px;
}
.mx_DevicesPanel_renameForm {
display: flex;
align-items: center;
gap: 5px;
.mx_Field_input {
width: 240px;
margin: 0;
}
}

View file

@ -17,4 +17,3 @@ limitations under the License.
.mx_E2eAdvancedPanel_settingLongDescription {
margin-right: 150px;
}

View file

@ -0,0 +1,81 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_FontScalingPanel {
color: $primary-content;
> .mx_SettingsTab_SubHeading {
margin-bottom: 32px;
}
}
.mx_FontScalingPanel .mx_Field {
width: 256px;
}
.mx_FontScalingPanel_fontSlider,
.mx_FontScalingPanel_fontSlider_preview {
@mixin mx_Settings_fullWidthField;
}
.mx_FontScalingPanel_fontSlider {
display: flex;
flex-direction: row;
align-items: center;
padding: 15px 15px 35px;
background: rgba($appearance-tab-border-color, 0.2);
border-radius: 10px;
font-size: 10px;
margin-top: 24px;
margin-bottom: 24px;
}
.mx_FontScalingPanel_fontSlider_preview {
border: 1px solid $appearance-tab-border-color;
border-radius: 10px;
padding: 0 16px 9px 16px;
pointer-events: none;
display: flow-root;
.mx_EventTile[data-layout=bubble] {
margin-top: 30px;
}
.mx_EventTile_msgOption {
display: none;
}
&.mx_IRCLayout {
padding-top: 9px;
}
}
.mx_FontScalingPanel_fontSlider_smallText {
font-size: 15px;
padding-right: 20px;
padding-left: 5px;
font-weight: 500;
}
.mx_FontScalingPanel_fontSlider_largeText {
font-size: 18px;
padding-left: 20px;
padding-right: 5px;
font-weight: 500;
}
.mx_FontScalingPanel_customFontSizeField {
margin-left: calc($font-16px + 10px);
}

View file

@ -26,12 +26,11 @@ limitations under the License.
.mx_SetIntegrationManager > .mx_SettingsTab_heading > .mx_SettingsTab_subheading {
display: inline-block;
padding-left: 5px;
margin-top: 0px;
}
.mx_SetIntegrationManager .mx_ToggleSwitch {
display: inline-block;
float: right;
top: 9px;
@mixin mx_Settings_fullWidthField;
}

View file

@ -0,0 +1,87 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_ThemeChoicePanel {
$radio-bg-color: $input-darker-bg-color;
color: $primary-content;
> .mx_ThemeSelectors {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin-top: 4px;
margin-bottom: 30px;
> .mx_RadioButton {
padding: $font-16px;
box-sizing: border-box;
border-radius: 10px;
width: 180px;
background: $radio-bg-color;
opacity: 0.4;
flex-shrink: 1;
flex-grow: 0;
margin-right: 15px;
margin-top: 10px;
font-weight: 600;
color: $muted-fg-color;
> span {
justify-content: center;
}
}
> .mx_RadioButton_enabled {
opacity: 1;
// These colors need to be hardcoded because they don't change with the theme
&.mx_ThemeSelector_light {
background-color: #f3f8fd;
color: #2e2f32;
}
&.mx_ThemeSelector_dark {
// 5% lightened version of 181b21
background-color: #25282e;
color: #f3f8fd;
> input > div {
border-color: $input-darker-bg-color;
> div {
border-color: $input-darker-bg-color;
}
}
}
&.mx_ThemeSelector_black {
background-color: #000000;
color: #f3f8fd;
> input > div {
border-color: $input-darker-bg-color;
> div {
border-color: $input-darker-bg-color;
}
}
}
}
}
}

View file

@ -14,147 +14,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_AppearanceUserSettingsTab_fontSlider,
.mx_AppearanceUserSettingsTab_fontSlider_preview {
@mixin mx_Settings_fullWidthField;
}
.mx_AppearanceUserSettingsTab .mx_Field {
width: 256px;
}
.mx_AppearanceUserSettingsTab_fontScaling {
color: $primary-content;
}
.mx_AppearanceUserSettingsTab_fontSlider {
display: flex;
flex-direction: row;
align-items: center;
padding: 15px;
background: rgba($appearance-tab-border-color, 0.2);
border-radius: 10px;
font-size: 10px;
margin-top: 24px;
margin-bottom: 24px;
}
.mx_AppearanceUserSettingsTab_fontSlider_preview {
border: 1px solid $appearance-tab-border-color;
border-radius: 10px;
padding: 0 16px 9px 16px;
pointer-events: none;
display: flow-root;
.mx_EventTile[data-layout=bubble] {
margin-top: 30px;
}
.mx_EventTile_msgOption {
display: none;
}
&.mx_IRCLayout {
padding-top: 9px;
}
}
.mx_AppearanceUserSettingsTab_fontSlider_smallText {
font-size: 15px;
padding-right: 20px;
padding-left: 5px;
font-weight: 500;
}
.mx_AppearanceUserSettingsTab_fontSlider_largeText {
font-size: 18px;
padding-left: 20px;
padding-right: 5px;
font-weight: 500;
}
.mx_AppearanceUserSettingsTab {
> .mx_SettingsTab_SubHeading {
margin-bottom: 32px;
}
}
.mx_AppearanceUserSettingsTab_themeSection {
$radio-bg-color: $input-darker-bg-color;
color: $primary-content;
> .mx_ThemeSelectors {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin-top: 4px;
margin-bottom: 30px;
> .mx_RadioButton {
padding: $font-16px;
box-sizing: border-box;
border-radius: 10px;
width: 180px;
background: $radio-bg-color;
opacity: 0.4;
flex-shrink: 1;
flex-grow: 0;
margin-right: 15px;
margin-top: 10px;
font-weight: 600;
color: $muted-fg-color;
> span {
justify-content: center;
}
}
> .mx_RadioButton_enabled {
opacity: 1;
// These colors need to be hardcoded because they don't change with the theme
&.mx_ThemeSelector_light {
background-color: #f3f8fd;
color: #2e2f32;
}
&.mx_ThemeSelector_dark {
// 5% lightened version of 181b21
background-color: #25282e;
color: #f3f8fd;
> input > div {
border-color: $input-darker-bg-color;
> div {
border-color: $input-darker-bg-color;
}
}
}
&.mx_ThemeSelector_black {
background-color: #000000;
color: #f3f8fd;
> input > div {
border-color: $input-darker-bg-color;
> div {
border-color: $input-darker-bg-color;
}
}
}
}
}
}
.mx_SettingsTab_customFontSizeField {
margin-left: calc($font-16px + 10px);
}
.mx_AppearanceUserSettingsTab_Advanced {
color: $primary-content;

View file

@ -35,7 +35,6 @@ limitations under the License.
margin-left: 2px;
margin-right: 2px;
&::before {
content: '';
display: inline-block;
@ -48,7 +47,6 @@ limitations under the License.
background-position: center;
}
&.mx_CallViewButtons_dialpad::before {
background-image: url('$(res)/img/voip/dialpad.svg');
}

View file

@ -200,7 +200,6 @@ limitations under the License.
}
}
.mx_CallView_presenting {
opacity: 1;
transition: opacity 0.5s;

View file

@ -1,4 +1 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 8V8C1.89543 8 1 7.10457 1 6V3C1 1.89543 1.89543 1 3 1H15C16.1046 1 17 1.89484 17 2.9994C17 3.88147 17 4.95392 17 6.00008C17 7.10465 16.1046 8 15 8H10.5" stroke="#737D8C" stroke-width="1.5" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.2011 16.7176C12.9087 17.011 12.9088 17.4866 13.2012 17.78C13.4936 18.0734 13.9677 18.0733 14.2601 17.78C14.9484 17.0894 15.6519 16.3829 16.1834 15.8491L16.8282 15.2014L17.0099 15.0188L17.0579 14.9706L17.0702 14.9582L17.0733 14.955L17.0741 14.9542L17.0743 14.954L17.0743 14.954L16.5444 14.4233L17.0744 14.954C17.3663 14.6606 17.3661 14.1855 17.0741 13.8922L14.2539 11.061C13.9616 10.7675 13.4875 10.7674 13.195 11.0606C12.9024 11.3539 12.9023 11.8295 13.1946 12.123L14.7442 13.6787L10.1137 13.6787C8.69795 13.6787 7.49996 12.4759 7.49996 10.9288L7.49996 7.00002C7.49996 6.58581 7.16417 6.25002 6.74996 6.25002C6.33574 6.25002 5.99996 6.58581 5.99996 7.00002L5.99996 10.9288C5.99996 13.2476 7.81395 15.1787 10.1137 15.1787H14.7341C14.2713 15.6436 13.7316 16.1854 13.2011 16.7176Z" fill="#737D8C"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" viewBox="0 0 18 18"><path fill="#17191C" d="M5 5.25a.75.75 0 0 0 0 1.5h8a.75.75 0 0 0 0-1.5H5ZM5 8.25a.75.75 0 0 0 0 1.5h4a.75.75 0 1 0 0-1.5H5Z"/><path fill="#17191C" fill-rule="evenodd" d="M3 .25A2.75 2.75 0 0 0 .25 3v14a.75.75 0 0 0 1.2.6L4.916 15c.217-.162.48-.25.75-.25H15A2.75 2.75 0 0 0 17.75 12V3A2.75 2.75 0 0 0 15 .25H3ZM1.75 3c0-.69.56-1.25 1.25-1.25h12c.69 0 1.25.56 1.25 1.25v9c0 .69-.56 1.25-1.25 1.25H5.666a2.75 2.75 0 0 0-1.65.55L1.75 15.5V3Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 562 B

View file

@ -1,6 +1,6 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="20" height="20" rx="4" fill="#FF4B55"/>
<rect width="20" height="20" rx="4" fill="#FF5B55"/>
<path d="M2 7H18V16C18 17.1046 17.1046 18 16 18H4C2.89543 18 2 17.1046 2 16V7Z" fill="white"/>
<rect x="3.96826" y="8.99951" width="2.99723" height="3" rx="0.25" fill="#FF4B55"/>
<rect x="10.9614" y="13" width="2.99723" height="3" rx="0.25" fill="#FF4B55"/>
<rect x="3.96826" y="8.99951" width="2.99723" height="3" rx="0.25" fill="#FF5B55"/>
<rect x="10.9614" y="13" width="2.99723" height="3" rx="0.25" fill="#FF5B55"/>
</svg>

Before

Width:  |  Height:  |  Size: 430 B

After

Width:  |  Height:  |  Size: 430 B

View file

@ -28,5 +28,5 @@
<path
id="path2"
d="M 12 2 C 6.47715 2 2 6.47715 2 12 C 2 17.5228 6.47715 22 12 22 C 17.5228 22 22 17.5228 22 12 C 22 6.47715 17.5228 2 12 2 z M 11.880859 5.5039062 C 12.720859 5.4439063 13.470547 6.0746875 13.560547 6.9296875 L 13.560547 7.1699219 L 13.080078 13.169922 C 13.035078 13.724922 12.570625 14.144531 12.015625 14.144531 L 11.925781 14.144531 C 11.400781 14.099531 10.996172 13.694922 10.951172 13.169922 L 10.470703 7.1699219 C 10.395703 6.3149219 11.025859 5.5639064 11.880859 5.5039062 z M 12 15.763672 C 12.729 15.763672 13.320312 16.354884 13.320312 17.083984 C 13.320313 17.812984 12.729 18.404297 12 18.404297 C 11.271 18.404297 10.679688 17.812984 10.679688 17.083984 C 10.679688 16.354884 11.271 15.763672 12 15.763672 z "
style="fill:#ff4b55;fill-opacity:1" />
style="fill:#ff5b55;fill-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,4 @@
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.999756 0.999756L8.99975 8.99975" stroke="#737D8C" stroke-width="2" stroke-linecap="round"/>
<path d="M9.00049 0.999756L1.00049 8.99975" stroke="#737D8C" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 310 B

View file

@ -1,3 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C9.23858 2 7 4.23858 7 7L17 7C17 4.23858 14.7614 2 12 2ZM2.29289 7.70711C1.90237 7.31658 1.90237 6.68342 2.29289 6.29289C2.68342 5.90237 3.31658 5.90237 3.70711 6.29289L6.41421 9H17.5858L20.2929 6.29289C20.6834 5.90237 21.3166 5.90237 21.7071 6.29289C22.0976 6.68342 22.0976 7.31658 21.7071 7.70711L19 10.4142V13H22C22.5523 13 23 13.4477 23 14C23 14.5523 22.5523 15 22 15H19C19 15.7795 18.8726 16.5292 18.6375 17.2295C18.6614 17.2493 18.6847 17.2705 18.7071 17.2929L21.7071 20.2929C22.0976 20.6834 22.0976 21.3166 21.7071 21.7071C21.3166 22.0976 20.6834 22.0976 20.2929 21.7071L17.6791 19.0933C16.5924 20.5983 14.9222 21.6542 13 21.9291L13 12C13 11.4477 12.5523 11 12 11C11.4477 11 11 11.4477 11 12V21.9291C9.07785 21.6542 7.40759 20.5983 6.32091 19.0933L3.70711 21.7071C3.31658 22.0976 2.68342 22.0976 2.29289 21.7071C1.90237 21.3166 1.90237 20.6834 2.29289 20.2929L5.29289 17.2929C5.31533 17.2705 5.33857 17.2493 5.36252 17.2295C5.1274 16.5292 5 15.7795 5 15H2C1.44772 15 1 14.5523 1 14C1 13.4477 1.44772 13 2 13H5V10.4142L2.29289 7.70711Z" fill="#FF4B55"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C9.23858 2 7 4.23858 7 7L17 7C17 4.23858 14.7614 2 12 2ZM2.29289 7.70711C1.90237 7.31658 1.90237 6.68342 2.29289 6.29289C2.68342 5.90237 3.31658 5.90237 3.70711 6.29289L6.41421 9H17.5858L20.2929 6.29289C20.6834 5.90237 21.3166 5.90237 21.7071 6.29289C22.0976 6.68342 22.0976 7.31658 21.7071 7.70711L19 10.4142V13H22C22.5523 13 23 13.4477 23 14C23 14.5523 22.5523 15 22 15H19C19 15.7795 18.8726 16.5292 18.6375 17.2295C18.6614 17.2493 18.6847 17.2705 18.7071 17.2929L21.7071 20.2929C22.0976 20.6834 22.0976 21.3166 21.7071 21.7071C21.3166 22.0976 20.6834 22.0976 20.2929 21.7071L17.6791 19.0933C16.5924 20.5983 14.9222 21.6542 13 21.9291L13 12C13 11.4477 12.5523 11 12 11C11.4477 11 11 11.4477 11 12V21.9291C9.07785 21.6542 7.40759 20.5983 6.32091 19.0933L3.70711 21.7071C3.31658 22.0976 2.68342 22.0976 2.29289 21.7071C1.90237 21.3166 1.90237 20.6834 2.29289 20.2929L5.29289 17.2929C5.31533 17.2705 5.33857 17.2493 5.36252 17.2295C5.1274 16.5292 5 15.7795 5 15H2C1.44772 15 1 14.5523 1 14C1 13.4477 1.44772 13 2 13H5V10.4142L2.29289 7.70711Z" fill="#FF5B55"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -1,96 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="128"
height="128"
viewBox="0 0 33.866666 33.866668"
version="1.1"
id="svg920"
inkscape:version="1.0.2 (e86c870879, 2021-01-15)"
sodipodi:docname="spinner.svg">
<defs
id="defs914" />
<metadata
id="metadata917">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="stroke-width:0;fill-opacity:0.30000001"
d="M 59,95.605469 V 123 c 0,2.77 2.23,5 5,5 2.77,0 5,-2.23 5,-5 V 95.605469 A 31.999998,31.999998 0 0 1 64,96 31.999998,31.999998 0 0 1 59,95.605469 Z"
transform="scale(0.26458333)"
id="path2350" />
<path
style="stroke-width:0;fill-opacity:0.7020452"
d="M 64,0 C 61.23,0 59,2.2300001 59,5 V 32.394531 A 31.999998,31.999998 0 0 1 64,32 31.999998,31.999998 0 0 1 69,32.394531 V 5 C 69,2.2300001 66.77,0 64,0 Z"
transform="scale(0.26458333)"
id="rect2283" />
<path
style="stroke-width:0;fill-opacity:0.30000001"
d="M 43.867188,88.871094 30.169922,112.5957 c -1.385,2.39889 -0.568812,5.44508 1.830078,6.83008 2.39889,1.385 5.445078,0.56881 6.830078,-1.83008 L 52.527344,93.873047 a 31.999998,31.999998 0 0 1 -8.660156,-5.001953 z"
transform="scale(0.26458333)"
id="path2346" />
<path
style="stroke-width:0;fill-opacity:0.80019373"
d="m 93.150391,7.9121094 c -1.599848,0.111837 -3.114844,0.992881 -3.980469,2.4921876 L 75.472656,34.126953 a 31.999998,31.999998 0 0 1 8.660156,5.001953 L 97.830078,15.404297 C 99.215078,13.005407 98.39889,9.9592187 96,8.5742188 95.100416,8.0548438 94.110299,7.8450072 93.150391,7.9121094 Z"
transform="scale(0.26458333)"
id="rect2285" />
<path
style="stroke-width:0;fill-opacity:0.30000001"
d="M 34.126953,75.474609 10.404297,89.169922 C 8.0054066,90.554922 7.1892188,93.60111 8.5742188,96 c 1.3849999,2.39889 4.4311882,3.215078 6.8300782,1.830078 L 39.128906,84.132812 a 31.999998,31.999998 0 0 1 -5.001953,-8.658203 z"
transform="scale(0.26458333)"
id="path2342" />
<path
style="stroke-width:0;fill-opacity:0.90226436"
d="m 115.44531,29.507812 c -0.95991,-0.0671 -1.95002,0.142735 -2.84961,0.66211 L 88.871094,43.867188 a 31.999998,31.999998 0 0 1 5.001953,8.658203 L 117.5957,38.830078 c 2.39889,-1.385 3.21508,-4.431188 1.83008,-6.830078 -0.86562,-1.499306 -2.38062,-2.38035 -3.98047,-2.492188 z"
transform="scale(0.26458333)"
id="rect2287" />
<path
style="stroke-width:0;fill-opacity:1"
d="M 95.605469,59 A 31.999998,31.999998 0 0 1 96,64 31.999998,31.999998 0 0 1 95.605469,69 H 123 c 2.77,0 5,-2.23 5,-5 0,-2.77 -2.23,-5 -5,-5 z"
transform="scale(0.26458333)"
id="path2338" />
<path
style="stroke-width:0;fill-opacity:0.40288368"
d="m 5,59 c -2.7699999,0 -5,2.23 -5,5 0,2.77 2.2300001,5 5,5 H 32.394531 A 31.999998,31.999998 0 0 1 32,64 31.999998,31.999998 0 0 1 32.394531,59 Z"
transform="scale(0.26458333)"
id="rect2289" />
<path
style="stroke-width:0;fill-opacity:0.30000001"
d="m 93.873047,75.472656 a 31.999998,31.999998 0 0 1 -5.001953,8.660156 L 112.5957,97.830078 c 2.39889,1.385 5.44508,0.568812 6.83008,-1.830078 1.385,-2.39889 0.56881,-5.445078 -1.83008,-6.830078 z"
transform="scale(0.26458333)"
id="path2334" />
<path
style="stroke-width:0;fill-opacity:0.49898377"
d="M 12.554688,29.507812 C 10.95484,29.61965 9.4398437,30.500694 8.5742188,32 c -1.385,2.39889 -0.5688122,5.445078 1.8300782,6.830078 l 23.722656,13.697266 a 31.999998,31.999998 0 0 1 5.001953,-8.660156 L 15.404297,30.169922 c -0.899584,-0.519375 -1.889701,-0.729212 -2.849609,-0.66211 z"
transform="scale(0.26458333)"
id="rect2291" />
<path
style="stroke-width:0;fill-opacity:0.30000001"
d="m 84.132812,88.871094 a 31.999998,31.999998 0 0 1 -8.658203,5.001953 L 89.169922,117.5957 c 1.385,2.39889 4.431188,3.21508 6.830078,1.83008 2.39889,-1.385 3.215078,-4.43119 1.830078,-6.83008 z"
transform="scale(0.26458333)"
id="path2330" />
<path
style="stroke-width:0;fill-opacity:0.5998317"
d="M 34.849609,7.9121094 C 33.889701,7.8450072 32.899584,8.0548438 32,8.5742188 29.60111,9.9592187 28.784922,13.005407 30.169922,15.404297 l 13.697266,23.724609 a 31.999998,31.999998 0 0 1 8.658203,-5.001953 L 38.830078,10.404297 C 37.964453,8.9049904 36.449457,8.0239464 34.849609,7.9121094 Z"
transform="scale(0.26458333)"
id="rect2293" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.1 KiB

View file

@ -0,0 +1,3 @@
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="15" cy="15" r="14" stroke="#E3E8F0" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 170 B

View file

@ -0,0 +1,3 @@
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M29 15C29 12.4307 28.293 9.91095 26.9563 7.7167C25.6197 5.52246 23.705 3.73836 21.4219 2.55979C19.1389 1.38123 16.5755 0.853662 14.0126 1.03487C11.4497 1.21607 8.98611 2.09906 6.8916 3.58713" stroke="#737D8C" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 387 B

View file

@ -165,6 +165,9 @@ $button-link-bg-color: transparent;
// Toggle switch
$togglesw-off-color: $room-highlight-color;
// Slider
$slider-background-color: $quinary-content;
$progressbar-fg-color: $accent-color;
$progressbar-bg-color: $system;
@ -209,6 +212,8 @@ $appearance-tab-border-color: $room-highlight-color;
$composer-shadow-color: rgba(0, 0, 0, 0.28);
$codeblock-background-color: #2a3039;
// Bubble tiles
$eventbubble-self-bg: #14322E;
$eventbubble-others-bg: $event-selected-color;

View file

@ -221,6 +221,8 @@ $appearance-tab-border-color: $room-highlight-color;
$composer-shadow-color: tranparent;
$codeblock-background-color: #2a3039;
// Bubble tiles
$eventbubble-self-bg: #14322E;
$eventbubble-others-bg: $event-selected-color;

View file

@ -334,6 +334,8 @@ $appearance-tab-border-color: $input-darker-bg-color;
$composer-shadow-color: tranparent;
$codeblock-background-color: $header-panel-bg-color;
// Bubble tiles
$eventbubble-self-bg: #F0FBF8;
$eventbubble-others-bg: $system;

View file

@ -0,0 +1,117 @@
//// Reference: https://www.figma.com/file/RnLKnv09glhxGIZtn8zfmh/UI-Themes-%26-Accessibility?node-id=321%3A65847
$accent: #268075;
$alert: #D62C25;
$links: #0A6ECA;
$primary-content: #17191C;
$secondary-content: #5E6266;
$tertiary-content: $secondary-content;
$quaternary-content: $secondary-content;
$quinary-content: $secondary-content;
$roomlist-button-bg-color: rgba(141, 151, 165, 0.2);
$username-variant1-color: #0A6ECA;
$username-variant2-color: #AC3BA8;
$username-variant3-color: #078662;
$username-variant4-color: #CC1449;
$username-variant5-color: #BE4C00;
$username-variant6-color: #1C7274;
$username-variant7-color: #5C56F5;
$username-variant8-color: #3E810A;
$accent-color: $accent;
$accent-color-50pct: rgba($accent-color, 0.5);
$accent-color-alt: $links;
$input-border-color: $secondary-content;
$input-darker-bg-color: $quinary-content;
$input-darker-fg-color: $secondary-content;
$input-lighter-fg-color: $input-darker-fg-color;
$input-valid-border-color: $accent-color;
$input-focused-border-color: $accent-color;
$button-bg-color: $accent-color;
$resend-button-divider-color: $input-darker-bg-color;
$icon-button-color: $quaternary-content;
$theme-button-bg-color: $quinary-content;
$presence-online: $accent-color;
$presence-offline: $quinary-content;
$pinned-color: $tertiary-content;
$tab-label-active-bg-color: $accent-color;
$button-primary-bg-color: $accent-color;
$button-secondary-bg-color: $accent-fg-color;
$button-link-fg-color: $accent-color;
$togglesw-on-color: $accent-color;
$slider-selection-color: $accent-color;
$progressbar-fg-color: $accent-color;
$message-action-bar-fg-color: $primary-content;
$reaction-row-button-selected-border-color: $accent-color;
$voice-record-stop-border-color: $quinary-content;
$voice-record-icon-color: $tertiary-content;
$appearance-tab-border-color: $input-darker-bg-color;
$eventbubble-reply-color: $quaternary-content;
$notice-primary-color: $alert;
$warning-color: $notice-primary-color; // red
$pinned-unread-color: $notice-primary-color;
$button-danger-bg-color: $notice-primary-color;
$mention-user-pill-bg-color: $warning-color;
$input-invalid-border-color: $warning-color;
$event-highlight-fg-color: $warning-color;
$roomtopic-color: $secondary-content;
@define-mixin mx_DialogButton_danger {
background-color: $accent-color;
}
@define-mixin mx_DialogButton_secondary {
// flip colours for the secondary ones
font-weight: 600;
border: 1px solid $accent-color !important;
color: $accent-color;
background-color: $button-secondary-bg-color;
}
@define-mixin mx_Dialog_link {
color: $accent-color;
text-decoration: none;
}
.mx_AccessibleButton {
margin-left: 4px;
}
.mx_AccessibleButton:focus {
outline: 2px solid $accent-color;
outline-offset: 2px;
}
.mx_BasicMessageComposer .mx_BasicMessageComposer_inputEmpty > :first-child::before {
color: $secondary-content;
opacity: 1 !important;
}
.mx_TextualEvent {
color: $secondary-content;
opacity: 1 !important;
}
.mx_Dialog, .mx_MatrixChat_wrapper {
:not(.mx_textinput):not(.mx_Field):not(.mx_no_textinput) > input[type=text]::placeholder,
:not(.mx_textinput):not(.mx_Field):not(.mx_no_textinput) > input[type=search]::placeholder,
.mx_textinput input::placeholder {
color: $input-darker-fg-color !important;
}
}
.mx_UserMenu_contextMenu .mx_UserMenu_contextMenu_header .mx_UserMenu_contextMenu_themeButton {
background-color: $roomlist-button-bg-color !important;
}
.mx_FontScalingPanel_fontSlider {
background-color: $roomlist-button-bg-color !important;
}
.mx_ThemeChoicePanel > .mx_ThemeSelectors > .mx_RadioButton input[type="radio"]:disabled + div {
border-color: $primary-content;
}
.mx_ThemeChoicePanel > .mx_ThemeSelectors > .mx_RadioButton.mx_RadioButton_disabled {
color: $primary-content;
}

View file

@ -0,0 +1,8 @@
@import "../../../../res/css/_font-sizes.scss";
@import "../../light/css/_paths.scss";
@import "../../light/css/_fonts.scss";
@import "../../light/css/_light.scss";
@import "_light-high-contrast.scss";
@import "../../light/css/_mods.scss";
@import "../../../../res/css/_components.scss";
@import url("highlight.js/styles/atom-one-light.css");

View file

@ -35,7 +35,7 @@ $space-nav: rgba($tertiary-content, 0.15);
// try to use these colors when possible
$accent-color: $accent;
$accent-bg-color: rgba(3, 179, 129, 0.16);
$notice-primary-color: #ff4b55;
$notice-primary-color: $alert;
$notice-primary-bg-color: rgba(255, 75, 85, 0.16);
$header-panel-bg-color: #f3f8fd;
@ -318,8 +318,8 @@ $breadcrumb-placeholder-bg-color: #e8eef5;
// These two don't change between themes. They are the $warning-color, but we don't
// want custom themes to affect them by accident.
$voice-record-stop-symbol-color: #ff4b55;
$voice-record-live-circle-color: #ff4b55;
$voice-record-stop-symbol-color: #ff5b55;
$voice-record-live-circle-color: #ff5b55;
$voice-record-stop-border-color: $quinary-content;
$voice-record-icon-color: $tertiary-content;
@ -333,6 +333,8 @@ $appearance-tab-border-color: $input-darker-bg-color;
}
$composer-shadow-color: rgba(0, 0, 0, 0.04);
$codeblock-background-color: $header-panel-bg-color;
// Bubble tiles
$eventbubble-self-bg: #F0FBF8;
$eventbubble-others-bg: $system;

View file

@ -0,0 +1,38 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
declare module "browser-encrypt-attachment" {
interface IInfo {
v: string;
key: {
alg: string;
key_ops: string[]; // eslint-disable-line camelcase
kty: string;
k: string;
ext: boolean;
};
iv: string;
hashes: {[alg: string]: string};
}
interface IEncryptedAttachment {
data: ArrayBuffer;
info: IInfo;
}
export function encryptAttachment(plaintextBuffer: ArrayBuffer): Promise<IEncryptedAttachment>;
export function decryptAttachment(ciphertextBuffer: ArrayBuffer, info: IInfo): Promise<ArrayBuffer>;
}

View file

@ -99,6 +99,7 @@ declare global {
mxSkinner?: Skinner;
mxOnRecaptchaLoaded?: () => void;
electron?: Electron;
mxSendSentryReport: (userText: string, issueUrl: string, error: Error) => Promise<void>;
}
interface DesktopCapturerSource {

View file

@ -0,0 +1,26 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
declare module "png-chunks-extract" {
interface IChunk {
name: string;
data: Uint8Array;
}
function extractPngChunks(data: Uint8Array | Buffer): IChunk[];
export default extractPngChunks;
}

View file

@ -18,6 +18,9 @@ limitations under the License.
import React from "react";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { IEncryptedFile, IMediaEventInfo } from "./customisations/models/IMediaEventContent";
import { IUploadOpts } from "matrix-js-sdk/src/@types/requests";
import { MsgType } from "matrix-js-sdk/src/@types/event";
import dis from './dispatcher/dispatcher';
import * as sdk from './index';
@ -307,7 +310,7 @@ function loadVideoElement(videoFile): Promise<HTMLVideoElement> {
function infoForVideoFile(matrixClient, roomId, videoFile) {
const thumbnailType = "image/jpeg";
let videoInfo;
let videoInfo: Partial<IMediaEventInfo>;
return loadVideoElement(videoFile).then((video) => {
return createThumbnail(video, video.videoWidth, video.videoHeight, thumbnailType);
}).then((result) => {
@ -356,49 +359,48 @@ export function uploadFile(
matrixClient: MatrixClient,
roomId: string,
file: File | Blob,
progressHandler?: any, // TODO: Types
): IAbortablePromise<{url?: string, file?: any}> { // TODO: Types
progressHandler?: IUploadOpts["progressHandler"],
): IAbortablePromise<{ url?: string, file?: IEncryptedFile }> {
let canceled = false;
if (matrixClient.isRoomEncrypted(roomId)) {
// If the room is encrypted then encrypt the file before uploading it.
// First read the file into memory.
let uploadPromise;
let encryptInfo;
let uploadPromise: IAbortablePromise<string>;
const prom = readFileAsArrayBuffer(file).then(function(data) {
if (canceled) throw new UploadCanceledError();
// Then encrypt the file.
return encrypt.encryptAttachment(data);
}).then(function(encryptResult) {
if (canceled) throw new UploadCanceledError();
// Record the information needed to decrypt the attachment.
encryptInfo = encryptResult.info;
// Pass the encrypted data as a Blob to the uploader.
const blob = new Blob([encryptResult.data]);
uploadPromise = matrixClient.uploadContent(blob, {
progressHandler: progressHandler,
progressHandler,
includeFilename: false,
});
return uploadPromise;
}).then(function(url) {
if (canceled) throw new UploadCanceledError();
// If the attachment is encrypted then bundle the URL along
// with the information needed to decrypt the attachment and
// add it under a file key.
encryptInfo.url = url;
if (file.type) {
encryptInfo.mimetype = file.type;
}
return { "file": encryptInfo };
}) as IAbortablePromise<{ file: any }>;
return uploadPromise.then(url => {
if (canceled) throw new UploadCanceledError();
// If the attachment is encrypted then bundle the URL along
// with the information needed to decrypt the attachment and
// add it under a file key.
return {
file: {
...encryptResult.info,
url,
},
};
});
}) as IAbortablePromise<{ file: IEncryptedFile }>;
prom.abort = () => {
canceled = true;
if (uploadPromise) matrixClient.cancelUpload(uploadPromise);
};
return prom;
} else {
const basePromise = matrixClient.uploadContent(file, {
progressHandler: progressHandler,
});
const basePromise = matrixClient.uploadContent(file, { progressHandler });
const promise1 = basePromise.then(function(url) {
if (canceled) throw new UploadCanceledError();
// If the attachment isn't encrypted then include the URL directly.
@ -554,29 +556,29 @@ export default class ContentMessages {
const prom = new Promise<void>((resolve) => {
if (file.type.indexOf('image/') === 0) {
content.msgtype = 'm.image';
content.msgtype = MsgType.Image;
infoForImageFile(matrixClient, roomId, file).then((imageInfo) => {
Object.assign(content.info, imageInfo);
resolve();
}, (e) => {
logger.error(e);
content.msgtype = 'm.file';
content.msgtype = MsgType.File;
resolve();
});
} else if (file.type.indexOf('audio/') === 0) {
content.msgtype = 'm.audio';
content.msgtype = MsgType.Audio;
resolve();
} else if (file.type.indexOf('video/') === 0) {
content.msgtype = 'm.video';
content.msgtype = MsgType.Video;
infoForVideoFile(matrixClient, roomId, file).then((videoInfo) => {
Object.assign(content.info, videoInfo);
resolve();
}, (e) => {
content.msgtype = 'm.file';
content.msgtype = MsgType.File;
resolve();
});
} else {
content.msgtype = 'm.file';
content.msgtype = MsgType.File;
resolve();
}
}) as IAbortablePromise<void>;

View file

@ -59,6 +59,7 @@ import SessionRestoreErrorDialog from "./components/views/dialogs/SessionRestore
import StorageEvictedDialog from "./components/views/dialogs/StorageEvictedDialog";
import { logger } from "matrix-js-sdk/src/logger";
import { setSentryUser } from "./sentry";
const HOMESERVER_URL_KEY = "mx_hs_url";
const ID_SERVER_URL_KEY = "mx_is_url";
@ -455,7 +456,7 @@ async function handleLoadSessionFailure(e: Error): Promise<boolean> {
logger.error("Unable to load session", e);
const modal = Modal.createTrackedDialog('Session Restore Error', '', SessionRestoreErrorDialog, {
error: e.message,
error: e,
});
const [success] = await modal.finished;
@ -582,6 +583,8 @@ async function doSetLoggedIn(
PosthogAnalytics.instance.updateAnonymityFromSettings(credentials.userId);
setSentryUser(credentials.userId);
const client = MatrixClientPeg.get();
if (credentials.freshLogin && SettingsStore.getValue("feature_dehydration")) {

View file

@ -295,6 +295,7 @@ class MatrixClientPegClass implements IMatrixClientPeg {
const notifTimelineSet = new EventTimelineSet(null, {
timelineSupport: true,
pendingEvents: false,
});
// XXX: what is our initial pagination token?! it somehow needs to be synchronised with /sync.
notifTimelineSet.getLiveTimeline().setPaginationToken("", EventTimeline.BACKWARDS);

View file

@ -18,7 +18,7 @@ limitations under the License.
import React from 'react';
import ReactDOM from 'react-dom';
import classNames from 'classnames';
import { defer } from "matrix-js-sdk/src/utils";
import { defer, sleep } from "matrix-js-sdk/src/utils";
import Analytics from './Analytics';
import dis from './dispatcher/dispatcher';
@ -332,7 +332,10 @@ export class ModalManager {
return this.priorityModal ? this.priorityModal : (this.modals[0] || this.staticModal);
}
private reRender() {
private async reRender() {
// await next tick because sometimes ReactDOM can race with itself and cause the modal to wrongly stick around
await sleep(0);
if (this.modals.length === 0 && !this.priorityModal && !this.staticModal) {
// If there is no modal to render, make all of Element available
// to screen reader users again

View file

@ -183,7 +183,7 @@ Response:
name: "dashboard",
data: {key: "val"}
}
room_id: !foo:bar,
room_id: "!foo:bar",
sender: "@alice:localhost"
}
]
@ -202,7 +202,7 @@ Example:
name: "dashboard",
data: {key: "val"}
}
room_id: !foo:bar,
room_id: "!foo:bar",
sender: "@alice:localhost"
}
]

View file

@ -1,6 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -15,8 +15,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
export interface ISsoRedirectOptions {
immediate?: boolean;
on_welcome_page?: boolean; // eslint-disable-line camelcase
}
export interface ConfigOptions {
[key: string]: any;
// sso_immediate_redirect is deprecated in favour of sso_redirect_options.immediate
sso_immediate_redirect?: boolean; // eslint-disable-line camelcase
sso_redirect_options?: ISsoRedirectOptions; // eslint-disable-line camelcase
}
export const DEFAULTS: ConfigOptions = {
@ -74,3 +83,14 @@ export default class SdkConfig {
SdkConfig.put(newConfig);
}
}
export function parseSsoRedirectOptions(config: ConfigOptions): ISsoRedirectOptions {
// Ignore deprecated options if the config is using new ones
if (config.sso_redirect_options) return config.sso_redirect_options;
// We can cheat here because the default is false anyways
if (config.sso_immediate_redirect) return { immediate: true };
// Default: do nothing
return {};
}

View file

@ -32,6 +32,7 @@ import SecurityCustomisations from "./customisations/Security";
import { DeviceTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning';
import { logger } from "matrix-js-sdk/src/logger";
import { ComponentType } from "react";
// This stores the secret storage private keys in memory for the JS SDK. This is
// only meant to act as a cache to avoid prompting the user multiple times
@ -335,7 +336,9 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
// This dialog calls bootstrap itself after guiding the user through
// passphrase creation.
const { finished } = Modal.createTrackedDialogAsync('Create Secret Storage dialog', '',
import("./async-components/views/dialogs/security/CreateSecretStorageDialog"),
import(
"./async-components/views/dialogs/security/CreateSecretStorageDialog"
) as unknown as Promise<ComponentType<{}>>,
{
forceReset,
},

View file

@ -1014,14 +1014,14 @@ export const Commands = [
new Command({
command: "msg",
description: _td("Sends a message to the given user"),
args: "<user-id> <message>",
args: "<user-id> [<message>]",
runFn: function(roomId, args) {
if (args) {
// matches the first whitespace delimited group and then the rest of the string
const matches = args.match(/^(\S+?)(?: +(.*))?$/s);
if (matches) {
const [userId, msg] = matches.slice(1);
if (msg && userId && userId.startsWith("@") && userId.includes(":")) {
if (userId && userId.startsWith("@") && userId.includes(":")) {
return success((async () => {
const cli = MatrixClientPeg.get();
const roomId = await ensureDMExists(cli, userId);
@ -1029,7 +1029,9 @@ export const Commands = [
action: 'view_room',
room_id: roomId,
});
cli.sendTextMessage(roomId, msg);
if (msg) {
cli.sendTextMessage(roomId, msg);
}
})());
}
}

View file

@ -181,7 +181,7 @@ export async function startTermsFlow(
return Promise.all(agreePromises);
}
export function dialogTermsInteractionCallback(
export async function dialogTermsInteractionCallback(
policiesAndServicePairs: {
service: Service;
policies: { [policy: string]: Policy };
@ -189,21 +189,18 @@ export function dialogTermsInteractionCallback(
agreedUrls: string[],
extraClassNames?: string,
): Promise<string[]> {
return new Promise((resolve, reject) => {
logger.log("Terms that need agreement", policiesAndServicePairs);
// FIXME: Using an import will result in test failures
const TermsDialog = sdk.getComponent("views.dialogs.TermsDialog");
logger.log("Terms that need agreement", policiesAndServicePairs);
// FIXME: Using an import will result in test failures
const TermsDialog = sdk.getComponent("views.dialogs.TermsDialog");
Modal.createTrackedDialog('Terms of Service', '', TermsDialog, {
policiesAndServicePairs,
agreedUrls,
onFinished: (done, agreedUrls) => {
if (!done) {
reject(new TermsNotSignedError());
return;
}
resolve(agreedUrls);
},
}, classNames("mx_TermsDialog", extraClassNames));
});
const { finished } = Modal.createTrackedDialog<[boolean, string[]]>('Terms of Service', '', TermsDialog, {
policiesAndServicePairs,
agreedUrls,
}, classNames("mx_TermsDialog", extraClassNames));
const [done, _agreedUrls] = await finished;
if (!done) {
throw new TermsNotSignedError();
}
return _agreedUrls;
}

View file

@ -13,6 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { _t } from './languageHandler';
import * as Roles from './Roles';
@ -25,9 +26,13 @@ import { Action } from './dispatcher/actions';
import defaultDispatcher from './dispatcher/dispatcher';
import { SetRightPanelPhasePayload } from './dispatcher/payloads/SetRightPanelPhasePayload';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { GuestAccess, HistoryVisibility, JoinRule } from "matrix-js-sdk/src/@types/partials";
import { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
import { MatrixClientPeg } from "./MatrixClientPeg";
import { ROOM_SECURITY_TAB } from "./components/views/dialogs/RoomSettingsDialog";
import { logger } from "matrix-js-sdk/src/logger";
import { removeDirectionOverrideChars } from 'matrix-js-sdk/src/utils';
// These functions are frequently used just to check whether an event has
// any text to display at all. For this reason they return deferred values
@ -97,18 +102,21 @@ function textForMemberEvent(ev: MatrixEvent, allowJSX: boolean, showHiddenEvents
if (prevContent && prevContent.membership === 'join') {
if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) {
return () => _t('%(oldDisplayName)s changed their display name to %(displayName)s', {
oldDisplayName: prevContent.displayname,
displayName: content.displayname,
// We're taking the display namke directly from the event content here so we need
// to strip direction override chars which the js-sdk would normally do when
// calculating the display name
oldDisplayName: removeDirectionOverrideChars(prevContent.displayname),
displayName: removeDirectionOverrideChars(content.displayname),
});
} else if (!prevContent.displayname && content.displayname) {
return () => _t('%(senderName)s set their display name to %(displayName)s', {
senderName: ev.getSender(),
displayName: content.displayname,
displayName: removeDirectionOverrideChars(content.displayname),
});
} else if (prevContent.displayname && !content.displayname) {
return () => _t('%(senderName)s removed their display name (%(oldDisplayName)s)', {
senderName,
oldDisplayName: prevContent.displayname,
oldDisplayName: removeDirectionOverrideChars(prevContent.displayname),
});
} else if (prevContent.avatar_url && !content.avatar_url) {
return () => _t('%(senderName)s removed their profile picture', { senderName });
@ -197,17 +205,38 @@ function textForTombstoneEvent(ev: MatrixEvent): () => string | null {
return () => _t('%(senderDisplayName)s upgraded this room.', { senderDisplayName });
}
function textForJoinRulesEvent(ev: MatrixEvent): () => string | null {
const onViewJoinRuleSettingsClick = () => {
defaultDispatcher.dispatch({
action: "open_room_settings",
initial_tab_id: ROOM_SECURITY_TAB,
});
};
function textForJoinRulesEvent(ev: MatrixEvent, allowJSX: boolean): () => string | JSX.Element | null {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
switch (ev.getContent().join_rule) {
case "public":
case JoinRule.Public:
return () => _t('%(senderDisplayName)s made the room public to whoever knows the link.', {
senderDisplayName,
});
case "invite":
case JoinRule.Invite:
return () => _t('%(senderDisplayName)s made the room invite only.', {
senderDisplayName,
});
case JoinRule.Restricted:
if (allowJSX) {
return () => <span>
{ _t('%(senderDisplayName)s changed who can join this room. <a>View settings</a>.', {
senderDisplayName,
}, {
"a": (sub) => <a onClick={onViewJoinRuleSettingsClick}>
{ sub }
</a>,
}) }
</span>;
}
return () => _t('%(senderDisplayName)s changed who can join this room.', { senderDisplayName });
default:
// The spec supports "knock" and "private", however nothing implements these.
return () => _t('%(senderDisplayName)s changed the join rule to %(rule)s', {
@ -220,9 +249,9 @@ function textForJoinRulesEvent(ev: MatrixEvent): () => string | null {
function textForGuestAccessEvent(ev: MatrixEvent): () => string | null {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
switch (ev.getContent().guest_access) {
case "can_join":
case GuestAccess.CanJoin:
return () => _t('%(senderDisplayName)s has allowed guests to join the room.', { senderDisplayName });
case "forbidden":
case GuestAccess.Forbidden:
return () => _t('%(senderDisplayName)s has prevented guests from joining the room.', { senderDisplayName });
default:
// There's no other options we can expect, however just for safety's sake we'll do this.
@ -308,11 +337,11 @@ function textForMessageEvent(ev: MatrixEvent): () => string | null {
|| redactedBecauseUserId });
}
}
if (ev.getContent().msgtype === "m.emote") {
if (ev.getContent().msgtype === MsgType.Emote) {
message = "* " + senderDisplayName + " " + message;
} else if (ev.getContent().msgtype === "m.image") {
} else if (ev.getContent().msgtype === MsgType.Image) {
message = _t('%(senderDisplayName)s sent an image.', { senderDisplayName });
} else if (ev.getType() == "m.sticker") {
} else if (ev.getType() == EventType.Sticker) {
message = _t('%(senderDisplayName)s sent a sticker.', { senderDisplayName });
} else {
// in this case, parse it as a plain text message
@ -392,15 +421,15 @@ function textForThreePidInviteEvent(event: MatrixEvent): () => string | null {
function textForHistoryVisibilityEvent(event: MatrixEvent): () => string | null {
const senderName = event.sender ? event.sender.name : event.getSender();
switch (event.getContent().history_visibility) {
case 'invited':
case HistoryVisibility.Invited:
return () => _t('%(senderName)s made future room history visible to all room members, '
+ 'from the point they are invited.', { senderName });
case 'joined':
case HistoryVisibility.Joined:
return () => _t('%(senderName)s made future room history visible to all room members, '
+ 'from the point they joined.', { senderName });
case 'shared':
case HistoryVisibility.Shared:
return () => _t('%(senderName)s made future room history visible to all room members.', { senderName });
case 'world_readable':
case HistoryVisibility.WorldReadable:
return () => _t('%(senderName)s made future room history visible to anyone.', { senderName });
default:
return () => _t('%(senderName)s made future room history visible to unknown (%(visibility)s).', {
@ -691,25 +720,25 @@ interface IHandlers {
}
const handlers: IHandlers = {
'm.room.message': textForMessageEvent,
'm.sticker': textForMessageEvent,
'm.call.invite': textForCallInviteEvent,
[EventType.RoomMessage]: textForMessageEvent,
[EventType.Sticker]: textForMessageEvent,
[EventType.CallInvite]: textForCallInviteEvent,
};
const stateHandlers: IHandlers = {
'm.room.canonical_alias': textForCanonicalAliasEvent,
'm.room.name': textForRoomNameEvent,
'm.room.topic': textForTopicEvent,
'm.room.member': textForMemberEvent,
"m.room.avatar": textForRoomAvatarEvent,
'm.room.third_party_invite': textForThreePidInviteEvent,
'm.room.history_visibility': textForHistoryVisibilityEvent,
'm.room.power_levels': textForPowerEvent,
'm.room.pinned_events': textForPinnedEvent,
'm.room.server_acl': textForServerACLEvent,
'm.room.tombstone': textForTombstoneEvent,
'm.room.join_rules': textForJoinRulesEvent,
'm.room.guest_access': textForGuestAccessEvent,
[EventType.RoomCanonicalAlias]: textForCanonicalAliasEvent,
[EventType.RoomName]: textForRoomNameEvent,
[EventType.RoomTopic]: textForTopicEvent,
[EventType.RoomMember]: textForMemberEvent,
[EventType.RoomAvatar]: textForRoomAvatarEvent,
[EventType.RoomThirdPartyInvite]: textForThreePidInviteEvent,
[EventType.RoomHistoryVisibility]: textForHistoryVisibilityEvent,
[EventType.RoomPowerLevels]: textForPowerEvent,
[EventType.RoomPinnedEvents]: textForPinnedEvent,
[EventType.RoomServerAcl]: textForServerACLEvent,
[EventType.RoomTombstone]: textForTombstoneEvent,
[EventType.RoomJoinRules]: textForJoinRulesEvent,
[EventType.RoomGuestAccess]: textForGuestAccessEvent,
'm.room.related_groups': textForRelatedGroupsEvent,
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)

View file

@ -24,6 +24,7 @@ import React, {
useReducer,
Reducer,
Dispatch,
RefObject,
} from "react";
import { Key } from "../Keyboard";
@ -63,7 +64,7 @@ const RovingTabIndexContext = createContext<IContext>({
});
RovingTabIndexContext.displayName = "RovingTabIndexContext";
enum Type {
export enum Type {
Register = "REGISTER",
Unregister = "UNREGISTER",
SetFocus = "SET_FOCUS",
@ -76,73 +77,67 @@ interface IAction {
};
}
const reducer = (state: IState, action: IAction) => {
export const reducer = (state: IState, action: IAction) => {
switch (action.type) {
case Type.Register: {
if (state.refs.length === 0) {
let left = 0;
let right = state.refs.length - 1;
let index = state.refs.length; // by default append to the end
// do a binary search to find the right slot
while (left <= right) {
index = Math.floor((left + right) / 2);
const ref = state.refs[index];
if (ref === action.payload.ref) {
return state; // already in refs, this should not happen
}
if (action.payload.ref.current.compareDocumentPosition(ref.current) & DOCUMENT_POSITION_PRECEDING) {
left = ++index;
} else {
right = index - 1;
}
}
if (!state.activeRef) {
// Our list of refs was empty, set activeRef to this first item
return {
...state,
activeRef: action.payload.ref,
refs: [action.payload.ref],
};
}
if (state.refs.includes(action.payload.ref)) {
return state; // already in refs, this should not happen
}
// find the index of the first ref which is not preceding this one in DOM order
let newIndex = state.refs.findIndex(ref => {
return ref.current.compareDocumentPosition(action.payload.ref.current) & DOCUMENT_POSITION_PRECEDING;
});
if (newIndex < 0) {
newIndex = state.refs.length; // append to the end
state.activeRef = action.payload.ref;
}
// update the refs list
return {
...state,
refs: [
...state.refs.slice(0, newIndex),
action.payload.ref,
...state.refs.slice(newIndex),
],
};
if (index < state.refs.length) {
state.refs.splice(index, 0, action.payload.ref);
} else {
state.refs.push(action.payload.ref);
}
return { ...state };
}
case Type.Unregister: {
// filter out the ref which we are removing
const refs = state.refs.filter(r => r !== action.payload.ref);
if (refs.length === state.refs.length) {
case Type.Unregister: {
const oldIndex = state.refs.findIndex(r => r === action.payload.ref);
if (oldIndex === -1) {
return state; // already removed, this should not happen
}
if (state.activeRef === action.payload.ref) {
if (state.refs.splice(oldIndex, 1)[0] === state.activeRef) {
// we just removed the active ref, need to replace it
// pick the ref which is now in the index the old ref was in
const oldIndex = state.refs.findIndex(r => r === action.payload.ref);
return {
...state,
activeRef: oldIndex >= refs.length ? refs[refs.length - 1] : refs[oldIndex],
refs,
};
const len = state.refs.length;
state.activeRef = oldIndex >= len ? state.refs[len - 1] : state.refs[oldIndex];
}
// update the refs list
return {
...state,
refs,
};
return { ...state };
}
case Type.SetFocus: {
// update active ref
return {
...state,
activeRef: action.payload.ref,
};
state.activeRef = action.payload.ref;
return { ...state };
}
default:
return state;
}
@ -151,13 +146,40 @@ const reducer = (state: IState, action: IAction) => {
interface IProps {
handleHomeEnd?: boolean;
handleUpDown?: boolean;
handleLeftRight?: boolean;
children(renderProps: {
onKeyDownHandler(ev: React.KeyboardEvent);
});
onKeyDown?(ev: React.KeyboardEvent, state: IState);
}
export const RovingTabIndexProvider: React.FC<IProps> = ({ children, handleHomeEnd, handleUpDown, onKeyDown }) => {
export const findSiblingElement = (
refs: RefObject<HTMLElement>[],
startIndex: number,
backwards = false,
): RefObject<HTMLElement> => {
if (backwards) {
for (let i = startIndex; i < refs.length && i >= 0; i--) {
if (refs[i].current.offsetParent !== null) {
return refs[i];
}
}
} else {
for (let i = startIndex; i < refs.length && i >= 0; i++) {
if (refs[i].current.offsetParent !== null) {
return refs[i];
}
}
}
};
export const RovingTabIndexProvider: React.FC<IProps> = ({
children,
handleHomeEnd,
handleUpDown,
handleLeftRight,
onKeyDown,
}) => {
const [state, dispatch] = useReducer<Reducer<IState, IAction>>(reducer, {
activeRef: null,
refs: [],
@ -166,6 +188,13 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({ children, handleHomeE
const context = useMemo<IContext>(() => ({ state, dispatch }), [state]);
const onKeyDownHandler = useCallback((ev) => {
if (onKeyDown) {
onKeyDown(ev, context.state);
if (ev.defaultPrevented) {
return;
}
}
let handled = false;
// Don't interfere with input default keydown behaviour
if (ev.target.tagName !== "INPUT" && ev.target.tagName !== "TEXTAREA") {
@ -174,43 +203,37 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({ children, handleHomeE
case Key.HOME:
if (handleHomeEnd) {
handled = true;
// move focus to first item
if (context.state.refs.length > 0) {
context.state.refs[0].current.focus();
}
// move focus to first (visible) item
findSiblingElement(context.state.refs, 0)?.current?.focus();
}
break;
case Key.END:
if (handleHomeEnd) {
handled = true;
// move focus to last item
if (context.state.refs.length > 0) {
context.state.refs[context.state.refs.length - 1].current.focus();
}
// move focus to last (visible) item
findSiblingElement(context.state.refs, context.state.refs.length - 1, true)?.current?.focus();
}
break;
case Key.ARROW_UP:
if (handleUpDown) {
case Key.ARROW_RIGHT:
if ((ev.key === Key.ARROW_UP && handleUpDown) || (ev.key === Key.ARROW_RIGHT && handleLeftRight)) {
handled = true;
if (context.state.refs.length > 0) {
const idx = context.state.refs.indexOf(context.state.activeRef);
if (idx > 0) {
context.state.refs[idx - 1].current.focus();
}
findSiblingElement(context.state.refs, idx - 1)?.current?.focus();
}
}
break;
case Key.ARROW_DOWN:
if (handleUpDown) {
case Key.ARROW_LEFT:
if ((ev.key === Key.ARROW_DOWN && handleUpDown) || (ev.key === Key.ARROW_LEFT && handleLeftRight)) {
handled = true;
if (context.state.refs.length > 0) {
const idx = context.state.refs.indexOf(context.state.activeRef);
if (idx < context.state.refs.length - 1) {
context.state.refs[idx + 1].current.focus();
}
findSiblingElement(context.state.refs, idx + 1, true)?.current?.focus();
}
}
break;
@ -220,10 +243,8 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({ children, handleHomeE
if (handled) {
ev.preventDefault();
ev.stopPropagation();
} else if (onKeyDown) {
return onKeyDown(ev, context.state);
}
}, [context.state, onKeyDown, handleHomeEnd, handleUpDown]);
}, [context.state, onKeyDown, handleHomeEnd, handleUpDown, handleLeftRight]);
return <RovingTabIndexContext.Provider value={context}>
{ children({ onKeyDownHandler }) }

View file

@ -16,7 +16,7 @@ limitations under the License.
import React from "react";
import { IState, RovingTabIndexProvider } from "./RovingTabIndex";
import { RovingTabIndexProvider } from "./RovingTabIndex";
import { Key } from "../Keyboard";
interface IProps extends Omit<React.HTMLProps<HTMLDivElement>, "onKeyDown"> {
@ -26,7 +26,7 @@ interface IProps extends Omit<React.HTMLProps<HTMLDivElement>, "onKeyDown"> {
// https://www.w3.org/TR/wai-aria-practices-1.1/#toolbar
// All buttons passed in children must use RovingTabIndex to set `onFocus`, `isActive`, `ref`
const Toolbar: React.FC<IProps> = ({ children, ...props }) => {
const onKeyDown = (ev: React.KeyboardEvent, state: IState) => {
const onKeyDown = (ev: React.KeyboardEvent) => {
const target = ev.target as HTMLElement;
// Don't interfere with input default keydown behaviour
if (target.tagName === "INPUT") return;
@ -42,15 +42,6 @@ const Toolbar: React.FC<IProps> = ({ children, ...props }) => {
}
break;
case Key.ARROW_LEFT:
case Key.ARROW_RIGHT:
if (state.refs.length > 0) {
const i = state.refs.findIndex(r => r === state.activeRef);
const delta = ev.key === Key.ARROW_RIGHT ? 1 : -1;
state.refs.slice((i + delta) % state.refs.length)[0].current.focus();
}
break;
default:
handled = false;
}

View file

@ -17,56 +17,70 @@ limitations under the License.
import React, { createRef } from 'react';
import FileSaver from 'file-saver';
import * as sdk from '../../../../index';
import { MatrixClientPeg } from '../../../../MatrixClientPeg';
import PropTypes from 'prop-types';
import { _t, _td } from '../../../../languageHandler';
import { accessSecretStorage } from '../../../../SecurityManager';
import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
import { copyNode } from "../../../../utils/strings";
import PassphraseField from "../../../../components/views/auth/PassphraseField";
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
import Field from "../../../../components/views/elements/Field";
import Spinner from "../../../../components/views/elements/Spinner";
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import DialogButtons from "../../../../components/views/elements/DialogButtons";
import { IValidationResult } from "../../../../components/views/elements/Validation";
import { IPreparedKeyBackupVersion } from "matrix-js-sdk/src/crypto/backup";
import { logger } from "matrix-js-sdk/src/logger";
const PHASE_PASSPHRASE = 0;
const PHASE_PASSPHRASE_CONFIRM = 1;
const PHASE_SHOWKEY = 2;
const PHASE_KEEPITSAFE = 3;
const PHASE_BACKINGUP = 4;
const PHASE_DONE = 5;
const PHASE_OPTOUT_CONFIRM = 6;
enum Phase {
Passphrase = "passphrase",
PassphraseConfirm = "passphrase_confirm",
ShowKey = "show_key",
KeepItSafe = "keep_it_safe",
BackingUp = "backing_up",
Done = "done",
OptOutConfirm = "opt_out_confirm",
}
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
interface IProps extends IDialogProps {}
interface IState {
secureSecretStorage: boolean;
phase: Phase;
passPhrase: string;
passPhraseValid: boolean;
passPhraseConfirm: string;
copied: boolean;
downloaded: boolean;
error?: string;
}
/*
* Walks the user through the process of creating an e2e key backup
* on the server.
*/
export default class CreateKeyBackupDialog extends React.PureComponent {
static propTypes = {
onFinished: PropTypes.func.isRequired,
}
export default class CreateKeyBackupDialog extends React.PureComponent<IProps, IState> {
private keyBackupInfo: Pick<IPreparedKeyBackupVersion, "recovery_key" | "algorithm" | "auth_data">;
private recoveryKeyNode = createRef<HTMLElement>();
private passphraseField = createRef<Field>();
constructor(props) {
constructor(props: IProps) {
super(props);
this._recoveryKeyNode = null;
this._keyBackupInfo = null;
this.state = {
secureSecretStorage: null,
phase: PHASE_PASSPHRASE,
phase: Phase.Passphrase,
passPhrase: '',
passPhraseValid: false,
passPhraseConfirm: '',
copied: false,
downloaded: false,
};
this._passphraseField = createRef();
}
async componentDidMount() {
public async componentDidMount(): Promise<void> {
const cli = MatrixClientPeg.get();
const secureSecretStorage = await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
this.setState({ secureSecretStorage });
@ -74,41 +88,37 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
// If we're using secret storage, skip ahead to the backing up step, as
// `accessSecretStorage` will handle passphrases as needed.
if (secureSecretStorage) {
this.setState({ phase: PHASE_BACKINGUP });
this._createBackup();
this.setState({ phase: Phase.BackingUp });
this.createBackup();
}
}
_collectRecoveryKeyNode = (n) => {
this._recoveryKeyNode = n;
}
_onCopyClick = () => {
const successful = copyNode(this._recoveryKeyNode);
private onCopyClick = (): void => {
const successful = copyNode(this.recoveryKeyNode.current);
if (successful) {
this.setState({
copied: true,
phase: PHASE_KEEPITSAFE,
phase: Phase.KeepItSafe,
});
}
}
};
_onDownloadClick = () => {
const blob = new Blob([this._keyBackupInfo.recovery_key], {
private onDownloadClick = (): void => {
const blob = new Blob([this.keyBackupInfo.recovery_key], {
type: 'text/plain;charset=us-ascii',
});
FileSaver.saveAs(blob, 'security-key.txt');
this.setState({
downloaded: true,
phase: PHASE_KEEPITSAFE,
phase: Phase.KeepItSafe,
});
}
};
_createBackup = async () => {
private createBackup = async (): Promise<void> => {
const { secureSecretStorage } = this.state;
this.setState({
phase: PHASE_BACKINGUP,
phase: Phase.BackingUp,
error: null,
});
let info;
@ -123,12 +133,12 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
});
} else {
info = await MatrixClientPeg.get().createKeyBackupVersion(
this._keyBackupInfo,
this.keyBackupInfo,
);
}
await MatrixClientPeg.get().scheduleAllGroupSessionsForBackup();
this.setState({
phase: PHASE_DONE,
phase: Phase.Done,
});
} catch (e) {
logger.error("Error creating key backup", e);
@ -143,97 +153,91 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
error: e,
});
}
}
};
_onCancel = () => {
private onCancel = (): void => {
this.props.onFinished(false);
}
};
_onDone = () => {
private onDone = (): void => {
this.props.onFinished(true);
}
};
_onOptOutClick = () => {
this.setState({ phase: PHASE_OPTOUT_CONFIRM });
}
private onSetUpClick = (): void => {
this.setState({ phase: Phase.Passphrase });
};
_onSetUpClick = () => {
this.setState({ phase: PHASE_PASSPHRASE });
}
_onSkipPassPhraseClick = async () => {
this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion();
private onSkipPassPhraseClick = async (): Promise<void> => {
this.keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion();
this.setState({
copied: false,
downloaded: false,
phase: PHASE_SHOWKEY,
phase: Phase.ShowKey,
});
}
};
_onPassPhraseNextClick = async (e) => {
private onPassPhraseNextClick = async (e: React.FormEvent): Promise<void> => {
e.preventDefault();
if (!this._passphraseField.current) return; // unmounting
if (!this.passphraseField.current) return; // unmounting
await this._passphraseField.current.validate({ allowEmpty: false });
if (!this._passphraseField.current.state.valid) {
this._passphraseField.current.focus();
this._passphraseField.current.validate({ allowEmpty: false, focused: true });
await this.passphraseField.current.validate({ allowEmpty: false });
if (!this.passphraseField.current.state.valid) {
this.passphraseField.current.focus();
this.passphraseField.current.validate({ allowEmpty: false, focused: true });
return;
}
this.setState({ phase: PHASE_PASSPHRASE_CONFIRM });
this.setState({ phase: Phase.PassphraseConfirm });
};
_onPassPhraseConfirmNextClick = async (e) => {
private onPassPhraseConfirmNextClick = async (e: React.FormEvent): Promise<void> => {
e.preventDefault();
if (this.state.passPhrase !== this.state.passPhraseConfirm) return;
this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion(this.state.passPhrase);
this.keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion(this.state.passPhrase);
this.setState({
copied: false,
downloaded: false,
phase: PHASE_SHOWKEY,
phase: Phase.ShowKey,
});
};
_onSetAgainClick = () => {
private onSetAgainClick = (): void => {
this.setState({
passPhrase: '',
passPhraseValid: false,
passPhraseConfirm: '',
phase: PHASE_PASSPHRASE,
phase: Phase.Passphrase,
});
}
};
_onKeepItSafeBackClick = () => {
private onKeepItSafeBackClick = (): void => {
this.setState({
phase: PHASE_SHOWKEY,
phase: Phase.ShowKey,
});
}
};
_onPassPhraseValidate = (result) => {
private onPassPhraseValidate = (result: IValidationResult): void => {
this.setState({
passPhraseValid: result.valid,
});
};
_onPassPhraseChange = (e) => {
private onPassPhraseChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({
passPhrase: e.target.value,
});
}
};
_onPassPhraseConfirmChange = (e) => {
private onPassPhraseConfirmChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({
passPhraseConfirm: e.target.value,
});
}
};
_renderPhasePassPhrase() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <form onSubmit={this._onPassPhraseNextClick}>
private renderPhasePassPhrase(): JSX.Element {
return <form onSubmit={this.onPassPhraseNextClick}>
<p>{ _t(
"<b>Warning</b>: You should only set up key backup from a trusted computer.", {},
{ b: sub => <b>{ sub }</b> },
@ -248,11 +252,11 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
<div className="mx_CreateKeyBackupDialog_passPhraseContainer">
<PassphraseField
className="mx_CreateKeyBackupDialog_passPhraseInput"
onChange={this._onPassPhraseChange}
onChange={this.onPassPhraseChange}
minScore={PASSWORD_MIN_SCORE}
value={this.state.passPhrase}
onValidate={this._onPassPhraseValidate}
fieldRef={this._passphraseField}
onValidate={this.onPassPhraseValidate}
fieldRef={this.passphraseField}
autoFocus={true}
label={_td("Enter a Security Phrase")}
labelEnterPassword={_td("Enter a Security Phrase")}
@ -264,23 +268,21 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
<DialogButtons
primaryButton={_t('Next')}
onPrimaryButtonClick={this._onPassPhraseNextClick}
onPrimaryButtonClick={this.onPassPhraseNextClick}
hasCancel={false}
disabled={!this.state.passPhraseValid}
/>
<details>
<summary>{ _t("Advanced") }</summary>
<AccessibleButton kind='primary' onClick={this._onSkipPassPhraseClick}>
<AccessibleButton kind='primary' onClick={this.onSkipPassPhraseClick}>
{ _t("Set up with a Security Key") }
</AccessibleButton>
</details>
</form>;
}
_renderPhasePassPhraseConfirm() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
private renderPhasePassPhraseConfirm(): JSX.Element {
let matchText;
let changeText;
if (this.state.passPhraseConfirm === this.state.passPhrase) {
@ -303,14 +305,13 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
passPhraseMatch = <div className="mx_CreateKeyBackupDialog_passPhraseMatch">
<div>{ matchText }</div>
<div>
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
<AccessibleButton element="span" className="mx_linkButton" onClick={this.onSetAgainClick}>
{ changeText }
</AccessibleButton>
</div>
</div>;
}
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <form onSubmit={this._onPassPhraseConfirmNextClick}>
return <form onSubmit={this.onPassPhraseConfirmNextClick}>
<p>{ _t(
"Enter your Security Phrase a second time to confirm it.",
) }</p>
@ -318,7 +319,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
<div className="mx_CreateKeyBackupDialog_passPhraseContainer">
<div>
<input type="password"
onChange={this._onPassPhraseConfirmChange}
onChange={this.onPassPhraseConfirmChange}
value={this.state.passPhraseConfirm}
className="mx_CreateKeyBackupDialog_passPhraseInput"
placeholder={_t("Repeat your Security Phrase...")}
@ -330,14 +331,14 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
</div>
<DialogButtons
primaryButton={_t('Next')}
onPrimaryButtonClick={this._onPassPhraseConfirmNextClick}
onPrimaryButtonClick={this.onPassPhraseConfirmNextClick}
hasCancel={false}
disabled={this.state.passPhrase !== this.state.passPhraseConfirm}
/>
</form>;
}
_renderPhaseShowKey() {
private renderPhaseShowKey(): JSX.Element {
return <div>
<p>{ _t(
"Your Security Key is a safety net - you can use it to restore " +
@ -352,13 +353,13 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
</div>
<div className="mx_CreateKeyBackupDialog_recoveryKeyContainer">
<div className="mx_CreateKeyBackupDialog_recoveryKey">
<code ref={this._collectRecoveryKeyNode}>{ this._keyBackupInfo.recovery_key }</code>
<code ref={this.recoveryKeyNode}>{ this.keyBackupInfo.recovery_key }</code>
</div>
<div className="mx_CreateKeyBackupDialog_recoveryKeyButtons">
<button className="mx_Dialog_primary" onClick={this._onCopyClick}>
<button className="mx_Dialog_primary" onClick={this.onCopyClick}>
{ _t("Copy") }
</button>
<button className="mx_Dialog_primary" onClick={this._onDownloadClick}>
<button className="mx_Dialog_primary" onClick={this.onDownloadClick}>
{ _t("Download") }
</button>
</div>
@ -367,7 +368,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
</div>;
}
_renderPhaseKeepItSafe() {
private renderPhaseKeepItSafe(): JSX.Element {
let introText;
if (this.state.copied) {
introText = _t(
@ -380,7 +381,6 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
{}, { b: s => <b>{ s }</b> },
);
}
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
{ introText }
<ul>
@ -389,107 +389,101 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
<li>{ _t("<b>Copy it</b> to your personal cloud storage", {}, { b: s => <b>{ s }</b> }) }</li>
</ul>
<DialogButtons primaryButton={_t("Continue")}
onPrimaryButtonClick={this._createBackup}
onPrimaryButtonClick={this.createBackup}
hasCancel={false}>
<button onClick={this._onKeepItSafeBackClick}>{ _t("Back") }</button>
<button onClick={this.onKeepItSafeBackClick}>{ _t("Back") }</button>
</DialogButtons>
</div>;
}
_renderBusyPhase(text) {
const Spinner = sdk.getComponent('views.elements.Spinner');
private renderBusyPhase(): JSX.Element {
return <div>
<Spinner />
</div>;
}
_renderPhaseDone() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
private renderPhaseDone(): JSX.Element {
return <div>
<p>{ _t(
"Your keys are being backed up (the first backup could take a few minutes).",
) }</p>
<DialogButtons primaryButton={_t('OK')}
onPrimaryButtonClick={this._onDone}
onPrimaryButtonClick={this.onDone}
hasCancel={false}
/>
</div>;
}
_renderPhaseOptOutConfirm() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
private renderPhaseOptOutConfirm(): JSX.Element {
return <div>
{ _t(
"Without setting up Secure Message Recovery, you won't be able to restore your " +
"encrypted message history if you log out or use another session.",
) }
<DialogButtons primaryButton={_t('Set up Secure Message Recovery')}
onPrimaryButtonClick={this._onSetUpClick}
onPrimaryButtonClick={this.onSetUpClick}
hasCancel={false}
>
<button onClick={this._onCancel}>I understand, continue without</button>
<button onClick={this.onCancel}>I understand, continue without</button>
</DialogButtons>
</div>;
}
_titleForPhase(phase) {
private titleForPhase(phase: Phase): string {
switch (phase) {
case PHASE_PASSPHRASE:
case Phase.Passphrase:
return _t('Secure your backup with a Security Phrase');
case PHASE_PASSPHRASE_CONFIRM:
case Phase.PassphraseConfirm:
return _t('Confirm your Security Phrase');
case PHASE_OPTOUT_CONFIRM:
case Phase.OptOutConfirm:
return _t('Warning!');
case PHASE_SHOWKEY:
case PHASE_KEEPITSAFE:
case Phase.ShowKey:
case Phase.KeepItSafe:
return _t('Make a copy of your Security Key');
case PHASE_BACKINGUP:
case Phase.BackingUp:
return _t('Starting backup...');
case PHASE_DONE:
case Phase.Done:
return _t('Success!');
default:
return _t("Create key backup");
}
}
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
public render(): JSX.Element {
let content;
if (this.state.error) {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
content = <div>
<p>{ _t("Unable to create key backup") }</p>
<div className="mx_Dialog_buttons">
<DialogButtons primaryButton={_t('Retry')}
onPrimaryButtonClick={this._createBackup}
onPrimaryButtonClick={this.createBackup}
hasCancel={true}
onCancel={this._onCancel}
onCancel={this.onCancel}
/>
</div>
</div>;
} else {
switch (this.state.phase) {
case PHASE_PASSPHRASE:
content = this._renderPhasePassPhrase();
case Phase.Passphrase:
content = this.renderPhasePassPhrase();
break;
case PHASE_PASSPHRASE_CONFIRM:
content = this._renderPhasePassPhraseConfirm();
case Phase.PassphraseConfirm:
content = this.renderPhasePassPhraseConfirm();
break;
case PHASE_SHOWKEY:
content = this._renderPhaseShowKey();
case Phase.ShowKey:
content = this.renderPhaseShowKey();
break;
case PHASE_KEEPITSAFE:
content = this._renderPhaseKeepItSafe();
case Phase.KeepItSafe:
content = this.renderPhaseKeepItSafe();
break;
case PHASE_BACKINGUP:
content = this._renderBusyPhase();
case Phase.BackingUp:
content = this.renderBusyPhase();
break;
case PHASE_DONE:
content = this._renderPhaseDone();
case Phase.Done:
content = this.renderPhaseDone();
break;
case PHASE_OPTOUT_CONFIRM:
content = this._renderPhaseOptOutConfirm();
case Phase.OptOutConfirm:
content = this.renderPhaseOptOutConfirm();
break;
}
}
@ -497,8 +491,8 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
return (
<BaseDialog className='mx_CreateKeyBackupDialog'
onFinished={this.props.onFinished}
title={this._titleForPhase(this.state.phase)}
hasCancel={[PHASE_PASSPHRASE, PHASE_DONE].includes(this.state.phase)}
title={this.titleForPhase(this.state.phase)}
hasCancel={[Phase.Passphrase, Phase.Done].includes(this.state.phase)}
>
<div>
{ content }

View file

@ -16,8 +16,6 @@ limitations under the License.
*/
import React, { createRef } from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../../index';
import { MatrixClientPeg } from '../../../../MatrixClientPeg';
import FileSaver from 'file-saver';
import { _t, _td } from '../../../../languageHandler';
@ -31,52 +29,105 @@ import AccessibleButton from "../../../../components/views/elements/AccessibleBu
import DialogButtons from "../../../../components/views/elements/DialogButtons";
import InlineSpinner from "../../../../components/views/elements/InlineSpinner";
import RestoreKeyBackupDialog from "../../../../components/views/dialogs/security/RestoreKeyBackupDialog";
import { getSecureBackupSetupMethods, isSecureBackupRequired } from '../../../../utils/WellKnownUtils';
import {
getSecureBackupSetupMethods,
isSecureBackupRequired,
SecureBackupSetupMethod,
} from '../../../../utils/WellKnownUtils';
import SecurityCustomisations from "../../../../customisations/Security";
import { logger } from "matrix-js-sdk/src/logger";
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
import Field from "../../../../components/views/elements/Field";
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import Spinner from "../../../../components/views/elements/Spinner";
import { TrustInfo } from "matrix-js-sdk/src/crypto/backup";
import { CrossSigningKeys } from "matrix-js-sdk";
import InteractiveAuthDialog from "../../../../components/views/dialogs/InteractiveAuthDialog";
import { IRecoveryKey } from "matrix-js-sdk/src/crypto/api";
import { IValidationResult } from "../../../../components/views/elements/Validation";
const PHASE_LOADING = 0;
const PHASE_LOADERROR = 1;
const PHASE_CHOOSE_KEY_PASSPHRASE = 2;
const PHASE_MIGRATE = 3;
const PHASE_PASSPHRASE = 4;
const PHASE_PASSPHRASE_CONFIRM = 5;
const PHASE_SHOWKEY = 6;
const PHASE_STORING = 8;
const PHASE_CONFIRM_SKIP = 10;
// I made a mistake while converting this and it has to be fixed!
enum Phase {
Loading = "loading",
LoadError = "load_error",
ChooseKeyPassphrase = "choose_key_passphrase",
Migrate = "migrate",
Passphrase = "passphrase",
PassphraseConfirm = "passphrase_confirm",
ShowKey = "show_key",
Storing = "storing",
ConfirmSkip = "confirm_skip",
}
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
// these end up as strings from being values in the radio buttons, so just use strings
const CREATE_STORAGE_OPTION_KEY = 'key';
const CREATE_STORAGE_OPTION_PASSPHRASE = 'passphrase';
interface IProps extends IDialogProps {
hasCancel: boolean;
accountPassword: string;
forceReset: boolean;
}
interface IState {
phase: Phase;
passPhrase: string;
passPhraseValid: boolean;
passPhraseConfirm: string;
copied: boolean;
downloaded: boolean;
setPassphrase: boolean;
backupInfo: IKeyBackupInfo;
backupSigStatus: TrustInfo;
// does the server offer a UI auth flow with just m.login.password
// for /keys/device_signing/upload?
canUploadKeysWithPasswordOnly: boolean;
accountPassword: string;
accountPasswordCorrect: boolean;
canSkip: boolean;
passPhraseKeySelected: string;
error?: string;
}
/*
* Walks the user through the process of creating a passphrase to guard Secure
* Secret Storage in account data.
*/
export default class CreateSecretStorageDialog extends React.PureComponent {
static propTypes = {
hasCancel: PropTypes.bool,
accountPassword: PropTypes.string,
forceReset: PropTypes.bool,
};
static defaultProps = {
export default class CreateSecretStorageDialog extends React.PureComponent<IProps, IState> {
public static defaultProps: Partial<IProps> = {
hasCancel: true,
forceReset: false,
};
private recoveryKey: IRecoveryKey;
private backupKey: Uint8Array;
private recoveryKeyNode = createRef<HTMLElement>();
private passphraseField = createRef<Field>();
constructor(props) {
constructor(props: IProps) {
super(props);
this._recoveryKey = null;
this._recoveryKeyNode = null;
this._backupKey = null;
let passPhraseKeySelected;
const setupMethods = getSecureBackupSetupMethods();
if (setupMethods.includes(SecureBackupSetupMethod.Key)) {
passPhraseKeySelected = SecureBackupSetupMethod.Key;
} else {
passPhraseKeySelected = SecureBackupSetupMethod.Passphrase;
}
const accountPassword = props.accountPassword || "";
let canUploadKeysWithPasswordOnly = null;
if (accountPassword) {
// If we have an account password in memory, let's simplify and
// assume it means password auth is also supported for device
// signing key upload as well. This avoids hitting the server to
// test auth flows, which may be slow under high load.
canUploadKeysWithPasswordOnly = true;
} else {
this.queryKeyUploadAuth();
}
this.state = {
phase: PHASE_LOADING,
phase: Phase.Loading,
passPhrase: '',
passPhraseValid: false,
passPhraseConfirm: '',
@ -87,55 +138,37 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
backupSigStatus: null,
// does the server offer a UI auth flow with just m.login.password
// for /keys/device_signing/upload?
canUploadKeysWithPasswordOnly: null,
accountPassword: props.accountPassword || "",
accountPasswordCorrect: null,
canSkip: !isSecureBackupRequired(),
canUploadKeysWithPasswordOnly,
passPhraseKeySelected,
accountPassword,
};
const setupMethods = getSecureBackupSetupMethods();
if (setupMethods.includes("key")) {
this.state.passPhraseKeySelected = CREATE_STORAGE_OPTION_KEY;
} else {
this.state.passPhraseKeySelected = CREATE_STORAGE_OPTION_PASSPHRASE;
}
MatrixClientPeg.get().on('crypto.keyBackupStatus', this.onKeyBackupStatusChange);
this._passphraseField = createRef();
MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatusChange);
if (this.state.accountPassword) {
// If we have an account password in memory, let's simplify and
// assume it means password auth is also supported for device
// signing key upload as well. This avoids hitting the server to
// test auth flows, which may be slow under high load.
this.state.canUploadKeysWithPasswordOnly = true;
} else {
this._queryKeyUploadAuth();
}
this._getInitialPhase();
this.getInitialPhase();
}
componentWillUnmount() {
MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatusChange);
public componentWillUnmount(): void {
MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this.onKeyBackupStatusChange);
}
_getInitialPhase() {
private getInitialPhase(): void {
const keyFromCustomisations = SecurityCustomisations.createSecretStorageKey?.();
if (keyFromCustomisations) {
logger.log("Created key via customisations, jumping to bootstrap step");
this._recoveryKey = {
this.recoveryKey = {
privateKey: keyFromCustomisations,
};
this._bootstrapSecretStorage();
this.bootstrapSecretStorage();
return;
}
this._fetchBackupInfo();
this.fetchBackupInfo();
}
async _fetchBackupInfo() {
private async fetchBackupInfo(): Promise<{ backupInfo: IKeyBackupInfo, backupSigStatus: TrustInfo }> {
try {
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
const backupSigStatus = (
@ -144,7 +177,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
);
const { forceReset } = this.props;
const phase = (backupInfo && !forceReset) ? PHASE_MIGRATE : PHASE_CHOOSE_KEY_PASSPHRASE;
const phase = (backupInfo && !forceReset) ? Phase.Migrate : Phase.ChooseKeyPassphrase;
this.setState({
phase,
@ -157,13 +190,13 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
backupSigStatus,
};
} catch (e) {
this.setState({ phase: PHASE_LOADERROR });
this.setState({ phase: Phase.LoadError });
}
}
async _queryKeyUploadAuth() {
private async queryKeyUploadAuth(): Promise<void> {
try {
await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {});
await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {} as CrossSigningKeys);
// We should never get here: the server should always require
// UI auth to upload device signing keys. If we do, we upload
// no keys which would be a no-op.
@ -182,59 +215,55 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
}
}
_onKeyBackupStatusChange = () => {
if (this.state.phase === PHASE_MIGRATE) this._fetchBackupInfo();
}
private onKeyBackupStatusChange = (): void => {
if (this.state.phase === Phase.Migrate) this.fetchBackupInfo();
};
_onKeyPassphraseChange = e => {
private onKeyPassphraseChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({
passPhraseKeySelected: e.target.value,
});
}
};
_collectRecoveryKeyNode = (n) => {
this._recoveryKeyNode = n;
}
_onChooseKeyPassphraseFormSubmit = async () => {
if (this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_KEY) {
this._recoveryKey =
private onChooseKeyPassphraseFormSubmit = async (): Promise<void> => {
if (this.state.passPhraseKeySelected === SecureBackupSetupMethod.Key) {
this.recoveryKey =
await MatrixClientPeg.get().createRecoveryKeyFromPassphrase();
this.setState({
copied: false,
downloaded: false,
setPassphrase: false,
phase: PHASE_SHOWKEY,
phase: Phase.ShowKey,
});
} else {
this.setState({
copied: false,
downloaded: false,
phase: PHASE_PASSPHRASE,
phase: Phase.Passphrase,
});
}
}
};
_onMigrateFormSubmit = (e) => {
private onMigrateFormSubmit = (e: React.FormEvent): void => {
e.preventDefault();
if (this.state.backupSigStatus.usable) {
this._bootstrapSecretStorage();
this.bootstrapSecretStorage();
} else {
this._restoreBackup();
this.restoreBackup();
}
}
};
_onCopyClick = () => {
const successful = copyNode(this._recoveryKeyNode);
private onCopyClick = (): void => {
const successful = copyNode(this.recoveryKeyNode.current);
if (successful) {
this.setState({
copied: true,
});
}
}
};
_onDownloadClick = () => {
const blob = new Blob([this._recoveryKey.encodedPrivateKey], {
private onDownloadClick = (): void => {
const blob = new Blob([this.recoveryKey.encodedPrivateKey], {
type: 'text/plain;charset=us-ascii',
});
FileSaver.saveAs(blob, 'security-key.txt');
@ -242,9 +271,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
this.setState({
downloaded: true,
});
}
};
_doBootstrapUIAuth = async (makeRequest) => {
private doBootstrapUIAuth = async (makeRequest: (authData: any) => void): Promise<void> => {
if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) {
await makeRequest({
type: 'm.login.password',
@ -258,8 +287,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
password: this.state.accountPassword,
});
} else {
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
const dialogAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: {
title: _t("Use Single Sign On to continue"),
@ -292,11 +319,11 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
throw new Error("Cross-signing key upload auth canceled");
}
}
}
};
_bootstrapSecretStorage = async () => {
private bootstrapSecretStorage = async (): Promise<void> => {
this.setState({
phase: PHASE_STORING,
phase: Phase.Storing,
error: null,
});
@ -308,7 +335,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
if (forceReset) {
logger.log("Forcing secret storage reset");
await cli.bootstrapSecretStorage({
createSecretStorageKey: async () => this._recoveryKey,
createSecretStorageKey: async () => this.recoveryKey,
setupNewKeyBackup: true,
setupNewSecretStorage: true,
});
@ -321,18 +348,18 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
// keys (and also happen to skip all post-authentication flows at the
// moment via token login)
await cli.bootstrapCrossSigning({
authUploadDeviceSigningKeys: this._doBootstrapUIAuth,
authUploadDeviceSigningKeys: this.doBootstrapUIAuth,
});
await cli.bootstrapSecretStorage({
createSecretStorageKey: async () => this._recoveryKey,
createSecretStorageKey: async () => this.recoveryKey,
keyBackupInfo: this.state.backupInfo,
setupNewKeyBackup: !this.state.backupInfo,
getKeyBackupPassphrase: () => {
getKeyBackupPassphrase: async () => {
// We may already have the backup key if we earlier went
// through the restore backup path, so pass it along
// rather than prompting again.
if (this._backupKey) {
return this._backupKey;
if (this.backupKey) {
return this.backupKey;
}
return promptForBackupPassphrase();
},
@ -344,27 +371,23 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
this.setState({
accountPassword: '',
accountPasswordCorrect: false,
phase: PHASE_MIGRATE,
phase: Phase.Migrate,
});
} else {
this.setState({ error: e });
}
logger.error("Error bootstrapping secret storage", e);
}
}
};
_onCancel = () => {
private onCancel = (): void => {
this.props.onFinished(false);
}
};
_onDone = () => {
this.props.onFinished(true);
}
_restoreBackup = async () => {
private restoreBackup = async (): Promise<void> => {
// It's possible we'll need the backup key later on for bootstrapping,
// so let's stash it here, rather than prompting for it twice.
const keyCallback = k => this._backupKey = k;
const keyCallback = k => this.backupKey = k;
const { finished } = Modal.createTrackedDialog(
'Restore Backup', '', RestoreKeyBackupDialog,
@ -376,122 +399,122 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
);
await finished;
const { backupSigStatus } = await this._fetchBackupInfo();
const { backupSigStatus } = await this.fetchBackupInfo();
if (
backupSigStatus.usable &&
this.state.canUploadKeysWithPasswordOnly &&
this.state.accountPassword
) {
this._bootstrapSecretStorage();
this.bootstrapSecretStorage();
}
}
};
_onLoadRetryClick = () => {
this.setState({ phase: PHASE_LOADING });
this._fetchBackupInfo();
}
private onLoadRetryClick = (): void => {
this.setState({ phase: Phase.Loading });
this.fetchBackupInfo();
};
_onShowKeyContinueClick = () => {
this._bootstrapSecretStorage();
}
private onShowKeyContinueClick = (): void => {
this.bootstrapSecretStorage();
};
_onCancelClick = () => {
this.setState({ phase: PHASE_CONFIRM_SKIP });
}
private onCancelClick = (): void => {
this.setState({ phase: Phase.ConfirmSkip });
};
_onGoBackClick = () => {
this.setState({ phase: PHASE_CHOOSE_KEY_PASSPHRASE });
}
private onGoBackClick = (): void => {
this.setState({ phase: Phase.ChooseKeyPassphrase });
};
_onPassPhraseNextClick = async (e) => {
private onPassPhraseNextClick = async (e: React.FormEvent) => {
e.preventDefault();
if (!this._passphraseField.current) return; // unmounting
if (!this.passphraseField.current) return; // unmounting
await this._passphraseField.current.validate({ allowEmpty: false });
if (!this._passphraseField.current.state.valid) {
this._passphraseField.current.focus();
this._passphraseField.current.validate({ allowEmpty: false, focused: true });
await this.passphraseField.current.validate({ allowEmpty: false });
if (!this.passphraseField.current.state.valid) {
this.passphraseField.current.focus();
this.passphraseField.current.validate({ allowEmpty: false, focused: true });
return;
}
this.setState({ phase: PHASE_PASSPHRASE_CONFIRM });
this.setState({ phase: Phase.PassphraseConfirm });
};
_onPassPhraseConfirmNextClick = async (e) => {
private onPassPhraseConfirmNextClick = async (e: React.FormEvent) => {
e.preventDefault();
if (this.state.passPhrase !== this.state.passPhraseConfirm) return;
this._recoveryKey =
this.recoveryKey =
await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(this.state.passPhrase);
this.setState({
copied: false,
downloaded: false,
setPassphrase: true,
phase: PHASE_SHOWKEY,
phase: Phase.ShowKey,
});
}
};
_onSetAgainClick = () => {
private onSetAgainClick = (): void => {
this.setState({
passPhrase: '',
passPhraseValid: false,
passPhraseConfirm: '',
phase: PHASE_PASSPHRASE,
phase: Phase.Passphrase,
});
}
};
_onPassPhraseValidate = (result) => {
private onPassPhraseValidate = (result: IValidationResult): void => {
this.setState({
passPhraseValid: result.valid,
});
};
_onPassPhraseChange = (e) => {
private onPassPhraseChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({
passPhrase: e.target.value,
});
}
};
_onPassPhraseConfirmChange = (e) => {
private onPassPhraseConfirmChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({
passPhraseConfirm: e.target.value,
});
}
};
_onAccountPasswordChange = (e) => {
private onAccountPasswordChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({
accountPassword: e.target.value,
});
}
};
_renderOptionKey() {
private renderOptionKey(): JSX.Element {
return (
<StyledRadioButton
key={CREATE_STORAGE_OPTION_KEY}
value={CREATE_STORAGE_OPTION_KEY}
key={SecureBackupSetupMethod.Key}
value={SecureBackupSetupMethod.Key}
name="keyPassphrase"
checked={this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_KEY}
onChange={this._onKeyPassphraseChange}
checked={this.state.passPhraseKeySelected === SecureBackupSetupMethod.Key}
onChange={this.onKeyPassphraseChange}
outlined
>
<div className="mx_CreateSecretStorageDialog_optionTitle">
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_secureBackup" />
{ _t("Generate a Security Key") }
</div>
<div>{ _t("Well generate a Security Key for you to store somewhere safe, like a password manager or a safe.") }</div>
<div>{ _t("We'll generate a Security Key for you to store somewhere safe, like a password manager or a safe.") }</div>
</StyledRadioButton>
);
}
_renderOptionPassphrase() {
private renderOptionPassphrase(): JSX.Element {
return (
<StyledRadioButton
key={CREATE_STORAGE_OPTION_PASSPHRASE}
value={CREATE_STORAGE_OPTION_PASSPHRASE}
key={SecureBackupSetupMethod.Passphrase}
value={SecureBackupSetupMethod.Passphrase}
name="keyPassphrase"
checked={this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_PASSPHRASE}
onChange={this._onKeyPassphraseChange}
checked={this.state.passPhraseKeySelected === SecureBackupSetupMethod.Passphrase}
onChange={this.onKeyPassphraseChange}
outlined
>
<div className="mx_CreateSecretStorageDialog_optionTitle">
@ -503,12 +526,14 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
);
}
_renderPhaseChooseKeyPassphrase() {
private renderPhaseChooseKeyPassphrase(): JSX.Element {
const setupMethods = getSecureBackupSetupMethods();
const optionKey = setupMethods.includes("key") ? this._renderOptionKey() : null;
const optionPassphrase = setupMethods.includes("passphrase") ? this._renderOptionPassphrase() : null;
const optionKey = setupMethods.includes(SecureBackupSetupMethod.Key) ? this.renderOptionKey() : null;
const optionPassphrase = setupMethods.includes(SecureBackupSetupMethod.Passphrase)
? this.renderOptionPassphrase()
: null;
return <form onSubmit={this._onChooseKeyPassphraseFormSubmit}>
return <form onSubmit={this.onChooseKeyPassphraseFormSubmit}>
<p className="mx_CreateSecretStorageDialog_centeredBody">{ _t(
"Safeguard against losing access to encrypted messages & data by " +
"backing up encryption keys on your server.",
@ -519,20 +544,19 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
</div>
<DialogButtons
primaryButton={_t("Continue")}
onPrimaryButtonClick={this._onChooseKeyPassphraseFormSubmit}
onCancel={this._onCancelClick}
onPrimaryButtonClick={this.onChooseKeyPassphraseFormSubmit}
onCancel={this.onCancelClick}
hasCancel={this.state.canSkip}
/>
</form>;
}
_renderPhaseMigrate() {
private renderPhaseMigrate(): JSX.Element {
// TODO: This is a temporary screen so people who have the labs flag turned on and
// click the button are aware they're making a change to their account.
// Once we're confident enough in this (and it's supported enough) we can do
// it automatically.
// https://github.com/vector-im/element-web/issues/11696
const Field = sdk.getComponent('views.elements.Field');
let authPrompt;
let nextCaption = _t("Next");
@ -543,7 +567,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
type="password"
label={_t("Password")}
value={this.state.accountPassword}
onChange={this._onAccountPasswordChange}
onChange={this.onAccountPasswordChange}
forceValidity={this.state.accountPasswordCorrect === false ? false : null}
autoFocus={true}
/></div>
@ -559,7 +583,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
</p>;
}
return <form onSubmit={this._onMigrateFormSubmit}>
return <form onSubmit={this.onMigrateFormSubmit}>
<p>{ _t(
"Upgrade this session to allow it to verify other sessions, " +
"granting them access to encrypted messages and marking them " +
@ -568,32 +592,32 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
<div>{ authPrompt }</div>
<DialogButtons
primaryButton={nextCaption}
onPrimaryButtonClick={this._onMigrateFormSubmit}
onPrimaryButtonClick={this.onMigrateFormSubmit}
hasCancel={false}
primaryDisabled={this.state.canUploadKeysWithPasswordOnly && !this.state.accountPassword}
>
<button type="button" className="danger" onClick={this._onCancelClick}>
<button type="button" className="danger" onClick={this.onCancelClick}>
{ _t('Skip') }
</button>
</DialogButtons>
</form>;
}
_renderPhasePassPhrase() {
return <form onSubmit={this._onPassPhraseNextClick}>
private renderPhasePassPhrase(): JSX.Element {
return <form onSubmit={this.onPassPhraseNextClick}>
<p>{ _t(
"Enter a security phrase only you know, as its used to safeguard your data. " +
"To be secure, you shouldnt re-use your account password.",
"Enter a security phrase only you know, as it's used to safeguard your data. " +
"To be secure, you shouldn't re-use your account password.",
) }</p>
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
<PassphraseField
className="mx_CreateSecretStorageDialog_passPhraseField"
onChange={this._onPassPhraseChange}
onChange={this.onPassPhraseChange}
minScore={PASSWORD_MIN_SCORE}
value={this.state.passPhrase}
onValidate={this._onPassPhraseValidate}
fieldRef={this._passphraseField}
onValidate={this.onPassPhraseValidate}
fieldRef={this.passphraseField}
autoFocus={true}
label={_td("Enter a Security Phrase")}
labelEnterPassword={_td("Enter a Security Phrase")}
@ -604,21 +628,19 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
<DialogButtons
primaryButton={_t('Continue')}
onPrimaryButtonClick={this._onPassPhraseNextClick}
onPrimaryButtonClick={this.onPassPhraseNextClick}
hasCancel={false}
disabled={!this.state.passPhraseValid}
>
<button type="button"
onClick={this._onCancelClick}
onClick={this.onCancelClick}
className="danger"
>{ _t("Cancel") }</button>
</DialogButtons>
</form>;
}
_renderPhasePassPhraseConfirm() {
const Field = sdk.getComponent('views.elements.Field');
private renderPhasePassPhraseConfirm(): JSX.Element {
let matchText;
let changeText;
if (this.state.passPhraseConfirm === this.state.passPhrase) {
@ -641,20 +663,20 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
passPhraseMatch = <div>
<div>{ matchText }</div>
<div>
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
<AccessibleButton element="span" className="mx_linkButton" onClick={this.onSetAgainClick}>
{ changeText }
</AccessibleButton>
</div>
</div>;
}
return <form onSubmit={this._onPassPhraseConfirmNextClick}>
return <form onSubmit={this.onPassPhraseConfirmNextClick}>
<p>{ _t(
"Enter your Security Phrase a second time to confirm it.",
) }</p>
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
<Field
type="password"
onChange={this._onPassPhraseConfirmChange}
onChange={this.onPassPhraseConfirmChange}
value={this.state.passPhraseConfirm}
className="mx_CreateSecretStorageDialog_passPhraseField"
label={_t("Confirm your Security Phrase")}
@ -667,24 +689,24 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
</div>
<DialogButtons
primaryButton={_t('Continue')}
onPrimaryButtonClick={this._onPassPhraseConfirmNextClick}
onPrimaryButtonClick={this.onPassPhraseConfirmNextClick}
hasCancel={false}
disabled={this.state.passPhrase !== this.state.passPhraseConfirm}
>
<button type="button"
onClick={this._onCancelClick}
onClick={this.onCancelClick}
className="danger"
>{ _t("Skip") }</button>
</DialogButtons>
</form>;
}
_renderPhaseShowKey() {
private renderPhaseShowKey(): JSX.Element {
let continueButton;
if (this.state.phase === PHASE_SHOWKEY) {
if (this.state.phase === Phase.ShowKey) {
continueButton = <DialogButtons primaryButton={_t("Continue")}
disabled={!this.state.downloaded && !this.state.copied && !this.state.setPassphrase}
onPrimaryButtonClick={this._onShowKeyContinueClick}
onPrimaryButtonClick={this.onShowKeyContinueClick}
hasCancel={false}
/>;
} else {
@ -695,18 +717,18 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
return <div>
<p>{ _t(
"Store your Security Key somewhere safe, like a password manager or a safe, " +
"as its used to safeguard your encrypted data.",
"as it's used to safeguard your encrypted data.",
) }</p>
<div className="mx_CreateSecretStorageDialog_primaryContainer">
<div className="mx_CreateSecretStorageDialog_recoveryKeyContainer">
<div className="mx_CreateSecretStorageDialog_recoveryKey">
<code ref={this._collectRecoveryKeyNode}>{ this._recoveryKey.encodedPrivateKey }</code>
<code ref={this.recoveryKeyNode}>{ this.recoveryKey.encodedPrivateKey }</code>
</div>
<div className="mx_CreateSecretStorageDialog_recoveryKeyButtons">
<AccessibleButton kind='primary'
className="mx_Dialog_primary"
onClick={this._onDownloadClick}
disabled={this.state.phase === PHASE_STORING}
onClick={this.onDownloadClick}
disabled={this.state.phase === Phase.Storing}
>
{ _t("Download") }
</AccessibleButton>
@ -714,8 +736,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
<AccessibleButton
kind='primary'
className="mx_Dialog_primary mx_CreateSecretStorageDialog_recoveryKeyButtons_copyBtn"
onClick={this._onCopyClick}
disabled={this.state.phase === PHASE_STORING}
onClick={this.onCopyClick}
disabled={this.state.phase === Phase.Storing}
>
{ this.state.copied ? _t("Copied!") : _t("Copy") }
</AccessibleButton>
@ -726,27 +748,26 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
</div>;
}
_renderBusyPhase() {
const Spinner = sdk.getComponent('views.elements.Spinner');
private renderBusyPhase(): JSX.Element {
return <div>
<Spinner />
</div>;
}
_renderPhaseLoadError() {
private renderPhaseLoadError(): JSX.Element {
return <div>
<p>{ _t("Unable to query secret storage status") }</p>
<div className="mx_Dialog_buttons">
<DialogButtons primaryButton={_t('Retry')}
onPrimaryButtonClick={this._onLoadRetryClick}
onPrimaryButtonClick={this.onLoadRetryClick}
hasCancel={this.state.canSkip}
onCancel={this._onCancel}
onCancel={this.onCancel}
/>
</div>
</div>;
}
_renderPhaseSkipConfirm() {
private renderPhaseSkipConfirm(): JSX.Element {
return <div>
<p>{ _t(
"If you cancel now, you may lose encrypted messages & data if you lose access to your logins.",
@ -755,98 +776,96 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
"You can also set up Secure Backup & manage your keys in Settings.",
) }</p>
<DialogButtons primaryButton={_t('Go back')}
onPrimaryButtonClick={this._onGoBackClick}
onPrimaryButtonClick={this.onGoBackClick}
hasCancel={false}
>
<button type="button" className="danger" onClick={this._onCancel}>{ _t('Cancel') }</button>
<button type="button" className="danger" onClick={this.onCancel}>{ _t('Cancel') }</button>
</DialogButtons>
</div>;
}
_titleForPhase(phase) {
private titleForPhase(phase: Phase): string {
switch (phase) {
case PHASE_CHOOSE_KEY_PASSPHRASE:
case Phase.ChooseKeyPassphrase:
return _t('Set up Secure Backup');
case PHASE_MIGRATE:
case Phase.Migrate:
return _t('Upgrade your encryption');
case PHASE_PASSPHRASE:
case Phase.Passphrase:
return _t('Set a Security Phrase');
case PHASE_PASSPHRASE_CONFIRM:
case Phase.PassphraseConfirm:
return _t('Confirm Security Phrase');
case PHASE_CONFIRM_SKIP:
case Phase.ConfirmSkip:
return _t('Are you sure?');
case PHASE_SHOWKEY:
case Phase.ShowKey:
return _t('Save your Security Key');
case PHASE_STORING:
case Phase.Storing:
return _t('Setting up keys');
default:
return '';
}
}
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
public render(): JSX.Element {
let content;
if (this.state.error) {
content = <div>
<p>{ _t("Unable to set up secret storage") }</p>
<div className="mx_Dialog_buttons">
<DialogButtons primaryButton={_t('Retry')}
onPrimaryButtonClick={this._bootstrapSecretStorage}
onPrimaryButtonClick={this.bootstrapSecretStorage}
hasCancel={this.state.canSkip}
onCancel={this._onCancel}
onCancel={this.onCancel}
/>
</div>
</div>;
} else {
switch (this.state.phase) {
case PHASE_LOADING:
content = this._renderBusyPhase();
case Phase.Loading:
content = this.renderBusyPhase();
break;
case PHASE_LOADERROR:
content = this._renderPhaseLoadError();
case Phase.LoadError:
content = this.renderPhaseLoadError();
break;
case PHASE_CHOOSE_KEY_PASSPHRASE:
content = this._renderPhaseChooseKeyPassphrase();
case Phase.ChooseKeyPassphrase:
content = this.renderPhaseChooseKeyPassphrase();
break;
case PHASE_MIGRATE:
content = this._renderPhaseMigrate();
case Phase.Migrate:
content = this.renderPhaseMigrate();
break;
case PHASE_PASSPHRASE:
content = this._renderPhasePassPhrase();
case Phase.Passphrase:
content = this.renderPhasePassPhrase();
break;
case PHASE_PASSPHRASE_CONFIRM:
content = this._renderPhasePassPhraseConfirm();
case Phase.PassphraseConfirm:
content = this.renderPhasePassPhraseConfirm();
break;
case PHASE_SHOWKEY:
content = this._renderPhaseShowKey();
case Phase.ShowKey:
content = this.renderPhaseShowKey();
break;
case PHASE_STORING:
content = this._renderBusyPhase();
case Phase.Storing:
content = this.renderBusyPhase();
break;
case PHASE_CONFIRM_SKIP:
content = this._renderPhaseSkipConfirm();
case Phase.ConfirmSkip:
content = this.renderPhaseSkipConfirm();
break;
}
}
let titleClass = null;
switch (this.state.phase) {
case PHASE_PASSPHRASE:
case PHASE_PASSPHRASE_CONFIRM:
case Phase.Passphrase:
case Phase.PassphraseConfirm:
titleClass = [
'mx_CreateSecretStorageDialog_titleWithIcon',
'mx_CreateSecretStorageDialog_securePhraseTitle',
];
break;
case PHASE_SHOWKEY:
case Phase.ShowKey:
titleClass = [
'mx_CreateSecretStorageDialog_titleWithIcon',
'mx_CreateSecretStorageDialog_secureBackupTitle',
];
break;
case PHASE_CHOOSE_KEY_PASSPHRASE:
case Phase.ChooseKeyPassphrase:
titleClass = 'mx_CreateSecretStorageDialog_centeredTitle';
break;
}
@ -854,9 +873,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
return (
<BaseDialog className='mx_CreateSecretStorageDialog'
onFinished={this.props.onFinished}
title={this._titleForPhase(this.state.phase)}
title={this.titleForPhase(this.state.phase)}
titleClass={titleClass}
hasCancel={this.props.hasCancel && [PHASE_PASSPHRASE].includes(this.state.phase)}
hasCancel={this.props.hasCancel && [Phase.Passphrase].includes(this.state.phase)}
fixedWidth={false}
>
<div>

View file

@ -16,47 +16,51 @@ limitations under the License.
import FileSaver from 'file-saver';
import React, { createRef } from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../../languageHandler';
import { MatrixClient } from 'matrix-js-sdk/src/client';
import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryption';
import * as sdk from '../../../../index';
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import { logger } from "matrix-js-sdk/src/logger";
const PHASE_EDIT = 1;
const PHASE_EXPORTING = 2;
enum Phase {
Edit = "edit",
Exporting = "exporting",
}
export default class ExportE2eKeysDialog extends React.Component {
static propTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
onFinished: PropTypes.func.isRequired,
};
interface IProps extends IDialogProps {
matrixClient: MatrixClient;
}
constructor(props) {
interface IState {
phase: Phase;
errStr: string;
}
export default class ExportE2eKeysDialog extends React.Component<IProps, IState> {
private unmounted = false;
private passphrase1 = createRef<HTMLInputElement>();
private passphrase2 = createRef<HTMLInputElement>();
constructor(props: IProps) {
super(props);
this._unmounted = false;
this._passphrase1 = createRef();
this._passphrase2 = createRef();
this.state = {
phase: PHASE_EDIT,
phase: Phase.Edit,
errStr: null,
};
}
componentWillUnmount() {
this._unmounted = true;
public componentWillUnmount(): void {
this.unmounted = true;
}
_onPassphraseFormSubmit = (ev) => {
private onPassphraseFormSubmit = (ev: React.FormEvent): boolean => {
ev.preventDefault();
const passphrase = this._passphrase1.current.value;
if (passphrase !== this._passphrase2.current.value) {
const passphrase = this.passphrase1.current.value;
if (passphrase !== this.passphrase2.current.value) {
this.setState({ errStr: _t('Passphrases must match') });
return false;
}
@ -65,11 +69,11 @@ export default class ExportE2eKeysDialog extends React.Component {
return false;
}
this._startExport(passphrase);
this.startExport(passphrase);
return false;
};
_startExport(passphrase) {
private startExport(passphrase: string): void {
// extra Promise.resolve() to turn synchronous exceptions into
// asynchronous ones.
Promise.resolve().then(() => {
@ -86,39 +90,37 @@ export default class ExportE2eKeysDialog extends React.Component {
this.props.onFinished(true);
}).catch((e) => {
logger.error("Error exporting e2e keys:", e);
if (this._unmounted) {
if (this.unmounted) {
return;
}
const msg = e.friendlyText || _t('Unknown error');
this.setState({
errStr: msg,
phase: PHASE_EDIT,
phase: Phase.Edit,
});
});
this.setState({
errStr: null,
phase: PHASE_EXPORTING,
phase: Phase.Exporting,
});
}
_onCancelClick = (ev) => {
private onCancelClick = (ev: React.MouseEvent): boolean => {
ev.preventDefault();
this.props.onFinished(false);
return false;
};
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const disableForm = (this.state.phase === PHASE_EXPORTING);
public render(): JSX.Element {
const disableForm = (this.state.phase === Phase.Exporting);
return (
<BaseDialog className='mx_exportE2eKeysDialog'
onFinished={this.props.onFinished}
title={_t("Export room keys")}
>
<form onSubmit={this._onPassphraseFormSubmit}>
<form onSubmit={this.onPassphraseFormSubmit}>
<div className="mx_Dialog_content">
<p>
{ _t(
@ -151,10 +153,10 @@ export default class ExportE2eKeysDialog extends React.Component {
</div>
<div className='mx_E2eKeysDialog_inputCell'>
<input
ref={this._passphrase1}
ref={this.passphrase1}
id='passphrase1'
autoFocus={true}
size='64'
size={64}
type='password'
disabled={disableForm}
/>
@ -167,9 +169,9 @@ export default class ExportE2eKeysDialog extends React.Component {
</label>
</div>
<div className='mx_E2eKeysDialog_inputCell'>
<input ref={this._passphrase2}
<input ref={this.passphrase2}
id='passphrase2'
size='64'
size={64}
type='password'
disabled={disableForm}
/>
@ -184,7 +186,7 @@ export default class ExportE2eKeysDialog extends React.Component {
value={_t('Export')}
disabled={disableForm}
/>
<button onClick={this._onCancelClick} disabled={disableForm}>
<button onClick={this.onCancelClick} disabled={disableForm}>
{ _t("Cancel") }
</button>
</div>

View file

@ -15,20 +15,19 @@ limitations under the License.
*/
import React, { createRef } from 'react';
import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk/src/client';
import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryption';
import * as sdk from '../../../../index';
import { _t } from '../../../../languageHandler';
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import { logger } from "matrix-js-sdk/src/logger";
function readFileAsArrayBuffer(file) {
function readFileAsArrayBuffer(file: File): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
resolve(e.target.result);
resolve(e.target.result as ArrayBuffer);
};
reader.onerror = reject;
@ -36,51 +35,57 @@ function readFileAsArrayBuffer(file) {
});
}
const PHASE_EDIT = 1;
const PHASE_IMPORTING = 2;
enum Phase {
Edit = "edit",
Importing = "importing",
}
export default class ImportE2eKeysDialog extends React.Component {
static propTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
onFinished: PropTypes.func.isRequired,
};
interface IProps extends IDialogProps {
matrixClient: MatrixClient;
}
constructor(props) {
interface IState {
enableSubmit: boolean;
phase: Phase;
errStr: string;
}
export default class ImportE2eKeysDialog extends React.Component<IProps, IState> {
private unmounted = false;
private file = createRef<HTMLInputElement>();
private passphrase = createRef<HTMLInputElement>();
constructor(props: IProps) {
super(props);
this._unmounted = false;
this._file = createRef();
this._passphrase = createRef();
this.state = {
enableSubmit: false,
phase: PHASE_EDIT,
phase: Phase.Edit,
errStr: null,
};
}
componentWillUnmount() {
this._unmounted = true;
public componentWillUnmount(): void {
this.unmounted = true;
}
_onFormChange = (ev) => {
const files = this._file.current.files || [];
private onFormChange = (ev: React.FormEvent): void => {
const files = this.file.current.files || [];
this.setState({
enableSubmit: (this._passphrase.current.value !== "" && files.length > 0),
enableSubmit: (this.passphrase.current.value !== "" && files.length > 0),
});
};
_onFormSubmit = (ev) => {
private onFormSubmit = (ev: React.FormEvent): boolean => {
ev.preventDefault();
this._startImport(this._file.current.files[0], this._passphrase.current.value);
this.startImport(this.file.current.files[0], this.passphrase.current.value);
return false;
};
_startImport(file, passphrase) {
private startImport(file: File, passphrase: string) {
this.setState({
errStr: null,
phase: PHASE_IMPORTING,
phase: Phase.Importing,
});
return readFileAsArrayBuffer(file).then((arrayBuffer) => {
@ -94,34 +99,32 @@ export default class ImportE2eKeysDialog extends React.Component {
this.props.onFinished(true);
}).catch((e) => {
logger.error("Error importing e2e keys:", e);
if (this._unmounted) {
if (this.unmounted) {
return;
}
const msg = e.friendlyText || _t('Unknown error');
this.setState({
errStr: msg,
phase: PHASE_EDIT,
phase: Phase.Edit,
});
});
}
_onCancelClick = (ev) => {
private onCancelClick = (ev: React.MouseEvent): boolean => {
ev.preventDefault();
this.props.onFinished(false);
return false;
};
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const disableForm = (this.state.phase !== PHASE_EDIT);
public render(): JSX.Element {
const disableForm = (this.state.phase !== Phase.Edit);
return (
<BaseDialog className='mx_importE2eKeysDialog'
onFinished={this.props.onFinished}
title={_t("Import room keys")}
>
<form onSubmit={this._onFormSubmit}>
<form onSubmit={this.onFormSubmit}>
<div className="mx_Dialog_content">
<p>
{ _t(
@ -149,11 +152,11 @@ export default class ImportE2eKeysDialog extends React.Component {
</div>
<div className='mx_E2eKeysDialog_inputCell'>
<input
ref={this._file}
ref={this.file}
id='importFile'
type='file'
autoFocus={true}
onChange={this._onFormChange}
onChange={this.onFormChange}
disabled={disableForm} />
</div>
</div>
@ -165,11 +168,11 @@ export default class ImportE2eKeysDialog extends React.Component {
</div>
<div className='mx_E2eKeysDialog_inputCell'>
<input
ref={this._passphrase}
ref={this.passphrase}
id='passphrase'
size='64'
size={64}
type='password'
onChange={this._onFormChange}
onChange={this.onFormChange}
disabled={disableForm} />
</div>
</div>
@ -182,7 +185,7 @@ export default class ImportE2eKeysDialog extends React.Component {
value={_t('Import')}
disabled={!this.state.enableSubmit || disableForm}
/>
<button onClick={this._onCancelClick} disabled={disableForm}>
<button onClick={this.onCancelClick} disabled={disableForm}>
{ _t("Cancel") }
</button>
</div>

View file

@ -16,43 +16,40 @@ limitations under the License.
*/
import React from "react";
import PropTypes from "prop-types";
import * as sdk from "../../../../index";
import { MatrixClientPeg } from '../../../../MatrixClientPeg';
import dis from "../../../../dispatcher/dispatcher";
import { _t } from "../../../../languageHandler";
import Modal from "../../../../Modal";
import RestoreKeyBackupDialog from "../../../../components/views/dialogs/security/RestoreKeyBackupDialog";
import { Action } from "../../../../dispatcher/actions";
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
import DialogButtons from "../../../../components/views/elements/DialogButtons";
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
export default class NewRecoveryMethodDialog extends React.PureComponent {
static propTypes = {
// As returned by js-sdk getKeyBackupVersion()
newVersionInfo: PropTypes.object,
onFinished: PropTypes.func.isRequired,
}
interface IProps extends IDialogProps {
newVersionInfo: IKeyBackupInfo;
}
onOkClick = () => {
export default class NewRecoveryMethodDialog extends React.PureComponent<IProps> {
private onOkClick = (): void => {
this.props.onFinished();
}
};
onGoToSettingsClick = () => {
private onGoToSettingsClick = (): void => {
this.props.onFinished();
dis.fire(Action.ViewUserSettings);
}
};
onSetupClick = async () => {
private onSetupClick = async (): Promise<void> => {
Modal.createTrackedDialog(
'Restore Backup', '', RestoreKeyBackupDialog, {
onFinished: this.props.onFinished,
}, null, /* priority = */ false, /* static = */ true,
);
}
render() {
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
};
public render(): JSX.Element {
const title = <span className="mx_KeyBackupFailedDialog_title">
{ _t("New Recovery Method") }
</span>;

View file

@ -15,36 +15,32 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import PropTypes from "prop-types";
import * as sdk from "../../../../index";
import React, { ComponentType } from "react";
import dis from "../../../../dispatcher/dispatcher";
import { _t } from "../../../../languageHandler";
import Modal from "../../../../Modal";
import { Action } from "../../../../dispatcher/actions";
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import DialogButtons from "../../../../components/views/elements/DialogButtons";
export default class RecoveryMethodRemovedDialog extends React.PureComponent {
static propTypes = {
onFinished: PropTypes.func.isRequired,
}
interface IProps extends IDialogProps {}
onGoToSettingsClick = () => {
export default class RecoveryMethodRemovedDialog extends React.PureComponent<IProps> {
private onGoToSettingsClick = (): void => {
this.props.onFinished();
dis.fire(Action.ViewUserSettings);
}
};
onSetupClick = () => {
private onSetupClick = (): void => {
this.props.onFinished();
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
import("./CreateKeyBackupDialog"),
import("./CreateKeyBackupDialog") as unknown as Promise<ComponentType<{}>>,
null, null, /* priority = */ false, /* static = */ true,
);
}
render() {
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
};
public render(): JSX.Element {
const title = <span className="mx_KeyBackupFailedDialog_title">
{ _t("Recovery Method Removed") }
</span>;

View file

@ -49,6 +49,8 @@ export interface IPosition {
bottom?: number;
left?: number;
right?: number;
rightAligned?: boolean;
bottomAligned?: boolean;
}
export enum ChevronFace {
@ -249,6 +251,8 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
let handled = true;
switch (ev.key) {
// XXX: this is imitating roving behaviour, it should really use the RovingTabIndex utils
// to inherit proper handling of unmount edge cases
case Key.TAB:
case Key.ESCAPE:
case Key.ARROW_LEFT: // close on left and right arrows too for when it is a context menu on a <Toolbar />
@ -344,6 +348,8 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
'mx_ContextualMenu_withChevron_right': chevronFace === ChevronFace.Right,
'mx_ContextualMenu_withChevron_top': chevronFace === ChevronFace.Top,
'mx_ContextualMenu_withChevron_bottom': chevronFace === ChevronFace.Bottom,
'mx_ContextualMenu_rightAligned': this.props.rightAligned === true,
'mx_ContextualMenu_bottomAligned': this.props.bottomAligned === true,
});
const menuStyle: CSSProperties = {};

View file

@ -40,6 +40,7 @@ import { replaceableComponent } from "../../utils/replaceableComponent";
import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore";
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
import UIStore from "../../stores/UIStore";
import { findSiblingElement, IState as IRovingTabIndexState } from "../../accessibility/RovingTabIndex";
interface IProps {
isMinimized: boolean;
@ -51,19 +52,12 @@ interface IState {
activeSpace?: Room;
}
// List of CSS classes which should be included in keyboard navigation within the room list
const cssClasses = [
"mx_RoomSearch_input",
"mx_RoomSearch_minimizedHandle", // minimized <RoomSearch />
"mx_RoomSublist_headerText",
"mx_RoomTile",
"mx_RoomSublist_showNButton",
];
@replaceableComponent("structures.LeftPanel")
export default class LeftPanel extends React.Component<IProps, IState> {
private ref: React.RefObject<HTMLDivElement> = createRef();
private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
private ref = createRef<HTMLDivElement>();
private listContainerRef = createRef<HTMLDivElement>();
private roomSearchRef = createRef<RoomSearch>();
private roomListRef = createRef<RoomList>();
private focusedElement = null;
private isDoingStickyHeaders = false;
@ -283,16 +277,25 @@ export default class LeftPanel extends React.Component<IProps, IState> {
this.focusedElement = null;
};
private onKeyDown = (ev: React.KeyboardEvent) => {
private onKeyDown = (ev: React.KeyboardEvent, state?: IRovingTabIndexState) => {
if (!this.focusedElement) return;
const action = getKeyBindingsManager().getRoomListAction(ev);
switch (action) {
case RoomListAction.NextRoom:
if (!state) {
ev.stopPropagation();
ev.preventDefault();
this.roomListRef.current?.focus();
}
break;
case RoomListAction.PrevRoom:
ev.stopPropagation();
ev.preventDefault();
this.onMoveFocus(action === RoomListAction.PrevRoom);
if (state && state.activeRef === findSiblingElement(state.refs, 0)) {
ev.stopPropagation();
ev.preventDefault();
this.roomSearchRef.current?.focus();
}
break;
}
};
@ -305,45 +308,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
}
};
private onMoveFocus = (up: boolean) => {
let element = this.focusedElement;
let descending = false; // are we currently descending or ascending through the DOM tree?
let classes: DOMTokenList;
do {
const child = up ? element.lastElementChild : element.firstElementChild;
const sibling = up ? element.previousElementSibling : element.nextElementSibling;
if (descending) {
if (child) {
element = child;
} else if (sibling) {
element = sibling;
} else {
descending = false;
element = element.parentElement;
}
} else {
if (sibling) {
element = sibling;
descending = true;
} else {
element = element.parentElement;
}
}
if (element) {
classes = element.classList;
}
} while (element && (!cssClasses.some(c => classes.contains(c)) || element.offsetParent === null));
if (element) {
element.focus();
this.focusedElement = element;
}
};
private renderHeader(): React.ReactNode {
return (
<div className="mx_LeftPanel_userHeader">
@ -388,7 +352,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
>
<RoomSearch
isMinimized={this.props.isMinimized}
onKeyDown={this.onKeyDown}
ref={this.roomSearchRef}
onSelectRoom={this.selectRoom}
/>
@ -417,6 +381,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
activeSpace={this.state.activeSpace}
onResize={this.refreshStickyHeaders}
onListCollapse={this.refreshStickyHeaders}
ref={this.roomListRef}
/>;
const containerClasses = classNames({

View file

@ -108,6 +108,7 @@ interface IProps {
currentGroupIsNew?: boolean;
justRegistered?: boolean;
roomJustCreatedOpts?: IOpts;
forceTimeline?: boolean; // see props on MatrixChat
}
interface IUsageLimit {
@ -611,6 +612,7 @@ class LoggedInView extends React.Component<IProps, IState> {
key={this.props.currentRoomId || 'roomview'}
resizeNotifier={this.props.resizeNotifier}
justCreatedOpts={this.props.roomJustCreatedOpts}
forceTimeline={this.props.forceTimeline}
/>;
break;

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef } from 'react';
import React, { ComponentType, createRef } from 'react';
import { createClient } from "matrix-js-sdk/src/matrix";
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
@ -176,6 +176,9 @@ interface IRoomInfo {
threepid_invite?: IThreepidInvite;
justCreatedOpts?: IOpts;
// Whether or not to override default behaviour to end up at a timeline
forceTimeline?: boolean;
}
/* eslint-enable camelcase */
@ -238,6 +241,7 @@ interface IState {
pendingInitialSync?: boolean;
justRegistered?: boolean;
roomJustCreatedOpts?: IOpts;
forceTimeline?: boolean; // see props
}
@replaceableComponent("structures.MatrixChat")
@ -872,6 +876,15 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
params.hs_url, params.is_url,
);
// If the hs url matches then take the hs name we know locally as it is likely prettier
const defaultConfig = SdkConfig.get()["validated_server_config"] as ValidatedServerConfig;
if (defaultConfig && defaultConfig.hsUrl === newState.serverConfig.hsUrl) {
newState.serverConfig.hsName = defaultConfig.hsName;
newState.serverConfig.hsNameIsDifferent = defaultConfig.hsNameIsDifferent;
newState.serverConfig.isDefault = defaultConfig.isDefault;
newState.serverConfig.isNameResolvable = defaultConfig.isNameResolvable;
}
newState.register_client_secret = params.client_secret;
newState.register_session_id = params.session_id;
newState.register_id_sid = params.sid;
@ -959,6 +972,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
page_type: PageType.RoomView,
threepidInvite: roomInfo.threepid_invite,
roomOobData: roomInfo.oob_data,
forceTimeline: roomInfo.forceTimeline,
ready: true,
roomJustCreatedOpts: roomInfo.justCreatedOpts,
}, () => {
@ -1587,12 +1601,16 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
if (haveNewVersion) {
Modal.createTrackedDialogAsync('New Recovery Method', 'New Recovery Method',
import('../../async-components/views/dialogs/security/NewRecoveryMethodDialog'),
import(
'../../async-components/views/dialogs/security/NewRecoveryMethodDialog'
) as unknown as Promise<ComponentType<{}>>,
{ newVersionInfo },
);
} else {
Modal.createTrackedDialogAsync('Recovery Method Removed', 'Recovery Method Removed',
import('../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog'),
import(
'../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog'
) as unknown as Promise<ComponentType<{}>>,
);
}
});

View file

@ -196,6 +196,7 @@ interface IReadReceiptForUser {
@replaceableComponent("structures.MessagePanel")
export default class MessagePanel extends React.Component<IProps, IState> {
static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;
// opaque readreceipt info for each userId; used by ReadReceiptMarker
// to manage its animations
@ -560,6 +561,9 @@ export default class MessagePanel extends React.Component<IProps, IState> {
}
private get pendingEditItem(): string | undefined {
if (!this.props.room) {
return undefined;
}
try {
return localStorage.getItem(`mx_edit_room_${this.props.room.roomId}_${this.context.timelineRenderingType}`);
} catch (err) {
@ -784,6 +788,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
showReadReceipts={this.props.showReadReceipts}
callEventGrouper={callEventGrouper}
hideSender={this.membersCount <= 2 && this.props.layout === Layout.Bubble}
timelineRenderingType={this.context.timelineRenderingType}
/>
</TileErrorBoundary>,
);

View file

@ -40,7 +40,7 @@ export default class NotificationPanel extends React.PureComponent<IProps> {
static contextType = RoomContext;
render() {
const emptyState = (<div className="mx_RightPanel_empty mx_NotificationPanel_empty">
<h2>{ _t('Youre all caught up') }</h2>
<h2>{ _t("You're all caught up") }</h2>
<p>{ _t('You have no visible notifications.') }</p>
</div>);

View file

@ -75,6 +75,8 @@ interface IState {
groupRoomId?: string;
groupId?: string;
event: MatrixEvent;
initialEvent?: MatrixEvent;
initialEventHighlighted?: boolean;
}
@replaceableComponent("structures.RightPanel")
@ -209,6 +211,8 @@ export default class RightPanel extends React.Component<IProps, IState> {
groupId: payload.groupId,
member: payload.member,
event: payload.event,
initialEvent: payload.initialEvent,
initialEventHighlighted: payload.highlighted,
verificationRequest: payload.verificationRequest,
verificationRequestPromise: payload.verificationRequestPromise,
widgetId: payload.widgetId,
@ -244,7 +248,7 @@ export default class RightPanel extends React.Component<IProps, IState> {
}
};
render() {
public render(): JSX.Element {
let panel = <div />;
const roomId = this.props.room ? this.props.room.roomId : undefined;
@ -327,6 +331,8 @@ export default class RightPanel extends React.Component<IProps, IState> {
resizeNotifier={this.props.resizeNotifier}
onClose={this.onClose}
mxEvent={this.state.event}
initialEvent={this.state.initialEvent}
initialEventHighlighted={this.state.initialEventHighlighted}
permalinkCreator={this.props.permalinkCreator}
e2eStatus={this.props.e2eStatus} />;
break;

Some files were not shown because too many files have changed in this diff Show more