Merge branch 'develop' into sort-imports
|
@ -11,7 +11,8 @@ module.exports = {
|
|||
"length-zero-no-unit": null,
|
||||
"rule-empty-line-before": null,
|
||||
"color-hex-length": null,
|
||||
"max-empty-lines": null,
|
||||
"max-empty-lines": 1,
|
||||
"no-eol-whitespace": true,
|
||||
"number-no-trailing-zeros": null,
|
||||
"number-leading-zero": null,
|
||||
"selector-list-comma-newline-after": null,
|
||||
|
|
90
CHANGELOG.md
|
@ -1,3 +1,93 @@
|
|||
Changes in [3.33.0](https://github.com/vector-im/element-desktop/releases/tag/v3.33.0) (2021-10-25)
|
||||
===================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Convert the "Cryptography" settings panel to an HTML table to assist screen reader users. ([\#6968](https://github.com/matrix-org/matrix-react-sdk/pull/6968)). Contributed by [andybalaam](https://github.com/andybalaam).
|
||||
* Swap order of private space creation and tweak copy ([\#6967](https://github.com/matrix-org/matrix-react-sdk/pull/6967)). Fixes vector-im/element-web#18768 and vector-im/element-web#18768.
|
||||
* Add spacing to Room settings - Notifications subsection ([\#6962](https://github.com/matrix-org/matrix-react-sdk/pull/6962)). Contributed by [CicadaCinema](https://github.com/CicadaCinema).
|
||||
* Use HTML tables for some tabular user interface areas, to assist with screen reader use ([\#6955](https://github.com/matrix-org/matrix-react-sdk/pull/6955)). Contributed by [andybalaam](https://github.com/andybalaam).
|
||||
* Fix space invite edge cases ([\#6884](https://github.com/matrix-org/matrix-react-sdk/pull/6884)). Fixes vector-im/element-web#19010 vector-im/element-web#17345 and vector-im/element-web#19010.
|
||||
* Allow options to cascade kicks/bans throughout spaces ([\#6829](https://github.com/matrix-org/matrix-react-sdk/pull/6829)). Fixes vector-im/element-web#18969 and vector-im/element-web#18969.
|
||||
* Make public space alias field mandatory again ([\#6921](https://github.com/matrix-org/matrix-react-sdk/pull/6921)). Fixes vector-im/element-web#19003 and vector-im/element-web#19003.
|
||||
* Add progress bar to restricted room upgrade dialog ([\#6919](https://github.com/matrix-org/matrix-react-sdk/pull/6919)). Fixes vector-im/element-web#19146 and vector-im/element-web#19146.
|
||||
* Add customisation point for visibility of invites and room creation ([\#6922](https://github.com/matrix-org/matrix-react-sdk/pull/6922)). Fixes vector-im/element-web#19331 and vector-im/element-web#19331.
|
||||
* Inhibit `Unable to get validated threepid` error during UIA ([\#6928](https://github.com/matrix-org/matrix-react-sdk/pull/6928)). Fixes vector-im/element-web#18883 and vector-im/element-web#18883.
|
||||
* Tweak room list skeleton UI height and behaviour ([\#6926](https://github.com/matrix-org/matrix-react-sdk/pull/6926)). Fixes vector-im/element-web#18231 vector-im/element-web#16581 and vector-im/element-web#18231.
|
||||
* If public room creation fails, retry without publishing it ([\#6872](https://github.com/matrix-org/matrix-react-sdk/pull/6872)). Fixes vector-im/element-web#19194 and vector-im/element-web#19194. Contributed by [AndrewFerr](https://github.com/AndrewFerr).
|
||||
* Iterate invite your teammates to Space view ([\#6925](https://github.com/matrix-org/matrix-react-sdk/pull/6925)). Fixes vector-im/element-web#18772 and vector-im/element-web#18772.
|
||||
* Make placeholder more grey when no input ([\#6840](https://github.com/matrix-org/matrix-react-sdk/pull/6840)). Fixes vector-im/element-web#17243 and vector-im/element-web#17243. Contributed by [wlach](https://github.com/wlach).
|
||||
* Respect tombstones in locally known rooms for Space children ([\#6906](https://github.com/matrix-org/matrix-react-sdk/pull/6906)). Fixes vector-im/element-web#19246 vector-im/element-web#19256 and vector-im/element-web#19246.
|
||||
* Improve emoji shortcodes generated from annotations ([\#6907](https://github.com/matrix-org/matrix-react-sdk/pull/6907)). Fixes vector-im/element-web#19304 and vector-im/element-web#19304.
|
||||
* Hide kick & ban options in UserInfo when looking at own profile ([\#6911](https://github.com/matrix-org/matrix-react-sdk/pull/6911)). Fixes vector-im/element-web#19066 and vector-im/element-web#19066.
|
||||
* Add progress bar to Community to Space migration tool ([\#6887](https://github.com/matrix-org/matrix-react-sdk/pull/6887)). Fixes vector-im/element-web#19216 and vector-im/element-web#19216.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix leave space cancel button exploding ([\#6966](https://github.com/matrix-org/matrix-react-sdk/pull/6966)).
|
||||
* Fix edge case behaviour of the space join spinner for guests ([\#6972](https://github.com/matrix-org/matrix-react-sdk/pull/6972)). Fixes vector-im/element-web#19359 and vector-im/element-web#19359.
|
||||
* Convert emoticon to emoji at the end of a line on send even if the cursor isn't there ([\#6965](https://github.com/matrix-org/matrix-react-sdk/pull/6965)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Fix text overflows button on Home page ([\#6898](https://github.com/matrix-org/matrix-react-sdk/pull/6898)). Fixes vector-im/element-web#19180 and vector-im/element-web#19180. Contributed by [oliver-pham](https://github.com/oliver-pham).
|
||||
* Space Room View should react to join rule changes down /sync ([\#6945](https://github.com/matrix-org/matrix-react-sdk/pull/6945)). Fixes vector-im/element-web#19390 and vector-im/element-web#19390.
|
||||
* Hide leave section button if user isn't in the room e.g peeking ([\#6920](https://github.com/matrix-org/matrix-react-sdk/pull/6920)). Fixes vector-im/element-web#17410 and vector-im/element-web#17410.
|
||||
* Fix bug where room list would get stuck showing no rooms ([\#6939](https://github.com/matrix-org/matrix-react-sdk/pull/6939)). Fixes vector-im/element-web#19373 and vector-im/element-web#19373.
|
||||
* Update room settings dialog title when room name changes ([\#6916](https://github.com/matrix-org/matrix-react-sdk/pull/6916)). Fixes vector-im/element-web#17480 and vector-im/element-web#17480. Contributed by [psrpinto](https://github.com/psrpinto).
|
||||
* Fix editing losing emote-ness and rainbow-ness of messages ([\#6931](https://github.com/matrix-org/matrix-react-sdk/pull/6931)). Fixes vector-im/element-web#19350 and vector-im/element-web#19350.
|
||||
* Remove semicolon from notifications panel ([\#6930](https://github.com/matrix-org/matrix-react-sdk/pull/6930)). Contributed by [robintown](https://github.com/robintown).
|
||||
* Prevent profile image in left panel's backdrop from being selected ([\#6924](https://github.com/matrix-org/matrix-react-sdk/pull/6924)). Contributed by [rom4nik](https://github.com/rom4nik).
|
||||
* Validate that the phone number verification field is filled before allowing user to submit ([\#6918](https://github.com/matrix-org/matrix-react-sdk/pull/6918)). Fixes vector-im/element-web#19316 and vector-im/element-web#19316. Contributed by [VFermat](https://github.com/VFermat).
|
||||
* Updated how save button becomes disabled in room settings to listen for all fields instead of the most recent ([\#6917](https://github.com/matrix-org/matrix-react-sdk/pull/6917)). Contributed by [LoganArnett](https://github.com/LoganArnett).
|
||||
* Use FocusLock around ContextMenus to simplify focus management ([\#6311](https://github.com/matrix-org/matrix-react-sdk/pull/6311)). Fixes vector-im/element-web#19259 and vector-im/element-web#19259.
|
||||
* Fix space hierarchy pagination ([\#6908](https://github.com/matrix-org/matrix-react-sdk/pull/6908)). Fixes vector-im/element-web#19276 and vector-im/element-web#19276.
|
||||
* Fix spaces keyboard shortcuts not working for last space ([\#6909](https://github.com/matrix-org/matrix-react-sdk/pull/6909)). Fixes vector-im/element-web#19255 and vector-im/element-web#19255.
|
||||
* Use fallback avatar only for DMs with 2 people. ([\#6895](https://github.com/matrix-org/matrix-react-sdk/pull/6895)). Fixes vector-im/element-web#18747 and vector-im/element-web#18747. Contributed by [andybalaam](https://github.com/andybalaam).
|
||||
|
||||
Changes in [3.33.0-rc.2](https://github.com/vector-im/element-desktop/releases/tag/v3.33.0-rc.2) (2021-10-20)
|
||||
=============================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix conflicting CSS on syntax highlighted blocks ([\#6991](https://github.com/matrix-org/matrix-react-sdk/pull/6991)). Fixes vector-im/element-web#19445
|
||||
|
||||
Changes in [3.33.0-rc.1](https://github.com/vector-im/element-desktop/releases/tag/v3.33.0-rc.1) (2021-10-19)
|
||||
=============================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Swap order of private space creation and tweak copy ([\#6967](https://github.com/matrix-org/matrix-react-sdk/pull/6967)). Fixes vector-im/element-web#18768 and vector-im/element-web#18768.
|
||||
* Add spacing to Room settings - Notifications subsection ([\#6962](https://github.com/matrix-org/matrix-react-sdk/pull/6962)). Contributed by [CicadaCinema](https://github.com/CicadaCinema).
|
||||
* Convert the "Cryptography" settings panel to an HTML to assist screen reader users. ([\#6968](https://github.com/matrix-org/matrix-react-sdk/pull/6968)). Contributed by [andybalaam](https://github.com/andybalaam).
|
||||
* Use HTML tables for some tabular user interface areas, to assist with screen reader use ([\#6955](https://github.com/matrix-org/matrix-react-sdk/pull/6955)). Contributed by [andybalaam](https://github.com/andybalaam).
|
||||
* Fix space invite edge cases ([\#6884](https://github.com/matrix-org/matrix-react-sdk/pull/6884)). Fixes vector-im/element-web#19010 vector-im/element-web#17345 and vector-im/element-web#19010.
|
||||
* Allow options to cascade kicks/bans throughout spaces ([\#6829](https://github.com/matrix-org/matrix-react-sdk/pull/6829)). Fixes vector-im/element-web#18969 and vector-im/element-web#18969.
|
||||
* Make public space alias field mandatory again ([\#6921](https://github.com/matrix-org/matrix-react-sdk/pull/6921)). Fixes vector-im/element-web#19003 and vector-im/element-web#19003.
|
||||
* Add progress bar to restricted room upgrade dialog ([\#6919](https://github.com/matrix-org/matrix-react-sdk/pull/6919)). Fixes vector-im/element-web#19146 and vector-im/element-web#19146.
|
||||
* Add customisation point for visibility of invites and room creation ([\#6922](https://github.com/matrix-org/matrix-react-sdk/pull/6922)). Fixes vector-im/element-web#19331 and vector-im/element-web#19331.
|
||||
* Inhibit `Unable to get validated threepid` error during UIA ([\#6928](https://github.com/matrix-org/matrix-react-sdk/pull/6928)). Fixes vector-im/element-web#18883 and vector-im/element-web#18883.
|
||||
* Tweak room list skeleton UI height and behaviour ([\#6926](https://github.com/matrix-org/matrix-react-sdk/pull/6926)). Fixes vector-im/element-web#18231 vector-im/element-web#16581 and vector-im/element-web#18231.
|
||||
* If public room creation fails, retry without publishing it ([\#6872](https://github.com/matrix-org/matrix-react-sdk/pull/6872)). Fixes vector-im/element-web#19194 and vector-im/element-web#19194. Contributed by [AndrewFerr](https://github.com/AndrewFerr).
|
||||
* Iterate invite your teammates to Space view ([\#6925](https://github.com/matrix-org/matrix-react-sdk/pull/6925)). Fixes vector-im/element-web#18772 and vector-im/element-web#18772.
|
||||
* Make placeholder more grey when no input ([\#6840](https://github.com/matrix-org/matrix-react-sdk/pull/6840)). Fixes vector-im/element-web#17243 and vector-im/element-web#17243. Contributed by [wlach](https://github.com/wlach).
|
||||
* Respect tombstones in locally known rooms for Space children ([\#6906](https://github.com/matrix-org/matrix-react-sdk/pull/6906)). Fixes vector-im/element-web#19246 vector-im/element-web#19256 and vector-im/element-web#19246.
|
||||
* Improve emoji shortcodes generated from annotations ([\#6907](https://github.com/matrix-org/matrix-react-sdk/pull/6907)). Fixes vector-im/element-web#19304 and vector-im/element-web#19304.
|
||||
* Hide kick & ban options in UserInfo when looking at own profile ([\#6911](https://github.com/matrix-org/matrix-react-sdk/pull/6911)). Fixes vector-im/element-web#19066 and vector-im/element-web#19066.
|
||||
* Add progress bar to Community to Space migration tool ([\#6887](https://github.com/matrix-org/matrix-react-sdk/pull/6887)). Fixes vector-im/element-web#19216 and vector-im/element-web#19216.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix leave space cancel button exploding ([\#6966](https://github.com/matrix-org/matrix-react-sdk/pull/6966)).
|
||||
* Fix edge case behaviour of the space join spinner for guests ([\#6972](https://github.com/matrix-org/matrix-react-sdk/pull/6972)). Fixes vector-im/element-web#19359 and vector-im/element-web#19359.
|
||||
* Convert emoticon to emoji at the end of a line on send even if the cursor isn't there ([\#6965](https://github.com/matrix-org/matrix-react-sdk/pull/6965)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Fix text overflows button on Home page ([\#6898](https://github.com/matrix-org/matrix-react-sdk/pull/6898)). Fixes vector-im/element-web#19180 and vector-im/element-web#19180. Contributed by [oliver-pham](https://github.com/oliver-pham).
|
||||
* Space Room View should react to join rule changes down /sync ([\#6945](https://github.com/matrix-org/matrix-react-sdk/pull/6945)). Fixes vector-im/element-web#19390 and vector-im/element-web#19390.
|
||||
* Hide leave section button if user isn't in the room e.g peeking ([\#6920](https://github.com/matrix-org/matrix-react-sdk/pull/6920)). Fixes vector-im/element-web#17410 and vector-im/element-web#17410.
|
||||
* Fix bug where room list would get stuck showing no rooms ([\#6939](https://github.com/matrix-org/matrix-react-sdk/pull/6939)). Fixes vector-im/element-web#19373 and vector-im/element-web#19373.
|
||||
* Update room settings dialog title when room name changes ([\#6916](https://github.com/matrix-org/matrix-react-sdk/pull/6916)). Fixes vector-im/element-web#17480 and vector-im/element-web#17480. Contributed by [psrpinto](https://github.com/psrpinto).
|
||||
* Fix editing losing emote-ness and rainbow-ness of messages ([\#6931](https://github.com/matrix-org/matrix-react-sdk/pull/6931)). Fixes vector-im/element-web#19350 and vector-im/element-web#19350.
|
||||
* Remove semicolon from notifications panel ([\#6930](https://github.com/matrix-org/matrix-react-sdk/pull/6930)). Contributed by [robintown](https://github.com/robintown).
|
||||
* Prevent profile image in left panel's backdrop from being selected ([\#6924](https://github.com/matrix-org/matrix-react-sdk/pull/6924)). Contributed by [rom4nik](https://github.com/rom4nik).
|
||||
* Validate that the phone number verification field is filled before allowing user to submit ([\#6918](https://github.com/matrix-org/matrix-react-sdk/pull/6918)). Fixes vector-im/element-web#19316 and vector-im/element-web#19316. Contributed by [VFermat](https://github.com/VFermat).
|
||||
* Updated how save button becomes disabled in room settings to listen for all fields instead of the most recent ([\#6917](https://github.com/matrix-org/matrix-react-sdk/pull/6917)). Contributed by [LoganArnett](https://github.com/LoganArnett).
|
||||
* Use FocusLock around ContextMenus to simplify focus management ([\#6311](https://github.com/matrix-org/matrix-react-sdk/pull/6311)). Fixes vector-im/element-web#19259 and vector-im/element-web#19259.
|
||||
* Fix space hierarchy pagination ([\#6908](https://github.com/matrix-org/matrix-react-sdk/pull/6908)). Fixes vector-im/element-web#19276 and vector-im/element-web#19276.
|
||||
* Fix spaces keyboard shortcuts not working for last space ([\#6909](https://github.com/matrix-org/matrix-react-sdk/pull/6909)). Fixes vector-im/element-web#19255 and vector-im/element-web#19255.
|
||||
* Use fallback avatar only for DMs with 2 people. ([\#6895](https://github.com/matrix-org/matrix-react-sdk/pull/6895)). Fixes vector-im/element-web#18747 and vector-im/element-web#18747. Contributed by [andybalaam](https://github.com/andybalaam).
|
||||
|
||||
Changes in [3.32.1](https://github.com/vector-im/element-desktop/releases/tag/v3.32.1) (2021-10-12)
|
||||
===================================================================================================
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "matrix-react-sdk",
|
||||
"version": "3.32.1",
|
||||
"version": "3.33.0",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
|
|
|
@ -141,12 +141,6 @@ input[type=search]::-webkit-search-results-decoration {
|
|||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
opacity: initial;
|
||||
font-weight: 400;
|
||||
}
|
||||
input::-moz-placeholder,
|
||||
textarea::-moz-placeholder {
|
||||
opacity: .6;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
input[type=text], input[type=password], textarea {
|
||||
|
|
|
@ -200,10 +200,10 @@
|
|||
@import "./views/right_panel/_EncryptionInfo.scss";
|
||||
@import "./views/right_panel/_PinnedMessagesCard.scss";
|
||||
@import "./views/right_panel/_RoomSummaryCard.scss";
|
||||
@import "./views/right_panel/_ThreadPanel.scss";
|
||||
@import "./views/right_panel/_UserInfo.scss";
|
||||
@import "./views/right_panel/_VerificationPanel.scss";
|
||||
@import "./views/right_panel/_WidgetCard.scss";
|
||||
@import "./views/right_panel/_ThreadPanel.scss";
|
||||
@import "./views/room_settings/_AliasSettings.scss";
|
||||
@import "./views/rooms/_AppsDrawer.scss";
|
||||
@import "./views/rooms/_Autocomplete.scss";
|
||||
|
|
|
@ -34,4 +34,3 @@ limitations under the License.
|
|||
.mx_CreateRoom_description {
|
||||
width: 330px;
|
||||
}
|
||||
|
||||
|
|
|
@ -43,8 +43,6 @@ $roomListCollapsedWidth: 68px;
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.mx_LeftPanel {
|
||||
background-color: $roomlist-bg-color;
|
||||
// TODO decrease this once Spaces launches as it'll no longer need to include the 56px Community Panel
|
||||
|
|
|
@ -32,7 +32,6 @@ limitations under the License.
|
|||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
@keyframes mx_RoomView_fileDropTarget_animation {
|
||||
from {
|
||||
opacity: 0;
|
||||
|
@ -112,7 +111,6 @@ limitations under the License.
|
|||
max-width: 1920px !important;
|
||||
}
|
||||
|
||||
|
||||
.mx_RoomView .mx_MainSplit {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
|
|
@ -203,7 +203,8 @@ limitations under the License.
|
|||
grid-row: 1;
|
||||
grid-column: 2;
|
||||
|
||||
.mx_InfoTooltip {
|
||||
.mx_InfoTooltip,
|
||||
.mx_SpaceHierarchy_roomTile_joined {
|
||||
display: inline;
|
||||
margin-left: 12px;
|
||||
color: $tertiary-content;
|
||||
|
@ -222,6 +223,25 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceHierarchy_roomTile_joined {
|
||||
position: relative;
|
||||
padding-left: 16px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
top: -2px;
|
||||
left: -4px;
|
||||
position: absolute;
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
mask-repeat: no-repeat;
|
||||
background-color: $accent-color;
|
||||
mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceHierarchy_roomTile_info {
|
||||
|
@ -268,6 +288,11 @@ limitations under the License.
|
|||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
&.mx_SpaceHierarchy_joining .mx_AccessibleButton {
|
||||
visibility: visible;
|
||||
padding: 4px 18px;
|
||||
}
|
||||
}
|
||||
|
||||
li.mx_SpaceHierarchy_roomTileWrapper {
|
||||
|
|
|
@ -348,7 +348,6 @@ $activeBorderColor: $secondary-content;
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
.mx_SpacePanel_sharePublicSpace {
|
||||
margin: 0;
|
||||
}
|
||||
|
|
|
@ -380,45 +380,6 @@ $SpaceRoomViewInnerWidth: 428px;
|
|||
}
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_betaWarning {
|
||||
padding: 12px 12px 12px 54px;
|
||||
position: relative;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-24px;
|
||||
width: 432px;
|
||||
border-radius: 8px;
|
||||
background-color: $info-plinth-bg-color;
|
||||
color: $secondary-content;
|
||||
box-sizing: border-box;
|
||||
|
||||
> h3 {
|
||||
font-weight: $font-semi-bold;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
> p {
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
|
||||
mask-position: center;
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
content: '';
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
left: 14px;
|
||||
background-color: $secondary-content;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceRoomView_inviteTeammates {
|
||||
// XXX remove this when spaces leaves Beta
|
||||
.mx_SpaceRoomView_inviteTeammates_betaDisclaimer {
|
||||
|
|
|
@ -64,4 +64,3 @@ limitations under the License.
|
|||
mask-size: contain;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -64,4 +64,3 @@ limitations under the License.
|
|||
padding: 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -58,4 +58,3 @@ limitations under the License.
|
|||
mask-size: 36px;
|
||||
mask-position: center;
|
||||
}
|
||||
|
||||
|
|
|
@ -50,4 +50,3 @@ limitations under the License.
|
|||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
.mx_WidgetCapabilitiesPromptDialog {
|
||||
.text-muted {
|
||||
font-size: $font-12px;
|
||||
|
@ -55,7 +54,6 @@ limitations under the License.
|
|||
width: $font-32px;
|
||||
height: $font-15px;
|
||||
|
||||
|
||||
&.mx_ToggleSwitch_on > .mx_ToggleSwitch_ball {
|
||||
left: calc(100% - $font-15px);
|
||||
}
|
||||
|
|
|
@ -130,4 +130,3 @@ input.mx_Dropdown_option:focus {
|
|||
margin-left: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
|
|
|
@ -61,4 +61,3 @@ limitations under the License.
|
|||
.mx_EditableItemList_label {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
|
|
|
@ -58,7 +58,6 @@ limitations under the License.
|
|||
height: $slider-selection-dot-size;
|
||||
background-color: $slider-selection-color;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 6px lightgrey;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
.mx_Checkbox {
|
||||
$size: $font-16px;
|
||||
$border-size: $font-1-5px;
|
||||
|
|
|
@ -49,4 +49,3 @@ limitations under the License.
|
|||
text-align: start;
|
||||
line-height: 17px !important;
|
||||
}
|
||||
|
||||
|
|
|
@ -34,4 +34,3 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -39,7 +39,6 @@ limitations under the License.
|
|||
background-color: $notice-primary-color;
|
||||
}
|
||||
|
||||
|
||||
.mx_cryptoEvent_state, .mx_cryptoEvent_buttons {
|
||||
grid-column: 3;
|
||||
grid-row: 1 / 3;
|
||||
|
|
|
@ -83,7 +83,7 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_RoomSummaryCard_e2ee_warning {
|
||||
background-color: #ff4b55;
|
||||
background-color: #ff5b55;
|
||||
&::before {
|
||||
mask-image: url('$(res)/img/e2e/warning.svg');
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
.mx_ThreadPanel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -223,7 +223,6 @@ limitations under the License.
|
|||
display: flex;
|
||||
margin: 8px 0;
|
||||
|
||||
|
||||
&.mx_UserInfo_device_verified {
|
||||
.mx_UserInfo_device_trusted {
|
||||
color: $accent-color;
|
||||
|
@ -267,7 +266,6 @@ limitations under the License.
|
|||
margin: 16px 0 8px;
|
||||
}
|
||||
|
||||
|
||||
.mx_VerificationShowSas {
|
||||
.mx_AccessibleButton + .mx_AccessibleButton {
|
||||
margin: 8px 0; // space between buttons
|
||||
|
|
|
@ -23,7 +23,6 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
.mx_UserInfo {
|
||||
.mx_EncryptionPanel_cancel {
|
||||
mask: url('$(res)/img/feather-customised/cancel.svg');
|
||||
|
|
|
@ -365,7 +365,6 @@ $MinWidth: 240px;
|
|||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
|
||||
.mx_AppLoading iframe {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -24,7 +24,6 @@ limitations under the License.
|
|||
margin: -7px -10px -5px -10px;
|
||||
overflow: visible !important; // override mx_EventTile_content
|
||||
|
||||
|
||||
.mx_BasicMessageComposer_input {
|
||||
border-radius: 4px;
|
||||
border: solid 1px $primary-hairline-color;
|
||||
|
|
|
@ -232,7 +232,7 @@ limitations under the License.
|
|||
.mx_EditMessageComposer_buttons {
|
||||
position: static;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin: 8px 0 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
|
@ -263,7 +263,6 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
.mx_EventTile_readAvatars {
|
||||
position: absolute;
|
||||
right: -110px;
|
||||
|
|
|
@ -401,7 +401,6 @@ $left-gutter: 64px;
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
.mx_EventTile_e2eIcon {
|
||||
position: relative;
|
||||
width: 14px;
|
||||
|
@ -486,7 +485,7 @@ $left-gutter: 64px;
|
|||
|
||||
pre, code {
|
||||
font-family: $monospace-font-family !important;
|
||||
background-color: $header-panel-bg-color;
|
||||
background-color: $codeblock-background-color;
|
||||
}
|
||||
|
||||
pre code > * {
|
||||
|
@ -581,7 +580,6 @@ $left-gutter: 64px;
|
|||
color: inherit;
|
||||
}
|
||||
|
||||
|
||||
/* Make h1 and h2 the same size as h3. */
|
||||
.mx_EventTile_content .markdown-body h1,
|
||||
.mx_EventTile_content .markdown-body h2 {
|
||||
|
@ -613,7 +611,6 @@ $left-gutter: 64px;
|
|||
|
||||
/* end of overrides */
|
||||
|
||||
|
||||
.mx_EventTile_keyRequestInfo {
|
||||
font-size: $font-12px;
|
||||
}
|
||||
|
@ -731,8 +728,6 @@ $left-gutter: 64px;
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.mx_ThreadView {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -247,7 +247,6 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
.mx_MessageComposer_upload::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/composer/attach.svg');
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
.mx_MessageComposerFormatBar {
|
||||
display: none;
|
||||
width: calc(32px * 5);
|
||||
width: calc(32px * 6);
|
||||
height: 32px;
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
|
@ -87,6 +87,11 @@ limitations under the License.
|
|||
.mx_MessageComposerFormatBar_buttonIconCode::after {
|
||||
mask-image: url('$(res)/img/element-icons/room/format-bar/code.svg');
|
||||
}
|
||||
|
||||
.mx_MessageComposerFormatBar_buttonIconInsertLink::after {
|
||||
mask-image: url('$(res)/img/element-icons/link.svg');
|
||||
mask-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_MessageComposerFormatBar_buttonTooltip {
|
||||
|
|
|
@ -52,4 +52,3 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -78,7 +78,8 @@ limitations under the License.
|
|||
|
||||
// Hack to cut content in <pre> tags too
|
||||
.mx_EventTile_pre_container > pre {
|
||||
overflow: hidden;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
|
|
|
@ -68,4 +68,3 @@ limitations under the License.
|
|||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -47,4 +47,3 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,4 +17,3 @@ limitations under the License.
|
|||
.mx_E2eAdvancedPanel_settingLongDescription {
|
||||
margin-right: 150px;
|
||||
}
|
||||
|
||||
|
|
|
@ -85,4 +85,3 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -35,7 +35,6 @@ limitations under the License.
|
|||
margin-left: 2px;
|
||||
margin-right: 2px;
|
||||
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
|
@ -48,7 +47,6 @@ limitations under the License.
|
|||
background-position: center;
|
||||
}
|
||||
|
||||
|
||||
&.mx_CallViewButtons_dialpad::before {
|
||||
background-image: url('$(res)/img/voip/dialpad.svg');
|
||||
}
|
||||
|
|
|
@ -200,7 +200,6 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
.mx_CallView_presenting {
|
||||
opacity: 1;
|
||||
transition: opacity 0.5s;
|
||||
|
|
|
@ -1,4 +1 @@
|
|||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 8V8C1.89543 8 1 7.10457 1 6V3C1 1.89543 1.89543 1 3 1H15C16.1046 1 17 1.89484 17 2.9994C17 3.88147 17 4.95392 17 6.00008C17 7.10465 16.1046 8 15 8H10.5" stroke="#737D8C" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.2011 16.7176C12.9087 17.011 12.9088 17.4866 13.2012 17.78C13.4936 18.0734 13.9677 18.0733 14.2601 17.78C14.9484 17.0894 15.6519 16.3829 16.1834 15.8491L16.8282 15.2014L17.0099 15.0188L17.0579 14.9706L17.0702 14.9582L17.0733 14.955L17.0741 14.9542L17.0743 14.954L17.0743 14.954L16.5444 14.4233L17.0744 14.954C17.3663 14.6606 17.3661 14.1855 17.0741 13.8922L14.2539 11.061C13.9616 10.7675 13.4875 10.7674 13.195 11.0606C12.9024 11.3539 12.9023 11.8295 13.1946 12.123L14.7442 13.6787L10.1137 13.6787C8.69795 13.6787 7.49996 12.4759 7.49996 10.9288L7.49996 7.00002C7.49996 6.58581 7.16417 6.25002 6.74996 6.25002C6.33574 6.25002 5.99996 6.58581 5.99996 7.00002L5.99996 10.9288C5.99996 13.2476 7.81395 15.1787 10.1137 15.1787H14.7341C14.2713 15.6436 13.7316 16.1854 13.2011 16.7176Z" fill="#737D8C"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" viewBox="0 0 18 18"><path fill="#17191C" d="M5 5.25a.75.75 0 0 0 0 1.5h8a.75.75 0 0 0 0-1.5H5ZM5 8.25a.75.75 0 0 0 0 1.5h4a.75.75 0 1 0 0-1.5H5Z"/><path fill="#17191C" fill-rule="evenodd" d="M3 .25A2.75 2.75 0 0 0 .25 3v14a.75.75 0 0 0 1.2.6L4.916 15c.217-.162.48-.25.75-.25H15A2.75 2.75 0 0 0 17.75 12V3A2.75 2.75 0 0 0 15 .25H3ZM1.75 3c0-.69.56-1.25 1.25-1.25h12c.69 0 1.25.56 1.25 1.25v9c0 .69-.56 1.25-1.25 1.25H5.666a2.75 2.75 0 0 0-1.65.55L1.75 15.5V3Z" clip-rule="evenodd"/></svg>
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 562 B |
|
@ -1,6 +1,6 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="20" height="20" rx="4" fill="#FF4B55"/>
|
||||
<rect width="20" height="20" rx="4" fill="#FF5B55"/>
|
||||
<path d="M2 7H18V16C18 17.1046 17.1046 18 16 18H4C2.89543 18 2 17.1046 2 16V7Z" fill="white"/>
|
||||
<rect x="3.96826" y="8.99951" width="2.99723" height="3" rx="0.25" fill="#FF4B55"/>
|
||||
<rect x="10.9614" y="13" width="2.99723" height="3" rx="0.25" fill="#FF4B55"/>
|
||||
<rect x="3.96826" y="8.99951" width="2.99723" height="3" rx="0.25" fill="#FF5B55"/>
|
||||
<rect x="10.9614" y="13" width="2.99723" height="3" rx="0.25" fill="#FF5B55"/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 430 B After Width: | Height: | Size: 430 B |
|
@ -28,5 +28,5 @@
|
|||
<path
|
||||
id="path2"
|
||||
d="M 12 2 C 6.47715 2 2 6.47715 2 12 C 2 17.5228 6.47715 22 12 22 C 17.5228 22 22 17.5228 22 12 C 22 6.47715 17.5228 2 12 2 z M 11.880859 5.5039062 C 12.720859 5.4439063 13.470547 6.0746875 13.560547 6.9296875 L 13.560547 7.1699219 L 13.080078 13.169922 C 13.035078 13.724922 12.570625 14.144531 12.015625 14.144531 L 11.925781 14.144531 C 11.400781 14.099531 10.996172 13.694922 10.951172 13.169922 L 10.470703 7.1699219 C 10.395703 6.3149219 11.025859 5.5639064 11.880859 5.5039062 z M 12 15.763672 C 12.729 15.763672 13.320312 16.354884 13.320312 17.083984 C 13.320313 17.812984 12.729 18.404297 12 18.404297 C 11.271 18.404297 10.679688 17.812984 10.679688 17.083984 C 10.679688 16.354884 11.271 15.763672 12 15.763672 z "
|
||||
style="fill:#ff4b55;fill-opacity:1" />
|
||||
style="fill:#ff5b55;fill-opacity:1" />
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
@ -1,3 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C9.23858 2 7 4.23858 7 7L17 7C17 4.23858 14.7614 2 12 2ZM2.29289 7.70711C1.90237 7.31658 1.90237 6.68342 2.29289 6.29289C2.68342 5.90237 3.31658 5.90237 3.70711 6.29289L6.41421 9H17.5858L20.2929 6.29289C20.6834 5.90237 21.3166 5.90237 21.7071 6.29289C22.0976 6.68342 22.0976 7.31658 21.7071 7.70711L19 10.4142V13H22C22.5523 13 23 13.4477 23 14C23 14.5523 22.5523 15 22 15H19C19 15.7795 18.8726 16.5292 18.6375 17.2295C18.6614 17.2493 18.6847 17.2705 18.7071 17.2929L21.7071 20.2929C22.0976 20.6834 22.0976 21.3166 21.7071 21.7071C21.3166 22.0976 20.6834 22.0976 20.2929 21.7071L17.6791 19.0933C16.5924 20.5983 14.9222 21.6542 13 21.9291L13 12C13 11.4477 12.5523 11 12 11C11.4477 11 11 11.4477 11 12V21.9291C9.07785 21.6542 7.40759 20.5983 6.32091 19.0933L3.70711 21.7071C3.31658 22.0976 2.68342 22.0976 2.29289 21.7071C1.90237 21.3166 1.90237 20.6834 2.29289 20.2929L5.29289 17.2929C5.31533 17.2705 5.33857 17.2493 5.36252 17.2295C5.1274 16.5292 5 15.7795 5 15H2C1.44772 15 1 14.5523 1 14C1 13.4477 1.44772 13 2 13H5V10.4142L2.29289 7.70711Z" fill="#FF4B55"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C9.23858 2 7 4.23858 7 7L17 7C17 4.23858 14.7614 2 12 2ZM2.29289 7.70711C1.90237 7.31658 1.90237 6.68342 2.29289 6.29289C2.68342 5.90237 3.31658 5.90237 3.70711 6.29289L6.41421 9H17.5858L20.2929 6.29289C20.6834 5.90237 21.3166 5.90237 21.7071 6.29289C22.0976 6.68342 22.0976 7.31658 21.7071 7.70711L19 10.4142V13H22C22.5523 13 23 13.4477 23 14C23 14.5523 22.5523 15 22 15H19C19 15.7795 18.8726 16.5292 18.6375 17.2295C18.6614 17.2493 18.6847 17.2705 18.7071 17.2929L21.7071 20.2929C22.0976 20.6834 22.0976 21.3166 21.7071 21.7071C21.3166 22.0976 20.6834 22.0976 20.2929 21.7071L17.6791 19.0933C16.5924 20.5983 14.9222 21.6542 13 21.9291L13 12C13 11.4477 12.5523 11 12 11C11.4477 11 11 11.4477 11 12V21.9291C9.07785 21.6542 7.40759 20.5983 6.32091 19.0933L3.70711 21.7071C3.31658 22.0976 2.68342 22.0976 2.29289 21.7071C1.90237 21.3166 1.90237 20.6834 2.29289 20.2929L5.29289 17.2929C5.31533 17.2705 5.33857 17.2493 5.36252 17.2295C5.1274 16.5292 5 15.7795 5 15H2C1.44772 15 1 14.5523 1 14C1 13.4477 1.44772 13 2 13H5V10.4142L2.29289 7.70711Z" fill="#FF5B55"/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
@ -165,6 +165,9 @@ $button-link-bg-color: transparent;
|
|||
// Toggle switch
|
||||
$togglesw-off-color: $room-highlight-color;
|
||||
|
||||
// Slider
|
||||
$slider-background-color: $quinary-content;
|
||||
|
||||
$progressbar-fg-color: $accent-color;
|
||||
$progressbar-bg-color: $system;
|
||||
|
||||
|
@ -209,6 +212,8 @@ $appearance-tab-border-color: $room-highlight-color;
|
|||
|
||||
$composer-shadow-color: rgba(0, 0, 0, 0.28);
|
||||
|
||||
$codeblock-background-color: #2a3039;
|
||||
|
||||
// Bubble tiles
|
||||
$eventbubble-self-bg: #14322E;
|
||||
$eventbubble-others-bg: $event-selected-color;
|
||||
|
|
|
@ -221,6 +221,8 @@ $appearance-tab-border-color: $room-highlight-color;
|
|||
|
||||
$composer-shadow-color: tranparent;
|
||||
|
||||
$codeblock-background-color: #2a3039;
|
||||
|
||||
// Bubble tiles
|
||||
$eventbubble-self-bg: #14322E;
|
||||
$eventbubble-others-bg: $event-selected-color;
|
||||
|
|
|
@ -334,6 +334,8 @@ $appearance-tab-border-color: $input-darker-bg-color;
|
|||
|
||||
$composer-shadow-color: tranparent;
|
||||
|
||||
$codeblock-background-color: $header-panel-bg-color;
|
||||
|
||||
// Bubble tiles
|
||||
$eventbubble-self-bg: #F0FBF8;
|
||||
$eventbubble-others-bg: $system;
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
//// Reference: https://www.figma.com/file/RnLKnv09glhxGIZtn8zfmh/UI-Themes-%26-Accessibility?node-id=321%3A65847
|
||||
$accent: #268075;
|
||||
$alert: #D62C25;
|
||||
$notice-primary-color: #D61C25;
|
||||
$links: #0A6ECA;
|
||||
$secondary-content: #5E6266;
|
||||
$tertiary-content: #5E6266; // Same as secondary
|
||||
$quaternary-content: #5E6266; // Same as secondary
|
||||
$tertiary-content: $secondary-content;
|
||||
$quaternary-content: $secondary-content;
|
||||
$quinary-content: $secondary-content;
|
||||
$roomlist-button-bg-color: rgba(141, 151, 165, 0.2);
|
||||
|
||||
$username-variant1-color: #0A6ECA;
|
||||
$username-variant2-color: #AC3BA8;
|
||||
|
@ -18,9 +19,13 @@ $username-variant8-color: #3E810A;
|
|||
|
||||
$accent-color: $accent;
|
||||
$accent-color-50pct: rgba($accent-color, 0.5);
|
||||
$accent-color-alt: $links;
|
||||
$input-border-color: $secondary-content;
|
||||
$input-darker-bg-color: $quinary-content;
|
||||
$input-darker-fg-color: $secondary-content;
|
||||
$input-lighter-fg-color: $input-darker-fg-color;
|
||||
$input-valid-border-color: $accent-color;
|
||||
$input-focused-border-color: $accent-color;
|
||||
$button-bg-color: $accent-color;
|
||||
$resend-button-divider-color: $input-darker-bg-color;
|
||||
$icon-button-color: $quaternary-content;
|
||||
|
@ -41,12 +46,14 @@ $voice-record-stop-border-color: $quinary-content;
|
|||
$voice-record-icon-color: $tertiary-content;
|
||||
$appearance-tab-border-color: $input-darker-bg-color;
|
||||
$eventbubble-reply-color: $quaternary-content;
|
||||
$notice-primary-color: $alert;
|
||||
$warning-color: $notice-primary-color; // red
|
||||
$pinned-unread-color: $notice-primary-color;
|
||||
$button-danger-bg-color: $notice-primary-color;
|
||||
$mention-user-pill-bg-color: $warning-color;
|
||||
$input-invalid-border-color: $warning-color;
|
||||
$event-highlight-fg-color: $warning-color;
|
||||
$roomtopic-color: $secondary-content;
|
||||
|
||||
@define-mixin mx_DialogButton_danger {
|
||||
background-color: $accent-color;
|
||||
|
@ -64,3 +71,38 @@ $event-highlight-fg-color: $warning-color;
|
|||
color: $accent-color;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton:focus {
|
||||
outline: 2px solid $accent-color;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.mx_BasicMessageComposer .mx_BasicMessageComposer_inputEmpty > :first-child::before {
|
||||
color: $secondary-content;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.mx_TextualEvent {
|
||||
color: $secondary-content;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.mx_Dialog, .mx_MatrixChat_wrapper {
|
||||
:not(.mx_textinput):not(.mx_Field):not(.mx_no_textinput) > input[type=text]::placeholder,
|
||||
:not(.mx_textinput):not(.mx_Field):not(.mx_no_textinput) > input[type=search]::placeholder,
|
||||
.mx_textinput input::placeholder {
|
||||
color: $input-darker-fg-color !important;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_UserMenu_contextMenu .mx_UserMenu_contextMenu_header .mx_UserMenu_contextMenu_themeButton {
|
||||
background-color: $roomlist-button-bg-color !important;
|
||||
}
|
||||
|
||||
.mx_FontScalingPanel_fontSlider {
|
||||
background-color: $roomlist-button-bg-color !important;
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ $space-nav: rgba($tertiary-content, 0.15);
|
|||
// try to use these colors when possible
|
||||
$accent-color: $accent;
|
||||
$accent-bg-color: rgba(3, 179, 129, 0.16);
|
||||
$notice-primary-color: #ff4b55;
|
||||
$notice-primary-color: $alert;
|
||||
$notice-primary-bg-color: rgba(255, 75, 85, 0.16);
|
||||
$header-panel-bg-color: #f3f8fd;
|
||||
|
||||
|
@ -318,8 +318,8 @@ $breadcrumb-placeholder-bg-color: #e8eef5;
|
|||
|
||||
// These two don't change between themes. They are the $warning-color, but we don't
|
||||
// want custom themes to affect them by accident.
|
||||
$voice-record-stop-symbol-color: #ff4b55;
|
||||
$voice-record-live-circle-color: #ff4b55;
|
||||
$voice-record-stop-symbol-color: #ff5b55;
|
||||
$voice-record-live-circle-color: #ff5b55;
|
||||
|
||||
$voice-record-stop-border-color: $quinary-content;
|
||||
$voice-record-icon-color: $tertiary-content;
|
||||
|
@ -333,6 +333,8 @@ $appearance-tab-border-color: $input-darker-bg-color;
|
|||
}
|
||||
$composer-shadow-color: rgba(0, 0, 0, 0.04);
|
||||
|
||||
$codeblock-background-color: $header-panel-bg-color;
|
||||
|
||||
// Bubble tiles
|
||||
$eventbubble-self-bg: #F0FBF8;
|
||||
$eventbubble-others-bg: $system;
|
||||
|
|
|
@ -99,6 +99,7 @@ declare global {
|
|||
mxSkinner?: Skinner;
|
||||
mxOnRecaptchaLoaded?: () => void;
|
||||
electron?: Electron;
|
||||
mxSendSentryReport: (userText: string, issueUrl: string, error: Error) => Promise<void>;
|
||||
}
|
||||
|
||||
interface DesktopCapturerSource {
|
||||
|
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import classNames from 'classnames';
|
||||
import { defer } from "matrix-js-sdk/src/utils";
|
||||
import { defer, sleep } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import Analytics from './Analytics';
|
||||
import dis from './dispatcher/dispatcher';
|
||||
|
@ -332,7 +332,10 @@ export class ModalManager {
|
|||
return this.priorityModal ? this.priorityModal : (this.modals[0] || this.staticModal);
|
||||
}
|
||||
|
||||
private reRender() {
|
||||
private async reRender() {
|
||||
// await next tick because sometimes ReactDOM can race with itself and cause the modal to wrongly stick around
|
||||
await sleep(0);
|
||||
|
||||
if (this.modals.length === 0 && !this.priorityModal && !this.staticModal) {
|
||||
// If there is no modal to render, make all of Element available
|
||||
// to screen reader users again
|
||||
|
|
|
@ -32,6 +32,10 @@ import AccessSecretStorageDialog from './components/views/dialogs/security/Acces
|
|||
import RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreKeyBackupDialog';
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import SecurityCustomisations from "./customisations/Security";
|
||||
import { DeviceTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning';
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { ComponentType } from "react";
|
||||
|
||||
// This stores the secret storage private keys in memory for the JS SDK. This is
|
||||
// only meant to act as a cache to avoid prompting the user multiple times
|
||||
|
@ -335,7 +339,9 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
|
|||
// This dialog calls bootstrap itself after guiding the user through
|
||||
// passphrase creation.
|
||||
const { finished } = Modal.createTrackedDialogAsync('Create Secret Storage dialog', '',
|
||||
import("./async-components/views/dialogs/security/CreateSecretStorageDialog"),
|
||||
import(
|
||||
"./async-components/views/dialogs/security/CreateSecretStorageDialog"
|
||||
) as unknown as Promise<ComponentType<{}>>,
|
||||
{
|
||||
forceReset,
|
||||
},
|
||||
|
|
|
@ -1013,14 +1013,14 @@ export const Commands = [
|
|||
new Command({
|
||||
command: "msg",
|
||||
description: _td("Sends a message to the given user"),
|
||||
args: "<user-id> <message>",
|
||||
args: "<user-id> [<message>]",
|
||||
runFn: function(roomId, args) {
|
||||
if (args) {
|
||||
// matches the first whitespace delimited group and then the rest of the string
|
||||
const matches = args.match(/^(\S+?)(?: +(.*))?$/s);
|
||||
if (matches) {
|
||||
const [userId, msg] = matches.slice(1);
|
||||
if (msg && userId && userId.startsWith("@") && userId.includes(":")) {
|
||||
if (userId && userId.startsWith("@") && userId.includes(":")) {
|
||||
return success((async () => {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const roomId = await ensureDMExists(cli, userId);
|
||||
|
@ -1028,7 +1028,9 @@ export const Commands = [
|
|||
action: 'view_room',
|
||||
room_id: roomId,
|
||||
});
|
||||
cli.sendTextMessage(roomId, msg);
|
||||
if (msg) {
|
||||
cli.sendTextMessage(roomId, msg);
|
||||
}
|
||||
})());
|
||||
}
|
||||
}
|
||||
|
|
31
src/Terms.ts
|
@ -180,7 +180,7 @@ export async function startTermsFlow(
|
|||
return Promise.all(agreePromises);
|
||||
}
|
||||
|
||||
export function dialogTermsInteractionCallback(
|
||||
export async function dialogTermsInteractionCallback(
|
||||
policiesAndServicePairs: {
|
||||
service: Service;
|
||||
policies: { [policy: string]: Policy };
|
||||
|
@ -188,21 +188,18 @@ export function dialogTermsInteractionCallback(
|
|||
agreedUrls: string[],
|
||||
extraClassNames?: string,
|
||||
): Promise<string[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
logger.log("Terms that need agreement", policiesAndServicePairs);
|
||||
// FIXME: Using an import will result in test failures
|
||||
const TermsDialog = sdk.getComponent("views.dialogs.TermsDialog");
|
||||
logger.log("Terms that need agreement", policiesAndServicePairs);
|
||||
// FIXME: Using an import will result in test failures
|
||||
const TermsDialog = sdk.getComponent("views.dialogs.TermsDialog");
|
||||
|
||||
Modal.createTrackedDialog('Terms of Service', '', TermsDialog, {
|
||||
policiesAndServicePairs,
|
||||
agreedUrls,
|
||||
onFinished: (done, agreedUrls) => {
|
||||
if (!done) {
|
||||
reject(new TermsNotSignedError());
|
||||
return;
|
||||
}
|
||||
resolve(agreedUrls);
|
||||
},
|
||||
}, classNames("mx_TermsDialog", extraClassNames));
|
||||
});
|
||||
const { finished } = Modal.createTrackedDialog<[boolean, string[]]>('Terms of Service', '', TermsDialog, {
|
||||
policiesAndServicePairs,
|
||||
agreedUrls,
|
||||
}, classNames("mx_TermsDialog", extraClassNames));
|
||||
|
||||
const [done, _agreedUrls] = await finished;
|
||||
if (!done) {
|
||||
throw new TermsNotSignedError();
|
||||
}
|
||||
return _agreedUrls;
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
@ -28,7 +29,11 @@ import { RightPanelPhases } from './stores/RightPanelStorePhases';
|
|||
import { Action } from './dispatcher/actions';
|
||||
import defaultDispatcher from './dispatcher/dispatcher';
|
||||
import { SetRightPanelPhasePayload } from './dispatcher/payloads/SetRightPanelPhasePayload';
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { GuestAccess, HistoryVisibility, JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||
import { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
|
||||
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||
import { ROOM_SECURITY_TAB } from "./components/views/dialogs/RoomSettingsDialog";
|
||||
|
||||
// These functions are frequently used just to check whether an event has
|
||||
// any text to display at all. For this reason they return deferred values
|
||||
|
@ -201,17 +206,38 @@ function textForTombstoneEvent(ev: MatrixEvent): () => string | null {
|
|||
return () => _t('%(senderDisplayName)s upgraded this room.', { senderDisplayName });
|
||||
}
|
||||
|
||||
function textForJoinRulesEvent(ev: MatrixEvent): () => string | null {
|
||||
const onViewJoinRuleSettingsClick = () => {
|
||||
defaultDispatcher.dispatch({
|
||||
action: "open_room_settings",
|
||||
initial_tab_id: ROOM_SECURITY_TAB,
|
||||
});
|
||||
};
|
||||
|
||||
function textForJoinRulesEvent(ev: MatrixEvent, allowJSX: boolean): () => string | JSX.Element | null {
|
||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
switch (ev.getContent().join_rule) {
|
||||
case "public":
|
||||
case JoinRule.Public:
|
||||
return () => _t('%(senderDisplayName)s made the room public to whoever knows the link.', {
|
||||
senderDisplayName,
|
||||
});
|
||||
case "invite":
|
||||
case JoinRule.Invite:
|
||||
return () => _t('%(senderDisplayName)s made the room invite only.', {
|
||||
senderDisplayName,
|
||||
});
|
||||
case JoinRule.Restricted:
|
||||
if (allowJSX) {
|
||||
return () => <span>
|
||||
{ _t('%(senderDisplayName)s changed who can join this room. <a>View settings</a>.', {
|
||||
senderDisplayName,
|
||||
}, {
|
||||
"a": (sub) => <a onClick={onViewJoinRuleSettingsClick}>
|
||||
{ sub }
|
||||
</a>,
|
||||
}) }
|
||||
</span>;
|
||||
}
|
||||
|
||||
return () => _t('%(senderDisplayName)s changed who can join this room.', { senderDisplayName });
|
||||
default:
|
||||
// The spec supports "knock" and "private", however nothing implements these.
|
||||
return () => _t('%(senderDisplayName)s changed the join rule to %(rule)s', {
|
||||
|
@ -224,9 +250,9 @@ function textForJoinRulesEvent(ev: MatrixEvent): () => string | null {
|
|||
function textForGuestAccessEvent(ev: MatrixEvent): () => string | null {
|
||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
switch (ev.getContent().guest_access) {
|
||||
case "can_join":
|
||||
case GuestAccess.CanJoin:
|
||||
return () => _t('%(senderDisplayName)s has allowed guests to join the room.', { senderDisplayName });
|
||||
case "forbidden":
|
||||
case GuestAccess.Forbidden:
|
||||
return () => _t('%(senderDisplayName)s has prevented guests from joining the room.', { senderDisplayName });
|
||||
default:
|
||||
// There's no other options we can expect, however just for safety's sake we'll do this.
|
||||
|
@ -312,11 +338,11 @@ function textForMessageEvent(ev: MatrixEvent): () => string | null {
|
|||
|| redactedBecauseUserId });
|
||||
}
|
||||
}
|
||||
if (ev.getContent().msgtype === "m.emote") {
|
||||
if (ev.getContent().msgtype === MsgType.Emote) {
|
||||
message = "* " + senderDisplayName + " " + message;
|
||||
} else if (ev.getContent().msgtype === "m.image") {
|
||||
} else if (ev.getContent().msgtype === MsgType.Image) {
|
||||
message = _t('%(senderDisplayName)s sent an image.', { senderDisplayName });
|
||||
} else if (ev.getType() == "m.sticker") {
|
||||
} else if (ev.getType() == EventType.Sticker) {
|
||||
message = _t('%(senderDisplayName)s sent a sticker.', { senderDisplayName });
|
||||
} else {
|
||||
// in this case, parse it as a plain text message
|
||||
|
@ -396,15 +422,15 @@ function textForThreePidInviteEvent(event: MatrixEvent): () => string | null {
|
|||
function textForHistoryVisibilityEvent(event: MatrixEvent): () => string | null {
|
||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||
switch (event.getContent().history_visibility) {
|
||||
case 'invited':
|
||||
case HistoryVisibility.Invited:
|
||||
return () => _t('%(senderName)s made future room history visible to all room members, '
|
||||
+ 'from the point they are invited.', { senderName });
|
||||
case 'joined':
|
||||
case HistoryVisibility.Joined:
|
||||
return () => _t('%(senderName)s made future room history visible to all room members, '
|
||||
+ 'from the point they joined.', { senderName });
|
||||
case 'shared':
|
||||
case HistoryVisibility.Shared:
|
||||
return () => _t('%(senderName)s made future room history visible to all room members.', { senderName });
|
||||
case 'world_readable':
|
||||
case HistoryVisibility.WorldReadable:
|
||||
return () => _t('%(senderName)s made future room history visible to anyone.', { senderName });
|
||||
default:
|
||||
return () => _t('%(senderName)s made future room history visible to unknown (%(visibility)s).', {
|
||||
|
@ -695,25 +721,25 @@ interface IHandlers {
|
|||
}
|
||||
|
||||
const handlers: IHandlers = {
|
||||
'm.room.message': textForMessageEvent,
|
||||
'm.sticker': textForMessageEvent,
|
||||
'm.call.invite': textForCallInviteEvent,
|
||||
[EventType.RoomMessage]: textForMessageEvent,
|
||||
[EventType.Sticker]: textForMessageEvent,
|
||||
[EventType.CallInvite]: textForCallInviteEvent,
|
||||
};
|
||||
|
||||
const stateHandlers: IHandlers = {
|
||||
'm.room.canonical_alias': textForCanonicalAliasEvent,
|
||||
'm.room.name': textForRoomNameEvent,
|
||||
'm.room.topic': textForTopicEvent,
|
||||
'm.room.member': textForMemberEvent,
|
||||
"m.room.avatar": textForRoomAvatarEvent,
|
||||
'm.room.third_party_invite': textForThreePidInviteEvent,
|
||||
'm.room.history_visibility': textForHistoryVisibilityEvent,
|
||||
'm.room.power_levels': textForPowerEvent,
|
||||
'm.room.pinned_events': textForPinnedEvent,
|
||||
'm.room.server_acl': textForServerACLEvent,
|
||||
'm.room.tombstone': textForTombstoneEvent,
|
||||
'm.room.join_rules': textForJoinRulesEvent,
|
||||
'm.room.guest_access': textForGuestAccessEvent,
|
||||
[EventType.RoomCanonicalAlias]: textForCanonicalAliasEvent,
|
||||
[EventType.RoomName]: textForRoomNameEvent,
|
||||
[EventType.RoomTopic]: textForTopicEvent,
|
||||
[EventType.RoomMember]: textForMemberEvent,
|
||||
[EventType.RoomAvatar]: textForRoomAvatarEvent,
|
||||
[EventType.RoomThirdPartyInvite]: textForThreePidInviteEvent,
|
||||
[EventType.RoomHistoryVisibility]: textForHistoryVisibilityEvent,
|
||||
[EventType.RoomPowerLevels]: textForPowerEvent,
|
||||
[EventType.RoomPinnedEvents]: textForPinnedEvent,
|
||||
[EventType.RoomServerAcl]: textForServerACLEvent,
|
||||
[EventType.RoomTombstone]: textForTombstoneEvent,
|
||||
[EventType.RoomJoinRules]: textForJoinRulesEvent,
|
||||
[EventType.RoomGuestAccess]: textForGuestAccessEvent,
|
||||
'm.room.related_groups': textForRelatedGroupsEvent,
|
||||
|
||||
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
|
||||
|
|
|
@ -24,6 +24,7 @@ import React, {
|
|||
useReducer,
|
||||
Reducer,
|
||||
Dispatch,
|
||||
RefObject,
|
||||
} from "react";
|
||||
|
||||
import { Key } from "../Keyboard";
|
||||
|
@ -63,7 +64,7 @@ const RovingTabIndexContext = createContext<IContext>({
|
|||
});
|
||||
RovingTabIndexContext.displayName = "RovingTabIndexContext";
|
||||
|
||||
enum Type {
|
||||
export enum Type {
|
||||
Register = "REGISTER",
|
||||
Unregister = "UNREGISTER",
|
||||
SetFocus = "SET_FOCUS",
|
||||
|
@ -76,73 +77,67 @@ interface IAction {
|
|||
};
|
||||
}
|
||||
|
||||
const reducer = (state: IState, action: IAction) => {
|
||||
export const reducer = (state: IState, action: IAction) => {
|
||||
switch (action.type) {
|
||||
case Type.Register: {
|
||||
if (state.refs.length === 0) {
|
||||
let left = 0;
|
||||
let right = state.refs.length - 1;
|
||||
let index = state.refs.length; // by default append to the end
|
||||
|
||||
// do a binary search to find the right slot
|
||||
while (left <= right) {
|
||||
index = Math.floor((left + right) / 2);
|
||||
const ref = state.refs[index];
|
||||
|
||||
if (ref === action.payload.ref) {
|
||||
return state; // already in refs, this should not happen
|
||||
}
|
||||
|
||||
if (action.payload.ref.current.compareDocumentPosition(ref.current) & DOCUMENT_POSITION_PRECEDING) {
|
||||
left = ++index;
|
||||
} else {
|
||||
right = index - 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!state.activeRef) {
|
||||
// Our list of refs was empty, set activeRef to this first item
|
||||
return {
|
||||
...state,
|
||||
activeRef: action.payload.ref,
|
||||
refs: [action.payload.ref],
|
||||
};
|
||||
}
|
||||
|
||||
if (state.refs.includes(action.payload.ref)) {
|
||||
return state; // already in refs, this should not happen
|
||||
}
|
||||
|
||||
// find the index of the first ref which is not preceding this one in DOM order
|
||||
let newIndex = state.refs.findIndex(ref => {
|
||||
return ref.current.compareDocumentPosition(action.payload.ref.current) & DOCUMENT_POSITION_PRECEDING;
|
||||
});
|
||||
|
||||
if (newIndex < 0) {
|
||||
newIndex = state.refs.length; // append to the end
|
||||
state.activeRef = action.payload.ref;
|
||||
}
|
||||
|
||||
// update the refs list
|
||||
return {
|
||||
...state,
|
||||
refs: [
|
||||
...state.refs.slice(0, newIndex),
|
||||
action.payload.ref,
|
||||
...state.refs.slice(newIndex),
|
||||
],
|
||||
};
|
||||
if (index < state.refs.length) {
|
||||
state.refs.splice(index, 0, action.payload.ref);
|
||||
} else {
|
||||
state.refs.push(action.payload.ref);
|
||||
}
|
||||
return { ...state };
|
||||
}
|
||||
case Type.Unregister: {
|
||||
// filter out the ref which we are removing
|
||||
const refs = state.refs.filter(r => r !== action.payload.ref);
|
||||
|
||||
if (refs.length === state.refs.length) {
|
||||
case Type.Unregister: {
|
||||
const oldIndex = state.refs.findIndex(r => r === action.payload.ref);
|
||||
|
||||
if (oldIndex === -1) {
|
||||
return state; // already removed, this should not happen
|
||||
}
|
||||
|
||||
if (state.activeRef === action.payload.ref) {
|
||||
if (state.refs.splice(oldIndex, 1)[0] === state.activeRef) {
|
||||
// we just removed the active ref, need to replace it
|
||||
// pick the ref which is now in the index the old ref was in
|
||||
const oldIndex = state.refs.findIndex(r => r === action.payload.ref);
|
||||
return {
|
||||
...state,
|
||||
activeRef: oldIndex >= refs.length ? refs[refs.length - 1] : refs[oldIndex],
|
||||
refs,
|
||||
};
|
||||
const len = state.refs.length;
|
||||
state.activeRef = oldIndex >= len ? state.refs[len - 1] : state.refs[oldIndex];
|
||||
}
|
||||
|
||||
// update the refs list
|
||||
return {
|
||||
...state,
|
||||
refs,
|
||||
};
|
||||
return { ...state };
|
||||
}
|
||||
|
||||
case Type.SetFocus: {
|
||||
// update active ref
|
||||
return {
|
||||
...state,
|
||||
activeRef: action.payload.ref,
|
||||
};
|
||||
state.activeRef = action.payload.ref;
|
||||
return { ...state };
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
@ -151,13 +146,40 @@ const reducer = (state: IState, action: IAction) => {
|
|||
interface IProps {
|
||||
handleHomeEnd?: boolean;
|
||||
handleUpDown?: boolean;
|
||||
handleLeftRight?: boolean;
|
||||
children(renderProps: {
|
||||
onKeyDownHandler(ev: React.KeyboardEvent);
|
||||
});
|
||||
onKeyDown?(ev: React.KeyboardEvent, state: IState);
|
||||
}
|
||||
|
||||
export const RovingTabIndexProvider: React.FC<IProps> = ({ children, handleHomeEnd, handleUpDown, onKeyDown }) => {
|
||||
export const findSiblingElement = (
|
||||
refs: RefObject<HTMLElement>[],
|
||||
startIndex: number,
|
||||
backwards = false,
|
||||
): RefObject<HTMLElement> => {
|
||||
if (backwards) {
|
||||
for (let i = startIndex; i < refs.length && i >= 0; i--) {
|
||||
if (refs[i].current.offsetParent !== null) {
|
||||
return refs[i];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let i = startIndex; i < refs.length && i >= 0; i++) {
|
||||
if (refs[i].current.offsetParent !== null) {
|
||||
return refs[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const RovingTabIndexProvider: React.FC<IProps> = ({
|
||||
children,
|
||||
handleHomeEnd,
|
||||
handleUpDown,
|
||||
handleLeftRight,
|
||||
onKeyDown,
|
||||
}) => {
|
||||
const [state, dispatch] = useReducer<Reducer<IState, IAction>>(reducer, {
|
||||
activeRef: null,
|
||||
refs: [],
|
||||
|
@ -166,6 +188,13 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({ children, handleHomeE
|
|||
const context = useMemo<IContext>(() => ({ state, dispatch }), [state]);
|
||||
|
||||
const onKeyDownHandler = useCallback((ev) => {
|
||||
if (onKeyDown) {
|
||||
onKeyDown(ev, context.state);
|
||||
if (ev.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let handled = false;
|
||||
// Don't interfere with input default keydown behaviour
|
||||
if (ev.target.tagName !== "INPUT" && ev.target.tagName !== "TEXTAREA") {
|
||||
|
@ -174,43 +203,37 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({ children, handleHomeE
|
|||
case Key.HOME:
|
||||
if (handleHomeEnd) {
|
||||
handled = true;
|
||||
// move focus to first item
|
||||
if (context.state.refs.length > 0) {
|
||||
context.state.refs[0].current.focus();
|
||||
}
|
||||
// move focus to first (visible) item
|
||||
findSiblingElement(context.state.refs, 0)?.current?.focus();
|
||||
}
|
||||
break;
|
||||
|
||||
case Key.END:
|
||||
if (handleHomeEnd) {
|
||||
handled = true;
|
||||
// move focus to last item
|
||||
if (context.state.refs.length > 0) {
|
||||
context.state.refs[context.state.refs.length - 1].current.focus();
|
||||
}
|
||||
// move focus to last (visible) item
|
||||
findSiblingElement(context.state.refs, context.state.refs.length - 1, true)?.current?.focus();
|
||||
}
|
||||
break;
|
||||
|
||||
case Key.ARROW_UP:
|
||||
if (handleUpDown) {
|
||||
case Key.ARROW_RIGHT:
|
||||
if ((ev.key === Key.ARROW_UP && handleUpDown) || (ev.key === Key.ARROW_RIGHT && handleLeftRight)) {
|
||||
handled = true;
|
||||
if (context.state.refs.length > 0) {
|
||||
const idx = context.state.refs.indexOf(context.state.activeRef);
|
||||
if (idx > 0) {
|
||||
context.state.refs[idx - 1].current.focus();
|
||||
}
|
||||
findSiblingElement(context.state.refs, idx - 1)?.current?.focus();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case Key.ARROW_DOWN:
|
||||
if (handleUpDown) {
|
||||
case Key.ARROW_LEFT:
|
||||
if ((ev.key === Key.ARROW_DOWN && handleUpDown) || (ev.key === Key.ARROW_LEFT && handleLeftRight)) {
|
||||
handled = true;
|
||||
if (context.state.refs.length > 0) {
|
||||
const idx = context.state.refs.indexOf(context.state.activeRef);
|
||||
if (idx < context.state.refs.length - 1) {
|
||||
context.state.refs[idx + 1].current.focus();
|
||||
}
|
||||
findSiblingElement(context.state.refs, idx + 1, true)?.current?.focus();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -220,10 +243,8 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({ children, handleHomeE
|
|||
if (handled) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
} else if (onKeyDown) {
|
||||
return onKeyDown(ev, context.state);
|
||||
}
|
||||
}, [context.state, onKeyDown, handleHomeEnd, handleUpDown]);
|
||||
}, [context.state, onKeyDown, handleHomeEnd, handleUpDown, handleLeftRight]);
|
||||
|
||||
return <RovingTabIndexContext.Provider value={context}>
|
||||
{ children({ onKeyDownHandler }) }
|
||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
import React from "react";
|
||||
|
||||
import { IState, RovingTabIndexProvider } from "./RovingTabIndex";
|
||||
import { RovingTabIndexProvider } from "./RovingTabIndex";
|
||||
import { Key } from "../Keyboard";
|
||||
|
||||
interface IProps extends Omit<React.HTMLProps<HTMLDivElement>, "onKeyDown"> {
|
||||
|
@ -26,7 +26,7 @@ interface IProps extends Omit<React.HTMLProps<HTMLDivElement>, "onKeyDown"> {
|
|||
// https://www.w3.org/TR/wai-aria-practices-1.1/#toolbar
|
||||
// All buttons passed in children must use RovingTabIndex to set `onFocus`, `isActive`, `ref`
|
||||
const Toolbar: React.FC<IProps> = ({ children, ...props }) => {
|
||||
const onKeyDown = (ev: React.KeyboardEvent, state: IState) => {
|
||||
const onKeyDown = (ev: React.KeyboardEvent) => {
|
||||
const target = ev.target as HTMLElement;
|
||||
// Don't interfere with input default keydown behaviour
|
||||
if (target.tagName === "INPUT") return;
|
||||
|
@ -42,15 +42,6 @@ const Toolbar: React.FC<IProps> = ({ children, ...props }) => {
|
|||
}
|
||||
break;
|
||||
|
||||
case Key.ARROW_LEFT:
|
||||
case Key.ARROW_RIGHT:
|
||||
if (state.refs.length > 0) {
|
||||
const i = state.refs.findIndex(r => r === state.activeRef);
|
||||
const delta = ev.key === Key.ARROW_RIGHT ? 1 : -1;
|
||||
state.refs.slice((i + delta) % state.refs.length)[0].current.focus();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
handled = false;
|
||||
}
|
||||
|
|
|
@ -17,56 +17,70 @@ limitations under the License.
|
|||
|
||||
import React, { createRef } from 'react';
|
||||
import FileSaver from 'file-saver';
|
||||
import PropTypes from 'prop-types';
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import * as sdk from '../../../../index';
|
||||
import { MatrixClientPeg } from '../../../../MatrixClientPeg';
|
||||
import { _t, _td } from '../../../../languageHandler';
|
||||
import { accessSecretStorage } from '../../../../SecurityManager';
|
||||
import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
|
||||
import { copyNode } from "../../../../utils/strings";
|
||||
import PassphraseField from "../../../../components/views/auth/PassphraseField";
|
||||
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
|
||||
import Field from "../../../../components/views/elements/Field";
|
||||
import Spinner from "../../../../components/views/elements/Spinner";
|
||||
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
|
||||
import DialogButtons from "../../../../components/views/elements/DialogButtons";
|
||||
import { IValidationResult } from "../../../../components/views/elements/Validation";
|
||||
import { IPreparedKeyBackupVersion } from "matrix-js-sdk/src/crypto/backup";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
const PHASE_PASSPHRASE = 0;
|
||||
const PHASE_PASSPHRASE_CONFIRM = 1;
|
||||
const PHASE_SHOWKEY = 2;
|
||||
const PHASE_KEEPITSAFE = 3;
|
||||
const PHASE_BACKINGUP = 4;
|
||||
const PHASE_DONE = 5;
|
||||
const PHASE_OPTOUT_CONFIRM = 6;
|
||||
enum Phase {
|
||||
Passphrase = "passphrase",
|
||||
PassphraseConfirm = "passphrase_confirm",
|
||||
ShowKey = "show_key",
|
||||
KeepItSafe = "keep_it_safe",
|
||||
BackingUp = "backing_up",
|
||||
Done = "done",
|
||||
OptOutConfirm = "opt_out_confirm",
|
||||
}
|
||||
|
||||
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
|
||||
|
||||
interface IProps extends IDialogProps {}
|
||||
|
||||
interface IState {
|
||||
secureSecretStorage: boolean;
|
||||
phase: Phase;
|
||||
passPhrase: string;
|
||||
passPhraseValid: boolean;
|
||||
passPhraseConfirm: string;
|
||||
copied: boolean;
|
||||
downloaded: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/*
|
||||
* Walks the user through the process of creating an e2e key backup
|
||||
* on the server.
|
||||
*/
|
||||
export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
}
|
||||
export default class CreateKeyBackupDialog extends React.PureComponent<IProps, IState> {
|
||||
private keyBackupInfo: Pick<IPreparedKeyBackupVersion, "recovery_key" | "algorithm" | "auth_data">;
|
||||
private recoveryKeyNode = createRef<HTMLElement>();
|
||||
private passphraseField = createRef<Field>();
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this._recoveryKeyNode = null;
|
||||
this._keyBackupInfo = null;
|
||||
|
||||
this.state = {
|
||||
secureSecretStorage: null,
|
||||
phase: PHASE_PASSPHRASE,
|
||||
phase: Phase.Passphrase,
|
||||
passPhrase: '',
|
||||
passPhraseValid: false,
|
||||
passPhraseConfirm: '',
|
||||
copied: false,
|
||||
downloaded: false,
|
||||
};
|
||||
|
||||
this._passphraseField = createRef();
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
public async componentDidMount(): Promise<void> {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const secureSecretStorage = await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
|
||||
this.setState({ secureSecretStorage });
|
||||
|
@ -74,41 +88,37 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
|||
// If we're using secret storage, skip ahead to the backing up step, as
|
||||
// `accessSecretStorage` will handle passphrases as needed.
|
||||
if (secureSecretStorage) {
|
||||
this.setState({ phase: PHASE_BACKINGUP });
|
||||
this._createBackup();
|
||||
this.setState({ phase: Phase.BackingUp });
|
||||
this.createBackup();
|
||||
}
|
||||
}
|
||||
|
||||
_collectRecoveryKeyNode = (n) => {
|
||||
this._recoveryKeyNode = n;
|
||||
}
|
||||
|
||||
_onCopyClick = () => {
|
||||
const successful = copyNode(this._recoveryKeyNode);
|
||||
private onCopyClick = (): void => {
|
||||
const successful = copyNode(this.recoveryKeyNode.current);
|
||||
if (successful) {
|
||||
this.setState({
|
||||
copied: true,
|
||||
phase: PHASE_KEEPITSAFE,
|
||||
phase: Phase.KeepItSafe,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_onDownloadClick = () => {
|
||||
const blob = new Blob([this._keyBackupInfo.recovery_key], {
|
||||
private onDownloadClick = (): void => {
|
||||
const blob = new Blob([this.keyBackupInfo.recovery_key], {
|
||||
type: 'text/plain;charset=us-ascii',
|
||||
});
|
||||
FileSaver.saveAs(blob, 'security-key.txt');
|
||||
|
||||
this.setState({
|
||||
downloaded: true,
|
||||
phase: PHASE_KEEPITSAFE,
|
||||
phase: Phase.KeepItSafe,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_createBackup = async () => {
|
||||
private createBackup = async (): Promise<void> => {
|
||||
const { secureSecretStorage } = this.state;
|
||||
this.setState({
|
||||
phase: PHASE_BACKINGUP,
|
||||
phase: Phase.BackingUp,
|
||||
error: null,
|
||||
});
|
||||
let info;
|
||||
|
@ -123,12 +133,12 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
|||
});
|
||||
} else {
|
||||
info = await MatrixClientPeg.get().createKeyBackupVersion(
|
||||
this._keyBackupInfo,
|
||||
this.keyBackupInfo,
|
||||
);
|
||||
}
|
||||
await MatrixClientPeg.get().scheduleAllGroupSessionsForBackup();
|
||||
this.setState({
|
||||
phase: PHASE_DONE,
|
||||
phase: Phase.Done,
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error("Error creating key backup", e);
|
||||
|
@ -143,97 +153,91 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
|||
error: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_onCancel = () => {
|
||||
private onCancel = (): void => {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
};
|
||||
|
||||
_onDone = () => {
|
||||
private onDone = (): void => {
|
||||
this.props.onFinished(true);
|
||||
}
|
||||
};
|
||||
|
||||
_onOptOutClick = () => {
|
||||
this.setState({ phase: PHASE_OPTOUT_CONFIRM });
|
||||
}
|
||||
private onSetUpClick = (): void => {
|
||||
this.setState({ phase: Phase.Passphrase });
|
||||
};
|
||||
|
||||
_onSetUpClick = () => {
|
||||
this.setState({ phase: PHASE_PASSPHRASE });
|
||||
}
|
||||
|
||||
_onSkipPassPhraseClick = async () => {
|
||||
this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion();
|
||||
private onSkipPassPhraseClick = async (): Promise<void> => {
|
||||
this.keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion();
|
||||
this.setState({
|
||||
copied: false,
|
||||
downloaded: false,
|
||||
phase: PHASE_SHOWKEY,
|
||||
phase: Phase.ShowKey,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_onPassPhraseNextClick = async (e) => {
|
||||
private onPassPhraseNextClick = async (e: React.FormEvent): Promise<void> => {
|
||||
e.preventDefault();
|
||||
if (!this._passphraseField.current) return; // unmounting
|
||||
if (!this.passphraseField.current) return; // unmounting
|
||||
|
||||
await this._passphraseField.current.validate({ allowEmpty: false });
|
||||
if (!this._passphraseField.current.state.valid) {
|
||||
this._passphraseField.current.focus();
|
||||
this._passphraseField.current.validate({ allowEmpty: false, focused: true });
|
||||
await this.passphraseField.current.validate({ allowEmpty: false });
|
||||
if (!this.passphraseField.current.state.valid) {
|
||||
this.passphraseField.current.focus();
|
||||
this.passphraseField.current.validate({ allowEmpty: false, focused: true });
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ phase: PHASE_PASSPHRASE_CONFIRM });
|
||||
this.setState({ phase: Phase.PassphraseConfirm });
|
||||
};
|
||||
|
||||
_onPassPhraseConfirmNextClick = async (e) => {
|
||||
private onPassPhraseConfirmNextClick = async (e: React.FormEvent): Promise<void> => {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.state.passPhrase !== this.state.passPhraseConfirm) return;
|
||||
|
||||
this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion(this.state.passPhrase);
|
||||
this.keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion(this.state.passPhrase);
|
||||
this.setState({
|
||||
copied: false,
|
||||
downloaded: false,
|
||||
phase: PHASE_SHOWKEY,
|
||||
phase: Phase.ShowKey,
|
||||
});
|
||||
};
|
||||
|
||||
_onSetAgainClick = () => {
|
||||
private onSetAgainClick = (): void => {
|
||||
this.setState({
|
||||
passPhrase: '',
|
||||
passPhraseValid: false,
|
||||
passPhraseConfirm: '',
|
||||
phase: PHASE_PASSPHRASE,
|
||||
phase: Phase.Passphrase,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_onKeepItSafeBackClick = () => {
|
||||
private onKeepItSafeBackClick = (): void => {
|
||||
this.setState({
|
||||
phase: PHASE_SHOWKEY,
|
||||
phase: Phase.ShowKey,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_onPassPhraseValidate = (result) => {
|
||||
private onPassPhraseValidate = (result: IValidationResult): void => {
|
||||
this.setState({
|
||||
passPhraseValid: result.valid,
|
||||
});
|
||||
};
|
||||
|
||||
_onPassPhraseChange = (e) => {
|
||||
private onPassPhraseChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
passPhrase: e.target.value,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_onPassPhraseConfirmChange = (e) => {
|
||||
private onPassPhraseConfirmChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
passPhraseConfirm: e.target.value,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_renderPhasePassPhrase() {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
||||
return <form onSubmit={this._onPassPhraseNextClick}>
|
||||
private renderPhasePassPhrase(): JSX.Element {
|
||||
return <form onSubmit={this.onPassPhraseNextClick}>
|
||||
<p>{ _t(
|
||||
"<b>Warning</b>: You should only set up key backup from a trusted computer.", {},
|
||||
{ b: sub => <b>{ sub }</b> },
|
||||
|
@ -248,11 +252,11 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
|||
<div className="mx_CreateKeyBackupDialog_passPhraseContainer">
|
||||
<PassphraseField
|
||||
className="mx_CreateKeyBackupDialog_passPhraseInput"
|
||||
onChange={this._onPassPhraseChange}
|
||||
onChange={this.onPassPhraseChange}
|
||||
minScore={PASSWORD_MIN_SCORE}
|
||||
value={this.state.passPhrase}
|
||||
onValidate={this._onPassPhraseValidate}
|
||||
fieldRef={this._passphraseField}
|
||||
onValidate={this.onPassPhraseValidate}
|
||||
fieldRef={this.passphraseField}
|
||||
autoFocus={true}
|
||||
label={_td("Enter a Security Phrase")}
|
||||
labelEnterPassword={_td("Enter a Security Phrase")}
|
||||
|
@ -264,23 +268,21 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
|||
|
||||
<DialogButtons
|
||||
primaryButton={_t('Next')}
|
||||
onPrimaryButtonClick={this._onPassPhraseNextClick}
|
||||
onPrimaryButtonClick={this.onPassPhraseNextClick}
|
||||
hasCancel={false}
|
||||
disabled={!this.state.passPhraseValid}
|
||||
/>
|
||||
|
||||
<details>
|
||||
<summary>{ _t("Advanced") }</summary>
|
||||
<AccessibleButton kind='primary' onClick={this._onSkipPassPhraseClick}>
|
||||
<AccessibleButton kind='primary' onClick={this.onSkipPassPhraseClick}>
|
||||
{ _t("Set up with a Security Key") }
|
||||
</AccessibleButton>
|
||||
</details>
|
||||
</form>;
|
||||
}
|
||||
|
||||
_renderPhasePassPhraseConfirm() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
private renderPhasePassPhraseConfirm(): JSX.Element {
|
||||
let matchText;
|
||||
let changeText;
|
||||
if (this.state.passPhraseConfirm === this.state.passPhrase) {
|
||||
|
@ -303,14 +305,13 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
|||
passPhraseMatch = <div className="mx_CreateKeyBackupDialog_passPhraseMatch">
|
||||
<div>{ matchText }</div>
|
||||
<div>
|
||||
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
|
||||
<AccessibleButton element="span" className="mx_linkButton" onClick={this.onSetAgainClick}>
|
||||
{ changeText }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return <form onSubmit={this._onPassPhraseConfirmNextClick}>
|
||||
return <form onSubmit={this.onPassPhraseConfirmNextClick}>
|
||||
<p>{ _t(
|
||||
"Enter your Security Phrase a second time to confirm it.",
|
||||
) }</p>
|
||||
|
@ -318,7 +319,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
|||
<div className="mx_CreateKeyBackupDialog_passPhraseContainer">
|
||||
<div>
|
||||
<input type="password"
|
||||
onChange={this._onPassPhraseConfirmChange}
|
||||
onChange={this.onPassPhraseConfirmChange}
|
||||
value={this.state.passPhraseConfirm}
|
||||
className="mx_CreateKeyBackupDialog_passPhraseInput"
|
||||
placeholder={_t("Repeat your Security Phrase...")}
|
||||
|
@ -330,14 +331,14 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
|||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t('Next')}
|
||||
onPrimaryButtonClick={this._onPassPhraseConfirmNextClick}
|
||||
onPrimaryButtonClick={this.onPassPhraseConfirmNextClick}
|
||||
hasCancel={false}
|
||||
disabled={this.state.passPhrase !== this.state.passPhraseConfirm}
|
||||
/>
|
||||
</form>;
|
||||
}
|
||||
|
||||
_renderPhaseShowKey() {
|
||||
private renderPhaseShowKey(): JSX.Element {
|
||||
return <div>
|
||||
<p>{ _t(
|
||||
"Your Security Key is a safety net - you can use it to restore " +
|
||||
|
@ -352,13 +353,13 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
|||
</div>
|
||||
<div className="mx_CreateKeyBackupDialog_recoveryKeyContainer">
|
||||
<div className="mx_CreateKeyBackupDialog_recoveryKey">
|
||||
<code ref={this._collectRecoveryKeyNode}>{ this._keyBackupInfo.recovery_key }</code>
|
||||
<code ref={this.recoveryKeyNode}>{ this.keyBackupInfo.recovery_key }</code>
|
||||
</div>
|
||||
<div className="mx_CreateKeyBackupDialog_recoveryKeyButtons">
|
||||
<button className="mx_Dialog_primary" onClick={this._onCopyClick}>
|
||||
<button className="mx_Dialog_primary" onClick={this.onCopyClick}>
|
||||
{ _t("Copy") }
|
||||
</button>
|
||||
<button className="mx_Dialog_primary" onClick={this._onDownloadClick}>
|
||||
<button className="mx_Dialog_primary" onClick={this.onDownloadClick}>
|
||||
{ _t("Download") }
|
||||
</button>
|
||||
</div>
|
||||
|
@ -367,7 +368,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
|||
</div>;
|
||||
}
|
||||
|
||||
_renderPhaseKeepItSafe() {
|
||||
private renderPhaseKeepItSafe(): JSX.Element {
|
||||
let introText;
|
||||
if (this.state.copied) {
|
||||
introText = _t(
|
||||
|
@ -380,7 +381,6 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
|||
{}, { b: s => <b>{ s }</b> },
|
||||
);
|
||||
}
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return <div>
|
||||
{ introText }
|
||||
<ul>
|
||||
|
@ -389,107 +389,101 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
|||
<li>{ _t("<b>Copy it</b> to your personal cloud storage", {}, { b: s => <b>{ s }</b> }) }</li>
|
||||
</ul>
|
||||
<DialogButtons primaryButton={_t("Continue")}
|
||||
onPrimaryButtonClick={this._createBackup}
|
||||
onPrimaryButtonClick={this.createBackup}
|
||||
hasCancel={false}>
|
||||
<button onClick={this._onKeepItSafeBackClick}>{ _t("Back") }</button>
|
||||
<button onClick={this.onKeepItSafeBackClick}>{ _t("Back") }</button>
|
||||
</DialogButtons>
|
||||
</div>;
|
||||
}
|
||||
|
||||
_renderBusyPhase(text) {
|
||||
const Spinner = sdk.getComponent('views.elements.Spinner');
|
||||
private renderBusyPhase(): JSX.Element {
|
||||
return <div>
|
||||
<Spinner />
|
||||
</div>;
|
||||
}
|
||||
|
||||
_renderPhaseDone() {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
private renderPhaseDone(): JSX.Element {
|
||||
return <div>
|
||||
<p>{ _t(
|
||||
"Your keys are being backed up (the first backup could take a few minutes).",
|
||||
) }</p>
|
||||
<DialogButtons primaryButton={_t('OK')}
|
||||
onPrimaryButtonClick={this._onDone}
|
||||
onPrimaryButtonClick={this.onDone}
|
||||
hasCancel={false}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
_renderPhaseOptOutConfirm() {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
private renderPhaseOptOutConfirm(): JSX.Element {
|
||||
return <div>
|
||||
{ _t(
|
||||
"Without setting up Secure Message Recovery, you won't be able to restore your " +
|
||||
"encrypted message history if you log out or use another session.",
|
||||
) }
|
||||
<DialogButtons primaryButton={_t('Set up Secure Message Recovery')}
|
||||
onPrimaryButtonClick={this._onSetUpClick}
|
||||
onPrimaryButtonClick={this.onSetUpClick}
|
||||
hasCancel={false}
|
||||
>
|
||||
<button onClick={this._onCancel}>I understand, continue without</button>
|
||||
<button onClick={this.onCancel}>I understand, continue without</button>
|
||||
</DialogButtons>
|
||||
</div>;
|
||||
}
|
||||
|
||||
_titleForPhase(phase) {
|
||||
private titleForPhase(phase: Phase): string {
|
||||
switch (phase) {
|
||||
case PHASE_PASSPHRASE:
|
||||
case Phase.Passphrase:
|
||||
return _t('Secure your backup with a Security Phrase');
|
||||
case PHASE_PASSPHRASE_CONFIRM:
|
||||
case Phase.PassphraseConfirm:
|
||||
return _t('Confirm your Security Phrase');
|
||||
case PHASE_OPTOUT_CONFIRM:
|
||||
case Phase.OptOutConfirm:
|
||||
return _t('Warning!');
|
||||
case PHASE_SHOWKEY:
|
||||
case PHASE_KEEPITSAFE:
|
||||
case Phase.ShowKey:
|
||||
case Phase.KeepItSafe:
|
||||
return _t('Make a copy of your Security Key');
|
||||
case PHASE_BACKINGUP:
|
||||
case Phase.BackingUp:
|
||||
return _t('Starting backup...');
|
||||
case PHASE_DONE:
|
||||
case Phase.Done:
|
||||
return _t('Success!');
|
||||
default:
|
||||
return _t("Create key backup");
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
public render(): JSX.Element {
|
||||
let content;
|
||||
if (this.state.error) {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
content = <div>
|
||||
<p>{ _t("Unable to create key backup") }</p>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<DialogButtons primaryButton={_t('Retry')}
|
||||
onPrimaryButtonClick={this._createBackup}
|
||||
onPrimaryButtonClick={this.createBackup}
|
||||
hasCancel={true}
|
||||
onCancel={this._onCancel}
|
||||
onCancel={this.onCancel}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
} else {
|
||||
switch (this.state.phase) {
|
||||
case PHASE_PASSPHRASE:
|
||||
content = this._renderPhasePassPhrase();
|
||||
case Phase.Passphrase:
|
||||
content = this.renderPhasePassPhrase();
|
||||
break;
|
||||
case PHASE_PASSPHRASE_CONFIRM:
|
||||
content = this._renderPhasePassPhraseConfirm();
|
||||
case Phase.PassphraseConfirm:
|
||||
content = this.renderPhasePassPhraseConfirm();
|
||||
break;
|
||||
case PHASE_SHOWKEY:
|
||||
content = this._renderPhaseShowKey();
|
||||
case Phase.ShowKey:
|
||||
content = this.renderPhaseShowKey();
|
||||
break;
|
||||
case PHASE_KEEPITSAFE:
|
||||
content = this._renderPhaseKeepItSafe();
|
||||
case Phase.KeepItSafe:
|
||||
content = this.renderPhaseKeepItSafe();
|
||||
break;
|
||||
case PHASE_BACKINGUP:
|
||||
content = this._renderBusyPhase();
|
||||
case Phase.BackingUp:
|
||||
content = this.renderBusyPhase();
|
||||
break;
|
||||
case PHASE_DONE:
|
||||
content = this._renderPhaseDone();
|
||||
case Phase.Done:
|
||||
content = this.renderPhaseDone();
|
||||
break;
|
||||
case PHASE_OPTOUT_CONFIRM:
|
||||
content = this._renderPhaseOptOutConfirm();
|
||||
case Phase.OptOutConfirm:
|
||||
content = this.renderPhaseOptOutConfirm();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -497,8 +491,8 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
|||
return (
|
||||
<BaseDialog className='mx_CreateKeyBackupDialog'
|
||||
onFinished={this.props.onFinished}
|
||||
title={this._titleForPhase(this.state.phase)}
|
||||
hasCancel={[PHASE_PASSPHRASE, PHASE_DONE].includes(this.state.phase)}
|
||||
title={this.titleForPhase(this.state.phase)}
|
||||
hasCancel={[Phase.Passphrase, Phase.Done].includes(this.state.phase)}
|
||||
>
|
||||
<div>
|
||||
{ content }
|
|
@ -16,12 +16,8 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, { createRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import FileSaver from 'file-saver';
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import * as sdk from '../../../../index';
|
||||
import { MatrixClientPeg } from '../../../../MatrixClientPeg';
|
||||
import FileSaver from 'file-saver';
|
||||
import { _t, _td } from '../../../../languageHandler';
|
||||
import Modal from '../../../../Modal';
|
||||
import { promptForBackupPassphrase } from '../../../../SecurityManager';
|
||||
|
@ -33,50 +29,105 @@ import AccessibleButton from "../../../../components/views/elements/AccessibleBu
|
|||
import DialogButtons from "../../../../components/views/elements/DialogButtons";
|
||||
import InlineSpinner from "../../../../components/views/elements/InlineSpinner";
|
||||
import RestoreKeyBackupDialog from "../../../../components/views/dialogs/security/RestoreKeyBackupDialog";
|
||||
import { getSecureBackupSetupMethods, isSecureBackupRequired } from '../../../../utils/WellKnownUtils';
|
||||
import {
|
||||
getSecureBackupSetupMethods,
|
||||
isSecureBackupRequired,
|
||||
SecureBackupSetupMethod,
|
||||
} from '../../../../utils/WellKnownUtils';
|
||||
import SecurityCustomisations from "../../../../customisations/Security";
|
||||
|
||||
const PHASE_LOADING = 0;
|
||||
const PHASE_LOADERROR = 1;
|
||||
const PHASE_CHOOSE_KEY_PASSPHRASE = 2;
|
||||
const PHASE_MIGRATE = 3;
|
||||
const PHASE_PASSPHRASE = 4;
|
||||
const PHASE_PASSPHRASE_CONFIRM = 5;
|
||||
const PHASE_SHOWKEY = 6;
|
||||
const PHASE_STORING = 8;
|
||||
const PHASE_CONFIRM_SKIP = 10;
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
|
||||
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
|
||||
import Field from "../../../../components/views/elements/Field";
|
||||
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
|
||||
import Spinner from "../../../../components/views/elements/Spinner";
|
||||
import { TrustInfo } from "matrix-js-sdk/src/crypto/backup";
|
||||
import { CrossSigningKeys } from "matrix-js-sdk";
|
||||
import InteractiveAuthDialog from "../../../../components/views/dialogs/InteractiveAuthDialog";
|
||||
import { IRecoveryKey } from "matrix-js-sdk/src/crypto/api";
|
||||
import { IValidationResult } from "../../../../components/views/elements/Validation";
|
||||
|
||||
// I made a mistake while converting this and it has to be fixed!
|
||||
enum Phase {
|
||||
Loading = "loading",
|
||||
LoadError = "load_error",
|
||||
ChooseKeyPassphrase = "choose_key_passphrase",
|
||||
Migrate = "migrate",
|
||||
Passphrase = "passphrase",
|
||||
PassphraseConfirm = "passphrase_confirm",
|
||||
ShowKey = "show_key",
|
||||
Storing = "storing",
|
||||
ConfirmSkip = "confirm_skip",
|
||||
}
|
||||
|
||||
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
|
||||
|
||||
// these end up as strings from being values in the radio buttons, so just use strings
|
||||
const CREATE_STORAGE_OPTION_KEY = 'key';
|
||||
const CREATE_STORAGE_OPTION_PASSPHRASE = 'passphrase';
|
||||
interface IProps extends IDialogProps {
|
||||
hasCancel: boolean;
|
||||
accountPassword: string;
|
||||
forceReset: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
phase: Phase;
|
||||
passPhrase: string;
|
||||
passPhraseValid: boolean;
|
||||
passPhraseConfirm: string;
|
||||
copied: boolean;
|
||||
downloaded: boolean;
|
||||
setPassphrase: boolean;
|
||||
backupInfo: IKeyBackupInfo;
|
||||
backupSigStatus: TrustInfo;
|
||||
// does the server offer a UI auth flow with just m.login.password
|
||||
// for /keys/device_signing/upload?
|
||||
canUploadKeysWithPasswordOnly: boolean;
|
||||
accountPassword: string;
|
||||
accountPasswordCorrect: boolean;
|
||||
canSkip: boolean;
|
||||
passPhraseKeySelected: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/*
|
||||
* Walks the user through the process of creating a passphrase to guard Secure
|
||||
* Secret Storage in account data.
|
||||
*/
|
||||
export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
hasCancel: PropTypes.bool,
|
||||
accountPassword: PropTypes.string,
|
||||
forceReset: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
export default class CreateSecretStorageDialog extends React.PureComponent<IProps, IState> {
|
||||
public static defaultProps: Partial<IProps> = {
|
||||
hasCancel: true,
|
||||
forceReset: false,
|
||||
};
|
||||
private recoveryKey: IRecoveryKey;
|
||||
private backupKey: Uint8Array;
|
||||
private recoveryKeyNode = createRef<HTMLElement>();
|
||||
private passphraseField = createRef<Field>();
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this._recoveryKey = null;
|
||||
this._recoveryKeyNode = null;
|
||||
this._backupKey = null;
|
||||
let passPhraseKeySelected;
|
||||
const setupMethods = getSecureBackupSetupMethods();
|
||||
if (setupMethods.includes(SecureBackupSetupMethod.Key)) {
|
||||
passPhraseKeySelected = SecureBackupSetupMethod.Key;
|
||||
} else {
|
||||
passPhraseKeySelected = SecureBackupSetupMethod.Passphrase;
|
||||
}
|
||||
|
||||
const accountPassword = props.accountPassword || "";
|
||||
let canUploadKeysWithPasswordOnly = null;
|
||||
if (accountPassword) {
|
||||
// If we have an account password in memory, let's simplify and
|
||||
// assume it means password auth is also supported for device
|
||||
// signing key upload as well. This avoids hitting the server to
|
||||
// test auth flows, which may be slow under high load.
|
||||
canUploadKeysWithPasswordOnly = true;
|
||||
} else {
|
||||
this.queryKeyUploadAuth();
|
||||
}
|
||||
|
||||
this.state = {
|
||||
phase: PHASE_LOADING,
|
||||
phase: Phase.Loading,
|
||||
passPhrase: '',
|
||||
passPhraseValid: false,
|
||||
passPhraseConfirm: '',
|
||||
|
@ -87,55 +138,37 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
backupSigStatus: null,
|
||||
// does the server offer a UI auth flow with just m.login.password
|
||||
// for /keys/device_signing/upload?
|
||||
canUploadKeysWithPasswordOnly: null,
|
||||
accountPassword: props.accountPassword || "",
|
||||
accountPasswordCorrect: null,
|
||||
canSkip: !isSecureBackupRequired(),
|
||||
canUploadKeysWithPasswordOnly,
|
||||
passPhraseKeySelected,
|
||||
accountPassword,
|
||||
};
|
||||
|
||||
const setupMethods = getSecureBackupSetupMethods();
|
||||
if (setupMethods.includes("key")) {
|
||||
this.state.passPhraseKeySelected = CREATE_STORAGE_OPTION_KEY;
|
||||
} else {
|
||||
this.state.passPhraseKeySelected = CREATE_STORAGE_OPTION_PASSPHRASE;
|
||||
}
|
||||
MatrixClientPeg.get().on('crypto.keyBackupStatus', this.onKeyBackupStatusChange);
|
||||
|
||||
this._passphraseField = createRef();
|
||||
|
||||
MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatusChange);
|
||||
|
||||
if (this.state.accountPassword) {
|
||||
// If we have an account password in memory, let's simplify and
|
||||
// assume it means password auth is also supported for device
|
||||
// signing key upload as well. This avoids hitting the server to
|
||||
// test auth flows, which may be slow under high load.
|
||||
this.state.canUploadKeysWithPasswordOnly = true;
|
||||
} else {
|
||||
this._queryKeyUploadAuth();
|
||||
}
|
||||
|
||||
this._getInitialPhase();
|
||||
this.getInitialPhase();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatusChange);
|
||||
public componentWillUnmount(): void {
|
||||
MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this.onKeyBackupStatusChange);
|
||||
}
|
||||
|
||||
_getInitialPhase() {
|
||||
private getInitialPhase(): void {
|
||||
const keyFromCustomisations = SecurityCustomisations.createSecretStorageKey?.();
|
||||
if (keyFromCustomisations) {
|
||||
logger.log("Created key via customisations, jumping to bootstrap step");
|
||||
this._recoveryKey = {
|
||||
this.recoveryKey = {
|
||||
privateKey: keyFromCustomisations,
|
||||
};
|
||||
this._bootstrapSecretStorage();
|
||||
this.bootstrapSecretStorage();
|
||||
return;
|
||||
}
|
||||
|
||||
this._fetchBackupInfo();
|
||||
this.fetchBackupInfo();
|
||||
}
|
||||
|
||||
async _fetchBackupInfo() {
|
||||
private async fetchBackupInfo(): Promise<{ backupInfo: IKeyBackupInfo, backupSigStatus: TrustInfo }> {
|
||||
try {
|
||||
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||
const backupSigStatus = (
|
||||
|
@ -144,7 +177,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
);
|
||||
|
||||
const { forceReset } = this.props;
|
||||
const phase = (backupInfo && !forceReset) ? PHASE_MIGRATE : PHASE_CHOOSE_KEY_PASSPHRASE;
|
||||
const phase = (backupInfo && !forceReset) ? Phase.Migrate : Phase.ChooseKeyPassphrase;
|
||||
|
||||
this.setState({
|
||||
phase,
|
||||
|
@ -157,13 +190,13 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
backupSigStatus,
|
||||
};
|
||||
} catch (e) {
|
||||
this.setState({ phase: PHASE_LOADERROR });
|
||||
this.setState({ phase: Phase.LoadError });
|
||||
}
|
||||
}
|
||||
|
||||
async _queryKeyUploadAuth() {
|
||||
private async queryKeyUploadAuth(): Promise<void> {
|
||||
try {
|
||||
await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {});
|
||||
await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {} as CrossSigningKeys);
|
||||
// We should never get here: the server should always require
|
||||
// UI auth to upload device signing keys. If we do, we upload
|
||||
// no keys which would be a no-op.
|
||||
|
@ -182,59 +215,55 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
_onKeyBackupStatusChange = () => {
|
||||
if (this.state.phase === PHASE_MIGRATE) this._fetchBackupInfo();
|
||||
}
|
||||
private onKeyBackupStatusChange = (): void => {
|
||||
if (this.state.phase === Phase.Migrate) this.fetchBackupInfo();
|
||||
};
|
||||
|
||||
_onKeyPassphraseChange = e => {
|
||||
private onKeyPassphraseChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
passPhraseKeySelected: e.target.value,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_collectRecoveryKeyNode = (n) => {
|
||||
this._recoveryKeyNode = n;
|
||||
}
|
||||
|
||||
_onChooseKeyPassphraseFormSubmit = async () => {
|
||||
if (this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_KEY) {
|
||||
this._recoveryKey =
|
||||
private onChooseKeyPassphraseFormSubmit = async (): Promise<void> => {
|
||||
if (this.state.passPhraseKeySelected === SecureBackupSetupMethod.Key) {
|
||||
this.recoveryKey =
|
||||
await MatrixClientPeg.get().createRecoveryKeyFromPassphrase();
|
||||
this.setState({
|
||||
copied: false,
|
||||
downloaded: false,
|
||||
setPassphrase: false,
|
||||
phase: PHASE_SHOWKEY,
|
||||
phase: Phase.ShowKey,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
copied: false,
|
||||
downloaded: false,
|
||||
phase: PHASE_PASSPHRASE,
|
||||
phase: Phase.Passphrase,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_onMigrateFormSubmit = (e) => {
|
||||
private onMigrateFormSubmit = (e: React.FormEvent): void => {
|
||||
e.preventDefault();
|
||||
if (this.state.backupSigStatus.usable) {
|
||||
this._bootstrapSecretStorage();
|
||||
this.bootstrapSecretStorage();
|
||||
} else {
|
||||
this._restoreBackup();
|
||||
this.restoreBackup();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_onCopyClick = () => {
|
||||
const successful = copyNode(this._recoveryKeyNode);
|
||||
private onCopyClick = (): void => {
|
||||
const successful = copyNode(this.recoveryKeyNode.current);
|
||||
if (successful) {
|
||||
this.setState({
|
||||
copied: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_onDownloadClick = () => {
|
||||
const blob = new Blob([this._recoveryKey.encodedPrivateKey], {
|
||||
private onDownloadClick = (): void => {
|
||||
const blob = new Blob([this.recoveryKey.encodedPrivateKey], {
|
||||
type: 'text/plain;charset=us-ascii',
|
||||
});
|
||||
FileSaver.saveAs(blob, 'security-key.txt');
|
||||
|
@ -242,9 +271,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
this.setState({
|
||||
downloaded: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_doBootstrapUIAuth = async (makeRequest) => {
|
||||
private doBootstrapUIAuth = async (makeRequest: (authData: any) => void): Promise<void> => {
|
||||
if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) {
|
||||
await makeRequest({
|
||||
type: 'm.login.password',
|
||||
|
@ -258,8 +287,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
password: this.state.accountPassword,
|
||||
});
|
||||
} else {
|
||||
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
|
||||
|
||||
const dialogAesthetics = {
|
||||
[SSOAuthEntry.PHASE_PREAUTH]: {
|
||||
title: _t("Use Single Sign On to continue"),
|
||||
|
@ -292,11 +319,11 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
throw new Error("Cross-signing key upload auth canceled");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_bootstrapSecretStorage = async () => {
|
||||
private bootstrapSecretStorage = async (): Promise<void> => {
|
||||
this.setState({
|
||||
phase: PHASE_STORING,
|
||||
phase: Phase.Storing,
|
||||
error: null,
|
||||
});
|
||||
|
||||
|
@ -308,7 +335,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
if (forceReset) {
|
||||
logger.log("Forcing secret storage reset");
|
||||
await cli.bootstrapSecretStorage({
|
||||
createSecretStorageKey: async () => this._recoveryKey,
|
||||
createSecretStorageKey: async () => this.recoveryKey,
|
||||
setupNewKeyBackup: true,
|
||||
setupNewSecretStorage: true,
|
||||
});
|
||||
|
@ -321,18 +348,18 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
// keys (and also happen to skip all post-authentication flows at the
|
||||
// moment via token login)
|
||||
await cli.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: this._doBootstrapUIAuth,
|
||||
authUploadDeviceSigningKeys: this.doBootstrapUIAuth,
|
||||
});
|
||||
await cli.bootstrapSecretStorage({
|
||||
createSecretStorageKey: async () => this._recoveryKey,
|
||||
createSecretStorageKey: async () => this.recoveryKey,
|
||||
keyBackupInfo: this.state.backupInfo,
|
||||
setupNewKeyBackup: !this.state.backupInfo,
|
||||
getKeyBackupPassphrase: () => {
|
||||
getKeyBackupPassphrase: async () => {
|
||||
// We may already have the backup key if we earlier went
|
||||
// through the restore backup path, so pass it along
|
||||
// rather than prompting again.
|
||||
if (this._backupKey) {
|
||||
return this._backupKey;
|
||||
if (this.backupKey) {
|
||||
return this.backupKey;
|
||||
}
|
||||
return promptForBackupPassphrase();
|
||||
},
|
||||
|
@ -344,27 +371,23 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
this.setState({
|
||||
accountPassword: '',
|
||||
accountPasswordCorrect: false,
|
||||
phase: PHASE_MIGRATE,
|
||||
phase: Phase.Migrate,
|
||||
});
|
||||
} else {
|
||||
this.setState({ error: e });
|
||||
}
|
||||
logger.error("Error bootstrapping secret storage", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_onCancel = () => {
|
||||
private onCancel = (): void => {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
};
|
||||
|
||||
_onDone = () => {
|
||||
this.props.onFinished(true);
|
||||
}
|
||||
|
||||
_restoreBackup = async () => {
|
||||
private restoreBackup = async (): Promise<void> => {
|
||||
// It's possible we'll need the backup key later on for bootstrapping,
|
||||
// so let's stash it here, rather than prompting for it twice.
|
||||
const keyCallback = k => this._backupKey = k;
|
||||
const keyCallback = k => this.backupKey = k;
|
||||
|
||||
const { finished } = Modal.createTrackedDialog(
|
||||
'Restore Backup', '', RestoreKeyBackupDialog,
|
||||
|
@ -376,103 +399,103 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
);
|
||||
|
||||
await finished;
|
||||
const { backupSigStatus } = await this._fetchBackupInfo();
|
||||
const { backupSigStatus } = await this.fetchBackupInfo();
|
||||
if (
|
||||
backupSigStatus.usable &&
|
||||
this.state.canUploadKeysWithPasswordOnly &&
|
||||
this.state.accountPassword
|
||||
) {
|
||||
this._bootstrapSecretStorage();
|
||||
this.bootstrapSecretStorage();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_onLoadRetryClick = () => {
|
||||
this.setState({ phase: PHASE_LOADING });
|
||||
this._fetchBackupInfo();
|
||||
}
|
||||
private onLoadRetryClick = (): void => {
|
||||
this.setState({ phase: Phase.Loading });
|
||||
this.fetchBackupInfo();
|
||||
};
|
||||
|
||||
_onShowKeyContinueClick = () => {
|
||||
this._bootstrapSecretStorage();
|
||||
}
|
||||
private onShowKeyContinueClick = (): void => {
|
||||
this.bootstrapSecretStorage();
|
||||
};
|
||||
|
||||
_onCancelClick = () => {
|
||||
this.setState({ phase: PHASE_CONFIRM_SKIP });
|
||||
}
|
||||
private onCancelClick = (): void => {
|
||||
this.setState({ phase: Phase.ConfirmSkip });
|
||||
};
|
||||
|
||||
_onGoBackClick = () => {
|
||||
this.setState({ phase: PHASE_CHOOSE_KEY_PASSPHRASE });
|
||||
}
|
||||
private onGoBackClick = (): void => {
|
||||
this.setState({ phase: Phase.ChooseKeyPassphrase });
|
||||
};
|
||||
|
||||
_onPassPhraseNextClick = async (e) => {
|
||||
private onPassPhraseNextClick = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!this._passphraseField.current) return; // unmounting
|
||||
if (!this.passphraseField.current) return; // unmounting
|
||||
|
||||
await this._passphraseField.current.validate({ allowEmpty: false });
|
||||
if (!this._passphraseField.current.state.valid) {
|
||||
this._passphraseField.current.focus();
|
||||
this._passphraseField.current.validate({ allowEmpty: false, focused: true });
|
||||
await this.passphraseField.current.validate({ allowEmpty: false });
|
||||
if (!this.passphraseField.current.state.valid) {
|
||||
this.passphraseField.current.focus();
|
||||
this.passphraseField.current.validate({ allowEmpty: false, focused: true });
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ phase: PHASE_PASSPHRASE_CONFIRM });
|
||||
this.setState({ phase: Phase.PassphraseConfirm });
|
||||
};
|
||||
|
||||
_onPassPhraseConfirmNextClick = async (e) => {
|
||||
private onPassPhraseConfirmNextClick = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.state.passPhrase !== this.state.passPhraseConfirm) return;
|
||||
|
||||
this._recoveryKey =
|
||||
this.recoveryKey =
|
||||
await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(this.state.passPhrase);
|
||||
this.setState({
|
||||
copied: false,
|
||||
downloaded: false,
|
||||
setPassphrase: true,
|
||||
phase: PHASE_SHOWKEY,
|
||||
phase: Phase.ShowKey,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_onSetAgainClick = () => {
|
||||
private onSetAgainClick = (): void => {
|
||||
this.setState({
|
||||
passPhrase: '',
|
||||
passPhraseValid: false,
|
||||
passPhraseConfirm: '',
|
||||
phase: PHASE_PASSPHRASE,
|
||||
phase: Phase.Passphrase,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_onPassPhraseValidate = (result) => {
|
||||
private onPassPhraseValidate = (result: IValidationResult): void => {
|
||||
this.setState({
|
||||
passPhraseValid: result.valid,
|
||||
});
|
||||
};
|
||||
|
||||
_onPassPhraseChange = (e) => {
|
||||
private onPassPhraseChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
passPhrase: e.target.value,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_onPassPhraseConfirmChange = (e) => {
|
||||
private onPassPhraseConfirmChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
passPhraseConfirm: e.target.value,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_onAccountPasswordChange = (e) => {
|
||||
private onAccountPasswordChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
accountPassword: e.target.value,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_renderOptionKey() {
|
||||
private renderOptionKey(): JSX.Element {
|
||||
return (
|
||||
<StyledRadioButton
|
||||
key={CREATE_STORAGE_OPTION_KEY}
|
||||
value={CREATE_STORAGE_OPTION_KEY}
|
||||
key={SecureBackupSetupMethod.Key}
|
||||
value={SecureBackupSetupMethod.Key}
|
||||
name="keyPassphrase"
|
||||
checked={this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_KEY}
|
||||
onChange={this._onKeyPassphraseChange}
|
||||
checked={this.state.passPhraseKeySelected === SecureBackupSetupMethod.Key}
|
||||
onChange={this.onKeyPassphraseChange}
|
||||
outlined
|
||||
>
|
||||
<div className="mx_CreateSecretStorageDialog_optionTitle">
|
||||
|
@ -484,14 +507,14 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
_renderOptionPassphrase() {
|
||||
private renderOptionPassphrase(): JSX.Element {
|
||||
return (
|
||||
<StyledRadioButton
|
||||
key={CREATE_STORAGE_OPTION_PASSPHRASE}
|
||||
value={CREATE_STORAGE_OPTION_PASSPHRASE}
|
||||
key={SecureBackupSetupMethod.Passphrase}
|
||||
value={SecureBackupSetupMethod.Passphrase}
|
||||
name="keyPassphrase"
|
||||
checked={this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_PASSPHRASE}
|
||||
onChange={this._onKeyPassphraseChange}
|
||||
checked={this.state.passPhraseKeySelected === SecureBackupSetupMethod.Passphrase}
|
||||
onChange={this.onKeyPassphraseChange}
|
||||
outlined
|
||||
>
|
||||
<div className="mx_CreateSecretStorageDialog_optionTitle">
|
||||
|
@ -503,12 +526,14 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
_renderPhaseChooseKeyPassphrase() {
|
||||
private renderPhaseChooseKeyPassphrase(): JSX.Element {
|
||||
const setupMethods = getSecureBackupSetupMethods();
|
||||
const optionKey = setupMethods.includes("key") ? this._renderOptionKey() : null;
|
||||
const optionPassphrase = setupMethods.includes("passphrase") ? this._renderOptionPassphrase() : null;
|
||||
const optionKey = setupMethods.includes(SecureBackupSetupMethod.Key) ? this.renderOptionKey() : null;
|
||||
const optionPassphrase = setupMethods.includes(SecureBackupSetupMethod.Passphrase)
|
||||
? this.renderOptionPassphrase()
|
||||
: null;
|
||||
|
||||
return <form onSubmit={this._onChooseKeyPassphraseFormSubmit}>
|
||||
return <form onSubmit={this.onChooseKeyPassphraseFormSubmit}>
|
||||
<p className="mx_CreateSecretStorageDialog_centeredBody">{ _t(
|
||||
"Safeguard against losing access to encrypted messages & data by " +
|
||||
"backing up encryption keys on your server.",
|
||||
|
@ -519,20 +544,19 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Continue")}
|
||||
onPrimaryButtonClick={this._onChooseKeyPassphraseFormSubmit}
|
||||
onCancel={this._onCancelClick}
|
||||
onPrimaryButtonClick={this.onChooseKeyPassphraseFormSubmit}
|
||||
onCancel={this.onCancelClick}
|
||||
hasCancel={this.state.canSkip}
|
||||
/>
|
||||
</form>;
|
||||
}
|
||||
|
||||
_renderPhaseMigrate() {
|
||||
private renderPhaseMigrate(): JSX.Element {
|
||||
// TODO: This is a temporary screen so people who have the labs flag turned on and
|
||||
// click the button are aware they're making a change to their account.
|
||||
// Once we're confident enough in this (and it's supported enough) we can do
|
||||
// it automatically.
|
||||
// https://github.com/vector-im/element-web/issues/11696
|
||||
const Field = sdk.getComponent('views.elements.Field');
|
||||
|
||||
let authPrompt;
|
||||
let nextCaption = _t("Next");
|
||||
|
@ -543,7 +567,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
type="password"
|
||||
label={_t("Password")}
|
||||
value={this.state.accountPassword}
|
||||
onChange={this._onAccountPasswordChange}
|
||||
onChange={this.onAccountPasswordChange}
|
||||
forceValidity={this.state.accountPasswordCorrect === false ? false : null}
|
||||
autoFocus={true}
|
||||
/></div>
|
||||
|
@ -559,7 +583,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
</p>;
|
||||
}
|
||||
|
||||
return <form onSubmit={this._onMigrateFormSubmit}>
|
||||
return <form onSubmit={this.onMigrateFormSubmit}>
|
||||
<p>{ _t(
|
||||
"Upgrade this session to allow it to verify other sessions, " +
|
||||
"granting them access to encrypted messages and marking them " +
|
||||
|
@ -568,19 +592,19 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
<div>{ authPrompt }</div>
|
||||
<DialogButtons
|
||||
primaryButton={nextCaption}
|
||||
onPrimaryButtonClick={this._onMigrateFormSubmit}
|
||||
onPrimaryButtonClick={this.onMigrateFormSubmit}
|
||||
hasCancel={false}
|
||||
primaryDisabled={this.state.canUploadKeysWithPasswordOnly && !this.state.accountPassword}
|
||||
>
|
||||
<button type="button" className="danger" onClick={this._onCancelClick}>
|
||||
<button type="button" className="danger" onClick={this.onCancelClick}>
|
||||
{ _t('Skip') }
|
||||
</button>
|
||||
</DialogButtons>
|
||||
</form>;
|
||||
}
|
||||
|
||||
_renderPhasePassPhrase() {
|
||||
return <form onSubmit={this._onPassPhraseNextClick}>
|
||||
private renderPhasePassPhrase(): JSX.Element {
|
||||
return <form onSubmit={this.onPassPhraseNextClick}>
|
||||
<p>{ _t(
|
||||
"Enter a security phrase only you know, as it’s used to safeguard your data. " +
|
||||
"To be secure, you shouldn’t re-use your account password.",
|
||||
|
@ -589,11 +613,11 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
|
||||
<PassphraseField
|
||||
className="mx_CreateSecretStorageDialog_passPhraseField"
|
||||
onChange={this._onPassPhraseChange}
|
||||
onChange={this.onPassPhraseChange}
|
||||
minScore={PASSWORD_MIN_SCORE}
|
||||
value={this.state.passPhrase}
|
||||
onValidate={this._onPassPhraseValidate}
|
||||
fieldRef={this._passphraseField}
|
||||
onValidate={this.onPassPhraseValidate}
|
||||
fieldRef={this.passphraseField}
|
||||
autoFocus={true}
|
||||
label={_td("Enter a Security Phrase")}
|
||||
labelEnterPassword={_td("Enter a Security Phrase")}
|
||||
|
@ -604,21 +628,19 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
|
||||
<DialogButtons
|
||||
primaryButton={_t('Continue')}
|
||||
onPrimaryButtonClick={this._onPassPhraseNextClick}
|
||||
onPrimaryButtonClick={this.onPassPhraseNextClick}
|
||||
hasCancel={false}
|
||||
disabled={!this.state.passPhraseValid}
|
||||
>
|
||||
<button type="button"
|
||||
onClick={this._onCancelClick}
|
||||
onClick={this.onCancelClick}
|
||||
className="danger"
|
||||
>{ _t("Cancel") }</button>
|
||||
</DialogButtons>
|
||||
</form>;
|
||||
}
|
||||
|
||||
_renderPhasePassPhraseConfirm() {
|
||||
const Field = sdk.getComponent('views.elements.Field');
|
||||
|
||||
private renderPhasePassPhraseConfirm(): JSX.Element {
|
||||
let matchText;
|
||||
let changeText;
|
||||
if (this.state.passPhraseConfirm === this.state.passPhrase) {
|
||||
|
@ -641,20 +663,20 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
passPhraseMatch = <div>
|
||||
<div>{ matchText }</div>
|
||||
<div>
|
||||
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
|
||||
<AccessibleButton element="span" className="mx_linkButton" onClick={this.onSetAgainClick}>
|
||||
{ changeText }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
return <form onSubmit={this._onPassPhraseConfirmNextClick}>
|
||||
return <form onSubmit={this.onPassPhraseConfirmNextClick}>
|
||||
<p>{ _t(
|
||||
"Enter your Security Phrase a second time to confirm it.",
|
||||
) }</p>
|
||||
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
|
||||
<Field
|
||||
type="password"
|
||||
onChange={this._onPassPhraseConfirmChange}
|
||||
onChange={this.onPassPhraseConfirmChange}
|
||||
value={this.state.passPhraseConfirm}
|
||||
className="mx_CreateSecretStorageDialog_passPhraseField"
|
||||
label={_t("Confirm your Security Phrase")}
|
||||
|
@ -667,24 +689,24 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t('Continue')}
|
||||
onPrimaryButtonClick={this._onPassPhraseConfirmNextClick}
|
||||
onPrimaryButtonClick={this.onPassPhraseConfirmNextClick}
|
||||
hasCancel={false}
|
||||
disabled={this.state.passPhrase !== this.state.passPhraseConfirm}
|
||||
>
|
||||
<button type="button"
|
||||
onClick={this._onCancelClick}
|
||||
onClick={this.onCancelClick}
|
||||
className="danger"
|
||||
>{ _t("Skip") }</button>
|
||||
</DialogButtons>
|
||||
</form>;
|
||||
}
|
||||
|
||||
_renderPhaseShowKey() {
|
||||
private renderPhaseShowKey(): JSX.Element {
|
||||
let continueButton;
|
||||
if (this.state.phase === PHASE_SHOWKEY) {
|
||||
if (this.state.phase === Phase.ShowKey) {
|
||||
continueButton = <DialogButtons primaryButton={_t("Continue")}
|
||||
disabled={!this.state.downloaded && !this.state.copied && !this.state.setPassphrase}
|
||||
onPrimaryButtonClick={this._onShowKeyContinueClick}
|
||||
onPrimaryButtonClick={this.onShowKeyContinueClick}
|
||||
hasCancel={false}
|
||||
/>;
|
||||
} else {
|
||||
|
@ -700,13 +722,13 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
<div className="mx_CreateSecretStorageDialog_primaryContainer">
|
||||
<div className="mx_CreateSecretStorageDialog_recoveryKeyContainer">
|
||||
<div className="mx_CreateSecretStorageDialog_recoveryKey">
|
||||
<code ref={this._collectRecoveryKeyNode}>{ this._recoveryKey.encodedPrivateKey }</code>
|
||||
<code ref={this.recoveryKeyNode}>{ this.recoveryKey.encodedPrivateKey }</code>
|
||||
</div>
|
||||
<div className="mx_CreateSecretStorageDialog_recoveryKeyButtons">
|
||||
<AccessibleButton kind='primary'
|
||||
className="mx_Dialog_primary"
|
||||
onClick={this._onDownloadClick}
|
||||
disabled={this.state.phase === PHASE_STORING}
|
||||
onClick={this.onDownloadClick}
|
||||
disabled={this.state.phase === Phase.Storing}
|
||||
>
|
||||
{ _t("Download") }
|
||||
</AccessibleButton>
|
||||
|
@ -714,8 +736,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
<AccessibleButton
|
||||
kind='primary'
|
||||
className="mx_Dialog_primary mx_CreateSecretStorageDialog_recoveryKeyButtons_copyBtn"
|
||||
onClick={this._onCopyClick}
|
||||
disabled={this.state.phase === PHASE_STORING}
|
||||
onClick={this.onCopyClick}
|
||||
disabled={this.state.phase === Phase.Storing}
|
||||
>
|
||||
{ this.state.copied ? _t("Copied!") : _t("Copy") }
|
||||
</AccessibleButton>
|
||||
|
@ -726,27 +748,26 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
</div>;
|
||||
}
|
||||
|
||||
_renderBusyPhase() {
|
||||
const Spinner = sdk.getComponent('views.elements.Spinner');
|
||||
private renderBusyPhase(): JSX.Element {
|
||||
return <div>
|
||||
<Spinner />
|
||||
</div>;
|
||||
}
|
||||
|
||||
_renderPhaseLoadError() {
|
||||
private renderPhaseLoadError(): JSX.Element {
|
||||
return <div>
|
||||
<p>{ _t("Unable to query secret storage status") }</p>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<DialogButtons primaryButton={_t('Retry')}
|
||||
onPrimaryButtonClick={this._onLoadRetryClick}
|
||||
onPrimaryButtonClick={this.onLoadRetryClick}
|
||||
hasCancel={this.state.canSkip}
|
||||
onCancel={this._onCancel}
|
||||
onCancel={this.onCancel}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
_renderPhaseSkipConfirm() {
|
||||
private renderPhaseSkipConfirm(): JSX.Element {
|
||||
return <div>
|
||||
<p>{ _t(
|
||||
"If you cancel now, you may lose encrypted messages & data if you lose access to your logins.",
|
||||
|
@ -755,98 +776,96 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
"You can also set up Secure Backup & manage your keys in Settings.",
|
||||
) }</p>
|
||||
<DialogButtons primaryButton={_t('Go back')}
|
||||
onPrimaryButtonClick={this._onGoBackClick}
|
||||
onPrimaryButtonClick={this.onGoBackClick}
|
||||
hasCancel={false}
|
||||
>
|
||||
<button type="button" className="danger" onClick={this._onCancel}>{ _t('Cancel') }</button>
|
||||
<button type="button" className="danger" onClick={this.onCancel}>{ _t('Cancel') }</button>
|
||||
</DialogButtons>
|
||||
</div>;
|
||||
}
|
||||
|
||||
_titleForPhase(phase) {
|
||||
private titleForPhase(phase: Phase): string {
|
||||
switch (phase) {
|
||||
case PHASE_CHOOSE_KEY_PASSPHRASE:
|
||||
case Phase.ChooseKeyPassphrase:
|
||||
return _t('Set up Secure Backup');
|
||||
case PHASE_MIGRATE:
|
||||
case Phase.Migrate:
|
||||
return _t('Upgrade your encryption');
|
||||
case PHASE_PASSPHRASE:
|
||||
case Phase.Passphrase:
|
||||
return _t('Set a Security Phrase');
|
||||
case PHASE_PASSPHRASE_CONFIRM:
|
||||
case Phase.PassphraseConfirm:
|
||||
return _t('Confirm Security Phrase');
|
||||
case PHASE_CONFIRM_SKIP:
|
||||
case Phase.ConfirmSkip:
|
||||
return _t('Are you sure?');
|
||||
case PHASE_SHOWKEY:
|
||||
case Phase.ShowKey:
|
||||
return _t('Save your Security Key');
|
||||
case PHASE_STORING:
|
||||
case Phase.Storing:
|
||||
return _t('Setting up keys');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
public render(): JSX.Element {
|
||||
let content;
|
||||
if (this.state.error) {
|
||||
content = <div>
|
||||
<p>{ _t("Unable to set up secret storage") }</p>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<DialogButtons primaryButton={_t('Retry')}
|
||||
onPrimaryButtonClick={this._bootstrapSecretStorage}
|
||||
onPrimaryButtonClick={this.bootstrapSecretStorage}
|
||||
hasCancel={this.state.canSkip}
|
||||
onCancel={this._onCancel}
|
||||
onCancel={this.onCancel}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
} else {
|
||||
switch (this.state.phase) {
|
||||
case PHASE_LOADING:
|
||||
content = this._renderBusyPhase();
|
||||
case Phase.Loading:
|
||||
content = this.renderBusyPhase();
|
||||
break;
|
||||
case PHASE_LOADERROR:
|
||||
content = this._renderPhaseLoadError();
|
||||
case Phase.LoadError:
|
||||
content = this.renderPhaseLoadError();
|
||||
break;
|
||||
case PHASE_CHOOSE_KEY_PASSPHRASE:
|
||||
content = this._renderPhaseChooseKeyPassphrase();
|
||||
case Phase.ChooseKeyPassphrase:
|
||||
content = this.renderPhaseChooseKeyPassphrase();
|
||||
break;
|
||||
case PHASE_MIGRATE:
|
||||
content = this._renderPhaseMigrate();
|
||||
case Phase.Migrate:
|
||||
content = this.renderPhaseMigrate();
|
||||
break;
|
||||
case PHASE_PASSPHRASE:
|
||||
content = this._renderPhasePassPhrase();
|
||||
case Phase.Passphrase:
|
||||
content = this.renderPhasePassPhrase();
|
||||
break;
|
||||
case PHASE_PASSPHRASE_CONFIRM:
|
||||
content = this._renderPhasePassPhraseConfirm();
|
||||
case Phase.PassphraseConfirm:
|
||||
content = this.renderPhasePassPhraseConfirm();
|
||||
break;
|
||||
case PHASE_SHOWKEY:
|
||||
content = this._renderPhaseShowKey();
|
||||
case Phase.ShowKey:
|
||||
content = this.renderPhaseShowKey();
|
||||
break;
|
||||
case PHASE_STORING:
|
||||
content = this._renderBusyPhase();
|
||||
case Phase.Storing:
|
||||
content = this.renderBusyPhase();
|
||||
break;
|
||||
case PHASE_CONFIRM_SKIP:
|
||||
content = this._renderPhaseSkipConfirm();
|
||||
case Phase.ConfirmSkip:
|
||||
content = this.renderPhaseSkipConfirm();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let titleClass = null;
|
||||
switch (this.state.phase) {
|
||||
case PHASE_PASSPHRASE:
|
||||
case PHASE_PASSPHRASE_CONFIRM:
|
||||
case Phase.Passphrase:
|
||||
case Phase.PassphraseConfirm:
|
||||
titleClass = [
|
||||
'mx_CreateSecretStorageDialog_titleWithIcon',
|
||||
'mx_CreateSecretStorageDialog_securePhraseTitle',
|
||||
];
|
||||
break;
|
||||
case PHASE_SHOWKEY:
|
||||
case Phase.ShowKey:
|
||||
titleClass = [
|
||||
'mx_CreateSecretStorageDialog_titleWithIcon',
|
||||
'mx_CreateSecretStorageDialog_secureBackupTitle',
|
||||
];
|
||||
break;
|
||||
case PHASE_CHOOSE_KEY_PASSPHRASE:
|
||||
case Phase.ChooseKeyPassphrase:
|
||||
titleClass = 'mx_CreateSecretStorageDialog_centeredTitle';
|
||||
break;
|
||||
}
|
||||
|
@ -854,9 +873,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
return (
|
||||
<BaseDialog className='mx_CreateSecretStorageDialog'
|
||||
onFinished={this.props.onFinished}
|
||||
title={this._titleForPhase(this.state.phase)}
|
||||
title={this.titleForPhase(this.state.phase)}
|
||||
titleClass={titleClass}
|
||||
hasCancel={this.props.hasCancel && [PHASE_PASSPHRASE].includes(this.state.phase)}
|
||||
hasCancel={this.props.hasCancel && [Phase.Passphrase].includes(this.state.phase)}
|
||||
fixedWidth={false}
|
||||
>
|
||||
<div>
|
|
@ -16,46 +16,51 @@ limitations under the License.
|
|||
|
||||
import FileSaver from 'file-saver';
|
||||
import React, { createRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../../languageHandler';
|
||||
|
||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||
import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryption';
|
||||
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
|
||||
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { _t } from '../../../../languageHandler';
|
||||
import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryption';
|
||||
import * as sdk from '../../../../index';
|
||||
enum Phase {
|
||||
Edit = "edit",
|
||||
Exporting = "exporting",
|
||||
}
|
||||
|
||||
const PHASE_EDIT = 1;
|
||||
const PHASE_EXPORTING = 2;
|
||||
interface IProps extends IDialogProps {
|
||||
matrixClient: MatrixClient;
|
||||
}
|
||||
|
||||
export default class ExportE2eKeysDialog extends React.Component {
|
||||
static propTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
interface IState {
|
||||
phase: Phase;
|
||||
errStr: string;
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
export default class ExportE2eKeysDialog extends React.Component<IProps, IState> {
|
||||
private unmounted = false;
|
||||
private passphrase1 = createRef<HTMLInputElement>();
|
||||
private passphrase2 = createRef<HTMLInputElement>();
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this._unmounted = false;
|
||||
|
||||
this._passphrase1 = createRef();
|
||||
this._passphrase2 = createRef();
|
||||
|
||||
this.state = {
|
||||
phase: PHASE_EDIT,
|
||||
phase: Phase.Edit,
|
||||
errStr: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
public componentWillUnmount(): void {
|
||||
this.unmounted = true;
|
||||
}
|
||||
|
||||
_onPassphraseFormSubmit = (ev) => {
|
||||
private onPassphraseFormSubmit = (ev: React.FormEvent): boolean => {
|
||||
ev.preventDefault();
|
||||
|
||||
const passphrase = this._passphrase1.current.value;
|
||||
if (passphrase !== this._passphrase2.current.value) {
|
||||
const passphrase = this.passphrase1.current.value;
|
||||
if (passphrase !== this.passphrase2.current.value) {
|
||||
this.setState({ errStr: _t('Passphrases must match') });
|
||||
return false;
|
||||
}
|
||||
|
@ -64,11 +69,11 @@ export default class ExportE2eKeysDialog extends React.Component {
|
|||
return false;
|
||||
}
|
||||
|
||||
this._startExport(passphrase);
|
||||
this.startExport(passphrase);
|
||||
return false;
|
||||
};
|
||||
|
||||
_startExport(passphrase) {
|
||||
private startExport(passphrase: string): void {
|
||||
// extra Promise.resolve() to turn synchronous exceptions into
|
||||
// asynchronous ones.
|
||||
Promise.resolve().then(() => {
|
||||
|
@ -85,39 +90,37 @@ export default class ExportE2eKeysDialog extends React.Component {
|
|||
this.props.onFinished(true);
|
||||
}).catch((e) => {
|
||||
logger.error("Error exporting e2e keys:", e);
|
||||
if (this._unmounted) {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
const msg = e.friendlyText || _t('Unknown error');
|
||||
this.setState({
|
||||
errStr: msg,
|
||||
phase: PHASE_EDIT,
|
||||
phase: Phase.Edit,
|
||||
});
|
||||
});
|
||||
|
||||
this.setState({
|
||||
errStr: null,
|
||||
phase: PHASE_EXPORTING,
|
||||
phase: Phase.Exporting,
|
||||
});
|
||||
}
|
||||
|
||||
_onCancelClick = (ev) => {
|
||||
private onCancelClick = (ev: React.MouseEvent): boolean => {
|
||||
ev.preventDefault();
|
||||
this.props.onFinished(false);
|
||||
return false;
|
||||
};
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
const disableForm = (this.state.phase === PHASE_EXPORTING);
|
||||
public render(): JSX.Element {
|
||||
const disableForm = (this.state.phase === Phase.Exporting);
|
||||
|
||||
return (
|
||||
<BaseDialog className='mx_exportE2eKeysDialog'
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("Export room keys")}
|
||||
>
|
||||
<form onSubmit={this._onPassphraseFormSubmit}>
|
||||
<form onSubmit={this.onPassphraseFormSubmit}>
|
||||
<div className="mx_Dialog_content">
|
||||
<p>
|
||||
{ _t(
|
||||
|
@ -150,10 +153,10 @@ export default class ExportE2eKeysDialog extends React.Component {
|
|||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputCell'>
|
||||
<input
|
||||
ref={this._passphrase1}
|
||||
ref={this.passphrase1}
|
||||
id='passphrase1'
|
||||
autoFocus={true}
|
||||
size='64'
|
||||
size={64}
|
||||
type='password'
|
||||
disabled={disableForm}
|
||||
/>
|
||||
|
@ -166,9 +169,9 @@ export default class ExportE2eKeysDialog extends React.Component {
|
|||
</label>
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputCell'>
|
||||
<input ref={this._passphrase2}
|
||||
<input ref={this.passphrase2}
|
||||
id='passphrase2'
|
||||
size='64'
|
||||
size={64}
|
||||
type='password'
|
||||
disabled={disableForm}
|
||||
/>
|
||||
|
@ -183,7 +186,7 @@ export default class ExportE2eKeysDialog extends React.Component {
|
|||
value={_t('Export')}
|
||||
disabled={disableForm}
|
||||
/>
|
||||
<button onClick={this._onCancelClick} disabled={disableForm}>
|
||||
<button onClick={this.onCancelClick} disabled={disableForm}>
|
||||
{ _t("Cancel") }
|
||||
</button>
|
||||
</div>
|
|
@ -15,19 +15,19 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, { createRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||
import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryption';
|
||||
import { _t } from '../../../../languageHandler';
|
||||
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
|
||||
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryption';
|
||||
import * as sdk from '../../../../index';
|
||||
import { _t } from '../../../../languageHandler';
|
||||
|
||||
function readFileAsArrayBuffer(file) {
|
||||
function readFileAsArrayBuffer(file: File): Promise<ArrayBuffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
resolve(e.target.result);
|
||||
resolve(e.target.result as ArrayBuffer);
|
||||
};
|
||||
reader.onerror = reject;
|
||||
|
||||
|
@ -35,51 +35,57 @@ function readFileAsArrayBuffer(file) {
|
|||
});
|
||||
}
|
||||
|
||||
const PHASE_EDIT = 1;
|
||||
const PHASE_IMPORTING = 2;
|
||||
enum Phase {
|
||||
Edit = "edit",
|
||||
Importing = "importing",
|
||||
}
|
||||
|
||||
export default class ImportE2eKeysDialog extends React.Component {
|
||||
static propTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
interface IProps extends IDialogProps {
|
||||
matrixClient: MatrixClient;
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
interface IState {
|
||||
enableSubmit: boolean;
|
||||
phase: Phase;
|
||||
errStr: string;
|
||||
}
|
||||
|
||||
export default class ImportE2eKeysDialog extends React.Component<IProps, IState> {
|
||||
private unmounted = false;
|
||||
private file = createRef<HTMLInputElement>();
|
||||
private passphrase = createRef<HTMLInputElement>();
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this._unmounted = false;
|
||||
|
||||
this._file = createRef();
|
||||
this._passphrase = createRef();
|
||||
|
||||
this.state = {
|
||||
enableSubmit: false,
|
||||
phase: PHASE_EDIT,
|
||||
phase: Phase.Edit,
|
||||
errStr: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
public componentWillUnmount(): void {
|
||||
this.unmounted = true;
|
||||
}
|
||||
|
||||
_onFormChange = (ev) => {
|
||||
const files = this._file.current.files || [];
|
||||
private onFormChange = (ev: React.FormEvent): void => {
|
||||
const files = this.file.current.files || [];
|
||||
this.setState({
|
||||
enableSubmit: (this._passphrase.current.value !== "" && files.length > 0),
|
||||
enableSubmit: (this.passphrase.current.value !== "" && files.length > 0),
|
||||
});
|
||||
};
|
||||
|
||||
_onFormSubmit = (ev) => {
|
||||
private onFormSubmit = (ev: React.FormEvent): boolean => {
|
||||
ev.preventDefault();
|
||||
this._startImport(this._file.current.files[0], this._passphrase.current.value);
|
||||
this.startImport(this.file.current.files[0], this.passphrase.current.value);
|
||||
return false;
|
||||
};
|
||||
|
||||
_startImport(file, passphrase) {
|
||||
private startImport(file: File, passphrase: string) {
|
||||
this.setState({
|
||||
errStr: null,
|
||||
phase: PHASE_IMPORTING,
|
||||
phase: Phase.Importing,
|
||||
});
|
||||
|
||||
return readFileAsArrayBuffer(file).then((arrayBuffer) => {
|
||||
|
@ -93,34 +99,32 @@ export default class ImportE2eKeysDialog extends React.Component {
|
|||
this.props.onFinished(true);
|
||||
}).catch((e) => {
|
||||
logger.error("Error importing e2e keys:", e);
|
||||
if (this._unmounted) {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
const msg = e.friendlyText || _t('Unknown error');
|
||||
this.setState({
|
||||
errStr: msg,
|
||||
phase: PHASE_EDIT,
|
||||
phase: Phase.Edit,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_onCancelClick = (ev) => {
|
||||
private onCancelClick = (ev: React.MouseEvent): boolean => {
|
||||
ev.preventDefault();
|
||||
this.props.onFinished(false);
|
||||
return false;
|
||||
};
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
const disableForm = (this.state.phase !== PHASE_EDIT);
|
||||
public render(): JSX.Element {
|
||||
const disableForm = (this.state.phase !== Phase.Edit);
|
||||
|
||||
return (
|
||||
<BaseDialog className='mx_importE2eKeysDialog'
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("Import room keys")}
|
||||
>
|
||||
<form onSubmit={this._onFormSubmit}>
|
||||
<form onSubmit={this.onFormSubmit}>
|
||||
<div className="mx_Dialog_content">
|
||||
<p>
|
||||
{ _t(
|
||||
|
@ -148,11 +152,11 @@ export default class ImportE2eKeysDialog extends React.Component {
|
|||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputCell'>
|
||||
<input
|
||||
ref={this._file}
|
||||
ref={this.file}
|
||||
id='importFile'
|
||||
type='file'
|
||||
autoFocus={true}
|
||||
onChange={this._onFormChange}
|
||||
onChange={this.onFormChange}
|
||||
disabled={disableForm} />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -164,11 +168,11 @@ export default class ImportE2eKeysDialog extends React.Component {
|
|||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputCell'>
|
||||
<input
|
||||
ref={this._passphrase}
|
||||
ref={this.passphrase}
|
||||
id='passphrase'
|
||||
size='64'
|
||||
size={64}
|
||||
type='password'
|
||||
onChange={this._onFormChange}
|
||||
onChange={this.onFormChange}
|
||||
disabled={disableForm} />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -181,7 +185,7 @@ export default class ImportE2eKeysDialog extends React.Component {
|
|||
value={_t('Import')}
|
||||
disabled={!this.state.enableSubmit || disableForm}
|
||||
/>
|
||||
<button onClick={this._onCancelClick} disabled={disableForm}>
|
||||
<button onClick={this.onCancelClick} disabled={disableForm}>
|
||||
{ _t("Cancel") }
|
||||
</button>
|
||||
</div>
|
|
@ -16,44 +16,40 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import * as sdk from "../../../../index";
|
||||
import { MatrixClientPeg } from '../../../../MatrixClientPeg';
|
||||
import dis from "../../../../dispatcher/dispatcher";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import Modal from "../../../../Modal";
|
||||
import RestoreKeyBackupDialog from "../../../../components/views/dialogs/security/RestoreKeyBackupDialog";
|
||||
import { Action } from "../../../../dispatcher/actions";
|
||||
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
|
||||
import DialogButtons from "../../../../components/views/elements/DialogButtons";
|
||||
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
|
||||
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
|
||||
|
||||
export default class NewRecoveryMethodDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// As returned by js-sdk getKeyBackupVersion()
|
||||
newVersionInfo: PropTypes.object,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
}
|
||||
interface IProps extends IDialogProps {
|
||||
newVersionInfo: IKeyBackupInfo;
|
||||
}
|
||||
|
||||
onOkClick = () => {
|
||||
export default class NewRecoveryMethodDialog extends React.PureComponent<IProps> {
|
||||
private onOkClick = (): void => {
|
||||
this.props.onFinished();
|
||||
}
|
||||
};
|
||||
|
||||
onGoToSettingsClick = () => {
|
||||
private onGoToSettingsClick = (): void => {
|
||||
this.props.onFinished();
|
||||
dis.fire(Action.ViewUserSettings);
|
||||
}
|
||||
};
|
||||
|
||||
onSetupClick = async () => {
|
||||
private onSetupClick = async (): Promise<void> => {
|
||||
Modal.createTrackedDialog(
|
||||
'Restore Backup', '', RestoreKeyBackupDialog, {
|
||||
onFinished: this.props.onFinished,
|
||||
}, null, /* priority = */ false, /* static = */ true,
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
|
||||
const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
const title = <span className="mx_KeyBackupFailedDialog_title">
|
||||
{ _t("New Recovery Method") }
|
||||
</span>;
|
|
@ -15,37 +15,32 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import * as sdk from "../../../../index";
|
||||
import React, { ComponentType } from "react";
|
||||
import dis from "../../../../dispatcher/dispatcher";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import Modal from "../../../../Modal";
|
||||
import { Action } from "../../../../dispatcher/actions";
|
||||
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
|
||||
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
|
||||
import DialogButtons from "../../../../components/views/elements/DialogButtons";
|
||||
|
||||
export default class RecoveryMethodRemovedDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
}
|
||||
interface IProps extends IDialogProps {}
|
||||
|
||||
onGoToSettingsClick = () => {
|
||||
export default class RecoveryMethodRemovedDialog extends React.PureComponent<IProps> {
|
||||
private onGoToSettingsClick = (): void => {
|
||||
this.props.onFinished();
|
||||
dis.fire(Action.ViewUserSettings);
|
||||
}
|
||||
};
|
||||
|
||||
onSetupClick = () => {
|
||||
private onSetupClick = (): void => {
|
||||
this.props.onFinished();
|
||||
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
|
||||
import("./CreateKeyBackupDialog"),
|
||||
import("./CreateKeyBackupDialog") as unknown as Promise<ComponentType<{}>>,
|
||||
null, null, /* priority = */ false, /* static = */ true,
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
|
||||
const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
const title = <span className="mx_KeyBackupFailedDialog_title">
|
||||
{ _t("Recovery Method Removed") }
|
||||
</span>;
|
|
@ -249,6 +249,8 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
|||
let handled = true;
|
||||
|
||||
switch (ev.key) {
|
||||
// XXX: this is imitating roving behaviour, it should really use the RovingTabIndex utils
|
||||
// to inherit proper handling of unmount edge cases
|
||||
case Key.TAB:
|
||||
case Key.ESCAPE:
|
||||
case Key.ARROW_LEFT: // close on left and right arrows too for when it is a context menu on a <Toolbar />
|
||||
|
|
|
@ -40,6 +40,7 @@ import { replaceableComponent } from "../../utils/replaceableComponent";
|
|||
import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore";
|
||||
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
|
||||
import UIStore from "../../stores/UIStore";
|
||||
import { findSiblingElement, IState as IRovingTabIndexState } from "../../accessibility/RovingTabIndex";
|
||||
|
||||
interface IProps {
|
||||
isMinimized: boolean;
|
||||
|
@ -51,19 +52,12 @@ interface IState {
|
|||
activeSpace?: Room;
|
||||
}
|
||||
|
||||
// List of CSS classes which should be included in keyboard navigation within the room list
|
||||
const cssClasses = [
|
||||
"mx_RoomSearch_input",
|
||||
"mx_RoomSearch_minimizedHandle", // minimized <RoomSearch />
|
||||
"mx_RoomSublist_headerText",
|
||||
"mx_RoomTile",
|
||||
"mx_RoomSublist_showNButton",
|
||||
];
|
||||
|
||||
@replaceableComponent("structures.LeftPanel")
|
||||
export default class LeftPanel extends React.Component<IProps, IState> {
|
||||
private ref: React.RefObject<HTMLDivElement> = createRef();
|
||||
private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
|
||||
private ref = createRef<HTMLDivElement>();
|
||||
private listContainerRef = createRef<HTMLDivElement>();
|
||||
private roomSearchRef = createRef<RoomSearch>();
|
||||
private roomListRef = createRef<RoomList>();
|
||||
private focusedElement = null;
|
||||
private isDoingStickyHeaders = false;
|
||||
|
||||
|
@ -283,16 +277,25 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
this.focusedElement = null;
|
||||
};
|
||||
|
||||
private onKeyDown = (ev: React.KeyboardEvent) => {
|
||||
private onKeyDown = (ev: React.KeyboardEvent, state?: IRovingTabIndexState) => {
|
||||
if (!this.focusedElement) return;
|
||||
|
||||
const action = getKeyBindingsManager().getRoomListAction(ev);
|
||||
switch (action) {
|
||||
case RoomListAction.NextRoom:
|
||||
if (!state) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.roomListRef.current?.focus();
|
||||
}
|
||||
break;
|
||||
|
||||
case RoomListAction.PrevRoom:
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.onMoveFocus(action === RoomListAction.PrevRoom);
|
||||
if (state && state.activeRef === findSiblingElement(state.refs, 0)) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.roomSearchRef.current?.focus();
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
@ -305,45 +308,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private onMoveFocus = (up: boolean) => {
|
||||
let element = this.focusedElement;
|
||||
|
||||
let descending = false; // are we currently descending or ascending through the DOM tree?
|
||||
let classes: DOMTokenList;
|
||||
|
||||
do {
|
||||
const child = up ? element.lastElementChild : element.firstElementChild;
|
||||
const sibling = up ? element.previousElementSibling : element.nextElementSibling;
|
||||
|
||||
if (descending) {
|
||||
if (child) {
|
||||
element = child;
|
||||
} else if (sibling) {
|
||||
element = sibling;
|
||||
} else {
|
||||
descending = false;
|
||||
element = element.parentElement;
|
||||
}
|
||||
} else {
|
||||
if (sibling) {
|
||||
element = sibling;
|
||||
descending = true;
|
||||
} else {
|
||||
element = element.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
if (element) {
|
||||
classes = element.classList;
|
||||
}
|
||||
} while (element && (!cssClasses.some(c => classes.contains(c)) || element.offsetParent === null));
|
||||
|
||||
if (element) {
|
||||
element.focus();
|
||||
this.focusedElement = element;
|
||||
}
|
||||
};
|
||||
|
||||
private renderHeader(): React.ReactNode {
|
||||
return (
|
||||
<div className="mx_LeftPanel_userHeader">
|
||||
|
@ -388,7 +352,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
>
|
||||
<RoomSearch
|
||||
isMinimized={this.props.isMinimized}
|
||||
onKeyDown={this.onKeyDown}
|
||||
ref={this.roomSearchRef}
|
||||
onSelectRoom={this.selectRoom}
|
||||
/>
|
||||
|
||||
|
@ -417,6 +381,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
activeSpace={this.state.activeSpace}
|
||||
onResize={this.refreshStickyHeaders}
|
||||
onListCollapse={this.refreshStickyHeaders}
|
||||
ref={this.roomListRef}
|
||||
/>;
|
||||
|
||||
const containerClasses = classNames({
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef } from 'react';
|
||||
import React, { ComponentType, createRef } from 'react';
|
||||
import { createClient } from "matrix-js-sdk/src/matrix";
|
||||
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
|
@ -1597,12 +1597,16 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
|
||||
if (haveNewVersion) {
|
||||
Modal.createTrackedDialogAsync('New Recovery Method', 'New Recovery Method',
|
||||
import('../../async-components/views/dialogs/security/NewRecoveryMethodDialog'),
|
||||
import(
|
||||
'../../async-components/views/dialogs/security/NewRecoveryMethodDialog'
|
||||
) as unknown as Promise<ComponentType<{}>>,
|
||||
{ newVersionInfo },
|
||||
);
|
||||
} else {
|
||||
Modal.createTrackedDialogAsync('Recovery Method Removed', 'Recovery Method Removed',
|
||||
import('../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog'),
|
||||
import(
|
||||
'../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog'
|
||||
) as unknown as Promise<ComponentType<{}>>,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -196,6 +196,7 @@ interface IReadReceiptForUser {
|
|||
@replaceableComponent("structures.MessagePanel")
|
||||
export default class MessagePanel extends React.Component<IProps, IState> {
|
||||
static contextType = RoomContext;
|
||||
public context!: React.ContextType<typeof RoomContext>;
|
||||
|
||||
// opaque readreceipt info for each userId; used by ReadReceiptMarker
|
||||
// to manage its animations
|
||||
|
@ -787,6 +788,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
showReadReceipts={this.props.showReadReceipts}
|
||||
callEventGrouper={callEventGrouper}
|
||||
hideSender={this.membersCount <= 2 && this.props.layout === Layout.Bubble}
|
||||
timelineRenderingType={this.context.timelineRenderingType}
|
||||
/>
|
||||
</TileErrorBoundary>,
|
||||
);
|
||||
|
|
|
@ -32,7 +32,6 @@ import SpaceStore, { UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES } from "../.
|
|||
|
||||
interface IProps {
|
||||
isMinimized: boolean;
|
||||
onKeyDown(ev: React.KeyboardEvent): void;
|
||||
/**
|
||||
* @returns true if a room has been selected and the search field should be cleared
|
||||
*/
|
||||
|
@ -133,11 +132,6 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||
this.clearInput();
|
||||
defaultDispatcher.fire(Action.FocusSendMessageComposer);
|
||||
break;
|
||||
case RoomListAction.NextRoom:
|
||||
case RoomListAction.PrevRoom:
|
||||
// we don't handle these actions here put pass the event on to the interested party (LeftPanel)
|
||||
this.props.onKeyDown(ev);
|
||||
break;
|
||||
case RoomListAction.SelectRoom: {
|
||||
const shouldClear = this.props.onSelectRoom();
|
||||
if (shouldClear) {
|
||||
|
@ -151,6 +145,10 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
public focus(): void {
|
||||
this.inputRef.current?.focus();
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const classes = classNames({
|
||||
'mx_RoomSearch': true,
|
||||
|
|
|
@ -93,6 +93,7 @@ import TopUnreadMessagesBar from "../views/rooms/TopUnreadMessagesBar";
|
|||
import SpaceStore from "../../stores/SpaceStore";
|
||||
import { dispatchShowThreadEvent } from '../../dispatcher/dispatch-actions/threads';
|
||||
import { fetchInitialEvent } from "../../utils/EventUtils";
|
||||
import { ComposerType } from "../../dispatcher/payloads/ComposerInsertPayload";
|
||||
|
||||
const DEBUG = false;
|
||||
let debuglog = function(msg: string) {};
|
||||
|
@ -863,10 +864,11 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
}
|
||||
|
||||
case Action.ComposerInsert: {
|
||||
if (payload.composerType) break;
|
||||
// re-dispatch to the correct composer
|
||||
dis.dispatch({
|
||||
...payload,
|
||||
action: this.state.editState ? "edit_composer_insert" : "send_composer_insert",
|
||||
composerType: this.state.editState ? ComposerType.Edit : ComposerType.Send,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -60,18 +60,15 @@ import { getDisplayAliasForRoom } from "./RoomDirectory";
|
|||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import { useEventEmitterState } from "../../hooks/useEventEmitter";
|
||||
import { IOOBData } from "../../stores/ThreepidInviteStore";
|
||||
import { awaitRoomDownSync } from "../../utils/RoomUpgrade";
|
||||
import RoomViewStore from "../../stores/RoomViewStore";
|
||||
|
||||
interface IProps {
|
||||
space: Room;
|
||||
initialText?: string;
|
||||
additionalButtons?: ReactNode;
|
||||
showRoom(
|
||||
cli: MatrixClient,
|
||||
hierarchy: RoomHierarchy,
|
||||
roomId: string,
|
||||
autoJoin?: boolean,
|
||||
roomType?: RoomType,
|
||||
): void;
|
||||
showRoom(cli: MatrixClient, hierarchy: RoomHierarchy, roomId: string, roomType?: RoomType): void;
|
||||
joinRoom(cli: MatrixClient, hierarchy: RoomHierarchy, roomId: string): void;
|
||||
}
|
||||
|
||||
interface ITileProps {
|
||||
|
@ -80,7 +77,8 @@ interface ITileProps {
|
|||
selected?: boolean;
|
||||
numChildRooms?: number;
|
||||
hasPermissions?: boolean;
|
||||
onViewRoomClick(autoJoin: boolean, roomType: RoomType): void;
|
||||
onViewRoomClick(): void;
|
||||
onJoinRoomClick(): void;
|
||||
onToggleClick?(): void;
|
||||
}
|
||||
|
||||
|
@ -91,31 +89,50 @@ const Tile: React.FC<ITileProps> = ({
|
|||
hasPermissions,
|
||||
onToggleClick,
|
||||
onViewRoomClick,
|
||||
onJoinRoomClick,
|
||||
numChildRooms,
|
||||
children,
|
||||
}) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const joinedRoom = cli.getRoom(room.room_id)?.getMyMembership() === "join" ? cli.getRoom(room.room_id) : null;
|
||||
const [joinedRoom, setJoinedRoom] = useState<Room>(() => {
|
||||
const cliRoom = cli.getRoom(room.room_id);
|
||||
return cliRoom?.getMyMembership() === "join" ? cliRoom : null;
|
||||
});
|
||||
const joinedRoomName = useEventEmitterState(joinedRoom, "Room.name", room => room?.name);
|
||||
const name = joinedRoomName || room.name || room.canonical_alias || room.aliases?.[0]
|
||||
|| (room.room_type === RoomType.Space ? _t("Unnamed Space") : _t("Unnamed Room"));
|
||||
|
||||
const [showChildren, toggleShowChildren] = useStateToggle(true);
|
||||
const [onFocus, isActive, ref] = useRovingTabIndex();
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const onPreviewClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
onViewRoomClick(false, room.room_type as RoomType);
|
||||
onViewRoomClick();
|
||||
};
|
||||
const onJoinClick = (ev: ButtonEvent) => {
|
||||
const onJoinClick = async (ev: ButtonEvent) => {
|
||||
setBusy(true);
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
onViewRoomClick(true, room.room_type as RoomType);
|
||||
onJoinRoomClick();
|
||||
setJoinedRoom(await awaitRoomDownSync(cli, room.room_id));
|
||||
setBusy(false);
|
||||
};
|
||||
|
||||
let button;
|
||||
if (joinedRoom) {
|
||||
if (busy) {
|
||||
button = <AccessibleTooltipButton
|
||||
disabled={true}
|
||||
onClick={onJoinClick}
|
||||
kind="primary_outline"
|
||||
onFocus={onFocus}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
title={_t("Joining")}
|
||||
>
|
||||
<Spinner w={24} h={24} />
|
||||
</AccessibleTooltipButton>;
|
||||
} else if (joinedRoom) {
|
||||
button = <AccessibleButton
|
||||
onClick={onPreviewClick}
|
||||
kind="primary_outline"
|
||||
|
@ -172,8 +189,15 @@ const Tile: React.FC<ITileProps> = ({
|
|||
description += " · " + topic;
|
||||
}
|
||||
|
||||
let joinedSection;
|
||||
if (joinedRoom) {
|
||||
joinedSection = <div className="mx_SpaceHierarchy_roomTile_joined">
|
||||
{ _t("Joined") }
|
||||
</div>;
|
||||
}
|
||||
|
||||
let suggestedSection;
|
||||
if (suggested) {
|
||||
if (suggested && (!joinedRoom || hasPermissions)) {
|
||||
suggestedSection = <InfoTooltip tooltip={_t("This room is suggested as a good one to join")}>
|
||||
{ _t("Suggested") }
|
||||
</InfoTooltip>;
|
||||
|
@ -183,6 +207,7 @@ const Tile: React.FC<ITileProps> = ({
|
|||
{ avatar }
|
||||
<div className="mx_SpaceHierarchy_roomTile_name">
|
||||
{ name }
|
||||
{ joinedSection }
|
||||
{ suggestedSection }
|
||||
</div>
|
||||
|
||||
|
@ -274,6 +299,7 @@ const Tile: React.FC<ITileProps> = ({
|
|||
<AccessibleButton
|
||||
className={classNames("mx_SpaceHierarchy_roomTile", {
|
||||
mx_SpaceHierarchy_subspace: room.room_type === RoomType.Space,
|
||||
mx_SpaceHierarchy_joining: busy,
|
||||
})}
|
||||
onClick={(hasPermissions && onToggleClick) ? onToggleClick : onPreviewClick}
|
||||
onKeyDown={onKeyDown}
|
||||
|
@ -288,13 +314,7 @@ const Tile: React.FC<ITileProps> = ({
|
|||
</li>;
|
||||
};
|
||||
|
||||
export const showRoom = (
|
||||
cli: MatrixClient,
|
||||
hierarchy: RoomHierarchy,
|
||||
roomId: string,
|
||||
autoJoin = false,
|
||||
roomType?: RoomType,
|
||||
) => {
|
||||
export const showRoom = (cli: MatrixClient, hierarchy: RoomHierarchy, roomId: string, roomType?: RoomType): void => {
|
||||
const room = hierarchy.roomMap.get(roomId);
|
||||
|
||||
// Don't let the user view a room they won't be able to either peek or join:
|
||||
|
@ -309,7 +329,6 @@ export const showRoom = (
|
|||
const roomAlias = getDisplayAliasForRoom(room) || undefined;
|
||||
dis.dispatch({
|
||||
action: "view_room",
|
||||
auto_join: autoJoin,
|
||||
should_peek: true,
|
||||
_type: "room_directory", // instrumentation
|
||||
room_alias: roomAlias,
|
||||
|
@ -324,13 +343,29 @@ export const showRoom = (
|
|||
});
|
||||
};
|
||||
|
||||
export const joinRoom = (cli: MatrixClient, hierarchy: RoomHierarchy, roomId: string): void => {
|
||||
// Don't let the user view a room they won't be able to either peek or join:
|
||||
// fail earlier so they don't have to click back to the directory.
|
||||
if (cli.isGuest()) {
|
||||
dis.dispatch({ action: "require_registration" });
|
||||
return;
|
||||
}
|
||||
|
||||
cli.joinRoom(roomId, {
|
||||
viaServers: Array.from(hierarchy.viaMap.get(roomId) || []),
|
||||
}).catch(err => {
|
||||
RoomViewStore.showJoinRoomError(err, roomId);
|
||||
});
|
||||
};
|
||||
|
||||
interface IHierarchyLevelProps {
|
||||
root: IHierarchyRoom;
|
||||
roomSet: Set<IHierarchyRoom>;
|
||||
hierarchy: RoomHierarchy;
|
||||
parents: Set<string>;
|
||||
selectedMap?: Map<string, Set<string>>;
|
||||
onViewRoomClick(roomId: string, autoJoin: boolean, roomType?: RoomType): void;
|
||||
onViewRoomClick(roomId: string, roomType?: RoomType): void;
|
||||
onJoinRoomClick(roomId: string): void;
|
||||
onToggleClick?(parentId: string, childId: string): void;
|
||||
}
|
||||
|
||||
|
@ -365,6 +400,7 @@ export const HierarchyLevel = ({
|
|||
parents,
|
||||
selectedMap,
|
||||
onViewRoomClick,
|
||||
onJoinRoomClick,
|
||||
onToggleClick,
|
||||
}: IHierarchyLevelProps) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
@ -392,9 +428,8 @@ export const HierarchyLevel = ({
|
|||
room={room}
|
||||
suggested={hierarchy.isSuggested(root.room_id, room.room_id)}
|
||||
selected={selectedMap?.get(root.room_id)?.has(room.room_id)}
|
||||
onViewRoomClick={(autoJoin, roomType) => {
|
||||
onViewRoomClick(room.room_id, autoJoin, roomType);
|
||||
}}
|
||||
onViewRoomClick={() => onViewRoomClick(room.room_id, room.room_type as RoomType)}
|
||||
onJoinRoomClick={() => onJoinRoomClick(room.room_id)}
|
||||
hasPermissions={hasPermissions}
|
||||
onToggleClick={onToggleClick ? () => onToggleClick(root.room_id, room.room_id) : undefined}
|
||||
/>
|
||||
|
@ -412,9 +447,8 @@ export const HierarchyLevel = ({
|
|||
}).length}
|
||||
suggested={hierarchy.isSuggested(root.room_id, space.room_id)}
|
||||
selected={selectedMap?.get(root.room_id)?.has(space.room_id)}
|
||||
onViewRoomClick={(autoJoin, roomType) => {
|
||||
onViewRoomClick(space.room_id, autoJoin, roomType);
|
||||
}}
|
||||
onViewRoomClick={() => onViewRoomClick(space.room_id, RoomType.Space)}
|
||||
onJoinRoomClick={() => onJoinRoomClick(space.room_id)}
|
||||
hasPermissions={hasPermissions}
|
||||
onToggleClick={onToggleClick ? () => onToggleClick(root.room_id, space.room_id) : undefined}
|
||||
>
|
||||
|
@ -425,6 +459,7 @@ export const HierarchyLevel = ({
|
|||
parents={newParents}
|
||||
selectedMap={selectedMap}
|
||||
onViewRoomClick={onViewRoomClick}
|
||||
onJoinRoomClick={onJoinRoomClick}
|
||||
onToggleClick={onToggleClick}
|
||||
/>
|
||||
</Tile>
|
||||
|
@ -537,9 +572,19 @@ const ManageButtons = ({ hierarchy, selected, setSelected, setError }: IManageBu
|
|||
onClick={async () => {
|
||||
setRemoving(true);
|
||||
try {
|
||||
const userId = cli.getUserId();
|
||||
for (const [parentId, childId] of selectedRelations) {
|
||||
await cli.sendStateEvent(parentId, EventType.SpaceChild, {}, childId);
|
||||
|
||||
// remove the child->parent relation too, if we have permission to.
|
||||
const childRoom = cli.getRoom(childId);
|
||||
const parentRelation = childRoom?.currentState.getStateEvents(EventType.SpaceParent, parentId);
|
||||
if (childRoom?.currentState.maySendStateEvent(EventType.SpaceParent, userId) &&
|
||||
Array.isArray(parentRelation?.getContent().via)
|
||||
) {
|
||||
await cli.sendStateEvent(childId, EventType.SpaceParent, {}, parentId);
|
||||
}
|
||||
|
||||
hierarchy.removeRelation(parentId, childId);
|
||||
}
|
||||
} catch (e) {
|
||||
|
@ -678,9 +723,8 @@ const SpaceHierarchy = ({
|
|||
parents={new Set()}
|
||||
selectedMap={selected}
|
||||
onToggleClick={hasPermissions ? onToggleClick : undefined}
|
||||
onViewRoomClick={(roomId, autoJoin, roomType) => {
|
||||
showRoom(cli, hierarchy, roomId, autoJoin, roomType);
|
||||
}}
|
||||
onViewRoomClick={(roomId, roomType) => showRoom(cli, hierarchy, roomId, roomType)}
|
||||
onJoinRoomClick={(roomId) => joinRoom(cli, hierarchy, roomId)}
|
||||
/>
|
||||
</>;
|
||||
} else if (!hierarchy.canLoadMore) {
|
||||
|
|
|
@ -56,7 +56,7 @@ import {
|
|||
showSpaceInvite,
|
||||
showSpaceSettings,
|
||||
} from "../../utils/space";
|
||||
import SpaceHierarchy, { showRoom } from "./SpaceHierarchy";
|
||||
import SpaceHierarchy, { joinRoom, showRoom } from "./SpaceHierarchy";
|
||||
import MemberAvatar from "../views/avatars/MemberAvatar";
|
||||
import SpaceStore from "../../stores/SpaceStore";
|
||||
import FacePile from "../views/elements/FacePile";
|
||||
|
@ -507,7 +507,7 @@ const SpaceLanding = ({ space }: { space: Room }) => {
|
|||
) }
|
||||
</RoomTopic>
|
||||
|
||||
<SpaceHierarchy space={space} showRoom={showRoom} additionalButtons={addRoomButton} />
|
||||
<SpaceHierarchy space={space} showRoom={showRoom} joinRoom={joinRoom} additionalButtons={addRoomButton} />
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
@ -667,10 +667,6 @@ const SpaceSetupPrivateScope = ({ space, justCreatedOpts, onFinished }) => {
|
|||
<h3>{ _t("Me and my teammates") }</h3>
|
||||
<div>{ _t("A private space for you and your teammates") }</div>
|
||||
</AccessibleButton>
|
||||
<div className="mx_SpaceRoomView_betaWarning">
|
||||
<h3>{ _t("Teammates might not be able to view or join any private rooms you make.") }</h3>
|
||||
<p>{ _t("We're working on this, but just want to let you know.") }</p>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
|
|
@ -17,23 +17,22 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
import Modal from "../../../Modal";
|
||||
import PasswordReset from "../../../PasswordReset";
|
||||
import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../../utils/AutoDiscoveryUtils";
|
||||
import classNames from 'classnames';
|
||||
import AuthPage from "../../views/auth/AuthPage";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import ServerPicker from "../../views/elements/ServerPicker";
|
||||
import EmailField from "../../views/auth/EmailField";
|
||||
import PassphraseField from '../../views/auth/PassphraseField';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { PASSWORD_MIN_SCORE } from '../../views/auth/RegistrationForm';
|
||||
import withValidation, { IValidationResult } from "../../views/elements/Validation";
|
||||
import * as Email from "../../../email";
|
||||
import { IValidationResult } from "../../views/elements/Validation";
|
||||
import InlineSpinner from '../../views/elements/InlineSpinner';
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
enum Phase {
|
||||
// Show the forgot password inputs
|
||||
|
@ -227,30 +226,10 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
|||
});
|
||||
}
|
||||
|
||||
private validateEmailRules = withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test({ value, allowEmpty }) {
|
||||
return allowEmpty || !!value;
|
||||
},
|
||||
invalid: () => _t("Enter email address"),
|
||||
}, {
|
||||
key: "email",
|
||||
test: ({ value }) => !value || Email.looksValid(value),
|
||||
invalid: () => _t("Doesn't look like a valid email address"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
private onEmailValidate = async (fieldState) => {
|
||||
const result = await this.validateEmailRules(fieldState);
|
||||
|
||||
private onEmailValidate = (result: IValidationResult) => {
|
||||
this.setState({
|
||||
emailFieldValid: result.valid,
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
private onPasswordValidate(result: IValidationResult) {
|
||||
|
@ -302,14 +281,12 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
|||
/>
|
||||
<form onSubmit={this.onSubmitForm}>
|
||||
<div className="mx_AuthBody_fieldRow">
|
||||
<Field
|
||||
<EmailField
|
||||
name="reset_email" // define a name so browser's password autofill gets less confused
|
||||
type="text"
|
||||
label={_t('Email')}
|
||||
value={this.state.email}
|
||||
fieldRef={field => this['email_field'] = field}
|
||||
autoFocus={true}
|
||||
onChange={this.onInputChanged.bind(this, "email")}
|
||||
ref={field => this['email_field'] = field}
|
||||
autoFocus
|
||||
onValidate={this.onEmailValidate}
|
||||
onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_email_focus")}
|
||||
onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_email_blur")}
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { PureComponent, RefCallback, RefObject } from "react";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Field, { IInputProps } from "../elements/Field";
|
||||
import { _t, _td } from "../../../languageHandler";
|
||||
import withValidation, { IFieldState, IValidationResult } from "../elements/Validation";
|
||||
import * as Email from "../../../email";
|
||||
|
||||
interface IProps extends Omit<IInputProps, "onValidate"> {
|
||||
id?: string;
|
||||
fieldRef?: RefCallback<Field> | RefObject<Field>;
|
||||
value: string;
|
||||
autoFocus?: boolean;
|
||||
|
||||
label?: string;
|
||||
labelRequired?: string;
|
||||
labelInvalid?: string;
|
||||
|
||||
// When present, completely overrides the default validation rules.
|
||||
validationRules?: (fieldState: IFieldState) => Promise<IValidationResult>;
|
||||
|
||||
onChange(ev: React.FormEvent<HTMLElement>): void;
|
||||
onValidate?(result: IValidationResult): void;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.EmailField")
|
||||
class EmailField extends PureComponent<IProps> {
|
||||
static defaultProps = {
|
||||
label: _td("Email"),
|
||||
labelRequired: _td("Enter email address"),
|
||||
labelInvalid: _td("Doesn't look like a valid email address"),
|
||||
};
|
||||
|
||||
public readonly validate = withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test: ({ value, allowEmpty }) => allowEmpty || !!value,
|
||||
invalid: () => _t(this.props.labelRequired),
|
||||
},
|
||||
{
|
||||
key: "email",
|
||||
test: ({ value }) => !value || Email.looksValid(value),
|
||||
invalid: () => _t(this.props.labelInvalid),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
onValidate = async (fieldState: IFieldState) => {
|
||||
let validate = this.validate;
|
||||
if (this.props.validationRules) {
|
||||
validate = this.props.validationRules;
|
||||
}
|
||||
|
||||
const result = await validate(fieldState);
|
||||
if (this.props.onValidate) {
|
||||
this.props.onValidate(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
render() {
|
||||
return <Field
|
||||
id={this.props.id}
|
||||
ref={this.props.fieldRef}
|
||||
type="text"
|
||||
label={_t(this.props.label)}
|
||||
value={this.props.value}
|
||||
autoFocus={this.props.autoFocus}
|
||||
onChange={this.props.onChange}
|
||||
onValidate={this.onValidate}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
export default EmailField;
|
|
@ -22,11 +22,11 @@ import SdkConfig from '../../../SdkConfig';
|
|||
import { ValidatedServerConfig } from "../../../utils/AutoDiscoveryUtils";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import withValidation from "../elements/Validation";
|
||||
import * as Email from "../../../email";
|
||||
import withValidation, { IValidationResult } from "../elements/Validation";
|
||||
import Field from "../elements/Field";
|
||||
import CountryDropdown from "./CountryDropdown";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import EmailField from "./EmailField";
|
||||
|
||||
// For validating phone numbers without country codes
|
||||
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
|
||||
|
@ -262,26 +262,8 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
|||
return result;
|
||||
};
|
||||
|
||||
private validateEmailRules = withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test({ value, allowEmpty }) {
|
||||
return allowEmpty || !!value;
|
||||
},
|
||||
invalid: () => _t("Enter email address"),
|
||||
}, {
|
||||
key: "email",
|
||||
test: ({ value }) => !value || Email.looksValid(value),
|
||||
invalid: () => _t("Doesn't look like a valid email address"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
private onEmailValidate = async (fieldState) => {
|
||||
const result = await this.validateEmailRules(fieldState);
|
||||
private onEmailValidate = (result: IValidationResult) => {
|
||||
this.markFieldValid(LoginField.Email, result.valid);
|
||||
return result;
|
||||
};
|
||||
|
||||
private validatePhoneNumberRules = withValidation({
|
||||
|
@ -332,12 +314,10 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
|||
switch (loginType) {
|
||||
case LoginField.Email:
|
||||
classes.error = this.props.loginIncorrect && !this.props.username;
|
||||
return <Field
|
||||
return <EmailField
|
||||
className={classNames(classes)}
|
||||
name="username" // make it a little easier for browser's remember-password
|
||||
key="email_input"
|
||||
type="text"
|
||||
label={_t("Email")}
|
||||
placeholder="joe@example.com"
|
||||
value={this.props.username}
|
||||
onChange={this.onUsernameChanged}
|
||||
|
@ -346,7 +326,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
|||
disabled={this.props.disableSubmit}
|
||||
autoFocus={autoFocus}
|
||||
onValidate={this.onEmailValidate}
|
||||
ref={field => this[LoginField.Email] = field}
|
||||
fieldRef={field => this[LoginField.Email] = field}
|
||||
/>;
|
||||
case LoginField.MatrixId:
|
||||
classes.error = this.props.loginIncorrect && !this.props.username;
|
||||
|
|
|
@ -24,8 +24,9 @@ import Modal from '../../../Modal';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import { SAFE_LOCALPART_REGEX } from '../../../Registration';
|
||||
import withValidation from '../elements/Validation';
|
||||
import withValidation, { IValidationResult } from '../elements/Validation';
|
||||
import { ValidatedServerConfig } from "../../../utils/AutoDiscoveryUtils";
|
||||
import EmailField from "./EmailField";
|
||||
import PassphraseField from "./PassphraseField";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import Field from '../elements/Field';
|
||||
|
@ -252,10 +253,8 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
|
|||
});
|
||||
};
|
||||
|
||||
private onEmailValidate = async fieldState => {
|
||||
const result = await this.validateEmailRules(fieldState);
|
||||
private onEmailValidate = (result: IValidationResult) => {
|
||||
this.markFieldValid(RegistrationField.Email, result.valid);
|
||||
return result;
|
||||
};
|
||||
|
||||
private validateEmailRules = withValidation({
|
||||
|
@ -425,14 +424,14 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
|
|||
if (!this.showEmail()) {
|
||||
return null;
|
||||
}
|
||||
const emailPlaceholder = this.authStepIsRequired('m.login.email.identity') ?
|
||||
const emailLabel = this.authStepIsRequired('m.login.email.identity') ?
|
||||
_t("Email") :
|
||||
_t("Email (optional)");
|
||||
return <Field
|
||||
ref={field => this[RegistrationField.Email] = field}
|
||||
type="text"
|
||||
label={emailPlaceholder}
|
||||
return <EmailField
|
||||
fieldRef={field => this[RegistrationField.Email] = field}
|
||||
label={emailLabel}
|
||||
value={this.state.email}
|
||||
validationRules={this.validateEmailRules.bind(this)}
|
||||
onChange={this.onEmailChange}
|
||||
onValidate={this.onEmailValidate}
|
||||
onFocus={() => CountlyAnalytics.instance.track("onboarding_registration_email_focus")}
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
import React, { ComponentProps } from 'react';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import classNames from "classnames";
|
||||
|
||||
import BaseAvatar from './BaseAvatar';
|
||||
|
@ -83,8 +84,7 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
|
|||
};
|
||||
}
|
||||
|
||||
// TODO: type when js-sdk has types
|
||||
private onRoomStateEvents = (ev: any) => {
|
||||
private onRoomStateEvents = (ev: MatrixEvent) => {
|
||||
if (!this.props.room ||
|
||||
ev.getRoomId() !== this.props.room.roomId ||
|
||||
ev.getType() !== 'm.room.avatar'
|
||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { ComponentType } from 'react';
|
||||
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
|
@ -85,7 +85,9 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
|
|||
|
||||
private onExportE2eKeysClicked = (): void => {
|
||||
Modal.createTrackedDialogAsync('Export E2E Keys', '',
|
||||
import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
|
||||
import(
|
||||
'../../../async-components/views/dialogs/security/ExportE2eKeysDialog'
|
||||
) as unknown as Promise<ComponentType<{}>>,
|
||||
{
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
},
|
||||
|
@ -111,7 +113,9 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
|
|||
);
|
||||
} else {
|
||||
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
|
||||
import("../../../async-components/views/dialogs/security/CreateKeyBackupDialog"),
|
||||
import(
|
||||
"../../../async-components/views/dialogs/security/CreateKeyBackupDialog"
|
||||
) as unknown as Promise<ComponentType<{}>>,
|
||||
null, null, /* priority = */ false, /* static = */ true,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -21,25 +21,14 @@ import { _t } from '../../../languageHandler';
|
|||
import { IDialogProps } from "./IDialogProps";
|
||||
import Field from "../elements/Field";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import withValidation from "../elements/Validation";
|
||||
import * as Email from "../../../email";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
import EmailField from "../auth/EmailField";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
onFinished(continued: boolean, email?: string): void;
|
||||
}
|
||||
|
||||
const validation = withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "email",
|
||||
test: ({ value }) => !value || Email.looksValid(value),
|
||||
invalid: () => _t("Doesn't look like a valid email address"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const RegistrationEmailPromptDialog: React.FC<IProps> = ({ onFinished }) => {
|
||||
const [email, setEmail] = useState("");
|
||||
const fieldRef = useRef<Field>();
|
||||
|
@ -47,11 +36,11 @@ const RegistrationEmailPromptDialog: React.FC<IProps> = ({ onFinished }) => {
|
|||
const onSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (email) {
|
||||
const valid = await fieldRef.current.validate({ allowEmpty: false });
|
||||
const valid = await fieldRef.current.validate({});
|
||||
|
||||
if (!valid) {
|
||||
fieldRef.current.focus();
|
||||
fieldRef.current.validate({ allowEmpty: false, focused: true });
|
||||
fieldRef.current.validate({ focused: true });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -72,16 +61,15 @@ const RegistrationEmailPromptDialog: React.FC<IProps> = ({ onFinished }) => {
|
|||
b: sub => <b>{ sub }</b>,
|
||||
}) }</p>
|
||||
<form onSubmit={onSubmit}>
|
||||
<Field
|
||||
ref={fieldRef}
|
||||
<EmailField
|
||||
fieldRef={fieldRef}
|
||||
autoFocus={true}
|
||||
type="text"
|
||||
label={_t("Email (optional)")}
|
||||
value={email}
|
||||
onChange={ev => {
|
||||
setEmail(ev.target.value);
|
||||
const target = ev.target as HTMLInputElement;
|
||||
setEmail(target.value);
|
||||
}}
|
||||
onValidate={async fieldState => await validation(fieldState)}
|
||||
onFocus={() => CountlyAnalytics.instance.track("onboarding_registration_email2_focus")}
|
||||
onBlur={() => CountlyAnalytics.instance.track("onboarding_registration_email2_blur")}
|
||||
/>
|
||||
|
|
|
@ -44,6 +44,7 @@ import MessageEditHistoryDialog from "../dialogs/MessageEditHistoryDialog";
|
|||
import EditMessageComposer from '../rooms/EditMessageComposer';
|
||||
import LinkPreviewGroup from '../rooms/LinkPreviewGroup';
|
||||
import { IBodyProps } from "./IBodyProps";
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
|
||||
const MAX_HIGHLIGHT_LENGTH = 4096;
|
||||
|
||||
|
@ -62,6 +63,9 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
|||
private unmounted = false;
|
||||
private pills: Element[] = [];
|
||||
|
||||
static contextType = RoomContext;
|
||||
public context!: React.ContextType<typeof RoomContext>;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
|
@ -406,6 +410,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
|||
dis.dispatch<ComposerInsertPayload>({
|
||||
action: Action.ComposerInsert,
|
||||
userId: mxEvent.getSender(),
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -75,6 +75,7 @@ import ConfirmSpaceUserActionDialog from "../dialogs/ConfirmSpaceUserActionDialo
|
|||
import { bulkSpaceBehaviour } from "../../../utils/space";
|
||||
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../../settings/UIFeature";
|
||||
import { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||
|
||||
export interface IDevice {
|
||||
deviceId: string;
|
||||
|
@ -377,6 +378,7 @@ const UserOptionsSection: React.FC<{
|
|||
dis.dispatch<ComposerInsertPayload>({
|
||||
action: Action.ComposerInsert,
|
||||
userId: member.userId,
|
||||
timelineRenderingType: TimelineRenderingType.Room,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ import {
|
|||
formatRangeAsCode,
|
||||
toggleInlineFormat,
|
||||
replaceRangeAndMoveCaret,
|
||||
formatRangeAsLink,
|
||||
} from '../../../editor/operations';
|
||||
import { getCaretOffsetAndText, getRangeForSelection } from '../../../editor/dom';
|
||||
import Autocomplete, { generateCompletionDomId } from '../rooms/Autocomplete';
|
||||
|
@ -476,6 +477,8 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
switch (autocompleteAction) {
|
||||
case AutocompleteAction.ForceComplete:
|
||||
case AutocompleteAction.Complete:
|
||||
this.historyManager.ensureLastChangesPushed(this.props.model);
|
||||
this.modifiedFlag = true;
|
||||
autoComplete.confirmCompletion();
|
||||
handled = true;
|
||||
break;
|
||||
|
@ -705,6 +708,9 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
case Formatting.Quote:
|
||||
formatRangeAsQuote(range);
|
||||
break;
|
||||
case Formatting.InsertLink:
|
||||
formatRangeAsLink(range);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -46,6 +46,7 @@ import { createRedactEventDialog } from '../dialogs/ConfirmRedactDialog';
|
|||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { withMatrixClientHOC, MatrixClientProps } from '../../../contexts/MatrixClientContext';
|
||||
import RoomContext from '../../../contexts/RoomContext';
|
||||
import { ComposerType } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||
|
||||
function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
|
||||
const html = mxEvent.getContent().formatted_body;
|
||||
|
@ -498,7 +499,12 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
|
|||
};
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
if (payload.action === "edit_composer_insert" && this.editorRef.current) {
|
||||
if (!this.editorRef.current) return;
|
||||
|
||||
if (payload.action === Action.ComposerInsert) {
|
||||
if (payload.timelineRenderingType !== this.context.timelineRenderingType) return;
|
||||
if (payload.composerType !== ComposerType.Edit) return;
|
||||
|
||||
if (payload.userId) {
|
||||
this.editorRef.current?.insertMention(payload.userId);
|
||||
} else if (payload.event) {
|
||||
|
@ -506,7 +512,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
|
|||
} else if (payload.text) {
|
||||
this.editorRef.current?.insertPlaintext(payload.text);
|
||||
}
|
||||
} else if (payload.action === Action.FocusEditMessageComposer && this.editorRef.current) {
|
||||
} else if (payload.action === Action.FocusEditMessageComposer) {
|
||||
this.editorRef.current.focus();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -22,7 +22,6 @@ import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|||
import { Relations } from "matrix-js-sdk/src/models/relations";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import ReplyChain from "../elements/ReplyChain";
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -62,6 +61,9 @@ import MKeyVerificationConclusion from "../messages/MKeyVerificationConclusion";
|
|||
import { dispatchShowThreadEvent } from '../../../dispatcher/dispatch-actions/threads';
|
||||
import { MessagePreviewStore } from '../../../stores/room-list/MessagePreviewStore';
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||
|
||||
const eventTileTypes = {
|
||||
[EventType.RoomMessage]: 'messages.MessageEvent',
|
||||
[EventType.Sticker]: 'messages.MessageEvent',
|
||||
|
@ -312,6 +314,8 @@ interface IProps {
|
|||
|
||||
// whether or not to display thread info
|
||||
showThreadInfo?: boolean;
|
||||
|
||||
timelineRenderingType?: TimelineRenderingType;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -854,10 +858,11 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
onSenderProfileClick = () => {
|
||||
const mxEvent = this.props.mxEvent;
|
||||
if (!this.props.timelineRenderingType) return;
|
||||
dis.dispatch<ComposerInsertPayload>({
|
||||
action: Action.ComposerInsert,
|
||||
userId: mxEvent.getSender(),
|
||||
userId: this.props.mxEvent.getSender(),
|
||||
timelineRenderingType: this.props.timelineRenderingType,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -1090,7 +1095,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
if (needsSenderProfile && this.props.hideSender !== true) {
|
||||
if (!this.props.tileShape) {
|
||||
if (!this.props.tileShape || this.props.tileShape === TileShape.Thread) {
|
||||
sender = <SenderProfile onClick={this.onSenderProfileClick}
|
||||
mxEvent={this.props.mxEvent}
|
||||
enableFlair={this.props.enableFlair}
|
||||
|
|
|
@ -253,7 +253,8 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
private ref: React.RefObject<HTMLDivElement> = createRef();
|
||||
private instanceId: number;
|
||||
|
||||
public static contextType = RoomContext;
|
||||
static contextType = RoomContext;
|
||||
public context!: React.ContextType<typeof RoomContext>;
|
||||
|
||||
static defaultProps = {
|
||||
compact: false,
|
||||
|
@ -399,13 +400,14 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private addEmoji(emoji: string): boolean {
|
||||
private addEmoji = (emoji: string): boolean => {
|
||||
dis.dispatch<ComposerInsertPayload>({
|
||||
action: Action.ComposerInsert,
|
||||
text: emoji,
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
private sendMessage = async () => {
|
||||
if (this.state.haveRecording && this.voiceRecordingButton.current) {
|
||||
|
|
|
@ -27,6 +27,7 @@ export enum Formatting {
|
|||
Strikethrough = "strikethrough",
|
||||
Code = "code",
|
||||
Quote = "quote",
|
||||
InsertLink = "insert_link",
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
|
@ -57,6 +58,7 @@ export default class MessageComposerFormatBar extends React.PureComponent<IProps
|
|||
<FormatButton label={_t("Strikethrough")} onClick={() => this.props.onAction(Formatting.Strikethrough)} icon="Strikethrough" visible={this.state.visible} />
|
||||
<FormatButton label={_t("Code block")} onClick={() => this.props.onAction(Formatting.Code)} icon="Code" visible={this.state.visible} />
|
||||
<FormatButton label={_t("Quote")} onClick={() => this.props.onAction(Formatting.Quote)} icon="Quote" shortcut={this.props.shortcuts.quote} visible={this.state.visible} />
|
||||
<FormatButton label={_t("Insert link")} onClick={() => this.props.onAction(Formatting.InsertLink)} icon="InsertLink" visible={this.state.visible} />
|
||||
</div>);
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ReactComponentElement } from "react";
|
||||
import React, { createRef, ReactComponentElement } from "react";
|
||||
import { Dispatcher } from "flux";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import * as fbEmitter from "fbemitter";
|
||||
|
@ -22,7 +22,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
|
|||
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||
|
||||
import { _t, _td } from "../../../languageHandler";
|
||||
import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
|
||||
import { RovingTabIndexProvider, IState as IRovingTabIndexState } from "../../../accessibility/RovingTabIndex";
|
||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
|
||||
import RoomViewStore from "../../../stores/RoomViewStore";
|
||||
|
@ -54,7 +54,7 @@ import { shouldShowComponent } from "../../../customisations/helpers/UIComponent
|
|||
import { UIComponent } from "../../../settings/UIFeature";
|
||||
|
||||
interface IProps {
|
||||
onKeyDown: (ev: React.KeyboardEvent) => void;
|
||||
onKeyDown: (ev: React.KeyboardEvent, state: IRovingTabIndexState) => void;
|
||||
onFocus: (ev: React.FocusEvent) => void;
|
||||
onBlur: (ev: React.FocusEvent) => void;
|
||||
onResize: () => void;
|
||||
|
@ -249,6 +249,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
private dispatcherRef;
|
||||
private customTagStoreRef;
|
||||
private roomStoreToken: fbEmitter.EventSubscription;
|
||||
private treeRef = createRef<HTMLDivElement>();
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
@ -505,6 +506,12 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
// focus the first focusable element in this aria treeview widget
|
||||
[...this.treeRef.current?.querySelectorAll<HTMLElement>('[role="treeitem"]')]
|
||||
.find(e => e.offsetParent !== null)?.focus();
|
||||
}
|
||||
|
||||
public render() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const userId = cli.getUserId();
|
||||
|
@ -584,7 +591,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
|
||||
const sublists = this.renderSublists();
|
||||
return (
|
||||
<RovingTabIndexProvider handleHomeEnd={true} onKeyDown={this.props.onKeyDown}>
|
||||
<RovingTabIndexProvider handleHomeEnd handleUpDown onKeyDown={this.props.onKeyDown}>
|
||||
{ ({ onKeyDownHandler }) => (
|
||||
<div
|
||||
onFocus={this.props.onFocus}
|
||||
|
@ -593,6 +600,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
className="mx_RoomList"
|
||||
role="tree"
|
||||
aria-label={_t("Rooms")}
|
||||
ref={this.treeRef}
|
||||
>
|
||||
{ sublists }
|
||||
{ explorePrompt }
|
||||
|
|
|
@ -58,6 +58,7 @@ import { ActionPayload } from "../../../dispatcher/payloads";
|
|||
import { decorateStartSendingTime, sendRoundTripMetric } from "../../../sendTimePerformanceMetrics";
|
||||
import RoomContext from '../../../contexts/RoomContext';
|
||||
import DocumentPosition from "../../../editor/position";
|
||||
import { ComposerType } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||
|
||||
function addReplyToMessageContent(
|
||||
content: IContent,
|
||||
|
@ -591,7 +592,10 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
this.editorRef.current?.focus();
|
||||
}
|
||||
break;
|
||||
case "send_composer_insert":
|
||||
case Action.ComposerInsert:
|
||||
if (payload.timelineRenderingType !== this.context.timelineRenderingType) break;
|
||||
if (payload.composerType !== ComposerType.Send) break;
|
||||
|
||||
if (payload.userId) {
|
||||
this.editorRef.current?.insertMention(payload.userId);
|
||||
} else if (payload.event) {
|
||||
|
|
|
@ -15,10 +15,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
|
||||
import Field from "../elements/Field";
|
||||
import React, { ComponentType } from 'react';
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import Spinner from '../elements/Spinner';
|
||||
|
@ -29,6 +27,7 @@ import PassphraseField from "../auth/PassphraseField";
|
|||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { PASSWORD_MIN_SCORE } from '../auth/RegistrationForm';
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import SetEmailDialog from "../dialogs/SetEmailDialog";
|
||||
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||
|
||||
|
@ -187,7 +186,9 @@ export default class ChangePassword extends React.Component<IProps, IState> {
|
|||
|
||||
private onExportE2eKeysClicked = (): void => {
|
||||
Modal.createTrackedDialogAsync('Export E2E Keys', 'Change Password',
|
||||
import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
|
||||
import(
|
||||
'../../../async-components/views/dialogs/security/ExportE2eKeysDialog'
|
||||
) as unknown as Promise<ComponentType<{}>>,
|
||||
{
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
},
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { ComponentType } from 'react';
|
||||
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -92,14 +92,18 @@ export default class CryptographyPanel extends React.Component<IProps, IState> {
|
|||
|
||||
private onExportE2eKeysClicked = (): void => {
|
||||
Modal.createTrackedDialogAsync('Export E2E Keys', '',
|
||||
import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
|
||||
import(
|
||||
'../../../async-components/views/dialogs/security/ExportE2eKeysDialog'
|
||||
) as unknown as Promise<ComponentType<{}>>,
|
||||
{ matrixClient: MatrixClientPeg.get() },
|
||||
);
|
||||
};
|
||||
|
||||
private onImportE2eKeysClicked = (): void => {
|
||||
Modal.createTrackedDialogAsync('Import E2E Keys', '',
|
||||
import('../../../async-components/views/dialogs/security/ImportE2eKeysDialog'),
|
||||
import(
|
||||
'../../../async-components/views/dialogs/security/ImportE2eKeysDialog'
|
||||
) as unknown as Promise<ComponentType<{}>>,
|
||||
{ matrixClient: MatrixClientPeg.get() },
|
||||
);
|
||||
};
|
||||
|
|
|
@ -15,10 +15,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
|
||||
import { TrustInfo } from "matrix-js-sdk/src/crypto/backup";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import React, { ComponentType } from 'react';
|
||||
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -30,6 +27,8 @@ import QuestionDialog from '../dialogs/QuestionDialog';
|
|||
import RestoreKeyBackupDialog from '../dialogs/security/RestoreKeyBackupDialog';
|
||||
import { accessSecretStorage } from '../../../SecurityManager';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
|
||||
import { TrustInfo } from "matrix-js-sdk/src/crypto/backup";
|
||||
|
||||
interface IState {
|
||||
loading: boolean;
|
||||
|
@ -44,6 +43,8 @@ interface IState {
|
|||
sessionsRemaining: number;
|
||||
}
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
@replaceableComponent("views.settings.SecureBackupPanel")
|
||||
export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
|
||||
private unmounted = false;
|
||||
|
@ -169,7 +170,9 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
|
|||
|
||||
private startNewBackup = (): void => {
|
||||
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
|
||||
import('../../../async-components/views/dialogs/security/CreateKeyBackupDialog'),
|
||||
import(
|
||||
'../../../async-components/views/dialogs/security/CreateKeyBackupDialog'
|
||||
) as unknown as Promise<ComponentType<{}>>,
|
||||
{
|
||||
onFinished: () => {
|
||||
this.loadBackupStatus();
|
||||
|
|
|
@ -19,7 +19,7 @@ import { logger } from "matrix-js-sdk/src/logger";
|
|||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { enumerateThemes } from "../../../theme";
|
||||
import { enumerateThemes, findHighContrastTheme, findNonHighContrastTheme, isHighContrastTheme } from "../../../theme";
|
||||
import ThemeWatcher from "../../../settings/watchers/ThemeWatcher";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
|
@ -159,7 +159,37 @@ export default class ThemeChoicePanel extends React.Component<IProps, IState> {
|
|||
this.setState({ customThemeUrl: e.target.value });
|
||||
};
|
||||
|
||||
public render() {
|
||||
private renderHighContrastCheckbox(): React.ReactElement<HTMLDivElement> {
|
||||
if (
|
||||
!this.state.useSystemTheme && (
|
||||
findHighContrastTheme(this.state.theme) ||
|
||||
isHighContrastTheme(this.state.theme)
|
||||
)
|
||||
) {
|
||||
return <div>
|
||||
<StyledCheckbox
|
||||
checked={isHighContrastTheme(this.state.theme)}
|
||||
onChange={(e) => this.highContrastThemeChanged(e.target.checked)}
|
||||
>
|
||||
{ _t( "Use high contrast" ) }
|
||||
</StyledCheckbox>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
private highContrastThemeChanged(checked: boolean): void {
|
||||
let newTheme: string;
|
||||
if (checked) {
|
||||
newTheme = findHighContrastTheme(this.state.theme);
|
||||
} else {
|
||||
newTheme = findNonHighContrastTheme(this.state.theme);
|
||||
}
|
||||
if (newTheme) {
|
||||
this.onThemeChange(newTheme);
|
||||
}
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<HTMLDivElement> {
|
||||
const themeWatcher = new ThemeWatcher();
|
||||
let systemThemeSection: JSX.Element;
|
||||
if (themeWatcher.isSystemThemeSupported()) {
|
||||
|
@ -210,7 +240,8 @@ export default class ThemeChoicePanel extends React.Component<IProps, IState> {
|
|||
|
||||
// XXX: replace any type here
|
||||
const themes = Object.entries<any>(enumerateThemes())
|
||||
.map(p => ({ id: p[0], name: p[1] })); // convert pairs to objects for code readability
|
||||
.map(p => ({ id: p[0], name: p[1] })) // convert pairs to objects for code readability
|
||||
.filter(p => !isHighContrastTheme(p.id));
|
||||
const builtInThemes = themes.filter(p => !p.id.startsWith("custom-"));
|
||||
const customThemes = themes.filter(p => !builtInThemes.includes(p))
|
||||
.sort((a, b) => compare(a.name, b.name));
|
||||
|
@ -229,12 +260,21 @@ export default class ThemeChoicePanel extends React.Component<IProps, IState> {
|
|||
className: "mx_ThemeSelector_" + t.id,
|
||||
}))}
|
||||
onChange={this.onThemeChange}
|
||||
value={this.state.useSystemTheme ? undefined : this.state.theme}
|
||||
value={this.apparentSelectedThemeId()}
|
||||
outlined
|
||||
/>
|
||||
</div>
|
||||
{ this.renderHighContrastCheckbox() }
|
||||
{ customThemeForm }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
apparentSelectedThemeId() {
|
||||
if (this.state.useSystemTheme) {
|
||||
return undefined;
|
||||
}
|
||||
const nonHighContrast = findNonHighContrastTheme(this.state.theme);
|
||||
return nonHighContrast ? nonHighContrast : this.state.theme;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,7 +43,6 @@ import SpaceStore, {
|
|||
} from "../../../stores/SpaceStore";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
|
||||
import { Key } from "../../../Keyboard";
|
||||
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
||||
import SpaceContextMenu from "../context_menus/SpaceContextMenu";
|
||||
import IconizedContextMenu, {
|
||||
|
@ -228,75 +227,12 @@ const SpacePanel = () => {
|
|||
return () => UIStore.instance.stopTrackingElementDimensions("SpacePanel");
|
||||
}, []);
|
||||
|
||||
const onKeyDown = (ev: React.KeyboardEvent) => {
|
||||
if (ev.defaultPrevented) return;
|
||||
|
||||
let handled = true;
|
||||
|
||||
switch (ev.key) {
|
||||
case Key.ARROW_UP:
|
||||
onMoveFocus(ev.target as Element, true);
|
||||
break;
|
||||
case Key.ARROW_DOWN:
|
||||
onMoveFocus(ev.target as Element, false);
|
||||
break;
|
||||
default:
|
||||
handled = false;
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
// consume all other keys in context menu
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const onMoveFocus = (element: Element, up: boolean) => {
|
||||
let descending = false; // are we currently descending or ascending through the DOM tree?
|
||||
let classes: DOMTokenList;
|
||||
|
||||
do {
|
||||
const child = up ? element.lastElementChild : element.firstElementChild;
|
||||
const sibling = up ? element.previousElementSibling : element.nextElementSibling;
|
||||
|
||||
if (descending) {
|
||||
if (child) {
|
||||
element = child;
|
||||
} else if (sibling) {
|
||||
element = sibling;
|
||||
} else {
|
||||
descending = false;
|
||||
element = element.parentElement;
|
||||
}
|
||||
} else {
|
||||
if (sibling) {
|
||||
element = sibling;
|
||||
descending = true;
|
||||
} else {
|
||||
element = element.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
if (element) {
|
||||
if (element.classList.contains("mx_ContextualMenu")) { // we hit the top
|
||||
element = up ? element.lastElementChild : element.firstElementChild;
|
||||
descending = true;
|
||||
}
|
||||
classes = element.classList;
|
||||
}
|
||||
} while (element && !classes.contains("mx_SpaceButton"));
|
||||
|
||||
if (element) {
|
||||
(element as HTMLElement).focus();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={result => {
|
||||
if (!result.destination) return; // dropped outside the list
|
||||
SpaceStore.instance.moveRootSpace(result.source.index, result.destination.index);
|
||||
}}>
|
||||
<RovingTabIndexProvider handleHomeEnd={true} onKeyDown={onKeyDown}>
|
||||
<RovingTabIndexProvider handleHomeEnd handleUpDown>
|
||||
{ ({ onKeyDownHandler }) => (
|
||||
<ul
|
||||
className={classNames("mx_SpacePanel", { collapsed: isPanelCollapsed })}
|
||||
|
|
|
@ -279,6 +279,8 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
if (window.electron?.getDesktopCapturerSources) {
|
||||
const { finished } = Modal.createDialog(DesktopCapturerSourcePicker);
|
||||
const [source] = await finished;
|
||||
if (!source) return;
|
||||
|
||||
isScreensharing = await this.props.call.setScreensharingEnabled(true, source);
|
||||
} else {
|
||||
isScreensharing = await this.props.call.setScreensharingEnabled(true);
|
||||
|
@ -545,6 +547,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
<div
|
||||
className={classes}
|
||||
onMouseMove={this.onMouseMove}
|
||||
ref={this.contentRef}
|
||||
>
|
||||
{ sidebar }
|
||||
<div className="mx_CallView_voice_avatarsContainer">
|
||||
|
|
|
@ -18,9 +18,17 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|||
|
||||
import { ActionPayload } from "../payloads";
|
||||
import { Action } from "../actions";
|
||||
import { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||
|
||||
export enum ComposerType {
|
||||
Send = "send",
|
||||
Edit = "edit",
|
||||
}
|
||||
|
||||
interface IBaseComposerInsertPayload extends ActionPayload {
|
||||
action: Action.ComposerInsert;
|
||||
timelineRenderingType: TimelineRenderingType;
|
||||
composerType?: ComposerType; // falsey if should be re-dispatched to the correct composer
|
||||
}
|
||||
|
||||
interface IComposerInsertMentionPayload extends IBaseComposerInsertPayload {
|
||||
|
|
|
@ -32,13 +32,13 @@ export function replaceRangeAndExpandSelection(range: Range, newParts: Part[]):
|
|||
});
|
||||
}
|
||||
|
||||
export function replaceRangeAndMoveCaret(range: Range, newParts: Part[]): void {
|
||||
export function replaceRangeAndMoveCaret(range: Range, newParts: Part[], offset = 0): void {
|
||||
const { model } = range;
|
||||
model.transform(() => {
|
||||
const oldLen = range.length;
|
||||
const addedLen = range.replace(newParts);
|
||||
const firstOffset = range.start.asOffset(model);
|
||||
const lastOffset = firstOffset.add(oldLen + addedLen);
|
||||
const lastOffset = firstOffset.add(oldLen + addedLen + offset);
|
||||
return lastOffset.asPosition(model);
|
||||
});
|
||||
}
|
||||
|
@ -103,6 +103,15 @@ export function formatRangeAsCode(range: Range): void {
|
|||
replaceRangeAndExpandSelection(range, parts);
|
||||
}
|
||||
|
||||
export function formatRangeAsLink(range: Range) {
|
||||
const { model, parts } = range;
|
||||
const { partCreator } = model;
|
||||
parts.unshift(partCreator.plain("["));
|
||||
parts.push(partCreator.plain("]()"));
|
||||
// We set offset to -1 here so that the caret lands between the brackets
|
||||
replaceRangeAndMoveCaret(range, parts, -1);
|
||||
}
|
||||
|
||||
// parts helper methods
|
||||
const isBlank = part => !part.text || !/\S/.test(part.text);
|
||||
const isNL = part => part.type === Type.Newline;
|
||||
|
|
|
@ -517,6 +517,8 @@
|
|||
"%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s upgraded this room.",
|
||||
"%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s made the room public to whoever knows the link.",
|
||||
"%(senderDisplayName)s made the room invite only.": "%(senderDisplayName)s made the room invite only.",
|
||||
"%(senderDisplayName)s changed who can join this room. <a>View settings</a>.": "%(senderDisplayName)s changed who can join this room. <a>View settings</a>.",
|
||||
"%(senderDisplayName)s changed who can join this room.": "%(senderDisplayName)s changed who can join this room.",
|
||||
"%(senderDisplayName)s changed the join rule to %(rule)s": "%(senderDisplayName)s changed the join rule to %(rule)s",
|
||||
"%(senderDisplayName)s has allowed guests to join the room.": "%(senderDisplayName)s has allowed guests to join the room.",
|
||||
"%(senderDisplayName)s has prevented guests from joining the room.": "%(senderDisplayName)s has prevented guests from joining the room.",
|
||||
|
@ -577,6 +579,7 @@
|
|||
"%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching %(newGlob)s for %(reason)s",
|
||||
"%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s",
|
||||
"Light": "Light",
|
||||
"Light high contrast": "Light high contrast",
|
||||
"Dark": "Dark",
|
||||
"%(displayName)s is typing …": "%(displayName)s is typing …",
|
||||
"%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …",
|
||||
|
@ -1291,6 +1294,7 @@
|
|||
"Invalid theme schema.": "Invalid theme schema.",
|
||||
"Error downloading theme information.": "Error downloading theme information.",
|
||||
"Theme added!": "Theme added!",
|
||||
"Use high contrast": "Use high contrast",
|
||||
"Custom theme URL": "Custom theme URL",
|
||||
"Add theme": "Add theme",
|
||||
"Theme": "Theme",
|
||||
|
@ -1606,6 +1610,7 @@
|
|||
"Strikethrough": "Strikethrough",
|
||||
"Code block": "Code block",
|
||||
"Quote": "Quote",
|
||||
"Insert link": "Insert link",
|
||||
"Only the two of you are in this conversation, unless either of you invites anyone to join.": "Only the two of you are in this conversation, unless either of you invites anyone to join.",
|
||||
"This is the beginning of your direct message history with <displayName/>.": "This is the beginning of your direct message history with <displayName/>.",
|
||||
"Topic: %(topic)s (<a>edit</a>)": "Topic: %(topic)s (<a>edit</a>)",
|
||||
|
@ -2520,7 +2525,6 @@
|
|||
"Message edits": "Message edits",
|
||||
"Modal Widget": "Modal Widget",
|
||||
"Data on this screen is shared with %(widgetDomain)s": "Data on this screen is shared with %(widgetDomain)s",
|
||||
"Doesn't look like a valid email address": "Doesn't look like a valid email address",
|
||||
"Continuing without email": "Continuing without email",
|
||||
"Just a heads up, if you don't add an email and forget your password, you could <b>permanently lose access to your account</b>.": "Just a heads up, if you don't add an email and forget your password, you could <b>permanently lose access to your account</b>.",
|
||||
"Email (optional)": "Email (optional)",
|
||||
|
@ -2735,6 +2739,9 @@
|
|||
"powered by Matrix": "powered by Matrix",
|
||||
"This homeserver would like to make sure you are not a robot.": "This homeserver would like to make sure you are not a robot.",
|
||||
"Country Dropdown": "Country Dropdown",
|
||||
"Email": "Email",
|
||||
"Enter email address": "Enter email address",
|
||||
"Doesn't look like a valid email address": "Doesn't look like a valid email address",
|
||||
"Confirm your identity by entering your account password below.": "Confirm your identity by entering your account password below.",
|
||||
"Password": "Password",
|
||||
"Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.": "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.",
|
||||
|
@ -2754,10 +2761,8 @@
|
|||
"Password is allowed, but unsafe": "Password is allowed, but unsafe",
|
||||
"Keep going...": "Keep going...",
|
||||
"Enter username": "Enter username",
|
||||
"Enter email address": "Enter email address",
|
||||
"Enter phone number": "Enter phone number",
|
||||
"That phone number doesn't look quite right, please check and try again": "That phone number doesn't look quite right, please check and try again",
|
||||
"Email": "Email",
|
||||
"Username": "Username",
|
||||
"Phone": "Phone",
|
||||
"Forgot password?": "Forgot password?",
|
||||
|
@ -2925,7 +2930,9 @@
|
|||
"Drop file here to upload": "Drop file here to upload",
|
||||
"You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.",
|
||||
"You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.",
|
||||
"Joining": "Joining",
|
||||
"You don't have permission": "You don't have permission",
|
||||
"Joined": "Joined",
|
||||
"This room is suggested as a good one to join": "This room is suggested as a good one to join",
|
||||
"Suggested": "Suggested",
|
||||
"Select a room below first": "Select a room below first",
|
||||
|
@ -2966,8 +2973,6 @@
|
|||
"A private space to organise your rooms": "A private space to organise your rooms",
|
||||
"Me and my teammates": "Me and my teammates",
|
||||
"A private space for you and your teammates": "A private space for you and your teammates",
|
||||
"Teammates might not be able to view or join any private rooms you make.": "Teammates might not be able to view or join any private rooms you make.",
|
||||
"We're working on this, but just want to let you know.": "We're working on this, but just want to let you know.",
|
||||
"Failed to invite the following users to your space: %(csvUsers)s": "Failed to invite the following users to your space: %(csvUsers)s",
|
||||
"Inviting...": "Inviting...",
|
||||
"Invite your teammates": "Invite your teammates",
|
||||
|
|
|
@ -220,3 +220,5 @@ export async function initSentry(sentryConfig: ISentryConfig): Promise<void> {
|
|||
tracesSampleRate: 1.0,
|
||||
});
|
||||
}
|
||||
|
||||
window.mxSendSentryReport = sendSentryReport;
|
||||
|
|
|
@ -307,7 +307,7 @@ class RoomViewStore extends Store<ActionPayload> {
|
|||
}
|
||||
}
|
||||
|
||||
private getInvitingUserId(roomId: string): string {
|
||||
private static getInvitingUserId(roomId: string): string {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(roomId);
|
||||
if (room && room.getMyMembership() === "invite") {
|
||||
|
@ -317,12 +317,7 @@ class RoomViewStore extends Store<ActionPayload> {
|
|||
}
|
||||
}
|
||||
|
||||
private joinRoomError(payload: ActionPayload) {
|
||||
this.setState({
|
||||
joining: false,
|
||||
joinError: payload.err,
|
||||
});
|
||||
const err = payload.err;
|
||||
public showJoinRoomError(err: Error | MatrixError, roomId: string) {
|
||||
let msg = err.message ? err.message : JSON.stringify(err);
|
||||
logger.log("Failed to join room:", msg);
|
||||
|
||||
|
@ -334,7 +329,7 @@ class RoomViewStore extends Store<ActionPayload> {
|
|||
{ _t("Please contact your homeserver administrator.") }
|
||||
</div>;
|
||||
} else if (err.httpStatus === 404) {
|
||||
const invitingUserId = this.getInvitingUserId(this.state.roomId);
|
||||
const invitingUserId = RoomViewStore.getInvitingUserId(roomId);
|
||||
// only provide a better error message for invites
|
||||
if (invitingUserId) {
|
||||
// if the inviting user is on the same HS, there can only be one cause: they left.
|
||||
|
@ -354,6 +349,14 @@ class RoomViewStore extends Store<ActionPayload> {
|
|||
});
|
||||
}
|
||||
|
||||
private joinRoomError(payload: ActionPayload) {
|
||||
this.setState({
|
||||
joining: false,
|
||||
joinError: payload.err,
|
||||
});
|
||||
this.showJoinRoomError(payload.err, this.state.roomId);
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.state = Object.assign({}, INITIAL_STATE);
|
||||
}
|
||||
|
|
|
@ -306,16 +306,23 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
return room?.currentState.getStateEvents(EventType.SpaceParent)
|
||||
.map(ev => {
|
||||
const content = ev.getContent();
|
||||
if (Array.isArray(content?.via) && (!canonicalOnly || content?.canonical)) {
|
||||
const parent = this.matrixClient.getRoom(ev.getStateKey());
|
||||
// only respect the relationship if the sender has sufficient permissions in the parent to set
|
||||
// child relations, as per MSC1772.
|
||||
// https://github.com/matrix-org/matrix-doc/blob/main/proposals/1772-groups-as-rooms.md#relationship-between-rooms-and-spaces
|
||||
if (parent?.currentState.maySendStateEvent(EventType.SpaceChild, userId)) {
|
||||
return parent;
|
||||
}
|
||||
if (!Array.isArray(content.via) || (canonicalOnly && !content.canonical)) {
|
||||
return; // skip
|
||||
}
|
||||
// else implicit undefined which causes this element to be filtered out
|
||||
|
||||
// only respect the relationship if the sender has sufficient permissions in the parent to set
|
||||
// child relations, as per MSC1772.
|
||||
// https://github.com/matrix-org/matrix-doc/blob/main/proposals/1772-groups-as-rooms.md#relationship-between-rooms-and-spaces
|
||||
const parent = this.matrixClient.getRoom(ev.getStateKey());
|
||||
const relation = parent?.currentState.getStateEvents(EventType.SpaceChild, roomId);
|
||||
if (!parent?.currentState.maySendStateEvent(EventType.SpaceChild, userId) ||
|
||||
// also skip this relation if the parent had this child added but then since removed it
|
||||
(relation && !Array.isArray(relation.getContent().via))
|
||||
) {
|
||||
return; // skip
|
||||
}
|
||||
|
||||
return parent;
|
||||
})
|
||||
.filter(Boolean) || [];
|
||||
}
|
||||
|
|