diff --git a/package.json b/package.json index 28f3d1633c..7c008d5ccc 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "is-ip": "^2.0.0", "linkifyjs": "^2.1.6", "lodash": "^4.17.14", - "matrix-js-sdk": "6.1.0", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "minimist": "^1.2.0", "pako": "^1.0.5", "parse5": "^5.1.1", @@ -87,7 +87,6 @@ "project-name-generator": "^2.1.7", "prop-types": "^15.5.8", "qrcode": "^1.4.4", - "qrcode-react": "^0.1.16", "qs": "^6.6.0", "react": "^16.9.0", "react-beautiful-dnd": "^4.0.1", @@ -118,8 +117,11 @@ "@babel/register": "^7.7.4", "@peculiar/webcrypto": "^1.0.22", "@types/classnames": "^2.2.10", + "@types/flux": "^3.1.9", "@types/modernizr": "^3.5.3", + "@types/qrcode": "^1.3.4", "@types/react": "16.9", + "@types/zxcvbn": "^4.4.0", "babel-eslint": "^10.0.3", "babel-jest": "^24.9.0", "chokidar": "^3.3.1", diff --git a/res/css/_components.scss b/res/css/_components.scss index 428a28ac3a..3a6a3257a3 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -41,6 +41,7 @@ @import "./views/auth/_CountryDropdown.scss"; @import "./views/auth/_InteractiveAuthEntryComponents.scss"; @import "./views/auth/_LanguageSelector.scss"; +@import "./views/auth/_PassphraseField.scss"; @import "./views/auth/_ServerConfig.scss"; @import "./views/auth/_ServerTypeSelector.scss"; @import "./views/auth/_Welcome.scss"; @@ -108,11 +109,13 @@ @import "./views/elements/_ManageIntegsButton.scss"; @import "./views/elements/_PowerSelector.scss"; @import "./views/elements/_ProgressBar.scss"; +@import "./views/elements/_QRCode.scss"; @import "./views/elements/_ReplyThread.scss"; @import "./views/elements/_ResizeHandle.scss"; @import "./views/elements/_RichText.scss"; @import "./views/elements/_RoleButton.scss"; @import "./views/elements/_RoomAliasField.scss"; +@import "./views/elements/_Slider.scss"; @import "./views/elements/_Spinner.scss"; @import "./views/elements/_SyntaxHighlight.scss"; @import "./views/elements/_TextWithTooltip.scss"; @@ -160,6 +163,8 @@ @import "./views/rooms/_EditMessageComposer.scss"; @import "./views/rooms/_EntityTile.scss"; @import "./views/rooms/_EventTile.scss"; +@import "./views/rooms/_GroupLayout.scss"; +@import "./views/rooms/_IRCLayout.scss"; @import "./views/rooms/_InviteOnlyIcon.scss"; @import "./views/rooms/_JumpToBottomButton.scss"; @import "./views/rooms/_LinkPreviewWidget.scss"; @@ -202,6 +207,7 @@ @import "./views/settings/tabs/room/_GeneralRoomSettingsTab.scss"; @import "./views/settings/tabs/room/_RolesRoomSettingsTab.scss"; @import "./views/settings/tabs/room/_SecurityRoomSettingsTab.scss"; +@import "./views/settings/tabs/user/_AppearanceUserSettingsTab.scss"; @import "./views/settings/tabs/user/_GeneralUserSettingsTab.scss"; @import "./views/settings/tabs/user/_HelpUserSettingsTab.scss"; @import "./views/settings/tabs/user/_MjolnirUserSettingsTab.scss"; diff --git a/res/css/structures/_TagPanel.scss b/res/css/structures/_TagPanel.scss index 4a78c8df92..1f8443e395 100644 --- a/res/css/structures/_TagPanel.scss +++ b/res/css/structures/_TagPanel.scss @@ -69,7 +69,7 @@ limitations under the License. height: 100%; } .mx_TagPanel .mx_TagPanel_tagTileContainer > div { - height: $font-40px; + height: 40px; padding: 10px 0 9px 0; } @@ -116,7 +116,7 @@ limitations under the License. position: absolute; left: -15px; border-radius: 0 3px 3px 0; - top: -8px; // (16px / 2) + top: -8px; // (16px from height / 2) } .mx_TagPanel .mx_TagTile.mx_AccessibleButton:focus { diff --git a/res/css/structures/_TopLeftMenuButton.scss b/res/css/structures/_TopLeftMenuButton.scss index 53d44e7c24..8d2e36bcd6 100644 --- a/res/css/structures/_TopLeftMenuButton.scss +++ b/res/css/structures/_TopLeftMenuButton.scss @@ -43,7 +43,7 @@ limitations under the License. margin: 0 7px; mask: url('$(res)/img/feather-customised/dropdown-arrow.svg'); mask-repeat: no-repeat; - width: 10px; + width: $font-22px; height: 6px; background-color: $roomsublist-label-fg-color; } diff --git a/res/css/views/auth/_AuthBody.scss b/res/css/views/auth/_AuthBody.scss index 4b2d6b1bf1..120da4c4f1 100644 --- a/res/css/views/auth/_AuthBody.scss +++ b/res/css/views/auth/_AuthBody.scss @@ -146,27 +146,3 @@ limitations under the License. .mx_AuthBody_spinner { margin: 1em 0; } - -.mx_AuthBody_passwordScore { - width: 100%; - appearance: none; - height: 4px; - border: 0; - border-radius: 2px; - position: absolute; - top: -12px; - - &::-moz-progress-bar { - border-radius: 2px; - background-color: $accent-color; - } - - &::-webkit-progress-bar, - &::-webkit-progress-value { - border-radius: 2px; - } - - &::-webkit-progress-value { - background-color: $accent-color; - } -} diff --git a/res/css/views/auth/_PassphraseField.scss b/res/css/views/auth/_PassphraseField.scss new file mode 100644 index 0000000000..d1b8c47d00 --- /dev/null +++ b/res/css/views/auth/_PassphraseField.scss @@ -0,0 +1,55 @@ +/* +Copyright 2020 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. +*/ + +$PassphraseStrengthHigh: $accent-color; +$PassphraseStrengthMedium: $username-variant5-color; +$PassphraseStrengthLow: $notice-primary-color; + +@define-mixin ProgressBarColour $colour { + color: $colour; + &::-moz-progress-bar { + background-color: $colour; + } + &::-webkit-progress-value { + background-color: $colour; + } +} + +progress.mx_PassphraseField_progress { + appearance: none; + width: 100%; + border: 0; + height: 4px; + position: absolute; + top: -12px; + + border-radius: 2px; + &::-moz-progress-bar { + border-radius: 2px; + } + &::-webkit-progress-bar, + &::-webkit-progress-value { + border-radius: 2px; + } + + @mixin ProgressBarColour $PassphraseStrengthLow; + &[value="2"], &[value="3"] { + @mixin ProgressBarColour $PassphraseStrengthMedium; + } + &[value="4"] { + @mixin ProgressBarColour $PassphraseStrengthHigh; + } +} diff --git a/res/css/views/dialogs/_ShareDialog.scss b/res/css/views/dialogs/_ShareDialog.scss index 9a2f67dea3..e08469ec6d 100644 --- a/res/css/views/dialogs/_ShareDialog.scss +++ b/res/css/views/dialogs/_ShareDialog.scss @@ -64,9 +64,6 @@ limitations under the License. .mx_ShareDialog_qrcode_container { float: left; - background-color: #ffffff; - padding: 5px; // makes qr code more readable in dark theme - border-radius: 5px; height: 256px; width: 256px; margin-right: 64px; diff --git a/res/css/views/dialogs/_UserSettingsDialog.scss b/res/css/views/dialogs/_UserSettingsDialog.scss index 4d831d7858..7adcc58c4e 100644 --- a/res/css/views/dialogs/_UserSettingsDialog.scss +++ b/res/css/views/dialogs/_UserSettingsDialog.scss @@ -21,6 +21,10 @@ limitations under the License. mask-image: url('$(res)/img/feather-customised/settings.svg'); } +.mx_UserSettingsDialog_appearanceIcon::before { + mask-image: url('$(res)/img/feather-customised/brush.svg'); +} + .mx_UserSettingsDialog_voiceIcon::before { mask-image: url('$(res)/img/feather-customised/phone.svg'); } diff --git a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss index b9babd05f5..9be98e25b2 100644 --- a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss +++ b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss @@ -35,17 +35,6 @@ limitations under the License. align-items: flex-start; } -.mx_CreateKeyBackupDialog_passPhraseHelp { - flex: 1; - height: 85px; - margin-left: 20px; - font-size: 80%; -} - -.mx_CreateKeyBackupDialog_passPhraseHelp progress { - width: 100%; -} - .mx_CreateKeyBackupDialog_passPhraseInput { flex: none; width: 250px; diff --git a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss index a9ebd54b31..63e5a3de09 100644 --- a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss +++ b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss @@ -68,17 +68,6 @@ limitations under the License. margin-top: 0px; } -.mx_CreateSecretStorageDialog_passPhraseHelp { - flex: 1; - height: 64px; - margin-left: 20px; - font-size: 80%; -} - -.mx_CreateSecretStorageDialog_passPhraseHelp progress { - width: 100%; -} - .mx_CreateSecretStorageDialog_passPhraseMatch { width: 200px; margin-left: 20px; diff --git a/src/utils/rem.js b/res/css/views/elements/_QRCode.scss similarity index 85% rename from src/utils/rem.js rename to res/css/views/elements/_QRCode.scss index 1f18c9de05..96d9114b54 100644 --- a/src/utils/rem.js +++ b/res/css/views/elements/_QRCode.scss @@ -14,7 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -// converts a pixel value to rem. -export default function(pixelVal) { - return pixelVal / 15 + "rem"; +.mx_QRCode { + img { + border-radius: 8px; + } } diff --git a/res/css/views/elements/_Slider.scss b/res/css/views/elements/_Slider.scss new file mode 100644 index 0000000000..58ba2813b4 --- /dev/null +++ b/res/css/views/elements/_Slider.scss @@ -0,0 +1,99 @@ +/* +Copyright 2020 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_Slider { + position: relative; + margin: 0px; + flex-grow: 1; +} + +.mx_Slider_dotContainer { + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.mx_Slider_bar { + display: flex; + box-sizing: border-box; + position: absolute; + height: 1em; + width: 100%; + padding: 0 0.5em; // half the width of a dot. + align-items: center; +} + +.mx_Slider_bar > hr { + width: 100%; + height: 0.4em; + background-color: $slider-background-color; + border: 0; +} + +.mx_Slider_selection { + display: flex; + align-items: center; + width: calc(100% - 1em); // 2 * half the width of a dot + height: 1em; + position: absolute; + pointer-events: none; +} + +.mx_Slider_selectionDot { + position: absolute; + width: 1.1em; + height: 1.1em; + background-color: $slider-selection-color; + border-radius: 50%; + box-shadow: 0 0 6px lightgrey; + z-index: 10; +} + +.mx_Slider_selection > hr { + margin: 0; + border: 0.2em solid $slider-selection-color; +} + +.mx_Slider_dot { + height: 1em; + width: 1em; + border-radius: 50%; + background-color: $slider-background-color; + z-index: 0; +} + +.mx_Slider_dotActive { + background-color: $slider-selection-color; +} + +.mx_Slider_dotValue { + display: flex; + flex-direction: column; + align-items: center; + color: $slider-background-color; +} + +// The following is a hack to center the labels without adding +// any width to the slider's dots. +.mx_Slider_labelContainer { + width: 1em; +} + +.mx_Slider_label { + position: relative; + width: fit-content; + left: -50%; +} diff --git a/res/css/views/rooms/_AppsDrawer.scss b/res/css/views/rooms/_AppsDrawer.scss index 1b1bab67bc..e4743f189e 100644 --- a/res/css/views/rooms/_AppsDrawer.scss +++ b/res/css/views/rooms/_AppsDrawer.scss @@ -96,6 +96,10 @@ $AppsDrawerBodyHeight: 273px; height: $AppsDrawerBodyHeight; } +.mx_AppTile_persistedWrapper > div { + height: 100%; +} + .mx_AppTile_mini .mx_AppTile_persistedWrapper { height: 114px; } diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index d4e54f4473..4297bf809c 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -37,7 +37,6 @@ limitations under the License. } .mx_EventTile_avatar { - position: absolute; top: 14px; left: 8px; cursor: pointer; @@ -68,11 +67,9 @@ limitations under the License. display: inline-block; /* anti-zalgo, with overflow hidden */ overflow: hidden; cursor: pointer; - padding-left: 65px; /* left gutter */ padding-bottom: 0px; padding-top: 0px; margin: 0px; - line-height: $font-17px; /* the next three lines, along with overflow hidden, truncate long display names */ white-space: nowrap; text-overflow: ellipsis; @@ -101,12 +98,9 @@ limitations under the License. .mx_EventTile .mx_MessageTimestamp { display: block; - visibility: hidden; white-space: nowrap; left: 0px; - width: 46px; /* 8 + 30 (avatar) + 8 */ text-align: center; - position: absolute; user-select: none; } @@ -117,10 +111,7 @@ limitations under the License. .mx_EventTile_line, .mx_EventTile_reply { position: relative; padding-left: 65px; /* left gutter */ - padding-top: 3px; - padding-bottom: 3px; border-radius: 4px; - line-height: $font-22px; } .mx_RoomView_timeline_rr_enabled, @@ -151,10 +142,6 @@ limitations under the License. margin-right: 10px; } -.mx_EventTile_info .mx_EventTile_line { - padding-left: 83px; -} - /* HACK to override line-height which is already marked important elsewhere */ .mx_EventTile_bigEmoji.mx_EventTile_bigEmoji { font-size: 48px !important; @@ -560,84 +547,6 @@ limitations under the License. /* end of overrides */ -.mx_MatrixChat_useCompactLayout { - .mx_EventTile { - padding-top: 4px; - } - - .mx_EventTile.mx_EventTile_info { - // same as the padding for non-compact .mx_EventTile.mx_EventTile_info - padding-top: 0px; - font-size: $font-13px; - .mx_EventTile_line, .mx_EventTile_reply { - line-height: $font-20px; - } - .mx_EventTile_avatar { - top: 4px; - } - } - - .mx_EventTile .mx_SenderProfile { - font-size: $font-13px; - } - - .mx_EventTile.mx_EventTile_emote { - // add a bit more space for emotes so that avatars don't collide - padding-top: 8px; - .mx_EventTile_avatar { - top: 2px; - } - .mx_EventTile_line, .mx_EventTile_reply { - padding-top: 0px; - padding-bottom: 1px; - } - } - - .mx_EventTile.mx_EventTile_emote.mx_EventTile_continuation { - padding-top: 0; - .mx_EventTile_line, .mx_EventTile_reply { - padding-top: 0px; - padding-bottom: 0px; - } - } - - .mx_EventTile_line, .mx_EventTile_reply { - padding-top: 0px; - padding-bottom: 0px; - } - - .mx_EventTile_avatar { - top: 2px; - } - - .mx_EventTile_e2eIcon { - top: 3px; - } - - .mx_EventTile_readAvatars { - top: 27px; - } - - .mx_EventTile_continuation .mx_EventTile_readAvatars, - .mx_EventTile_emote .mx_EventTile_readAvatars { - top: 5px; - } - - .mx_EventTile_info .mx_EventTile_readAvatars { - top: 4px; - } - - .mx_RoomView_MessageList h2 { - margin-top: 6px; - } - - .mx_EventTile_content .markdown-body { - p, ul, ol, dl, blockquote, pre, table { - margin-bottom: 4px; // 1/4 of the non-compact margin-bottom - } - } -} - .mx_EventTile_tileError { color: red; text-align: center; diff --git a/res/css/views/rooms/_GroupLayout.scss b/res/css/views/rooms/_GroupLayout.scss new file mode 100644 index 0000000000..40440f7d49 --- /dev/null +++ b/res/css/views/rooms/_GroupLayout.scss @@ -0,0 +1,132 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2020 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. +*/ + +$left-gutter: 65px; + +.mx_GroupLayout { + + .mx_EventTile { + > .mx_SenderProfile { + line-height: $font-17px; + padding-left: $left-gutter; + } + + > .mx_EventTile_line { + padding-left: $left-gutter; + } + + > .mx_EventTile_avatar { + position: absolute; + } + + .mx_MessageTimestamp { + visibility: hidden; + position: absolute; + width: 46px; /* 8 + 30 (avatar) + 8 */ + } + + .mx_EventTile_line, .mx_EventTile_reply { + padding-top: 3px; + padding-bottom: 3px; + line-height: $font-22px; + } + } + + .mx_EventTile_info .mx_EventTile_line { + padding-left: calc($left-gutter + 18px); + } +} + +/* Compact layout overrides */ + +.mx_MatrixChat_useCompactLayout { + .mx_EventTile { + padding-top: 4px; + } + + .mx_EventTile.mx_EventTile_info { + // same as the padding for non-compact .mx_EventTile.mx_EventTile_info + padding-top: 0px; + font-size: $font-13px; + .mx_EventTile_line, .mx_EventTile_reply { + line-height: $font-20px; + } + .mx_EventTile_avatar { + top: 4px; + } + } + + .mx_EventTile .mx_SenderProfile { + font-size: $font-13px; + } + + .mx_EventTile.mx_EventTile_emote { + // add a bit more space for emotes so that avatars don't collide + padding-top: 8px; + .mx_EventTile_avatar { + top: 2px; + } + .mx_EventTile_line, .mx_EventTile_reply { + padding-top: 0px; + padding-bottom: 1px; + } + } + + .mx_EventTile.mx_EventTile_emote.mx_EventTile_continuation { + padding-top: 0; + .mx_EventTile_line, .mx_EventTile_reply { + padding-top: 0px; + padding-bottom: 0px; + } + } + + .mx_EventTile_line, .mx_EventTile_reply { + padding-top: 0px; + padding-bottom: 0px; + } + + .mx_EventTile_avatar { + top: 2px; + } + + .mx_EventTile_e2eIcon { + top: 3px; + } + + .mx_EventTile_readAvatars { + top: 27px; + } + + .mx_EventTile_continuation .mx_EventTile_readAvatars, + .mx_EventTile_emote .mx_EventTile_readAvatars { + top: 5px; + } + + .mx_EventTile_info .mx_EventTile_readAvatars { + top: 4px; + } + + .mx_RoomView_MessageList h2 { + margin-top: 6px; + } + + .mx_EventTile_content .markdown-body { + p, ul, ol, dl, blockquote, pre, table { + margin-bottom: 4px; // 1/4 of the non-compact margin-bottom + } + } +} diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss new file mode 100644 index 0000000000..5f88473c5f --- /dev/null +++ b/res/css/views/rooms/_IRCLayout.scss @@ -0,0 +1,214 @@ +/* +Copyright 2020 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. +*/ + +$icon-width: 14px; +$timestamp-width: 45px; +$right-padding: 5px; +$irc-line-height: $font-18px; + +.mx_IRCLayout { + --name-width: 70px; + + line-height: $irc-line-height !important; + + .mx_EventTile { + + // timestamps are links which shouldn't be underlined + > a { + text-decoration: none; + } + + display: flex; + flex-direction: row; + align-items: flex-start; + padding-top: 0; + + > * { + margin-right: $right-padding; + } + + > .mx_EventTile_msgOption { + order: 4; + flex-shrink: 0; + } + + > .mx_SenderProfile { + order: 2; + flex-shrink: 0; + width: var(--name-width); + text-overflow: ellipsis; + text-align: right; + display: flex; + align-items: center; + overflow: visible; + justify-content: flex-end; + } + + .mx_EventTile_line, .mx_EventTile_reply { + padding: 0; + display: flex; + flex-direction: column; + order: 3; + flex-grow: 1; + } + + > .mx_EventTile_avatar { + order: 1; + position: relative; + top: 0; + left: 0; + flex-shrink: 0; + height: $irc-line-height; + display: flex; + align-items: center; + + // Need to use important to override the js provided height and width values. + > .mx_BaseAvatar, .mx_BaseAvatar > * { + height: $font-14px !important; + width: $font-14px !important; + font-size: $font-10px !important; + line-height: $font-15px !important; + } + } + + .mx_MessageTimestamp { + font-size: $font-10px; + width: $timestamp-width; + text-align: right; + } + + .mx_EventTile_e2eIcon { + position: relative; + right: unset; + left: unset; + top: -2px; + padding: 0; + } + + .mx_EventTile_line { + .mx_EventTile_e2eIcon, + .mx_TextualEvent, + .mx_MTextBody, + .mx_ReplyThread_wrapper_empty { + display: inline-block; + } + } + + .mx_EvenTile_line .mx_MessageActionBar, + .mx_EvenTile_line .mx_ReplyThread_wrapper { + display: block; + } + + .mx_EventTile_reply { + order: 3; + } + + .mx_EditMessageComposer_buttons { + position: relative; + } + } + + .mx_EventTile_emote { + > .mx_EventTile_avatar { + margin-left: calc(var(--name-width) + $icon-width + $right-padding); + } + } + + blockquote { + margin: 0; + } + + .mx_EventListSummary { + > .mx_EventTile_line { + padding-left: calc(var(--name-width) + $icon-width + $timestamp-width + 3 * $right-padding); // 15 px of padding + } + + .mx_EventListSummary_avatars { + padding: 0; + margin: 0 9px 0 0; + } + } + + .mx_EventTile.mx_EventTile_info { + .mx_EventTile_avatar { + left: calc(var(--name-width) + 10px + $icon-width); + top: 0; + } + + .mx_EventTile_line { + left: calc(var(--name-width) + 10px + $icon-width); + } + + .mx_TextualEvent { + line-height: $irc-line-height; + } + } + + // Suppress highlight thing from the normal Layout. + .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line, + .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line, + .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line { + padding-left: 0; + border-left: 0; + } + + .mx_SenderProfile_hover { + background-color: $primary-bg-color; + overflow: hidden; + + > span { + display: flex; + + > .mx_SenderProfile_name { + overflow: hidden; + text-overflow: ellipsis; + } + } + } + + .mx_SenderProfile:hover { + justify-content: flex-start; + } + + .mx_SenderProfile_hover:hover { + overflow: visible; + width: max(auto, 100%); + z-index: 10; + } + + .mx_ReplyThread { + margin: 0; + .mx_SenderProfile { + width: unset; + max-width: var(--name-width); + } + } + + .mx_ProfileResizer { + position: absolute; + height: 100%; + width: 15px; + left: calc(80px + var(--name-width)); + cursor: col-resize; + z-index: 100; + } + + // Need to use important to override the js provided height and width values. + .mx_Flair > img { + height: $font-14px !important; + width: $font-14px !important; + } +} diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 5bc7d5624d..759dce5afa 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -20,7 +20,7 @@ limitations under the License. flex-direction: row; align-items: center; cursor: pointer; - height: $font-34px; + height: 32px; margin: 0; padding: 0 8px 0 10px; position: relative; @@ -81,6 +81,7 @@ limitations under the License. .mx_RoomTile_avatar_container { position: relative; + display: flex; } .mx_RoomTile_avatar { diff --git a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss new file mode 100644 index 0000000000..e82ae3c575 --- /dev/null +++ b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss @@ -0,0 +1,45 @@ +/* +Copyright 2020 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_AppearanceUserSettingsTab_fontSlider, +.mx_AppearanceUserSettingsTab_themeSection .mx_Field, +.mx_AppearanceUserSettingsTab_fontScaling .mx_Field { + @mixin mx_Settings_fullWidthField; +} + +.mx_AppearanceUserSettingsTab_fontSlider { + display: flex; + flex-direction: row; + align-items: center; + padding: 15px; + background: $font-slider-bg-color; + border-radius: 10px; + font-size: 10px; + margin-top: 24px; + margin-bottom: 24px; +} + +.mx_AppearanceUserSettingsTab_fontSlider_smallText { + font-size: 15px; + padding-right: 20px; + padding-left: 5px; +} + +.mx_AppearanceUserSettingsTab_fontSlider_largeText { + font-size: 18px; + padding-left: 20px; + padding-right: 5px; +} diff --git a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss index 95a46b51ee..6c9b89cf5a 100644 --- a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss @@ -14,8 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_GeneralUserSettingsTab_changePassword .mx_Field, -.mx_GeneralUserSettingsTab_themeSection .mx_Field { +.mx_GeneralUserSettingsTab_changePassword .mx_Field { @mixin mx_Settings_fullWidthField; } diff --git a/res/img/feather-customised/brush.svg b/res/img/feather-customised/brush.svg new file mode 100644 index 0000000000..d7f2738629 --- /dev/null +++ b/res/img/feather-customised/brush.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 6224c0820f..9fb36ef1a3 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -180,6 +180,9 @@ $breadcrumb-placeholder-bg-color: #272c35; $user-tile-hover-bg-color: $header-panel-bg-color; +// FontSlider colors +$font-slider-bg-color: $room-highlight-color; + // ***** Mixins! ***** @define-mixin mx_DialogButton { diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index f5f3013354..78fe2a74c5 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -262,6 +262,10 @@ $togglesw-off-color: #c1c9d6; $togglesw-on-color: $accent-color; $togglesw-ball-color: #fff; +// Slider +$slider-selection-color: $accent-color; +$slider-background-color: #c1c9d6; + $progressbar-color: #000; $room-warning-bg-color: $yellow-background; @@ -302,6 +306,9 @@ $breadcrumb-placeholder-bg-color: #e8eef5; $user-tile-hover-bg-color: $header-panel-bg-color; +// FontSlider colors +$font-slider-bg-color: rgba($input-darker-bg-color, 0.2); + // ***** Mixins! ***** @define-mixin mx_DialogButton { diff --git a/src/BasePlatform.js b/src/BasePlatform.js index 7214031586..e3cbc4dcf0 100644 --- a/src/BasePlatform.js +++ b/src/BasePlatform.js @@ -20,7 +20,7 @@ limitations under the License. */ import {MatrixClient} from "matrix-js-sdk"; -import dis from './dispatcher'; +import dis from './dispatcher/dispatcher'; import BaseEventIndexManager from './indexing/BaseEventIndexManager'; /** @@ -167,13 +167,9 @@ export default class BasePlatform { setLanguage(preferredLangs: string[]) {} - getSSOCallbackUrl(hsUrl: string, isUrl: string): URL { + getSSOCallbackUrl(hsUrl: string, isUrl: string, fragmentAfterLogin: string): URL { const url = new URL(window.location.href); - // XXX: at this point, the fragment will always be #/login, which is no - // use to anyone. Ideally, we would get the intended fragment from - // MatrixChat.screenAfterLogin so that you could follow #/room links etc - // through an SSO login. - url.hash = ""; + url.hash = fragmentAfterLogin || ""; url.searchParams.set("homeserver", hsUrl); url.searchParams.set("identityServer", isUrl); return url; @@ -183,9 +179,11 @@ export default class BasePlatform { * Begin Single Sign On flows. * @param {MatrixClient} mxClient the matrix client using which we should start the flow * @param {"sso"|"cas"} loginType the type of SSO it is, CAS/SSO. + * @param {string} fragmentAfterLogin the hash to pass to the app during sso callback. */ - startSingleSignOn(mxClient: MatrixClient, loginType: "sso"|"cas") { - const callbackUrl = this.getSSOCallbackUrl(mxClient.getHomeserverUrl(), mxClient.getIdentityServerUrl()); + startSingleSignOn(mxClient: MatrixClient, loginType: "sso" | "cas", fragmentAfterLogin: string) { + const callbackUrl = this.getSSOCallbackUrl(mxClient.getHomeserverUrl(), mxClient.getIdentityServerUrl(), + fragmentAfterLogin); window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType); // redirect to SSO } diff --git a/src/CallHandler.js b/src/CallHandler.js index 2bfe10850a..c95ed16eb3 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -59,7 +59,7 @@ import Modal from './Modal'; import * as sdk from './index'; import { _t } from './languageHandler'; import Matrix from 'matrix-js-sdk'; -import dis from './dispatcher'; +import dis from './dispatcher/dispatcher'; import { showUnknownDeviceDialogForCalls } from './cryptodevices'; import WidgetUtils from './utils/WidgetUtils'; import WidgetEchoStore from './stores/WidgetEchoStore'; diff --git a/src/ContentMessages.js b/src/ContentMessages.js index 34379c029b..4f5a1a1220 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -18,7 +18,7 @@ limitations under the License. 'use strict'; import extend from './extend'; -import dis from './dispatcher'; +import dis from './dispatcher/dispatcher'; import {MatrixClientPeg} from './MatrixClientPeg'; import * as sdk from './index'; import { _t } from './languageHandler'; diff --git a/src/FontWatcher.js b/src/FontWatcher.js new file mode 100644 index 0000000000..006df202ad --- /dev/null +++ b/src/FontWatcher.js @@ -0,0 +1,51 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import dis from './dispatcher/dispatcher'; +import SettingsStore, {SettingLevel} from './settings/SettingsStore'; + +export class FontWatcher { + static MIN_SIZE = 13; + static MAX_SIZE = 20; + + constructor() { + this._dispatcherRef = null; + } + + start() { + this._setRootFontSize(SettingsStore.getValue("fontSize")); + this._dispatcherRef = dis.register(this._onAction); + } + + stop() { + dis.unregister(this._dispatcherRef); + } + + _onAction = (payload) => { + if (payload.action === 'update-font-size') { + this._setRootFontSize(payload.size); + } + }; + + _setRootFontSize = (size) => { + const fontSize = Math.max(Math.min(FontWatcher.MAX_SIZE, size), FontWatcher.MIN_SIZE); + + if (fontSize != size) { + SettingsStore.setValue("fontSize", null, SettingLevel.Device, fontSize); + } + document.querySelector(":root").style.fontSize = fontSize + "px"; + }; +} diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js index c9793d40f7..102afa6bf1 100644 --- a/src/FromWidgetPostMessageApi.js +++ b/src/FromWidgetPostMessageApi.js @@ -17,7 +17,7 @@ limitations under the License. */ import URL from 'url'; -import dis from './dispatcher'; +import dis from './dispatcher/dispatcher'; import WidgetMessagingEndpoint from './WidgetMessagingEndpoint'; import ActiveWidgetStore from './stores/ActiveWidgetStore'; import {MatrixClientPeg} from "./MatrixClientPeg"; diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 1baa6c8e0c..22c5d48317 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -26,7 +26,7 @@ import Analytics from './Analytics'; import Notifier from './Notifier'; import UserActivity from './UserActivity'; import Presence from './Presence'; -import dis from './dispatcher'; +import dis from './dispatcher/dispatcher'; import DMRoomMap from './utils/DMRoomMap'; import Modal from './Modal'; import * as sdk from './index'; diff --git a/src/Modal.js b/src/Modal.js index de441740f1..9b9f190d58 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -18,7 +18,7 @@ limitations under the License. import React from 'react'; import ReactDOM from 'react-dom'; import Analytics from './Analytics'; -import dis from './dispatcher'; +import dis from './dispatcher/dispatcher'; import {defer} from './utils/promise'; import AsyncWrapper from './AsyncWrapper'; diff --git a/src/Notifier.js b/src/Notifier.js index ec92840998..2ffa92452b 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -21,7 +21,7 @@ import PlatformPeg from './PlatformPeg'; import * as TextForEvent from './TextForEvent'; import Analytics from './Analytics'; import * as Avatar from './Avatar'; -import dis from './dispatcher'; +import dis from './dispatcher/dispatcher'; import * as sdk from './index'; import { _t } from './languageHandler'; import Modal from './Modal'; diff --git a/src/Presence.js b/src/Presence.js index 2fc13a090b..42bca35f96 100644 --- a/src/Presence.js +++ b/src/Presence.js @@ -17,7 +17,7 @@ limitations under the License. */ import {MatrixClientPeg} from "./MatrixClientPeg"; -import dis from "./dispatcher"; +import dis from "./dispatcher/dispatcher"; import Timer from './utils/Timer'; // Time in ms after that a user is considered as unavailable/away diff --git a/src/Registration.js b/src/Registration.js index ca162bac03..32c3d9cc35 100644 --- a/src/Registration.js +++ b/src/Registration.js @@ -20,7 +20,7 @@ limitations under the License. * registration code. */ -import dis from './dispatcher'; +import dis from './dispatcher/dispatcher'; import * as sdk from './index'; import Modal from './Modal'; import { _t } from './languageHandler'; diff --git a/src/Resend.js b/src/Resend.js index 6d6c18cf27..f5f24bffa5 100644 --- a/src/Resend.js +++ b/src/Resend.js @@ -16,7 +16,7 @@ limitations under the License. */ import {MatrixClientPeg} from './MatrixClientPeg'; -import dis from './dispatcher'; +import dis from './dispatcher/dispatcher'; import { EventStatus } from 'matrix-js-sdk'; export default class Resend { diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index 9731e42825..315c2d86f4 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -238,7 +238,7 @@ Example: import {MatrixClientPeg} from './MatrixClientPeg'; import { MatrixEvent } from 'matrix-js-sdk'; -import dis from './dispatcher'; +import dis from './dispatcher/dispatcher'; import WidgetUtils from './utils/WidgetUtils'; import RoomViewStore from './stores/RoomViewStore'; import { _t } from './languageHandler'; diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index fbb9e2eb0e..d81da80e8d 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -21,7 +21,7 @@ limitations under the License. import * as React from 'react'; import {MatrixClientPeg} from './MatrixClientPeg'; -import dis from './dispatcher'; +import dis from './dispatcher/dispatcher'; import * as sdk from './index'; import {_t, _td} from './languageHandler'; import Modal from './Modal'; @@ -41,6 +41,8 @@ import { parseFragment as parseHtml } from "parse5"; import sendBugReport from "./rageshake/submit-rageshake"; import SdkConfig from "./SdkConfig"; import { ensureDMExists } from "./createRoom"; +import { ViewUserPayload } from "./dispatcher/payloads/ViewUserPayload"; +import { Action } from "./dispatcher/actions"; // XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816 interface HTMLInputEvent extends Event { @@ -943,8 +945,10 @@ export const Commands = [ } const member = MatrixClientPeg.get().getRoom(roomId).getMember(userId); - dis.dispatch({ - action: 'view_user', + dis.dispatch({ + action: Action.ViewUser, + // XXX: We should be using a real member object and not assuming what the + // receiver wants. member: member || {userId}, }); return success(); diff --git a/src/UserActivity.js b/src/UserActivity.js index 0d1b4d0cc0..0174aebaf5 100644 --- a/src/UserActivity.js +++ b/src/UserActivity.js @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import dis from './dispatcher'; +import dis from './dispatcher/dispatcher'; import Timer from './utils/Timer'; // important these are larger than the timeouts of timers diff --git a/src/accessibility/KeyboardShortcuts.tsx b/src/accessibility/KeyboardShortcuts.tsx index bcbf3d6810..f527ab4a14 100644 --- a/src/accessibility/KeyboardShortcuts.tsx +++ b/src/accessibility/KeyboardShortcuts.tsx @@ -34,6 +34,7 @@ export enum Categories { CALLS = "Calls", COMPOSER = "Composer", ROOM_LIST = "Room List", + ROOM = "Room", AUTOCOMPLETE = "Autocomplete", } @@ -142,6 +143,34 @@ const shortcuts: Record = { }, ], + [Categories.ROOM]: [ + { + keybinds: [{ + key: Key.PAGE_UP, + }, { + key: Key.PAGE_DOWN, + }], + description: _td("Scroll up/down in the timeline"), + }, { + keybinds: [{ + key: Key.ESCAPE, + }], + description: _td("Dismiss read marker and jump to bottom"), + }, { + keybinds: [{ + modifiers: [Modifiers.SHIFT], + key: Key.PAGE_UP, + }], + description: _td("Jump to oldest unread message"), + }, { + keybinds: [{ + modifiers: [CMD_OR_CTRL, Modifiers.SHIFT], + key: Key.U, + }], + description: _td("Upload a file"), + } + ], + [Categories.ROOM_LIST]: [ { keybinds: [{ @@ -181,13 +210,6 @@ const shortcuts: Record = { [Categories.NAVIGATION]: [ { - keybinds: [{ - key: Key.PAGE_UP, - }, { - key: Key.PAGE_DOWN, - }], - description: _td("Scroll up/down in the timeline"), - }, { keybinds: [{ modifiers: [Modifiers.ALT, Modifiers.SHIFT], key: Key.ARROW_UP, @@ -257,10 +279,11 @@ const shortcuts: Record = { const categoryOrder = [ Categories.COMPOSER, - Categories.CALLS, - Categories.ROOM_LIST, Categories.AUTOCOMPLETE, + Categories.ROOM, + Categories.ROOM_LIST, Categories.NAVIGATION, + Categories.CALLS, ]; interface IModal { diff --git a/src/actions/GroupActions.js b/src/actions/GroupActions.js deleted file mode 100644 index 006c2da5b8..0000000000 --- a/src/actions/GroupActions.js +++ /dev/null @@ -1,34 +0,0 @@ -/* -Copyright 2017 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { asyncAction } from './actionCreators'; - -const GroupActions = {}; - -/** - * Creates an action thunk that will do an asynchronous request to fetch - * the groups to which a user is joined. - * - * @param {MatrixClient} matrixClient the matrix client to query. - * @returns {function} an action thunk that will dispatch actions - * indicating the status of the request. - * @see asyncAction - */ -GroupActions.fetchJoinedGroups = function(matrixClient) { - return asyncAction('GroupActions.fetchJoinedGroups', () => matrixClient.getJoinedGroups()); -}; - -export default GroupActions; diff --git a/src/actions/GroupActions.ts b/src/actions/GroupActions.ts new file mode 100644 index 0000000000..81470d1221 --- /dev/null +++ b/src/actions/GroupActions.ts @@ -0,0 +1,34 @@ +/* +Copyright 2017 New Vector Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { asyncAction } from './actionCreators'; +import { AsyncActionPayload } from "../dispatcher/payloads"; +import { MatrixClient } from "matrix-js-sdk/src/client"; + +export default class GroupActions { + /** + * Creates an action thunk that will do an asynchronous request to fetch + * the groups to which a user is joined. + * + * @param {MatrixClient} matrixClient the matrix client to query. + * @returns {AsyncActionPayload} An async action payload. + * @see asyncAction + */ + public static fetchJoinedGroups(matrixClient: MatrixClient): AsyncActionPayload { + return asyncAction('GroupActions.fetchJoinedGroups', () => matrixClient.getJoinedGroups(), null); + } +} diff --git a/src/actions/MatrixActionCreators.js b/src/actions/MatrixActionCreators.js index c89ec44435..93a4fcf07c 100644 --- a/src/actions/MatrixActionCreators.js +++ b/src/actions/MatrixActionCreators.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import dis from '../dispatcher'; +import dis from '../dispatcher/dispatcher'; // TODO: migrate from sync_state to MatrixActions.sync so that more js-sdk events // become dispatches in the same place. diff --git a/src/actions/RoomListActions.js b/src/actions/RoomListActions.js deleted file mode 100644 index 10a3848dda..0000000000 --- a/src/actions/RoomListActions.js +++ /dev/null @@ -1,145 +0,0 @@ -/* -Copyright 2018 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { asyncAction } from './actionCreators'; -import RoomListStore, {TAG_DM} from '../stores/RoomListStore'; -import Modal from '../Modal'; -import * as Rooms from '../Rooms'; -import { _t } from '../languageHandler'; -import * as sdk from '../index'; - -const RoomListActions = {}; - -/** - * Creates an action thunk that will do an asynchronous request to - * tag room. - * - * @param {MatrixClient} matrixClient the matrix client to set the - * account data on. - * @param {Room} room the room to tag. - * @param {string} oldTag the tag to remove (unless oldTag ==== newTag) - * @param {string} newTag the tag with which to tag the room. - * @param {?number} oldIndex the previous position of the room in the - * list of rooms. - * @param {?number} newIndex the new position of the room in the list - * of rooms. - * @returns {function} an action thunk. - * @see asyncAction - */ -RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex, newIndex) { - let metaData = null; - - // Is the tag ordered manually? - if (newTag && !newTag.match(/^(m\.lowpriority|im\.vector\.fake\.(invite|recent|direct|archived))$/)) { - const lists = RoomListStore.getRoomLists(); - const newList = [...lists[newTag]]; - - newList.sort((a, b) => a.tags[newTag].order - b.tags[newTag].order); - - // If the room was moved "down" (increasing index) in the same list we - // need to use the orders of the tiles with indices shifted by +1 - const offset = ( - newTag === oldTag && oldIndex < newIndex - ) ? 1 : 0; - - const indexBefore = offset + newIndex - 1; - const indexAfter = offset + newIndex; - - const prevOrder = indexBefore <= 0 ? - 0 : newList[indexBefore].tags[newTag].order; - const nextOrder = indexAfter >= newList.length ? - 1 : newList[indexAfter].tags[newTag].order; - - metaData = { - order: (prevOrder + nextOrder) / 2.0, - }; - } - - return asyncAction('RoomListActions.tagRoom', () => { - const promises = []; - const roomId = room.roomId; - - // Evil hack to get DMs behaving - if ((oldTag === undefined && newTag === TAG_DM) || - (oldTag === TAG_DM && newTag === undefined) - ) { - return Rooms.guessAndSetDMRoom( - room, newTag === TAG_DM, - ).catch((err) => { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - console.error("Failed to set direct chat tag " + err); - Modal.createTrackedDialog('Failed to set direct chat tag', '', ErrorDialog, { - title: _t('Failed to set direct chat tag'), - description: ((err && err.message) ? err.message : _t('Operation failed')), - }); - }); - } - - const hasChangedSubLists = oldTag !== newTag; - - // More evilness: We will still be dealing with moving to favourites/low prio, - // but we avoid ever doing a request with TAG_DM. - // - // if we moved lists, remove the old tag - if (oldTag && oldTag !== TAG_DM && - hasChangedSubLists - ) { - const promiseToDelete = matrixClient.deleteRoomTag( - roomId, oldTag, - ).catch(function(err) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - console.error("Failed to remove tag " + oldTag + " from room: " + err); - Modal.createTrackedDialog('Failed to remove tag from room', '', ErrorDialog, { - title: _t('Failed to remove tag %(tagName)s from room', {tagName: oldTag}), - description: ((err && err.message) ? err.message : _t('Operation failed')), - }); - }); - - promises.push(promiseToDelete); - } - - // if we moved lists or the ordering changed, add the new tag - if (newTag && newTag !== TAG_DM && - (hasChangedSubLists || metaData) - ) { - // metaData is the body of the PUT to set the tag, so it must - // at least be an empty object. - metaData = metaData || {}; - - const promiseToAdd = matrixClient.setRoomTag(roomId, newTag, metaData).catch(function(err) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - console.error("Failed to add tag " + newTag + " to room: " + err); - Modal.createTrackedDialog('Failed to add tag to room', '', ErrorDialog, { - title: _t('Failed to add tag %(tagName)s to room', {tagName: newTag}), - description: ((err && err.message) ? err.message : _t('Operation failed')), - }); - - throw err; - }); - - promises.push(promiseToAdd); - } - - return Promise.all(promises); - }, () => { - // For an optimistic update - return { - room, oldTag, newTag, metaData, - }; - }); -}; - -export default RoomListActions; diff --git a/src/actions/RoomListActions.ts b/src/actions/RoomListActions.ts new file mode 100644 index 0000000000..eb9831ec47 --- /dev/null +++ b/src/actions/RoomListActions.ts @@ -0,0 +1,151 @@ +/* +Copyright 2018 New Vector Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { asyncAction } from './actionCreators'; +import RoomListStore, { TAG_DM } from '../stores/RoomListStore'; +import Modal from '../Modal'; +import * as Rooms from '../Rooms'; +import { _t } from '../languageHandler'; +import * as sdk from '../index'; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { AsyncActionPayload } from "../dispatcher/payloads"; + +export default class RoomListActions { + /** + * Creates an action thunk that will do an asynchronous request to + * tag room. + * + * @param {MatrixClient} matrixClient the matrix client to set the + * account data on. + * @param {Room} room the room to tag. + * @param {string} oldTag the tag to remove (unless oldTag ==== newTag) + * @param {string} newTag the tag with which to tag the room. + * @param {?number} oldIndex the previous position of the room in the + * list of rooms. + * @param {?number} newIndex the new position of the room in the list + * of rooms. + * @returns {AsyncActionPayload} an async action payload + * @see asyncAction + */ + public static tagRoom( + matrixClient: MatrixClient, room: Room, + oldTag: string, newTag: string, + oldIndex: number | null, newIndex: number | null, + ): AsyncActionPayload { + let metaData = null; + + // Is the tag ordered manually? + if (newTag && !newTag.match(/^(m\.lowpriority|im\.vector\.fake\.(invite|recent|direct|archived))$/)) { + const lists = RoomListStore.getRoomLists(); + const newList = [...lists[newTag]]; + + newList.sort((a, b) => a.tags[newTag].order - b.tags[newTag].order); + + // If the room was moved "down" (increasing index) in the same list we + // need to use the orders of the tiles with indices shifted by +1 + const offset = ( + newTag === oldTag && oldIndex < newIndex + ) ? 1 : 0; + + const indexBefore = offset + newIndex - 1; + const indexAfter = offset + newIndex; + + const prevOrder = indexBefore <= 0 ? + 0 : newList[indexBefore].tags[newTag].order; + const nextOrder = indexAfter >= newList.length ? + 1 : newList[indexAfter].tags[newTag].order; + + metaData = { + order: (prevOrder + nextOrder) / 2.0, + }; + } + + return asyncAction('RoomListActions.tagRoom', () => { + const promises = []; + const roomId = room.roomId; + + // Evil hack to get DMs behaving + if ((oldTag === undefined && newTag === TAG_DM) || + (oldTag === TAG_DM && newTag === undefined) + ) { + return Rooms.guessAndSetDMRoom( + room, newTag === TAG_DM, + ).catch((err) => { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to set direct chat tag " + err); + Modal.createTrackedDialog('Failed to set direct chat tag', '', ErrorDialog, { + title: _t('Failed to set direct chat tag'), + description: ((err && err.message) ? err.message : _t('Operation failed')), + }); + }); + } + + const hasChangedSubLists = oldTag !== newTag; + + // More evilness: We will still be dealing with moving to favourites/low prio, + // but we avoid ever doing a request with TAG_DM. + // + // if we moved lists, remove the old tag + if (oldTag && oldTag !== TAG_DM && + hasChangedSubLists + ) { + const promiseToDelete = matrixClient.deleteRoomTag( + roomId, oldTag, + ).catch(function (err) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to remove tag " + oldTag + " from room: " + err); + Modal.createTrackedDialog('Failed to remove tag from room', '', ErrorDialog, { + title: _t('Failed to remove tag %(tagName)s from room', {tagName: oldTag}), + description: ((err && err.message) ? err.message : _t('Operation failed')), + }); + }); + + promises.push(promiseToDelete); + } + + // if we moved lists or the ordering changed, add the new tag + if (newTag && newTag !== TAG_DM && + (hasChangedSubLists || metaData) + ) { + // metaData is the body of the PUT to set the tag, so it must + // at least be an empty object. + metaData = metaData || {}; + + const promiseToAdd = matrixClient.setRoomTag(roomId, newTag, metaData).catch(function (err) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to add tag " + newTag + " to room: " + err); + Modal.createTrackedDialog('Failed to add tag to room', '', ErrorDialog, { + title: _t('Failed to add tag %(tagName)s to room', {tagName: newTag}), + description: ((err && err.message) ? err.message : _t('Operation failed')), + }); + + throw err; + }); + + promises.push(promiseToAdd); + } + + return Promise.all(promises); + }, () => { + // For an optimistic update + return { + room, oldTag, newTag, metaData, + }; + }); + } +} diff --git a/src/actions/TagOrderActions.js b/src/actions/TagOrderActions.js deleted file mode 100644 index a257ff16d8..0000000000 --- a/src/actions/TagOrderActions.js +++ /dev/null @@ -1,109 +0,0 @@ -/* -Copyright 2017 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import Analytics from '../Analytics'; -import { asyncAction } from './actionCreators'; -import TagOrderStore from '../stores/TagOrderStore'; - -const TagOrderActions = {}; - -/** - * Creates an action thunk that will do an asynchronous request to - * move a tag in TagOrderStore to destinationIx. - * - * @param {MatrixClient} matrixClient the matrix client to set the - * account data on. - * @param {string} tag the tag to move. - * @param {number} destinationIx the new position of the tag. - * @returns {function} an action thunk that will dispatch actions - * indicating the status of the request. - * @see asyncAction - */ -TagOrderActions.moveTag = function(matrixClient, tag, destinationIx) { - // Only commit tags if the state is ready, i.e. not null - let tags = TagOrderStore.getOrderedTags(); - let removedTags = TagOrderStore.getRemovedTagsAccountData() || []; - if (!tags) { - return; - } - - tags = tags.filter((t) => t !== tag); - tags = [...tags.slice(0, destinationIx), tag, ...tags.slice(destinationIx)]; - - removedTags = removedTags.filter((t) => t !== tag); - - const storeId = TagOrderStore.getStoreId(); - - return asyncAction('TagOrderActions.moveTag', () => { - Analytics.trackEvent('TagOrderActions', 'commitTagOrdering'); - return matrixClient.setAccountData( - 'im.vector.web.tag_ordering', - {tags, removedTags, _storeId: storeId}, - ); - }, () => { - // For an optimistic update - return {tags, removedTags}; - }); -}; - -/** - * Creates an action thunk that will do an asynchronous request to - * label a tag as removed in im.vector.web.tag_ordering account data. - * - * The reason this is implemented with new state `removedTags` is that - * we incrementally and initially populate `tags` with groups that - * have been joined. If we remove a group from `tags`, it will just - * get added (as it looks like a group we've recently joined). - * - * NB: If we ever support adding of tags (which is planned), we should - * take special care to remove the tag from `removedTags` when we add - * it. - * - * @param {MatrixClient} matrixClient the matrix client to set the - * account data on. - * @param {string} tag the tag to remove. - * @returns {function} an action thunk that will dispatch actions - * indicating the status of the request. - * @see asyncAction - */ -TagOrderActions.removeTag = function(matrixClient, tag) { - // Don't change tags, just removedTags - const tags = TagOrderStore.getOrderedTags(); - const removedTags = TagOrderStore.getRemovedTagsAccountData() || []; - - if (removedTags.includes(tag)) { - // Return a thunk that doesn't do anything, we don't even need - // an asynchronous action here, the tag is already removed. - return () => {}; - } - - removedTags.push(tag); - - const storeId = TagOrderStore.getStoreId(); - - return asyncAction('TagOrderActions.removeTag', () => { - Analytics.trackEvent('TagOrderActions', 'removeTag'); - return matrixClient.setAccountData( - 'im.vector.web.tag_ordering', - {tags, removedTags, _storeId: storeId}, - ); - }, () => { - // For an optimistic update - return {removedTags}; - }); -}; - -export default TagOrderActions; diff --git a/src/actions/TagOrderActions.ts b/src/actions/TagOrderActions.ts new file mode 100644 index 0000000000..bf1820d5d1 --- /dev/null +++ b/src/actions/TagOrderActions.ts @@ -0,0 +1,111 @@ +/* +Copyright 2017 New Vector Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import Analytics from '../Analytics'; +import { asyncAction } from './actionCreators'; +import TagOrderStore from '../stores/TagOrderStore'; +import { AsyncActionPayload } from "../dispatcher/payloads"; +import { MatrixClient } from "matrix-js-sdk/src/client"; + +export default class TagOrderActions { + + /** + * Creates an action thunk that will do an asynchronous request to + * move a tag in TagOrderStore to destinationIx. + * + * @param {MatrixClient} matrixClient the matrix client to set the + * account data on. + * @param {string} tag the tag to move. + * @param {number} destinationIx the new position of the tag. + * @returns {AsyncActionPayload} an async action payload that will + * dispatch actions indicating the status of the request. + * @see asyncAction + */ + public static moveTag(matrixClient: MatrixClient, tag: string, destinationIx: number): AsyncActionPayload { + // Only commit tags if the state is ready, i.e. not null + let tags = TagOrderStore.getOrderedTags(); + let removedTags = TagOrderStore.getRemovedTagsAccountData() || []; + if (!tags) { + return; + } + + tags = tags.filter((t) => t !== tag); + tags = [...tags.slice(0, destinationIx), tag, ...tags.slice(destinationIx)]; + + removedTags = removedTags.filter((t) => t !== tag); + + const storeId = TagOrderStore.getStoreId(); + + return asyncAction('TagOrderActions.moveTag', () => { + Analytics.trackEvent('TagOrderActions', 'commitTagOrdering'); + return matrixClient.setAccountData( + 'im.vector.web.tag_ordering', + {tags, removedTags, _storeId: storeId}, + ); + }, () => { + // For an optimistic update + return {tags, removedTags}; + }); + }; + + /** + * Creates an action thunk that will do an asynchronous request to + * label a tag as removed in im.vector.web.tag_ordering account data. + * + * The reason this is implemented with new state `removedTags` is that + * we incrementally and initially populate `tags` with groups that + * have been joined. If we remove a group from `tags`, it will just + * get added (as it looks like a group we've recently joined). + * + * NB: If we ever support adding of tags (which is planned), we should + * take special care to remove the tag from `removedTags` when we add + * it. + * + * @param {MatrixClient} matrixClient the matrix client to set the + * account data on. + * @param {string} tag the tag to remove. + * @returns {function} an async action payload that will dispatch + * actions indicating the status of the request. + * @see asyncAction + */ + public static removeTag(matrixClient: MatrixClient, tag: string): AsyncActionPayload { + // Don't change tags, just removedTags + const tags = TagOrderStore.getOrderedTags(); + const removedTags = TagOrderStore.getRemovedTagsAccountData() || []; + + if (removedTags.includes(tag)) { + // Return a thunk that doesn't do anything, we don't even need + // an asynchronous action here, the tag is already removed. + return new AsyncActionPayload(() => {}); + } + + removedTags.push(tag); + + const storeId = TagOrderStore.getStoreId(); + + return asyncAction('TagOrderActions.removeTag', () => { + Analytics.trackEvent('TagOrderActions', 'removeTag'); + return matrixClient.setAccountData( + 'im.vector.web.tag_ordering', + {tags, removedTags, _storeId: storeId}, + ); + }, () => { + // For an optimistic update + return {removedTags}; + }); + } +} diff --git a/src/actions/actionCreators.js b/src/actions/actionCreators.ts similarity index 76% rename from src/actions/actionCreators.js rename to src/actions/actionCreators.ts index 967ce609e7..c789e3cd07 100644 --- a/src/actions/actionCreators.js +++ b/src/actions/actionCreators.ts @@ -1,5 +1,6 @@ /* Copyright 2017 New Vector Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,6 +15,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { AsyncActionPayload } from "../dispatcher/payloads"; + /** * Create an action thunk that will dispatch actions indicating the current * status of the Promise returned by fn. @@ -25,9 +28,9 @@ limitations under the License. * @param {function?} pendingFn a function that returns an object to assign * to the `request` key of the ${id}.pending * payload. - * @returns {function} an action thunk - a function that uses its single - * argument as a dispatch function to dispatch the - * following actions: + * @returns {AsyncActionPayload} an async action payload. Includes a function + * that uses its single argument as a dispatch function + * to dispatch the following actions: * `${id}.pending` and either * `${id}.success` or * `${id}.failure`. @@ -41,12 +44,11 @@ limitations under the License. * result is the result of the promise returned by * `fn`. */ -export function asyncAction(id, fn, pendingFn) { - return (dispatch) => { +export function asyncAction(id: string, fn: () => Promise, pendingFn: () => any | null): AsyncActionPayload { + const helper = (dispatch) => { dispatch({ action: id + '.pending', - request: - typeof pendingFn === 'function' ? pendingFn() : undefined, + request: typeof pendingFn === 'function' ? pendingFn() : undefined, }); fn().then((result) => { dispatch({action: id + '.success', result}); @@ -54,4 +56,5 @@ export function asyncAction(id, fn, pendingFn) { dispatch({action: id + '.failure', err}); }); }; + return new AsyncActionPayload(helper); } diff --git a/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js b/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js index 120b086ef6..ec4b88f759 100644 --- a/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js +++ b/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js @@ -17,11 +17,12 @@ limitations under the License. import React from 'react'; import * as sdk from '../../../../index'; import PropTypes from 'prop-types'; -import dis from "../../../../dispatcher"; +import dis from "../../../../dispatcher/dispatcher"; import { _t } from '../../../../languageHandler'; import SettingsStore, {SettingLevel} from "../../../../settings/SettingsStore"; import EventIndexPeg from "../../../../indexing/EventIndexPeg"; +import {Action} from "../../../../dispatcher/actions"; /* * Allows the user to disable the Event Index. @@ -47,7 +48,7 @@ export default class DisableEventIndexDialog extends React.Component { await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, false); await EventIndexPeg.deleteEventIndex(); this.props.onFinished(); - dis.dispatch({ action: 'view_user_settings' }); + dis.fire(Action.ViewUserSettings); } render() { diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index e4e39400f6..532b2f960f 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -15,17 +15,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +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 { scorePassword } from '../../../../utils/PasswordScorer'; -import { _t } from '../../../../languageHandler'; +import {_t, _td} from '../../../../languageHandler'; import { accessSecretStorage } from '../../../../CrossSigningManager'; import SettingsStore from '../../../../settings/SettingsStore'; import AccessibleButton from "../../../../components/views/elements/AccessibleButton"; import {copyNode} from "../../../../utils/strings"; +import PassphraseField from "../../../../components/views/auth/PassphraseField"; const PHASE_PASSPHRASE = 0; const PHASE_PASSPHRASE_CONFIRM = 1; @@ -36,7 +36,6 @@ const PHASE_DONE = 5; const PHASE_OPTOUT_CONFIRM = 6; const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc. -const PASSPHRASE_FEEDBACK_DELAY = 500; // How long after keystroke to offer passphrase feedback, ms. /* * Walks the user through the process of creating an e2e key backup @@ -52,17 +51,18 @@ export default class CreateKeyBackupDialog extends React.PureComponent { this._recoveryKeyNode = null; this._keyBackupInfo = null; - this._setZxcvbnResultTimeout = null; this.state = { secureSecretStorage: null, phase: PHASE_PASSPHRASE, passPhrase: '', + passPhraseValid: false, passPhraseConfirm: '', copied: false, downloaded: false, - zxcvbnResult: null, }; + + this._passphraseField = createRef(); } async componentDidMount() { @@ -81,12 +81,6 @@ export default class CreateKeyBackupDialog extends React.PureComponent { } } - componentWillUnmount() { - if (this._setZxcvbnResultTimeout !== null) { - clearTimeout(this._setZxcvbnResultTimeout); - } - } - _collectRecoveryKeyNode = (n) => { this._recoveryKeyNode = n; } @@ -180,22 +174,16 @@ export default class CreateKeyBackupDialog extends React.PureComponent { _onPassPhraseNextClick = async (e) => { e.preventDefault(); + if (!this._passphraseField.current) return; // unmounting - // If we're waiting for the timeout before updating the result at this point, - // skip ahead and do it now, otherwise we'll deny the attempt to proceed - // even if the user entered a valid passphrase - if (this._setZxcvbnResultTimeout !== null) { - clearTimeout(this._setZxcvbnResultTimeout); - this._setZxcvbnResultTimeout = null; - await new Promise((resolve) => { - this.setState({ - zxcvbnResult: scorePassword(this.state.passPhrase), - }, resolve); - }); - } - if (this._passPhraseIsValid()) { - this.setState({phase: PHASE_PASSPHRASE_CONFIRM}); + 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}); }; _onPassPhraseConfirmNextClick = async (e) => { @@ -214,9 +202,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent { _onSetAgainClick = () => { this.setState({ passPhrase: '', + passPhraseValid: false, passPhraseConfirm: '', phase: PHASE_PASSPHRASE, - zxcvbnResult: null, }); } @@ -226,23 +214,16 @@ export default class CreateKeyBackupDialog extends React.PureComponent { }); } + _onPassPhraseValidate = (result) => { + this.setState({ + passPhraseValid: result.valid, + }); + }; + _onPassPhraseChange = (e) => { this.setState({ passPhrase: e.target.value, }); - - if (this._setZxcvbnResultTimeout !== null) { - clearTimeout(this._setZxcvbnResultTimeout); - } - this._setZxcvbnResultTimeout = setTimeout(() => { - this._setZxcvbnResultTimeout = null; - this.setState({ - // precompute this and keep it in state: zxcvbn is fast but - // we use it in a couple of different places so no point recomputing - // it unnecessarily. - zxcvbnResult: scorePassword(this.state.passPhrase), - }); - }, PASSPHRASE_FEEDBACK_DELAY); } _onPassPhraseConfirmChange = (e) => { @@ -251,35 +232,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent { }); } - _passPhraseIsValid() { - return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE; - } - _renderPhasePassPhrase() { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - let strengthMeter; - let helpText; - if (this.state.zxcvbnResult) { - if (this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE) { - helpText = _t("Great! This recovery passphrase looks strong enough."); - } else { - const suggestions = []; - for (let i = 0; i < this.state.zxcvbnResult.feedback.suggestions.length; ++i) { - suggestions.push(
{this.state.zxcvbnResult.feedback.suggestions[i]}
); - } - const suggestionBlock =
{suggestions.length > 0 ? suggestions : _t("Keep going...")}
; - - helpText =
- {this.state.zxcvbnResult.feedback.warning} - {suggestionBlock} -
; - } - strengthMeter =
- -
; - } - return

{_t( "Warning: You should only set up key backup from a trusted computer.", {}, @@ -293,17 +248,19 @@ export default class CreateKeyBackupDialog extends React.PureComponent {

- -
- {strengthMeter} - {helpText} -
@@ -311,7 +268,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent { primaryButton={_t('Next')} onPrimaryButtonClick={this._onPassPhraseNextClick} hasCancel={false} - disabled={!this._passPhraseIsValid()} + disabled={!this.state.passPhraseValid} />
diff --git a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js index 9e2264a960..74552a5c08 100644 --- a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js +++ b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js @@ -19,9 +19,10 @@ import React from "react"; import PropTypes from "prop-types"; import * as sdk from "../../../../index"; import {MatrixClientPeg} from '../../../../MatrixClientPeg'; -import dis from "../../../../dispatcher"; +import dis from "../../../../dispatcher/dispatcher"; import { _t } from "../../../../languageHandler"; import Modal from "../../../../Modal"; +import {Action} from "../../../../dispatcher/actions"; export default class NewRecoveryMethodDialog extends React.PureComponent { static propTypes = { @@ -36,7 +37,7 @@ export default class NewRecoveryMethodDialog extends React.PureComponent { onGoToSettingsClick = () => { this.props.onFinished(); - dis.dispatch({ action: 'view_user_settings' }); + dis.fire(Action.ViewUserSettings); } onSetupClick = async () => { diff --git a/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js b/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js index c5222dafd5..cda353e717 100644 --- a/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js +++ b/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js @@ -18,9 +18,10 @@ limitations under the License. import React from "react"; import PropTypes from "prop-types"; import * as sdk from "../../../../index"; -import dis from "../../../../dispatcher"; +import dis from "../../../../dispatcher/dispatcher"; import { _t } from "../../../../languageHandler"; import Modal from "../../../../Modal"; +import {Action} from "../../../../dispatcher/actions"; export default class RecoveryMethodRemovedDialog extends React.PureComponent { static propTypes = { @@ -29,7 +30,7 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent { onGoToSettingsClick = () => { this.props.onFinished(); - dis.dispatch({ action: 'view_user_settings' }); + dis.fire(Action.ViewUserSettings); } onSetupClick = () => { diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index c24623e30e..12b71206d0 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -15,17 +15,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, {createRef} from 'react'; import PropTypes from 'prop-types'; import * as sdk from '../../../../index'; import {MatrixClientPeg} from '../../../../MatrixClientPeg'; -import { scorePassword } from '../../../../utils/PasswordScorer'; import FileSaver from 'file-saver'; -import { _t } from '../../../../languageHandler'; +import {_t, _td} from '../../../../languageHandler'; import Modal from '../../../../Modal'; import { promptForBackupPassphrase } from '../../../../CrossSigningManager'; import {copyNode} from "../../../../utils/strings"; import {SSOAuthEntry} from "../../../../components/views/auth/InteractiveAuthEntryComponents"; +import PassphraseField from "../../../../components/views/auth/PassphraseField"; const PHASE_LOADING = 0; const PHASE_LOADERROR = 1; @@ -39,7 +39,6 @@ const PHASE_DONE = 8; const PHASE_CONFIRM_SKIP = 9; const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc. -const PASSPHRASE_FEEDBACK_DELAY = 500; // How long after keystroke to offer passphrase feedback, ms. /* * Walks the user through the process of creating a passphrase to guard Secure @@ -62,16 +61,15 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this._recoveryKey = null; this._recoveryKeyNode = null; - this._setZxcvbnResultTimeout = null; this._backupKey = null; this.state = { phase: PHASE_LOADING, passPhrase: '', + passPhraseValid: false, passPhraseConfirm: '', copied: false, downloaded: false, - zxcvbnResult: null, backupInfo: null, backupSigStatus: null, // does the server offer a UI auth flow with just m.login.password @@ -83,6 +81,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent { useKeyBackup: true, }; + this._passphraseField = createRef(); + this._fetchBackupInfo(); if (this.state.accountPassword) { // If we have an account password in memory, let's simplify and @@ -99,9 +99,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { componentWillUnmount() { MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatusChange); - if (this._setZxcvbnResultTimeout !== null) { - clearTimeout(this._setZxcvbnResultTimeout); - } } async _fetchBackupInfo() { @@ -364,22 +361,16 @@ export default class CreateSecretStorageDialog extends React.PureComponent { _onPassPhraseNextClick = async (e) => { e.preventDefault(); + if (!this._passphraseField.current) return; // unmounting - // If we're waiting for the timeout before updating the result at this point, - // skip ahead and do it now, otherwise we'll deny the attempt to proceed - // even if the user entered a valid passphrase - if (this._setZxcvbnResultTimeout !== null) { - clearTimeout(this._setZxcvbnResultTimeout); - this._setZxcvbnResultTimeout = null; - await new Promise((resolve) => { - this.setState({ - zxcvbnResult: scorePassword(this.state.passPhrase), - }, resolve); - }); - } - if (this._passPhraseIsValid()) { - this.setState({phase: PHASE_PASSPHRASE_CONFIRM}); + 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}); }; _onPassPhraseConfirmNextClick = async (e) => { @@ -399,9 +390,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent { _onSetAgainClick = () => { this.setState({ passPhrase: '', + passPhraseValid: false, passPhraseConfirm: '', phase: PHASE_PASSPHRASE, - zxcvbnResult: null, }); } @@ -411,23 +402,16 @@ export default class CreateSecretStorageDialog extends React.PureComponent { }); } + _onPassPhraseValidate = (result) => { + this.setState({ + passPhraseValid: result.valid, + }); + }; + _onPassPhraseChange = (e) => { this.setState({ passPhrase: e.target.value, }); - - if (this._setZxcvbnResultTimeout !== null) { - clearTimeout(this._setZxcvbnResultTimeout); - } - this._setZxcvbnResultTimeout = setTimeout(() => { - this._setZxcvbnResultTimeout = null; - this.setState({ - // precompute this and keep it in state: zxcvbn is fast but - // we use it in a couple of different places so no point recomputing - // it unnecessarily. - zxcvbnResult: scorePassword(this.state.passPhrase), - }); - }, PASSPHRASE_FEEDBACK_DELAY); } _onPassPhraseConfirmChange = (e) => { @@ -436,10 +420,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { }); } - _passPhraseIsValid() { - return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE; - } - _onAccountPasswordChange = (e) => { this.setState({ accountPassword: e.target.value, @@ -502,37 +482,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent { _renderPhasePassPhrase() { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - const Field = sdk.getComponent('views.elements.Field'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch'); - let strengthMeter; - let helpText; - if (this.state.zxcvbnResult) { - if (this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE) { - helpText = _t("Great! This recovery passphrase looks strong enough."); - } else { - // We take the warning from zxcvbn or failing that, the first - // suggestion. In practice The first is generally the most relevant - // and it's probably better to present the user with one thing to - // improve about their password than a whole collection - it can - // spit out a warning and multiple suggestions which starts getting - // very information-dense. - const suggestion = ( - this.state.zxcvbnResult.feedback.warning || - this.state.zxcvbnResult.feedback.suggestions[0] - ); - const suggestionBlock =
{suggestion || _t("Keep going...")}
; - - helpText =
- {suggestionBlock} -
; - } - strengthMeter =
- -
; - } - return

{_t( "Set a recovery passphrase to secure encrypted information and recover it if you log out. " + @@ -540,19 +492,19 @@ export default class CreateSecretStorageDialog extends React.PureComponent { )}

- -
- {strengthMeter} - {helpText} -
.": "Mitte midagi ei kuvata? Kõik Matrix'i kliendid ei toeta veel interaktiivset verifitseerimist. .", + "Waiting for %(userId)s to confirm...": "Ootan kinnitust kasutajalt %(userId)s…", + "Skip": "Jäta vahele", + "Token incorrect": "Vigane tunnusluba" } diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index e8eef891be..ae08572eb2 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -1282,7 +1282,7 @@ "Premium hosting for organisations Learn more": "Premium-ylläpitoa organisaatioille. Lue lisää", "Other": "Muut", "Find other public servers or use a custom server": "Etsi muita julkisia palvelimia tai käytä mukautettua palvelinta", - "Please install Chrome, Firefox, or Safari for the best experience.": "Parhaan käyttökokemuksen saa Chromella, Firefoxilla tai Safarilla.", + "Please install Chrome, Firefox, or Safari for the best experience.": "Asenna Chrome, Firefox tai Safari, jotta kaikki toimii parhaiten.", "

HTML for your community's page

\n

\n Use the long description to introduce new members to the community, or distribute\n some important links\n

\n

\n You can even use 'img' tags\n

\n": "

HTML-kuvaus yhteisösi etusivulle

\n

\n Käytä pitkää kuvausta esitelläksesi yhteisöäsi uusille jäsenille tai jakaaksesi tärkeitä\n linkkejä\n

\n

\n Voit jopa käyttää 'img'-tageja\n

\n", "Unable to join community": "Yhteisöön liittyminen epäonnistui", "You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "Olet tämän yhteisön ylläpitäjä. Et voi liittyä uudelleen ilman kutsua toiselta ylläpitäjältä.", @@ -2167,5 +2167,27 @@ "Sends a message as html, without interpreting it as markdown": "Lähettää viestin HTML-muodossa, tulkitsematta sitä Markdowniksi", "Please supply a widget URL or embed code": "Anna sovelman osoite tai upotettava koodinpätkä", "You signed in to a new session without verifying it:": "Olet kirjautunut uuteen istuntoon varmentamatta sitä:", - "Verify your other session using one of the options below.": "Varmenna toinen istuntosi käyttämällä yhtä seuraavista tavoista." + "Verify your other session using one of the options below.": "Varmenna toinen istuntosi käyttämällä yhtä seuraavista tavoista.", + "Click the button below to confirm deleting these sessions.|other": "Napsauta alla olevaa painiketta vahvistaaksesi näiden istuntojen poistamisen.", + "Click the button below to confirm deleting these sessions.|one": "Napsauta alla olevaa painiketta vahvistaaksesi tämän istunnon poistamisen.", + "Backup has a signature from unknown session with ID %(deviceId)s": "Varmuuskopiossa on allekirjoitus tuntemattomasta istunnosta tunnuksella %(deviceId)s", + "Error downloading theme information.": "Virhe ladattaessa teematietoa.", + "Waiting for you to accept on your other session…": "Odotetaan, että hyväksyt toisen istunnon…", + "Almost there! Is your other session showing the same shield?": "Melkein valmista! Näyttääkö toinen istuntosi saman kilven?", + "Almost there! Is %(displayName)s showing the same shield?": "Melkein valmista! Näyttääkö %(displayName)s saman kilven?", + "Message deleted": "Viesti poistettu", + "Message deleted by %(name)s": "%(name)s poisti viestin", + "QR Code": "QR-koodi", + "To continue, use Single Sign On to prove your identity.": "Todista henkilöllisyytesi kertakirjautumisen avulla jatkaaksesi.", + "If they don't match, the security of your communication may be compromised.": "Jos ne eivät täsmää, viestinnän turvallisuus saattaa olla vaarantunut.", + "This session, or the other session": "Tämä tai toinen istunto", + "We recommend you change your password and recovery key in Settings immediately": "Suosittelemme, että vaihdat salasanasi ja palautusavaimesi asetuksissa välittömästi", + "Restoring keys from backup": "Palautetaan avaimia varmuuskopiosta", + "Fetching keys from server...": "Noudetaan avaimia palvelimelta...", + "%(completed)s of %(total)s keys restored": "%(completed)s / %(total)s avainta palautettu", + "Keys restored": "Avaimet palautettu", + "Successfully restored %(sessionCount)s keys": "%(sessionCount)s avaimen palautus onnistui", + "This requires the latest Riot on your other devices:": "Tämä vaatii uusimman Riotin muilla laitteillasi:", + "Currently indexing: %(currentRoom)s": "Indeksoidaan huonetta: %(currentRoom)s", + "Jump to oldest unread message": "Siirry vanhimpaan lukemattomaan viestiin" } diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 3fac2168a2..c231769f27 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2431,5 +2431,9 @@ "Confirm to continue": "Confirmer pour continuer", "Click the button below to confirm your identity.": "Cliquez sur le bouton ci-dessous pour confirmer votre identité.", "Confirm encryption setup": "Confirmer la configuration du chiffrement", - "Click the button below to confirm setting up encryption.": "Cliquez sur le bouton ci-dessous pour confirmer la configuration du chiffrement." + "Click the button below to confirm setting up encryption.": "Cliquez sur le bouton ci-dessous pour confirmer la configuration du chiffrement.", + "QR Code": "Code QR", + "Dismiss read marker and jump to bottom": "Ignorer le signet de lecture et aller en bas", + "Jump to oldest unread message": "Aller au plus vieux message non lu", + "Upload a file": "Envoyer un fichier" } diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index 346dda09f2..3328292be0 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -698,9 +698,9 @@ "Failed to remove tag %(tagName)s from room": "Fallo ao eliminar a etiqueta %(tagName)s da sala", "Failed to add tag %(tagName)s to room": "Fallo ao engadir a etiqueta %(tagName)s a sala", "Key request sent.": "Petición de chave enviada.", - "Flair": "Aura", - "Showing flair for these communities:": "Mostrar a aura para estas comunidades:", - "Display your community flair in rooms configured to show it.": "Mostrar a aura da súa comunidade nas salas configuradas para que a mostren.", + "Flair": "Popularidade", + "Showing flair for these communities:": "Mostrar a popularidade destas comunidades:", + "Display your community flair in rooms configured to show it.": "Mostrar a popularidade da túa comunidade nas salas configuradas para que a mostren.", "Did you know: you can use communities to filter your Riot.im experience!": "Sabía que pode utilizar as comunidades para mellorar a súa experiencia con Riot.im!", "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "Para establecer un filtro, arrastre un avatar da comunidade sobre o panel de filtros na parte esquerda da pantalla. Pode pulsar nun avatar no panel de filtrado en calquera momento para ver só salas e xente asociada a esa comunidade.", "Deops user with given id": "Degradar o usuario con esa ID", @@ -822,7 +822,7 @@ "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Os informes de depuración conteñen datos de utilización do aplicativo como o seu nome de usuario, os IDs ou alcumes de salas e grupos que vostede visitou e os nomes de usuarios doutras usuarias. Non conteñen mensaxes.", "Unhide Preview": "Desagochar a vista previa", "Unable to join network": "Non se puido conectar a rede", - "You might have configured them in a client other than Riot. You cannot tune them in Riot but they still apply": "Pode que os configurase nun cliente diferente de Riot. Non pode establecelos desde Riot pero aínda así aplicaranse", + "You might have configured them in a client other than Riot. You cannot tune them in Riot but they still apply": "Pode que os configurases nun cliente diferente de Riot. Non podes establecelos desde Riot pero aínda así aplicaranse", "Sorry, your browser is not able to run Riot.": "Desculpe, o seu navegador non pode executar Riot.", "Uploaded on %(date)s by %(user)s": "Subido a %(date)s por %(user)s", "Messages in group chats": "Mensaxes en grupos de chat", @@ -919,5 +919,23 @@ "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.": "A súa mensaxe non foi enviada porque este servidor acadou o Límite Mensual de Usuaria Activa. Por favor contacte coa administración do servizo para continuar utilizando o servizo.", "Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.": "A súa mensaxe non foi enviada porque o servidor superou o límite de recursos. Por favor contacte coa administración do servizo para continuar utilizando o servizo.", "Legal": "Legal", - "Please contact your service administrator to continue using this service.": "Por favor contacte coa administración do servizo para continuar utilizando o servizo." + "Please contact your service administrator to continue using this service.": "Por favor contacte coa administración do servizo para continuar utilizando o servizo.", + "Use Single Sign On to continue": "Usar Single Sign On para continuar", + "Confirm adding this email address by using Single Sign On to prove your identity.": "Confirma que queres engadir este email usando Single Sign On como proba de identidade.", + "Single Sign On": "Single Sign On", + "Confirm adding email": "Confirma novo email", + "Click the button below to confirm adding this email address.": "Preme no botón inferior para confirmar que queres engadir o email.", + "Confirm": "Confirmar", + "Add Email Address": "Engadir email", + "Confirm adding this phone number by using Single Sign On to prove your identity.": "Confirma que queres engadir este teléfono usando Single Sign On como proba de identidade.", + "Confirm adding phone number": "Confirma a adición do teléfono", + "Click the button below to confirm adding this phone number.": "Preme no botón inferior para confirmar que engades este número.", + "Add Phone Number": "Engadir novo Número", + "The version of Riot": "A versión de Riot", + "Whether or not you're logged in (we don't record your username)": "Se estás conectada ou non (non rexistramos o teu nome de usuaria)", + "Whether you're using Riot on a device where touch is the primary input mechanism": "Se estás conectada utilizando Riot nun dispositivo maiormente táctil", + "Whether you're using Riot as an installed Progressive Web App": "Se estás a usar Riot como unha Progressive Web App instalada", + "Your user agent": "User Agent do navegador", + "The information being sent to us to help make Riot better includes:": "Información que nos envías para mellorar Riot inclúe:", + "Please install Chrome, Firefox, or Safari for the best experience.": "Instala Chrome, Firefox, ou Safari para ter unha mellor experiencia." } diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index afad390192..17254a77db 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -2409,5 +2409,20 @@ "Successfully restored %(sessionCount)s keys": "Kulcsok (%(sessionCount)s) sikeresen visszaállítva", "You signed in to a new session without verifying it:": "Ellenőrzés nélkül jelentkeztél be egy új munkamenetbe:", "Verify your other session using one of the options below.": "Ellenőrizd a másik munkameneted a lenti lehetőségek egyikével.", - "Invite someone using their name, username (like ), email address or share this room.": "Hívj meg valakit a nevével, felhasználónevével (mint ), e-mail címével vagy oszd meg ezt a szobát." + "Invite someone using their name, username (like ), email address or share this room.": "Hívj meg valakit a nevével, felhasználónevével (mint ), e-mail címével vagy oszd meg ezt a szobát.", + "Opens chat with the given user": "Beszélgetés megnyitása a megadott felhasználóval", + "Sends a message to the given user": "Üzenet küldése a megadott felhasználónak", + "Waiting for your other session to verify…": "A másik munkameneted ellenőrzésére várunk…", + "You've successfully verified your device!": "Sikeresen ellenőrizted az eszközödet!", + "Message deleted": "Üzenet törölve", + "Message deleted by %(name)s": "Üzenetet törölte: %(name)s", + "QR Code": "QR kód", + "To continue, use Single Sign On to prove your identity.": "A folytatáshoz a személyazonosságod megerősítéséhez használd az egyszeri bejelentkezést.", + "Confirm to continue": "Erősítsd meg a továbblépéshez", + "Click the button below to confirm your identity.": "A személyazonosságod megerősítéséhez kattints az alábbi gombra.", + "Confirm encryption setup": "Erősítsd meg a titkosítási beállításokat", + "Click the button below to confirm setting up encryption.": "Az alábbi gomb megnyomásával erősítsd meg, hogy megadod a titkosítási beállításokat.", + "Dismiss read marker and jump to bottom": "Az olvasottak jel eltűntetése és ugrás a végére", + "Jump to oldest unread message": "A legrégebbi olvasatlan üzenetre ugrás", + "Upload a file": "Fájl feltöltése" } diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index e79db1b061..014d6015b3 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -2426,5 +2426,9 @@ "Confirm to continue": "Conferma per continuare", "Click the button below to confirm your identity.": "Clicca il pulsante sotto per confermare la tua identità.", "Confirm encryption setup": "Conferma impostazione cifratura", - "Click the button below to confirm setting up encryption.": "Clicca il pulsante sotto per confermare l'impostazione della cifratura." + "Click the button below to confirm setting up encryption.": "Clicca il pulsante sotto per confermare l'impostazione della cifratura.", + "QR Code": "Codice QR", + "Dismiss read marker and jump to bottom": "Scarta il segno di lettura e salta alla fine", + "Jump to oldest unread message": "Salta al messaggio non letto più vecchio", + "Upload a file": "Invia un file" } diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json index fbecaa4845..e85732ed82 100644 --- a/src/i18n/strings/sk.json +++ b/src/i18n/strings/sk.json @@ -628,7 +628,7 @@ "Export room keys": "Exportovať kľúče miestností", "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "Tento proces vás prevedie exportom kľúčov určených na dešifrovanie správ, ktoré ste dostali v šifrovaných miestnostiach do lokálneho súboru. Tieto kľúče zo súboru môžete neskôr importovať do iného Matrix klienta, aby ste v ňom mohli dešifrovať vaše šifrované správy.", "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "Tento súbor umožní komukoľvek, k to má ku nemu prístup dešifrovať všetky vami viditeľné šifrované správy, mali by ste teda byť opatrní a tento súbor si bezpečne uchovať. Aby bolo toto pre vás jednoduchšie, nižšie zadajte heslo, ktorým budú údaje v súbore zašifrované. Importovať údaje zo súboru bude možné len po zadaní tohoto istého hesla.", - "Enter passphrase": "Zadajte heslo", + "Enter passphrase": "Zadajte (dlhé) heslo", "Confirm passphrase": "Potvrďte heslo", "Export": "Exportovať", "Import room keys": "Importovať kľúče miestností", @@ -1519,5 +1519,17 @@ "Whether you're using Riot on a device where touch is the primary input mechanism": "Či používate Riot na zariadení, ktorého hlavným vstupným mechanizmom je dotyk (mobil, tablet,...)", "Whether you're using Riot as an installed Progressive Web App": "Či používate Riot ako nainštalovanú Progresívnu Webovú Aplikáciu", "Your user agent": "Identifikátor vášho prehliadača", - "The information being sent to us to help make Riot better includes:": "Informácie, ktoré nám posielate, aby sme zlepšili Riot, zahŕňajú:" + "The information being sent to us to help make Riot better includes:": "Informácie, ktoré nám posielate, aby sme zlepšili Riot, zahŕňajú:", + "There are unknown sessions in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "V miestnosti je neznáma relácia: pokiaľ budete pokračovať bez jej overenia, bude schopná odpočúvať váš hovor.", + "Review Sessions": "Overiť reláciu", + "If you cancel now, you won't complete verifying the other user.": "Pokiaľ teraz proces zrušíte, nedokončíte overenie druhého používateľa.", + "If you cancel now, you won't complete verifying your other session.": "Pokiaľ teraz proces zrušíte, nedokončíte overenie vašej druhej relácie.", + "If you cancel now, you won't complete your operation.": "Pokiaľ teraz proces zrušíte, nedokončíte ho.", + "Cancel entering passphrase?": "Zrušiť zadávanie (dlhého) hesla.", + "Setting up keys": "Príprava kľúčov", + "Verify this session": "Overiť túto reláciu", + "Keep recovery passphrase in memory for this session": "Ponechať (dlhé) heslo pre obnovu zálohy v pamäti pre túto reláciu", + "Enter recovery passphrase": "Zadajte (dlhé) heslo pre obnovu zálohy", + "Unable to access secret storage. Please verify that you entered the correct recovery passphrase.": "Nemožno sa dostať do tajného úložiska. Prosím, overte, že ste zadali správne (dlhé) heslo pre obnovu zálohy.", + "Access your secure message history and your cross-signing identity for verifying other sessions by entering your recovery passphrase.": "Získajte prístup k vašej zabezpečenej histórií správ a vašemu krížom-podpísanej identite na potvrdenie iných relácií zadaním vášho (dlhého) hesla na obnovu zálohy." } diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index ee2781f445..7058ad67b0 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -2408,5 +2408,21 @@ "or another cross-signing capable Matrix client": "ose një tjetër klient Matrix i aftë për cross-signing), email address or share this room.": "Ftoni dikë duke përdorur emrin e tij, emrin e përdoruesit (bie fjala, ), adresën email ose duke ndarë me të këtë dhomë.", + "Confirm encryption setup": "Ripohoni ujdisje fshehtëzimi", + "Click the button below to confirm setting up encryption.": "Klikoni mbi butonin më poshtë që të ripohoni ujdisjen e fshehtëzimit.", + "Dismiss read marker and jump to bottom": "Mos merr parasysh piketë leximi dhe hidhu te fundi", + "Jump to oldest unread message": "Hidhu te mesazhi më i vjetër i palexuar", + "Upload a file": "Ngarkoni një kartelë" } diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json index dc3cc6d176..481167a645 100644 --- a/src/i18n/strings/zh_Hans.json +++ b/src/i18n/strings/zh_Hans.json @@ -1468,5 +1468,31 @@ "%(senderName)s placed a video call. (not supported by this browser)": "%(senderName)s 发起了视频通话。(此浏览器不支持)", "%(senderName)s removed the rule banning users matching %(glob)s": "%(senderName)s 移除了禁止匹配 %(glob)s 的用户的规则", "%(senderName)s removed the rule banning servers matching %(glob)s": "%(senderName)s 移除了禁止匹配 %(glob)s 的服务器的规则", - "%(senderName)s removed a ban rule matching %(glob)s": "%(senderName)s 移除了禁止匹配 %(glob)s 的规则" + "%(senderName)s removed a ban rule matching %(glob)s": "%(senderName)s 移除了禁止匹配 %(glob)s 的规则", + "%(senderName)s removed the rule banning rooms matching %(glob)s": "%(senderName)s 删除了禁止聊天室匹配%(glob)s的规则", + "%(senderName)s updated an invalid ban rule": "%(senderName)s 更新了一个无效的禁止规则", + "%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s": "%(senderName)s 更新了由于%(reason)s 而禁止用户匹配%(glob)s的规则", + "%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s": "%(senderName)s 更新了由于%(reason)s而禁止聊天室匹配%(glob)s的规则", + "%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s": "%(senderName)s 更新了由于%(reason)s而禁止服务器匹配%(glob)s的规则", + "%(senderName)s updated a ban rule matching %(glob)s for %(reason)s": "%(senderName)s 更新了由于%(reason)s而禁止匹配%(glob)s的规则", + "%(senderName)s created a rule banning users matching %(glob)s for %(reason)s": "%(senderName)s 创建了因为%(reason)s而禁止用户匹配%(glob)s的规则", + "%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s": "%(senderName)s 创建了由于%(reason)s而禁止聊天室匹配%(glob)s的规则", + "%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s": "%(senderName)s 创建了由于%(reason)s而禁止服务器匹配%(glob)s的规则", + "%(senderName)s created a ban rule matching %(glob)s for %(reason)s": "%(senderName)s 创建了由于%(reason)s而禁止匹配%(glob)s的股则", + "%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s 更改了一个由于%(reason)s而禁止用户%(oldGlob)s跟%(newGlob)s匹配的规则", + "%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s更改了一个由于%(reason)s而禁止聊天室%(oldGlob)s跟%(newGlob)s匹配的规则", + "%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s 更新了一个由于%(reason)s而禁止服务器%(oldGlob)s跟%(newGlob)s匹配的规则", + "%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s 更新了一个由于%(reason)s而禁止%(oldGlob)s跟%(newGlob)s匹配的规则", + "You signed in to a new session without verifying it:": "您登陆了未经过验证的新会话:", + "Verify your other session using one of the options below.": "使用以下选项之一验证您的其他会话。", + "%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s(%(userId)s)登陆到未验证的新会话:", + "Ask this user to verify their session, or manually verify it below.": "要求该用户验证其会话,或在下面手动进行验证。", + "Not Trusted": "不可信任", + "Manually Verify by Text": "手动验证文字", + "Interactively verify by Emoji": "通过表情符号进行交互式验证", + "Done": "完成", + "Cannot reach homeserver": "不可连接到主服务器", + "Ensure you have a stable internet connection, or get in touch with the server admin": "确保您的网络连接稳定,或与服务器管理员联系", + "Ask your Riot admin to check your config for incorrect or duplicate entries.": "跟您的Riot管理员确认您的配置不正确或重复的条目。", + "Cannot reach identity server": "不可连接到身份服务器" } diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 0ca6cfc586..4995eeccb0 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2430,5 +2430,9 @@ "Confirm to continue": "確認以繼續", "Click the button below to confirm your identity.": "點擊下方按鈕以確認您的身份。", "Confirm encryption setup": "確認加密設定", - "Click the button below to confirm setting up encryption.": "點擊下方按鈕以確認設定加密。" + "Click the button below to confirm setting up encryption.": "點擊下方按鈕以確認設定加密。", + "QR Code": "QR Code", + "Dismiss read marker and jump to bottom": "取消讀取標記並跳至底部", + "Jump to oldest unread message": "跳至最舊的未讀訊息", + "Upload a file": "上傳檔案" } diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js index 02151f8474..d372c38405 100644 --- a/src/indexing/EventIndex.js +++ b/src/indexing/EventIndex.js @@ -489,14 +489,20 @@ export default class EventIndex extends EventEmitter { return object; }); - // Create a new checkpoint so we can continue crawling the room for - // messages. - const newCheckpoint = { - roomId: checkpoint.roomId, - token: res.end, - fullCrawl: checkpoint.fullCrawl, - direction: checkpoint.direction, - }; + let newCheckpoint; + + // The token can be null for some reason. Don't create a checkpoint + // in that case since adding it to the db will fail. + if (res.end) { + // Create a new checkpoint so we can continue crawling the room + // for messages. + newCheckpoint = { + roomId: checkpoint.roomId, + token: res.end, + fullCrawl: checkpoint.fullCrawl, + direction: checkpoint.direction, + }; + } try { for (let i = 0; i < redactionEvents.length; i++) { @@ -506,6 +512,15 @@ export default class EventIndex extends EventEmitter { const eventsAlreadyAdded = await indexManager.addHistoricEvents( events, newCheckpoint, checkpoint); + + // We didn't get a valid new checkpoint from the server, nothing + // to do here anymore. + if (!newCheckpoint) { + console.log("EventIndex: The server didn't return a valid ", + "new checkpoint, not continuing the crawl.", checkpoint); + continue; + } + // If all events were already indexed we assume that we catched // up with our index and don't need to crawl the room further. // Let us delete the checkpoint in that case, otherwise push diff --git a/src/linkify-matrix.js b/src/linkify-matrix.js index ee9f703136..77c62ce84d 100644 --- a/src/linkify-matrix.js +++ b/src/linkify-matrix.js @@ -44,7 +44,7 @@ function matrixLinkify(linkify) { const S_HASH = S_START.jump(TT.POUND); const S_HASH_NAME = new linkify.parser.State(); const S_HASH_NAME_COLON = new linkify.parser.State(); - const S_HASH_NAME_COLON_DOMAIN = new linkify.parser.State(); + const S_HASH_NAME_COLON_DOMAIN = new linkify.parser.State(ROOMALIAS); const S_HASH_NAME_COLON_DOMAIN_DOT = new linkify.parser.State(); const S_ROOMALIAS = new linkify.parser.State(ROOMALIAS); const S_ROOMALIAS_COLON = new linkify.parser.State(); @@ -92,7 +92,7 @@ function matrixLinkify(linkify) { const S_AT = S_START.jump(TT.AT); const S_AT_NAME = new linkify.parser.State(); const S_AT_NAME_COLON = new linkify.parser.State(); - const S_AT_NAME_COLON_DOMAIN = new linkify.parser.State(); + const S_AT_NAME_COLON_DOMAIN = new linkify.parser.State(USERID); const S_AT_NAME_COLON_DOMAIN_DOT = new linkify.parser.State(); const S_USERID = new linkify.parser.State(USERID); const S_USERID_COLON = new linkify.parser.State(); @@ -138,7 +138,7 @@ function matrixLinkify(linkify) { const S_PLUS = S_START.jump(TT.PLUS); const S_PLUS_NAME = new linkify.parser.State(); const S_PLUS_NAME_COLON = new linkify.parser.State(); - const S_PLUS_NAME_COLON_DOMAIN = new linkify.parser.State(); + const S_PLUS_NAME_COLON_DOMAIN = new linkify.parser.State(GROUPID); const S_PLUS_NAME_COLON_DOMAIN_DOT = new linkify.parser.State(); const S_GROUPID = new linkify.parser.State(GROUPID); const S_GROUPID_COLON = new linkify.parser.State(); diff --git a/src/mjolnir/Mjolnir.js b/src/mjolnir/Mjolnir.js index 5836ffd57a..9876cb1f7f 100644 --- a/src/mjolnir/Mjolnir.js +++ b/src/mjolnir/Mjolnir.js @@ -18,7 +18,7 @@ import {MatrixClientPeg} from "../MatrixClientPeg"; import {ALL_RULE_TYPES, BanList} from "./BanList"; import SettingsStore, {SettingLevel} from "../settings/SettingsStore"; import {_t} from "../languageHandler"; -import dis from "../dispatcher"; +import dis from "../dispatcher/dispatcher"; // TODO: Move this and related files to the js-sdk or something once finalized. diff --git a/src/rageshake/submit-rageshake.ts b/src/rageshake/submit-rageshake.ts index e5027e0d37..9f9d7898cb 100644 --- a/src/rageshake/submit-rageshake.ts +++ b/src/rageshake/submit-rageshake.ts @@ -133,7 +133,6 @@ export default async function sendBugReport(bugReportEndpoint: string, opts: IOp body.append("cross_signing_supported_by_hs", String(await client.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing"))); body.append("cross_signing_ready", String(await client.isCrossSigningReady())); - body.append("ssss_key_needs_upgrade", String(await client.secretStorageKeyNeedsUpgrade())); } } diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 5c6d843349..400012e168 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -29,6 +29,7 @@ import ThemeController from './controllers/ThemeController'; import PushToMatrixClientController from './controllers/PushToMatrixClientController'; import ReloadOnChangeController from "./controllers/ReloadOnChangeController"; import {RIGHT_PANEL_PHASES} from "../stores/RightPanelStorePhases"; +import FontSizeController from './controllers/FontSizeController'; // These are just a bunch of helper arrays to avoid copy/pasting a bunch of times const LEVELS_ROOM_SETTINGS = ['device', 'room-device', 'room-account', 'account', 'config']; @@ -94,6 +95,12 @@ export const SETTINGS = { // // not use this for new settings. // invertedSettingName: "my-negative-setting", // }, + "feature_font_scaling": { + isFeature: true, + displayName: _td("Font scaling"), + supportedLevels: LEVELS_FEATURE, + default: false, + }, "feature_pinning": { isFeature: true, displayName: _td("Message Pinning"), @@ -137,6 +144,12 @@ export const SETTINGS = { supportedLevels: LEVELS_FEATURE, default: false, }, + "feature_irc_ui": { + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + displayName: _td('Use IRC layout'), + default: false, + isFeature: true, + }, "mjolnirRooms": { supportedLevels: ['account'], default: [], @@ -158,6 +171,17 @@ export const SETTINGS = { displayName: _td("Show info about bridges in room settings"), default: false, }, + "fontSize": { + displayName: _td("Font size"), + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + default: 16, + controller: new FontSizeController(), + }, + "useCustomFontSize": { + displayName: _td("Custom font size"), + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + default: false, + }, "MessageComposerInput.suggestEmoji": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Enable Emoji suggestions while typing'), @@ -519,4 +543,11 @@ export const SETTINGS = { MatrixClient.prototype.setCryptoTrustCrossSignedDevices, true, ), }, + "ircDisplayNameWidth": { + // We specifically want to have room-device > device so that users may set a device default + // with a per-room override. + supportedLevels: ['room-device', 'device'], + displayName: _td("IRC display name width"), + default: 80, + }, }; diff --git a/src/settings/SettingsStore.js b/src/settings/SettingsStore.js index 0122916bc3..4b18a27c6c 100644 --- a/src/settings/SettingsStore.js +++ b/src/settings/SettingsStore.js @@ -24,7 +24,7 @@ import RoomSettingsHandler from "./handlers/RoomSettingsHandler"; import ConfigSettingsHandler from "./handlers/ConfigSettingsHandler"; import {_t} from '../languageHandler'; import SdkConfig from "../SdkConfig"; -import dis from '../dispatcher'; +import dis from '../dispatcher/dispatcher'; import {SETTINGS} from "./Settings"; import LocalEchoWrapper from "./handlers/LocalEchoWrapper"; import {WatchManager} from "./WatchManager"; @@ -370,6 +370,21 @@ export default class SettingsStore { return SettingsStore._getFinalValue(setting, level, roomId, null, null); } + /** + * Gets the default value of a setting. + * @param {string} settingName The name of the setting to read the value of. + * @param {String} roomId The room ID to read the setting value in, may be null. + * @return {*} The default value + */ + static getDefaultValue(settingName) { + // Verify that the setting is actually a setting + if (!SETTINGS[settingName]) { + throw new Error("Setting '" + settingName + "' does not appear to be a setting."); + } + + return SETTINGS[settingName].default; + } + static _getFinalValue(setting, level, roomId, calculatedValue, calculatedAtLevel) { let resultingValue = calculatedValue; diff --git a/src/settings/controllers/CustomStatusController.js b/src/settings/controllers/CustomStatusController.js index 0fc6619d92..031387bb6a 100644 --- a/src/settings/controllers/CustomStatusController.js +++ b/src/settings/controllers/CustomStatusController.js @@ -15,7 +15,7 @@ limitations under the License. */ import SettingController from "./SettingController"; -import dis from "../../dispatcher"; +import dis from "../../dispatcher/dispatcher"; export default class CustomStatusController extends SettingController { onChange(level, roomId, newValue) { diff --git a/src/settings/controllers/FontSizeController.js b/src/settings/controllers/FontSizeController.js new file mode 100644 index 0000000000..3ef01ab99b --- /dev/null +++ b/src/settings/controllers/FontSizeController.js @@ -0,0 +1,32 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import SettingController from "./SettingController"; +import dis from "../../dispatcher/dispatcher"; + +export default class FontSizeController extends SettingController { + constructor() { + super(); + } + + onChange(level, roomId, newValue) { + // Dispatch font size change so that everything open responds to the change. + dis.dispatch({ + action: "update-font-size", + size: newValue, + }); + } +} diff --git a/src/stores/CustomRoomTagStore.js b/src/stores/CustomRoomTagStore.js index 909282c085..c67868e2c6 100644 --- a/src/stores/CustomRoomTagStore.js +++ b/src/stores/CustomRoomTagStore.js @@ -13,7 +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 dis from '../dispatcher'; +import dis from '../dispatcher/dispatcher'; import * as RoomNotifs from '../RoomNotifs'; import RoomListStore from './RoomListStore'; import EventEmitter from 'events'; diff --git a/src/stores/GroupStore.js b/src/stores/GroupStore.js index 78a144f755..d4097184a1 100644 --- a/src/stores/GroupStore.js +++ b/src/stores/GroupStore.js @@ -18,7 +18,7 @@ import EventEmitter from 'events'; import { groupMemberFromApiObject, groupRoomFromApiObject } from '../groups'; import FlairStore from './FlairStore'; import {MatrixClientPeg} from '../MatrixClientPeg'; -import dis from '../dispatcher'; +import dis from '../dispatcher/dispatcher'; function parseMembersResponse(response) { return response.chunk.map((apiMember) => groupMemberFromApiObject(apiMember)); diff --git a/src/stores/LifecycleStore.js b/src/stores/LifecycleStore.js index 904f29f7b3..a12bac7dd6 100644 --- a/src/stores/LifecycleStore.js +++ b/src/stores/LifecycleStore.js @@ -15,7 +15,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 dis from '../dispatcher'; +import dis from '../dispatcher/dispatcher'; import {Store} from 'flux/utils'; const INITIAL_STATE = { diff --git a/src/stores/RightPanelStore.js b/src/stores/RightPanelStore.js index 3a5605ba3f..a73f3befbb 100644 --- a/src/stores/RightPanelStore.js +++ b/src/stores/RightPanelStore.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import dis from '../dispatcher'; +import dis from '../dispatcher/dispatcher'; import {pendingVerificationRequestForUser} from '../verification'; import {Store} from 'flux/utils'; import SettingsStore, {SettingLevel} from "../settings/SettingsStore"; diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index 89edc9a8ef..d7b6759195 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ import {Store} from 'flux/utils'; -import dis from '../dispatcher'; +import dis from '../dispatcher/dispatcher'; import DMRoomMap from '../utils/DMRoomMap'; import * as Unread from '../Unread'; import SettingsStore from "../settings/SettingsStore"; diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index 841734dfb7..3d82d086d7 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -15,7 +15,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 dis from '../dispatcher'; +import dis from '../dispatcher/dispatcher'; import {Store} from 'flux/utils'; import {MatrixClientPeg} from '../MatrixClientPeg'; import * as sdk from '../index'; diff --git a/src/stores/SessionStore.js b/src/stores/SessionStore.js index f38bc046d0..096811940c 100644 --- a/src/stores/SessionStore.js +++ b/src/stores/SessionStore.js @@ -14,7 +14,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 dis from '../dispatcher'; +import dis from '../dispatcher/dispatcher'; import {Store} from 'flux/utils'; const INITIAL_STATE = { diff --git a/src/stores/TagOrderStore.js b/src/stores/TagOrderStore.js index c05728e497..2acf531d86 100644 --- a/src/stores/TagOrderStore.js +++ b/src/stores/TagOrderStore.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ import {Store} from 'flux/utils'; -import dis from '../dispatcher'; +import dis from '../dispatcher/dispatcher'; import GroupStore from './GroupStore'; import Analytics from '../Analytics'; import * as RoomNotifs from "../RoomNotifs"; diff --git a/src/theme.js b/src/theme.js index 2ccce81a8d..72b6e93443 100644 --- a/src/theme.js +++ b/src/theme.js @@ -19,7 +19,7 @@ import {_t} from "./languageHandler"; export const DEFAULT_THEME = "light"; import Tinter from "./Tinter"; -import dis from "./dispatcher"; +import dis from "./dispatcher/dispatcher"; import SettingsStore, {SettingLevel} from "./settings/SettingsStore"; import ThemeController from "./settings/controllers/ThemeController"; @@ -81,7 +81,7 @@ export class ThemeWatcher { } getEffectiveTheme() { - // Dev note: Much of this logic is replicated in the GeneralUserSettingsTab + // Dev note: Much of this logic is replicated in the AppearanceUserSettingsTab // XXX: checking the isLight flag here makes checking it in the ThemeController // itself completely redundant since we just override the result here and we're diff --git a/src/utils/PasswordScorer.js b/src/utils/PasswordScorer.ts similarity index 98% rename from src/utils/PasswordScorer.js rename to src/utils/PasswordScorer.ts index 9d89942bf5..d8f3b0fb96 100644 --- a/src/utils/PasswordScorer.js +++ b/src/utils/PasswordScorer.ts @@ -63,7 +63,7 @@ _td("Short keyboard patterns are easy to guess"); * @param {string} password Password to score * @returns {object} Score result with `score` and `feedback` properties */ -export function scorePassword(password) { +export function scorePassword(password: string) { if (password.length === 0) return null; const userInputs = ZXCVBN_USER_INPUTS.slice(); diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js index ad4c02887e..35e23f0429 100644 --- a/src/utils/WidgetUtils.js +++ b/src/utils/WidgetUtils.js @@ -18,7 +18,7 @@ limitations under the License. import {MatrixClientPeg} from '../MatrixClientPeg'; import SdkConfig from "../SdkConfig"; -import dis from '../dispatcher'; +import dis from '../dispatcher/dispatcher'; import * as url from "url"; import WidgetEchoStore from '../stores/WidgetEchoStore'; diff --git a/src/utils/units.ts b/src/utils/units.ts new file mode 100644 index 0000000000..54dd6b0523 --- /dev/null +++ b/src/utils/units.ts @@ -0,0 +1,27 @@ +/* +Copyright 2020 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. +*/ + +/* Simple utils for formatting style values + */ + +// converts a pixel value to rem. +export function toRem(pixelValue: number): string { + return pixelValue / 15 + "rem"; +} + +export function toPx(pixelValue: number): string { + return pixelValue + "px"; +} diff --git a/src/verification.js b/src/verification.js index f488b2ebeb..289ac9544b 100644 --- a/src/verification.js +++ b/src/verification.js @@ -15,7 +15,7 @@ limitations under the License. */ import {MatrixClientPeg} from './MatrixClientPeg'; -import dis from "./dispatcher"; +import dis from "./dispatcher/dispatcher"; import Modal from './Modal'; import * as sdk from './index'; import { _t } from './languageHandler'; diff --git a/test/components/views/messages/TextualBody-test.js b/test/components/views/messages/TextualBody-test.js index 59671327ce..4e93b3bb64 100644 --- a/test/components/views/messages/TextualBody-test.js +++ b/test/components/views/messages/TextualBody-test.js @@ -206,7 +206,7 @@ describe("", () => { 'Hey ' + '' + 'Member' + ''); }); diff --git a/test/components/views/rooms/RoomList-test.js b/test/components/views/rooms/RoomList-test.js index 8dc4647920..235ed61016 100644 --- a/test/components/views/rooms/RoomList-test.js +++ b/test/components/views/rooms/RoomList-test.js @@ -9,7 +9,7 @@ import {MatrixClientPeg} from '../../../../src/MatrixClientPeg'; import sdk from '../../../skinned-sdk'; import { DragDropContext } from 'react-beautiful-dnd'; -import dis from '../../../../src/dispatcher'; +import dis from '../../../../src/dispatcher/dispatcher'; import DMRoomMap from '../../../../src/utils/DMRoomMap.js'; import GroupStore from '../../../../src/stores/GroupStore.js'; diff --git a/test/test-utils.js b/test/test-utils.js index d7aa9d5de9..2d7c1bd62c 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -2,7 +2,7 @@ import React from 'react'; import {MatrixClientPeg as peg} from '../src/MatrixClientPeg'; -import dis from '../src/dispatcher'; +import dis from '../src/dispatcher/dispatcher'; import {makeType} from "../src/utils/TypeUtils"; import {ValidatedServerConfig} from "../src/utils/AutoDiscoveryUtils"; import ShallowRenderer from 'react-test-renderer/shallow'; diff --git a/tsconfig.json b/tsconfig.json index b87f640734..8a01ca335e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,8 @@ "jsx": "react", "types": [ "node", - "react" + "react", + "flux" ] }, "include": [ diff --git a/yarn.lock b/yarn.lock index 0edea5433e..93118dab22 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1218,6 +1218,19 @@ resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== +"@types/fbemitter@*": + version "2.0.32" + resolved "https://registry.yarnpkg.com/@types/fbemitter/-/fbemitter-2.0.32.tgz#8ed204da0f54e9c8eaec31b1eec91e25132d082c" + integrity sha1-jtIE2g9U6cjq7DGx7skeJRMtCCw= + +"@types/flux@^3.1.9": + version "3.1.9" + resolved "https://registry.yarnpkg.com/@types/flux/-/flux-3.1.9.tgz#ddfc9641ee2e2e6cb6cd730c6a48ef82e2076711" + integrity sha512-bSbDf4tTuN9wn3LTGPnH9wnSSLtR5rV7UPWFpM00NJ1pSTBwCzeZG07XsZ9lBkxwngrqjDtM97PLt5IuIdCQUA== + dependencies: + "@types/fbemitter" "*" + "@types/react" "*" + "@types/glob@^7.1.1": version "7.1.1" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" @@ -1272,6 +1285,21 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== +"@types/qrcode@^1.3.4": + version "1.3.4" + resolved "https://registry.yarnpkg.com/@types/qrcode/-/qrcode-1.3.4.tgz#984d97bb72caa558d470158701081ccb712f616b" + integrity sha512-aILE5yvKaqQXlY0YPMEYwK/KwdD43fwQTyagj0ffBBTQj8h//085Zp8LUrOnZ9FT69x64f5UgDo0EueY4BPAdg== + dependencies: + "@types/node" "*" + +"@types/react@*": + version "16.9.35" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.35.tgz#a0830d172e8aadd9bd41709ba2281a3124bbd368" + integrity sha512-q0n0SsWcGc8nDqH2GJfWQWUOmZSJhXV64CjVN5SvcNti3TdEaA3AH0D8DwNmMdzjMAC/78tB8nAZIlV8yTz+zQ== + dependencies: + "@types/prop-types" "*" + csstype "^2.2.0" + "@types/react@16.9": version "16.9.32" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.32.tgz#f6368625b224604148d1ddf5920e4fefbd98d383" @@ -1318,6 +1346,11 @@ dependencies: "@types/yargs-parser" "*" +"@types/zxcvbn@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@types/zxcvbn/-/zxcvbn-4.4.0.tgz#fbc1d941cc6d9d37d18405c513ba6b294f89b609" + integrity sha512-GQLOT+SN20a+AI51y3fAimhyTF4Y0RG+YP3gf91OibIZ7CJmPFgoZi+ZR5a+vRbS01LbQosITWum4ATmJ1Z6Pg== + "@typescript-eslint/experimental-utils@^2.5.0": version "2.27.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.27.0.tgz#801a952c10b58e486c9a0b36cf21e2aab1e9e01a" @@ -5720,10 +5753,9 @@ mathml-tag-names@^2.0.1: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -matrix-js-sdk@6.1.0: +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": version "6.1.0" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-6.1.0.tgz#c28ad67c113c4aa9c8bce409c7ba550170bdc2ee" - integrity sha512-N+vCgxWORvhh7AGyWZlU5Z2brojbbnHnWlMkBF6JjWe6a+pfpjmRKp5/jeQpOz6yfe56sIQvU7ikBZl3JjlMiw== + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/e3c6a0e1a08a3812ba988e60eb5a2a013bb27404" dependencies: "@babel/runtime" "^7.8.3" another-json "^0.2.0" @@ -6852,18 +6884,6 @@ pvutils@latest: resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.0.17.tgz#ade3c74dfe7178944fe44806626bd2e249d996bf" integrity sha512-wLHYUQxWaXVQvKnwIDWFVKDJku9XDCvyhhxoq8dc5MFdIlRenyPI9eSfEtcvgHgD7FlvCyGAlWgOzRnZD99GZQ== -qr.js@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/qr.js/-/qr.js-0.0.0.tgz#cace86386f59a0db8050fa90d9b6b0e88a1e364f" - integrity sha1-ys6GOG9ZoNuAUPqQ2baw6IoeNk8= - -qrcode-react@^0.1.16: - version "0.1.16" - resolved "https://registry.yarnpkg.com/qrcode-react/-/qrcode-react-0.1.16.tgz#d064999d510ffc3e55a9ca3ffcf6c203c69f1517" - integrity sha512-FK+QCfFqCQMSxUE1byzglERJQkwKqXYvYMCS+/Ad2zACJOfoHkHHtRqsQQPji7lfb1y1qCXLvL+3eP1hAfg8Ng== - dependencies: - qr.js "0.0.0" - qrcode@^1.4.4: version "1.4.4" resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.4.4.tgz#f0c43568a7e7510a55efc3b88d9602f71963ea83"