Merge remote-tracking branch 'origin/develop' into dbkr/new_look_callview
This commit is contained in:
commit
41e885dd67
70 changed files with 1738 additions and 1109 deletions
76
CHANGELOG.md
76
CHANGELOG.md
|
@ -1,3 +1,79 @@
|
||||||
|
Changes in [3.8.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.8.0) (2020-11-09)
|
||||||
|
===================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.8.0-rc.1...v3.8.0)
|
||||||
|
|
||||||
|
* Upgrade JS SDK to 9.1.0
|
||||||
|
|
||||||
|
Changes in [3.8.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.8.0-rc.1) (2020-11-04)
|
||||||
|
=============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.7.1...v3.8.0-rc.1)
|
||||||
|
|
||||||
|
* Upgrade JS SDK to 9.1.0-rc.1
|
||||||
|
* Log when saving profile
|
||||||
|
[\#5394](https://github.com/matrix-org/matrix-react-sdk/pull/5394)
|
||||||
|
* Translations update from Weblate
|
||||||
|
[\#5395](https://github.com/matrix-org/matrix-react-sdk/pull/5395)
|
||||||
|
* Hide prompt to add email for notifications if 3pid ui feature is off
|
||||||
|
[\#5392](https://github.com/matrix-org/matrix-react-sdk/pull/5392)
|
||||||
|
* Fix room list message preview copy for hangup events
|
||||||
|
[\#5388](https://github.com/matrix-org/matrix-react-sdk/pull/5388)
|
||||||
|
* Track UISIs as Countly Events
|
||||||
|
[\#5382](https://github.com/matrix-org/matrix-react-sdk/pull/5382)
|
||||||
|
* Don't let users accidentally redact ACL events
|
||||||
|
[\#5384](https://github.com/matrix-org/matrix-react-sdk/pull/5384)
|
||||||
|
* Two more easy files to remove from eslintignore
|
||||||
|
[\#5378](https://github.com/matrix-org/matrix-react-sdk/pull/5378)
|
||||||
|
* Fix Widget OpenID Permissions for realsies
|
||||||
|
[\#5381](https://github.com/matrix-org/matrix-react-sdk/pull/5381)
|
||||||
|
* Fix regression with OpenID permissions on widgets
|
||||||
|
[\#5380](https://github.com/matrix-org/matrix-react-sdk/pull/5380)
|
||||||
|
* Fix room directory events happening in the wrong order for Funnels
|
||||||
|
[\#5379](https://github.com/matrix-org/matrix-react-sdk/pull/5379)
|
||||||
|
* Remove a couple more files from eslintignore
|
||||||
|
[\#5377](https://github.com/matrix-org/matrix-react-sdk/pull/5377)
|
||||||
|
* Fix countly method bindings and errors
|
||||||
|
[\#5376](https://github.com/matrix-org/matrix-react-sdk/pull/5376)
|
||||||
|
* Fix a bunch of silly lint errors
|
||||||
|
[\#5375](https://github.com/matrix-org/matrix-react-sdk/pull/5375)
|
||||||
|
* Typescript: ImageUtils
|
||||||
|
[\#5374](https://github.com/matrix-org/matrix-react-sdk/pull/5374)
|
||||||
|
* Convert AuxPanel to TypeScript
|
||||||
|
[\#5373](https://github.com/matrix-org/matrix-react-sdk/pull/5373)
|
||||||
|
* Only pass metrics if they exist otherwise Countly will be unhappy!
|
||||||
|
[\#5372](https://github.com/matrix-org/matrix-react-sdk/pull/5372)
|
||||||
|
* Fix CountlyAnalytics NPE on MatrixClientPeg
|
||||||
|
[\#5370](https://github.com/matrix-org/matrix-react-sdk/pull/5370)
|
||||||
|
* fix CountlyAnalytics canEnable on wrong target
|
||||||
|
[\#5369](https://github.com/matrix-org/matrix-react-sdk/pull/5369)
|
||||||
|
* Initial Countly work
|
||||||
|
[\#5365](https://github.com/matrix-org/matrix-react-sdk/pull/5365)
|
||||||
|
* Fix videos not playing in non-encrypted rooms
|
||||||
|
[\#5368](https://github.com/matrix-org/matrix-react-sdk/pull/5368)
|
||||||
|
* Fix custom tag layout which regressed in #5309
|
||||||
|
[\#5367](https://github.com/matrix-org/matrix-react-sdk/pull/5367)
|
||||||
|
* Watch replyToEvent at RoomView to prevent races
|
||||||
|
[\#5360](https://github.com/matrix-org/matrix-react-sdk/pull/5360)
|
||||||
|
* Add a UI Feature flag for room history settings
|
||||||
|
[\#5362](https://github.com/matrix-org/matrix-react-sdk/pull/5362)
|
||||||
|
* Hide inline images when preference disabled
|
||||||
|
[\#5361](https://github.com/matrix-org/matrix-react-sdk/pull/5361)
|
||||||
|
* Fix React warning by moving handler to each button
|
||||||
|
[\#5359](https://github.com/matrix-org/matrix-react-sdk/pull/5359)
|
||||||
|
* Do not preload encrypted videos|images unless autoplay or thumbnailing is on
|
||||||
|
[\#5352](https://github.com/matrix-org/matrix-react-sdk/pull/5352)
|
||||||
|
* Fix theme variable passed to Jitsi
|
||||||
|
[\#5357](https://github.com/matrix-org/matrix-react-sdk/pull/5357)
|
||||||
|
* docs: added comment explanation
|
||||||
|
[\#5349](https://github.com/matrix-org/matrix-react-sdk/pull/5349)
|
||||||
|
* Modal Widgets - MSC2790
|
||||||
|
[\#5252](https://github.com/matrix-org/matrix-react-sdk/pull/5252)
|
||||||
|
* Widgets fixes
|
||||||
|
[\#5350](https://github.com/matrix-org/matrix-react-sdk/pull/5350)
|
||||||
|
* Fix User Menu avatar colouring being based on wrong string
|
||||||
|
[\#5348](https://github.com/matrix-org/matrix-react-sdk/pull/5348)
|
||||||
|
* Support 'answered elsewhere'
|
||||||
|
[\#5345](https://github.com/matrix-org/matrix-react-sdk/pull/5345)
|
||||||
|
|
||||||
Changes in [3.7.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.7.1) (2020-10-28)
|
Changes in [3.7.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.7.1) (2020-10-28)
|
||||||
===================================================================================================
|
===================================================================================================
|
||||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.7.0...v3.7.1)
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.7.0...v3.7.1)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "3.7.1",
|
"version": "3.8.0",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -79,7 +79,7 @@
|
||||||
"linkifyjs": "^2.1.9",
|
"linkifyjs": "^2.1.9",
|
||||||
"lodash": "^4.17.19",
|
"lodash": "^4.17.19",
|
||||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||||
"matrix-widget-api": "^0.1.0-beta.5",
|
"matrix-widget-api": "^0.1.0-beta.8",
|
||||||
"minimist": "^1.2.5",
|
"minimist": "^1.2.5",
|
||||||
"pako": "^1.0.11",
|
"pako": "^1.0.11",
|
||||||
"parse5": "^5.1.1",
|
"parse5": "^5.1.1",
|
||||||
|
|
|
@ -110,11 +110,11 @@
|
||||||
@import "./views/elements/_EventListSummary.scss";
|
@import "./views/elements/_EventListSummary.scss";
|
||||||
@import "./views/elements/_Field.scss";
|
@import "./views/elements/_Field.scss";
|
||||||
@import "./views/elements/_FormButton.scss";
|
@import "./views/elements/_FormButton.scss";
|
||||||
@import "./views/elements/_IconButton.scss";
|
|
||||||
@import "./views/elements/_ImageView.scss";
|
@import "./views/elements/_ImageView.scss";
|
||||||
@import "./views/elements/_InfoTooltip.scss";
|
@import "./views/elements/_InfoTooltip.scss";
|
||||||
@import "./views/elements/_InlineSpinner.scss";
|
@import "./views/elements/_InlineSpinner.scss";
|
||||||
@import "./views/elements/_ManageIntegsButton.scss";
|
@import "./views/elements/_ManageIntegsButton.scss";
|
||||||
|
@import "./views/elements/_MiniAvatarUploader.scss";
|
||||||
@import "./views/elements/_PowerSelector.scss";
|
@import "./views/elements/_PowerSelector.scss";
|
||||||
@import "./views/elements/_ProgressBar.scss";
|
@import "./views/elements/_ProgressBar.scss";
|
||||||
@import "./views/elements/_QRCode.scss";
|
@import "./views/elements/_QRCode.scss";
|
||||||
|
@ -139,6 +139,7 @@
|
||||||
@import "./views/groups/_GroupUserSettings.scss";
|
@import "./views/groups/_GroupUserSettings.scss";
|
||||||
@import "./views/messages/_CreateEvent.scss";
|
@import "./views/messages/_CreateEvent.scss";
|
||||||
@import "./views/messages/_DateSeparator.scss";
|
@import "./views/messages/_DateSeparator.scss";
|
||||||
|
@import "./views/messages/_EventTileBubble.scss";
|
||||||
@import "./views/messages/_MEmoteBody.scss";
|
@import "./views/messages/_MEmoteBody.scss";
|
||||||
@import "./views/messages/_MFileBody.scss";
|
@import "./views/messages/_MFileBody.scss";
|
||||||
@import "./views/messages/_MImageBody.scss";
|
@import "./views/messages/_MImageBody.scss";
|
||||||
|
@ -182,6 +183,7 @@
|
||||||
@import "./views/rooms/_MemberList.scss";
|
@import "./views/rooms/_MemberList.scss";
|
||||||
@import "./views/rooms/_MessageComposer.scss";
|
@import "./views/rooms/_MessageComposer.scss";
|
||||||
@import "./views/rooms/_MessageComposerFormatBar.scss";
|
@import "./views/rooms/_MessageComposerFormatBar.scss";
|
||||||
|
@import "./views/rooms/_NewRoomIntro.scss";
|
||||||
@import "./views/rooms/_NotificationBadge.scss";
|
@import "./views/rooms/_NotificationBadge.scss";
|
||||||
@import "./views/rooms/_PinnedEventTile.scss";
|
@import "./views/rooms/_PinnedEventTile.scss";
|
||||||
@import "./views/rooms/_PinnedEventsPanel.scss";
|
@import "./views/rooms/_PinnedEventsPanel.scss";
|
||||||
|
@ -225,6 +227,7 @@
|
||||||
@import "./views/settings/tabs/user/_SecurityUserSettingsTab.scss";
|
@import "./views/settings/tabs/user/_SecurityUserSettingsTab.scss";
|
||||||
@import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss";
|
@import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss";
|
||||||
@import "./views/terms/_InlineTermsAgreement.scss";
|
@import "./views/terms/_InlineTermsAgreement.scss";
|
||||||
|
@import "./views/toasts/_AnalyticsToast.scss";
|
||||||
@import "./views/toasts/_NonUrgentEchoFailureToast.scss";
|
@import "./views/toasts/_NonUrgentEchoFailureToast.scss";
|
||||||
@import "./views/verification/_VerificationShowSas.scss";
|
@import "./views/verification/_VerificationShowSas.scss";
|
||||||
@import "./views/voip/_CallContainer.scss";
|
@import "./views/voip/_CallContainer.scss";
|
||||||
|
|
|
@ -26,9 +26,10 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_HomePage_default {
|
.mx_HomePage_default {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
.mx_HomePage_default_wrapper {
|
.mx_HomePage_default_wrapper {
|
||||||
padding: 25vh 0 12px;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
|
@ -50,46 +51,12 @@ limitations under the License.
|
||||||
color: $muted-fg-color;
|
color: $muted-fg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_HomePage_userAvatar {
|
.mx_MiniAvatarUploader {
|
||||||
position: relative;
|
|
||||||
width: min-content;
|
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
||||||
&::before, &::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
height: 26px;
|
|
||||||
width: 26px;
|
|
||||||
|
|
||||||
right: -6px;
|
|
||||||
bottom: -6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
background-color: $primary-bg-color;
|
|
||||||
border-radius: 50%;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
background-color: $secondary-fg-color;
|
|
||||||
mask-position: center;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-image: url('$(res)/img/element-icons/camera.svg');
|
|
||||||
mask-size: 16px;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mx_HomePage_userAvatar_busy::after {
|
|
||||||
background: url("$(res)/img/spinner.gif") no-repeat center;
|
|
||||||
background-size: 80%;
|
|
||||||
mask: unset;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_HomePage_default_buttons {
|
.mx_HomePage_default_buttons {
|
||||||
margin: 80px auto 0;
|
margin: 60px auto 0;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
|
|
||||||
.mx_AccessibleButton {
|
.mx_AccessibleButton {
|
||||||
|
@ -97,7 +64,7 @@ limitations under the License.
|
||||||
|
|
||||||
width: 160px;
|
width: 160px;
|
||||||
height: 132px;
|
height: 132px;
|
||||||
margin: 0 20px;
|
margin: 20px;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
|
@ -153,16 +153,6 @@ limitations under the License.
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomStatusBar_isAlone {
|
|
||||||
height: 50px;
|
|
||||||
line-height: $font-50px;
|
|
||||||
|
|
||||||
color: $primary-fg-color;
|
|
||||||
opacity: 0.5;
|
|
||||||
overflow-y: hidden;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_MatrixChat_useCompactLayout {
|
.mx_MatrixChat_useCompactLayout {
|
||||||
.mx_RoomStatusBar {
|
.mx_RoomStatusBar {
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
|
|
|
@ -14,6 +14,35 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
.mx_InteractiveAuthEntryComponents_emailWrapper {
|
||||||
|
padding-right: 60px;
|
||||||
|
position: relative;
|
||||||
|
margin-top: 32px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
|
||||||
|
&::before, &::after {
|
||||||
|
position: absolute;
|
||||||
|
width: 116px;
|
||||||
|
height: 116px;
|
||||||
|
content: "";
|
||||||
|
right: -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: rgba(244, 246, 250, 0.91);
|
||||||
|
border-radius: 50%;
|
||||||
|
top: -20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
background-image: url('$(res)/img/element-icons/email-prompt.svg');
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
background-size: contain;
|
||||||
|
top: -25px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_InteractiveAuthEntryComponents_msisdnWrapper {
|
.mx_InteractiveAuthEntryComponents_msisdnWrapper {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,55 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.mx_IconButton {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border-radius: 100%;
|
|
||||||
background-color: $accent-bg-color;
|
|
||||||
// don't shrink or grow if in a flex container
|
|
||||||
flex: 0 0 auto;
|
|
||||||
|
|
||||||
&.mx_AccessibleButton_disabled {
|
|
||||||
background-color: none;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
background-color: lightgrey;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 90%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-position: center;
|
|
||||||
mask-size: 55%;
|
|
||||||
background-color: $accent-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mx_IconButton_icon_check::before {
|
|
||||||
mask-image: url('$(res)/img/feather-customised/check.svg');
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mx_IconButton_icon_edit::before {
|
|
||||||
mask-image: url('$(res)/img/feather-customised/edit.svg');
|
|
||||||
}
|
|
||||||
}
|
|
56
res/css/views/elements/_MiniAvatarUploader.scss
Normal file
56
res/css/views/elements/_MiniAvatarUploader.scss
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_MiniAvatarUploader {
|
||||||
|
position: relative;
|
||||||
|
width: min-content;
|
||||||
|
|
||||||
|
&::before, &::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
height: 26px;
|
||||||
|
width: 26px;
|
||||||
|
|
||||||
|
right: -6px;
|
||||||
|
bottom: -6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: $primary-bg-color;
|
||||||
|
border-radius: 50%;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
background-color: $secondary-fg-color;
|
||||||
|
mask-position: center;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-image: url('$(res)/img/element-icons/camera.svg');
|
||||||
|
mask-size: 16px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_MiniAvatarUploader_busy::after {
|
||||||
|
background: url("$(res)/img/spinner.gif") no-repeat center;
|
||||||
|
background-size: 80%;
|
||||||
|
mask: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MiniAvatarUploader_input {
|
||||||
|
display: none;
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2018 New Vector Ltd
|
Copyright 2018, 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,25 +15,8 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_CreateEvent {
|
.mx_CreateEvent {
|
||||||
background-color: $info-plinth-bg-color;
|
&::before {
|
||||||
padding-left: 20px;
|
background-color: $composer-e2e-icon-color;
|
||||||
padding-right: 20px;
|
mask-image: url('$(res)/img/element-icons/chat-bubbles.svg');
|
||||||
padding-top: 10px;
|
}
|
||||||
padding-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CreateEvent_image {
|
|
||||||
float: left;
|
|
||||||
margin-right: 20px;
|
|
||||||
width: 72px;
|
|
||||||
height: 34px;
|
|
||||||
|
|
||||||
background-color: $primary-fg-color;
|
|
||||||
mask: url('$(res)/img/room-continuation.svg');
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-position: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CreateEvent_header {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
|
|
60
res/css/views/messages/_EventTileBubble.scss
Normal file
60
res/css/views/messages/_EventTileBubble.scss
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_EventTileBubble {
|
||||||
|
background-color: $dark-panel-bg-color;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 10px auto;
|
||||||
|
max-width: 75%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 24px minmax(0, 1fr) min-content;
|
||||||
|
|
||||||
|
&::before, &::after {
|
||||||
|
position: relative;
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1 / 3;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
content: "";
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: contain;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTileBubble_title, .mx_EventTileBubble_subtitle {
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTileBubble_title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: $font-15px;
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTileBubble_subtitle {
|
||||||
|
font-size: $font-12px;
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 2;
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,41 +15,8 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_MJitsiWidgetEvent {
|
.mx_MJitsiWidgetEvent {
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 24px minmax(0, 1fr) min-content;
|
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
grid-column: 1;
|
|
||||||
grid-row: 1 / 3;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
content: "";
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-position: center;
|
|
||||||
mask-size: contain;
|
|
||||||
background-color: $composer-e2e-icon-color; // XXX: Variable abuse
|
background-color: $composer-e2e-icon-color; // XXX: Variable abuse
|
||||||
margin-top: 4px;
|
|
||||||
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
|
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MJitsiWidgetEvent_title {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: $font-15px;
|
|
||||||
grid-column: 2;
|
|
||||||
grid-row: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_MJitsiWidgetEvent_subtitle {
|
|
||||||
grid-column: 2;
|
|
||||||
grid-row: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_MJitsiWidgetEvent_title,
|
|
||||||
.mx_MJitsiWidgetEvent_subtitle {
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,28 +15,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_cryptoEvent {
|
.mx_cryptoEvent {
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 24px minmax(0, 1fr) min-content;
|
|
||||||
|
|
||||||
&.mx_cryptoEvent_icon::before,
|
|
||||||
&.mx_cryptoEvent_icon::after {
|
|
||||||
grid-column: 1;
|
|
||||||
grid-row: 1 / 3;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
content: "";
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-position: center;
|
|
||||||
mask-size: contain;
|
|
||||||
mask-image: url('$(res)/img/e2e/normal.svg');
|
|
||||||
background-color: $composer-e2e-icon-color;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// white infill for the transparency
|
// white infill for the transparency
|
||||||
&.mx_cryptoEvent_icon::before {
|
&.mx_cryptoEvent_icon::before {
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
|
@ -46,6 +24,11 @@ limitations under the License.
|
||||||
mask-size: 90%;
|
mask-size: 90%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.mx_cryptoEvent_icon::after {
|
||||||
|
mask-image: url('$(res)/img/e2e/normal.svg');
|
||||||
|
background-color: $composer-e2e-icon-color;
|
||||||
|
}
|
||||||
|
|
||||||
&.mx_cryptoEvent_icon_verified::after {
|
&.mx_cryptoEvent_icon_verified::after {
|
||||||
mask-image: url("$(res)/img/e2e/verified.svg");
|
mask-image: url("$(res)/img/e2e/verified.svg");
|
||||||
background-color: $accent-color;
|
background-color: $accent-color;
|
||||||
|
@ -56,25 +39,6 @@ limitations under the License.
|
||||||
background-color: $notice-primary-color;
|
background-color: $notice-primary-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_cryptoEvent_title, .mx_cryptoEvent_subtitle, .mx_cryptoEvent_state {
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_cryptoEvent_title {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: $font-15px;
|
|
||||||
grid-column: 2;
|
|
||||||
grid-row: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_cryptoEvent_subtitle {
|
|
||||||
grid-column: 2;
|
|
||||||
grid-row: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_cryptoEvent_state, .mx_cryptoEvent_subtitle {
|
|
||||||
font-size: $font-12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_cryptoEvent_state, .mx_cryptoEvent_buttons {
|
.mx_cryptoEvent_state, .mx_cryptoEvent_buttons {
|
||||||
grid-column: 3;
|
grid-column: 3;
|
||||||
|
@ -92,5 +56,7 @@ limitations under the License.
|
||||||
margin: auto 0;
|
margin: auto 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: $notice-secondary-color;
|
color: $notice-secondary-color;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
font-size: $font-12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -173,26 +173,12 @@ limitations under the License.
|
||||||
|
|
||||||
margin: 6px 0;
|
margin: 6px 0;
|
||||||
|
|
||||||
.mx_IconButton, .mx_Spinner {
|
|
||||||
margin-left: 20px;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
mask-size: 80%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_UserInfo_roleDescription {
|
.mx_UserInfo_roleDescription {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
// try to make it the same height as the dropdown
|
// try to make it the same height as the dropdown
|
||||||
margin: 11px 0 12px 0;
|
margin: 11px 0 12px 0;
|
||||||
|
|
||||||
.mx_IconButton {
|
|
||||||
margin-left: 6px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Field {
|
.mx_Field {
|
||||||
|
|
|
@ -25,15 +25,6 @@ $left-gutter: 64px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_bubble {
|
|
||||||
background-color: $dark-panel-bg-color;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
margin: 10px auto;
|
|
||||||
max-width: 75%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile.mx_EventTile_info {
|
.mx_EventTile.mx_EventTile_info {
|
||||||
padding-top: 0px;
|
padding-top: 0px;
|
||||||
}
|
}
|
||||||
|
@ -131,9 +122,10 @@ $left-gutter: 64px;
|
||||||
grid-template-columns: 1fr 100px;
|
grid-template-columns: 1fr 100px;
|
||||||
|
|
||||||
.mx_EventTile_line {
|
.mx_EventTile_line {
|
||||||
margin-right: 0px;
|
margin-right: 0;
|
||||||
grid-column: 1 / 3;
|
grid-column: 1 / 3;
|
||||||
padding: 0;
|
// override default padding of mx_EventTile_line so that we can be centered
|
||||||
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_msgOption {
|
.mx_EventTile_msgOption {
|
||||||
|
|
67
res/css/views/rooms/_NewRoomIntro.scss
Normal file
67
res/css/views/rooms/_NewRoomIntro.scss
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_NewRoomIntro {
|
||||||
|
margin: 40px 0 48px 64px;
|
||||||
|
|
||||||
|
.mx_MiniAvatarUploader_hasAvatar:not(.mx_MiniAvatarUploader_busy):not(:hover) {
|
||||||
|
&::before, &::after {
|
||||||
|
content: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton_kind_link {
|
||||||
|
padding: 0;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_NewRoomIntro_buttons {
|
||||||
|
margin-top: 28px;
|
||||||
|
|
||||||
|
.mx_AccessibleButton {
|
||||||
|
line-height: $font-24px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
background-color: $button-fg-color;
|
||||||
|
mask-position: center;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: 20px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin-right: 5px;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_NewRoomIntro_inviteButton::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/invite.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> h2 {
|
||||||
|
margin-top: 24px;
|
||||||
|
font-size: $font-24px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
> p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: $font-15px;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,7 +33,6 @@ limitations under the License.
|
||||||
|
|
||||||
div:first-child {
|
div:first-child {
|
||||||
font-weight: $font-semi-bold;
|
font-weight: $font-semi-bold;
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AccessibleButton {
|
.mx_AccessibleButton {
|
||||||
|
@ -41,6 +40,7 @@ limitations under the License.
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 0 0 0 24px;
|
padding: 0 0 0 24px;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
|
@ -53,6 +53,13 @@ limitations under the License.
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
mask-size: contain;
|
mask-size: contain;
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_RoomList_explorePrompt_startChat::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/feedback.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_RoomList_explorePrompt_explore::before {
|
||||||
mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
|
mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
27
res/css/views/toasts/_AnalyticsToast.scss
Normal file
27
res/css/views/toasts/_AnalyticsToast.scss
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_AnalyticsToast {
|
||||||
|
.mx_AccessibleButton_kind_danger {
|
||||||
|
background: none;
|
||||||
|
color: $accent-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton_kind_primary {
|
||||||
|
background: $accent-color;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
11
res/img/element-icons/chat-bubbles.svg
Normal file
11
res/img/element-icons/chat-bubbles.svg
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0)">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.90964 11.5297C9.09231 11.5297 11.6724 8.94865 11.6724 5.76483C11.6724 2.581 9.09231 0 5.90964 0C2.72697 0 0.146904 2.581 0.146904 5.76483C0.146904 6.65678 0.3494 7.50142 0.710912 8.25525L0.0648767 10.3556C-0.171716 11.1248 0.550948 11.8442 1.31906 11.6041L3.39724 10.9544C4.15657 11.323 5.00898 11.5297 5.90964 11.5297Z" fill="black"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.03851 12.8449C5.70399 13.1151 6.4314 13.2638 7.19345 13.2638C10.3676 13.2638 13.5 10.6832 13.5 7.49979C13.5 6.63255 13.2676 5.81005 12.8651 5.07227C14.6487 6.05071 15.8583 7.94999 15.8583 10.1326C15.8583 11.0243 15.6564 11.8688 15.2959 12.6224L15.9404 14.7232C16.1765 15.4926 15.4533 16.2114 14.6854 15.9708L12.6155 15.322C11.8585 15.6902 11.0088 15.8966 10.111 15.8966C7.91459 15.8966 6.00594 14.661 5.03851 12.8449Z" fill="black"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0">
|
||||||
|
<rect width="16" height="16" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
13
res/img/element-icons/email-prompt.svg
Normal file
13
res/img/element-icons/email-prompt.svg
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<svg width="57" height="77" viewBox="0 0 57 77" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.55298 38.9352H4C1.79086 38.9352 0 40.726 0 42.9352V72.0304C0 74.2396 1.79086 76.0304 4 76.0304H53C55.2091 76.0304 57 74.2396 57 72.0304V42.9352C57 40.726 55.2091 38.9352 53 38.9352H51.365V41.6473H5.55298V38.9352ZM26.9753 61.3068L3.10141 43.4482C2.33137 42.8721 2.73876 41.6474 3.70041 41.6474H28.459H53.3841C54.3282 41.6474 54.7464 42.8352 54.0107 43.4268L31.8776 61.2212C30.4545 62.3653 28.4374 62.4005 26.9753 61.3068Z" fill="#8A8C8E"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M49.5885 33.0898C48.9384 33.2156 48.2703 33.2911 47.5885 33.3119V34.706V44.4238V54.1415H49.5885V44.4238V34.706V33.0898ZM36.5604 14.2706H13.7177C10.9562 14.2706 8.71765 16.5092 8.71765 19.2706V34.706V44.4238V54.1415H10.7177V44.4238V34.706V19.2706C10.7177 17.6138 12.0608 16.2706 13.7177 16.2706H35.5616C35.8354 15.571 36.1706 14.9022 36.5604 14.2706Z" fill="#8A8C8E"/>
|
||||||
|
<path d="M16.6589 30.5414H37.4826" stroke="#8A8C8E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<line x1="16.2706" y1="37.8708" x2="40.6473" y2="37.8708" stroke="#8A8C8E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<line x1="16.2706" y1="44.812" x2="40.6473" y2="44.812" stroke="#8A8C8E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<circle cx="47.2003" cy="20.8237" r="9.71771" fill="#FE2928"/>
|
||||||
|
<rect x="45.812" y="14.5765" width="2.77649" height="8.32946" rx="1" fill="white"/>
|
||||||
|
<rect x="45.812" y="24.2943" width="2.77649" height="2.77649" rx="1" fill="white"/>
|
||||||
|
<line x1="27.3766" y1="1" x2="27.3766" y2="10.106" stroke="#8A8C8E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<line x1="34.3179" y1="6.55298" x2="34.3179" y2="10.106" stroke="#8A8C8E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<line x1="20.4354" y1="6.55298" x2="20.4354" y2="10.106" stroke="#8A8C8E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2 KiB |
|
@ -1,6 +0,0 @@
|
||||||
<svg width="72" height="34" viewBox="0 0 72 34" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M1 7.26087V1H28.7889V7.26087M1 7.26087V33H28.7889V7.26087M1 7.26087H28.7889M4.16583 4.13043H16.8291" stroke="#454545" stroke-width="2" stroke-linejoin="round"/>
|
|
||||||
<path d="M43.2109 7.26087V1H70.9999V7.26087M43.2109 7.26087V33H70.9999V7.26087M43.2109 7.26087H70.9999M46.3768 4.13043H59.0401" stroke="#454545" stroke-width="2" stroke-linejoin="round"/>
|
|
||||||
<path d="M27.03 28.8262C34.2226 28.8262 36.0207 26.343 36.0207 25.1014V16.0996C36.0207 12.1264 43.6283 11.3401 47.432 11.4436" stroke="black" stroke-width="2"/>
|
|
||||||
</svg>
|
|
||||||
|
|
Before Width: | Height: | Size: 623 B |
|
@ -40,11 +40,11 @@ export function inviteMultipleToRoom(roomId, addrs) {
|
||||||
return inviter.invite(addrs).then(states => Promise.resolve({states, inviter}));
|
return inviter.invite(addrs).then(states => Promise.resolve({states, inviter}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showStartChatInviteDialog() {
|
export function showStartChatInviteDialog(initialText) {
|
||||||
// This dialog handles the room creation internally - we don't need to worry about it.
|
// This dialog handles the room creation internally - we don't need to worry about it.
|
||||||
const InviteDialog = sdk.getComponent("dialogs.InviteDialog");
|
const InviteDialog = sdk.getComponent("dialogs.InviteDialog");
|
||||||
Modal.createTrackedDialog(
|
Modal.createTrackedDialog(
|
||||||
'Start DM', '', InviteDialog, {kind: KIND_DM},
|
'Start DM', '', InviteDialog, {kind: KIND_DM, initialText},
|
||||||
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
|
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,8 +50,8 @@ class Skinner {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// components have to be functions.
|
// components have to be functions or forwardRef objects with a render function.
|
||||||
const validType = typeof comp === 'function';
|
const validType = typeof comp === 'function' || comp.render;
|
||||||
if (!validType) {
|
if (!validType) {
|
||||||
throw new Error(`Not a valid component: ${name} (type = ${typeof(comp)}).`);
|
throw new Error(`Not a valid component: ${name} (type = ${typeof(comp)}).`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1002,14 +1002,29 @@ export const Commands = [
|
||||||
description: _td("Opens chat with the given user"),
|
description: _td("Opens chat with the given user"),
|
||||||
args: "<user-id>",
|
args: "<user-id>",
|
||||||
runFn: function(roomId, userId) {
|
runFn: function(roomId, userId) {
|
||||||
if (!userId || !userId.startsWith("@") || !userId.includes(":")) {
|
// easter-egg for now: look up phone numbers through the thirdparty API
|
||||||
|
// (very dumb phone number detection...)
|
||||||
|
const isPhoneNumber = userId && /^\+?[0123456789]+$/.test(userId);
|
||||||
|
if (!userId || (!userId.startsWith("@") || !userId.includes(":")) && !isPhoneNumber) {
|
||||||
return reject(this.getUsage());
|
return reject(this.getUsage());
|
||||||
}
|
}
|
||||||
|
|
||||||
return success((async () => {
|
return success((async () => {
|
||||||
|
if (isPhoneNumber) {
|
||||||
|
const results = await MatrixClientPeg.get().getThirdpartyUser('im.vector.protocol.pstn', {
|
||||||
|
'm.id.phone': userId,
|
||||||
|
});
|
||||||
|
if (!results || results.length === 0 || !results[0].userid) {
|
||||||
|
throw new Error("Unable to find Matrix ID for phone number");
|
||||||
|
}
|
||||||
|
userId = results[0].userid;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roomId = await ensureDMExists(MatrixClientPeg.get(), userId);
|
||||||
|
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
room_id: await ensureDMExists(MatrixClientPeg.get(), userId),
|
room_id: roomId,
|
||||||
});
|
});
|
||||||
})());
|
})());
|
||||||
},
|
},
|
||||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {useContext, useRef, useState} from "react";
|
import {useContext, useState} from "react";
|
||||||
|
|
||||||
import AutoHideScrollbar from './AutoHideScrollbar';
|
import AutoHideScrollbar from './AutoHideScrollbar';
|
||||||
import {getHomePageUrl} from "../../utils/pages";
|
import {getHomePageUrl} from "../../utils/pages";
|
||||||
|
@ -24,16 +24,13 @@ import SdkConfig from "../../SdkConfig";
|
||||||
import * as sdk from "../../index";
|
import * as sdk from "../../index";
|
||||||
import dis from "../../dispatcher/dispatcher";
|
import dis from "../../dispatcher/dispatcher";
|
||||||
import {Action} from "../../dispatcher/actions";
|
import {Action} from "../../dispatcher/actions";
|
||||||
import {Transition} from "react-transition-group";
|
|
||||||
import BaseAvatar from "../views/avatars/BaseAvatar";
|
import BaseAvatar from "../views/avatars/BaseAvatar";
|
||||||
import {OwnProfileStore} from "../../stores/OwnProfileStore";
|
import {OwnProfileStore} from "../../stores/OwnProfileStore";
|
||||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||||
import Tooltip from "../views/elements/Tooltip";
|
|
||||||
import {UPDATE_EVENT} from "../../stores/AsyncStore";
|
import {UPDATE_EVENT} from "../../stores/AsyncStore";
|
||||||
import {useEventEmitter} from "../../hooks/useEventEmitter";
|
import {useEventEmitter} from "../../hooks/useEventEmitter";
|
||||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
import classNames from "classnames";
|
import MiniAvatarUploader, {AVATAR_SIZE} from "../views/elements/MiniAvatarUploader";
|
||||||
import {ENTERING} from "react-transition-group/Transition";
|
|
||||||
|
|
||||||
const onClickSendDm = () => dis.dispatch({action: 'view_create_chat'});
|
const onClickSendDm = () => dis.dispatch({action: 'view_create_chat'});
|
||||||
const onClickExplore = () => dis.fire(Action.ViewRoomDirectory);
|
const onClickExplore = () => dis.fire(Action.ViewRoomDirectory);
|
||||||
|
@ -43,11 +40,9 @@ interface IProps {
|
||||||
justRegistered?: boolean;
|
justRegistered?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const avatarSize = 52;
|
|
||||||
|
|
||||||
const getOwnProfile = (userId: string) => ({
|
const getOwnProfile = (userId: string) => ({
|
||||||
displayName: OwnProfileStore.instance.displayName || userId,
|
displayName: OwnProfileStore.instance.displayName || userId,
|
||||||
avatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(avatarSize),
|
avatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(AVATAR_SIZE),
|
||||||
});
|
});
|
||||||
|
|
||||||
const UserWelcomeTop = () => {
|
const UserWelcomeTop = () => {
|
||||||
|
@ -57,56 +52,23 @@ const UserWelcomeTop = () => {
|
||||||
useEventEmitter(OwnProfileStore.instance, UPDATE_EVENT, () => {
|
useEventEmitter(OwnProfileStore.instance, UPDATE_EVENT, () => {
|
||||||
setOwnProfile(getOwnProfile(userId));
|
setOwnProfile(getOwnProfile(userId));
|
||||||
});
|
});
|
||||||
const [busy, setBusy] = useState(false);
|
|
||||||
|
|
||||||
const uploadRef = useRef<HTMLInputElement>();
|
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
<input
|
<MiniAvatarUploader
|
||||||
type="file"
|
hasAvatar={!!ownProfile.avatarUrl}
|
||||||
ref={uploadRef}
|
hasAvatarLabel={_t("Great, that'll help people know it's you")}
|
||||||
className="mx_ProfileSettings_avatarUpload"
|
noAvatarLabel={_t("Add a photo so people know it's you.")}
|
||||||
onChange={async (ev) => {
|
setAvatarUrl={url => cli.setAvatarUrl(url)}
|
||||||
if (!ev.target.files?.length) return;
|
|
||||||
setBusy(true);
|
|
||||||
const file = ev.target.files[0];
|
|
||||||
const uri = await cli.uploadContent(file);
|
|
||||||
await cli.setAvatarUrl(uri);
|
|
||||||
setBusy(false);
|
|
||||||
}}
|
|
||||||
accept="image/*"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<AccessibleButton
|
|
||||||
className={classNames("mx_HomePage_userAvatar", {
|
|
||||||
mx_HomePage_userAvatar_busy: busy,
|
|
||||||
})}
|
|
||||||
disabled={busy}
|
|
||||||
onClick={() => {
|
|
||||||
uploadRef.current.click();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<BaseAvatar
|
<BaseAvatar
|
||||||
idName={userId}
|
idName={userId}
|
||||||
name={ownProfile.displayName}
|
name={ownProfile.displayName}
|
||||||
url={ownProfile.avatarUrl}
|
url={ownProfile.avatarUrl}
|
||||||
width={avatarSize}
|
width={AVATAR_SIZE}
|
||||||
height={avatarSize}
|
height={AVATAR_SIZE}
|
||||||
resizeMethod="crop"
|
resizeMethod="crop"
|
||||||
/>
|
/>
|
||||||
|
</MiniAvatarUploader>
|
||||||
<Transition appear in timeout={3000}>
|
|
||||||
{state => (
|
|
||||||
<Tooltip
|
|
||||||
label={ownProfile.avatarUrl || busy
|
|
||||||
? _t("Great, that'll help people know it's you")
|
|
||||||
: _t("Add a photo so people know it's you.")}
|
|
||||||
visible={state !== ENTERING}
|
|
||||||
forceOnRight
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Transition>
|
|
||||||
</AccessibleButton>
|
|
||||||
|
|
||||||
<h1>{ _t("Welcome %(name)s", { name: ownProfile.displayName }) }</h1>
|
<h1>{ _t("Welcome %(name)s", { name: ownProfile.displayName }) }</h1>
|
||||||
<h4>{ _t("Now, let's help you get started") }</h4>
|
<h4>{ _t("Now, let's help you get started") }</h4>
|
||||||
|
|
|
@ -653,8 +653,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
case Action.ViewRoomDirectory: {
|
case Action.ViewRoomDirectory: {
|
||||||
const RoomDirectory = sdk.getComponent("structures.RoomDirectory");
|
const RoomDirectory = sdk.getComponent("structures.RoomDirectory");
|
||||||
Modal.createTrackedDialog('Room directory', '', RoomDirectory, {},
|
Modal.createTrackedDialog('Room directory', '', RoomDirectory, {
|
||||||
'mx_RoomDirectory_dialogWrapper', false, true);
|
initialText: payload.initialText,
|
||||||
|
}, 'mx_RoomDirectory_dialogWrapper', false, true);
|
||||||
|
|
||||||
// View the welcome or home page if we need something to look at
|
// View the welcome or home page if we need something to look at
|
||||||
this.viewSomethingBehindModal();
|
this.viewSomethingBehindModal();
|
||||||
|
@ -677,7 +678,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
this.chatCreateOrReuse(payload.user_id);
|
this.chatCreateOrReuse(payload.user_id);
|
||||||
break;
|
break;
|
||||||
case 'view_create_chat':
|
case 'view_create_chat':
|
||||||
showStartChatInviteDialog();
|
showStartChatInviteDialog(payload.initialText || "");
|
||||||
break;
|
break;
|
||||||
case 'view_invite':
|
case 'view_invite':
|
||||||
showRoomInviteDialog(payload.roomId);
|
showRoomInviteDialog(payload.roomId);
|
||||||
|
|
|
@ -30,6 +30,8 @@ import {_t} from "../../languageHandler";
|
||||||
import {haveTileForEvent} from "../views/rooms/EventTile";
|
import {haveTileForEvent} from "../views/rooms/EventTile";
|
||||||
import {textForEvent} from "../../TextForEvent";
|
import {textForEvent} from "../../TextForEvent";
|
||||||
import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer";
|
import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer";
|
||||||
|
import DMRoomMap from "../../utils/DMRoomMap";
|
||||||
|
import NewRoomIntro from "../views/rooms/NewRoomIntro";
|
||||||
|
|
||||||
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||||
const continuedTypes = ['m.sticker', 'm.room.message'];
|
const continuedTypes = ['m.sticker', 'm.room.message'];
|
||||||
|
@ -952,15 +954,25 @@ class CreationGrouper {
|
||||||
}).reduce((a, b) => a.concat(b), []);
|
}).reduce((a, b) => a.concat(b), []);
|
||||||
// Get sender profile from the latest event in the summary as the m.room.create doesn't contain one
|
// Get sender profile from the latest event in the summary as the m.room.create doesn't contain one
|
||||||
const ev = this.events[this.events.length - 1];
|
const ev = this.events[this.events.length - 1];
|
||||||
|
|
||||||
|
let summaryText;
|
||||||
|
const roomId = ev.getRoomId();
|
||||||
|
const creator = ev.sender ? ev.sender.name : ev.getSender();
|
||||||
|
if (DMRoomMap.shared().getUserIdForRoomId(roomId)) {
|
||||||
|
summaryText = _t("%(creator)s created this DM.", { creator });
|
||||||
|
} else {
|
||||||
|
summaryText = _t("%(creator)s created and configured the room.", { creator });
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.push(<NewRoomIntro key="newroomintro" />);
|
||||||
|
|
||||||
ret.push(
|
ret.push(
|
||||||
<EventListSummary
|
<EventListSummary
|
||||||
key="roomcreationsummary"
|
key="roomcreationsummary"
|
||||||
events={this.events}
|
events={this.events}
|
||||||
onToggle={panel._onHeightChanged} // Update scroll state
|
onToggle={panel._onHeightChanged} // Update scroll state
|
||||||
summaryMembers={[ev.sender]}
|
summaryMembers={[ev.sender]}
|
||||||
summaryText={_t("%(creator)s created and configured the room.", {
|
summaryText={summaryText}
|
||||||
creator: ev.sender ? ev.sender.name : ev.getSender(),
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
{ eventTiles }
|
{ eventTiles }
|
||||||
</EventListSummary>,
|
</EventListSummary>,
|
||||||
|
|
|
@ -44,6 +44,7 @@ function track(action) {
|
||||||
|
|
||||||
export default class RoomDirectory extends React.Component {
|
export default class RoomDirectory extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
initialText: PropTypes.string,
|
||||||
onFinished: PropTypes.func.isRequired,
|
onFinished: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -61,7 +62,7 @@ export default class RoomDirectory extends React.Component {
|
||||||
error: null,
|
error: null,
|
||||||
instanceId: undefined,
|
instanceId: undefined,
|
||||||
roomServer: MatrixClientPeg.getHomeserverName(),
|
roomServer: MatrixClientPeg.getHomeserverName(),
|
||||||
filterString: null,
|
filterString: this.props.initialText || "",
|
||||||
selectedCommunityId: SettingsStore.getValue("feature_communities_v2_prototypes")
|
selectedCommunityId: SettingsStore.getValue("feature_communities_v2_prototypes")
|
||||||
? selectedCommunityId
|
? selectedCommunityId
|
||||||
: null,
|
: null,
|
||||||
|
@ -686,6 +687,7 @@ export default class RoomDirectory extends React.Component {
|
||||||
onJoinClick={this.onJoinFromSearchClick}
|
onJoinClick={this.onJoinFromSearchClick}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
showJoinButton={showJoinButton}
|
showJoinButton={showJoinButton}
|
||||||
|
initialText={this.props.initialText}
|
||||||
/>
|
/>
|
||||||
{dropdown}
|
{dropdown}
|
||||||
</div>;
|
</div>;
|
||||||
|
|
|
@ -148,7 +148,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
||||||
onBlur={this.onBlur}
|
onBlur={this.onBlur}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
placeholder={_t("Search")}
|
placeholder={_t("Filter")}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -164,7 +164,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
||||||
if (this.props.isMinimized) {
|
if (this.props.isMinimized) {
|
||||||
icon = (
|
icon = (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
title={_t("Search rooms")}
|
title={_t("Filter rooms and people")}
|
||||||
className="mx_RoomSearch_icon mx_RoomSearch_minimizedHandle"
|
className="mx_RoomSearch_icon mx_RoomSearch_minimizedHandle"
|
||||||
onClick={this.openSearch}
|
onClick={this.openSearch}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -41,9 +41,6 @@ export default class RoomStatusBar extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
// the room this statusbar is representing.
|
// the room this statusbar is representing.
|
||||||
room: PropTypes.object.isRequired,
|
room: PropTypes.object.isRequired,
|
||||||
// This is true when the user is alone in the room, but has also sent a message.
|
|
||||||
// Used to suggest to the user to invite someone
|
|
||||||
sentMessageAndIsAlone: PropTypes.bool,
|
|
||||||
|
|
||||||
// The active call in the room, if any (means we show the call bar
|
// The active call in the room, if any (means we show the call bar
|
||||||
// along with the status of the call)
|
// along with the status of the call)
|
||||||
|
@ -68,10 +65,6 @@ export default class RoomStatusBar extends React.Component {
|
||||||
// 'you are alone' bar
|
// 'you are alone' bar
|
||||||
onInviteClick: PropTypes.func,
|
onInviteClick: PropTypes.func,
|
||||||
|
|
||||||
// callback for when the user clicks on the 'stop warning me' button in the
|
|
||||||
// 'you are alone' bar
|
|
||||||
onStopWarningClick: PropTypes.func,
|
|
||||||
|
|
||||||
// callback for when we do something that changes the size of the
|
// callback for when we do something that changes the size of the
|
||||||
// status bar. This is used to trigger a re-layout in the parent
|
// status bar. This is used to trigger a re-layout in the parent
|
||||||
// component.
|
// component.
|
||||||
|
@ -159,10 +152,7 @@ export default class RoomStatusBar extends React.Component {
|
||||||
// changed - so we use '0' to indicate normal size, and other values to
|
// changed - so we use '0' to indicate normal size, and other values to
|
||||||
// indicate other sizes.
|
// indicate other sizes.
|
||||||
_getSize() {
|
_getSize() {
|
||||||
if (this._shouldShowConnectionError() ||
|
if (this._shouldShowConnectionError() || this._showCallBar()) {
|
||||||
this._showCallBar() ||
|
|
||||||
this.props.sentMessageAndIsAlone
|
|
||||||
) {
|
|
||||||
return STATUS_BAR_EXPANDED;
|
return STATUS_BAR_EXPANDED;
|
||||||
} else if (this.state.unsentMessages.length > 0) {
|
} else if (this.state.unsentMessages.length > 0) {
|
||||||
return STATUS_BAR_EXPANDED_LARGE;
|
return STATUS_BAR_EXPANDED_LARGE;
|
||||||
|
@ -325,24 +315,6 @@ export default class RoomStatusBar extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If you're alone in the room, and have sent a message, suggest to invite someone
|
|
||||||
if (this.props.sentMessageAndIsAlone && !this.props.isPeeking) {
|
|
||||||
return (
|
|
||||||
<div className="mx_RoomStatusBar_isAlone">
|
|
||||||
{ _t("There's no one else here! Would you like to <inviteText>invite others</inviteText> " +
|
|
||||||
"or <nowarnText>stop warning about the empty room</nowarnText>?",
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
'inviteText': (sub) =>
|
|
||||||
<a className="mx_RoomStatusBar_resend_link" key="invite" onClick={this.props.onInviteClick}>{ sub }</a>,
|
|
||||||
'nowarnText': (sub) =>
|
|
||||||
<a className="mx_RoomStatusBar_resend_link" key="nowarn" onClick={this.props.onStopWarningClick}>{ sub }</a>,
|
|
||||||
},
|
|
||||||
) }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -150,7 +150,6 @@ export interface IState {
|
||||||
guestsCanJoin: boolean;
|
guestsCanJoin: boolean;
|
||||||
canPeek: boolean;
|
canPeek: boolean;
|
||||||
showApps: boolean;
|
showApps: boolean;
|
||||||
isAlone: boolean;
|
|
||||||
isPeeking: boolean;
|
isPeeking: boolean;
|
||||||
showingPinned: boolean;
|
showingPinned: boolean;
|
||||||
showReadReceipts: boolean;
|
showReadReceipts: boolean;
|
||||||
|
@ -223,7 +222,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
guestsCanJoin: false,
|
guestsCanJoin: false,
|
||||||
canPeek: false,
|
canPeek: false,
|
||||||
showApps: false,
|
showApps: false,
|
||||||
isAlone: false,
|
|
||||||
isPeeking: false,
|
isPeeking: false,
|
||||||
showingPinned: false,
|
showingPinned: false,
|
||||||
showReadReceipts: true,
|
showReadReceipts: true,
|
||||||
|
@ -705,9 +703,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
private onAction = payload => {
|
private onAction = payload => {
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'message_send_failed':
|
|
||||||
case 'message_sent':
|
case 'message_sent':
|
||||||
this.checkIfAlone(this.state.room);
|
this.checkDesktopNotifications();
|
||||||
break;
|
break;
|
||||||
case 'post_sticker_message':
|
case 'post_sticker_message':
|
||||||
this.injectSticker(
|
this.injectSticker(
|
||||||
|
@ -1025,36 +1022,15 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// rate limited because a power level change will emit an event for every member in the room.
|
// rate limited because a power level change will emit an event for every member in the room.
|
||||||
private updateRoomMembers = rateLimitedFunc((dueToMember) => {
|
private updateRoomMembers = rateLimitedFunc(() => {
|
||||||
this.updateDMState();
|
this.updateDMState();
|
||||||
|
|
||||||
let memberCountInfluence = 0;
|
|
||||||
if (dueToMember && dueToMember.membership === "invite" && this.state.room.getInvitedMemberCount() === 0) {
|
|
||||||
// A member got invited, but the room hasn't detected that change yet. Influence the member
|
|
||||||
// count by 1 to counteract this.
|
|
||||||
memberCountInfluence = 1;
|
|
||||||
}
|
|
||||||
this.checkIfAlone(this.state.room, memberCountInfluence);
|
|
||||||
|
|
||||||
this.updateE2EStatus(this.state.room);
|
this.updateE2EStatus(this.state.room);
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
private checkIfAlone(room: Room, countInfluence?: number) {
|
private checkDesktopNotifications() {
|
||||||
let warnedAboutLonelyRoom = false;
|
const memberCount = this.state.room.getJoinedMemberCount() + this.state.room.getInvitedMemberCount();
|
||||||
if (localStorage) {
|
// if they are not alone prompt the user about notifications so they don't miss replies
|
||||||
warnedAboutLonelyRoom = Boolean(localStorage.getItem('mx_user_alone_warned_' + this.state.room.roomId));
|
if (memberCount > 1 && Notifier.shouldShowPrompt()) {
|
||||||
}
|
|
||||||
if (warnedAboutLonelyRoom) {
|
|
||||||
if (this.state.isAlone) this.setState({isAlone: false});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let joinedOrInvitedMemberCount = room.getJoinedMemberCount() + room.getInvitedMemberCount();
|
|
||||||
if (countInfluence) joinedOrInvitedMemberCount += countInfluence;
|
|
||||||
this.setState({isAlone: joinedOrInvitedMemberCount === 1});
|
|
||||||
|
|
||||||
// if they are not alone additionally prompt the user about notifications so they don't miss replies
|
|
||||||
if (joinedOrInvitedMemberCount > 1 && Notifier.shouldShowPrompt()) {
|
|
||||||
showNotificationsToast(true);
|
showNotificationsToast(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1091,14 +1067,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
action: 'view_invite',
|
action: 'view_invite',
|
||||||
roomId: this.state.room.roomId,
|
roomId: this.state.room.roomId,
|
||||||
});
|
});
|
||||||
this.setState({isAlone: false}); // there's a good chance they'll invite someone
|
|
||||||
};
|
|
||||||
|
|
||||||
private onStopAloneWarningClick = () => {
|
|
||||||
if (localStorage) {
|
|
||||||
localStorage.setItem('mx_user_alone_warned_' + this.state.room.roomId, String(true));
|
|
||||||
}
|
|
||||||
this.setState({isAlone: false});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private onJoinButtonClicked = () => {
|
private onJoinButtonClicked = () => {
|
||||||
|
@ -1147,16 +1115,9 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
ev.dataTransfer.dropEffect = 'none';
|
ev.dataTransfer.dropEffect = 'none';
|
||||||
|
|
||||||
const items = [...ev.dataTransfer.items];
|
if (ev.dataTransfer.types.includes("Files") || ev.dataTransfer.types.includes("application/x-moz-file")) {
|
||||||
if (items.length >= 1) {
|
this.setState({ draggingFile: true });
|
||||||
const isDraggingFiles = items.every(function(item) {
|
ev.dataTransfer.dropEffect = 'copy';
|
||||||
return item.kind == 'file';
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isDraggingFiles) {
|
|
||||||
this.setState({ draggingFile: true });
|
|
||||||
ev.dataTransfer.dropEffect = 'copy';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1797,12 +1758,10 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
isStatusAreaExpanded = this.state.statusBarVisible;
|
isStatusAreaExpanded = this.state.statusBarVisible;
|
||||||
statusBar = <RoomStatusBar
|
statusBar = <RoomStatusBar
|
||||||
room={this.state.room}
|
room={this.state.room}
|
||||||
sentMessageAndIsAlone={this.state.isAlone}
|
|
||||||
callState={this.state.callState}
|
callState={this.state.callState}
|
||||||
callType={activeCall ? activeCall.type : null}
|
callType={activeCall ? activeCall.type : null}
|
||||||
isPeeking={myMembership !== "join"}
|
isPeeking={myMembership !== "join"}
|
||||||
onInviteClick={this.onInviteButtonClick}
|
onInviteClick={this.onInviteButtonClick}
|
||||||
onStopWarningClick={this.onStopAloneWarningClick}
|
|
||||||
onVisible={this.onStatusBarVisible}
|
onVisible={this.onStatusBarVisible}
|
||||||
onHidden={this.onStatusBarHidden}
|
onHidden={this.onStatusBarHidden}
|
||||||
/>;
|
/>;
|
||||||
|
|
|
@ -55,11 +55,11 @@ export default class ToastContainer extends React.Component<{}, IState> {
|
||||||
let toast;
|
let toast;
|
||||||
if (totalCount !== 0) {
|
if (totalCount !== 0) {
|
||||||
const topToast = this.state.toasts[0];
|
const topToast = this.state.toasts[0];
|
||||||
const {title, icon, key, component, props} = topToast;
|
const {title, icon, key, component, className, props} = topToast;
|
||||||
const toastClasses = classNames("mx_Toast_toast", {
|
const toastClasses = classNames("mx_Toast_toast", {
|
||||||
"mx_Toast_hasIcon": icon,
|
"mx_Toast_hasIcon": icon,
|
||||||
[`mx_Toast_icon_${icon}`]: icon,
|
[`mx_Toast_icon_${icon}`]: icon,
|
||||||
});
|
}, className);
|
||||||
|
|
||||||
let countIndicator;
|
let countIndicator;
|
||||||
if (isStacked || this.state.countSeen > 0) {
|
if (isStacked || this.state.countSeen > 0) {
|
||||||
|
|
|
@ -190,11 +190,18 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
this.setState({contextMenuPosition: null}); // also close the menu
|
this.setState({contextMenuPosition: null}); // also close the menu
|
||||||
};
|
};
|
||||||
|
|
||||||
private onSignOutClick = (ev: ButtonEvent) => {
|
private onSignOutClick = async (ev: ButtonEvent) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
|
||||||
Modal.createTrackedDialog('Logout from LeftPanel', '', LogoutDialog);
|
const cli = MatrixClientPeg.get();
|
||||||
|
if (!cli || !cli.isCryptoEnabled() || !(await cli.exportRoomKeys())?.length) {
|
||||||
|
// log out without user prompt if they have no local megolm sessions
|
||||||
|
dis.dispatch({action: 'logout'});
|
||||||
|
} else {
|
||||||
|
Modal.createTrackedDialog('Logout from LeftPanel', '', LogoutDialog);
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({contextMenuPosition: null}); // also close the menu
|
this.setState({contextMenuPosition: null}); // also close the menu
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -203,6 +210,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
|
||||||
defaultDispatcher.dispatch({action: 'view_home_page'});
|
defaultDispatcher.dispatch({action: 'view_home_page'});
|
||||||
|
this.setState({contextMenuPosition: null}); // also close the menu
|
||||||
};
|
};
|
||||||
|
|
||||||
private onCommunitySettingsClick = (ev: ButtonEvent) => {
|
private onCommunitySettingsClick = (ev: ButtonEvent) => {
|
||||||
|
|
|
@ -502,6 +502,11 @@ export default class Registration extends React.Component {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hide the server picker once the user is doing UI Auth unless encountered a fatal server error
|
||||||
|
if (this.state.phase !== PHASE_SERVER_DETAILS && this.state.doingUIAuth && !this.state.serverErrorIsFatal) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// If we're on a different phase, we only show the server type selector,
|
// If we're on a different phase, we only show the server type selector,
|
||||||
// which is always shown if we allow custom URLs at all.
|
// which is always shown if we allow custom URLs at all.
|
||||||
// (if there's a fatal server error, we need to show the full server
|
// (if there's a fatal server error, we need to show the full server
|
||||||
|
@ -582,17 +587,6 @@ export default class Registration extends React.Component {
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>;
|
</div>;
|
||||||
} else if (this.state.flows.length) {
|
} else if (this.state.flows.length) {
|
||||||
let onEditServerDetailsClick = null;
|
|
||||||
// If custom URLs are allowed and we haven't selected the Free server type, wire
|
|
||||||
// up the server details edit link.
|
|
||||||
if (
|
|
||||||
PHASES_ENABLED &&
|
|
||||||
!SdkConfig.get()['disable_custom_urls'] &&
|
|
||||||
this.state.serverType !== ServerType.FREE
|
|
||||||
) {
|
|
||||||
onEditServerDetailsClick = this.onEditServerDetailsClick;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <RegistrationForm
|
return <RegistrationForm
|
||||||
defaultUsername={this.state.formVals.username}
|
defaultUsername={this.state.formVals.username}
|
||||||
defaultEmail={this.state.formVals.email}
|
defaultEmail={this.state.formVals.email}
|
||||||
|
@ -600,7 +594,6 @@ export default class Registration extends React.Component {
|
||||||
defaultPhoneNumber={this.state.formVals.phoneNumber}
|
defaultPhoneNumber={this.state.formVals.phoneNumber}
|
||||||
defaultPassword={this.state.formVals.password}
|
defaultPassword={this.state.formVals.password}
|
||||||
onRegisterClick={this.onFormSubmit}
|
onRegisterClick={this.onFormSubmit}
|
||||||
onEditServerDetailsClick={onEditServerDetailsClick}
|
|
||||||
flows={this.state.flows}
|
flows={this.state.flows}
|
||||||
serverConfig={this.props.serverConfig}
|
serverConfig={this.props.serverConfig}
|
||||||
canSubmit={!this.state.serverErrorIsFatal}
|
canSubmit={!this.state.serverErrorIsFatal}
|
||||||
|
@ -686,11 +679,48 @@ export default class Registration extends React.Component {
|
||||||
{ regDoneText }
|
{ regDoneText }
|
||||||
</div>;
|
</div>;
|
||||||
} else {
|
} else {
|
||||||
|
let yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', {
|
||||||
|
serverName: this.props.serverConfig.hsName,
|
||||||
|
});
|
||||||
|
if (this.props.serverConfig.hsNameIsDifferent) {
|
||||||
|
const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip");
|
||||||
|
|
||||||
|
yourMatrixAccountText = _t('Create your Matrix account on <underlinedServerName />', {}, {
|
||||||
|
'underlinedServerName': () => {
|
||||||
|
return <TextWithTooltip
|
||||||
|
class="mx_Login_underlinedServerName"
|
||||||
|
tooltip={this.props.serverConfig.hsUrl}
|
||||||
|
>
|
||||||
|
{this.props.serverConfig.hsName}
|
||||||
|
</TextWithTooltip>;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If custom URLs are allowed, user is not doing UIA flows and they haven't selected the Free server type,
|
||||||
|
// wire up the server details edit link.
|
||||||
|
let editLink = null;
|
||||||
|
if (PHASES_ENABLED &&
|
||||||
|
!SdkConfig.get()['disable_custom_urls'] &&
|
||||||
|
this.state.serverType !== ServerType.FREE &&
|
||||||
|
!this.state.doingUIAuth
|
||||||
|
) {
|
||||||
|
editLink = (
|
||||||
|
<a className="mx_AuthBody_editServerDetails" href="#" onClick={this.onEditServerDetailsClick}>
|
||||||
|
{_t('Change')}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
body = <div>
|
body = <div>
|
||||||
<h2>{ _t('Create your account') }</h2>
|
<h2>{ _t('Create your account') }</h2>
|
||||||
{ errorText }
|
{ errorText }
|
||||||
{ serverDeadSection }
|
{ serverDeadSection }
|
||||||
{ this.renderServerComponent() }
|
{ this.renderServerComponent() }
|
||||||
|
{ this.state.phase !== PHASE_SERVER_DETAILS && <h3>
|
||||||
|
{yourMatrixAccountText}
|
||||||
|
{editLink}
|
||||||
|
</h3> }
|
||||||
{ this.renderRegisterComponent() }
|
{ this.renderRegisterComponent() }
|
||||||
{ goBack }
|
{ goBack }
|
||||||
{ signIn }
|
{ signIn }
|
||||||
|
|
|
@ -102,6 +102,10 @@ export default class CaptchaForm extends React.Component {
|
||||||
console.log("Loaded recaptcha script.");
|
console.log("Loaded recaptcha script.");
|
||||||
try {
|
try {
|
||||||
this._renderRecaptcha(DIV_ID);
|
this._renderRecaptcha(DIV_ID);
|
||||||
|
// clear error if re-rendered
|
||||||
|
this.setState({
|
||||||
|
errorText: null,
|
||||||
|
});
|
||||||
CountlyAnalytics.instance.track("onboarding_grecaptcha_loaded");
|
CountlyAnalytics.instance.track("onboarding_grecaptcha_loaded");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
|
|
@ -123,7 +123,7 @@ export default class CountryDropdown extends React.Component {
|
||||||
const options = displayedCountries.map((country) => {
|
const options = displayedCountries.map((country) => {
|
||||||
return <div className="mx_CountryDropdown_option" key={country.iso2}>
|
return <div className="mx_CountryDropdown_option" key={country.iso2}>
|
||||||
{ this._flagImgForIso2(country.iso2) }
|
{ this._flagImgForIso2(country.iso2) }
|
||||||
{ country.name } (+{ country.prefix })
|
{ _t(country.name) } (+{ country.prefix })
|
||||||
</div>;
|
</div>;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -421,12 +421,12 @@ export class EmailIdentityAuthEntry extends React.Component {
|
||||||
return <Spinner />;
|
return <Spinner />;
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="mx_InteractiveAuthEntryComponents_emailWrapper">
|
||||||
<p>{ _t("An email has been sent to %(emailAddress)s",
|
<p>{ _t("A confirmation email has been sent to %(emailAddress)s",
|
||||||
{ emailAddress: (sub) => <i>{ this.props.inputs.emailAddress }</i> },
|
{ emailAddress: (sub) => <b>{ this.props.inputs.emailAddress }</b> },
|
||||||
) }
|
) }
|
||||||
</p>
|
</p>
|
||||||
<p>{ _t("Please check your email to continue registration.") }</p>
|
<p>{ _t("Open the link in the email to continue registration.") }</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,7 +51,6 @@ export default class RegistrationForm extends React.Component {
|
||||||
defaultUsername: PropTypes.string,
|
defaultUsername: PropTypes.string,
|
||||||
defaultPassword: PropTypes.string,
|
defaultPassword: PropTypes.string,
|
||||||
onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise
|
onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise
|
||||||
onEditServerDetailsClick: PropTypes.func,
|
|
||||||
flows: PropTypes.arrayOf(PropTypes.object).isRequired,
|
flows: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
||||||
canSubmit: PropTypes.bool,
|
canSubmit: PropTypes.bool,
|
||||||
|
@ -250,6 +249,7 @@ export default class RegistrationForm extends React.Component {
|
||||||
|
|
||||||
validateEmailRules = withValidation({
|
validateEmailRules = withValidation({
|
||||||
description: () => _t("Use an email address to recover your account"),
|
description: () => _t("Use an email address to recover your account"),
|
||||||
|
hideDescriptionIfValid: true,
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
key: "required",
|
key: "required",
|
||||||
|
@ -326,6 +326,7 @@ export default class RegistrationForm extends React.Component {
|
||||||
|
|
||||||
validatePhoneNumberRules = withValidation({
|
validatePhoneNumberRules = withValidation({
|
||||||
description: () => _t("Other users can invite you to rooms using your contact details"),
|
description: () => _t("Other users can invite you to rooms using your contact details"),
|
||||||
|
hideDescriptionIfValid: true,
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
key: "required",
|
key: "required",
|
||||||
|
@ -356,6 +357,7 @@ export default class RegistrationForm extends React.Component {
|
||||||
|
|
||||||
validateUsernameRules = withValidation({
|
validateUsernameRules = withValidation({
|
||||||
description: () => _t("Use lowercase letters, numbers, dashes and underscores only"),
|
description: () => _t("Use lowercase letters, numbers, dashes and underscores only"),
|
||||||
|
hideDescriptionIfValid: true,
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
key: "required",
|
key: "required",
|
||||||
|
@ -458,7 +460,7 @@ export default class RegistrationForm extends React.Component {
|
||||||
ref={field => this[FIELD_PASSWORD_CONFIRM] = field}
|
ref={field => this[FIELD_PASSWORD_CONFIRM] = field}
|
||||||
type="password"
|
type="password"
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
label={_t("Confirm")}
|
label={_t("Confirm password")}
|
||||||
value={this.state.passwordConfirm}
|
value={this.state.passwordConfirm}
|
||||||
onChange={this.onPasswordConfirmChange}
|
onChange={this.onPasswordConfirmChange}
|
||||||
onValidate={this.onPasswordConfirmValidate}
|
onValidate={this.onPasswordConfirmValidate}
|
||||||
|
@ -510,33 +512,6 @@ export default class RegistrationForm extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', {
|
|
||||||
serverName: this.props.serverConfig.hsName,
|
|
||||||
});
|
|
||||||
if (this.props.serverConfig.hsNameIsDifferent) {
|
|
||||||
const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip");
|
|
||||||
|
|
||||||
yourMatrixAccountText = _t('Create your Matrix account on <underlinedServerName />', {}, {
|
|
||||||
'underlinedServerName': () => {
|
|
||||||
return <TextWithTooltip
|
|
||||||
class="mx_Login_underlinedServerName"
|
|
||||||
tooltip={this.props.serverConfig.hsUrl}
|
|
||||||
>
|
|
||||||
{this.props.serverConfig.hsName}
|
|
||||||
</TextWithTooltip>;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let editLink = null;
|
|
||||||
if (this.props.onEditServerDetailsClick) {
|
|
||||||
editLink = <a className="mx_AuthBody_editServerDetails"
|
|
||||||
href="#" onClick={this.props.onEditServerDetailsClick}
|
|
||||||
>
|
|
||||||
{_t('Change')}
|
|
||||||
</a>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const registerButton = (
|
const registerButton = (
|
||||||
<input className="mx_Login_submit" type="submit" value={_t("Register")} disabled={!this.props.canSubmit} />
|
<input className="mx_Login_submit" type="submit" value={_t("Register")} disabled={!this.props.canSubmit} />
|
||||||
);
|
);
|
||||||
|
@ -572,10 +547,6 @@ export default class RegistrationForm extends React.Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3>
|
|
||||||
{yourMatrixAccountText}
|
|
||||||
{editLink}
|
|
||||||
</h3>
|
|
||||||
<form onSubmit={this.onSubmit}>
|
<form onSubmit={this.onSubmit}>
|
||||||
<div className="mx_AuthBody_fieldRow">
|
<div className="mx_AuthBody_fieldRow">
|
||||||
{this.renderUsername()}
|
{this.renderUsername()}
|
||||||
|
|
|
@ -51,7 +51,8 @@ const calculateUrls = (url, urls) => {
|
||||||
_urls = urls || [];
|
_urls = urls || [];
|
||||||
|
|
||||||
if (url) {
|
if (url) {
|
||||||
_urls.unshift(url); // put in urls[0]
|
// copy urls and put url first
|
||||||
|
_urls = [url, ..._urls];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,7 @@ interface IProps {
|
||||||
height?: number;
|
height?: number;
|
||||||
resizeMethod?: ResizeMethod;
|
resizeMethod?: ResizeMethod;
|
||||||
viewAvatarOnClick?: boolean;
|
viewAvatarOnClick?: boolean;
|
||||||
|
onClick?(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
@ -130,7 +131,7 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const {room, oobData, viewAvatarOnClick, ...otherProps} = this.props;
|
const {room, oobData, viewAvatarOnClick, onClick, ...otherProps} = this.props;
|
||||||
|
|
||||||
const roomName = room ? room.name : oobData.name;
|
const roomName = room ? room.name : oobData.name;
|
||||||
|
|
||||||
|
@ -139,7 +140,7 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
|
||||||
name={roomName}
|
name={roomName}
|
||||||
idName={room ? room.roomId : null}
|
idName={room ? room.roomId : null}
|
||||||
urls={this.state.urls}
|
urls={this.state.urls}
|
||||||
onClick={viewAvatarOnClick && this.state.urls[0] ? this.onRoomAvatarClick : null}
|
onClick={viewAvatarOnClick && this.state.urls[0] ? this.onRoomAvatarClick : onClick}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,8 +48,8 @@ export default (props) => {
|
||||||
title: _t('Feedback sent'),
|
title: _t('Feedback sent'),
|
||||||
description: _t('Thank you!'),
|
description: _t('Thank you!'),
|
||||||
});
|
});
|
||||||
props.onFinished();
|
|
||||||
}
|
}
|
||||||
|
props.onFinished();
|
||||||
};
|
};
|
||||||
|
|
||||||
const brand = SdkConfig.get().brand;
|
const brand = SdkConfig.get().brand;
|
||||||
|
|
|
@ -31,7 +31,7 @@ import dis from "../../../dispatcher/dispatcher";
|
||||||
import IdentityAuthClient from "../../../IdentityAuthClient";
|
import IdentityAuthClient from "../../../IdentityAuthClient";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import {humanizeTime} from "../../../utils/humanize";
|
import {humanizeTime} from "../../../utils/humanize";
|
||||||
import createRoom, {canEncryptToAllUsers, privateShouldBeEncrypted} from "../../../createRoom";
|
import createRoom, {canEncryptToAllUsers, findDMForUser, privateShouldBeEncrypted} from "../../../createRoom";
|
||||||
import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite";
|
import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite";
|
||||||
import {Key} from "../../../Keyboard";
|
import {Key} from "../../../Keyboard";
|
||||||
import {Action} from "../../../dispatcher/actions";
|
import {Action} from "../../../dispatcher/actions";
|
||||||
|
@ -41,6 +41,7 @@ import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import {UIFeature} from "../../../settings/UIFeature";
|
import {UIFeature} from "../../../settings/UIFeature";
|
||||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||||
|
import {Room} from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
|
@ -308,10 +309,14 @@ export default class InviteDialog extends React.PureComponent {
|
||||||
|
|
||||||
// The room ID this dialog is for. Only required for KIND_INVITE.
|
// The room ID this dialog is for. Only required for KIND_INVITE.
|
||||||
roomId: PropTypes.string,
|
roomId: PropTypes.string,
|
||||||
|
|
||||||
|
// Initial value to populate the filter with
|
||||||
|
initialText: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
kind: KIND_DM,
|
kind: KIND_DM,
|
||||||
|
initialText: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
_debounceTimer: number = null;
|
_debounceTimer: number = null;
|
||||||
|
@ -338,7 +343,7 @@ export default class InviteDialog extends React.PureComponent {
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
targets: [], // array of Member objects (see interface above)
|
targets: [], // array of Member objects (see interface above)
|
||||||
filterText: "",
|
filterText: this.props.initialText,
|
||||||
recents: InviteDialog.buildRecents(alreadyInvited),
|
recents: InviteDialog.buildRecents(alreadyInvited),
|
||||||
numRecentsShown: INITIAL_ROOMS_SHOWN,
|
numRecentsShown: INITIAL_ROOMS_SHOWN,
|
||||||
suggestions: this._buildSuggestions(alreadyInvited),
|
suggestions: this._buildSuggestions(alreadyInvited),
|
||||||
|
@ -356,6 +361,12 @@ export default class InviteDialog extends React.PureComponent {
|
||||||
this._editorRef = createRef();
|
this._editorRef = createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (this.props.initialText) {
|
||||||
|
this._updateSuggestions(this.props.initialText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number} {
|
static buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number} {
|
||||||
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
|
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
|
||||||
|
|
||||||
|
@ -575,7 +586,12 @@ export default class InviteDialog extends React.PureComponent {
|
||||||
const targetIds = targets.map(t => t.userId);
|
const targetIds = targets.map(t => t.userId);
|
||||||
|
|
||||||
// Check if there is already a DM with these people and reuse it if possible.
|
// Check if there is already a DM with these people and reuse it if possible.
|
||||||
const existingRoom = DMRoomMap.shared().getDMRoomForIdentifiers(targetIds);
|
let existingRoom: Room;
|
||||||
|
if (targetIds.length === 1) {
|
||||||
|
existingRoom = findDMForUser(MatrixClientPeg.get(), targetIds[0]);
|
||||||
|
} else {
|
||||||
|
existingRoom = DMRoomMap.shared().getDMRoomForIdentifiers(targetIds);
|
||||||
|
}
|
||||||
if (existingRoom) {
|
if (existingRoom) {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
|
@ -687,6 +703,115 @@ export default class InviteDialog extends React.PureComponent {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_updateSuggestions = async (term) => {
|
||||||
|
MatrixClientPeg.get().searchUserDirectory({term}).then(async r => {
|
||||||
|
if (term !== this.state.filterText) {
|
||||||
|
// Discard the results - we were probably too slow on the server-side to make
|
||||||
|
// these results useful. This is a race we want to avoid because we could overwrite
|
||||||
|
// more accurate results.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!r.results) r.results = [];
|
||||||
|
|
||||||
|
// While we're here, try and autocomplete a search result for the mxid itself
|
||||||
|
// if there's no matches (and the input looks like a mxid).
|
||||||
|
if (term[0] === '@' && term.indexOf(':') > 1) {
|
||||||
|
try {
|
||||||
|
const profile = await MatrixClientPeg.get().getProfileInfo(term);
|
||||||
|
if (profile) {
|
||||||
|
// If we have a profile, we have enough information to assume that
|
||||||
|
// the mxid can be invited - add it to the list. We stick it at the
|
||||||
|
// top so it is most obviously presented to the user.
|
||||||
|
r.results.splice(0, 0, {
|
||||||
|
user_id: term,
|
||||||
|
display_name: profile['displayname'],
|
||||||
|
avatar_url: profile['avatar_url'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Non-fatal error trying to make an invite for a user ID");
|
||||||
|
console.warn(e);
|
||||||
|
|
||||||
|
// Add a result anyways, just without a profile. We stick it at the
|
||||||
|
// top so it is most obviously presented to the user.
|
||||||
|
r.results.splice(0, 0, {
|
||||||
|
user_id: term,
|
||||||
|
display_name: term,
|
||||||
|
avatar_url: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
serverResultsMixin: r.results.map(u => ({
|
||||||
|
userId: u.user_id,
|
||||||
|
user: new DirectoryMember(u),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}).catch(e => {
|
||||||
|
console.error("Error searching user directory:");
|
||||||
|
console.error(e);
|
||||||
|
this.setState({serverResultsMixin: []}); // clear results because it's moderately fatal
|
||||||
|
});
|
||||||
|
|
||||||
|
// Whenever we search the directory, also try to search the identity server. It's
|
||||||
|
// all debounced the same anyways.
|
||||||
|
if (!this.state.canUseIdentityServer) {
|
||||||
|
// The user doesn't have an identity server set - warn them of that.
|
||||||
|
this.setState({tryingIdentityServer: true});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (term.indexOf('@') > 0 && Email.looksValid(term) && SettingsStore.getValue(UIFeature.IdentityServer)) {
|
||||||
|
// Start off by suggesting the plain email while we try and resolve it
|
||||||
|
// to a real account.
|
||||||
|
this.setState({
|
||||||
|
// per above: the userId is a lie here - it's just a regular identifier
|
||||||
|
threepidResultsMixin: [{user: new ThreepidMember(term), userId: term}],
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const authClient = new IdentityAuthClient();
|
||||||
|
const token = await authClient.getAccessToken();
|
||||||
|
if (term !== this.state.filterText) return; // abandon hope
|
||||||
|
|
||||||
|
const lookup = await MatrixClientPeg.get().lookupThreePid(
|
||||||
|
'email',
|
||||||
|
term,
|
||||||
|
undefined, // callback
|
||||||
|
token,
|
||||||
|
);
|
||||||
|
if (term !== this.state.filterText) return; // abandon hope
|
||||||
|
|
||||||
|
if (!lookup || !lookup.mxid) {
|
||||||
|
// We weren't able to find anyone - we're already suggesting the plain email
|
||||||
|
// as an alternative, so do nothing.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We append the user suggestion to give the user an option to click
|
||||||
|
// the email anyways, and so we don't cause things to jump around. In
|
||||||
|
// theory, the user would see the user pop up and think "ah yes, that
|
||||||
|
// person!"
|
||||||
|
const profile = await MatrixClientPeg.get().getProfileInfo(lookup.mxid);
|
||||||
|
if (term !== this.state.filterText || !profile) return; // abandon hope
|
||||||
|
this.setState({
|
||||||
|
threepidResultsMixin: [...this.state.threepidResultsMixin, {
|
||||||
|
user: new DirectoryMember({
|
||||||
|
user_id: lookup.mxid,
|
||||||
|
display_name: profile.displayname,
|
||||||
|
avatar_url: profile.avatar_url,
|
||||||
|
}),
|
||||||
|
userId: lookup.mxid,
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error searching identity server:");
|
||||||
|
console.error(e);
|
||||||
|
this.setState({threepidResultsMixin: []}); // clear results because it's moderately fatal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
_updateFilter = (e) => {
|
_updateFilter = (e) => {
|
||||||
const term = e.target.value;
|
const term = e.target.value;
|
||||||
this.setState({filterText: term});
|
this.setState({filterText: term});
|
||||||
|
@ -697,113 +822,8 @@ export default class InviteDialog extends React.PureComponent {
|
||||||
if (this._debounceTimer) {
|
if (this._debounceTimer) {
|
||||||
clearTimeout(this._debounceTimer);
|
clearTimeout(this._debounceTimer);
|
||||||
}
|
}
|
||||||
this._debounceTimer = setTimeout(async () => {
|
this._debounceTimer = setTimeout(() => {
|
||||||
MatrixClientPeg.get().searchUserDirectory({term}).then(async r => {
|
this._updateSuggestions(term);
|
||||||
if (term !== this.state.filterText) {
|
|
||||||
// Discard the results - we were probably too slow on the server-side to make
|
|
||||||
// these results useful. This is a race we want to avoid because we could overwrite
|
|
||||||
// more accurate results.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!r.results) r.results = [];
|
|
||||||
|
|
||||||
// While we're here, try and autocomplete a search result for the mxid itself
|
|
||||||
// if there's no matches (and the input looks like a mxid).
|
|
||||||
if (term[0] === '@' && term.indexOf(':') > 1) {
|
|
||||||
try {
|
|
||||||
const profile = await MatrixClientPeg.get().getProfileInfo(term);
|
|
||||||
if (profile) {
|
|
||||||
// If we have a profile, we have enough information to assume that
|
|
||||||
// the mxid can be invited - add it to the list. We stick it at the
|
|
||||||
// top so it is most obviously presented to the user.
|
|
||||||
r.results.splice(0, 0, {
|
|
||||||
user_id: term,
|
|
||||||
display_name: profile['displayname'],
|
|
||||||
avatar_url: profile['avatar_url'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("Non-fatal error trying to make an invite for a user ID");
|
|
||||||
console.warn(e);
|
|
||||||
|
|
||||||
// Add a result anyways, just without a profile. We stick it at the
|
|
||||||
// top so it is most obviously presented to the user.
|
|
||||||
r.results.splice(0, 0, {
|
|
||||||
user_id: term,
|
|
||||||
display_name: term,
|
|
||||||
avatar_url: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
serverResultsMixin: r.results.map(u => ({
|
|
||||||
userId: u.user_id,
|
|
||||||
user: new DirectoryMember(u),
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
}).catch(e => {
|
|
||||||
console.error("Error searching user directory:");
|
|
||||||
console.error(e);
|
|
||||||
this.setState({serverResultsMixin: []}); // clear results because it's moderately fatal
|
|
||||||
});
|
|
||||||
|
|
||||||
// Whenever we search the directory, also try to search the identity server. It's
|
|
||||||
// all debounced the same anyways.
|
|
||||||
if (!this.state.canUseIdentityServer) {
|
|
||||||
// The user doesn't have an identity server set - warn them of that.
|
|
||||||
this.setState({tryingIdentityServer: true});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (term.indexOf('@') > 0 && Email.looksValid(term) && SettingsStore.getValue(UIFeature.IdentityServer)) {
|
|
||||||
// Start off by suggesting the plain email while we try and resolve it
|
|
||||||
// to a real account.
|
|
||||||
this.setState({
|
|
||||||
// per above: the userId is a lie here - it's just a regular identifier
|
|
||||||
threepidResultsMixin: [{user: new ThreepidMember(term), userId: term}],
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
const authClient = new IdentityAuthClient();
|
|
||||||
const token = await authClient.getAccessToken();
|
|
||||||
if (term !== this.state.filterText) return; // abandon hope
|
|
||||||
|
|
||||||
const lookup = await MatrixClientPeg.get().lookupThreePid(
|
|
||||||
'email',
|
|
||||||
term,
|
|
||||||
undefined, // callback
|
|
||||||
token,
|
|
||||||
);
|
|
||||||
if (term !== this.state.filterText) return; // abandon hope
|
|
||||||
|
|
||||||
if (!lookup || !lookup.mxid) {
|
|
||||||
// We weren't able to find anyone - we're already suggesting the plain email
|
|
||||||
// as an alternative, so do nothing.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We append the user suggestion to give the user an option to click
|
|
||||||
// the email anyways, and so we don't cause things to jump around. In
|
|
||||||
// theory, the user would see the user pop up and think "ah yes, that
|
|
||||||
// person!"
|
|
||||||
const profile = await MatrixClientPeg.get().getProfileInfo(lookup.mxid);
|
|
||||||
if (term !== this.state.filterText || !profile) return; // abandon hope
|
|
||||||
this.setState({
|
|
||||||
threepidResultsMixin: [...this.state.threepidResultsMixin, {
|
|
||||||
user: new DirectoryMember({
|
|
||||||
user_id: lookup.mxid,
|
|
||||||
display_name: profile.displayname,
|
|
||||||
avatar_url: profile.avatar_url,
|
|
||||||
}),
|
|
||||||
userId: lookup.mxid,
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error searching identity server:");
|
|
||||||
console.error(e);
|
|
||||||
this.setState({threepidResultsMixin: []}); // clear results because it's moderately fatal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 150); // 150ms debounce (human reaction time + some)
|
}, 150); // 150ms debounce (human reaction time + some)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,11 @@ import {
|
||||||
IModalWidgetCloseRequest,
|
IModalWidgetCloseRequest,
|
||||||
IModalWidgetOpenRequestData,
|
IModalWidgetOpenRequestData,
|
||||||
IModalWidgetReturnData,
|
IModalWidgetReturnData,
|
||||||
|
ISetModalButtonEnabledActionRequest,
|
||||||
|
IWidgetApiAcknowledgeResponseData,
|
||||||
|
IWidgetApiErrorResponseData,
|
||||||
|
BuiltInModalButtonID,
|
||||||
|
ModalButtonID,
|
||||||
ModalButtonKind,
|
ModalButtonKind,
|
||||||
Widget,
|
Widget,
|
||||||
WidgetApiFromWidgetAction,
|
WidgetApiFromWidgetAction,
|
||||||
|
@ -31,6 +36,7 @@ import {StopGapWidgetDriver} from "../../../stores/widgets/StopGapWidgetDriver";
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||||
import RoomViewStore from "../../../stores/RoomViewStore";
|
import RoomViewStore from "../../../stores/RoomViewStore";
|
||||||
import {OwnProfileStore} from "../../../stores/OwnProfileStore";
|
import {OwnProfileStore} from "../../../stores/OwnProfileStore";
|
||||||
|
import { arrayFastClone } from "../../../utils/arrays";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
widgetDefinition: IModalWidgetOpenRequestData;
|
widgetDefinition: IModalWidgetOpenRequestData;
|
||||||
|
@ -40,15 +46,19 @@ interface IProps {
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
messaging?: ClientWidgetApi;
|
messaging?: ClientWidgetApi;
|
||||||
|
disabledButtonIds: ModalButtonID[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_BUTTONS = 3;
|
const MAX_BUTTONS = 3;
|
||||||
|
|
||||||
export default class ModalWidgetDialog extends React.PureComponent<IProps, IState> {
|
export default class ModalWidgetDialog extends React.PureComponent<IProps, IState> {
|
||||||
private readonly widget: Widget;
|
private readonly widget: Widget;
|
||||||
|
private readonly possibleButtons: ModalButtonID[];
|
||||||
private appFrame: React.RefObject<HTMLIFrameElement> = React.createRef();
|
private appFrame: React.RefObject<HTMLIFrameElement> = React.createRef();
|
||||||
|
|
||||||
state: IState = {};
|
state: IState = {
|
||||||
|
disabledButtonIds: [],
|
||||||
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -58,6 +68,7 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
|
||||||
creatorUserId: MatrixClientPeg.get().getUserId(),
|
creatorUserId: MatrixClientPeg.get().getUserId(),
|
||||||
id: `modal_${this.props.sourceWidgetId}`,
|
id: `modal_${this.props.sourceWidgetId}`,
|
||||||
});
|
});
|
||||||
|
this.possibleButtons = (this.props.widgetDefinition.buttons || []).map(b => b.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
|
@ -79,12 +90,35 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
|
||||||
private onLoad = () => {
|
private onLoad = () => {
|
||||||
this.state.messaging.once("ready", this.onReady);
|
this.state.messaging.once("ready", this.onReady);
|
||||||
this.state.messaging.on(`action:${WidgetApiFromWidgetAction.CloseModalWidget}`, this.onWidgetClose);
|
this.state.messaging.on(`action:${WidgetApiFromWidgetAction.CloseModalWidget}`, this.onWidgetClose);
|
||||||
|
this.state.messaging.on(`action:${WidgetApiFromWidgetAction.SetModalButtonEnabled}`, this.onButtonEnableToggle);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onWidgetClose = (ev: CustomEvent<IModalWidgetCloseRequest>) => {
|
private onWidgetClose = (ev: CustomEvent<IModalWidgetCloseRequest>) => {
|
||||||
this.props.onFinished(true, ev.detail.data);
|
this.props.onFinished(true, ev.detail.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onButtonEnableToggle = (ev: CustomEvent<ISetModalButtonEnabledActionRequest>) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
const isClose = ev.detail.data.button === BuiltInModalButtonID.Close;
|
||||||
|
if (isClose || !this.possibleButtons.includes(ev.detail.data.button)) {
|
||||||
|
return this.state.messaging.transport.reply(ev.detail, {
|
||||||
|
error: {message: "Invalid button"},
|
||||||
|
} as IWidgetApiErrorResponseData);
|
||||||
|
}
|
||||||
|
|
||||||
|
let buttonIds: ModalButtonID[];
|
||||||
|
if (ev.detail.data.enabled) {
|
||||||
|
buttonIds = arrayFastClone(this.state.disabledButtonIds).filter(i => i !== ev.detail.data.button);
|
||||||
|
} else {
|
||||||
|
// use a set to swap the operation to avoid memory leaky arrays.
|
||||||
|
const tempSet = new Set(this.state.disabledButtonIds);
|
||||||
|
tempSet.add(ev.detail.data.button);
|
||||||
|
buttonIds = Array.from(tempSet);
|
||||||
|
}
|
||||||
|
this.setState({disabledButtonIds: buttonIds});
|
||||||
|
this.state.messaging.transport.reply(ev.detail, {} as IWidgetApiAcknowledgeResponseData);
|
||||||
|
};
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const templated = this.widget.getCompleteUrl({
|
const templated = this.widget.getCompleteUrl({
|
||||||
currentRoomId: RoomViewStore.getRoomId(),
|
currentRoomId: RoomViewStore.getRoomId(),
|
||||||
|
|
|
@ -39,7 +39,7 @@ interface IProps extends React.InputHTMLAttributes<Element> {
|
||||||
tabIndex?: number;
|
tabIndex?: number;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
onClick?(e?: ButtonEvent): void;
|
onClick(e?: ButtonEvent): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IAccessibleButtonProps extends React.InputHTMLAttributes<Element> {
|
interface IAccessibleButtonProps extends React.InputHTMLAttributes<Element> {
|
||||||
|
|
|
@ -20,8 +20,8 @@ import * as sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
export default class DirectorySearchBox extends React.Component {
|
export default class DirectorySearchBox extends React.Component {
|
||||||
constructor() {
|
constructor(props) {
|
||||||
super();
|
super(props);
|
||||||
this._collectInput = this._collectInput.bind(this);
|
this._collectInput = this._collectInput.bind(this);
|
||||||
this._onClearClick = this._onClearClick.bind(this);
|
this._onClearClick = this._onClearClick.bind(this);
|
||||||
this._onChange = this._onChange.bind(this);
|
this._onChange = this._onChange.bind(this);
|
||||||
|
@ -31,7 +31,7 @@ export default class DirectorySearchBox extends React.Component {
|
||||||
this.input = null;
|
this.input = null;
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
value: '',
|
value: this.props.initialText || '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,15 +90,20 @@ export default class DirectorySearchBox extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className={`mx_DirectorySearchBox ${this.props.className} mx_textinput`}>
|
return <div className={`mx_DirectorySearchBox ${this.props.className} mx_textinput`}>
|
||||||
<input type="text" name="dirsearch" value={this.state.value}
|
<input
|
||||||
className="mx_textinput_icon mx_textinput_search"
|
type="text"
|
||||||
ref={this._collectInput}
|
name="dirsearch"
|
||||||
onChange={this._onChange} onKeyUp={this._onKeyUp}
|
value={this.state.value}
|
||||||
placeholder={this.props.placeholder} autoFocus
|
className="mx_textinput_icon mx_textinput_search"
|
||||||
/>
|
ref={this._collectInput}
|
||||||
{ joinButton }
|
onChange={this._onChange}
|
||||||
<AccessibleButton className="mx_DirectorySearchBox_clear" onClick={this._onClearClick}></AccessibleButton>
|
onKeyUp={this._onKeyUp}
|
||||||
</div>;
|
placeholder={this.props.placeholder}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
{ joinButton }
|
||||||
|
<AccessibleButton className="mx_DirectorySearchBox_clear" onClick={this._onClearClick} />
|
||||||
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,4 +114,5 @@ DirectorySearchBox.propTypes = {
|
||||||
onJoinClick: PropTypes.func,
|
onJoinClick: PropTypes.func,
|
||||||
placeholder: PropTypes.string,
|
placeholder: PropTypes.string,
|
||||||
showJoinButton: PropTypes.bool,
|
showJoinButton: PropTypes.bool,
|
||||||
|
initialText: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2019 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 from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import AccessibleButton from "./AccessibleButton";
|
|
||||||
|
|
||||||
export default function IconButton(props) {
|
|
||||||
const {icon, className, ...restProps} = props;
|
|
||||||
|
|
||||||
let newClassName = (className || "") + " mx_IconButton";
|
|
||||||
newClassName = newClassName + " mx_IconButton_icon_" + icon;
|
|
||||||
|
|
||||||
const allProps = Object.assign({}, restProps, {className: newClassName});
|
|
||||||
|
|
||||||
return React.createElement(AccessibleButton, allProps);
|
|
||||||
}
|
|
||||||
|
|
||||||
IconButton.propTypes = Object.assign({
|
|
||||||
icon: PropTypes.string,
|
|
||||||
}, AccessibleButton.propTypes);
|
|
90
src/components/views/elements/MiniAvatarUploader.tsx
Normal file
90
src/components/views/elements/MiniAvatarUploader.tsx
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, {useContext, useRef, useState} from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import AccessibleButton from "./AccessibleButton";
|
||||||
|
import Tooltip from './Tooltip';
|
||||||
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
import {useTimeout} from "../../../hooks/useTimeout";
|
||||||
|
|
||||||
|
export const AVATAR_SIZE = 52;
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
hasAvatar: boolean;
|
||||||
|
noAvatarLabel?: string;
|
||||||
|
hasAvatarLabel?: string;
|
||||||
|
setAvatarUrl(url: string): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MiniAvatarUploader: React.FC<IProps> = ({ hasAvatar, hasAvatarLabel, noAvatarLabel, setAvatarUrl, children }) => {
|
||||||
|
const cli = useContext(MatrixClientContext);
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [hover, setHover] = useState(false);
|
||||||
|
const [show, setShow] = useState(false);
|
||||||
|
|
||||||
|
useTimeout(() => {
|
||||||
|
setShow(true);
|
||||||
|
}, 3000); // show after 3 seconds
|
||||||
|
useTimeout(() => {
|
||||||
|
setShow(false);
|
||||||
|
}, 13000); // hide after being shown for 10 seconds
|
||||||
|
|
||||||
|
const uploadRef = useRef<HTMLInputElement>();
|
||||||
|
|
||||||
|
const label = (hasAvatar || busy) ? hasAvatarLabel : noAvatarLabel;
|
||||||
|
|
||||||
|
return <React.Fragment>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={uploadRef}
|
||||||
|
className="mx_MiniAvatarUploader_input"
|
||||||
|
onChange={async (ev) => {
|
||||||
|
if (!ev.target.files?.length) return;
|
||||||
|
setBusy(true);
|
||||||
|
const file = ev.target.files[0];
|
||||||
|
const uri = await cli.uploadContent(file);
|
||||||
|
await setAvatarUrl(uri);
|
||||||
|
setBusy(false);
|
||||||
|
}}
|
||||||
|
accept="image/*"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AccessibleButton
|
||||||
|
className={classNames("mx_MiniAvatarUploader", {
|
||||||
|
mx_MiniAvatarUploader_busy: busy,
|
||||||
|
mx_MiniAvatarUploader_hasAvatar: hasAvatar,
|
||||||
|
})}
|
||||||
|
disabled={busy}
|
||||||
|
onClick={() => {
|
||||||
|
uploadRef.current.click();
|
||||||
|
}}
|
||||||
|
onMouseOver={() => setHover(true)}
|
||||||
|
onMouseLeave={() => setHover(false)}
|
||||||
|
>
|
||||||
|
{ children }
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
label={label}
|
||||||
|
visible={!!label && (hover || show)}
|
||||||
|
forceOnRight
|
||||||
|
/>
|
||||||
|
</AccessibleButton>
|
||||||
|
</React.Fragment>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MiniAvatarUploader;
|
|
@ -33,6 +33,7 @@ interface IRule<T, D = void> {
|
||||||
interface IArgs<T, D = void> {
|
interface IArgs<T, D = void> {
|
||||||
rules: IRule<T, D>[];
|
rules: IRule<T, D>[];
|
||||||
description(this: T, derivedData: D): React.ReactChild;
|
description(this: T, derivedData: D): React.ReactChild;
|
||||||
|
hideDescriptionIfValid?: boolean;
|
||||||
deriveData?(data: Data): Promise<D>;
|
deriveData?(data: Data): Promise<D>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,6 +55,8 @@ export interface IValidationResult {
|
||||||
* @param {Function} description
|
* @param {Function} description
|
||||||
* Function that returns a string summary of the kind of value that will
|
* Function that returns a string summary of the kind of value that will
|
||||||
* meet the validation rules. Shown at the top of the validation feedback.
|
* meet the validation rules. Shown at the top of the validation feedback.
|
||||||
|
* @param {Boolean} hideDescriptionIfValid
|
||||||
|
* If true, don't show the description if the validation passes validation.
|
||||||
* @param {Function} deriveData
|
* @param {Function} deriveData
|
||||||
* Optional function that returns a Promise to an object of generic type D.
|
* Optional function that returns a Promise to an object of generic type D.
|
||||||
* The result of this Promise is passed to rule methods `skip`, `test`, `valid`, and `invalid`.
|
* The result of this Promise is passed to rule methods `skip`, `test`, `valid`, and `invalid`.
|
||||||
|
@ -71,7 +74,9 @@ export interface IValidationResult {
|
||||||
* A validation function that takes in the current input value and returns
|
* A validation function that takes in the current input value and returns
|
||||||
* the overall validity and a feedback UI that can be rendered for more detail.
|
* the overall validity and a feedback UI that can be rendered for more detail.
|
||||||
*/
|
*/
|
||||||
export default function withValidation<T = undefined, D = void>({ description, deriveData, rules }: IArgs<T, D>) {
|
export default function withValidation<T = undefined, D = void>({
|
||||||
|
description, hideDescriptionIfValid, deriveData, rules,
|
||||||
|
}: IArgs<T, D>) {
|
||||||
return async function onValidate({ value, focused, allowEmpty = true }: IFieldState): Promise<IValidationResult> {
|
return async function onValidate({ value, focused, allowEmpty = true }: IFieldState): Promise<IValidationResult> {
|
||||||
if (!value && allowEmpty) {
|
if (!value && allowEmpty) {
|
||||||
return {
|
return {
|
||||||
|
@ -156,7 +161,7 @@ export default function withValidation<T = undefined, D = void>({ description, d
|
||||||
}
|
}
|
||||||
|
|
||||||
let summary;
|
let summary;
|
||||||
if (description) {
|
if (description && (details || !hideDescriptionIfValid)) {
|
||||||
// We're setting `this` to whichever component holds the validation
|
// We're setting `this` to whichever component holds the validation
|
||||||
// function. That allows rules to access the state of the component.
|
// function. That allows rules to access the state of the component.
|
||||||
const content = description.call(this, derivedData);
|
const content = description.call(this, derivedData);
|
||||||
|
|
|
@ -1,63 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { _t } from '../../../languageHandler';
|
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
|
||||||
|
|
||||||
export default class EncryptionEvent extends React.Component {
|
|
||||||
render() {
|
|
||||||
const {mxEvent} = this.props;
|
|
||||||
|
|
||||||
let body;
|
|
||||||
let classes = "mx_EventTile_bubble mx_cryptoEvent mx_cryptoEvent_icon";
|
|
||||||
const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(mxEvent.getRoomId());
|
|
||||||
if (mxEvent.getContent().algorithm === 'm.megolm.v1.aes-sha2' && isRoomEncrypted) {
|
|
||||||
body = <div>
|
|
||||||
<div className="mx_cryptoEvent_title">{_t("Encryption enabled")}</div>
|
|
||||||
<div className="mx_cryptoEvent_subtitle">
|
|
||||||
{_t(
|
|
||||||
"Messages in this room are end-to-end encrypted. " +
|
|
||||||
"Learn more & verify this user in their user profile.",
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
} else if (isRoomEncrypted) {
|
|
||||||
body = <div>
|
|
||||||
<div className="mx_cryptoEvent_title">{_t("Encryption enabled")}</div>
|
|
||||||
<div className="mx_cryptoEvent_subtitle">
|
|
||||||
{_t("Ignored attempt to disable encryption")}
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
} else {
|
|
||||||
body = <div>
|
|
||||||
<div className="mx_cryptoEvent_title">{_t("Encryption not enabled")}</div>
|
|
||||||
<div className="mx_cryptoEvent_subtitle">{_t("The encryption used by this room isn't supported.")}</div>
|
|
||||||
</div>;
|
|
||||||
classes += " mx_cryptoEvent_icon_warning";
|
|
||||||
}
|
|
||||||
|
|
||||||
return (<div className={classes}>
|
|
||||||
{body}
|
|
||||||
</div>);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EncryptionEvent.propTypes = {
|
|
||||||
/* the MatrixEvent to show */
|
|
||||||
mxEvent: PropTypes.object.isRequired,
|
|
||||||
};
|
|
68
src/components/views/messages/EncryptionEvent.tsx
Normal file
68
src/components/views/messages/EncryptionEvent.tsx
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, {forwardRef, useContext} from 'react';
|
||||||
|
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||||
|
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
|
import EventTileBubble from "./EventTileBubble";
|
||||||
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
mxEvent: MatrixEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EncryptionEvent = forwardRef<HTMLDivElement, IProps>(({mxEvent}, ref) => {
|
||||||
|
const cli = useContext(MatrixClientContext);
|
||||||
|
const roomId = mxEvent.getRoomId();
|
||||||
|
const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(roomId);
|
||||||
|
|
||||||
|
if (mxEvent.getContent().algorithm === 'm.megolm.v1.aes-sha2' && isRoomEncrypted) {
|
||||||
|
let subtitle: string;
|
||||||
|
const dmPartner = DMRoomMap.shared().getUserIdForRoomId(roomId);
|
||||||
|
if (dmPartner) {
|
||||||
|
const displayName = cli?.getRoom(roomId)?.getMember(dmPartner)?.rawDisplayName || dmPartner;
|
||||||
|
subtitle = _t("Messages here are end-to-end encrypted. " +
|
||||||
|
"Verify %(displayName)s in their profile - tap on their avatar.", { displayName });
|
||||||
|
} else {
|
||||||
|
subtitle = _t("Messages in this room are end-to-end encrypted. " +
|
||||||
|
"When people join, you can verify them in their profile, just tap on their avatar.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return <EventTileBubble
|
||||||
|
className="mx_cryptoEvent mx_cryptoEvent_icon"
|
||||||
|
title={_t("Encryption enabled")}
|
||||||
|
subtitle={subtitle}
|
||||||
|
/>;
|
||||||
|
} else if (isRoomEncrypted) {
|
||||||
|
return <EventTileBubble
|
||||||
|
className="mx_cryptoEvent mx_cryptoEvent_icon"
|
||||||
|
title={_t("Encryption enabled")}
|
||||||
|
subtitle={_t("Ignored attempt to disable encryption")}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <EventTileBubble
|
||||||
|
className="mx_cryptoEvent mx_cryptoEvent_icon mx_cryptoEvent_icon_warning"
|
||||||
|
title={_t("Encryption not enabled")}
|
||||||
|
subtitle={_t("The encryption used by this room isn't supported.")}
|
||||||
|
ref={ref}
|
||||||
|
/>;
|
||||||
|
});
|
||||||
|
|
||||||
|
export default EncryptionEvent;
|
34
src/components/views/messages/EventTileBubble.tsx
Normal file
34
src/components/views/messages/EventTileBubble.tsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, {forwardRef, ReactNode} from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
className: string;
|
||||||
|
title: string;
|
||||||
|
subtitle?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EventTileBubble = forwardRef<HTMLDivElement, IProps>(({ className, title, subtitle, children }, ref) => {
|
||||||
|
return <div className={classNames("mx_EventTileBubble", className)} ref={ref}>
|
||||||
|
<div className="mx_EventTileBubble_title">{ title }</div>
|
||||||
|
{ subtitle && <div className="mx_EventTileBubble_subtitle">{ subtitle }</div> }
|
||||||
|
{ children }
|
||||||
|
</div>;
|
||||||
|
});
|
||||||
|
|
||||||
|
export default EventTileBubble;
|
|
@ -18,6 +18,7 @@ import React from 'react';
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import WidgetStore from "../../../stores/WidgetStore";
|
import WidgetStore from "../../../stores/WidgetStore";
|
||||||
|
import EventTileBubble from "./EventTileBubble";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
|
@ -40,37 +41,24 @@ export default class MJitsiWidgetEvent extends React.PureComponent<IProps> {
|
||||||
|
|
||||||
if (!url) {
|
if (!url) {
|
||||||
// removed
|
// removed
|
||||||
return (
|
return <EventTileBubble
|
||||||
<div className='mx_EventTile_bubble mx_MJitsiWidgetEvent'>
|
className="mx_MJitsiWidgetEvent"
|
||||||
<div className='mx_MJitsiWidgetEvent_title'>
|
title={_t('Video conference ended by %(senderName)s', {senderName})}
|
||||||
{_t('Video conference ended by %(senderName)s', {senderName})}
|
/>;
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (prevUrl) {
|
} else if (prevUrl) {
|
||||||
// modified
|
// modified
|
||||||
return (
|
return <EventTileBubble
|
||||||
<div className='mx_EventTile_bubble mx_MJitsiWidgetEvent'>
|
className="mx_MJitsiWidgetEvent"
|
||||||
<div className='mx_MJitsiWidgetEvent_title'>
|
title={_t('Video conference updated by %(senderName)s', {senderName})}
|
||||||
{_t('Video conference updated by %(senderName)s', {senderName})}
|
subtitle={joinCopy}
|
||||||
</div>
|
/>;
|
||||||
<div className='mx_MJitsiWidgetEvent_subtitle'>
|
|
||||||
{joinCopy}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
// assume added
|
// assume added
|
||||||
return (
|
return <EventTileBubble
|
||||||
<div className='mx_EventTile_bubble mx_MJitsiWidgetEvent'>
|
className="mx_MJitsiWidgetEvent"
|
||||||
<div className='mx_MJitsiWidgetEvent_title'>
|
title={_t("Video conference started by %(senderName)s", {senderName})}
|
||||||
{_t("Video conference started by %(senderName)s", {senderName})}
|
subtitle={joinCopy}
|
||||||
</div>
|
/>;
|
||||||
<div className='mx_MJitsiWidgetEvent_subtitle'>
|
|
||||||
{joinCopy}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import {getNameForEventRoom, userLabelForEventRoom}
|
import {getNameForEventRoom, userLabelForEventRoom}
|
||||||
from '../../../utils/KeyVerificationStateObserver';
|
from '../../../utils/KeyVerificationStateObserver';
|
||||||
|
import EventTileBubble from "./EventTileBubble";
|
||||||
|
|
||||||
export default class MKeyVerificationConclusion extends React.Component {
|
export default class MKeyVerificationConclusion extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -115,14 +116,14 @@ export default class MKeyVerificationConclusion extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (title) {
|
if (title) {
|
||||||
const subtitle = userLabelForEventRoom(request.otherUserId, mxEvent.getRoomId());
|
const classes = classNames("mx_cryptoEvent mx_cryptoEvent_icon", {
|
||||||
const classes = classNames("mx_EventTile_bubble", "mx_cryptoEvent", "mx_cryptoEvent_icon", {
|
|
||||||
mx_cryptoEvent_icon_verified: request.done,
|
mx_cryptoEvent_icon_verified: request.done,
|
||||||
});
|
});
|
||||||
return (<div className={classes}>
|
return <EventTileBubble
|
||||||
<div className="mx_cryptoEvent_title">{title}</div>
|
className={classes}
|
||||||
<div className="mx_cryptoEvent_subtitle">{subtitle}</div>
|
title={title}
|
||||||
</div>);
|
subtitle={userLabelForEventRoom(request.otherUserId, mxEvent.getRoomId())}
|
||||||
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {getNameForEventRoom, userLabelForEventRoom}
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||||
import {Action} from "../../../dispatcher/actions";
|
import {Action} from "../../../dispatcher/actions";
|
||||||
|
import EventTileBubble from "./EventTileBubble";
|
||||||
|
|
||||||
export default class MKeyVerificationRequest extends React.Component {
|
export default class MKeyVerificationRequest extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -146,10 +147,8 @@ export default class MKeyVerificationRequest extends React.Component {
|
||||||
|
|
||||||
if (!request.initiatedByMe) {
|
if (!request.initiatedByMe) {
|
||||||
const name = getNameForEventRoom(request.requestingUserId, mxEvent.getRoomId());
|
const name = getNameForEventRoom(request.requestingUserId, mxEvent.getRoomId());
|
||||||
title = (<div className="mx_cryptoEvent_title">{
|
title = _t("%(name)s wants to verify", {name});
|
||||||
_t("%(name)s wants to verify", {name})}</div>);
|
subtitle = userLabelForEventRoom(request.requestingUserId, mxEvent.getRoomId());
|
||||||
subtitle = (<div className="mx_cryptoEvent_subtitle">{
|
|
||||||
userLabelForEventRoom(request.requestingUserId, mxEvent.getRoomId())}</div>);
|
|
||||||
if (request.canAccept) {
|
if (request.canAccept) {
|
||||||
stateNode = (<div className="mx_cryptoEvent_buttons">
|
stateNode = (<div className="mx_cryptoEvent_buttons">
|
||||||
<FormButton kind="danger" onClick={this._onRejectClicked} label={_t("Decline")} />
|
<FormButton kind="danger" onClick={this._onRejectClicked} label={_t("Decline")} />
|
||||||
|
@ -157,18 +156,18 @@ export default class MKeyVerificationRequest extends React.Component {
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
} else { // request sent by us
|
} else { // request sent by us
|
||||||
title = (<div className="mx_cryptoEvent_title">{
|
title = _t("You sent a verification request");
|
||||||
_t("You sent a verification request")}</div>);
|
subtitle = userLabelForEventRoom(request.receivingUserId, mxEvent.getRoomId());
|
||||||
subtitle = (<div className="mx_cryptoEvent_subtitle">{
|
|
||||||
userLabelForEventRoom(request.receivingUserId, mxEvent.getRoomId())}</div>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (title) {
|
if (title) {
|
||||||
return (<div className="mx_EventTile_bubble mx_cryptoEvent mx_cryptoEvent_icon">
|
return <EventTileBubble
|
||||||
{title}
|
className="mx_cryptoEvent mx_cryptoEvent_icon"
|
||||||
{subtitle}
|
title={title}
|
||||||
{stateNode}
|
subtitle={subtitle}
|
||||||
</div>);
|
>
|
||||||
|
{ stateNode }
|
||||||
|
</EventTileBubble>;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import dis from '../../../dispatcher/dispatcher';
|
||||||
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||||
|
import EventTileBubble from "./EventTileBubble";
|
||||||
|
|
||||||
export default class RoomCreate extends React.Component {
|
export default class RoomCreate extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -51,17 +52,16 @@ export default class RoomCreate extends React.Component {
|
||||||
const permalinkCreator = new RoomPermalinkCreator(prevRoom, predecessor['room_id']);
|
const permalinkCreator = new RoomPermalinkCreator(prevRoom, predecessor['room_id']);
|
||||||
permalinkCreator.load();
|
permalinkCreator.load();
|
||||||
const predecessorPermalink = permalinkCreator.forEvent(predecessor['event_id']);
|
const predecessorPermalink = permalinkCreator.forEvent(predecessor['event_id']);
|
||||||
return <div className="mx_CreateEvent">
|
const link = (
|
||||||
<div className="mx_CreateEvent_image" />
|
<a href={predecessorPermalink} onClick={this._onLinkClicked}>
|
||||||
<div className="mx_CreateEvent_header">
|
|
||||||
{_t("This room is a continuation of another conversation.")}
|
|
||||||
</div>
|
|
||||||
<a className="mx_CreateEvent_link"
|
|
||||||
href={predecessorPermalink}
|
|
||||||
onClick={this._onLinkClicked}
|
|
||||||
>
|
|
||||||
{_t("Click here to see older messages.")}
|
{_t("Click here to see older messages.")}
|
||||||
</a>
|
</a>
|
||||||
</div>;
|
);
|
||||||
|
|
||||||
|
return <EventTileBubble
|
||||||
|
className="mx_CreateEvent"
|
||||||
|
title={_t("This room is a continuation of another conversation.")}
|
||||||
|
subtitle={link}
|
||||||
|
/>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import {_t} from '../../../languageHandler';
|
import {_t} from '../../../languageHandler';
|
||||||
import createRoom, {privateShouldBeEncrypted} from '../../../createRoom';
|
import createRoom, { findDMForUser, privateShouldBeEncrypted } from '../../../createRoom';
|
||||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
|
@ -51,7 +51,6 @@ import BaseCard from "./BaseCard";
|
||||||
import {E2EStatus} from "../../../utils/ShieldUtils";
|
import {E2EStatus} from "../../../utils/ShieldUtils";
|
||||||
import ImageView from "../elements/ImageView";
|
import ImageView from "../elements/ImageView";
|
||||||
import Spinner from "../elements/Spinner";
|
import Spinner from "../elements/Spinner";
|
||||||
import IconButton from "../elements/IconButton";
|
|
||||||
import PowerSelector from "../elements/PowerSelector";
|
import PowerSelector from "../elements/PowerSelector";
|
||||||
import MemberAvatar from "../avatars/MemberAvatar";
|
import MemberAvatar from "../avatars/MemberAvatar";
|
||||||
import PresenceLabel from "../rooms/PresenceLabel";
|
import PresenceLabel from "../rooms/PresenceLabel";
|
||||||
|
@ -106,17 +105,7 @@ export const getE2EStatus = (cli: MatrixClient, userId: string, devices: IDevice
|
||||||
};
|
};
|
||||||
|
|
||||||
async function openDMForUser(matrixClient: MatrixClient, userId: string) {
|
async function openDMForUser(matrixClient: MatrixClient, userId: string) {
|
||||||
const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId);
|
const lastActiveRoom = findDMForUser(matrixClient, userId);
|
||||||
const lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => {
|
|
||||||
const room = matrixClient.getRoom(roomId);
|
|
||||||
if (!room || room.getMyMembership() === "leave") {
|
|
||||||
return lastActiveRoom;
|
|
||||||
}
|
|
||||||
if (!lastActiveRoom || lastActiveRoom.getLastActiveTimestamp() < room.getLastActiveTimestamp()) {
|
|
||||||
return room;
|
|
||||||
}
|
|
||||||
return lastActiveRoom;
|
|
||||||
}, null);
|
|
||||||
|
|
||||||
if (lastActiveRoom) {
|
if (lastActiveRoom) {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
|
@ -1028,24 +1017,15 @@ const PowerLevelSection: React.FC<{
|
||||||
roomPermissions: IRoomPermissions;
|
roomPermissions: IRoomPermissions;
|
||||||
powerLevels: IPowerLevelsContent;
|
powerLevels: IPowerLevelsContent;
|
||||||
}> = ({user, room, roomPermissions, powerLevels}) => {
|
}> = ({user, room, roomPermissions, powerLevels}) => {
|
||||||
const [isEditing, setEditing] = useState(false);
|
if (roomPermissions.canEdit) {
|
||||||
if (isEditing) {
|
return (<PowerLevelEditor user={user} room={room} roomPermissions={roomPermissions} />);
|
||||||
return (<PowerLevelEditor
|
|
||||||
user={user} room={room} roomPermissions={roomPermissions}
|
|
||||||
onFinished={() => setEditing(false)} />);
|
|
||||||
} else {
|
} else {
|
||||||
const powerLevelUsersDefault = powerLevels.users_default || 0;
|
const powerLevelUsersDefault = powerLevels.users_default || 0;
|
||||||
const powerLevel = parseInt(user.powerLevel, 10);
|
const powerLevel = parseInt(user.powerLevel, 10);
|
||||||
const modifyButton = roomPermissions.canEdit ?
|
|
||||||
(<IconButton icon="edit" onClick={() => setEditing(true)} />) : null;
|
|
||||||
const role = textualPowerLevel(powerLevel, powerLevelUsersDefault);
|
const role = textualPowerLevel(powerLevel, powerLevelUsersDefault);
|
||||||
const label = _t("<strong>%(role)s</strong> in %(roomName)s",
|
|
||||||
{role, roomName: room.name},
|
|
||||||
{strong: label => <strong>{label}</strong>},
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_UserInfo_profileField">
|
<div className="mx_UserInfo_profileField">
|
||||||
<div className="mx_UserInfo_roleDescription">{label}{modifyButton}</div>
|
<div className="mx_UserInfo_roleDescription">{role}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1055,20 +1035,15 @@ const PowerLevelEditor: React.FC<{
|
||||||
user: User;
|
user: User;
|
||||||
room: Room;
|
room: Room;
|
||||||
roomPermissions: IRoomPermissions;
|
roomPermissions: IRoomPermissions;
|
||||||
onFinished(): void;
|
}> = ({user, room, roomPermissions}) => {
|
||||||
}> = ({user, room, roomPermissions, onFinished}) => {
|
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
|
|
||||||
const [isUpdating, setIsUpdating] = useState(false);
|
|
||||||
const [selectedPowerLevel, setSelectedPowerLevel] = useState(parseInt(user.powerLevel, 10));
|
const [selectedPowerLevel, setSelectedPowerLevel] = useState(parseInt(user.powerLevel, 10));
|
||||||
const [isDirty, setIsDirty] = useState(false);
|
const onPowerChange = useCallback(async (powerLevelStr: string) => {
|
||||||
const onPowerChange = useCallback((powerLevel) => {
|
const powerLevel = parseInt(powerLevelStr, 10);
|
||||||
setIsDirty(true);
|
setSelectedPowerLevel(powerLevel);
|
||||||
setSelectedPowerLevel(parseInt(powerLevel, 10));
|
|
||||||
}, [setSelectedPowerLevel, setIsDirty]);
|
|
||||||
|
|
||||||
const changePowerLevel = useCallback(async () => {
|
const applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => {
|
||||||
const _applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => {
|
|
||||||
return cli.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then(
|
return cli.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then(
|
||||||
function() {
|
function() {
|
||||||
// NO-OP; rely on the m.room.member event coming down else we could
|
// NO-OP; rely on the m.room.member event coming down else we could
|
||||||
|
@ -1084,64 +1059,42 @@ const PowerLevelEditor: React.FC<{
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
const roomId = user.roomId;
|
||||||
if (!isDirty) {
|
const target = user.userId;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsUpdating(true);
|
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||||
|
if (!powerLevelEvent) return;
|
||||||
|
|
||||||
const powerLevel = selectedPowerLevel;
|
const myUserId = cli.getUserId();
|
||||||
|
const myPower = powerLevelEvent.getContent().users[myUserId];
|
||||||
|
if (myPower && parseInt(myPower) === powerLevel) {
|
||||||
|
const {finished} = Modal.createTrackedDialog('Promote to PL100 Warning', '', QuestionDialog, {
|
||||||
|
title: _t("Warning!"),
|
||||||
|
description:
|
||||||
|
<div>
|
||||||
|
{ _t("You will not be able to undo this change as you are promoting the user " +
|
||||||
|
"to have the same power level as yourself.") }<br />
|
||||||
|
{ _t("Are you sure?") }
|
||||||
|
</div>,
|
||||||
|
button: _t("Continue"),
|
||||||
|
});
|
||||||
|
|
||||||
const roomId = user.roomId;
|
const [confirmed] = await finished;
|
||||||
const target = user.userId;
|
if (!confirmed) return;
|
||||||
|
} else if (myUserId === target) {
|
||||||
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
|
|
||||||
if (!powerLevelEvent) return;
|
|
||||||
|
|
||||||
if (!powerLevelEvent.getContent().users) {
|
|
||||||
_applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const myUserId = cli.getUserId();
|
|
||||||
// If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
|
// If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
|
||||||
if (myUserId === target) {
|
try {
|
||||||
try {
|
if (!(await warnSelfDemote())) return;
|
||||||
if (!(await warnSelfDemote())) return;
|
} catch (e) {
|
||||||
} catch (e) {
|
console.error("Failed to warn about self demotion: ", e);
|
||||||
console.error("Failed to warn about self demotion: ", e);
|
|
||||||
}
|
|
||||||
await _applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const myPower = powerLevelEvent.getContent().users[myUserId];
|
|
||||||
if (parseInt(myPower) === powerLevel) {
|
|
||||||
const {finished} = Modal.createTrackedDialog('Promote to PL100 Warning', '', QuestionDialog, {
|
|
||||||
title: _t("Warning!"),
|
|
||||||
description:
|
|
||||||
<div>
|
|
||||||
{ _t("You will not be able to undo this change as you are promoting the user " +
|
|
||||||
"to have the same power level as yourself.") }<br />
|
|
||||||
{ _t("Are you sure?") }
|
|
||||||
</div>,
|
|
||||||
button: _t("Continue"),
|
|
||||||
});
|
|
||||||
|
|
||||||
const [confirmed] = await finished;
|
|
||||||
if (!confirmed) return;
|
|
||||||
}
|
|
||||||
await _applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
|
|
||||||
} finally {
|
|
||||||
onFinished();
|
|
||||||
}
|
}
|
||||||
}, [user.roomId, user.userId, cli, selectedPowerLevel, isDirty, setIsUpdating, onFinished, room]);
|
|
||||||
|
await applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
|
||||||
|
}, [user.roomId, user.userId, cli, room]);
|
||||||
|
|
||||||
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
|
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||||
const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0;
|
const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0;
|
||||||
const buttonOrSpinner = isUpdating ? <Spinner w={16} h={16} /> :
|
|
||||||
<IconButton icon="check" onClick={changePowerLevel} />;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_UserInfo_profileField">
|
<div className="mx_UserInfo_profileField">
|
||||||
|
@ -1151,9 +1104,7 @@ const PowerLevelEditor: React.FC<{
|
||||||
maxValue={roomPermissions.modifyLevelMax}
|
maxValue={roomPermissions.modifyLevelMax}
|
||||||
usersDefault={powerLevelUsersDefault}
|
usersDefault={powerLevelUsersDefault}
|
||||||
onChange={onPowerChange}
|
onChange={onPowerChange}
|
||||||
disabled={isUpdating}
|
|
||||||
/>
|
/>
|
||||||
{buttonOrSpinner}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1343,13 +1294,17 @@ const BasicUserInfo: React.FC<{
|
||||||
}
|
}
|
||||||
|
|
||||||
let memberDetails;
|
let memberDetails;
|
||||||
if (room && member.roomId) {
|
// hide the Roles section for DMs as it doesn't make sense there
|
||||||
memberDetails = <PowerLevelSection
|
if (room && member.roomId && !DMRoomMap.shared().getUserIdForRoomId(member.roomId)) {
|
||||||
powerLevels={powerLevels}
|
memberDetails = <div className="mx_UserInfo_container">
|
||||||
user={member}
|
<h3>{ _t("Role") }</h3>
|
||||||
room={room}
|
<PowerLevelSection
|
||||||
roomPermissions={roomPermissions}
|
powerLevels={powerLevels}
|
||||||
/>;
|
user={member}
|
||||||
|
room={room}
|
||||||
|
roomPermissions={roomPermissions}
|
||||||
|
/>
|
||||||
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// only display the devices list if our client supports E2E
|
// only display the devices list if our client supports E2E
|
||||||
|
@ -1419,12 +1374,7 @@ const BasicUserInfo: React.FC<{
|
||||||
);
|
);
|
||||||
|
|
||||||
return <React.Fragment>
|
return <React.Fragment>
|
||||||
{ memberDetails &&
|
{ memberDetails }
|
||||||
<div className="mx_UserInfo_container mx_UserInfo_separator mx_UserInfo_memberDetailsContainer">
|
|
||||||
<div className="mx_UserInfo_memberDetails">
|
|
||||||
{ memberDetails }
|
|
||||||
</div>
|
|
||||||
</div> }
|
|
||||||
|
|
||||||
{ securitySection }
|
{ securitySection }
|
||||||
<UserOptionsSection
|
<UserOptionsSection
|
||||||
|
|
|
@ -21,6 +21,7 @@ import ReplyThread from "../elements/ReplyThread";
|
||||||
import React, {createRef} from 'react';
|
import React, {createRef} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import {EventType} from "matrix-js-sdk/src/@types/event";
|
||||||
import { _t, _td } from '../../../languageHandler';
|
import { _t, _td } from '../../../languageHandler';
|
||||||
import * as TextForEvent from "../../../TextForEvent";
|
import * as TextForEvent from "../../../TextForEvent";
|
||||||
import * as sdk from "../../../index";
|
import * as sdk from "../../../index";
|
||||||
|
@ -646,12 +647,13 @@ export default class EventTile extends React.Component {
|
||||||
|
|
||||||
// Info messages are basically information about commands processed on a room
|
// Info messages are basically information about commands processed on a room
|
||||||
const isBubbleMessage = eventType.startsWith("m.key.verification") ||
|
const isBubbleMessage = eventType.startsWith("m.key.verification") ||
|
||||||
(eventType === "m.room.message" && msgtype && msgtype.startsWith("m.key.verification")) ||
|
(eventType === EventType.RoomMessage && msgtype && msgtype.startsWith("m.key.verification")) ||
|
||||||
(eventType === "m.room.encryption") ||
|
(eventType === EventType.RoomCreate) ||
|
||||||
|
(eventType === EventType.RoomEncryption) ||
|
||||||
(tileHandler === "messages.MJitsiWidgetEvent");
|
(tileHandler === "messages.MJitsiWidgetEvent");
|
||||||
let isInfoMessage = (
|
let isInfoMessage = (
|
||||||
!isBubbleMessage && eventType !== 'm.room.message' &&
|
!isBubbleMessage && eventType !== EventType.RoomMessage &&
|
||||||
eventType !== 'm.sticker' && eventType !== 'm.room.create'
|
eventType !== EventType.Sticker && eventType !== EventType.RoomCreate
|
||||||
);
|
);
|
||||||
|
|
||||||
// If we're showing hidden events in the timeline, we should use the
|
// If we're showing hidden events in the timeline, we should use the
|
||||||
|
|
135
src/components/views/rooms/NewRoomIntro.tsx
Normal file
135
src/components/views/rooms/NewRoomIntro.tsx
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, {useContext} from "react";
|
||||||
|
import {EventType} from "matrix-js-sdk/src/@types/event";
|
||||||
|
|
||||||
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
import RoomContext from "../../../contexts/RoomContext";
|
||||||
|
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||||
|
import {_t} from "../../../languageHandler";
|
||||||
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
|
import MiniAvatarUploader, {AVATAR_SIZE} from "../elements/MiniAvatarUploader";
|
||||||
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
|
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||||
|
import {ViewUserPayload} from "../../../dispatcher/payloads/ViewUserPayload";
|
||||||
|
import {Action} from "../../../dispatcher/actions";
|
||||||
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
|
|
||||||
|
const NewRoomIntro = () => {
|
||||||
|
const cli = useContext(MatrixClientContext);
|
||||||
|
const {room, roomId} = useContext(RoomContext);
|
||||||
|
|
||||||
|
const dmPartner = DMRoomMap.shared().getUserIdForRoomId(roomId);
|
||||||
|
let body;
|
||||||
|
if (dmPartner) {
|
||||||
|
let caption;
|
||||||
|
if ((room.getJoinedMemberCount() + room.getInvitedMemberCount()) === 2) {
|
||||||
|
caption = _t("Only the two of you are in this conversation, unless either of you invites anyone to join.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const member = room?.getMember(dmPartner);
|
||||||
|
const displayName = member?.rawDisplayName || dmPartner;
|
||||||
|
body = <React.Fragment>
|
||||||
|
<RoomAvatar room={room} width={AVATAR_SIZE} height={AVATAR_SIZE} onClick={() => {
|
||||||
|
defaultDispatcher.dispatch<ViewUserPayload>({
|
||||||
|
action: Action.ViewUser,
|
||||||
|
// XXX: We should be using a real member object and not assuming what the receiver wants.
|
||||||
|
member: member || {userId: dmPartner},
|
||||||
|
});
|
||||||
|
}} />
|
||||||
|
|
||||||
|
<h2>{ room.name }</h2>
|
||||||
|
|
||||||
|
<p>{_t("This is the beginning of your direct message history with <displayName/>.", {}, {
|
||||||
|
displayName: () => <b>{ displayName }</b>,
|
||||||
|
})}</p>
|
||||||
|
{ caption && <p>{ caption }</p> }
|
||||||
|
</React.Fragment>;
|
||||||
|
} else {
|
||||||
|
const topic = room.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic;
|
||||||
|
const canAddTopic = room.currentState.maySendStateEvent(EventType.RoomTopic, cli.getUserId());
|
||||||
|
|
||||||
|
const onTopicClick = () => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: "open_room_settings",
|
||||||
|
room_id: roomId,
|
||||||
|
}, true);
|
||||||
|
// focus the topic field to help the user find it as it'll gain an outline
|
||||||
|
setImmediate(() => {
|
||||||
|
window.document.getElementById("profileTopic").focus();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let topicText;
|
||||||
|
if (canAddTopic && topic) {
|
||||||
|
topicText = _t("Topic: %(topic)s (<a>edit</a>)", { topic }, {
|
||||||
|
a: sub => <AccessibleButton kind="link" onClick={onTopicClick}>{ sub }</AccessibleButton>,
|
||||||
|
});
|
||||||
|
} else if (topic) {
|
||||||
|
topicText = _t("Topic: %(topic)s ", { topic });
|
||||||
|
} else if (canAddTopic) {
|
||||||
|
topicText = _t("<a>Add a topic</a> to help people know what it is about.", {}, {
|
||||||
|
a: sub => <AccessibleButton kind="link" onClick={onTopicClick}>{ sub }</AccessibleButton>,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const creator = room.currentState.getStateEvents(EventType.RoomCreate, "")?.getSender();
|
||||||
|
const creatorName = room?.getMember(creator)?.rawDisplayName || creator;
|
||||||
|
|
||||||
|
let createdText;
|
||||||
|
if (creator === cli.getUserId()) {
|
||||||
|
createdText = _t("You created this room.");
|
||||||
|
} else {
|
||||||
|
createdText = _t("%(displayName)s created this room.", {
|
||||||
|
displayName: creatorName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const onInviteClick = () => {
|
||||||
|
dis.dispatch({ action: "view_invite", roomId });
|
||||||
|
};
|
||||||
|
|
||||||
|
const avatarUrl = room.currentState.getStateEvents(EventType.RoomAvatar, "")?.getContent()?.url;
|
||||||
|
body = <React.Fragment>
|
||||||
|
<MiniAvatarUploader
|
||||||
|
hasAvatar={!!avatarUrl}
|
||||||
|
noAvatarLabel={_t("Add a photo, so people can easily spot your room.")}
|
||||||
|
setAvatarUrl={url => cli.sendStateEvent(roomId, EventType.RoomAvatar, { url }, '')}
|
||||||
|
>
|
||||||
|
<RoomAvatar room={room} width={AVATAR_SIZE} height={AVATAR_SIZE} />
|
||||||
|
</MiniAvatarUploader>
|
||||||
|
|
||||||
|
<h2>{ room.name }</h2>
|
||||||
|
|
||||||
|
<p>{createdText} {_t("This is the start of <roomName/>.", {}, {
|
||||||
|
roomName: () => <b>{ room.name }</b>,
|
||||||
|
})}</p>
|
||||||
|
<p>{topicText}</p>
|
||||||
|
<div className="mx_NewRoomIntro_buttons">
|
||||||
|
<AccessibleButton className="mx_NewRoomIntro_inviteButton" kind="primary" onClick={onInviteClick}>
|
||||||
|
{_t("Invite to this room")}
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
</React.Fragment>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="mx_NewRoomIntro">
|
||||||
|
{ body }
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewRoomIntro;
|
|
@ -58,6 +58,7 @@ interface IProps {
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
sublists: ITagMap;
|
sublists: ITagMap;
|
||||||
|
isNameFiltering: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TAG_ORDER: TagID[] = [
|
const TAG_ORDER: TagID[] = [
|
||||||
|
@ -183,6 +184,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
sublists: {},
|
sublists: {},
|
||||||
|
isNameFiltering: !!RoomListStore.instance.getFirstNameFilterCondition(),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||||
|
@ -253,7 +255,8 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||||
return CustomRoomTagStore.getTags()[t];
|
return CustomRoomTagStore.getTags()[t];
|
||||||
});
|
});
|
||||||
|
|
||||||
let doUpdate = arrayHasDiff(previousListIds, newListIds);
|
const isNameFiltering = !!RoomListStore.instance.getFirstNameFilterCondition();
|
||||||
|
let doUpdate = this.state.isNameFiltering !== isNameFiltering || arrayHasDiff(previousListIds, newListIds);
|
||||||
if (!doUpdate) {
|
if (!doUpdate) {
|
||||||
// so we didn't have the visible sublists change, but did the contents of those
|
// so we didn't have the visible sublists change, but did the contents of those
|
||||||
// sublists change significantly enough to break the sticky headers? Probably, so
|
// sublists change significantly enough to break the sticky headers? Probably, so
|
||||||
|
@ -275,14 +278,20 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||||
const newSublists = objectWithOnly(newLists, newListIds);
|
const newSublists = objectWithOnly(newLists, newListIds);
|
||||||
const sublists = objectShallowClone(newSublists, (k, v) => arrayFastClone(v));
|
const sublists = objectShallowClone(newSublists, (k, v) => arrayFastClone(v));
|
||||||
|
|
||||||
this.setState({sublists}, () => {
|
this.setState({sublists, isNameFiltering}, () => {
|
||||||
this.props.onResize();
|
this.props.onResize();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onStartChat = () => {
|
||||||
|
const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search;
|
||||||
|
dis.dispatch({ action: "view_create_chat", initialText });
|
||||||
|
};
|
||||||
|
|
||||||
private onExplore = () => {
|
private onExplore = () => {
|
||||||
dis.fire(Action.ViewRoomDirectory);
|
const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search;
|
||||||
|
dis.dispatch({ action: Action.ViewRoomDirectory, initialText });
|
||||||
};
|
};
|
||||||
|
|
||||||
private renderCommunityInvites(): TemporaryTile[] {
|
private renderCommunityInvites(): TemporaryTile[] {
|
||||||
|
@ -332,7 +341,9 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||||
return p;
|
return p;
|
||||||
}, [] as TagID[]);
|
}, [] as TagID[]);
|
||||||
|
|
||||||
const showSkeleton = tagOrder.every(tag => !this.state.sublists[tag]?.length);
|
// show a skeleton UI if the user is in no rooms and they are not filtering
|
||||||
|
const showSkeleton = !this.state.isNameFiltering &&
|
||||||
|
Object.values(RoomListStore.instance.unfilteredLists).every(list => !list?.length);
|
||||||
|
|
||||||
for (const orderedTagId of tagOrder) {
|
for (const orderedTagId of tagOrder) {
|
||||||
const orderedRooms = this.state.sublists[orderedTagId] || [];
|
const orderedRooms = this.state.sublists[orderedTagId] || [];
|
||||||
|
@ -369,10 +380,21 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||||
public render() {
|
public render() {
|
||||||
let explorePrompt: JSX.Element;
|
let explorePrompt: JSX.Element;
|
||||||
if (!this.props.isMinimized) {
|
if (!this.props.isMinimized) {
|
||||||
if (RoomListStore.instance.getFirstNameFilterCondition()) {
|
if (this.state.isNameFiltering) {
|
||||||
explorePrompt = <div className="mx_RoomList_explorePrompt">
|
explorePrompt = <div className="mx_RoomList_explorePrompt">
|
||||||
<div>{_t("Can't see what you’re looking for?")}</div>
|
<div>{_t("Can't see what you’re looking for?")}</div>
|
||||||
<AccessibleButton kind="link" onClick={this.onExplore}>
|
<AccessibleButton
|
||||||
|
className="mx_RoomList_explorePrompt_startChat"
|
||||||
|
kind="link"
|
||||||
|
onClick={this.onStartChat}
|
||||||
|
>
|
||||||
|
{_t("Start a new chat")}
|
||||||
|
</AccessibleButton>
|
||||||
|
<AccessibleButton
|
||||||
|
className="mx_RoomList_explorePrompt_explore"
|
||||||
|
kind="link"
|
||||||
|
onClick={this.onExplore}
|
||||||
|
>
|
||||||
{_t("Explore all public rooms")}
|
{_t("Explore all public rooms")}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>;
|
</div>;
|
||||||
|
@ -384,7 +406,18 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||||
if (unfilteredRooms.length < 1 && unfilteredHistorical < 1) {
|
if (unfilteredRooms.length < 1 && unfilteredHistorical < 1) {
|
||||||
explorePrompt = <div className="mx_RoomList_explorePrompt">
|
explorePrompt = <div className="mx_RoomList_explorePrompt">
|
||||||
<div>{_t("Use the + to make a new room or explore existing ones below")}</div>
|
<div>{_t("Use the + to make a new room or explore existing ones below")}</div>
|
||||||
<AccessibleButton kind="link" onClick={this.onExplore}>
|
<AccessibleButton
|
||||||
|
className="mx_RoomList_explorePrompt_startChat"
|
||||||
|
kind="link"
|
||||||
|
onClick={this.onStartChat}
|
||||||
|
>
|
||||||
|
{_t("Start a new chat")}
|
||||||
|
</AccessibleButton>
|
||||||
|
<AccessibleButton
|
||||||
|
className="mx_RoomList_explorePrompt_explore"
|
||||||
|
kind="link"
|
||||||
|
onClick={this.onExplore}
|
||||||
|
>
|
||||||
{_t("Explore all public rooms")}
|
{_t("Explore all public rooms")}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>;
|
</div>;
|
||||||
|
|
|
@ -308,6 +308,9 @@ export default class SendMessageComposer extends React.Component {
|
||||||
const startTime = CountlyAnalytics.getTimestamp();
|
const startTime = CountlyAnalytics.getTimestamp();
|
||||||
const {roomId} = this.props.room;
|
const {roomId} = this.props.room;
|
||||||
const content = createMessageContent(this.model, this.props.permalinkCreator, replyToEvent);
|
const content = createMessageContent(this.model, this.props.permalinkCreator, replyToEvent);
|
||||||
|
// don't bother sending an empty message
|
||||||
|
if (!content.body.trim()) return;
|
||||||
|
|
||||||
const prom = this.context.sendMessage(roomId, content);
|
const prom = this.context.sendMessage(roomId, content);
|
||||||
if (replyToEvent) {
|
if (replyToEvent) {
|
||||||
// Clear reply_to_event as we put the message into the queue
|
// Clear reply_to_event as we put the message into the queue
|
||||||
|
|
|
@ -129,11 +129,13 @@ export default class EventIndexPanel extends React.Component {
|
||||||
eventIndexingSettings = (
|
eventIndexingSettings = (
|
||||||
<div>
|
<div>
|
||||||
<div className='mx_SettingsTab_subsectionText'>
|
<div className='mx_SettingsTab_subsectionText'>
|
||||||
{_t( "Securely cache encrypted messages locally for them " +
|
{_t("Securely cache encrypted messages locally for them " +
|
||||||
"to appear in search results, using ")
|
"to appear in search results, using %(size)s to store messages from %(count)s rooms.",
|
||||||
} {formatBytes(this.state.eventIndexSize, 0)}
|
{
|
||||||
{_t( " to store messages from ")}
|
size: formatBytes(this.state.eventIndexSize, 0),
|
||||||
{formatCountLong(this.state.roomCount)} {_t("rooms.")}
|
count: formatCountLong(this.state.roomCount),
|
||||||
|
},
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<AccessibleButton kind="primary" onClick={this._onManage}>
|
<AccessibleButton kind="primary" onClick={this._onManage}>
|
||||||
|
|
|
@ -29,7 +29,6 @@ const RoomContext = createContext<IState>({
|
||||||
guestsCanJoin: false,
|
guestsCanJoin: false,
|
||||||
canPeek: false,
|
canPeek: false,
|
||||||
showApps: false,
|
showApps: false,
|
||||||
isAlone: false,
|
|
||||||
isPeeking: false,
|
isPeeking: false,
|
||||||
showingPinned: false,
|
showingPinned: false,
|
||||||
showReadReceipts: true,
|
showReadReceipts: true,
|
||||||
|
|
|
@ -15,20 +15,21 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import {Room} from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import * as sdk from './index';
|
import * as sdk from './index';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
import dis from "./dispatcher/dispatcher";
|
import dis from "./dispatcher/dispatcher";
|
||||||
import * as Rooms from "./Rooms";
|
import * as Rooms from "./Rooms";
|
||||||
import DMRoomMap from "./utils/DMRoomMap";
|
import DMRoomMap from "./utils/DMRoomMap";
|
||||||
import {getAddressType} from "./UserAddress";
|
import { getAddressType } from "./UserAddress";
|
||||||
import { getE2EEWellKnown } from "./utils/WellKnownUtils";
|
import { getE2EEWellKnown } from "./utils/WellKnownUtils";
|
||||||
import GroupStore from "./stores/GroupStore";
|
import GroupStore from "./stores/GroupStore";
|
||||||
import CountlyAnalytics from "./CountlyAnalytics";
|
import CountlyAnalytics from "./CountlyAnalytics";
|
||||||
|
import { isJoinedOrNearlyJoined } from "./utils/membership";
|
||||||
|
|
||||||
// we define a number of interfaces which take their names from the js-sdk
|
// we define a number of interfaces which take their names from the js-sdk
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
|
@ -236,9 +237,16 @@ export function findDMForUser(client: MatrixClient, userId: string): Room {
|
||||||
const roomIds = DMRoomMap.shared().getDMRoomsForUserId(userId);
|
const roomIds = DMRoomMap.shared().getDMRoomsForUserId(userId);
|
||||||
const rooms = roomIds.map(id => client.getRoom(id));
|
const rooms = roomIds.map(id => client.getRoom(id));
|
||||||
const suitableDMRooms = rooms.filter(r => {
|
const suitableDMRooms = rooms.filter(r => {
|
||||||
|
// Validate that we are joined and the other person is also joined. We'll also make sure
|
||||||
|
// that the room also looks like a DM (until we have canonical DMs to tell us). For now,
|
||||||
|
// a DM is a room of two people that contains those two people exactly. This does mean
|
||||||
|
// that bots, assistants, etc will ruin a room's DM-ness, though this is a problem for
|
||||||
|
// canonical DMs to solve.
|
||||||
if (r && r.getMyMembership() === "join") {
|
if (r && r.getMyMembership() === "join") {
|
||||||
const member = r.getMember(userId);
|
const members = r.currentState.getMembers();
|
||||||
return member && (member.membership === "invite" || member.membership === "join");
|
const joinedMembers = members.filter(m => isJoinedOrNearlyJoined(m.membership));
|
||||||
|
const otherMember = joinedMembers.find(m => m.userId === userId);
|
||||||
|
return otherMember && joinedMembers.length === 2;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}).sort((r1, r2) => {
|
}).sort((r1, r2) => {
|
||||||
|
|
|
@ -117,6 +117,255 @@
|
||||||
"Unable to enable Notifications": "Unable to enable Notifications",
|
"Unable to enable Notifications": "Unable to enable Notifications",
|
||||||
"This email address was not found": "This email address was not found",
|
"This email address was not found": "This email address was not found",
|
||||||
"Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Your email address does not appear to be associated with a Matrix ID on this Homeserver.",
|
"Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Your email address does not appear to be associated with a Matrix ID on this Homeserver.",
|
||||||
|
"United Kingdom": "United Kingdom",
|
||||||
|
"United States": "United States",
|
||||||
|
"Afghanistan": "Afghanistan",
|
||||||
|
"Åland Islands": "Åland Islands",
|
||||||
|
"Albania": "Albania",
|
||||||
|
"Algeria": "Algeria",
|
||||||
|
"American Samoa": "American Samoa",
|
||||||
|
"Andorra": "Andorra",
|
||||||
|
"Angola": "Angola",
|
||||||
|
"Anguilla": "Anguilla",
|
||||||
|
"Antarctica": "Antarctica",
|
||||||
|
"Antigua & Barbuda": "Antigua & Barbuda",
|
||||||
|
"Argentina": "Argentina",
|
||||||
|
"Armenia": "Armenia",
|
||||||
|
"Aruba": "Aruba",
|
||||||
|
"Australia": "Australia",
|
||||||
|
"Austria": "Austria",
|
||||||
|
"Azerbaijan": "Azerbaijan",
|
||||||
|
"Bahamas": "Bahamas",
|
||||||
|
"Bahrain": "Bahrain",
|
||||||
|
"Bangladesh": "Bangladesh",
|
||||||
|
"Barbados": "Barbados",
|
||||||
|
"Belarus": "Belarus",
|
||||||
|
"Belgium": "Belgium",
|
||||||
|
"Belize": "Belize",
|
||||||
|
"Benin": "Benin",
|
||||||
|
"Bermuda": "Bermuda",
|
||||||
|
"Bhutan": "Bhutan",
|
||||||
|
"Bolivia": "Bolivia",
|
||||||
|
"Bosnia": "Bosnia",
|
||||||
|
"Botswana": "Botswana",
|
||||||
|
"Bouvet Island": "Bouvet Island",
|
||||||
|
"Brazil": "Brazil",
|
||||||
|
"British Indian Ocean Territory": "British Indian Ocean Territory",
|
||||||
|
"British Virgin Islands": "British Virgin Islands",
|
||||||
|
"Brunei": "Brunei",
|
||||||
|
"Bulgaria": "Bulgaria",
|
||||||
|
"Burkina Faso": "Burkina Faso",
|
||||||
|
"Burundi": "Burundi",
|
||||||
|
"Cambodia": "Cambodia",
|
||||||
|
"Cameroon": "Cameroon",
|
||||||
|
"Canada": "Canada",
|
||||||
|
"Cape Verde": "Cape Verde",
|
||||||
|
"Caribbean Netherlands": "Caribbean Netherlands",
|
||||||
|
"Cayman Islands": "Cayman Islands",
|
||||||
|
"Central African Republic": "Central African Republic",
|
||||||
|
"Chad": "Chad",
|
||||||
|
"Chile": "Chile",
|
||||||
|
"China": "China",
|
||||||
|
"Christmas Island": "Christmas Island",
|
||||||
|
"Cocos (Keeling) Islands": "Cocos (Keeling) Islands",
|
||||||
|
"Colombia": "Colombia",
|
||||||
|
"Comoros": "Comoros",
|
||||||
|
"Congo - Brazzaville": "Congo - Brazzaville",
|
||||||
|
"Congo - Kinshasa": "Congo - Kinshasa",
|
||||||
|
"Cook Islands": "Cook Islands",
|
||||||
|
"Costa Rica": "Costa Rica",
|
||||||
|
"Croatia": "Croatia",
|
||||||
|
"Cuba": "Cuba",
|
||||||
|
"Curaçao": "Curaçao",
|
||||||
|
"Cyprus": "Cyprus",
|
||||||
|
"Czech Republic": "Czech Republic",
|
||||||
|
"Côte d’Ivoire": "Côte d’Ivoire",
|
||||||
|
"Denmark": "Denmark",
|
||||||
|
"Djibouti": "Djibouti",
|
||||||
|
"Dominica": "Dominica",
|
||||||
|
"Dominican Republic": "Dominican Republic",
|
||||||
|
"Ecuador": "Ecuador",
|
||||||
|
"Egypt": "Egypt",
|
||||||
|
"El Salvador": "El Salvador",
|
||||||
|
"Equatorial Guinea": "Equatorial Guinea",
|
||||||
|
"Eritrea": "Eritrea",
|
||||||
|
"Estonia": "Estonia",
|
||||||
|
"Ethiopia": "Ethiopia",
|
||||||
|
"Falkland Islands": "Falkland Islands",
|
||||||
|
"Faroe Islands": "Faroe Islands",
|
||||||
|
"Fiji": "Fiji",
|
||||||
|
"Finland": "Finland",
|
||||||
|
"France": "France",
|
||||||
|
"French Guiana": "French Guiana",
|
||||||
|
"French Polynesia": "French Polynesia",
|
||||||
|
"French Southern Territories": "French Southern Territories",
|
||||||
|
"Gabon": "Gabon",
|
||||||
|
"Gambia": "Gambia",
|
||||||
|
"Georgia": "Georgia",
|
||||||
|
"Germany": "Germany",
|
||||||
|
"Ghana": "Ghana",
|
||||||
|
"Gibraltar": "Gibraltar",
|
||||||
|
"Greece": "Greece",
|
||||||
|
"Greenland": "Greenland",
|
||||||
|
"Grenada": "Grenada",
|
||||||
|
"Guadeloupe": "Guadeloupe",
|
||||||
|
"Guam": "Guam",
|
||||||
|
"Guatemala": "Guatemala",
|
||||||
|
"Guernsey": "Guernsey",
|
||||||
|
"Guinea": "Guinea",
|
||||||
|
"Guinea-Bissau": "Guinea-Bissau",
|
||||||
|
"Guyana": "Guyana",
|
||||||
|
"Haiti": "Haiti",
|
||||||
|
"Heard & McDonald Islands": "Heard & McDonald Islands",
|
||||||
|
"Honduras": "Honduras",
|
||||||
|
"Hong Kong": "Hong Kong",
|
||||||
|
"Hungary": "Hungary",
|
||||||
|
"Iceland": "Iceland",
|
||||||
|
"India": "India",
|
||||||
|
"Indonesia": "Indonesia",
|
||||||
|
"Iran": "Iran",
|
||||||
|
"Iraq": "Iraq",
|
||||||
|
"Ireland": "Ireland",
|
||||||
|
"Isle of Man": "Isle of Man",
|
||||||
|
"Israel": "Israel",
|
||||||
|
"Italy": "Italy",
|
||||||
|
"Jamaica": "Jamaica",
|
||||||
|
"Japan": "Japan",
|
||||||
|
"Jersey": "Jersey",
|
||||||
|
"Jordan": "Jordan",
|
||||||
|
"Kazakhstan": "Kazakhstan",
|
||||||
|
"Kenya": "Kenya",
|
||||||
|
"Kiribati": "Kiribati",
|
||||||
|
"Kosovo": "Kosovo",
|
||||||
|
"Kuwait": "Kuwait",
|
||||||
|
"Kyrgyzstan": "Kyrgyzstan",
|
||||||
|
"Laos": "Laos",
|
||||||
|
"Latvia": "Latvia",
|
||||||
|
"Lebanon": "Lebanon",
|
||||||
|
"Lesotho": "Lesotho",
|
||||||
|
"Liberia": "Liberia",
|
||||||
|
"Libya": "Libya",
|
||||||
|
"Liechtenstein": "Liechtenstein",
|
||||||
|
"Lithuania": "Lithuania",
|
||||||
|
"Luxembourg": "Luxembourg",
|
||||||
|
"Macau": "Macau",
|
||||||
|
"Macedonia": "Macedonia",
|
||||||
|
"Madagascar": "Madagascar",
|
||||||
|
"Malawi": "Malawi",
|
||||||
|
"Malaysia": "Malaysia",
|
||||||
|
"Maldives": "Maldives",
|
||||||
|
"Mali": "Mali",
|
||||||
|
"Malta": "Malta",
|
||||||
|
"Marshall Islands": "Marshall Islands",
|
||||||
|
"Martinique": "Martinique",
|
||||||
|
"Mauritania": "Mauritania",
|
||||||
|
"Mauritius": "Mauritius",
|
||||||
|
"Mayotte": "Mayotte",
|
||||||
|
"Mexico": "Mexico",
|
||||||
|
"Micronesia": "Micronesia",
|
||||||
|
"Moldova": "Moldova",
|
||||||
|
"Monaco": "Monaco",
|
||||||
|
"Mongolia": "Mongolia",
|
||||||
|
"Montenegro": "Montenegro",
|
||||||
|
"Montserrat": "Montserrat",
|
||||||
|
"Morocco": "Morocco",
|
||||||
|
"Mozambique": "Mozambique",
|
||||||
|
"Myanmar": "Myanmar",
|
||||||
|
"Namibia": "Namibia",
|
||||||
|
"Nauru": "Nauru",
|
||||||
|
"Nepal": "Nepal",
|
||||||
|
"Netherlands": "Netherlands",
|
||||||
|
"New Caledonia": "New Caledonia",
|
||||||
|
"New Zealand": "New Zealand",
|
||||||
|
"Nicaragua": "Nicaragua",
|
||||||
|
"Niger": "Niger",
|
||||||
|
"Nigeria": "Nigeria",
|
||||||
|
"Niue": "Niue",
|
||||||
|
"Norfolk Island": "Norfolk Island",
|
||||||
|
"North Korea": "North Korea",
|
||||||
|
"Northern Mariana Islands": "Northern Mariana Islands",
|
||||||
|
"Norway": "Norway",
|
||||||
|
"Oman": "Oman",
|
||||||
|
"Pakistan": "Pakistan",
|
||||||
|
"Palau": "Palau",
|
||||||
|
"Palestine": "Palestine",
|
||||||
|
"Panama": "Panama",
|
||||||
|
"Papua New Guinea": "Papua New Guinea",
|
||||||
|
"Paraguay": "Paraguay",
|
||||||
|
"Peru": "Peru",
|
||||||
|
"Philippines": "Philippines",
|
||||||
|
"Pitcairn Islands": "Pitcairn Islands",
|
||||||
|
"Poland": "Poland",
|
||||||
|
"Portugal": "Portugal",
|
||||||
|
"Puerto Rico": "Puerto Rico",
|
||||||
|
"Qatar": "Qatar",
|
||||||
|
"Romania": "Romania",
|
||||||
|
"Russia": "Russia",
|
||||||
|
"Rwanda": "Rwanda",
|
||||||
|
"Réunion": "Réunion",
|
||||||
|
"Samoa": "Samoa",
|
||||||
|
"San Marino": "San Marino",
|
||||||
|
"Saudi Arabia": "Saudi Arabia",
|
||||||
|
"Senegal": "Senegal",
|
||||||
|
"Serbia": "Serbia",
|
||||||
|
"Seychelles": "Seychelles",
|
||||||
|
"Sierra Leone": "Sierra Leone",
|
||||||
|
"Singapore": "Singapore",
|
||||||
|
"Sint Maarten": "Sint Maarten",
|
||||||
|
"Slovakia": "Slovakia",
|
||||||
|
"Slovenia": "Slovenia",
|
||||||
|
"Solomon Islands": "Solomon Islands",
|
||||||
|
"Somalia": "Somalia",
|
||||||
|
"South Africa": "South Africa",
|
||||||
|
"South Georgia & South Sandwich Islands": "South Georgia & South Sandwich Islands",
|
||||||
|
"South Korea": "South Korea",
|
||||||
|
"South Sudan": "South Sudan",
|
||||||
|
"Spain": "Spain",
|
||||||
|
"Sri Lanka": "Sri Lanka",
|
||||||
|
"St. Barthélemy": "St. Barthélemy",
|
||||||
|
"St. Helena": "St. Helena",
|
||||||
|
"St. Kitts & Nevis": "St. Kitts & Nevis",
|
||||||
|
"St. Lucia": "St. Lucia",
|
||||||
|
"St. Martin": "St. Martin",
|
||||||
|
"St. Pierre & Miquelon": "St. Pierre & Miquelon",
|
||||||
|
"St. Vincent & Grenadines": "St. Vincent & Grenadines",
|
||||||
|
"Sudan": "Sudan",
|
||||||
|
"Suriname": "Suriname",
|
||||||
|
"Svalbard & Jan Mayen": "Svalbard & Jan Mayen",
|
||||||
|
"Swaziland": "Swaziland",
|
||||||
|
"Sweden": "Sweden",
|
||||||
|
"Switzerland": "Switzerland",
|
||||||
|
"Syria": "Syria",
|
||||||
|
"São Tomé & Príncipe": "São Tomé & Príncipe",
|
||||||
|
"Taiwan": "Taiwan",
|
||||||
|
"Tajikistan": "Tajikistan",
|
||||||
|
"Tanzania": "Tanzania",
|
||||||
|
"Thailand": "Thailand",
|
||||||
|
"Timor-Leste": "Timor-Leste",
|
||||||
|
"Togo": "Togo",
|
||||||
|
"Tokelau": "Tokelau",
|
||||||
|
"Tonga": "Tonga",
|
||||||
|
"Trinidad & Tobago": "Trinidad & Tobago",
|
||||||
|
"Tunisia": "Tunisia",
|
||||||
|
"Turkey": "Turkey",
|
||||||
|
"Turkmenistan": "Turkmenistan",
|
||||||
|
"Turks & Caicos Islands": "Turks & Caicos Islands",
|
||||||
|
"Tuvalu": "Tuvalu",
|
||||||
|
"U.S. Virgin Islands": "U.S. Virgin Islands",
|
||||||
|
"Uganda": "Uganda",
|
||||||
|
"Ukraine": "Ukraine",
|
||||||
|
"United Arab Emirates": "United Arab Emirates",
|
||||||
|
"Uruguay": "Uruguay",
|
||||||
|
"Uzbekistan": "Uzbekistan",
|
||||||
|
"Vanuatu": "Vanuatu",
|
||||||
|
"Vatican City": "Vatican City",
|
||||||
|
"Venezuela": "Venezuela",
|
||||||
|
"Vietnam": "Vietnam",
|
||||||
|
"Wallis & Futuna": "Wallis & Futuna",
|
||||||
|
"Western Sahara": "Western Sahara",
|
||||||
|
"Yemen": "Yemen",
|
||||||
|
"Zambia": "Zambia",
|
||||||
|
"Zimbabwe": "Zimbabwe",
|
||||||
"Sign In or Create Account": "Sign In or Create Account",
|
"Sign In or Create Account": "Sign In or Create Account",
|
||||||
"Use your account or create a new one to continue.": "Use your account or create a new one to continue.",
|
"Use your account or create a new one to continue.": "Use your account or create a new one to continue.",
|
||||||
"Create Account": "Create Account",
|
"Create Account": "Create Account",
|
||||||
|
@ -399,7 +648,7 @@
|
||||||
"Unknown App": "Unknown App",
|
"Unknown App": "Unknown App",
|
||||||
"Help us improve %(brand)s": "Help us improve %(brand)s",
|
"Help us improve %(brand)s": "Help us improve %(brand)s",
|
||||||
"Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.": "Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.",
|
"Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.": "Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.",
|
||||||
"I want to help": "I want to help",
|
"Yes": "Yes",
|
||||||
"No": "No",
|
"No": "No",
|
||||||
"Review where you’re logged in": "Review where you’re logged in",
|
"Review where you’re logged in": "Review where you’re logged in",
|
||||||
"Verify all your sessions to ensure your account & messages are safe": "Verify all your sessions to ensure your account & messages are safe",
|
"Verify all your sessions to ensure your account & messages are safe": "Verify all your sessions to ensure your account & messages are safe",
|
||||||
|
@ -686,9 +935,8 @@
|
||||||
"Failed to set display name": "Failed to set display name",
|
"Failed to set display name": "Failed to set display name",
|
||||||
"Encryption": "Encryption",
|
"Encryption": "Encryption",
|
||||||
"Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.",
|
"Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.",
|
||||||
"Securely cache encrypted messages locally for them to appear in search results, using ": "Securely cache encrypted messages locally for them to appear in search results, using ",
|
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(count)s rooms.|other": "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(count)s rooms.",
|
||||||
" to store messages from ": " to store messages from ",
|
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(count)s rooms.|one": "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(count)s room.",
|
||||||
"rooms.": "rooms.",
|
|
||||||
"Manage": "Manage",
|
"Manage": "Manage",
|
||||||
"Securely cache encrypted messages locally for them to appear in search results.": "Securely cache encrypted messages locally for them to appear in search results.",
|
"Securely cache encrypted messages locally for them to appear in search results.": "Securely cache encrypted messages locally for them to appear in search results.",
|
||||||
"%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with <nativeLink>search components added</nativeLink>.": "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with <nativeLink>search components added</nativeLink>.",
|
"%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with <nativeLink>search components added</nativeLink>.": "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with <nativeLink>search components added</nativeLink>.",
|
||||||
|
@ -1086,6 +1334,15 @@
|
||||||
"Strikethrough": "Strikethrough",
|
"Strikethrough": "Strikethrough",
|
||||||
"Code block": "Code block",
|
"Code block": "Code block",
|
||||||
"Quote": "Quote",
|
"Quote": "Quote",
|
||||||
|
"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>)",
|
||||||
|
"Topic: %(topic)s ": "Topic: %(topic)s ",
|
||||||
|
"<a>Add a topic</a> to help people know what it is about.": "<a>Add a topic</a> to help people know what it is about.",
|
||||||
|
"You created this room.": "You created this room.",
|
||||||
|
"%(displayName)s created this room.": "%(displayName)s created this room.",
|
||||||
|
"Add a photo, so people can easily spot your room.": "Add a photo, so people can easily spot your room.",
|
||||||
|
"This is the start of <roomName/>.": "This is the start of <roomName/>.",
|
||||||
"No pinned messages.": "No pinned messages.",
|
"No pinned messages.": "No pinned messages.",
|
||||||
"Loading...": "Loading...",
|
"Loading...": "Loading...",
|
||||||
"Pinned Messages": "Pinned Messages",
|
"Pinned Messages": "Pinned Messages",
|
||||||
|
@ -1133,6 +1390,7 @@
|
||||||
"Historical": "Historical",
|
"Historical": "Historical",
|
||||||
"Custom Tag": "Custom Tag",
|
"Custom Tag": "Custom Tag",
|
||||||
"Can't see what you’re looking for?": "Can't see what you’re looking for?",
|
"Can't see what you’re looking for?": "Can't see what you’re looking for?",
|
||||||
|
"Start a new chat": "Start a new chat",
|
||||||
"Explore all public rooms": "Explore all public rooms",
|
"Explore all public rooms": "Explore all public rooms",
|
||||||
"Use the + to make a new room or explore existing ones below": "Use the + to make a new room or explore existing ones below",
|
"Use the + to make a new room or explore existing ones below": "Use the + to make a new room or explore existing ones below",
|
||||||
"%(count)s results|other": "%(count)s results",
|
"%(count)s results|other": "%(count)s results",
|
||||||
|
@ -1340,7 +1598,6 @@
|
||||||
"Remove this user from community?": "Remove this user from community?",
|
"Remove this user from community?": "Remove this user from community?",
|
||||||
"Failed to withdraw invitation": "Failed to withdraw invitation",
|
"Failed to withdraw invitation": "Failed to withdraw invitation",
|
||||||
"Failed to remove user from community": "Failed to remove user from community",
|
"Failed to remove user from community": "Failed to remove user from community",
|
||||||
"<strong>%(role)s</strong> in %(roomName)s": "<strong>%(role)s</strong> in %(roomName)s",
|
|
||||||
"Failed to change power level": "Failed to change power level",
|
"Failed to change power level": "Failed to change power level",
|
||||||
"You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.",
|
"You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.",
|
||||||
"Are you sure?": "Are you sure?",
|
"Are you sure?": "Are you sure?",
|
||||||
|
@ -1348,6 +1605,7 @@
|
||||||
"Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?",
|
"Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?",
|
||||||
"Deactivate user": "Deactivate user",
|
"Deactivate user": "Deactivate user",
|
||||||
"Failed to deactivate user": "Failed to deactivate user",
|
"Failed to deactivate user": "Failed to deactivate user",
|
||||||
|
"Role": "Role",
|
||||||
"This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.",
|
"This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.",
|
||||||
"Security": "Security",
|
"Security": "Security",
|
||||||
"The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.": "The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.",
|
"The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.": "The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.",
|
||||||
|
@ -1358,7 +1616,6 @@
|
||||||
"Verify by emoji": "Verify by emoji",
|
"Verify by emoji": "Verify by emoji",
|
||||||
"Almost there! Is your other session showing the same shield?": "Almost there! Is your other session showing the same shield?",
|
"Almost there! Is your other session showing the same shield?": "Almost there! Is your other session showing the same shield?",
|
||||||
"Almost there! Is %(displayName)s showing the same shield?": "Almost there! Is %(displayName)s showing the same shield?",
|
"Almost there! Is %(displayName)s showing the same shield?": "Almost there! Is %(displayName)s showing the same shield?",
|
||||||
"Yes": "Yes",
|
|
||||||
"Verify all users in a room to ensure it's secure.": "Verify all users in a room to ensure it's secure.",
|
"Verify all users in a room to ensure it's secure.": "Verify all users in a room to ensure it's secure.",
|
||||||
"In encrypted rooms, verify all users to ensure it’s secure.": "In encrypted rooms, verify all users to ensure it’s secure.",
|
"In encrypted rooms, verify all users to ensure it’s secure.": "In encrypted rooms, verify all users to ensure it’s secure.",
|
||||||
"You've successfully verified your device!": "You've successfully verified your device!",
|
"You've successfully verified your device!": "You've successfully verified your device!",
|
||||||
|
@ -1384,8 +1641,9 @@
|
||||||
"Today": "Today",
|
"Today": "Today",
|
||||||
"Yesterday": "Yesterday",
|
"Yesterday": "Yesterday",
|
||||||
"View Source": "View Source",
|
"View Source": "View Source",
|
||||||
|
"Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.",
|
||||||
|
"Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.",
|
||||||
"Encryption enabled": "Encryption enabled",
|
"Encryption enabled": "Encryption enabled",
|
||||||
"Messages in this room are end-to-end encrypted. Learn more & verify this user in their user profile.": "Messages in this room are end-to-end encrypted. Learn more & verify this user in their user profile.",
|
|
||||||
"Ignored attempt to disable encryption": "Ignored attempt to disable encryption",
|
"Ignored attempt to disable encryption": "Ignored attempt to disable encryption",
|
||||||
"Encryption not enabled": "Encryption not enabled",
|
"Encryption not enabled": "Encryption not enabled",
|
||||||
"The encryption used by this room isn't supported.": "The encryption used by this room isn't supported.",
|
"The encryption used by this room isn't supported.": "The encryption used by this room isn't supported.",
|
||||||
|
@ -1431,8 +1689,8 @@
|
||||||
"%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s changed the avatar for %(roomName)s",
|
"%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s changed the avatar for %(roomName)s",
|
||||||
"%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s removed the room avatar.",
|
"%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s removed the room avatar.",
|
||||||
"%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s changed the room avatar to <img/>",
|
"%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s changed the room avatar to <img/>",
|
||||||
"This room is a continuation of another conversation.": "This room is a continuation of another conversation.",
|
|
||||||
"Click here to see older messages.": "Click here to see older messages.",
|
"Click here to see older messages.": "Click here to see older messages.",
|
||||||
|
"This room is a continuation of another conversation.": "This room is a continuation of another conversation.",
|
||||||
"Copied!": "Copied!",
|
"Copied!": "Copied!",
|
||||||
"Failed to copy": "Failed to copy",
|
"Failed to copy": "Failed to copy",
|
||||||
"Add an Integration": "Add an Integration",
|
"Add an Integration": "Add an Integration",
|
||||||
|
@ -1951,8 +2209,8 @@
|
||||||
"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.",
|
"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.",
|
||||||
"Please review and accept all of the homeserver's policies": "Please review and accept all of the homeserver's policies",
|
"Please review and accept all of the homeserver's policies": "Please review and accept all of the homeserver's policies",
|
||||||
"Please review and accept the policies of this homeserver:": "Please review and accept the policies of this homeserver:",
|
"Please review and accept the policies of this homeserver:": "Please review and accept the policies of this homeserver:",
|
||||||
"An email has been sent to %(emailAddress)s": "An email has been sent to %(emailAddress)s",
|
"A confirmation email has been sent to %(emailAddress)s": "A confirmation email has been sent to %(emailAddress)s",
|
||||||
"Please check your email to continue registration.": "Please check your email to continue registration.",
|
"Open the link in the email to continue registration.": "Open the link in the email to continue registration.",
|
||||||
"Token incorrect": "Token incorrect",
|
"Token incorrect": "Token incorrect",
|
||||||
"A text message has been sent to %(msisdn)s": "A text message has been sent to %(msisdn)s",
|
"A text message has been sent to %(msisdn)s": "A text message has been sent to %(msisdn)s",
|
||||||
"Please enter the code it contains:": "Please enter the code it contains:",
|
"Please enter the code it contains:": "Please enter the code it contains:",
|
||||||
|
@ -1989,8 +2247,6 @@
|
||||||
"Enter username": "Enter username",
|
"Enter username": "Enter username",
|
||||||
"Email (optional)": "Email (optional)",
|
"Email (optional)": "Email (optional)",
|
||||||
"Phone (optional)": "Phone (optional)",
|
"Phone (optional)": "Phone (optional)",
|
||||||
"Create your Matrix account on %(serverName)s": "Create your Matrix account on %(serverName)s",
|
|
||||||
"Create your Matrix account on <underlinedServerName />": "Create your Matrix account on <underlinedServerName />",
|
|
||||||
"Register": "Register",
|
"Register": "Register",
|
||||||
"Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.": "Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.",
|
"Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.": "Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.",
|
||||||
"Set an email for account recovery. Use email to optionally be discoverable by existing contacts.": "Set an email for account recovery. Use email to optionally be discoverable by existing contacts.",
|
"Set an email for account recovery. Use email to optionally be discoverable by existing contacts.": "Set an email for account recovery. Use email to optionally be discoverable by existing contacts.",
|
||||||
|
@ -2082,6 +2338,7 @@
|
||||||
"Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.",
|
"Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.",
|
||||||
"Self-verification request": "Self-verification request",
|
"Self-verification request": "Self-verification request",
|
||||||
"Logout": "Logout",
|
"Logout": "Logout",
|
||||||
|
"%(creator)s created this DM.": "%(creator)s created this DM.",
|
||||||
"%(creator)s created and configured the room.": "%(creator)s created and configured the room.",
|
"%(creator)s created and configured the room.": "%(creator)s created and configured the room.",
|
||||||
"Your Communities": "Your Communities",
|
"Your Communities": "Your Communities",
|
||||||
"Did you know: you can use communities to filter your %(brand)s experience!": "Did you know: you can use communities to filter your %(brand)s experience!",
|
"Did you know: you can use communities to filter your %(brand)s experience!": "Did you know: you can use communities to filter your %(brand)s experience!",
|
||||||
|
@ -2112,8 +2369,9 @@
|
||||||
"Find a room… (e.g. %(exampleRoom)s)": "Find a room… (e.g. %(exampleRoom)s)",
|
"Find a room… (e.g. %(exampleRoom)s)": "Find a room… (e.g. %(exampleRoom)s)",
|
||||||
"If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.": "If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.",
|
"If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.": "If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.",
|
||||||
"Explore rooms in %(communityName)s": "Explore rooms in %(communityName)s",
|
"Explore rooms in %(communityName)s": "Explore rooms in %(communityName)s",
|
||||||
|
"Filter": "Filter",
|
||||||
"Clear filter": "Clear filter",
|
"Clear filter": "Clear filter",
|
||||||
"Search rooms": "Search rooms",
|
"Filter rooms and people": "Filter rooms and people",
|
||||||
"You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.": "You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.",
|
"You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.": "You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.",
|
||||||
"Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.": "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.",
|
"Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.": "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.",
|
||||||
"Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.": "Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.",
|
"Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.": "Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.",
|
||||||
|
@ -2128,7 +2386,6 @@
|
||||||
"Starting microphone...": "Starting microphone...",
|
"Starting microphone...": "Starting microphone...",
|
||||||
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
|
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
|
||||||
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
|
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
|
||||||
"There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?",
|
|
||||||
"You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",
|
"You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",
|
||||||
"You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?",
|
"You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?",
|
||||||
"Search failed": "Search failed",
|
"Search failed": "Search failed",
|
||||||
|
@ -2214,6 +2471,8 @@
|
||||||
"<a>Log in</a> to your new account.": "<a>Log in</a> to your new account.",
|
"<a>Log in</a> to your new account.": "<a>Log in</a> to your new account.",
|
||||||
"You can now close this window or <a>log in</a> to your new account.": "You can now close this window or <a>log in</a> to your new account.",
|
"You can now close this window or <a>log in</a> to your new account.": "You can now close this window or <a>log in</a> to your new account.",
|
||||||
"Registration Successful": "Registration Successful",
|
"Registration Successful": "Registration Successful",
|
||||||
|
"Create your Matrix account on %(serverName)s": "Create your Matrix account on %(serverName)s",
|
||||||
|
"Create your Matrix account on <underlinedServerName />": "Create your Matrix account on <underlinedServerName />",
|
||||||
"Create your account": "Create your account",
|
"Create your account": "Create your account",
|
||||||
"Use Recovery Key or Passphrase": "Use Recovery Key or Passphrase",
|
"Use Recovery Key or Passphrase": "Use Recovery Key or Passphrase",
|
||||||
"Use Recovery Key": "Use Recovery Key",
|
"Use Recovery Key": "Use Recovery Key",
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -25,6 +25,7 @@ export interface IToast<C extends ComponentClass> {
|
||||||
title: string;
|
title: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
component: C;
|
component: C;
|
||||||
|
className?: string;
|
||||||
props?: Omit<React.ComponentProps<C>, "toastKey">; // toastKey is injected by ToastContainer
|
props?: Omit<React.ComponentProps<C>, "toastKey">; // toastKey is injected by ToastContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -64,12 +64,13 @@ export const showToast = (policyUrl?: string) => {
|
||||||
) : sub,
|
) : sub,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
acceptLabel: _t("I want to help"),
|
acceptLabel: _t("Yes"),
|
||||||
onAccept,
|
onAccept,
|
||||||
rejectLabel: _t("No"),
|
rejectLabel: _t("No"),
|
||||||
onReject,
|
onReject,
|
||||||
},
|
},
|
||||||
component: GenericToast,
|
component: GenericToast,
|
||||||
|
className: "mx_AnalyticsToast",
|
||||||
priority: 10,
|
priority: 10,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -78,6 +78,11 @@ export function getEffectiveMembership(membership: string): EffectiveMembership
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isJoinedOrNearlyJoined(membership: string): boolean {
|
||||||
|
const effective = getEffectiveMembership(membership);
|
||||||
|
return effective === EffectiveMembership.Join || effective === EffectiveMembership.Invite;
|
||||||
|
}
|
||||||
|
|
||||||
export async function leaveRoomBehaviour(roomId: string) {
|
export async function leaveRoomBehaviour(roomId: string) {
|
||||||
let leavingAllVersions = true;
|
let leavingAllVersions = true;
|
||||||
const history = await MatrixClientPeg.get().getRoomUpgradeHistory(roomId);
|
const history = await MatrixClientPeg.get().getRoomUpgradeHistory(roomId);
|
||||||
|
|
|
@ -38,6 +38,7 @@ import { configure, mount } from "enzyme";
|
||||||
import Velocity from 'velocity-animate';
|
import Velocity from 'velocity-animate';
|
||||||
import MatrixClientContext from "../../../src/contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../src/contexts/MatrixClientContext";
|
||||||
import RoomContext from "../../../src/contexts/RoomContext";
|
import RoomContext from "../../../src/contexts/RoomContext";
|
||||||
|
import DMRoomMap from "../../../src/utils/DMRoomMap";
|
||||||
|
|
||||||
configure({ adapter: new Adapter() });
|
configure({ adapter: new Adapter() });
|
||||||
|
|
||||||
|
@ -52,7 +53,7 @@ class WrappedMessagePanel extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <MatrixClientContext.Provider value={client}>
|
return <MatrixClientContext.Provider value={client}>
|
||||||
<RoomContext.Provider value={{ canReact: true, canReply: true }}>
|
<RoomContext.Provider value={{ canReact: true, canReply: true, room, roomId: room.roomId }}>
|
||||||
<MessagePanel room={room} {...this.props} resizeNotifier={this.state.resizeNotifier} />
|
<MessagePanel room={room} {...this.props} resizeNotifier={this.state.resizeNotifier} />
|
||||||
</RoomContext.Provider>
|
</RoomContext.Provider>
|
||||||
</MatrixClientContext.Provider>;
|
</MatrixClientContext.Provider>;
|
||||||
|
@ -79,6 +80,8 @@ describe('MessagePanel', function() {
|
||||||
// complete without this even if we mock the clock and tick it
|
// complete without this even if we mock the clock and tick it
|
||||||
// what should be the correct amount of time).
|
// what should be the correct amount of time).
|
||||||
Velocity.mock = true;
|
Velocity.mock = true;
|
||||||
|
|
||||||
|
DMRoomMap.makeShared();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function() {
|
afterEach(function() {
|
||||||
|
@ -433,8 +436,8 @@ describe('MessagePanel', function() {
|
||||||
const rm = res.find('.mx_RoomView_myReadMarker_container').getDOMNode();
|
const rm = res.find('.mx_RoomView_myReadMarker_container').getDOMNode();
|
||||||
|
|
||||||
const rows = res.find('.mx_RoomView_MessageList').children();
|
const rows = res.find('.mx_RoomView_MessageList').children();
|
||||||
expect(rows.length).toEqual(6);
|
expect(rows.length).toEqual(7); // 6 events + the NewRoomIntro
|
||||||
expect(rm.previousSibling).toEqual(rows.at(4).getDOMNode());
|
expect(rm.previousSibling).toEqual(rows.at(5).getDOMNode());
|
||||||
|
|
||||||
// read marker should be hidden given props and at the last event
|
// read marker should be hidden given props and at the last event
|
||||||
expect(isReadMarkerVisible(rm)).toBeFalsy();
|
expect(isReadMarkerVisible(rm)).toBeFalsy();
|
||||||
|
|
|
@ -242,6 +242,7 @@ export function mkStubRoom(roomId = null) {
|
||||||
setBlacklistUnverifiedDevices: jest.fn(),
|
setBlacklistUnverifiedDevices: jest.fn(),
|
||||||
on: jest.fn(),
|
on: jest.fn(),
|
||||||
removeListener: jest.fn(),
|
removeListener: jest.fn(),
|
||||||
|
getDMInviter: jest.fn(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -6506,8 +6506,8 @@ mathml-tag-names@^2.0.1:
|
||||||
integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
|
integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
|
||||||
|
|
||||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
|
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
|
||||||
version "9.0.1"
|
version "9.1.0"
|
||||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/f8863d5c2488a31a093f5a1b11761d7ac3829470"
|
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/5ac00e346593f29f324b3af8e322928a6e1c427a"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.11.2"
|
"@babel/runtime" "^7.11.2"
|
||||||
another-json "^0.2.0"
|
another-json "^0.2.0"
|
||||||
|
@ -6532,10 +6532,10 @@ matrix-react-test-utils@^0.2.2:
|
||||||
resolved "https://registry.yarnpkg.com/matrix-react-test-utils/-/matrix-react-test-utils-0.2.2.tgz#c87144d3b910c7edc544a6699d13c7c2bf02f853"
|
resolved "https://registry.yarnpkg.com/matrix-react-test-utils/-/matrix-react-test-utils-0.2.2.tgz#c87144d3b910c7edc544a6699d13c7c2bf02f853"
|
||||||
integrity sha512-49+7gfV6smvBIVbeloql+37IeWMTD+fiywalwCqk8Dnz53zAFjKSltB3rmWHso1uecLtQEcPtCijfhzcLXAxTQ==
|
integrity sha512-49+7gfV6smvBIVbeloql+37IeWMTD+fiywalwCqk8Dnz53zAFjKSltB3rmWHso1uecLtQEcPtCijfhzcLXAxTQ==
|
||||||
|
|
||||||
matrix-widget-api@^0.1.0-beta.5:
|
matrix-widget-api@^0.1.0-beta.8:
|
||||||
version "0.1.0-beta.5"
|
version "0.1.0-beta.8"
|
||||||
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.5.tgz#dd7f24a177aa590d812bd4e92e2c3ac225c5557e"
|
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.8.tgz#17e85c03c46353373890b869b1fd46162bdb0026"
|
||||||
integrity sha512-J3GBJtVMFuEM/EWFylc0IlkPjdgmWxrkGYPaZ0LSmxp+OlNJxYfnWPR6F6HveW+Z8C1i0vq+BTueofSqKv2zDg==
|
integrity sha512-sWqyWs0RQqny/BimZUOxUd9BTJBzQmJlJ1i3lsSh1JBygV+aK5xQsONL97fc4i6/nwQPK72uCVDF+HwTtkpAbQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
events "^3.2.0"
|
events "^3.2.0"
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue