Merge branch 'develop' into travis/msc2873

This commit is contained in:
Travis Ralston 2021-05-14 14:22:53 -06:00
commit 89933d779c
41 changed files with 913 additions and 663 deletions

View file

@ -98,6 +98,7 @@
@import "./views/dialogs/_SpaceSettingsDialog.scss"; @import "./views/dialogs/_SpaceSettingsDialog.scss";
@import "./views/dialogs/_TabbedIntegrationManagerDialog.scss"; @import "./views/dialogs/_TabbedIntegrationManagerDialog.scss";
@import "./views/dialogs/_TermsDialog.scss"; @import "./views/dialogs/_TermsDialog.scss";
@import "./views/dialogs/_UntrustedDeviceDialog.scss";
@import "./views/dialogs/_UploadConfirmDialog.scss"; @import "./views/dialogs/_UploadConfirmDialog.scss";
@import "./views/dialogs/_UserSettingsDialog.scss"; @import "./views/dialogs/_UserSettingsDialog.scss";
@import "./views/dialogs/_WidgetCapabilitiesPromptDialog.scss"; @import "./views/dialogs/_WidgetCapabilitiesPromptDialog.scss";

View file

@ -54,7 +54,8 @@ limitations under the License.
display: flex; display: flex;
margin-top: 12px; margin-top: 12px;
.mx_BaseAvatar { // we can't target .mx_BaseAvatar here as it'll break the decorated avatar styling
.mx_DecoratedRoomAvatar {
margin-right: 12px; margin-right: 12px;
} }
@ -75,6 +76,10 @@ limitations under the License.
} }
.mx_AddExistingToSpace_section_spaces { .mx_AddExistingToSpace_section_spaces {
.mx_BaseAvatar {
margin-right: 12px;
}
.mx_BaseAvatar_image { .mx_BaseAvatar_image {
border-radius: 8px; border-radius: 8px;
} }
@ -105,6 +110,90 @@ limitations under the License.
mask-position: center; mask-position: center;
} }
} }
.mx_AddExistingToSpace_footer {
display: flex;
margin-top: 20px;
> span {
flex-grow: 1;
font-size: $font-12px;
line-height: $font-15px;
color: $secondary-fg-color;
.mx_ProgressBar {
height: 8px;
width: 100%;
@mixin ProgressBarBorderRadius 8px;
}
.mx_AddExistingToSpace_progressText {
margin-top: 8px;
font-size: $font-15px;
line-height: $font-24px;
color: $primary-fg-color;
}
> * {
vertical-align: middle;
}
}
.mx_AddExistingToSpace_error {
padding-left: 12px;
> img {
align-self: center;
}
.mx_AddExistingToSpace_errorHeading {
font-weight: $font-semi-bold;
font-size: $font-15px;
line-height: $font-18px;
color: $notice-primary-color;
}
.mx_AddExistingToSpace_errorCaption {
margin-top: 4px;
font-size: $font-12px;
line-height: $font-15px;
color: $primary-fg-color;
}
}
.mx_AccessibleButton {
display: inline-block;
align-self: center;
}
.mx_AccessibleButton_kind_primary {
padding: 8px 36px;
}
.mx_AddExistingToSpace_retryButton {
margin-left: 12px;
padding-left: 24px;
position: relative;
&::before {
content: '';
position: absolute;
background-color: $primary-fg-color;
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
mask-image: url('$(res)/img/element-icons/retry.svg');
width: 18px;
height: 18px;
left: 0;
}
}
.mx_AccessibleButton_kind_link {
padding: 0;
}
}
} }
.mx_AddExistingToSpaceDialog { .mx_AddExistingToSpaceDialog {
@ -189,88 +278,4 @@ limitations under the License.
.mx_AddExistingToSpace { .mx_AddExistingToSpace {
display: contents; display: contents;
} }
.mx_AddExistingToSpaceDialog_footer {
display: flex;
margin-top: 20px;
> span {
flex-grow: 1;
font-size: $font-12px;
line-height: $font-15px;
color: $secondary-fg-color;
.mx_ProgressBar {
height: 8px;
width: 100%;
@mixin ProgressBarBorderRadius 8px;
}
.mx_AddExistingToSpaceDialog_progressText {
margin-top: 8px;
font-size: $font-15px;
line-height: $font-24px;
color: $primary-fg-color;
}
> * {
vertical-align: middle;
}
}
.mx_AddExistingToSpaceDialog_error {
padding-left: 12px;
> img {
align-self: center;
}
.mx_AddExistingToSpaceDialog_errorHeading {
font-weight: $font-semi-bold;
font-size: $font-15px;
line-height: $font-18px;
color: $notice-primary-color;
}
.mx_AddExistingToSpaceDialog_errorCaption {
margin-top: 4px;
font-size: $font-12px;
line-height: $font-15px;
color: $primary-fg-color;
}
}
.mx_AccessibleButton {
display: inline-block;
align-self: center;
}
.mx_AccessibleButton_kind_primary {
padding: 8px 36px;
}
.mx_AddExistingToSpaceDialog_retryButton {
margin-left: 12px;
padding-left: 24px;
position: relative;
&::before {
content: '';
position: absolute;
background-color: $primary-fg-color;
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
mask-image: url('$(res)/img/element-icons/retry.svg');
width: 18px;
height: 18px;
left: 0;
}
}
.mx_AccessibleButton_kind_link {
padding: 0;
}
}
} }

View file

@ -0,0 +1,26 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_UntrustedDeviceDialog {
.mx_Dialog_title {
display: flex;
align-items: center;
.mx_E2EIcon {
margin-left: 0;
}
}
}

View file

@ -85,6 +85,7 @@ limitations under the License.
left: 0; left: 0;
height: 100%; height: 100%;
width: 100%; width: 100%;
mask-size: 18px;
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-position: center; mask-position: center;
background-color: $message-action-bar-fg-color; background-color: $message-action-bar-fg-color;

View file

@ -17,18 +17,55 @@ limitations under the License.
.mx_ReactionsRow { .mx_ReactionsRow {
margin: 6px 0; margin: 6px 0;
color: $primary-fg-color; color: $primary-fg-color;
.mx_ReactionsRow_addReactionButton {
position: relative;
display: none; // show on hover of the .mx_EventTile
width: 24px;
height: 24px;
vertical-align: middle;
margin-left: 4px;
&::before {
content: '';
position: absolute;
height: 100%;
width: 100%;
mask-size: 16px;
mask-repeat: no-repeat;
mask-position: center;
background-color: $tertiary-fg-color;
mask-image: url('$(res)/img/element-icons/room/message-bar/emoji.svg');
}
&.mx_ReactionsRow_addReactionButton_active {
display: inline-block; // keep showing whilst the context menu is shown
}
&:hover, &.mx_ReactionsRow_addReactionButton_active {
&::before {
background-color: $primary-fg-color;
}
}
}
}
.mx_EventTile:hover .mx_ReactionsRow_addReactionButton {
display: inline-block;
} }
.mx_ReactionsRow_showAll { .mx_ReactionsRow_showAll {
text-decoration: none; text-decoration: none;
font-size: $font-10px; font-size: $font-12px;
font-weight: 600; line-height: $font-20px;
margin-left: 6px; margin-left: 4px;
vertical-align: top; vertical-align: middle;
&:hover, &:link, &:visited {
&:link, color: $tertiary-fg-color;
&:visited { }
color: $accent-color;
&:hover {
color: $primary-fg-color;
} }
} }

View file

@ -16,14 +16,15 @@ limitations under the License.
.mx_ReactionsRowButton { .mx_ReactionsRowButton {
display: inline-flex; display: inline-flex;
line-height: $font-21px; line-height: $font-20px;
margin-right: 6px; margin-right: 6px;
padding: 0 6px; padding: 1px 6px;
border: 1px solid $reaction-row-button-border-color; border: 1px solid $reaction-row-button-border-color;
border-radius: 10px; border-radius: 10px;
background-color: $reaction-row-button-bg-color; background-color: $reaction-row-button-bg-color;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
vertical-align: middle;
&:hover { &:hover {
border-color: $reaction-row-button-hover-border-color; border-color: $reaction-row-button-hover-border-color;

View file

@ -98,7 +98,7 @@ limitations under the License.
position: relative; position: relative;
width: 24px; width: 24px;
height: 24px; height: 24px;
border-radius: 32px; border-radius: 8px;
&::before { &::before {
content: ''; content: '';
@ -114,6 +114,11 @@ limitations under the License.
} }
} }
.mx_RoomSublist_auxButton:hover,
.mx_RoomSublist_menuButton:hover {
background: $roomlist-button-bg-color;
}
// Hide the menu button by default // Hide the menu button by default
.mx_RoomSublist_menuButton { .mx_RoomSublist_menuButton {
visibility: hidden; visibility: hidden;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 380 KiB

View file

@ -1,7 +1,7 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="path-1-inside-1" fill="white"> <mask id="path-1-inside-1" fill="white">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 20C15.5228 20 20 15.5228 20 10C20 4.47715 15.5228 0 10 0C4.47715 0 0 4.47715 0 10C0 15.5228 4.47715 20 10 20ZM13.389 11.7659C13.6794 11.3162 14.2793 11.187 14.729 11.4774C15.1788 11.7677 15.308 12.3676 15.0176 12.8174C13.9565 14.461 12.1059 15.5526 9.99965 15.5526C7.89343 15.5526 6.04281 14.461 4.98167 12.8174C4.69133 12.3677 4.82053 11.7677 5.27025 11.4774C5.71997 11.187 6.31991 11.3162 6.61025 11.7659C7.32946 12.88 8.57908 13.6141 9.99965 13.6141C11.4202 13.6141 12.6698 12.88 13.389 11.7659ZM8 8C8 8.82843 7.44036 9.5 6.75 9.5C6.05964 9.5 5.5 8.82843 5.5 8C5.5 7.17157 6.05964 6.5 6.75 6.5C7.44036 6.5 8 7.17157 8 8ZM13.25 9.5C13.9404 9.5 14.5 8.82843 14.5 8C14.5 7.17157 13.9404 6.5 13.25 6.5C12.5596 6.5 12 7.17157 12 8C12 8.82843 12.5596 9.5 13.25 9.5Z"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M10 20C15.5228 20 20 15.5228 20 10C20 4.47715 15.5228 0 10 0C4.47715 0 0 4.47715 0 10C0 15.5228 4.47715 20 10 20ZM7.99989 7.5C7.99989 8.33 7.32989 9 6.49989 9C5.66989 9 4.99989 8.33 4.99989 7.5C4.99989 6.67 5.66989 6 6.49989 6C7.32989 6 7.99989 6.67 7.99989 7.5ZM14.9999 7.5C14.9999 8.33 14.3299 9 13.4999 9C12.6699 9 11.9999 8.33 11.9999 7.5C11.9999 6.67 12.6699 6 13.4999 6C14.3299 6 14.9999 6.67 14.9999 7.5ZM9.99989 15.5C12.3299 15.5 14.3099 14.04 15.1099 12H4.88989C5.68989 14.04 7.66989 15.5 9.99989 15.5Z"/>
</mask> </mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 20C15.5228 20 20 15.5228 20 10C20 4.47715 15.5228 0 10 0C4.47715 0 0 4.47715 0 10C0 15.5228 4.47715 20 10 20ZM13.389 11.7659C13.6794 11.3162 14.2793 11.187 14.729 11.4774C15.1788 11.7677 15.308 12.3676 15.0176 12.8174C13.9565 14.461 12.1059 15.5526 9.99965 15.5526C7.89343 15.5526 6.04281 14.461 4.98167 12.8174C4.69133 12.3677 4.82053 11.7677 5.27025 11.4774C5.71997 11.187 6.31991 11.3162 6.61025 11.7659C7.32946 12.88 8.57908 13.6141 9.99965 13.6141C11.4202 13.6141 12.6698 12.88 13.389 11.7659ZM8 8C8 8.82843 7.44036 9.5 6.75 9.5C6.05964 9.5 5.5 8.82843 5.5 8C5.5 7.17157 6.05964 6.5 6.75 6.5C7.44036 6.5 8 7.17157 8 8ZM13.25 9.5C13.9404 9.5 14.5 8.82843 14.5 8C14.5 7.17157 13.9404 6.5 13.25 6.5C12.5596 6.5 12 7.17157 12 8C12 8.82843 12.5596 9.5 13.25 9.5Z" fill="black"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M10 20C15.5228 20 20 15.5228 20 10C20 4.47715 15.5228 0 10 0C4.47715 0 0 4.47715 0 10C0 15.5228 4.47715 20 10 20ZM7.99989 7.5C7.99989 8.33 7.32989 9 6.49989 9C5.66989 9 4.99989 8.33 4.99989 7.5C4.99989 6.67 5.66989 6 6.49989 6C7.32989 6 7.99989 6.67 7.99989 7.5ZM14.9999 7.5C14.9999 8.33 14.3299 9 13.4999 9C12.6699 9 11.9999 8.33 11.9999 7.5C11.9999 6.67 12.6699 6 13.4999 6C14.3299 6 14.9999 6.67 14.9999 7.5ZM9.99989 15.5C12.3299 15.5 14.3099 14.04 15.1099 12H4.88989C5.68989 14.04 7.66989 15.5 9.99989 15.5Z" fill="#737D8C"/>
<path d="M14.1748 11.4164C14.0254 11.6478 14.0919 11.9565 14.3233 12.1059C14.5546 12.2553 14.8633 12.1888 15.0127 11.9574L14.1748 11.4164ZM15.148 11.7479C15.2974 11.5165 15.2309 11.2078 14.9995 11.0584C14.7681 10.909 14.4595 10.9755 14.3101 11.2069L15.148 11.7479ZM15.0127 11.9574L15.148 11.7479L14.3101 11.2069L14.1748 11.4164L15.0127 11.9574ZM14.729 11.4774L15.27 10.6394L15.27 10.6394L14.729 11.4774ZM13.389 11.7659L12.5511 11.225V11.225L13.389 11.7659ZM15.0176 12.8174L14.1797 12.2764V12.2764L15.0176 12.8174ZM4.98167 12.8174L5.8196 12.2764L5.8196 12.2764L4.98167 12.8174ZM5.27025 11.4774L4.72928 10.6394L4.72928 10.6394L5.27025 11.4774ZM6.61025 11.7659L5.77233 12.3069L6.61025 11.7659ZM19.0026 10C19.0026 14.972 14.972 19.0026 10 19.0026V20.9974C16.0737 20.9974 20.9974 16.0737 20.9974 10H19.0026ZM10 0.997378C14.972 0.997378 19.0026 5.02799 19.0026 10H20.9974C20.9974 3.92632 16.0737 -0.997378 10 -0.997378V0.997378ZM0.997378 10C0.997378 5.02799 5.02799 0.997378 10 0.997378V-0.997378C3.92632 -0.997378 -0.997378 3.92632 -0.997378 10H0.997378ZM10 19.0026C5.02799 19.0026 0.997378 14.972 0.997378 10H-0.997378C-0.997378 16.0737 3.92632 20.9974 10 20.9974V19.0026ZM15.27 10.6394C14.3575 10.0503 13.1402 10.3125 12.5511 11.225L14.227 12.3069C14.2259 12.3086 14.2229 12.312 14.2187 12.315C14.215 12.3174 14.2118 12.3186 14.2092 12.3192C14.2067 12.3197 14.2033 12.32 14.1989 12.3192C14.1939 12.3183 14.1898 12.3164 14.1881 12.3153L15.27 10.6394ZM15.8555 13.3583C16.4447 12.4458 16.1825 11.2286 15.27 10.6394L14.1881 12.3153C14.1863 12.3142 14.1829 12.3113 14.18 12.307C14.1775 12.3033 14.1764 12.3001 14.1758 12.2976C14.1753 12.2951 14.175 12.2917 14.1758 12.2873C14.1767 12.2822 14.1786 12.2781 14.1797 12.2764L15.8555 13.3583ZM9.99965 16.55C12.4589 16.55 14.6186 15.2742 15.8555 13.3583L14.1797 12.2764C13.2943 13.6478 11.7528 14.5552 9.99965 14.5552V16.55ZM4.14375 13.3583C5.38067 15.2742 7.54041 16.55 9.99965 16.55V14.5552C8.24645 14.5552 6.70495 13.6478 5.8196 12.2764L4.14375 13.3583ZM4.72928 10.6394C3.81679 11.2286 3.55464 12.4458 4.14375 13.3583L5.8196 12.2764C5.8207 12.2781 5.82261 12.2822 5.8235 12.2873C5.82426 12.2917 5.824 12.2951 5.82346 12.2976C5.82292 12.3001 5.82175 12.3033 5.81926 12.307C5.81635 12.3113 5.81294 12.3142 5.81122 12.3153L4.72928 10.6394ZM7.44818 11.225C6.85907 10.3125 5.64178 10.0503 4.72928 10.6394L5.81122 12.3153C5.8095 12.3164 5.80542 12.3183 5.80034 12.3192C5.79597 12.32 5.79256 12.3197 5.79004 12.3192C5.78752 12.3186 5.7843 12.3174 5.78064 12.315C5.77637 12.3121 5.77344 12.3086 5.77233 12.3069L7.44818 11.225ZM9.99965 12.6167C8.93153 12.6167 7.99128 12.0662 7.44818 11.225L5.77233 12.3069C6.66764 13.6937 8.22663 14.6115 9.99965 14.6115V12.6167ZM12.5511 11.225C12.008 12.0662 11.0678 12.6167 9.99965 12.6167V14.6115C11.7727 14.6115 13.3316 13.6937 14.227 12.3069L12.5511 11.225ZM6.75 10.4974C8.15374 10.4974 8.99738 9.20127 8.99738 8H7.00262C7.00262 8.19305 6.93691 8.33907 6.86768 8.42215C6.80022 8.50311 6.75428 8.50262 6.75 8.50262V10.4974ZM4.50262 8C4.50262 9.20127 5.34626 10.4974 6.75 10.4974V8.50262C6.74572 8.50262 6.69978 8.50311 6.63232 8.42215C6.56309 8.33907 6.49738 8.19305 6.49738 8H4.50262ZM6.75 5.50262C5.34626 5.50262 4.50262 6.79873 4.50262 8H6.49738C6.49738 7.80695 6.56309 7.66093 6.63232 7.57785C6.69978 7.49689 6.74572 7.49738 6.75 7.49738V5.50262ZM8.99738 8C8.99738 6.79873 8.15374 5.50262 6.75 5.50262V7.49738C6.75428 7.49738 6.80022 7.49689 6.86768 7.57785C6.93691 7.66093 7.00262 7.80695 7.00262 8H8.99738ZM13.5026 8C13.5026 8.19305 13.4369 8.33907 13.3677 8.42215C13.3002 8.50311 13.2543 8.50262 13.25 8.50262V10.4974C14.6537 10.4974 15.4974 9.20127 15.4974 8H13.5026ZM13.25 7.49738C13.2543 7.49738 13.3002 7.49689 13.3677 7.57785C13.4369 7.66093 13.5026 7.80695 13.5026 8H15.4974C15.4974 6.79873 14.6537 5.50262 13.25 5.50262V7.49738ZM12.9974 8C12.9974 7.80695 13.0631 7.66093 13.1323 7.57785C13.1998 7.49689 13.2457 7.49738 13.25 7.49738V5.50262C11.8463 5.50262 11.0026 6.79873 11.0026 8H12.9974ZM13.25 8.50262C13.2457 8.50262 13.1998 8.50311 13.1323 8.42215C13.0631 8.33907 12.9974 8.19305 12.9974 8H11.0026C11.0026 9.20127 11.8463 10.4974 13.25 10.4974V8.50262Z" fill="black" mask="url(#path-1-inside-1)"/> <path d="M15.1099 12L16.0384 12.3641L16.5724 11.0026H15.1099V12ZM4.88989 12V11.0026H3.42744L3.96136 12.3641L4.88989 12ZM19.0026 10C19.0026 14.972 14.972 19.0026 10 19.0026V20.9974C16.0737 20.9974 20.9974 16.0737 20.9974 10H19.0026ZM10 0.997378C14.972 0.997378 19.0026 5.02799 19.0026 10H20.9974C20.9974 3.92632 16.0737 -0.997378 10 -0.997378V0.997378ZM0.997378 10C0.997378 5.02799 5.02799 0.997378 10 0.997378V-0.997378C3.92632 -0.997378 -0.997378 3.92632 -0.997378 10H0.997378ZM10 19.0026C5.02799 19.0026 0.997378 14.972 0.997378 10H-0.997378C-0.997378 16.0737 3.92632 20.9974 10 20.9974V19.0026ZM6.49989 9.99738C7.88073 9.99738 8.99727 8.88084 8.99727 7.5H7.00251C7.00251 7.77916 6.77906 8.00262 6.49989 8.00262V9.99738ZM4.00251 7.5C4.00251 8.88084 5.11906 9.99738 6.49989 9.99738V8.00262C6.22073 8.00262 5.99727 7.77916 5.99727 7.5H4.00251ZM6.49989 5.00262C5.11906 5.00262 4.00251 6.11916 4.00251 7.5H5.99727C5.99727 7.22084 6.22073 6.99738 6.49989 6.99738V5.00262ZM8.99727 7.5C8.99727 6.11916 7.88073 5.00262 6.49989 5.00262V6.99738C6.77906 6.99738 7.00251 7.22084 7.00251 7.5H8.99727ZM13.4999 9.99738C14.8807 9.99738 15.9973 8.88084 15.9973 7.5H14.0025C14.0025 7.77916 13.7791 8.00262 13.4999 8.00262V9.99738ZM11.0025 7.5C11.0025 8.88084 12.1191 9.99738 13.4999 9.99738V8.00262C13.2207 8.00262 12.9973 7.77916 12.9973 7.5H11.0025ZM13.4999 5.00262C12.1191 5.00262 11.0025 6.11916 11.0025 7.5H12.9973C12.9973 7.22084 13.2207 6.99738 13.4999 6.99738V5.00262ZM15.9973 7.5C15.9973 6.11916 14.8807 5.00262 13.4999 5.00262V6.99738C13.7791 6.99738 14.0025 7.22084 14.0025 7.5H15.9973ZM14.1814 11.6359C13.5241 13.3119 11.9006 14.5026 9.99989 14.5026V16.4974C12.7592 16.4974 15.0957 14.7681 16.0384 12.3641L14.1814 11.6359ZM4.88989 12.9974H15.1099V11.0026H4.88989V12.9974ZM9.99989 14.5026C8.09919 14.5026 6.47569 13.3119 5.81842 11.6359L3.96136 12.3641C4.9041 14.7681 7.24059 16.4974 9.99989 16.4974V14.5026Z" fill="#737D8C" mask="url(#path-1-inside-1)"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View file

@ -1,5 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<ellipse cx="5" cy="5.75" rx="1.5" ry="1.75" fill="black"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M20 1C19.4477 1 19 1.44772 19 2V4H17C16.4477 4 16 4.44771 16 5C16 5.55228 16.4477 6 17 6H19V8C19 8.55228 19.4477 9 20 9C20.5523 9 21 8.55228 21 8V6H23C23.5523 6 24 5.55228 24 5C24 4.44772 23.5523 4 23 4H21V2C21 1.44772 20.5523 1 20 1ZM7 9.5C7 8.67 7.67 8 8.5 8C9.33 8 10 8.67 10 9.5C10 10.33 9.33 11 8.5 11C7.67 11 7 10.33 7 9.5ZM15.5 11C16.33 11 17 10.33 17 9.5C17 8.67 16.33 8 15.5 8C14.67 8 14 8.67 14 9.5C14 10.33 14.67 11 15.5 11ZM12 17.5C14.33 17.5 16.31 16.04 17.11 14H6.89001C7.69001 16.04 9.67001 17.5 12 17.5ZM4 12C4 7.58172 7.58172 4 12 4C12.6108 4 13.2045 4.06827 13.7742 4.1972C14.3129 4.3191 14.8484 3.98125 14.9703 3.44258C15.0922 2.90392 14.7543 2.36843 14.2156 2.24653C13.502 2.08504 12.7603 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 11.7878 21.9934 11.5771 21.9803 11.368C21.9459 10.8168 21.4711 10.3978 20.9199 10.4323C20.3687 10.4667 19.9498 10.9414 19.9842 11.4926C19.9947 11.6603 20 11.8295 20 12C20 16.4183 16.4183 20 12 20C7.58172 20 4 16.4183 4 12Z" fill="#737D8C"/>
<ellipse cx="13" cy="5.75" rx="1.5" ry="1.75" fill="black"/>
<path d="M4.66558 10.6543C4.47466 10.2867 4.0219 10.1435 3.65431 10.3344C3.28672 10.5253 3.1435 10.9781 3.33442 11.3457L4.66558 10.6543ZM14.678 11.3206C14.8551 10.9462 14.6951 10.4991 14.3206 10.322C13.9462 10.1449 13.4991 10.3049 13.322 10.6794L14.678 11.3206ZM4 11C3.33442 11.3457 3.33458 11.346 3.33475 11.3463C3.33481 11.3464 3.33499 11.3468 3.33512 11.347C3.33537 11.3475 3.33565 11.3481 3.33596 11.3486C3.33657 11.3498 3.33729 11.3512 3.3381 11.3527C3.33972 11.3558 3.34175 11.3596 3.34417 11.3641C3.34901 11.3731 3.35546 11.3849 3.36353 11.3993C3.37966 11.4282 3.40229 11.4677 3.43154 11.5162C3.48998 11.6131 3.57517 11.7467 3.68808 11.9048C3.91323 12.2199 4.25254 12.6377 4.71468 13.056C5.64215 13.8956 7.08135 14.75 9.06977 14.75V13.25C7.54656 13.25 6.45087 12.6044 5.72136 11.944C5.35502 11.6123 5.08532 11.2801 4.90858 11.0327C4.82054 10.9095 4.75657 10.8088 4.71608 10.7416C4.69586 10.7081 4.68159 10.6831 4.67318 10.668C4.66898 10.6605 4.66625 10.6555 4.66499 10.6531C4.66435 10.652 4.66409 10.6515 4.66419 10.6516C4.66424 10.6517 4.66438 10.652 4.66461 10.6524C4.66473 10.6527 4.66487 10.6529 4.66503 10.6532C4.66511 10.6534 4.66525 10.6537 4.66529 10.6537C4.66543 10.654 4.66558 10.6543 4 11ZM9.06977 14.75C11.0611 14.75 12.4696 13.893 13.3669 13.0451C13.8129 12.6236 14.1351 12.2027 14.3471 11.8853C14.4535 11.7261 14.5331 11.5914 14.5875 11.4936C14.6148 11.4446 14.6358 11.4047 14.6508 11.3754C14.6583 11.3608 14.6643 11.3488 14.6688 11.3396C14.6711 11.335 14.673 11.3311 14.6745 11.3279C14.6753 11.3263 14.676 11.3249 14.6765 11.3237C14.6768 11.3231 14.6771 11.3225 14.6774 11.322C14.6775 11.3218 14.6776 11.3214 14.6777 11.3213C14.6779 11.3209 14.678 11.3206 14 11C13.322 10.6794 13.3221 10.6791 13.3223 10.6788C13.3223 10.6787 13.3224 10.6784 13.3225 10.6783C13.3227 10.6779 13.3228 10.6776 13.3229 10.6774C13.3232 10.6769 13.3233 10.6766 13.3234 10.6764C13.3235 10.6762 13.3233 10.6766 13.3228 10.6776C13.3217 10.6798 13.3193 10.6846 13.3156 10.6919C13.3081 10.7066 13.2952 10.7312 13.2768 10.7642C13.24 10.8304 13.1813 10.9301 13.0998 11.0522C12.9361 11.2973 12.6842 11.6264 12.3366 11.9549C11.6466 12.607 10.5901 13.25 9.06977 13.25V14.75Z" fill="black"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -167,7 +167,7 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({children, handleHomeEn
const onKeyDownHandler = useCallback((ev) => { const onKeyDownHandler = useCallback((ev) => {
let handled = false; let handled = false;
// Don't interfere with input default keydown behaviour // Don't interfere with input default keydown behaviour
if (handleHomeEnd && ev.target.tagName !== "INPUT") { if (handleHomeEnd && ev.target.tagName !== "INPUT" && ev.target.tagName !== "TEXTAREA") {
// check if we actually have any items // check if we actually have any items
switch (ev.key) { switch (ev.key) {
case Key.HOME: case Key.HOME:

View file

@ -222,10 +222,12 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
}; };
private onKeyDown = (ev: React.KeyboardEvent) => { private onKeyDown = (ev: React.KeyboardEvent) => {
// don't let keyboard handling escape the context menu
ev.stopPropagation();
if (!this.props.managed) { if (!this.props.managed) {
if (ev.key === Key.ESCAPE) { if (ev.key === Key.ESCAPE) {
this.props.onFinished(); this.props.onFinished();
ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
} }
return; return;
@ -258,7 +260,6 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
if (handled) { if (handled) {
// consume all other keys in context menu // consume all other keys in context menu
ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
} }
}; };

View file

@ -34,6 +34,7 @@ import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResiz
import DMRoomMap from "../../utils/DMRoomMap"; import DMRoomMap from "../../utils/DMRoomMap";
import NewRoomIntro from "../views/rooms/NewRoomIntro"; import NewRoomIntro from "../views/rooms/NewRoomIntro";
import {replaceableComponent} from "../../utils/replaceableComponent"; import {replaceableComponent} from "../../utils/replaceableComponent";
import defaultDispatcher from '../../dispatcher/dispatcher';
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'];
@ -471,6 +472,10 @@ export default class MessagePanel extends React.Component {
return {nextEvent, nextTile}; return {nextEvent, nextTile};
} }
get _roomHasPendingEdit() {
return localStorage.getItem(`mx_edit_room_${this.props.room.roomId}`);
}
_getEventTiles() { _getEventTiles() {
this.eventNodes = {}; this.eventNodes = {};
@ -559,6 +564,13 @@ export default class MessagePanel extends React.Component {
} }
} }
if (!this.props.editState && this._roomHasPendingEdit) {
defaultDispatcher.dispatch({
action: "edit_event",
event: this.props.room.findEventById(this._roomHasPendingEdit),
});
}
if (grouper) { if (grouper) {
ret.push(...grouper.getTiles()); ret.push(...grouper.getTiles());
} }
@ -574,7 +586,6 @@ export default class MessagePanel extends React.Component {
const isEditing = this.props.editState && const isEditing = this.props.editState &&
this.props.editState.getEvent().getId() === mxEv.getId(); this.props.editState.getEvent().getId() === mxEv.getId();
// local echoes have a fake date, which could even be yesterday. Treat them // local echoes have a fake date, which could even be yesterday. Treat them
// as 'today' for the date separators. // as 'today' for the date separators.
let ts1 = mxEv.getTs(); let ts1 = mxEv.getTs();

View file

@ -52,8 +52,6 @@ import {useStateToggle} from "../../hooks/useStateToggle";
import SpaceStore from "../../stores/SpaceStore"; import SpaceStore from "../../stores/SpaceStore";
import FacePile from "../views/elements/FacePile"; import FacePile from "../views/elements/FacePile";
import {AddExistingToSpace} from "../views/dialogs/AddExistingToSpaceDialog"; import {AddExistingToSpace} from "../views/dialogs/AddExistingToSpaceDialog";
import {sleep} from "../../utils/promise";
import {calculateRoomVia} from "../../utils/permalinks/Permalinks";
import {ChevronFace, ContextMenuButton, useContextMenu} from "./ContextMenu"; import {ChevronFace, ContextMenuButton, useContextMenu} from "./ContextMenu";
import IconizedContextMenu, { import IconizedContextMenu, {
IconizedContextMenuOption, IconizedContextMenuOption,
@ -78,6 +76,7 @@ interface IProps {
interface IState { interface IState {
phase: Phase; phase: Phase;
createdRooms?: boolean; // internal state for the creation wizard
showRightPanel: boolean; showRightPanel: boolean;
myMembership: string; myMembership: string;
} }
@ -461,7 +460,8 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
setError(""); setError("");
setBusy(true); setBusy(true);
try { try {
await Promise.all(roomNames.map(name => name.trim()).filter(Boolean).map(name => { const filteredRoomNames = roomNames.map(name => name.trim()).filter(Boolean);
await Promise.all(filteredRoomNames.map(name => {
return createRoom({ return createRoom({
createOpts: { createOpts: {
preset: space.getJoinRule() === "public" ? Preset.PublicChat : Preset.PrivateChat, preset: space.getJoinRule() === "public" ? Preset.PublicChat : Preset.PrivateChat,
@ -474,7 +474,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
parentSpace: space, parentSpace: space,
}); });
})); }));
onFinished(); onFinished(filteredRoomNames.length > 0);
} catch (e) { } catch (e) {
console.error("Failed to create initial space rooms", e); console.error("Failed to create initial space rooms", e);
setError(_t("Failed to create initial space rooms")); setError(_t("Failed to create initial space rooms"));
@ -484,7 +484,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
let onClick = (ev) => { let onClick = (ev) => {
ev.preventDefault(); ev.preventDefault();
onFinished(); onFinished(false);
}; };
let buttonLabel = _t("Skip for now"); let buttonLabel = _t("Skip for now");
if (roomNames.some(name => name.trim())) { if (roomNames.some(name => name.trim())) {
@ -517,39 +517,6 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
}; };
const SpaceAddExistingRooms = ({ space, onFinished }) => { const SpaceAddExistingRooms = ({ space, onFinished }) => {
const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
let onClick = onFinished;
let buttonLabel = _t("Skip for now");
if (selectedToAdd.size > 0) {
onClick = async () => {
setBusy(true);
for (const room of selectedToAdd) {
const via = calculateRoomVia(room);
try {
await SpaceStore.instance.addRoomToSpace(space, room.roomId, via).catch(async e => {
if (e.errcode === "M_LIMIT_EXCEEDED") {
await sleep(e.data.retry_after_ms);
return SpaceStore.instance.addRoomToSpace(space, room.roomId, via); // retry
}
throw e;
});
} catch (e) {
console.error("Failed to add rooms to space", e);
setError(_t("Failed to add rooms to space"));
break;
}
}
setBusy(false);
};
buttonLabel = busy ? _t("Adding...") : _t("Add");
}
return <div> return <div>
<h1>{ _t("What do you want to organise?") }</h1> <h1>{ _t("What do you want to organise?") }</h1>
<div className="mx_SpaceRoomView_description"> <div className="mx_SpaceRoomView_description">
@ -557,35 +524,24 @@ const SpaceAddExistingRooms = ({ space, onFinished }) => {
"no one will be informed. You can add more later.") } "no one will be informed. You can add more later.") }
</div> </div>
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
<AddExistingToSpace <AddExistingToSpace
space={space} space={space}
selected={selectedToAdd} emptySelectionButton={
onChange={(checked, room) => { <AccessibleButton kind="primary" onClick={onFinished}>
if (checked) { { _t("Skip for now") }
selectedToAdd.add(room); </AccessibleButton>
} else { }
selectedToAdd.delete(room); onFinished={onFinished}
}
setSelectedToAdd(new Set(selectedToAdd));
}}
/> />
<div className="mx_SpaceRoomView_buttons"> <div className="mx_SpaceRoomView_buttons">
<AccessibleButton
kind="primary"
disabled={busy}
onClick={onClick}
>
{ buttonLabel }
</AccessibleButton>
</div> </div>
<SpaceFeedbackPrompt /> <SpaceFeedbackPrompt />
</div>; </div>;
}; };
const SpaceSetupPublicShare = ({ justCreatedOpts, space, onFinished }) => { const SpaceSetupPublicShare = ({ justCreatedOpts, space, onFinished, createdRooms }) => {
return <div className="mx_SpaceRoomView_publicShare"> return <div className="mx_SpaceRoomView_publicShare">
<h1>{ _t("Share %(name)s", { <h1>{ _t("Share %(name)s", {
name: justCreatedOpts?.createOpts?.name || space.name, name: justCreatedOpts?.createOpts?.name || space.name,
@ -598,7 +554,7 @@ const SpaceSetupPublicShare = ({ justCreatedOpts, space, onFinished }) => {
<div className="mx_SpaceRoomView_buttons"> <div className="mx_SpaceRoomView_buttons">
<AccessibleButton kind="primary" onClick={onFinished}> <AccessibleButton kind="primary" onClick={onFinished}>
{ _t("Go to my first room") } { createdRooms ? _t("Go to my first room") : _t("Go to my space") }
</AccessibleButton> </AccessibleButton>
</div> </div>
<SpaceFeedbackPrompt /> <SpaceFeedbackPrompt />
@ -891,13 +847,14 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
_t("Let's create a room for each of them.") + "\n" + _t("Let's create a room for each of them.") + "\n" +
_t("You can add more later too, including already existing ones.") _t("You can add more later too, including already existing ones.")
} }
onFinished={() => this.setState({ phase: Phase.PublicShare })} onFinished={(createdRooms: boolean) => this.setState({ phase: Phase.PublicShare, createdRooms })}
/>; />;
case Phase.PublicShare: case Phase.PublicShare:
return <SpaceSetupPublicShare return <SpaceSetupPublicShare
justCreatedOpts={this.props.justCreatedOpts} justCreatedOpts={this.props.justCreatedOpts}
space={this.props.space} space={this.props.space}
onFinished={this.goToFirstRoom} onFinished={this.goToFirstRoom}
createdRooms={this.state.createdRooms}
/>; />;
case Phase.PrivateScope: case Phase.PrivateScope:
@ -919,7 +876,7 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
title={_t("What projects are you working on?")} title={_t("What projects are you working on?")}
description={_t("We'll create rooms for each of them. " + description={_t("We'll create rooms for each of them. " +
"You can add more later too, including already existing ones.")} "You can add more later too, including already existing ones.")}
onFinished={() => this.setState({ phase: Phase.Landing })} onFinished={(createdRooms: boolean) => this.setState({ phase: Phase.Landing, createdRooms })}
/>; />;
case Phase.PrivateExistingRooms: case Phase.PrivateExistingRooms:
return <SpaceAddExistingRooms return <SpaceAddExistingRooms

View file

@ -20,7 +20,6 @@ import { Room } from "matrix-js-sdk/src/models/room";
import { User } from "matrix-js-sdk/src/models/user"; import { User } from "matrix-js-sdk/src/models/user";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { TagID } from '../../../stores/room-list/models';
import RoomAvatar from "./RoomAvatar"; import RoomAvatar from "./RoomAvatar";
import NotificationBadge from '../rooms/NotificationBadge'; import NotificationBadge from '../rooms/NotificationBadge';
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
@ -35,7 +34,6 @@ import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps { interface IProps {
room: Room; room: Room;
avatarSize: number; avatarSize: number;
tag: TagID;
displayBadge?: boolean; displayBadge?: boolean;
forceCount?: boolean; forceCount?: boolean;
oobData?: object; oobData?: object;

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, {useContext, useMemo, useState} from "react"; import React, {ReactNode, useContext, useMemo, useState} from "react";
import classNames from "classnames"; import classNames from "classnames";
import {Room} from "matrix-js-sdk/src/models/room"; import {Room} from "matrix-js-sdk/src/models/room";
import {MatrixClient} from "matrix-js-sdk/src/client"; import {MatrixClient} from "matrix-js-sdk/src/client";
@ -37,6 +37,7 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
import ProgressBar from "../elements/ProgressBar"; import ProgressBar from "../elements/ProgressBar";
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView"; import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
interface IProps extends IDialogProps { interface IProps extends IDialogProps {
matrixClient: MatrixClient; matrixClient: MatrixClient;
@ -46,7 +47,10 @@ interface IProps extends IDialogProps {
const Entry = ({ room, checked, onChange }) => { const Entry = ({ room, checked, onChange }) => {
return <label className="mx_AddExistingToSpace_entry"> return <label className="mx_AddExistingToSpace_entry">
<RoomAvatar room={room} height={32} width={32} /> { room?.isSpaceRoom()
? <RoomAvatar room={room} height={32} width={32} />
: <DecoratedRoomAvatar room={room} avatarSize={32} />
}
<span className="mx_AddExistingToSpace_entry_name">{ room.name }</span> <span className="mx_AddExistingToSpace_entry_name">{ room.name }</span>
<StyledCheckbox <StyledCheckbox
onChange={onChange ? (e) => onChange(e.target.checked) : null} onChange={onChange ? (e) => onChange(e.target.checked) : null}
@ -58,14 +62,23 @@ const Entry = ({ room, checked, onChange }) => {
interface IAddExistingToSpaceProps { interface IAddExistingToSpaceProps {
space: Room; space: Room;
selected: Set<Room>; footerPrompt?: ReactNode;
onChange(checked: boolean, room: Room): void; emptySelectionButton?: ReactNode;
onFinished(added: boolean): void;
} }
export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space, selected, onChange }) => { export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
space,
footerPrompt,
emptySelectionButton,
onFinished,
}) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const visibleRooms = useMemo(() => sortRooms(cli.getVisibleRooms()), [cli]); const visibleRooms = useMemo(() => sortRooms(cli.getVisibleRooms()), [cli]);
const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
const [progress, setProgress] = useState<number>(null);
const [error, setError] = useState<Error>(null);
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const lcQuery = query.toLowerCase(); const lcQuery = query.toLowerCase();
@ -93,120 +106,6 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space,
return arr; return arr;
}, [[], [], []]); }, [[], [], []]);
return <div className="mx_AddExistingToSpace">
<SearchBox
className="mx_textinput_icon mx_textinput_search"
placeholder={ _t("Filter your rooms and spaces") }
onSearch={setQuery}
autoComplete={true}
autoFocus={true}
/>
<AutoHideScrollbar className="mx_AddExistingToSpace_content" id="mx_AddExistingToSpace">
{ rooms.length > 0 ? (
<div className="mx_AddExistingToSpace_section">
<h3>{ _t("Rooms") }</h3>
{ rooms.map(room => {
return <Entry
key={room.roomId}
room={room}
checked={selected.has(room)}
onChange={onChange ? (checked) => {
onChange(checked, room);
} : null}
/>;
}) }
</div>
) : undefined }
{ spaces.length > 0 ? (
<div className="mx_AddExistingToSpace_section mx_AddExistingToSpace_section_spaces">
<h3>{ _t("Spaces") }</h3>
<div className="mx_AddExistingToSpace_section_experimental">
<div>{ _t("Feeling experimental?") }</div>
<div>{ _t("You can add existing spaces to a space.") }</div>
</div>
{ spaces.map(space => {
return <Entry
key={space.roomId}
room={space}
checked={selected.has(space)}
onChange={onChange ? (checked) => {
onChange(checked, space);
} : null}
/>;
}) }
</div>
) : null }
{ dms.length > 0 ? (
<div className="mx_AddExistingToSpace_section">
<h3>{ _t("Direct Messages") }</h3>
{ dms.map(room => {
return <Entry
key={room.roomId}
room={room}
checked={selected.has(room)}
onChange={onChange ? (checked) => {
onChange(checked, room);
} : null}
/>;
}) }
</div>
) : null }
{ spaces.length + rooms.length + dms.length < 1 ? <span className="mx_AddExistingToSpace_noResults">
{ _t("No results") }
</span> : undefined }
</AutoHideScrollbar>
</div>;
};
const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => {
const [selectedSpace, setSelectedSpace] = useState(space);
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
const [progress, setProgress] = useState<number>(null);
const [error, setError] = useState<Error>(null);
let spaceOptionSection;
if (existingSubspaces.length > 0) {
const options = [space, ...existingSubspaces].map((space) => {
const classes = classNames("mx_AddExistingToSpaceDialog_dropdownOption", {
mx_AddExistingToSpaceDialog_dropdownOptionActive: space === selectedSpace,
});
return <div key={space.roomId} className={classes}>
<RoomAvatar room={space} width={24} height={24} />
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
</div>;
});
spaceOptionSection = (
<Dropdown
id="mx_SpaceSelectDropdown"
onOptionChange={(key: string) => {
setSelectedSpace(existingSubspaces.find(space => space.roomId === key) || space);
}}
value={selectedSpace.roomId}
label={_t("Space selection")}
>
{ options }
</Dropdown>
);
} else {
spaceOptionSection = <div className="mx_AddExistingToSpaceDialog_onlySpace">
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
</div>;
}
const title = <React.Fragment>
<RoomAvatar room={selectedSpace} height={40} width={40} />
<div>
<h1>{ _t("Add existing rooms") }</h1>
{ spaceOptionSection }
</div>
</React.Fragment>;
const addRooms = async () => { const addRooms = async () => {
setError(null); setError(null);
setProgress(0); setProgress(0);
@ -269,20 +168,145 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
</div> </div>
</span>; </span>;
} else { } else {
let button = emptySelectionButton;
if (!button || selectedToAdd.size > 0) {
button = <AccessibleButton kind="primary" disabled={selectedToAdd.size < 1} onClick={addRooms}>
{ _t("Add") }
</AccessibleButton>;
}
footer = <> footer = <>
<span> <span>
<div>{ _t("Want to add a new room instead?") }</div> { footerPrompt }
<AccessibleButton onClick={() => onCreateRoomClick(cli, space)} kind="link">
{ _t("Create a new room") }
</AccessibleButton>
</span> </span>
<AccessibleButton kind="primary" disabled={selectedToAdd.size < 1} onClick={addRooms}> { button }
{ _t("Add") }
</AccessibleButton>
</>; </>;
} }
const onChange = !busy && !error ? (checked, room) => {
if (checked) {
selectedToAdd.add(room);
} else {
selectedToAdd.delete(room);
}
setSelectedToAdd(new Set(selectedToAdd));
} : null;
return <div className="mx_AddExistingToSpace">
<SearchBox
className="mx_textinput_icon mx_textinput_search"
placeholder={ _t("Filter your rooms and spaces") }
onSearch={setQuery}
autoComplete={true}
autoFocus={true}
/>
<AutoHideScrollbar className="mx_AddExistingToSpace_content" id="mx_AddExistingToSpace">
{ rooms.length > 0 ? (
<div className="mx_AddExistingToSpace_section">
<h3>{ _t("Rooms") }</h3>
{ rooms.map(room => {
return <Entry
key={room.roomId}
room={room}
checked={selectedToAdd.has(room)}
onChange={onChange ? (checked) => {
onChange(checked, room);
} : null}
/>;
}) }
</div>
) : undefined }
{ spaces.length > 0 ? (
<div className="mx_AddExistingToSpace_section mx_AddExistingToSpace_section_spaces">
<h3>{ _t("Spaces") }</h3>
<div className="mx_AddExistingToSpace_section_experimental">
<div>{ _t("Feeling experimental?") }</div>
<div>{ _t("You can add existing spaces to a space.") }</div>
</div>
{ spaces.map(space => {
return <Entry
key={space.roomId}
room={space}
checked={selectedToAdd.has(space)}
onChange={onChange ? (checked) => {
onChange(checked, space);
} : null}
/>;
}) }
</div>
) : null }
{ dms.length > 0 ? (
<div className="mx_AddExistingToSpace_section">
<h3>{ _t("Direct Messages") }</h3>
{ dms.map(room => {
return <Entry
key={room.roomId}
room={room}
checked={selectedToAdd.has(room)}
onChange={onChange ? (checked) => {
onChange(checked, room);
} : null}
/>;
}) }
</div>
) : null }
{ spaces.length + rooms.length + dms.length < 1 ? <span className="mx_AddExistingToSpace_noResults">
{ _t("No results") }
</span> : undefined }
</AutoHideScrollbar>
<div className="mx_AddExistingToSpace_footer">
{ footer }
</div>
</div>;
};
const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => {
const [selectedSpace, setSelectedSpace] = useState(space);
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
let spaceOptionSection;
if (existingSubspaces.length > 0) {
const options = [space, ...existingSubspaces].map((space) => {
const classes = classNames("mx_AddExistingToSpaceDialog_dropdownOption", {
mx_AddExistingToSpaceDialog_dropdownOptionActive: space === selectedSpace,
});
return <div key={space.roomId} className={classes}>
<RoomAvatar room={space} width={24} height={24} />
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
</div>;
});
spaceOptionSection = (
<Dropdown
id="mx_SpaceSelectDropdown"
onOptionChange={(key: string) => {
setSelectedSpace(existingSubspaces.find(space => space.roomId === key) || space);
}}
value={selectedSpace.roomId}
label={_t("Space selection")}
>
{ options }
</Dropdown>
);
} else {
spaceOptionSection = <div className="mx_AddExistingToSpaceDialog_onlySpace">
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
</div>;
}
const title = <React.Fragment>
<RoomAvatar room={selectedSpace} height={40} width={40} />
<div>
<h1>{ _t("Add existing rooms") }</h1>
{ spaceOptionSection }
</div>
</React.Fragment>;
return <BaseDialog return <BaseDialog
title={title} title={title}
className="mx_AddExistingToSpaceDialog" className="mx_AddExistingToSpaceDialog"
@ -293,21 +317,16 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
<MatrixClientContext.Provider value={cli}> <MatrixClientContext.Provider value={cli}>
<AddExistingToSpace <AddExistingToSpace
space={space} space={space}
selected={selectedToAdd} onFinished={onFinished}
onChange={!busy && !error ? (checked, room) => { footerPrompt={<>
if (checked) { <div>{ _t("Want to add a new room instead?") }</div>
selectedToAdd.add(room); <AccessibleButton onClick={() => onCreateRoomClick(cli, space)} kind="link">
} else { { _t("Create a new room") }
selectedToAdd.delete(room); </AccessibleButton>
} </>}
setSelectedToAdd(new Set(selectedToAdd));
} : null}
/> />
</MatrixClientContext.Provider> </MatrixClientContext.Provider>
<div className="mx_AddExistingToSpaceDialog_footer">
{ footer }
</div>
<SpaceFeedbackPrompt onClick={() => onFinished(false)} /> <SpaceFeedbackPrompt onClick={() => onFinished(false)} />
</BaseDialog>; </BaseDialog>;
}; };

View file

@ -0,0 +1,73 @@
/*
Copyright 2019, 2020, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import { User } from "matrix-js-sdk/src/models/user";
import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import E2EIcon from "../rooms/E2EIcon";
import AccessibleButton from "../elements/AccessibleButton";
import BaseDialog from "./BaseDialog";
import { IDialogProps } from "./IDialogProps";
import { IDevice } from "../right_panel/UserInfo";
interface IProps extends IDialogProps {
user: User;
device: IDevice;
}
const UntrustedDeviceDialog: React.FC<IProps> = ({device, user, onFinished}) => {
let askToVerifyText;
let newSessionText;
if (MatrixClientPeg.get().getUserId() === user.userId) {
newSessionText = _t("You signed in to a new session without verifying it:");
askToVerifyText = _t("Verify your other session using one of the options below.");
} else {
newSessionText = _t("%(name)s (%(userId)s) signed in to a new session without verifying it:",
{name: user.displayName, userId: user.userId});
askToVerifyText = _t("Ask this user to verify their session, or manually verify it below.");
}
return <BaseDialog
onFinished={onFinished}
className="mx_UntrustedDeviceDialog"
title={<>
<E2EIcon status="warning" size={24} hideTooltip={true} />
{ _t("Not Trusted")}
</>}
>
<div className="mx_Dialog_content" id='mx_Dialog_content'>
<p>{newSessionText}</p>
<p>{device.getDisplayName()} ({device.deviceId})</p>
<p>{askToVerifyText}</p>
</div>
<div className='mx_Dialog_buttons'>
<AccessibleButton element="button" kind="secondary" onClick={() => onFinished("legacy")}>
{ _t("Manually Verify by Text") }
</AccessibleButton>
<AccessibleButton element="button" kind="secondary" onClick={() => onFinished("sas")}>
{ _t("Interactively verify by Emoji") }
</AccessibleButton>
<AccessibleButton kind="primary" onClick={() => onFinished(false)}>
{ _t("Done") }
</AccessibleButton>
</div>
</BaseDialog>;
};
export default UntrustedDeviceDialog;

View file

@ -207,6 +207,7 @@ export default class ImageView extends React.Component<IProps, IState> {
a.href = this.props.src; a.href = this.props.src;
a.download = this.props.name; a.download = this.props.name;
a.target = "_blank"; a.target = "_blank";
a.rel = "noreferrer noopener";
a.click(); a.click();
}; };
@ -442,16 +443,16 @@ export default class ImageView extends React.Component<IProps, IState> {
<div className="mx_ImageView_panel"> <div className="mx_ImageView_panel">
{info} {info}
<div className="mx_ImageView_toolbar"> <div className="mx_ImageView_toolbar">
<AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_rotateCW"
title={_t("Rotate Right")}
onClick={this.onRotateClockwiseClick}>
</AccessibleTooltipButton>
<AccessibleTooltipButton <AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_rotateCCW" className="mx_ImageView_button mx_ImageView_button_rotateCCW"
title={_t("Rotate Left")} title={_t("Rotate Left")}
onClick={ this.onRotateCounterClockwiseClick }> onClick={ this.onRotateCounterClockwiseClick }>
</AccessibleTooltipButton> </AccessibleTooltipButton>
<AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_rotateCW"
title={_t("Rotate Right")}
onClick={this.onRotateClockwiseClick}>
</AccessibleTooltipButton>
{zoomOutButton} {zoomOutButton}
{zoomInButton} {zoomInButton}
<AccessibleTooltipButton <AccessibleTooltipButton

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2019 New Vector Ltd Copyright 2019, 2021 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.
@ -14,29 +14,68 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React from "react";
import PropTypes from 'prop-types'; import classNames from "classnames";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Relations } from "matrix-js-sdk/src/models/relations";
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { isContentActionable } from '../../../utils/EventUtils'; import { isContentActionable } from '../../../utils/EventUtils';
import {MatrixClientPeg} from '../../../MatrixClientPeg'; import { replaceableComponent } from "../../../utils/replaceableComponent";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton";
import { aboveLeftOf, ContextMenu, useContextMenu } from "../../structures/ContextMenu";
import ReactionPicker from "../emojipicker/ReactionPicker";
import ReactionsRowButton from "./ReactionsRowButton";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
// The maximum number of reactions to initially show on a message. // The maximum number of reactions to initially show on a message.
const MAX_ITEMS_WHEN_LIMITED = 8; const MAX_ITEMS_WHEN_LIMITED = 8;
@replaceableComponent("views.messages.ReactionsRow") const ReactButton = ({ mxEvent, reactions }: IProps) => {
export default class ReactionsRow extends React.PureComponent { const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
static propTypes = {
// The event we're displaying reactions for let contextMenu;
mxEvent: PropTypes.object.isRequired, if (menuDisplayed) {
// The Relations model from the JS SDK for reactions to `mxEvent` const buttonRect = button.current.getBoundingClientRect();
reactions: PropTypes.object, contextMenu = <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu} managed={false}>
<ReactionPicker mxEvent={mxEvent} reactions={reactions} onFinished={closeMenu} />
</ContextMenu>;
} }
constructor(props) { return <React.Fragment>
super(props); <ContextMenuTooltipButton
className={classNames("mx_ReactionsRow_addReactionButton", {
mx_ReactionsRow_addReactionButton_active: menuDisplayed,
})}
title={_t("Add reaction")}
onClick={openMenu}
isExpanded={menuDisplayed}
inputRef={button}
/>
{ contextMenu }
</React.Fragment>;
};
interface IProps {
// The event we're displaying reactions for
mxEvent: MatrixEvent;
// The Relations model from the JS SDK for reactions to `mxEvent`
reactions?: Relations;
}
interface IState {
myReactions: MatrixEvent[];
showAll: boolean;
}
@replaceableComponent("views.messages.ReactionsRow")
export default class ReactionsRow extends React.PureComponent<IProps, IState> {
static contextType = MatrixClientContext;
constructor(props, context) {
super(props, context);
if (props.reactions) { if (props.reactions) {
props.reactions.on("Relations.add", this.onReactionsChange); props.reactions.on("Relations.add", this.onReactionsChange);
@ -92,7 +131,7 @@ export default class ReactionsRow extends React.PureComponent {
if (!reactions) { if (!reactions) {
return null; return null;
} }
const userId = MatrixClientPeg.get().getUserId(); const userId = this.context.getUserId();
const myReactions = reactions.getAnnotationsBySender()[userId]; const myReactions = reactions.getAnnotationsBySender()[userId];
if (!myReactions) { if (!myReactions) {
return null; return null;
@ -114,7 +153,6 @@ export default class ReactionsRow extends React.PureComponent {
return null; return null;
} }
const ReactionsRowButton = sdk.getComponent('messages.ReactionsRowButton');
let items = reactions.getSortedAnnotationsByKey().map(([content, events]) => { let items = reactions.getSortedAnnotationsByKey().map(([content, events]) => {
const count = events.size; const count = events.size;
if (!count) { if (!count) {
@ -151,13 +189,21 @@ export default class ReactionsRow extends React.PureComponent {
</a>; </a>;
} }
const cli = this.context;
let addReactionButton;
if (cli.getRoom(mxEvent.getRoomId()).currentState.maySendEvent(EventType.Reaction, cli.getUserId())) {
addReactionButton = <ReactButton mxEvent={mxEvent} reactions={reactions} />;
}
return <div return <div
className="mx_ReactionsRow" className="mx_ReactionsRow"
role="toolbar" role="toolbar"
aria-label={_t("Reactions")} aria-label={_t("Reactions")}
> >
{items} { items }
{showAllButton} { showAllButton }
{ addReactionButton }
</div>; </div>;
} }
} }

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2019 New Vector Ltd Copyright 2019, 2021 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.
@ -14,49 +14,54 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React from "react";
import PropTypes from 'prop-types'; import classNames from "classnames";
import classNames from 'classnames'; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils'; import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import ReactionsRowButtonTooltip from "./ReactionsRowButtonTooltip";
import AccessibleButton from "../elements/AccessibleButton";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
interface IProps {
// The event we're displaying reactions for
mxEvent: MatrixEvent;
// The reaction content / key / emoji
content: string;
// The count of votes for this key
count: number;
// A Set of Matrix reaction events for this key
reactionEvents: Set<MatrixEvent>;
// A possible Matrix event if the current user has voted for this type
myReactionEvent?: MatrixEvent;
}
interface IState {
tooltipRendered: boolean;
tooltipVisible: boolean;
}
@replaceableComponent("views.messages.ReactionsRowButton") @replaceableComponent("views.messages.ReactionsRowButton")
export default class ReactionsRowButton extends React.PureComponent { export default class ReactionsRowButton extends React.PureComponent<IProps, IState> {
static propTypes = { static contextType = MatrixClientContext;
// The event we're displaying reactions for
mxEvent: PropTypes.object.isRequired,
// The reaction content / key / emoji
content: PropTypes.string.isRequired,
// The count of votes for this key
count: PropTypes.number.isRequired,
// A Set of Martix reaction events for this key
reactionEvents: PropTypes.object.isRequired,
// A possible Matrix event if the current user has voted for this type
myReactionEvent: PropTypes.object,
}
constructor(props) { state = {
super(props); tooltipRendered: false,
tooltipVisible: false,
};
this.state = { onClick = () => {
tooltipVisible: false,
};
}
onClick = (ev) => {
const { mxEvent, myReactionEvent, content } = this.props; const { mxEvent, myReactionEvent, content } = this.props;
if (myReactionEvent) { if (myReactionEvent) {
MatrixClientPeg.get().redactEvent( this.context.redactEvent(
mxEvent.getRoomId(), mxEvent.getRoomId(),
myReactionEvent.getId(), myReactionEvent.getId(),
); );
} else { } else {
MatrixClientPeg.get().sendEvent(mxEvent.getRoomId(), "m.reaction", { this.context.sendEvent(mxEvent.getRoomId(), "m.reaction", {
"m.relates_to": { "m.relates_to": {
"rel_type": "m.annotation", "rel_type": "m.annotation",
"event_id": mxEvent.getId(), "event_id": mxEvent.getId(),
@ -83,8 +88,6 @@ export default class ReactionsRowButton extends React.PureComponent {
} }
render() { render() {
const ReactionsRowButtonTooltip =
sdk.getComponent('messages.ReactionsRowButtonTooltip');
const { mxEvent, content, count, reactionEvents, myReactionEvent } = this.props; const { mxEvent, content, count, reactionEvents, myReactionEvent } = this.props;
const classes = classNames({ const classes = classNames({
@ -102,7 +105,7 @@ export default class ReactionsRowButton extends React.PureComponent {
/>; />;
} }
const room = MatrixClientPeg.get().getRoom(mxEvent.getRoomId()); const room = this.context.getRoom(mxEvent.getRoomId());
let label; let label;
if (room) { if (room) {
const senders = []; const senders = [];
@ -130,7 +133,6 @@ export default class ReactionsRowButton extends React.PureComponent {
); );
} }
const isPeeking = room.getMyMembership() !== "join"; const isPeeking = room.getMyMembership() !== "join";
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return <AccessibleButton return <AccessibleButton
className={classes} className={classes}
aria-label={label} aria-label={label}

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2019 The Matrix.org Foundation C.I.C. Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); 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.
@ -14,33 +14,34 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React from "react";
import PropTypes from 'prop-types'; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import { unicodeToShortcode } from '../../../HtmlUtils'; import { unicodeToShortcode } from '../../../HtmlUtils';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils'; import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import Tooltip from "../elements/Tooltip";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
interface IProps {
// The event we're displaying reactions for
mxEvent: MatrixEvent;
// The reaction content / key / emoji
content: string;
// A Set of Matrix reaction events for this key
reactionEvents: Set<MatrixEvent>;
visible: boolean;
}
@replaceableComponent("views.messages.ReactionsRowButtonTooltip") @replaceableComponent("views.messages.ReactionsRowButtonTooltip")
export default class ReactionsRowButtonTooltip extends React.PureComponent { export default class ReactionsRowButtonTooltip extends React.PureComponent<IProps> {
static propTypes = { static contextType = MatrixClientContext;
// The event we're displaying reactions for
mxEvent: PropTypes.object.isRequired,
// The reaction content / key / emoji
content: PropTypes.string.isRequired,
// A Set of Martix reaction events for this key
reactionEvents: PropTypes.object.isRequired,
visible: PropTypes.bool.isRequired,
}
render() { render() {
const Tooltip = sdk.getComponent('elements.Tooltip');
const { content, reactionEvents, mxEvent, visible } = this.props; const { content, reactionEvents, mxEvent, visible } = this.props;
const room = MatrixClientPeg.get().getRoom(mxEvent.getRoomId()); const room = this.context.getRoom(mxEvent.getRoomId());
let tooltipLabel; let tooltipLabel;
if (room) { if (room) {
const senders = []; const senders = [];

View file

@ -67,7 +67,7 @@ import RoomAvatar from "../avatars/RoomAvatar";
import RoomName from "../elements/RoomName"; import RoomName from "../elements/RoomName";
import {mediaFromMxc} from "../../../customisations/Media"; import {mediaFromMxc} from "../../../customisations/Media";
interface IDevice { export interface IDevice {
deviceId: string; deviceId: string;
ambiguous?: boolean; ambiguous?: boolean;
getDisplayName(): string; getDisplayName(): string;

View file

@ -35,6 +35,7 @@ import {Action} from "../../../dispatcher/actions";
import CountlyAnalytics from "../../../CountlyAnalytics"; import CountlyAnalytics from "../../../CountlyAnalytics";
import {getKeyBindingsManager, MessageComposerAction} from '../../../KeyBindingsManager'; import {getKeyBindingsManager, MessageComposerAction} from '../../../KeyBindingsManager';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
import SendHistoryManager from '../../../SendHistoryManager';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
function _isReply(mxEvent) { function _isReply(mxEvent) {
@ -122,6 +123,7 @@ export default class EditMessageComposer extends React.Component {
saveDisabled: true, saveDisabled: true,
}; };
this._createEditorModel(); this._createEditorModel();
window.addEventListener("beforeunload", this._saveStoredEditorState);
} }
_setEditorRef = ref => { _setEditorRef = ref => {
@ -175,11 +177,55 @@ export default class EditMessageComposer extends React.Component {
} }
} }
get _editorRoomKey() {
return `mx_edit_room_${this._getRoom().roomId}`;
}
get _editorStateKey() {
return `mx_edit_state_${this.props.editState.getEvent().getId()}`;
}
_cancelEdit = () => { _cancelEdit = () => {
this._clearStoredEditorState();
dis.dispatch({action: "edit_event", event: null}); dis.dispatch({action: "edit_event", event: null});
dis.fire(Action.FocusComposer); dis.fire(Action.FocusComposer);
} }
get _shouldSaveStoredEditorState() {
return localStorage.getItem(this._editorRoomKey) !== null;
}
_restoreStoredEditorState(partCreator) {
const json = localStorage.getItem(this._editorStateKey);
if (json) {
try {
const {parts: serializedParts} = JSON.parse(json);
const parts = serializedParts.map(p => partCreator.deserializePart(p));
return parts;
} catch (e) {
console.error("Error parsing editing state: ", e);
}
}
}
_clearStoredEditorState() {
localStorage.removeItem(this._editorRoomKey);
localStorage.removeItem(this._editorStateKey);
}
_clearPreviousEdit() {
if (localStorage.getItem(this._editorRoomKey)) {
localStorage.removeItem(`mx_edit_state_${localStorage.getItem(this._editorRoomKey)}`);
}
}
_saveStoredEditorState() {
const item = SendHistoryManager.createItem(this.model);
this._clearPreviousEdit();
localStorage.setItem(this._editorRoomKey, this.props.editState.getEvent().getId());
localStorage.setItem(this._editorStateKey, JSON.stringify(item));
}
_isSlashCommand() { _isSlashCommand() {
const parts = this.model.parts; const parts = this.model.parts;
const firstPart = parts[0]; const firstPart = parts[0];
@ -266,6 +312,7 @@ export default class EditMessageComposer extends React.Component {
const editedEvent = this.props.editState.getEvent(); const editedEvent = this.props.editState.getEvent();
const editContent = createEditContent(this.model, editedEvent); const editContent = createEditContent(this.model, editedEvent);
const newContent = editContent["m.new_content"]; const newContent = editContent["m.new_content"];
let shouldSend = true; let shouldSend = true;
// If content is modified then send an updated event into the room // If content is modified then send an updated event into the room
@ -311,6 +358,7 @@ export default class EditMessageComposer extends React.Component {
if (shouldSend) { if (shouldSend) {
this._cancelPreviousPendingEdit(); this._cancelPreviousPendingEdit();
const prom = this.context.sendMessage(roomId, editContent); const prom = this.context.sendMessage(roomId, editContent);
this._clearStoredEditorState();
dis.dispatch({action: "message_sent"}); dis.dispatch({action: "message_sent"});
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, true, false, editContent); CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, true, false, editContent);
} }
@ -346,6 +394,10 @@ export default class EditMessageComposer extends React.Component {
// then when mounting the editor again with the same editor state, // then when mounting the editor again with the same editor state,
// it will set the cursor at the end. // it will set the cursor at the end.
this.props.editState.setEditorState(caret, parts); this.props.editState.setEditorState(caret, parts);
window.removeEventListener("beforeunload", this._saveStoredEditorState);
if (this._shouldSaveStoredEditorState) {
this._saveStoredEditorState();
}
} }
_createEditorModel() { _createEditorModel() {
@ -358,10 +410,11 @@ export default class EditMessageComposer extends React.Component {
// restore serialized parts from the state // restore serialized parts from the state
parts = editState.getSerializedParts().map(p => partCreator.deserializePart(p)); parts = editState.getSerializedParts().map(p => partCreator.deserializePart(p));
} else { } else {
// otherwise, parse the body of the event //otherwise, either restore serialized parts from localStorage or parse the body of the event
parts = parseEvent(editState.getEvent(), partCreator); parts = this._restoreStoredEditorState(partCreator) || parseEvent(editState.getEvent(), partCreator);
} }
this.model = new EditorModel(parts, partCreator); this.model = new EditorModel(parts, partCreator);
this._saveStoredEditorState();
} }
_getInitialCaretPosition() { _getInitialCaretPosition() {

View file

@ -23,11 +23,9 @@ import defaultDispatcher from "../../../dispatcher/dispatcher";
import Analytics from "../../../Analytics"; import Analytics from "../../../Analytics";
import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { CSSTransition } from "react-transition-group"; import { CSSTransition } from "react-transition-group";
import RoomListStore from "../../../stores/room-list/RoomListStore";
import { DefaultTagID } from "../../../stores/room-list/models";
import { RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex"; import { RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex";
import Toolbar from "../../../accessibility/Toolbar"; import Toolbar from "../../../accessibility/Toolbar";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps { interface IProps {
} }
@ -84,8 +82,6 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
public render(): React.ReactElement { public render(): React.ReactElement {
const tiles = BreadcrumbsStore.instance.rooms.map((r, i) => { const tiles = BreadcrumbsStore.instance.rooms.map((r, i) => {
const roomTags = RoomListStore.instance.getTagsForRoom(r);
const roomTag = roomTags.includes(DefaultTagID.DM) ? DefaultTagID.DM : roomTags[0];
return ( return (
<RovingAccessibleTooltipButton <RovingAccessibleTooltipButton
className="mx_RoomBreadcrumbs_crumb" className="mx_RoomBreadcrumbs_crumb"
@ -98,7 +94,6 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
<DecoratedRoomAvatar <DecoratedRoomAvatar
room={r} room={r}
avatarSize={32} avatarSize={32}
tag={roomTag}
displayBadge={true} displayBadge={true}
forceCount={true} forceCount={true}
/> />

View file

@ -27,7 +27,6 @@ import SettingsStore from "../../../settings/SettingsStore";
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons'; import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
import E2EIcon from './E2EIcon'; import E2EIcon from './E2EIcon';
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import {DefaultTagID} from "../../../stores/room-list/models";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import RoomTopic from "../elements/RoomTopic"; import RoomTopic from "../elements/RoomTopic";
import RoomName from "../elements/RoomName"; import RoomName from "../elements/RoomName";
@ -177,7 +176,6 @@ export default class RoomHeader extends React.Component {
roomAvatar = <DecoratedRoomAvatar roomAvatar = <DecoratedRoomAvatar
room={this.props.room} room={this.props.room}
avatarSize={32} avatarSize={32}
tag={DefaultTagID.Untagged} // to apply room publicity badging
oobData={this.props.oobData} oobData={this.props.oobData}
viewAvatarOnClick={true} viewAvatarOnClick={true}
/>; />;

View file

@ -576,7 +576,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
const roomAvatar = <DecoratedRoomAvatar const roomAvatar = <DecoratedRoomAvatar
room={this.props.room} room={this.props.room}
avatarSize={32} avatarSize={32}
tag={this.props.tag}
displayBadge={this.props.isMinimized} displayBadge={this.props.isMinimized}
oobData={({avatarUrl: roomProfile.avatarMxc})} oobData={({avatarUrl: roomProfile.avatarMxc})}
/>; />;

View file

@ -232,7 +232,7 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
<p> <p>
{this.state.enabling {this.state.enabling
? <InlineSpinner /> ? <InlineSpinner />
: _t("Message search initilisation failed") : _t("Message search initialisation failed")
} }
</p> </p>
{EventIndexPeg.error && ( {EventIndexPeg.error && (

View file

@ -23,6 +23,7 @@ import {copyPlaintext} from "../../../utils/strings";
import {sleep} from "../../../utils/promise"; import {sleep} from "../../../utils/promise";
import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"; import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
import {showRoomInviteDialog} from "../../../RoomInvite"; import {showRoomInviteDialog} from "../../../RoomInvite";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
interface IProps { interface IProps {
space: Room; space: Room;
@ -50,7 +51,7 @@ const SpacePublicShare = ({ space, onFinished }: IProps) => {
<h3>{ _t("Share invite link") }</h3> <h3>{ _t("Share invite link") }</h3>
<span>{ copiedText }</span> <span>{ copiedText }</span>
</AccessibleButton> </AccessibleButton>
<AccessibleButton { space.canInvite(MatrixClientPeg.get()?.getUserId()) ? <AccessibleButton
className="mx_SpacePublicShare_inviteButton" className="mx_SpacePublicShare_inviteButton"
onClick={() => { onClick={() => {
showRoomInviteDialog(space.roomId); showRoomInviteDialog(space.roomId);
@ -59,7 +60,7 @@ const SpacePublicShare = ({ space, onFinished }: IProps) => {
> >
<h3>{ _t("Invite people") }</h3> <h3>{ _t("Invite people") }</h3>
<span>{ _t("Invite with email or username") }</span> <span>{ _t("Invite with email or username") }</span>
</AccessibleButton> </AccessibleButton> : null }
</div>; </div>;
}; };

View file

@ -209,7 +209,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
const userId = this.context.getUserId(); const userId = this.context.getUserId();
let inviteOption; let inviteOption;
if (this.props.space.canInvite(userId)) { if (this.props.space.getJoinRule() === "public" || this.props.space.canInvite(userId)) {
inviteOption = ( inviteOption = (
<IconizedContextMenuOption <IconizedContextMenuOption
className="mx_SpacePanel_contextMenu_inviteButton" className="mx_SpacePanel_contextMenu_inviteButton"

View file

@ -57,8 +57,8 @@ export default class PlaybackWaveform extends React.PureComponent<IProps, IState
}; };
private onTimeUpdate = (time: number[]) => { private onTimeUpdate = (time: number[]) => {
// Track percentages to very coarse precision, otherwise 0.002 ends up highlighting a bar. // Track percentages to a general precision to avoid over-waking the component.
const progress = Number(percentageOf(time[0], 0, time[1]).toFixed(1)); const progress = Number(percentageOf(time[0], 0, time[1]).toFixed(3));
this.setState({progress}); this.setState({progress});
}; };

View file

@ -578,14 +578,6 @@
"%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s", "%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s",
"Light": "Light", "Light": "Light",
"Dark": "Dark", "Dark": "Dark",
"You signed in to a new session without verifying it:": "You signed in to a new session without verifying it:",
"Verify your other session using one of the options below.": "Verify your other session using one of the options below.",
"%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s (%(userId)s) signed in to a new session without verifying it:",
"Ask this user to verify their session, or manually verify it below.": "Ask this user to verify their session, or manually verify it below.",
"Not Trusted": "Not Trusted",
"Manually Verify by Text": "Manually Verify by Text",
"Interactively verify by Emoji": "Interactively verify by Emoji",
"Done": "Done",
"%(displayName)s is typing …": "%(displayName)s is typing …", "%(displayName)s is typing …": "%(displayName)s is typing …",
"%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …", "%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …",
"%(names)s and %(count)s others are typing …|one": "%(names)s and one other is typing …", "%(names)s and %(count)s others are typing …|one": "%(names)s and one other is typing …",
@ -1097,7 +1089,7 @@
"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>.",
"%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> for encrypted messages to appear in search results.": "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> for encrypted messages to appear in search results.", "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> for encrypted messages to appear in search results.": "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> for encrypted messages to appear in search results.",
"Message search initilisation failed": "Message search initilisation failed", "Message search initialisation failed": "Message search initialisation failed",
"Connecting to integration manager...": "Connecting to integration manager...", "Connecting to integration manager...": "Connecting to integration manager...",
"Cannot connect to integration manager": "Cannot connect to integration manager", "Cannot connect to integration manager": "Cannot connect to integration manager",
"The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.", "The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.",
@ -1864,6 +1856,7 @@
"You sent a verification request": "You sent a verification request", "You sent a verification request": "You sent a verification request",
"Error decrypting video": "Error decrypting video", "Error decrypting video": "Error decrypting video",
"Error processing voice message": "Error processing voice message", "Error processing voice message": "Error processing voice message",
"Add reaction": "Add reaction",
"Show all": "Show all", "Show all": "Show all",
"Reactions": "Reactions", "Reactions": "Reactions",
"<reactors/><reactedWith> reacted with %(content)s</reactedWith>": "<reactors/><reactedWith> reacted with %(content)s</reactedWith>", "<reactors/><reactedWith> reacted with %(content)s</reactedWith>": "<reactors/><reactedWith> reacted with %(content)s</reactedWith>",
@ -1948,8 +1941,8 @@
"%(count)s people you know have already joined|one": "%(count)s person you know has already joined", "%(count)s people you know have already joined|one": "%(count)s person you know has already joined",
"Zoom out": "Zoom out", "Zoom out": "Zoom out",
"Zoom in": "Zoom in", "Zoom in": "Zoom in",
"Rotate Right": "Rotate Right",
"Rotate Left": "Rotate Left", "Rotate Left": "Rotate Left",
"Rotate Right": "Rotate Right",
"Download": "Download", "Download": "Download",
"Information": "Information", "Information": "Information",
"View message": "View message", "View message": "View message",
@ -2040,15 +2033,15 @@
"Add a new server...": "Add a new server...", "Add a new server...": "Add a new server...",
"%(networkName)s rooms": "%(networkName)s rooms", "%(networkName)s rooms": "%(networkName)s rooms",
"Matrix rooms": "Matrix rooms", "Matrix rooms": "Matrix rooms",
"Not all selected were added": "Not all selected were added",
"Adding rooms... (%(progress)s out of %(count)s)|other": "Adding rooms... (%(progress)s out of %(count)s)",
"Adding rooms... (%(progress)s out of %(count)s)|one": "Adding room...",
"Filter your rooms and spaces": "Filter your rooms and spaces", "Filter your rooms and spaces": "Filter your rooms and spaces",
"Feeling experimental?": "Feeling experimental?", "Feeling experimental?": "Feeling experimental?",
"You can add existing spaces to a space.": "You can add existing spaces to a space.", "You can add existing spaces to a space.": "You can add existing spaces to a space.",
"Direct Messages": "Direct Messages", "Direct Messages": "Direct Messages",
"Space selection": "Space selection", "Space selection": "Space selection",
"Add existing rooms": "Add existing rooms", "Add existing rooms": "Add existing rooms",
"Not all selected were added": "Not all selected were added",
"Adding rooms... (%(progress)s out of %(count)s)|other": "Adding rooms... (%(progress)s out of %(count)s)",
"Adding rooms... (%(progress)s out of %(count)s)|one": "Adding room...",
"Want to add a new room instead?": "Want to add a new room instead?", "Want to add a new room instead?": "Want to add a new room instead?",
"Create a new room": "Create a new room", "Create a new room": "Create a new room",
"Matrix ID": "Matrix ID", "Matrix ID": "Matrix ID",
@ -2066,6 +2059,7 @@
"Close dialog": "Close dialog", "Close dialog": "Close dialog",
"Beta feedback": "Beta feedback", "Beta feedback": "Beta feedback",
"Thank you for your feedback, we really appreciate it.": "Thank you for your feedback, we really appreciate it.", "Thank you for your feedback, we really appreciate it.": "Thank you for your feedback, we really appreciate it.",
"Done": "Done",
"%(featureName)s beta feedback": "%(featureName)s beta feedback", "%(featureName)s beta feedback": "%(featureName)s beta feedback",
"Your platform and username will be noted to help us use your feedback as much as we can.": "Your platform and username will be noted to help us use your feedback as much as we can.", "Your platform and username will be noted to help us use your feedback as much as we can.": "Your platform and username will be noted to help us use your feedback as much as we can.",
"To leave the beta, visit your settings.": "To leave the beta, visit your settings.", "To leave the beta, visit your settings.": "To leave the beta, visit your settings.",
@ -2389,6 +2383,13 @@
"Summary": "Summary", "Summary": "Summary",
"Document": "Document", "Document": "Document",
"Next": "Next", "Next": "Next",
"You signed in to a new session without verifying it:": "You signed in to a new session without verifying it:",
"Verify your other session using one of the options below.": "Verify your other session using one of the options below.",
"%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s (%(userId)s) signed in to a new session without verifying it:",
"Ask this user to verify their session, or manually verify it below.": "Ask this user to verify their session, or manually verify it below.",
"Not Trusted": "Not Trusted",
"Manually Verify by Text": "Manually Verify by Text",
"Interactively verify by Emoji": "Interactively verify by Emoji",
"Upload files (%(current)s of %(total)s)": "Upload files (%(current)s of %(total)s)", "Upload files (%(current)s of %(total)s)": "Upload files (%(current)s of %(total)s)",
"Upload files": "Upload files", "Upload files": "Upload files",
"Upload all": "Upload all", "Upload all": "Upload all",
@ -2703,13 +2704,12 @@
"Failed to create initial space rooms": "Failed to create initial space rooms", "Failed to create initial space rooms": "Failed to create initial space rooms",
"Skip for now": "Skip for now", "Skip for now": "Skip for now",
"Creating rooms...": "Creating rooms...", "Creating rooms...": "Creating rooms...",
"Failed to add rooms to space": "Failed to add rooms to space",
"Adding...": "Adding...",
"What do you want to organise?": "What do you want to organise?", "What do you want to organise?": "What do you want to organise?",
"Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.", "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.",
"Share %(name)s": "Share %(name)s", "Share %(name)s": "Share %(name)s",
"It's just you at the moment, it will be even better with others.": "It's just you at the moment, it will be even better with others.", "It's just you at the moment, it will be even better with others.": "It's just you at the moment, it will be even better with others.",
"Go to my first room": "Go to my first room", "Go to my first room": "Go to my first room",
"Go to my space": "Go to my space",
"Who are you working with?": "Who are you working with?", "Who are you working with?": "Who are you working with?",
"Make sure the right people have access to %(name)s": "Make sure the right people have access to %(name)s", "Make sure the right people have access to %(name)s": "Make sure the right people have access to %(name)s",
"Just me": "Just me", "Just me": "Just me",

View file

@ -254,11 +254,15 @@ matrixLinkify.options = {
target: function(href, type) { target: function(href, type) {
if (type === 'url') { if (type === 'url') {
const transformed = tryTransformPermalinkToLocalHref(href); try {
if (transformed !== href || decodeURIComponent(href).match(matrixLinkify.ELEMENT_URL_PATTERN)) { const transformed = tryTransformPermalinkToLocalHref(href);
return null; if (transformed !== href || decodeURIComponent(href).match(matrixLinkify.ELEMENT_URL_PATTERN)) {
} else { return null;
return '_blank'; } else {
return '_blank';
}
} catch (e) {
// malformed URI
} }
} }
return null; return null;

View file

@ -16,14 +16,12 @@ limitations under the License.
import SettingsStore from "../settings/SettingsStore"; import SettingsStore from "../settings/SettingsStore";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { ActionPayload } from "../dispatcher/payloads"; import { ActionPayload } from "../dispatcher/payloads";
import { AsyncStoreWithClient } from "./AsyncStoreWithClient"; import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
import defaultDispatcher from "../dispatcher/dispatcher"; import defaultDispatcher from "../dispatcher/dispatcher";
import { arrayHasDiff } from "../utils/arrays"; import { arrayHasDiff } from "../utils/arrays";
import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
import { SettingLevel } from "../settings/SettingLevel"; import { SettingLevel } from "../settings/SettingLevel";
import { MatrixClientPeg } from '../MatrixClientPeg';
const MAX_ROOMS = 20; // arbitrary const MAX_ROOMS = 20; // arbitrary
const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90s, the time we wait for an autojoined room to show up const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90s, the time we wait for an autojoined room to show up
@ -61,30 +59,6 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
return this.matrixClient && this.matrixClient.getVisibleRooms().length >= 20; return this.matrixClient && this.matrixClient.getVisibleRooms().length >= 20;
} }
componentDidUpdate(prevProps, prevState) {
const prevRoomCount = (prevState.rooms?.length || 0);
const currentRoomCount = (this.state.rooms?.length || 0)
// Only decrypting the breadcrumb rooms events on app initialisation
// when room count transitions from 0 to the number of rooms it contains
if (prevRoomCount === 0 && currentRoomCount > prevRoomCount) {
const client = MatrixClientPeg.get();
// Rooms in the breadcrumb have a good chance to be interacted with
// again by a user. Decrypting the messages ahead of time will help
// reduce content shift on first render
this.state.rooms?.forEach(async room => {
const [cryptoEvent] = room.currentState.getStateEvents(EventType.RoomEncryption);
if (cryptoEvent) {
if (!client.isRoomEncrypted(room.roomId)) {
// XXX: Private member access
await client._crypto.onCryptoEvent(cryptoEvent);
}
room?.decryptAllEvents();
}
});
}
}
protected async onAction(payload: ActionPayload) { protected async onAction(payload: ActionPayload) {
if (!this.matrixClient) return; if (!this.matrixClient) return;

View file

@ -34,6 +34,7 @@ import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm";
import { getListAlgorithmInstance } from "./list-ordering"; import { getListAlgorithmInstance } from "./list-ordering";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { VisibilityProvider } from "../filters/VisibilityProvider"; import { VisibilityProvider } from "../filters/VisibilityProvider";
import { MultiLock } from "../../../utils/MultiLock";
/** /**
* Fired when the Algorithm has determined a list has been updated. * Fired when the Algorithm has determined a list has been updated.
@ -77,6 +78,7 @@ export class Algorithm extends EventEmitter {
} = {}; } = {};
private allowedByFilter: Map<IFilterCondition, Room[]> = new Map<IFilterCondition, Room[]>(); private allowedByFilter: Map<IFilterCondition, Room[]> = new Map<IFilterCondition, Room[]>();
private allowedRoomsByFilters: Set<Room> = new Set<Room>(); private allowedRoomsByFilters: Set<Room> = new Set<Room>();
private handlerLock = new MultiLock();
/** /**
* Set to true to suspend emissions of algorithm updates. * Set to true to suspend emissions of algorithm updates.
@ -679,191 +681,204 @@ export class Algorithm extends EventEmitter {
public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> { public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> {
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
console.log(`Handle room update for ${room.roomId} called with cause ${cause}`); console.log(`Acquiring lock for ${room.roomId} with cause ${cause}`);
} }
if (!this.algorithms) throw new Error("Not ready: no algorithms to determine tags from"); const release = await this.handlerLock.acquire(room.roomId);
try {
// Note: check the isSticky against the room ID just in case the reference is wrong if (SettingsStore.getValue("advancedRoomListLogging")) {
const isSticky = this._stickyRoom && this._stickyRoom.room && this._stickyRoom.room.roomId === room.roomId; // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
if (cause === RoomUpdateCause.NewRoom) { console.log(`Handle room update for ${room.roomId} called with cause ${cause}`);
const isForLastSticky = this._lastStickyRoom && this._lastStickyRoom.room === room;
const roomTags = this.roomIdsToTags[room.roomId];
const hasTags = roomTags && roomTags.length > 0;
// Don't change the cause if the last sticky room is being re-added. If we fail to
// pass the cause through as NewRoom, we'll fail to lie to the algorithm and thus
// lose the room.
if (hasTags && !isForLastSticky) {
console.warn(`${room.roomId} is reportedly new but is already known - assuming TagChange instead`);
cause = RoomUpdateCause.PossibleTagChange;
} }
if (!this.algorithms) throw new Error("Not ready: no algorithms to determine tags from");
// Check to see if the room is known first // Note: check the isSticky against the room ID just in case the reference is wrong
let knownRoomRef = this.rooms.includes(room); const isSticky = this._stickyRoom && this._stickyRoom.room && this._stickyRoom.room.roomId === room.roomId;
if (hasTags && !knownRoomRef) { if (cause === RoomUpdateCause.NewRoom) {
console.warn(`${room.roomId} might be a reference change - attempting to update reference`); const isForLastSticky = this._lastStickyRoom && this._lastStickyRoom.room === room;
this.rooms = this.rooms.map(r => r.roomId === room.roomId ? room : r); const roomTags = this.roomIdsToTags[room.roomId];
knownRoomRef = this.rooms.includes(room); const hasTags = roomTags && roomTags.length > 0;
if (!knownRoomRef) {
console.warn(`${room.roomId} is still not referenced. It may be sticky.`); // Don't change the cause if the last sticky room is being re-added. If we fail to
// pass the cause through as NewRoom, we'll fail to lie to the algorithm and thus
// lose the room.
if (hasTags && !isForLastSticky) {
console.warn(`${room.roomId} is reportedly new but is already known - assuming TagChange instead`);
cause = RoomUpdateCause.PossibleTagChange;
}
// Check to see if the room is known first
let knownRoomRef = this.rooms.includes(room);
if (hasTags && !knownRoomRef) {
console.warn(`${room.roomId} might be a reference change - attempting to update reference`);
this.rooms = this.rooms.map(r => r.roomId === room.roomId ? room : r);
knownRoomRef = this.rooms.includes(room);
if (!knownRoomRef) {
console.warn(`${room.roomId} is still not referenced. It may be sticky.`);
}
}
// If we have tags for a room and don't have the room referenced, something went horribly
// wrong - the reference should have been updated above.
if (hasTags && !knownRoomRef && !isSticky) {
throw new Error(`${room.roomId} is missing from room array but is known`);
}
// Like above, update the reference to the sticky room if we need to
if (hasTags && isSticky) {
// Go directly in and set the sticky room's new reference, being careful not
// to trigger a sticky room update ourselves.
this._stickyRoom.room = room;
}
// If after all that we're still a NewRoom update, add the room if applicable.
// We don't do this for the sticky room (because it causes duplication issues)
// or if we know about the reference (as it should be replaced).
if (cause === RoomUpdateCause.NewRoom && !isSticky && !knownRoomRef) {
this.rooms.push(room);
} }
} }
// If we have tags for a room and don't have the room referenced, something went horribly let didTagChange = false;
// wrong - the reference should have been updated above. if (cause === RoomUpdateCause.PossibleTagChange) {
if (hasTags && !knownRoomRef && !isSticky) { const oldTags = this.roomIdsToTags[room.roomId] || [];
throw new Error(`${room.roomId} is missing from room array but is known - trying to find duplicate`); const newTags = this.getTagsForRoom(room);
} const diff = arrayDiff(oldTags, newTags);
if (diff.removed.length > 0 || diff.added.length > 0) {
for (const rmTag of diff.removed) {
if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
console.log(`Removing ${room.roomId} from ${rmTag}`);
}
const algorithm: OrderingAlgorithm = this.algorithms[rmTag];
if (!algorithm) throw new Error(`No algorithm for ${rmTag}`);
await algorithm.handleRoomUpdate(room, RoomUpdateCause.RoomRemoved);
this._cachedRooms[rmTag] = algorithm.orderedRooms;
this.recalculateFilteredRoomsForTag(rmTag); // update filter to re-sort the list
this.recalculateStickyRoom(rmTag); // update sticky room to make sure it moves if needed
}
for (const addTag of diff.added) {
if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
console.log(`Adding ${room.roomId} to ${addTag}`);
}
const algorithm: OrderingAlgorithm = this.algorithms[addTag];
if (!algorithm) throw new Error(`No algorithm for ${addTag}`);
await algorithm.handleRoomUpdate(room, RoomUpdateCause.NewRoom);
this._cachedRooms[addTag] = algorithm.orderedRooms;
}
// Like above, update the reference to the sticky room if we need to // Update the tag map so we don't regen it in a moment
if (hasTags && isSticky) { this.roomIdsToTags[room.roomId] = newTags;
// Go directly in and set the sticky room's new reference, being careful not
// to trigger a sticky room update ourselves.
this._stickyRoom.room = room;
}
// If after all that we're still a NewRoom update, add the room if applicable.
// We don't do this for the sticky room (because it causes duplication issues)
// or if we know about the reference (as it should be replaced).
if (cause === RoomUpdateCause.NewRoom && !isSticky && !knownRoomRef) {
this.rooms.push(room);
}
}
let didTagChange = false;
if (cause === RoomUpdateCause.PossibleTagChange) {
const oldTags = this.roomIdsToTags[room.roomId] || [];
const newTags = this.getTagsForRoom(room);
const diff = arrayDiff(oldTags, newTags);
if (diff.removed.length > 0 || diff.added.length > 0) {
for (const rmTag of diff.removed) {
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
console.log(`Removing ${room.roomId} from ${rmTag}`); console.log(`Changing update cause for ${room.roomId} to Timeline to sort rooms`);
} }
const algorithm: OrderingAlgorithm = this.algorithms[rmTag]; cause = RoomUpdateCause.Timeline;
if (!algorithm) throw new Error(`No algorithm for ${rmTag}`); didTagChange = true;
await algorithm.handleRoomUpdate(room, RoomUpdateCause.RoomRemoved);
this._cachedRooms[rmTag] = algorithm.orderedRooms;
this.recalculateFilteredRoomsForTag(rmTag); // update filter to re-sort the list
this.recalculateStickyRoom(rmTag); // update sticky room to make sure it moves if needed
}
for (const addTag of diff.added) {
if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
console.log(`Adding ${room.roomId} to ${addTag}`);
}
const algorithm: OrderingAlgorithm = this.algorithms[addTag];
if (!algorithm) throw new Error(`No algorithm for ${addTag}`);
await algorithm.handleRoomUpdate(room, RoomUpdateCause.NewRoom);
this._cachedRooms[addTag] = algorithm.orderedRooms;
}
// Update the tag map so we don't regen it in a moment
this.roomIdsToTags[room.roomId] = newTags;
if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
console.log(`Changing update cause for ${room.roomId} to Timeline to sort rooms`);
}
cause = RoomUpdateCause.Timeline;
didTagChange = true;
} else {
if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
console.log(`Received no-op update for ${room.roomId} - changing to Timeline update`);
}
cause = RoomUpdateCause.Timeline;
}
if (didTagChange && isSticky) {
// Manually update the tag for the sticky room without triggering a sticky room
// update. The update will be handled implicitly by the sticky room handling and
// requires no changes on our part, if we're in the middle of a sticky room change.
if (this._lastStickyRoom) {
this._stickyRoom = {
room,
tag: this.roomIdsToTags[room.roomId][0],
position: 0, // right at the top as it changed tags
};
} else { } else {
// We have to clear the lock as the sticky room change will trigger updates. if (SettingsStore.getValue("advancedRoomListLogging")) {
await this.setStickyRoom(room); // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
console.log(`Received no-op update for ${room.roomId} - changing to Timeline update`);
}
cause = RoomUpdateCause.Timeline;
}
if (didTagChange && isSticky) {
// Manually update the tag for the sticky room without triggering a sticky room
// update. The update will be handled implicitly by the sticky room handling and
// requires no changes on our part, if we're in the middle of a sticky room change.
if (this._lastStickyRoom) {
this._stickyRoom = {
room,
tag: this.roomIdsToTags[room.roomId][0],
position: 0, // right at the top as it changed tags
};
} else {
// We have to clear the lock as the sticky room change will trigger updates.
await this.setStickyRoom(room);
}
} }
} }
}
// If the update is for a room change which might be the sticky room, prevent it. We // If the update is for a room change which might be the sticky room, prevent it. We
// need to make sure that the causes (NewRoom and RoomRemoved) are still triggered though // need to make sure that the causes (NewRoom and RoomRemoved) are still triggered though
// as the sticky room relies on this. // as the sticky room relies on this.
if (cause !== RoomUpdateCause.NewRoom && cause !== RoomUpdateCause.RoomRemoved) { if (cause !== RoomUpdateCause.NewRoom && cause !== RoomUpdateCause.RoomRemoved) {
if (this.stickyRoom === room) { if (this.stickyRoom === room) {
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
console.warn(`[RoomListDebug] Received ${cause} update for sticky room ${room.roomId} - ignoring`); console.warn(
`[RoomListDebug] Received ${cause} update for sticky room ${room.roomId} - ignoring`,
);
}
return false;
} }
return false;
} }
}
if (!this.roomIdsToTags[room.roomId]) { if (!this.roomIdsToTags[room.roomId]) {
if (CAUSES_REQUIRING_ROOM.includes(cause)) { if (CAUSES_REQUIRING_ROOM.includes(cause)) {
if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
console.warn(`Skipping tag update for ${room.roomId} because we don't know about the room`);
}
return false;
}
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
console.warn(`Skipping tag update for ${room.roomId} because we don't know about the room`); console.log(`[RoomListDebug] Updating tags for room ${room.roomId} (${room.name})`);
}
// Get the tags for the room and populate the cache
const roomTags = this.getTagsForRoom(room).filter(t => !isNullOrUndefined(this.cachedRooms[t]));
// "This should never happen" condition - we specify DefaultTagID.Untagged in getTagsForRoom(),
// which means we should *always* have a tag to go off of.
if (!roomTags.length) throw new Error(`Tags cannot be determined for ${room.roomId}`);
this.roomIdsToTags[room.roomId] = roomTags;
if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
console.log(`[RoomListDebug] Updated tags for ${room.roomId}:`, roomTags);
} }
return false;
} }
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
console.log(`[RoomListDebug] Updating tags for room ${room.roomId} (${room.name})`); console.log(`[RoomListDebug] Reached algorithmic handling for ${room.roomId} and cause ${cause}`);
} }
// Get the tags for the room and populate the cache const tags = this.roomIdsToTags[room.roomId];
const roomTags = this.getTagsForRoom(room).filter(t => !isNullOrUndefined(this.cachedRooms[t])); if (!tags) {
console.warn(`No tags known for "${room.name}" (${room.roomId})`);
return false;
}
// "This should never happen" condition - we specify DefaultTagID.Untagged in getTagsForRoom(), let changed = didTagChange;
// which means we should *always* have a tag to go off of. for (const tag of tags) {
if (!roomTags.length) throw new Error(`Tags cannot be determined for ${room.roomId}`); const algorithm: OrderingAlgorithm = this.algorithms[tag];
if (!algorithm) throw new Error(`No algorithm for ${tag}`);
this.roomIdsToTags[room.roomId] = roomTags; await algorithm.handleRoomUpdate(room, cause);
this._cachedRooms[tag] = algorithm.orderedRooms;
// Flag that we've done something
this.recalculateFilteredRoomsForTag(tag); // update filter to re-sort the list
this.recalculateStickyRoom(tag); // update sticky room to make sure it appears if needed
changed = true;
}
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
console.log(`[RoomListDebug] Updated tags for ${room.roomId}:`, roomTags); console.log(
`[RoomListDebug] Finished handling ${room.roomId} with cause ${cause} (changed=${changed})`,
);
} }
return changed;
} finally {
release();
} }
if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
console.log(`[RoomListDebug] Reached algorithmic handling for ${room.roomId} and cause ${cause}`);
}
const tags = this.roomIdsToTags[room.roomId];
if (!tags) {
console.warn(`No tags known for "${room.name}" (${room.roomId})`);
return false;
}
let changed = didTagChange;
for (const tag of tags) {
const algorithm: OrderingAlgorithm = this.algorithms[tag];
if (!algorithm) throw new Error(`No algorithm for ${tag}`);
await algorithm.handleRoomUpdate(room, cause);
this._cachedRooms[tag] = algorithm.orderedRooms;
// Flag that we've done something
this.recalculateFilteredRoomsForTag(tag); // update filter to re-sort the list
this.recalculateStickyRoom(tag); // update sticky room to make sure it appears if needed
changed = true;
}
if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
console.log(`[RoomListDebug] Finished handling ${room.roomId} with cause ${cause} (changed=${changed})`);
}
return changed;
} }
} }

30
src/utils/MultiLock.ts Normal file
View file

@ -0,0 +1,30 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { EnhancedMap } from "./maps";
import AwaitLock from "await-lock";
export type DoneFn = () => void;
export class MultiLock {
private locks = new EnhancedMap<string, AwaitLock>();
public async acquire(key: string): Promise<DoneFn> {
const lock = this.locks.getOrCreate(key, new AwaitLock());
await lock.acquireAsync();
return () => lock.release();
}
}

View file

@ -75,7 +75,8 @@ export function arraySmoothingResample(input: number[], points: number): number[
for (let i = 1; i < input.length - 1; i += 2) { for (let i = 1; i < input.length - 1; i += 2) {
const prevPoint = input[i - 1]; const prevPoint = input[i - 1];
const nextPoint = input[i + 1]; const nextPoint = input[i + 1];
const average = (prevPoint + nextPoint) / 2; const currPoint = input[i];
const average = (prevPoint + nextPoint + currPoint) / 3;
samples.push(average); samples.push(average);
} }
input = samples; input = samples;

View file

@ -346,9 +346,14 @@ export function tryTransformPermalinkToLocalHref(permalink: string): string {
return permalink; return permalink;
} }
const m = decodeURIComponent(permalink).match(matrixLinkify.ELEMENT_URL_PATTERN); try {
if (m) { const m = decodeURIComponent(permalink).match(matrixLinkify.ELEMENT_URL_PATTERN);
return m[1]; if (m) {
return m[1];
}
} catch (e) {
// Not a valid URI
return permalink;
} }
// A bit of a hack to convert permalinks of unknown origin to Element links // A bit of a hack to convert permalinks of unknown origin to Element links

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2019, 2020 The Matrix.org Foundation C.I.C. Copyright 2019, 2020, 2021 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.
@ -14,16 +14,19 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {MatrixClientPeg} from './MatrixClientPeg'; import { User } from "matrix-js-sdk/src/models/user";
import { MatrixClientPeg } from './MatrixClientPeg';
import dis from "./dispatcher/dispatcher"; import dis from "./dispatcher/dispatcher";
import Modal from './Modal'; import Modal from './Modal';
import * as sdk from './index'; import * as sdk from './index';
import { _t } from './languageHandler'; import { RightPanelPhases } from "./stores/RightPanelStorePhases";
import {RightPanelPhases} from "./stores/RightPanelStorePhases"; import { findDMForUser } from './createRoom';
import {findDMForUser} from './createRoom'; import { accessSecretStorage } from './SecurityManager';
import {accessSecretStorage} from './SecurityManager'; import { verificationMethods } from 'matrix-js-sdk/src/crypto';
import {verificationMethods} from 'matrix-js-sdk/src/crypto'; import { Action } from './dispatcher/actions';
import {Action} from './dispatcher/actions'; import UntrustedDeviceDialog from "./components/views/dialogs/UntrustedDeviceDialog";
import {IDevice} from "./components/views/right_panel/UserInfo";
async function enable4SIfNeeded() { async function enable4SIfNeeded() {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
@ -39,40 +42,7 @@ async function enable4SIfNeeded() {
return true; return true;
} }
function UntrustedDeviceDialog(props) { export async function verifyDevice(user: User, device: IDevice) {
const {device, user, onFinished} = props;
const BaseDialog = sdk.getComponent("dialogs.BaseDialog");
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
let askToVerifyText;
let newSessionText;
if (MatrixClientPeg.get().getUserId() === user.userId) {
newSessionText = _t("You signed in to a new session without verifying it:");
askToVerifyText = _t("Verify your other session using one of the options below.");
} else {
newSessionText = _t("%(name)s (%(userId)s) signed in to a new session without verifying it:",
{name: user.displayName, userId: user.userId});
askToVerifyText = _t("Ask this user to verify their session, or manually verify it below.");
}
return <BaseDialog
onFinished={onFinished}
headerImage={require("../res/img/e2e/warning.svg")}
title={_t("Not Trusted")}>
<div className="mx_Dialog_content" id='mx_Dialog_content'>
<p>{newSessionText}</p>
<p>{device.getDisplayName()} ({device.deviceId})</p>
<p>{askToVerifyText}</p>
</div>
<div className='mx_Dialog_buttons'>
<AccessibleButton element="button" kind="secondary" onClick={() => onFinished("legacy")}>{_t("Manually Verify by Text")}</AccessibleButton>
<AccessibleButton element="button" kind="secondary" onClick={() => onFinished("sas")}>{_t("Interactively verify by Emoji")}</AccessibleButton>
<AccessibleButton kind="primary" onClick={() => onFinished()}>{_t("Done")}</AccessibleButton>
</div>
</BaseDialog>;
}
export async function verifyDevice(user, device) {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
if (cli.isGuest()) { if (cli.isGuest()) {
dis.dispatch({action: 'require_registration'}); dis.dispatch({action: 'require_registration'});
@ -115,7 +85,7 @@ export async function verifyDevice(user, device) {
}); });
} }
export async function legacyVerifyUser(user) { export async function legacyVerifyUser(user: User) {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
if (cli.isGuest()) { if (cli.isGuest()) {
dis.dispatch({action: 'require_registration'}); dis.dispatch({action: 'require_registration'});
@ -135,7 +105,7 @@ export async function legacyVerifyUser(user) {
}); });
} }
export async function verifyUser(user) { export async function verifyUser(user: User) {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
if (cli.isGuest()) { if (cli.isGuest()) {
dis.dispatch({action: 'require_registration'}); dis.dispatch({action: 'require_registration'});
@ -155,7 +125,7 @@ export async function verifyUser(user) {
}); });
} }
export function pendingVerificationRequestForUser(user) { export function pendingVerificationRequestForUser(user: User) {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const dmRoom = findDMForUser(cli, user.userId); const dmRoom = findDMForUser(cli, user.userId);
if (dmRoom) { if (dmRoom) {

View file

@ -15,12 +15,13 @@ limitations under the License.
*/ */
import EventEmitter from "events"; import EventEmitter from "events";
import {UPDATE_EVENT} from "../stores/AsyncStore"; import { UPDATE_EVENT } from "../stores/AsyncStore";
import {arrayFastResample, arrayRescale, arraySeed, arraySmoothingResample} from "../utils/arrays"; import { arrayFastResample, arrayRescale, arraySeed, arraySmoothingResample } from "../utils/arrays";
import {SimpleObservable} from "matrix-widget-api"; import { SimpleObservable } from "matrix-widget-api";
import {IDestroyable} from "../utils/IDestroyable"; import { IDestroyable } from "../utils/IDestroyable";
import {PlaybackClock} from "./PlaybackClock"; import { PlaybackClock } from "./PlaybackClock";
import {createAudioContext, decodeOgg} from "./compat"; import { createAudioContext, decodeOgg } from "./compat";
import { clamp } from "../utils/numbers";
export enum PlaybackState { export enum PlaybackState {
Decoding = "decoding", Decoding = "decoding",
@ -33,9 +34,20 @@ export const PLAYBACK_WAVEFORM_SAMPLES = 39;
const DEFAULT_WAVEFORM = arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES); const DEFAULT_WAVEFORM = arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES);
function makePlaybackWaveform(input: number[]): number[] { function makePlaybackWaveform(input: number[]): number[] {
// We use a smoothing resample to keep the rough shape of the waveform the user will be seeing. We // First, convert negative amplitudes to positive so we don't detect zero as "noisy".
// then rescale so the user can see the waveform properly (loud noises == 100%). const noiseWaveform = input.map(v => Math.abs(v));
return arrayRescale(arraySmoothingResample(input, PLAYBACK_WAVEFORM_SAMPLES), 0, 1);
// Next, we'll resample the waveform using a smoothing approach so we can keep the same rough shape.
// We also rescale the waveform to be 0-1 for the remaining function logic.
const resampled = arrayRescale(arraySmoothingResample(noiseWaveform, PLAYBACK_WAVEFORM_SAMPLES), 0, 1);
// Then, we'll do a high and low pass filter to isolate actual speaking volumes within the rescaled
// waveform. Most speech happens below the 0.5 mark.
const filtered = resampled.map(v => clamp(v, 0.1, 0.5));
// Finally, we'll rescale the filtered waveform (0.1-0.5 becomes 0-1 again) so the user sees something
// sensible. This is what we return to keep our contract of "values between zero and one".
return arrayRescale(filtered, 0, 1);
} }
export class Playback extends EventEmitter implements IDestroyable { export class Playback extends EventEmitter implements IDestroyable {
@ -126,6 +138,7 @@ export class Playback extends EventEmitter implements IDestroyable {
this.waveformObservable.update(this.resampledWaveform); this.waveformObservable.update(this.resampledWaveform);
this.emit(PlaybackState.Stopped); // signal that we're not decoding anymore this.emit(PlaybackState.Stopped); // signal that we're not decoding anymore
this.clock.flagLoadTime(); // must happen first because setting the duration fires a clock update
this.clock.durationSeconds = this.audioBuf.duration; this.clock.durationSeconds = this.audioBuf.duration;
} }

View file

@ -54,6 +54,15 @@ export class PlaybackClock implements IDestroyable {
} }
}; };
/**
* Mark the time in the audio context where the clip starts/has been loaded.
* This is to ensure the clock isn't skewed into thinking it is ~0.5s into
* a clip when the duration is set.
*/
public flagLoadTime() {
this.clipStart = this.context.currentTime;
}
public flagStart() { public flagStart() {
if (this.stopped) { if (this.stopped) {
this.clipStart = this.context.currentTime; this.clipStart = this.context.currentTime;

View file

@ -73,10 +73,10 @@ describe('arrays', () => {
// we'd be feeding a thousand values in and seeing what a curve of 250 values looks like, // we'd be feeding a thousand values in and seeing what a curve of 250 values looks like,
// but that's not really feasible to manually verify accuracy. // but that's not really feasible to manually verify accuracy.
[ [
{input: [2, 2, 0, 2, 2, 0, 2, 2, 0], output: [1, 1, 2, 1]}, // Odd -> Even {input: [4, 4, 1, 4, 4, 1, 4, 4, 1], output: [3, 3, 3, 3]}, // Odd -> Even
{input: [2, 2, 0, 2, 2, 0, 2, 2, 0], output: [1, 1, 2]}, // Odd -> Odd {input: [4, 4, 1, 4, 4, 1, 4, 4, 1], output: [3, 3, 3]}, // Odd -> Odd
{input: [2, 2, 0, 2, 2, 0, 2, 2], output: [1, 1, 2]}, // Even -> Odd {input: [4, 4, 1, 4, 4, 1, 4, 4], output: [3, 3, 3]}, // Even -> Odd
{input: [2, 2, 0, 2, 2, 0, 2, 2], output: [1, 2]}, // Even -> Even {input: [4, 4, 1, 4, 4, 1, 4, 4], output: [3, 3]}, // Even -> Even
].forEach((c, i) => expectSample(i, c.input, c.output, true)); ].forEach((c, i) => expectSample(i, c.input, c.output, true));
}); });