mirror of
https://github.com/element-hq/element-web
synced 2024-11-25 18:55:58 +03:00
Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
commit
b762303102
181 changed files with 4794 additions and 3252 deletions
|
@ -86,6 +86,7 @@
|
||||||
@import "./views/dialogs/_IncomingSasDialog.scss";
|
@import "./views/dialogs/_IncomingSasDialog.scss";
|
||||||
@import "./views/dialogs/_InviteDialog.scss";
|
@import "./views/dialogs/_InviteDialog.scss";
|
||||||
@import "./views/dialogs/_KeyboardShortcutsDialog.scss";
|
@import "./views/dialogs/_KeyboardShortcutsDialog.scss";
|
||||||
|
@import "./views/dialogs/_ManageRestrictedJoinRuleDialog.scss";
|
||||||
@import "./views/dialogs/_MessageEditHistoryDialog.scss";
|
@import "./views/dialogs/_MessageEditHistoryDialog.scss";
|
||||||
@import "./views/dialogs/_ModalWidgetDialog.scss";
|
@import "./views/dialogs/_ModalWidgetDialog.scss";
|
||||||
@import "./views/dialogs/_NewSessionReviewDialog.scss";
|
@import "./views/dialogs/_NewSessionReviewDialog.scss";
|
||||||
|
@ -172,7 +173,6 @@
|
||||||
@import "./views/messages/_MStickerBody.scss";
|
@import "./views/messages/_MStickerBody.scss";
|
||||||
@import "./views/messages/_MTextBody.scss";
|
@import "./views/messages/_MTextBody.scss";
|
||||||
@import "./views/messages/_MVideoBody.scss";
|
@import "./views/messages/_MVideoBody.scss";
|
||||||
@import "./views/messages/_MVoiceMessageBody.scss";
|
|
||||||
@import "./views/messages/_MediaBody.scss";
|
@import "./views/messages/_MediaBody.scss";
|
||||||
@import "./views/messages/_MessageActionBar.scss";
|
@import "./views/messages/_MessageActionBar.scss";
|
||||||
@import "./views/messages/_MessageTimestamp.scss";
|
@import "./views/messages/_MessageTimestamp.scss";
|
||||||
|
|
|
@ -45,9 +45,14 @@ limitations under the License.
|
||||||
|
|
||||||
/* Overrides for the attachment body tiles */
|
/* Overrides for the attachment body tiles */
|
||||||
|
|
||||||
.mx_FilePanel .mx_EventTile {
|
.mx_FilePanel .mx_EventTile:not([data-layout=bubble]) {
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
margin-top: 32px;
|
margin-top: 10px;
|
||||||
|
padding-top: 0;
|
||||||
|
|
||||||
|
.mx_EventTile_line {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_FilePanel .mx_EventTile .mx_MImageBody {
|
.mx_FilePanel .mx_EventTile .mx_MImageBody {
|
||||||
|
|
|
@ -84,7 +84,7 @@ limitations under the License.
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_NotificationPanel .mx_EventTile_senderDetails {
|
.mx_NotificationPanel .mx_EventTile:not([data-layout=bubble]) .mx_EventTile_senderDetails {
|
||||||
padding-left: 36px; // align with the room name
|
padding-left: 36px; // align with the room name
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
@ -105,7 +105,7 @@ limitations under the License.
|
||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_NotificationPanel .mx_EventTile_line {
|
.mx_NotificationPanel .mx_EventTile:not([data-layout=bubble]) .mx_EventTile_line {
|
||||||
margin-right: 0px;
|
margin-right: 0px;
|
||||||
padding-left: 36px; // align with the room name
|
padding-left: 36px; // align with the room name
|
||||||
padding-top: 0px;
|
padding-top: 0px;
|
||||||
|
|
|
@ -234,6 +234,9 @@ $SpaceRoomViewInnerWidth: 428px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SpaceRoomView_landing {
|
.mx_SpaceRoomView_landing {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
> .mx_BaseAvatar_image,
|
> .mx_BaseAvatar_image,
|
||||||
> .mx_BaseAvatar > .mx_BaseAvatar_image {
|
> .mx_BaseAvatar > .mx_BaseAvatar_image {
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
@ -340,6 +343,7 @@ $SpaceRoomViewInnerWidth: 428px;
|
||||||
|
|
||||||
.mx_SearchBox {
|
.mx_SearchBox {
|
||||||
margin: 0 0 20px;
|
margin: 0 0 20px;
|
||||||
|
flex: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SpaceFeedbackPrompt {
|
.mx_SpaceFeedbackPrompt {
|
||||||
|
@ -350,6 +354,11 @@ $SpaceRoomViewInnerWidth: 428px;
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_SpaceRoomDirectory_list {
|
||||||
|
// we don't want this container to get forced into the flexbox layout
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SpaceRoomView_privateScope {
|
.mx_SpaceRoomView_privateScope {
|
||||||
|
|
|
@ -14,9 +14,8 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_AudioPlayer_container {
|
.mx_MediaBody.mx_AudioPlayer_container {
|
||||||
padding: 16px 12px 12px 12px;
|
padding: 16px 12px 12px 12px;
|
||||||
max-width: 267px; // use max to make the control fit in the files/pinned panels
|
|
||||||
|
|
||||||
.mx_AudioPlayer_primaryContainer {
|
.mx_AudioPlayer_primaryContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -18,10 +18,10 @@ limitations under the License.
|
||||||
// are shared amongst multiple voice message components.
|
// are shared amongst multiple voice message components.
|
||||||
|
|
||||||
// Container for live recording and playback controls
|
// Container for live recording and playback controls
|
||||||
.mx_VoiceMessagePrimaryContainer {
|
.mx_MediaBody.mx_VoiceMessagePrimaryContainer {
|
||||||
// 7px top and bottom for visual design. 12px left & right, but the waveform (right)
|
// The waveform (right) has a 1px padding on it that we want to account for, otherwise
|
||||||
// has a 1px padding on it that we want to account for.
|
// inherit from mx_MediaBody
|
||||||
padding: 7px 12px 7px 11px;
|
padding-right: 11px;
|
||||||
|
|
||||||
// Cheat at alignment a bit
|
// Cheat at alignment a bit
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -27,7 +27,6 @@ limitations under the License.
|
||||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=255139
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=255139
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
line-height: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_BaseAvatar_initial {
|
.mx_BaseAvatar_initial {
|
||||||
|
|
|
@ -65,7 +65,7 @@ limitations under the License.
|
||||||
.mx_CreateRoomDialog_aliasContainer {
|
.mx_CreateRoomDialog_aliasContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
// put margin on container so it can collapse with siblings
|
// put margin on container so it can collapse with siblings
|
||||||
margin: 10px 0;
|
margin: 24px 0 10px;
|
||||||
|
|
||||||
.mx_RoomAliasField {
|
.mx_RoomAliasField {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -101,10 +101,6 @@ limitations under the License.
|
||||||
margin-left: 30px;
|
margin-left: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CreateRoomDialog_topic {
|
|
||||||
margin-bottom: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_Dialog_content > .mx_SettingsFlag {
|
.mx_Dialog_content > .mx_SettingsFlag {
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
}
|
}
|
||||||
|
@ -113,5 +109,56 @@ limitations under the License.
|
||||||
margin: 0 85px 0 0;
|
margin: 0 85px 0 0;
|
||||||
font-size: $font-12px;
|
font-size: $font-12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_Dropdown {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: normal;
|
||||||
|
font-family: $font-family;
|
||||||
|
font-size: $font-14px;
|
||||||
|
color: $primary-fg-color;
|
||||||
|
|
||||||
|
.mx_Dropdown_input {
|
||||||
|
border: 1px solid $input-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Dropdown_option {
|
||||||
|
font-size: $font-14px;
|
||||||
|
line-height: $font-32px;
|
||||||
|
height: 32px;
|
||||||
|
min-height: 32px;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
padding-left: 30px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
left: 6px;
|
||||||
|
top: 8px;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-position: center;
|
||||||
|
background-color: $secondary-fg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CreateRoomDialog_dropdown_invite::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/lock.svg');
|
||||||
|
mask-size: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CreateRoomDialog_dropdown_public::before {
|
||||||
|
mask-image: url('$(res)/img/globe.svg');
|
||||||
|
mask-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CreateRoomDialog_dropdown_restricted::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/community-members.svg');
|
||||||
|
mask-size: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,10 @@ limitations under the License.
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.mx_EventTile[data-layout=bubble] {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
150
res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.scss
Normal file
150
res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.scss
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
/*
|
||||||
|
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_ManageRestrictedJoinRuleDialog_wrapper {
|
||||||
|
.mx_Dialog {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ManageRestrictedJoinRuleDialog {
|
||||||
|
width: 480px;
|
||||||
|
color: $primary-fg-color;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
min-height: 0;
|
||||||
|
height: 60vh;
|
||||||
|
|
||||||
|
.mx_SearchBox {
|
||||||
|
// To match the space around the title
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ManageRestrictedJoinRuleDialog_content {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ManageRestrictedJoinRuleDialog_noResults {
|
||||||
|
display: block;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ManageRestrictedJoinRuleDialog_section {
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
font-size: $font-12px;
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
line-height: $font-15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ManageRestrictedJoinRuleDialog_entry {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 12px;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.mx_RoomAvatar_isSpaceRoom,
|
||||||
|
.mx_RoomAvatar_isSpaceRoom img {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ManageRestrictedJoinRuleDialog_entry_name {
|
||||||
|
margin: 0 8px;
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: 30px;
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ManageRestrictedJoinRuleDialog_entry_description {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: $font-12px;
|
||||||
|
line-height: $font-15px;
|
||||||
|
color: $tertiary-fg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Checkbox {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ManageRestrictedJoinRuleDialog_section_spaces {
|
||||||
|
.mx_BaseAvatar {
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_BaseAvatar_image {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ManageRestrictedJoinRuleDialog_section_info {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 12px 0;
|
||||||
|
padding: 8px 8px 8px 42px;
|
||||||
|
background-color: $header-panel-bg-color;
|
||||||
|
|
||||||
|
font-size: $font-12px;
|
||||||
|
line-height: $font-15px;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 10px;
|
||||||
|
top: calc(50% - 8px); // vertical centering
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
background-color: $secondary-fg-color;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
|
||||||
|
mask-position: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ManageRestrictedJoinRuleDialog_footer {
|
||||||
|
margin-top: 20px;
|
||||||
|
|
||||||
|
.mx_ManageRestrictedJoinRuleDialog_footer_buttons {
|
||||||
|
display: flex;
|
||||||
|
width: max-content;
|
||||||
|
margin-left: auto;
|
||||||
|
|
||||||
|
.mx_AccessibleButton {
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
& + .mx_AccessibleButton {
|
||||||
|
margin-left: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,7 +27,7 @@ limitations under the License.
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: 3px;
|
border-radius: 4px;
|
||||||
border: 1px solid $strong-input-border-color;
|
border: 1px solid $strong-input-border-color;
|
||||||
font-size: $font-12px;
|
font-size: $font-12px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
@ -109,7 +109,7 @@ input.mx_Dropdown_option:focus {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
border-radius: 3px;
|
border-radius: 4px;
|
||||||
border: 1px solid $input-focused-border-color;
|
border: 1px solid $input-focused-border-color;
|
||||||
background-color: $primary-bg-color;
|
background-color: $primary-bg-color;
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
|
|
|
@ -19,8 +19,9 @@ limitations under the License.
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
padding-left: 10px;
|
padding: 0 10px;
|
||||||
border-left: 4px solid $button-bg-color;
|
border-left: 2px solid $button-bg-color;
|
||||||
|
border-radius: 2px;
|
||||||
|
|
||||||
.mx_ReplyThread_show {
|
.mx_ReplyThread_show {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
|
@ -60,12 +60,6 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MFileBody_info {
|
.mx_MFileBody_info {
|
||||||
background-color: $message-body-panel-bg-color;
|
|
||||||
border-radius: 12px;
|
|
||||||
width: 243px; // same width as a playable voice message, accounting for padding
|
|
||||||
padding: 6px 12px;
|
|
||||||
color: $message-body-panel-fg-color;
|
|
||||||
|
|
||||||
.mx_MFileBody_info_icon {
|
.mx_MFileBody_info_icon {
|
||||||
background-color: $message-body-panel-icon-bg-color;
|
background-color: $message-body-panel-icon-bg-color;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
|
|
|
@ -16,10 +16,6 @@ limitations under the License.
|
||||||
|
|
||||||
$timelineImageBorderRadius: 4px;
|
$timelineImageBorderRadius: 4px;
|
||||||
|
|
||||||
.mx_MImageBody {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_MImageBody_thumbnail {
|
.mx_MImageBody_thumbnail {
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
border-radius: $timelineImageBorderRadius;
|
border-radius: $timelineImageBorderRadius;
|
||||||
|
@ -28,7 +24,7 @@ $timelineImageBorderRadius: 4px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
> canvas {
|
> div > canvas {
|
||||||
border-radius: $timelineImageBorderRadius;
|
border-radius: $timelineImageBorderRadius;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,9 +20,11 @@ limitations under the License.
|
||||||
.mx_MediaBody {
|
.mx_MediaBody {
|
||||||
background-color: $message-body-panel-bg-color;
|
background-color: $message-body-panel-bg-color;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
max-width: 243px; // use max-width instead of width so it fits within right panels
|
||||||
|
|
||||||
color: $message-body-panel-fg-color;
|
color: $message-body-panel-fg-color;
|
||||||
font-size: $font-14px;
|
font-size: $font-14px;
|
||||||
line-height: $font-24px;
|
line-height: $font-24px;
|
||||||
}
|
|
||||||
|
|
||||||
|
padding: 6px 12px;
|
||||||
|
}
|
||||||
|
|
|
@ -38,7 +38,8 @@ limitations under the License.
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover,
|
||||||
|
&.mx_EventTile_selected {
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -80,7 +81,7 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_MessageActionBar {
|
.mx_MessageActionBar {
|
||||||
right: 0;
|
right: 0;
|
||||||
transform: translate3d(50%, 50%, 0);
|
transform: translate3d(90%, 50%, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
--backgroundColor: $eventbubble-others-bg;
|
--backgroundColor: $eventbubble-others-bg;
|
||||||
|
@ -91,12 +92,17 @@ limitations under the License.
|
||||||
float: right;
|
float: right;
|
||||||
> a {
|
> a {
|
||||||
left: auto;
|
left: auto;
|
||||||
right: -48px;
|
right: -68px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.mx_SenderProfile {
|
.mx_SenderProfile {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_ReplyTile .mx_SenderProfile {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_ReactionsRow {
|
.mx_ReactionsRow {
|
||||||
float: right;
|
float: right;
|
||||||
clear: right;
|
clear: right;
|
||||||
|
@ -126,7 +132,9 @@ limitations under the License.
|
||||||
margin: 0 -12px 0 -9px;
|
margin: 0 -12px 0 -9px;
|
||||||
> a {
|
> a {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: -48px;
|
padding: 10px 20px;
|
||||||
|
top: 0;
|
||||||
|
left: -68px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,13 +155,17 @@ limitations under the License.
|
||||||
.mx_EventTile_avatar {
|
.mx_EventTile_avatar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
line-height: 1;
|
|
||||||
img {
|
img {
|
||||||
box-shadow: 0 0 0 3px $eventbubble-avatar-outline;
|
box-shadow: 0 0 0 3px $eventbubble-avatar-outline;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_BaseAvatar,
|
||||||
|
.mx_EventTile_avatar {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
&[data-has-reply=true] {
|
&[data-has-reply=true] {
|
||||||
> .mx_EventTile_line {
|
> .mx_EventTile_line {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -213,6 +225,7 @@ limitations under the License.
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
padding: 5px 0;
|
||||||
|
|
||||||
.mx_EventTile_avatar {
|
.mx_EventTile_avatar {
|
||||||
position: static;
|
position: static;
|
||||||
|
@ -254,7 +267,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MessageActionBar {
|
.mx_MessageActionBar {
|
||||||
transform: translate3d(50%, 0, 0);
|
transform: translate3d(90%, 0, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -279,7 +292,7 @@ limitations under the License.
|
||||||
& + .mx_EventListSummary {
|
& + .mx_EventListSummary {
|
||||||
.mx_EventTile {
|
.mx_EventTile {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
padding: 0;
|
padding: 2px 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -132,7 +132,8 @@ $hover-select-border: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_EventTile_info .mx_EventTile_line {
|
&.mx_EventTile_info .mx_EventTile_line,
|
||||||
|
& ~ .mx_EventListSummary .mx_EventTile_avatar ~ .mx_EventTile_line {
|
||||||
padding-left: calc($left-gutter + 18px);
|
padding-left: calc($left-gutter + 18px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -471,6 +472,10 @@ $hover-select-border: 4px;
|
||||||
background-color: $header-panel-bg-color;
|
background-color: $header-panel-bg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pre code > * {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
// have to use overlay rather than auto otherwise Linux and Windows
|
// have to use overlay rather than auto otherwise Linux and Windows
|
||||||
// Chrome gets very confused about vertical spacing:
|
// Chrome gets very confused about vertical spacing:
|
||||||
|
|
|
@ -26,6 +26,7 @@ $left-gutter: 64px;
|
||||||
|
|
||||||
> .mx_EventTile_avatar {
|
> .mx_EventTile_avatar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
z-index: 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MessageTimestamp {
|
.mx_MessageTimestamp {
|
||||||
|
|
|
@ -19,7 +19,8 @@ limitations under the License.
|
||||||
margin-right: 15px;
|
margin-right: 15px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
display: flex;
|
display: flex;
|
||||||
border-left: 4px solid $preview-widget-bar-color;
|
border-left: 2px solid $preview-widget-bar-color;
|
||||||
|
border-radius: 2px;
|
||||||
color: $preview-widget-fg-color;
|
color: $preview-widget-fg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,7 +34,7 @@ limitations under the License.
|
||||||
.mx_LinkPreviewWidget_caption {
|
.mx_LinkPreviewWidget_caption {
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
overflow-x: hidden; // cause it to wrap rather than clip
|
overflow: hidden; // cause it to wrap rather than clip
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_LinkPreviewWidget_title {
|
.mx_LinkPreviewWidget_title {
|
||||||
|
|
|
@ -29,8 +29,10 @@ limitations under the License.
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
// min-height at this level so the mx_BasicMessageComposer_input
|
// min-height at this level so the mx_BasicMessageComposer_input
|
||||||
// still stays vertically centered when less than 50px
|
// still stays vertically centered when less than 55px.
|
||||||
min-height: 50px;
|
// We also set this to ensure the voice message recording widget
|
||||||
|
// doesn't cause a jump.
|
||||||
|
min-height: 55px;
|
||||||
|
|
||||||
.mx_BasicMessageComposer_input {
|
.mx_BasicMessageComposer_input {
|
||||||
padding: 3px 0;
|
padding: 3px 0;
|
||||||
|
|
|
@ -47,14 +47,14 @@ limitations under the License.
|
||||||
color: $settings-subsection-fg-color;
|
color: $settings-subsection-fg-color;
|
||||||
font-size: $font-14px;
|
font-size: $font-14px;
|
||||||
display: block;
|
display: block;
|
||||||
margin: 10px 100px 10px 0; // Align with the rest of the view
|
margin: 10px 80px 10px 0; // Align with the rest of the view
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SettingsTab_section {
|
.mx_SettingsTab_section {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
|
|
||||||
.mx_SettingsFlag {
|
.mx_SettingsFlag {
|
||||||
margin-right: 100px;
|
margin-right: 80px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,44 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
.mx_SecurityRoomSettingsTab {
|
||||||
|
.mx_SettingsTab_showAdvanced {
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SecurityRoomSettingsTab_spacesWithAccess {
|
||||||
|
> h4 {
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
font-size: $font-12px;
|
||||||
|
line-height: $font-15px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
> span {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: $font-14px;
|
||||||
|
line-height: 32px; // matches height of avatar for v-align
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
img.mx_RoomAvatar_isSpaceRoom,
|
||||||
|
.mx_RoomAvatar_isSpaceRoom img {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_BaseAvatar {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& + span {
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_SecurityRoomSettingsTab_warning {
|
.mx_SecurityRoomSettingsTab_warning {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
|
@ -26,5 +64,51 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SecurityRoomSettingsTab_encryptionSection {
|
.mx_SecurityRoomSettingsTab_encryptionSection {
|
||||||
margin-bottom: 25px;
|
padding-bottom: 24px;
|
||||||
|
border-bottom: 1px solid $menu-border-color;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SecurityRoomSettingsTab_upgradeRequired {
|
||||||
|
margin-left: 16px;
|
||||||
|
padding: 4px 16px;
|
||||||
|
border: 1px solid $accent-color;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: $accent-color;
|
||||||
|
font-size: $font-12px;
|
||||||
|
line-height: $font-15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SecurityRoomSettingsTab_joinRule {
|
||||||
|
.mx_RadioButton {
|
||||||
|
padding-top: 16px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
.mx_RadioButton_content {
|
||||||
|
margin-left: 14px;
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
color: $primary-fg-color;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> span {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 34px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
|
||||||
|
& + .mx_RadioButton {
|
||||||
|
border-top: 1px solid $menu-border-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton_kind_link {
|
||||||
|
padding: 0;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,8 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_AppearanceUserSettingsTab_fontSlider,
|
.mx_AppearanceUserSettingsTab_fontSlider,
|
||||||
.mx_AppearanceUserSettingsTab_fontSlider_preview,
|
.mx_AppearanceUserSettingsTab_fontSlider_preview {
|
||||||
.mx_AppearanceUserSettingsTab_Layout {
|
|
||||||
@mixin mx_Settings_fullWidthField;
|
@mixin mx_Settings_fullWidthField;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,13 +158,10 @@ limitations under the License.
|
||||||
.mx_AppearanceUserSettingsTab_Layout_RadioButtons {
|
.mx_AppearanceUserSettingsTab_Layout_RadioButtons {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
gap: 24px;
|
||||||
|
|
||||||
color: $primary-fg-color;
|
color: $primary-fg-color;
|
||||||
|
|
||||||
.mx_AppearanceUserSettingsTab_spacer {
|
|
||||||
width: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .mx_AppearanceUserSettingsTab_Layout_RadioButton {
|
> .mx_AppearanceUserSettingsTab_Layout_RadioButton {
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
|
@ -215,6 +211,21 @@ limitations under the License.
|
||||||
.mx_RadioButton_checked {
|
.mx_RadioButton_checked {
|
||||||
background-color: rgba($accent-color, 0.08);
|
background-color: rgba($accent-color, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_EventTile {
|
||||||
|
margin: 0;
|
||||||
|
&[data-layout=bubble] {
|
||||||
|
margin-right: 40px;
|
||||||
|
}
|
||||||
|
&[data-layout=irc] {
|
||||||
|
> a {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.mx_EventTile_line {
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppearanceUserSettingsTab_Advanced {
|
.mx_AppearanceUserSettingsTab_Advanced {
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
<svg height="12" viewBox="0 0 12 12" width="12" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g style="stroke:#454545;stroke-width:.8;fill:none;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round" transform="translate(1 1)">
|
|
||||||
<circle cx="5" cy="5" r="5"/>
|
|
||||||
<path d="m0 5h10"/>
|
|
||||||
<path d="m5 0c1.25064019 1.36917645 1.96137638 3.14601693 2 5-.03862362 1.85398307-.74935981 3.63082355-2 5-1.25064019-1.36917645-1.96137638-3.14601693-2-5 .03862362-1.85398307.74935981-3.63082355 2-5z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 524 B |
|
@ -209,8 +209,8 @@ $user-tile-hover-bg-color: $header-panel-bg-color;
|
||||||
|
|
||||||
$message-body-panel-fg-color: $secondary-fg-color;
|
$message-body-panel-fg-color: $secondary-fg-color;
|
||||||
$message-body-panel-bg-color: #394049; // "Dark Tile"
|
$message-body-panel-bg-color: #394049; // "Dark Tile"
|
||||||
$message-body-panel-icon-fg-color: #21262C; // "Separator"
|
$message-body-panel-icon-fg-color: $secondary-fg-color;
|
||||||
$message-body-panel-icon-bg-color: $tertiary-fg-color;
|
$message-body-panel-icon-bg-color: #21262C; // "System Dark"
|
||||||
|
|
||||||
$voice-record-stop-border-color: $quaternary-fg-color;
|
$voice-record-stop-border-color: $quaternary-fg-color;
|
||||||
$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color;
|
$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color;
|
||||||
|
@ -295,3 +295,11 @@ $eventbubble-reply-color: #C1C6CD;
|
||||||
.hljs-tag {
|
.hljs-tag {
|
||||||
color: inherit; // Without this they'd be weirdly blue which doesn't match the theme
|
color: inherit; // Without this they'd be weirdly blue which doesn't match the theme
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hljs-addition {
|
||||||
|
background: #1a4b59;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-deletion {
|
||||||
|
background: #53232a;
|
||||||
|
}
|
||||||
|
|
|
@ -207,8 +207,8 @@ $user-tile-hover-bg-color: $header-panel-bg-color;
|
||||||
|
|
||||||
$message-body-panel-fg-color: $secondary-fg-color;
|
$message-body-panel-fg-color: $secondary-fg-color;
|
||||||
$message-body-panel-bg-color: #394049;
|
$message-body-panel-bg-color: #394049;
|
||||||
$message-body-panel-icon-fg-color: $primary-bg-color;
|
$message-body-panel-icon-fg-color: $secondary-fg-color;
|
||||||
$message-body-panel-icon-bg-color: $secondary-fg-color;
|
$message-body-panel-icon-bg-color: #21262C;
|
||||||
|
|
||||||
// See non-legacy dark for variable information
|
// See non-legacy dark for variable information
|
||||||
$voice-record-stop-border-color: #6F7882;
|
$voice-record-stop-border-color: #6F7882;
|
||||||
|
|
|
@ -331,7 +331,7 @@ $user-tile-hover-bg-color: $header-panel-bg-color;
|
||||||
$message-body-panel-fg-color: $secondary-fg-color;
|
$message-body-panel-fg-color: $secondary-fg-color;
|
||||||
$message-body-panel-bg-color: #E3E8F0;
|
$message-body-panel-bg-color: #E3E8F0;
|
||||||
$message-body-panel-icon-fg-color: $secondary-fg-color;
|
$message-body-panel-icon-fg-color: $secondary-fg-color;
|
||||||
$message-body-panel-icon-bg-color: $primary-bg-color;
|
$message-body-panel-icon-bg-color: #F4F6FA;
|
||||||
|
|
||||||
// See non-legacy _light for variable information
|
// See non-legacy _light for variable information
|
||||||
$voice-record-stop-symbol-color: #ff4b55;
|
$voice-record-stop-symbol-color: #ff4b55;
|
||||||
|
|
|
@ -327,7 +327,7 @@ $user-tile-hover-bg-color: $header-panel-bg-color;
|
||||||
$message-body-panel-fg-color: $secondary-fg-color;
|
$message-body-panel-fg-color: $secondary-fg-color;
|
||||||
$message-body-panel-bg-color: #E3E8F0; // "Separator"
|
$message-body-panel-bg-color: #E3E8F0; // "Separator"
|
||||||
$message-body-panel-icon-fg-color: $secondary-fg-color;
|
$message-body-panel-icon-fg-color: $secondary-fg-color;
|
||||||
$message-body-panel-icon-bg-color: $primary-bg-color;
|
$message-body-panel-icon-bg-color: #F4F6FA;
|
||||||
|
|
||||||
// These two don't change between themes. They are the $warning-color, but we don't
|
// These two don't change between themes. They are the $warning-color, but we don't
|
||||||
// want custom themes to affect them by accident.
|
// want custom themes to affect them by accident.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||||
|
|
||||||
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,6 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_MVoiceMessageBody {
|
declare module "*.svg" {
|
||||||
display: inline-block; // makes the playback controls magically line up
|
const path: string;
|
||||||
|
export default path;
|
||||||
}
|
}
|
|
@ -57,7 +57,7 @@ const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i');
|
||||||
|
|
||||||
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
|
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
|
||||||
|
|
||||||
export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet'];
|
export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet', 'matrix'];
|
||||||
|
|
||||||
const MEDIA_API_MXC_REGEX = /\/_matrix\/media\/r0\/(?:download|thumbnail)\/(.+?)\/(.+?)(?:[?/]|$)/;
|
const MEDIA_API_MXC_REGEX = /\/_matrix\/media\/r0\/(?:download|thumbnail)\/(.+?)\/(.+?)(?:[?/]|$)/;
|
||||||
|
|
||||||
|
|
|
@ -51,10 +51,15 @@ export async function startAnyRegistrationFlow(options) {
|
||||||
description: _t("Use your account or create a new one to continue."),
|
description: _t("Use your account or create a new one to continue."),
|
||||||
button: _t("Create Account"),
|
button: _t("Create Account"),
|
||||||
extraButtons: [
|
extraButtons: [
|
||||||
<button key="start_login" onClick={() => {
|
<button
|
||||||
modal.close();
|
key="start_login"
|
||||||
dis.dispatch({ action: 'start_login', screenAfterLogin: options.screen_after });
|
onClick={() => {
|
||||||
}}>{ _t('Sign In') }</button>,
|
modal.close();
|
||||||
|
dis.dispatch({ action: 'start_login', screenAfterLogin: options.screen_after });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ _t('Sign In') }
|
||||||
|
</button>,
|
||||||
],
|
],
|
||||||
onFinished: (proceed) => {
|
onFinished: (proceed) => {
|
||||||
if (proceed) {
|
if (proceed) {
|
||||||
|
|
|
@ -34,7 +34,6 @@ import { getAddressType } from './UserAddress';
|
||||||
import { abbreviateUrl } from './utils/UrlUtils';
|
import { abbreviateUrl } from './utils/UrlUtils';
|
||||||
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from './utils/IdentityServerUtils';
|
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from './utils/IdentityServerUtils';
|
||||||
import { isPermalinkHost, parsePermalink } from "./utils/permalinks/Permalinks";
|
import { isPermalinkHost, parsePermalink } from "./utils/permalinks/Permalinks";
|
||||||
import { inviteUsersToRoom } from "./RoomInvite";
|
|
||||||
import { WidgetType } from "./widgets/WidgetType";
|
import { WidgetType } from "./widgets/WidgetType";
|
||||||
import { Jitsi } from "./widgets/Jitsi";
|
import { Jitsi } from "./widgets/Jitsi";
|
||||||
import { parseFragment as parseHtml, Element as ChildElement } from "parse5";
|
import { parseFragment as parseHtml, Element as ChildElement } from "parse5";
|
||||||
|
@ -49,6 +48,7 @@ import { UIFeature } from "./settings/UIFeature";
|
||||||
import { CHAT_EFFECTS } from "./effects";
|
import { CHAT_EFFECTS } from "./effects";
|
||||||
import CallHandler from "./CallHandler";
|
import CallHandler from "./CallHandler";
|
||||||
import { guessAndSetDMRoom } from "./Rooms";
|
import { guessAndSetDMRoom } from "./Rooms";
|
||||||
|
import { upgradeRoom } from './utils/RoomUpgrade';
|
||||||
import UploadConfirmDialog from './components/views/dialogs/UploadConfirmDialog';
|
import UploadConfirmDialog from './components/views/dialogs/UploadConfirmDialog';
|
||||||
import ErrorDialog from './components/views/dialogs/ErrorDialog';
|
import ErrorDialog from './components/views/dialogs/ErrorDialog';
|
||||||
import DevtoolsDialog from './components/views/dialogs/DevtoolsDialog';
|
import DevtoolsDialog from './components/views/dialogs/DevtoolsDialog';
|
||||||
|
@ -277,50 +277,8 @@ export const Commands = [
|
||||||
/*isPriority=*/false, /*isStatic=*/true);
|
/*isPriority=*/false, /*isStatic=*/true);
|
||||||
|
|
||||||
return success(finished.then(async ([resp]) => {
|
return success(finished.then(async ([resp]) => {
|
||||||
if (!resp.continue) return;
|
if (!resp?.continue) return;
|
||||||
|
await upgradeRoom(room, args, resp.invite);
|
||||||
let checkForUpgradeFn;
|
|
||||||
try {
|
|
||||||
const upgradePromise = cli.upgradeRoom(roomId, args);
|
|
||||||
|
|
||||||
// We have to wait for the js-sdk to give us the room back so
|
|
||||||
// we can more effectively abuse the MultiInviter behaviour
|
|
||||||
// which heavily relies on the Room object being available.
|
|
||||||
if (resp.invite) {
|
|
||||||
checkForUpgradeFn = async (newRoom) => {
|
|
||||||
// The upgradePromise should be done by the time we await it here.
|
|
||||||
const { replacement_room: newRoomId } = await upgradePromise;
|
|
||||||
if (newRoom.roomId !== newRoomId) return;
|
|
||||||
|
|
||||||
const toInvite = [
|
|
||||||
...room.getMembersWithMembership("join"),
|
|
||||||
...room.getMembersWithMembership("invite"),
|
|
||||||
].map(m => m.userId).filter(m => m !== cli.getUserId());
|
|
||||||
|
|
||||||
if (toInvite.length > 0) {
|
|
||||||
// Errors are handled internally to this function
|
|
||||||
await inviteUsersToRoom(newRoomId, toInvite);
|
|
||||||
}
|
|
||||||
|
|
||||||
cli.removeListener('Room', checkForUpgradeFn);
|
|
||||||
};
|
|
||||||
cli.on('Room', checkForUpgradeFn);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We have to await after so that the checkForUpgradesFn has a proper reference
|
|
||||||
// to the new room's ID.
|
|
||||||
await upgradePromise;
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
|
|
||||||
if (checkForUpgradeFn) cli.removeListener('Room', checkForUpgradeFn);
|
|
||||||
|
|
||||||
Modal.createTrackedDialog('Slash Commands', 'room upgrade error', ErrorDialog, {
|
|
||||||
title: _t('Error upgrading room'),
|
|
||||||
description: _t(
|
|
||||||
'Double check that your server supports the room version chosen and try again.'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
return reject(this.getUsage());
|
return reject(this.getUsage());
|
||||||
|
|
|
@ -14,35 +14,33 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
|
|
||||||
const emailRegex = /^\S+@\S+\.\S+$/;
|
const emailRegex = /^\S+@\S+\.\S+$/;
|
||||||
const mxUserIdRegex = /^@\S+:\S+$/;
|
const mxUserIdRegex = /^@\S+:\S+$/;
|
||||||
const mxRoomIdRegex = /^!\S+:\S+$/;
|
const mxRoomIdRegex = /^!\S+:\S+$/;
|
||||||
|
|
||||||
export const addressTypes = ['mx-user-id', 'mx-room-id', 'email'];
|
|
||||||
|
|
||||||
export enum AddressType {
|
export enum AddressType {
|
||||||
Email = "email",
|
Email = "email",
|
||||||
MatrixUserId = "mx-user-id",
|
MatrixUserId = "mx-user-id",
|
||||||
MatrixRoomId = "mx-room-id",
|
MatrixRoomId = "mx-room-id",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const addressTypes = [AddressType.Email, AddressType.MatrixRoomId, AddressType.MatrixUserId];
|
||||||
|
|
||||||
// PropType definition for an object describing
|
// PropType definition for an object describing
|
||||||
// an address that can be invited to a room (which
|
// an address that can be invited to a room (which
|
||||||
// could be a third party identifier or a matrix ID)
|
// could be a third party identifier or a matrix ID)
|
||||||
// along with some additional information about the
|
// along with some additional information about the
|
||||||
// address / target.
|
// address / target.
|
||||||
export const UserAddressType = PropTypes.shape({
|
export interface IUserAddress {
|
||||||
addressType: PropTypes.oneOf(addressTypes).isRequired,
|
addressType: AddressType;
|
||||||
address: PropTypes.string.isRequired,
|
address: string;
|
||||||
displayName: PropTypes.string,
|
displayName?: string;
|
||||||
avatarMxc: PropTypes.string,
|
avatarMxc?: string;
|
||||||
// true if the address is known to be a valid address (eg. is a real
|
// true if the address is known to be a valid address (eg. is a real
|
||||||
// user we've seen) or false otherwise (eg. is just an address the
|
// user we've seen) or false otherwise (eg. is just an address the
|
||||||
// user has entered)
|
// user has entered)
|
||||||
isKnown: PropTypes.bool,
|
isKnown?: boolean;
|
||||||
});
|
}
|
||||||
|
|
||||||
export function getAddressType(inputText: string): AddressType | null {
|
export function getAddressType(inputText: string): AddressType | null {
|
||||||
if (emailRegex.test(inputText)) {
|
if (emailRegex.test(inputText)) {
|
||||||
|
|
|
@ -269,7 +269,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>{ _t("Advanced") }</summary>
|
<summary>{ _t("Advanced") }</summary>
|
||||||
<AccessibleButton kind='primary' onClick={this._onSkipPassPhraseClick} >
|
<AccessibleButton kind='primary' onClick={this._onSkipPassPhraseClick}>
|
||||||
{ _t("Set up with a Security Key") }
|
{ _t("Set up with a Security Key") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</details>
|
</details>
|
||||||
|
|
|
@ -474,7 +474,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
outlined
|
outlined
|
||||||
>
|
>
|
||||||
<div className="mx_CreateSecretStorageDialog_optionTitle">
|
<div className="mx_CreateSecretStorageDialog_optionTitle">
|
||||||
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_secureBackup"></span>
|
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_secureBackup" />
|
||||||
{ _t("Generate a Security Key") }
|
{ _t("Generate a Security Key") }
|
||||||
</div>
|
</div>
|
||||||
<div>{ _t("We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.") }</div>
|
<div>{ _t("We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.") }</div>
|
||||||
|
@ -493,7 +493,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
outlined
|
outlined
|
||||||
>
|
>
|
||||||
<div className="mx_CreateSecretStorageDialog_optionTitle">
|
<div className="mx_CreateSecretStorageDialog_optionTitle">
|
||||||
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_securePhrase"></span>
|
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_securePhrase" />
|
||||||
{ _t("Enter a Security Phrase") }
|
{ _t("Enter a Security Phrase") }
|
||||||
</div>
|
</div>
|
||||||
<div>{ _t("Use a secret phrase only you know, and optionally save a Security Key to use for backup.") }</div>
|
<div>{ _t("Use a secret phrase only you know, and optionally save a Security Key to use for backup.") }</div>
|
||||||
|
@ -701,7 +701,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
<code ref={this._collectRecoveryKeyNode}>{ this._recoveryKey.encodedPrivateKey }</code>
|
<code ref={this._collectRecoveryKeyNode}>{ this._recoveryKey.encodedPrivateKey }</code>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_CreateSecretStorageDialog_recoveryKeyButtons">
|
<div className="mx_CreateSecretStorageDialog_recoveryKeyButtons">
|
||||||
<AccessibleButton kind='primary' className="mx_Dialog_primary"
|
<AccessibleButton kind='primary'
|
||||||
|
className="mx_Dialog_primary"
|
||||||
onClick={this._onDownloadClick}
|
onClick={this._onDownloadClick}
|
||||||
disabled={this.state.phase === PHASE_STORING}
|
disabled={this.state.phase === PHASE_STORING}
|
||||||
>
|
>
|
||||||
|
|
|
@ -148,8 +148,12 @@ export default class ExportE2eKeysDialog extends React.Component {
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_E2eKeysDialog_inputCell'>
|
<div className='mx_E2eKeysDialog_inputCell'>
|
||||||
<input ref={this._passphrase1} id='passphrase1'
|
<input
|
||||||
autoFocus={true} size='64' type='password'
|
ref={this._passphrase1}
|
||||||
|
id='passphrase1'
|
||||||
|
autoFocus={true}
|
||||||
|
size='64'
|
||||||
|
type='password'
|
||||||
disabled={disableForm}
|
disabled={disableForm}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -161,8 +165,10 @@ export default class ExportE2eKeysDialog extends React.Component {
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_E2eKeysDialog_inputCell'>
|
<div className='mx_E2eKeysDialog_inputCell'>
|
||||||
<input ref={this._passphrase2} id='passphrase2'
|
<input ref={this._passphrase2}
|
||||||
size='64' type='password'
|
id='passphrase2'
|
||||||
|
size='64'
|
||||||
|
type='password'
|
||||||
disabled={disableForm}
|
disabled={disableForm}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -174,7 +174,10 @@ export default class ImportE2eKeysDialog extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_Dialog_buttons'>
|
<div className='mx_Dialog_buttons'>
|
||||||
<input className='mx_Dialog_primary' type='submit' value={_t('Import')}
|
<input
|
||||||
|
className='mx_Dialog_primary'
|
||||||
|
type='submit'
|
||||||
|
value={_t('Import')}
|
||||||
disabled={!this.state.enableSubmit || disableForm}
|
disabled={!this.state.enableSubmit || disableForm}
|
||||||
/>
|
/>
|
||||||
<button onClick={this._onCancelClick} disabled={disableForm}>
|
<button onClick={this._onCancelClick} disabled={disableForm}>
|
||||||
|
|
37
src/audio/ManagedPlayback.ts
Normal file
37
src/audio/ManagedPlayback.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
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 { DEFAULT_WAVEFORM, Playback } from "./Playback";
|
||||||
|
import { PlaybackManager } from "./PlaybackManager";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A managed playback is a Playback instance that is guided by a PlaybackManager.
|
||||||
|
*/
|
||||||
|
export class ManagedPlayback extends Playback {
|
||||||
|
public constructor(private manager: PlaybackManager, buf: ArrayBuffer, seedWaveform = DEFAULT_WAVEFORM) {
|
||||||
|
super(buf, seedWaveform);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async play(): Promise<void> {
|
||||||
|
this.manager.playOnly(this);
|
||||||
|
return super.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroy() {
|
||||||
|
this.manager.destroyPlaybackInstance(this);
|
||||||
|
super.destroy();
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,7 +32,7 @@ export enum PlaybackState {
|
||||||
|
|
||||||
export const PLAYBACK_WAVEFORM_SAMPLES = 39;
|
export const PLAYBACK_WAVEFORM_SAMPLES = 39;
|
||||||
const THUMBNAIL_WAVEFORM_SAMPLES = 100; // arbitrary: [30,120]
|
const THUMBNAIL_WAVEFORM_SAMPLES = 100; // arbitrary: [30,120]
|
||||||
const DEFAULT_WAVEFORM = arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES);
|
export const DEFAULT_WAVEFORM = arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES);
|
||||||
|
|
||||||
function makePlaybackWaveform(input: number[]): number[] {
|
function makePlaybackWaveform(input: number[]): number[] {
|
||||||
// First, convert negative amplitudes to positive so we don't detect zero as "noisy".
|
// First, convert negative amplitudes to positive so we don't detect zero as "noisy".
|
||||||
|
@ -59,9 +59,10 @@ export class Playback extends EventEmitter implements IDestroyable {
|
||||||
public readonly thumbnailWaveform: number[];
|
public readonly thumbnailWaveform: number[];
|
||||||
|
|
||||||
private readonly context: AudioContext;
|
private readonly context: AudioContext;
|
||||||
private source: AudioBufferSourceNode;
|
private source: AudioBufferSourceNode | MediaElementAudioSourceNode;
|
||||||
private state = PlaybackState.Decoding;
|
private state = PlaybackState.Decoding;
|
||||||
private audioBuf: AudioBuffer;
|
private audioBuf: AudioBuffer;
|
||||||
|
private element: HTMLAudioElement;
|
||||||
private resampledWaveform: number[];
|
private resampledWaveform: number[];
|
||||||
private waveformObservable = new SimpleObservable<number[]>();
|
private waveformObservable = new SimpleObservable<number[]>();
|
||||||
private readonly clock: PlaybackClock;
|
private readonly clock: PlaybackClock;
|
||||||
|
@ -129,36 +130,64 @@ export class Playback extends EventEmitter implements IDestroyable {
|
||||||
this.removeAllListeners();
|
this.removeAllListeners();
|
||||||
this.clock.destroy();
|
this.clock.destroy();
|
||||||
this.waveformObservable.close();
|
this.waveformObservable.close();
|
||||||
|
if (this.element) {
|
||||||
|
URL.revokeObjectURL(this.element.src);
|
||||||
|
this.element.remove();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async prepare() {
|
public async prepare() {
|
||||||
// Safari compat: promise API not supported on this function
|
// The point where we use an audio element is fairly arbitrary, though we don't want
|
||||||
this.audioBuf = await new Promise((resolve, reject) => {
|
// it to be too low. As of writing, voice messages want to show a waveform but audio
|
||||||
this.context.decodeAudioData(this.buf, b => resolve(b), async e => {
|
// messages do not. Using an audio element means we can't show a waveform preview, so
|
||||||
// This error handler is largely for Safari as well, which doesn't support Opus/Ogg
|
// we try to target the difference between a voice message file and large audio file.
|
||||||
// very well.
|
// Overall, the point of this is to avoid memory-related issues due to storing a massive
|
||||||
console.error("Error decoding recording: ", e);
|
// audio buffer in memory, as that can balloon to far greater than the input buffer's
|
||||||
console.warn("Trying to re-encode to WAV instead...");
|
// byte length.
|
||||||
|
if (this.buf.byteLength > 5 * 1024 * 1024) { // 5mb
|
||||||
|
console.log("Audio file too large: processing through <audio /> element");
|
||||||
|
this.element = document.createElement("AUDIO") as HTMLAudioElement;
|
||||||
|
const prom = new Promise((resolve, reject) => {
|
||||||
|
this.element.onloadeddata = () => resolve(null);
|
||||||
|
this.element.onerror = (e) => reject(e);
|
||||||
|
});
|
||||||
|
this.element.src = URL.createObjectURL(new Blob([this.buf]));
|
||||||
|
await prom; // make sure the audio element is ready for us
|
||||||
|
} else {
|
||||||
|
// Safari compat: promise API not supported on this function
|
||||||
|
this.audioBuf = await new Promise((resolve, reject) => {
|
||||||
|
this.context.decodeAudioData(this.buf, b => resolve(b), async e => {
|
||||||
|
try {
|
||||||
|
// This error handler is largely for Safari as well, which doesn't support Opus/Ogg
|
||||||
|
// very well.
|
||||||
|
console.error("Error decoding recording: ", e);
|
||||||
|
console.warn("Trying to re-encode to WAV instead...");
|
||||||
|
|
||||||
const wav = await decodeOgg(this.buf);
|
const wav = await decodeOgg(this.buf);
|
||||||
|
|
||||||
// noinspection ES6MissingAwait - not needed when using callbacks
|
// noinspection ES6MissingAwait - not needed when using callbacks
|
||||||
this.context.decodeAudioData(wav, b => resolve(b), e => {
|
this.context.decodeAudioData(wav, b => resolve(b), e => {
|
||||||
console.error("Still failed to decode recording: ", e);
|
console.error("Still failed to decode recording: ", e);
|
||||||
reject(e);
|
reject(e);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Caught decoding error:", e);
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// Update the waveform to the real waveform once we have channel data to use. We don't
|
// Update the waveform to the real waveform once we have channel data to use. We don't
|
||||||
// exactly trust the user-provided waveform to be accurate...
|
// exactly trust the user-provided waveform to be accurate...
|
||||||
const waveform = Array.from(this.audioBuf.getChannelData(0));
|
const waveform = Array.from(this.audioBuf.getChannelData(0));
|
||||||
this.resampledWaveform = makePlaybackWaveform(waveform);
|
this.resampledWaveform = makePlaybackWaveform(waveform);
|
||||||
|
}
|
||||||
|
|
||||||
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.flagLoadTime(); // must happen first because setting the duration fires a clock update
|
||||||
this.clock.durationSeconds = this.audioBuf.duration;
|
this.clock.durationSeconds = this.element ? this.element.duration : this.audioBuf.duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
private onPlaybackEnd = async () => {
|
private onPlaybackEnd = async () => {
|
||||||
|
@ -171,7 +200,11 @@ export class Playback extends EventEmitter implements IDestroyable {
|
||||||
if (this.state === PlaybackState.Stopped) {
|
if (this.state === PlaybackState.Stopped) {
|
||||||
this.disconnectSource();
|
this.disconnectSource();
|
||||||
this.makeNewSourceBuffer();
|
this.makeNewSourceBuffer();
|
||||||
this.source.start();
|
if (this.element) {
|
||||||
|
await this.element.play();
|
||||||
|
} else {
|
||||||
|
(this.source as AudioBufferSourceNode).start();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We use the context suspend/resume functions because it allows us to pause a source
|
// We use the context suspend/resume functions because it allows us to pause a source
|
||||||
|
@ -182,13 +215,21 @@ export class Playback extends EventEmitter implements IDestroyable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private disconnectSource() {
|
private disconnectSource() {
|
||||||
|
if (this.element) return; // leave connected, we can (and must) re-use it
|
||||||
this.source?.disconnect();
|
this.source?.disconnect();
|
||||||
this.source?.removeEventListener("ended", this.onPlaybackEnd);
|
this.source?.removeEventListener("ended", this.onPlaybackEnd);
|
||||||
}
|
}
|
||||||
|
|
||||||
private makeNewSourceBuffer() {
|
private makeNewSourceBuffer() {
|
||||||
this.source = this.context.createBufferSource();
|
if (this.element && this.source) return; // leave connected, we can (and must) re-use it
|
||||||
this.source.buffer = this.audioBuf;
|
|
||||||
|
if (this.element) {
|
||||||
|
this.source = this.context.createMediaElementSource(this.element);
|
||||||
|
} else {
|
||||||
|
this.source = this.context.createBufferSource();
|
||||||
|
this.source.buffer = this.audioBuf;
|
||||||
|
}
|
||||||
|
|
||||||
this.source.addEventListener("ended", this.onPlaybackEnd);
|
this.source.addEventListener("ended", this.onPlaybackEnd);
|
||||||
this.source.connect(this.context.destination);
|
this.source.connect(this.context.destination);
|
||||||
}
|
}
|
||||||
|
@ -241,7 +282,11 @@ export class Playback extends EventEmitter implements IDestroyable {
|
||||||
// when it comes time to the user hitting play. After a couple jumps, the user
|
// when it comes time to the user hitting play. After a couple jumps, the user
|
||||||
// will have desynced the clock enough to be about 10-15 seconds off, while this
|
// will have desynced the clock enough to be about 10-15 seconds off, while this
|
||||||
// keeps it as close to perfect as humans can perceive.
|
// keeps it as close to perfect as humans can perceive.
|
||||||
this.source.start(now, timeSeconds);
|
if (this.element) {
|
||||||
|
this.element.currentTime = timeSeconds;
|
||||||
|
} else {
|
||||||
|
(this.source as AudioBufferSourceNode).start(now, timeSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
// Dev note: it's critical that the code gap between `this.source.start()` and
|
// Dev note: it's critical that the code gap between `this.source.start()` and
|
||||||
// `this.pause()` is as small as possible: we do not want to delay *anything*
|
// `this.pause()` is as small as possible: we do not want to delay *anything*
|
|
@ -103,8 +103,8 @@ export class PlaybackClock implements IDestroyable {
|
||||||
* @param {MatrixEvent} event The event to use for placeholders.
|
* @param {MatrixEvent} event The event to use for placeholders.
|
||||||
*/
|
*/
|
||||||
public populatePlaceholdersFrom(event: MatrixEvent) {
|
public populatePlaceholdersFrom(event: MatrixEvent) {
|
||||||
const durationSeconds = Number(event.getContent()['info']?.['duration']);
|
const durationMs = Number(event.getContent()['info']?.['duration']);
|
||||||
if (Number.isFinite(durationSeconds)) this.placeholderDuration = durationSeconds;
|
if (Number.isFinite(durationMs)) this.placeholderDuration = durationMs / 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -132,6 +132,10 @@ export class PlaybackClock implements IDestroyable {
|
||||||
|
|
||||||
public flagStop() {
|
public flagStop() {
|
||||||
this.stopped = true;
|
this.stopped = true;
|
||||||
|
|
||||||
|
// Reset the clock time now so that the update going out will trigger components
|
||||||
|
// to check their seek/position information (alongside the clock).
|
||||||
|
this.clipStart = this.context.currentTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
public syncTo(contextTime: number, clipTime: number) {
|
public syncTo(contextTime: number, clipTime: number) {
|
54
src/audio/PlaybackManager.ts
Normal file
54
src/audio/PlaybackManager.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
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 { DEFAULT_WAVEFORM, Playback } from "./Playback";
|
||||||
|
import { ManagedPlayback } from "./ManagedPlayback";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles management of playback instances to ensure certain functionality, like
|
||||||
|
* one playback operating at any one time.
|
||||||
|
*/
|
||||||
|
export class PlaybackManager {
|
||||||
|
private static internalInstance: PlaybackManager;
|
||||||
|
|
||||||
|
private instances: ManagedPlayback[] = [];
|
||||||
|
|
||||||
|
public static get instance(): PlaybackManager {
|
||||||
|
if (!PlaybackManager.internalInstance) {
|
||||||
|
PlaybackManager.internalInstance = new PlaybackManager();
|
||||||
|
}
|
||||||
|
return PlaybackManager.internalInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops all other playback instances. If no playback is provided, all instances
|
||||||
|
* are stopped.
|
||||||
|
* @param playback Optional. The playback to leave untouched.
|
||||||
|
*/
|
||||||
|
public playOnly(playback?: Playback) {
|
||||||
|
this.instances.filter(p => p !== playback).forEach(p => p.stop());
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroyPlaybackInstance(playback: ManagedPlayback) {
|
||||||
|
this.instances = this.instances.filter(p => p !== playback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public createPlaybackInstance(buf: ArrayBuffer, waveform = DEFAULT_WAVEFORM): Playback {
|
||||||
|
const instance = new ManagedPlayback(this, buf, waveform);
|
||||||
|
this.instances.push(instance);
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
}
|
|
@ -333,12 +333,17 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
|
||||||
|
|
||||||
if (this.lastUpload) return this.lastUpload;
|
if (this.lastUpload) return this.lastUpload;
|
||||||
|
|
||||||
this.emit(RecordingState.Uploading);
|
try {
|
||||||
const { url: mxc, file: encrypted } = await uploadFile(this.client, inRoomId, new Blob([this.audioBuffer], {
|
this.emit(RecordingState.Uploading);
|
||||||
type: this.contentType,
|
const { url: mxc, file: encrypted } = await uploadFile(this.client, inRoomId, new Blob([this.audioBuffer], {
|
||||||
}));
|
type: this.contentType,
|
||||||
this.lastUpload = { mxc, encrypted };
|
}));
|
||||||
this.emit(RecordingState.Uploaded);
|
this.lastUpload = { mxc, encrypted };
|
||||||
|
this.emit(RecordingState.Uploaded);
|
||||||
|
} catch (e) {
|
||||||
|
this.emit(RecordingState.Ended);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
return this.lastUpload;
|
return this.lastUpload;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -120,8 +120,7 @@ export default class EmbeddedPage extends React.PureComponent {
|
||||||
|
|
||||||
const content = <div className={`${className}_body`}
|
const content = <div className={`${className}_body`}
|
||||||
dangerouslySetInnerHTML={{ __html: this.state.page }}
|
dangerouslySetInnerHTML={{ __html: this.state.page }}
|
||||||
>
|
/>;
|
||||||
</div>;
|
|
||||||
|
|
||||||
if (this.props.scrollbar) {
|
if (this.props.scrollbar) {
|
||||||
return <AutoHideScrollbar className={classes}>
|
return <AutoHideScrollbar className={classes}>
|
||||||
|
|
|
@ -36,6 +36,7 @@ import ResizeNotifier from '../../utils/ResizeNotifier';
|
||||||
import TimelinePanel from "./TimelinePanel";
|
import TimelinePanel from "./TimelinePanel";
|
||||||
import Spinner from "../views/elements/Spinner";
|
import Spinner from "../views/elements/Spinner";
|
||||||
import { TileShape } from '../views/rooms/EventTile';
|
import { TileShape } from '../views/rooms/EventTile';
|
||||||
|
import { Layout } from "../../settings/Layout";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
|
@ -267,6 +268,7 @@ class FilePanel extends React.Component<IProps, IState> {
|
||||||
tileShape={TileShape.FileGrid}
|
tileShape={TileShape.FileGrid}
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
empty={emptyState}
|
empty={emptyState}
|
||||||
|
layout={Layout.Group}
|
||||||
/>
|
/>
|
||||||
</BaseCard>
|
</BaseCard>
|
||||||
);
|
);
|
||||||
|
|
|
@ -222,7 +222,7 @@ class FeaturedRoom extends React.Component {
|
||||||
|
|
||||||
let roomNameNode = null;
|
let roomNameNode = null;
|
||||||
if (permalink) {
|
if (permalink) {
|
||||||
roomNameNode = <a href={permalink} onClick={this.onClick} >{ roomName }</a>;
|
roomNameNode = <a href={permalink} onClick={this.onClick}>{ roomName }</a>;
|
||||||
} else {
|
} else {
|
||||||
roomNameNode = <span>{ roomName }</span>;
|
roomNameNode = <span>{ roomName }</span>;
|
||||||
}
|
}
|
||||||
|
@ -1185,10 +1185,13 @@ export default class GroupView extends React.Component {
|
||||||
avatarImage = <Spinner />;
|
avatarImage = <Spinner />;
|
||||||
} else {
|
} else {
|
||||||
const GroupAvatar = sdk.getComponent('avatars.GroupAvatar');
|
const GroupAvatar = sdk.getComponent('avatars.GroupAvatar');
|
||||||
avatarImage = <GroupAvatar groupId={this.props.groupId}
|
avatarImage = <GroupAvatar
|
||||||
|
groupId={this.props.groupId}
|
||||||
groupName={this.state.profileForm.name}
|
groupName={this.state.profileForm.name}
|
||||||
groupAvatarUrl={this.state.profileForm.avatar_url}
|
groupAvatarUrl={this.state.profileForm.avatar_url}
|
||||||
width={28} height={28} resizeMethod='crop'
|
width={28}
|
||||||
|
height={28}
|
||||||
|
resizeMethod='crop'
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1199,9 +1202,12 @@ export default class GroupView extends React.Component {
|
||||||
</label>
|
</label>
|
||||||
<div className="mx_GroupView_avatarPicker_edit">
|
<div className="mx_GroupView_avatarPicker_edit">
|
||||||
<label htmlFor="avatarInput" className="mx_GroupView_avatarPicker_label">
|
<label htmlFor="avatarInput" className="mx_GroupView_avatarPicker_label">
|
||||||
<img src={require("../../../res/img/camera.svg")}
|
<img
|
||||||
alt={_t("Upload avatar")} title={_t("Upload avatar")}
|
src={require("../../../res/img/camera.svg")}
|
||||||
width="17" height="15" />
|
alt={_t("Upload avatar")}
|
||||||
|
title={_t("Upload avatar")}
|
||||||
|
width="17"
|
||||||
|
height="15" />
|
||||||
</label>
|
</label>
|
||||||
<input id="avatarInput" className="mx_GroupView_uploadInput" type="file" onChange={this._onAvatarSelected} />
|
<input id="avatarInput" className="mx_GroupView_uploadInput" type="file" onChange={this._onAvatarSelected} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1238,7 +1244,8 @@ export default class GroupView extends React.Component {
|
||||||
groupAvatarUrl={groupAvatarUrl}
|
groupAvatarUrl={groupAvatarUrl}
|
||||||
groupName={groupName}
|
groupName={groupName}
|
||||||
onClick={onGroupHeaderItemClick}
|
onClick={onGroupHeaderItemClick}
|
||||||
width={28} height={28}
|
width={28}
|
||||||
|
height={28}
|
||||||
/>;
|
/>;
|
||||||
if (summary.profile && summary.profile.name) {
|
if (summary.profile && summary.profile.name) {
|
||||||
nameNode = <div onClick={onGroupHeaderItemClick}>
|
nameNode = <div onClick={onGroupHeaderItemClick}>
|
||||||
|
@ -1269,28 +1276,32 @@ export default class GroupView extends React.Component {
|
||||||
key="_cancelButton"
|
key="_cancelButton"
|
||||||
onClick={this._onCancelClick}
|
onClick={this._onCancelClick}
|
||||||
>
|
>
|
||||||
<img src={require("../../../res/img/cancel.svg")} className="mx_filterFlipColor"
|
<img
|
||||||
width="18" height="18" alt={_t("Cancel")} />
|
src={require("../../../res/img/cancel.svg")}
|
||||||
|
className="mx_filterFlipColor"
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
alt={_t("Cancel")} />
|
||||||
</AccessibleButton>,
|
</AccessibleButton>,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
if (summary.user && summary.user.membership === 'join') {
|
if (summary.user && summary.user.membership === 'join') {
|
||||||
rightButtons.push(
|
rightButtons.push(
|
||||||
<AccessibleButton className="mx_GroupHeader_button mx_GroupHeader_editButton"
|
<AccessibleButton
|
||||||
|
className="mx_GroupHeader_button mx_GroupHeader_editButton"
|
||||||
key="_editButton"
|
key="_editButton"
|
||||||
onClick={this._onEditClick}
|
onClick={this._onEditClick}
|
||||||
title={_t("Community Settings")}
|
title={_t("Community Settings")}
|
||||||
>
|
/>,
|
||||||
</AccessibleButton>,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
rightButtons.push(
|
rightButtons.push(
|
||||||
<AccessibleButton className="mx_GroupHeader_button mx_GroupHeader_shareButton"
|
<AccessibleButton
|
||||||
|
className="mx_GroupHeader_button mx_GroupHeader_shareButton"
|
||||||
key="_shareButton"
|
key="_shareButton"
|
||||||
onClick={this._onShareClick}
|
onClick={this._onShareClick}
|
||||||
title={_t('Share Community')}
|
title={_t('Share Community')}
|
||||||
>
|
/>,
|
||||||
</AccessibleButton>,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -236,6 +236,8 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
// A map of <callId, CallEventGrouper>
|
// A map of <callId, CallEventGrouper>
|
||||||
private callEventGroupers = new Map<string, CallEventGrouper>();
|
private callEventGroupers = new Map<string, CallEventGrouper>();
|
||||||
|
|
||||||
|
private membersCount = 0;
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
|
@ -256,11 +258,14 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
this.calculateRoomMembersCount();
|
||||||
|
this.props.room?.on("RoomState.members", this.calculateRoomMembersCount);
|
||||||
this.isMounted = true;
|
this.isMounted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.isMounted = false;
|
this.isMounted = false;
|
||||||
|
this.props.room?.off("RoomState.members", this.calculateRoomMembersCount);
|
||||||
SettingsStore.unwatchSetting(this.showTypingNotificationsWatcherRef);
|
SettingsStore.unwatchSetting(this.showTypingNotificationsWatcherRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -274,6 +279,10 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private calculateRoomMembersCount = (): void => {
|
||||||
|
this.membersCount = this.props.room?.getMembers().length || 0;
|
||||||
|
};
|
||||||
|
|
||||||
private onShowTypingNotificationsChange = (): void => {
|
private onShowTypingNotificationsChange = (): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
showTypingNotifications: SettingsStore.getValue("showTypingNotifications"),
|
showTypingNotifications: SettingsStore.getValue("showTypingNotifications"),
|
||||||
|
@ -711,7 +720,6 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
isLastSuccessful = isLastSuccessful && mxEv.getSender() === MatrixClientPeg.get().getUserId();
|
isLastSuccessful = isLastSuccessful && mxEv.getSender() === MatrixClientPeg.get().getUserId();
|
||||||
|
|
||||||
const callEventGrouper = this.callEventGroupers.get(mxEv.getContent().call_id);
|
const callEventGrouper = this.callEventGroupers.get(mxEv.getContent().call_id);
|
||||||
|
|
||||||
// use txnId as key if available so that we don't remount during sending
|
// use txnId as key if available so that we don't remount during sending
|
||||||
ret.push(
|
ret.push(
|
||||||
<TileErrorBoundary key={mxEv.getTxnId() || eventId} mxEvent={mxEv}>
|
<TileErrorBoundary key={mxEv.getTxnId() || eventId} mxEvent={mxEv}>
|
||||||
|
@ -743,7 +751,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
enableFlair={this.props.enableFlair}
|
enableFlair={this.props.enableFlair}
|
||||||
showReadReceipts={this.props.showReadReceipts}
|
showReadReceipts={this.props.showReadReceipts}
|
||||||
callEventGrouper={callEventGrouper}
|
callEventGrouper={callEventGrouper}
|
||||||
hideSender={this.props.room?.getMembers().length <= 2 && this.props.layout === Layout.Bubble}
|
hideSender={this.membersCount <= 2 && this.props.layout === Layout.Bubble}
|
||||||
/>
|
/>
|
||||||
</TileErrorBoundary>,
|
</TileErrorBoundary>,
|
||||||
);
|
);
|
||||||
|
|
|
@ -109,8 +109,7 @@ export default class MyGroups extends React.Component {
|
||||||
<SimpleRoomHeader title={_t("Communities")} icon={require("../../../res/img/icons-groups.svg")} />
|
<SimpleRoomHeader title={_t("Communities")} icon={require("../../../res/img/icons-groups.svg")} />
|
||||||
<div className='mx_MyGroups_header'>
|
<div className='mx_MyGroups_header'>
|
||||||
<div className="mx_MyGroups_headerCard">
|
<div className="mx_MyGroups_headerCard">
|
||||||
<AccessibleButton className='mx_MyGroups_headerCard_button' onClick={this._onCreateGroupClick}>
|
<AccessibleButton className='mx_MyGroups_headerCard_button' onClick={this._onCreateGroupClick} />
|
||||||
</AccessibleButton>
|
|
||||||
<div className="mx_MyGroups_headerCard_content">
|
<div className="mx_MyGroups_headerCard_content">
|
||||||
<div className="mx_MyGroups_headerCard_header">
|
<div className="mx_MyGroups_headerCard_header">
|
||||||
{ _t('Create a new community') }
|
{ _t('Create a new community') }
|
||||||
|
|
|
@ -23,6 +23,7 @@ import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
import TimelinePanel from "./TimelinePanel";
|
import TimelinePanel from "./TimelinePanel";
|
||||||
import Spinner from "../views/elements/Spinner";
|
import Spinner from "../views/elements/Spinner";
|
||||||
import { TileShape } from "../views/rooms/EventTile";
|
import { TileShape } from "../views/rooms/EventTile";
|
||||||
|
import { Layout } from "../../settings/Layout";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
onClose(): void;
|
onClose(): void;
|
||||||
|
@ -52,6 +53,7 @@ export default class NotificationPanel extends React.PureComponent<IProps> {
|
||||||
tileShape={TileShape.Notif}
|
tileShape={TileShape.Notif}
|
||||||
empty={emptyState}
|
empty={emptyState}
|
||||||
alwaysShowTimestamps={true}
|
alwaysShowTimestamps={true}
|
||||||
|
layout={Layout.Group}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -266,8 +266,12 @@ export default class RoomStatusBar extends React.PureComponent {
|
||||||
<div className="mx_RoomStatusBar">
|
<div className="mx_RoomStatusBar">
|
||||||
<div role="alert">
|
<div role="alert">
|
||||||
<div className="mx_RoomStatusBar_connectionLostBar">
|
<div className="mx_RoomStatusBar_connectionLostBar">
|
||||||
<img src={require("../../../res/img/feather-customised/warning-triangle.svg")} width="24"
|
<img
|
||||||
height="24" title="/!\ " alt="/!\ " />
|
src={require("../../../res/img/feather-customised/warning-triangle.svg")}
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
title="/!\ "
|
||||||
|
alt="/!\ " />
|
||||||
<div>
|
<div>
|
||||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||||
{ _t('Connectivity to the server has been lost.') }
|
{ _t('Connectivity to the server has been lost.') }
|
||||||
|
|
|
@ -166,6 +166,10 @@ export interface IState {
|
||||||
canReply: boolean;
|
canReply: boolean;
|
||||||
layout: Layout;
|
layout: Layout;
|
||||||
lowBandwidth: boolean;
|
lowBandwidth: boolean;
|
||||||
|
alwaysShowTimestamps: boolean;
|
||||||
|
showTwelveHourTimestamps: boolean;
|
||||||
|
readMarkerInViewThresholdMs: number;
|
||||||
|
readMarkerOutOfViewThresholdMs: number;
|
||||||
showHiddenEventsInTimeline: boolean;
|
showHiddenEventsInTimeline: boolean;
|
||||||
showReadReceipts: boolean;
|
showReadReceipts: boolean;
|
||||||
showRedactions: boolean;
|
showRedactions: boolean;
|
||||||
|
@ -231,6 +235,10 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
canReply: false,
|
canReply: false,
|
||||||
layout: SettingsStore.getValue("layout"),
|
layout: SettingsStore.getValue("layout"),
|
||||||
lowBandwidth: SettingsStore.getValue("lowBandwidth"),
|
lowBandwidth: SettingsStore.getValue("lowBandwidth"),
|
||||||
|
alwaysShowTimestamps: SettingsStore.getValue("alwaysShowTimestamps"),
|
||||||
|
showTwelveHourTimestamps: SettingsStore.getValue("showTwelveHourTimestamps"),
|
||||||
|
readMarkerInViewThresholdMs: SettingsStore.getValue("readMarkerInViewThresholdMs"),
|
||||||
|
readMarkerOutOfViewThresholdMs: SettingsStore.getValue("readMarkerOutOfViewThresholdMs"),
|
||||||
showHiddenEventsInTimeline: SettingsStore.getValue("showHiddenEventsInTimeline"),
|
showHiddenEventsInTimeline: SettingsStore.getValue("showHiddenEventsInTimeline"),
|
||||||
showReadReceipts: true,
|
showReadReceipts: true,
|
||||||
showRedactions: true,
|
showRedactions: true,
|
||||||
|
@ -263,14 +271,26 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
WidgetStore.instance.on(UPDATE_EVENT, this.onWidgetStoreUpdate);
|
WidgetStore.instance.on(UPDATE_EVENT, this.onWidgetStoreUpdate);
|
||||||
|
|
||||||
this.settingWatchers = [
|
this.settingWatchers = [
|
||||||
SettingsStore.watchSetting("layout", null, () =>
|
SettingsStore.watchSetting("layout", null, (...[,,, value]) =>
|
||||||
this.setState({ layout: SettingsStore.getValue("layout") }),
|
this.setState({ layout: value as Layout }),
|
||||||
),
|
),
|
||||||
SettingsStore.watchSetting("lowBandwidth", null, () =>
|
SettingsStore.watchSetting("lowBandwidth", null, (...[,,, value]) =>
|
||||||
this.setState({ lowBandwidth: SettingsStore.getValue("lowBandwidth") }),
|
this.setState({ lowBandwidth: value as boolean }),
|
||||||
),
|
),
|
||||||
SettingsStore.watchSetting("showHiddenEventsInTimeline", null, () =>
|
SettingsStore.watchSetting("alwaysShowTimestamps", null, (...[,,, value]) =>
|
||||||
this.setState({ showHiddenEventsInTimeline: SettingsStore.getValue("showHiddenEventsInTimeline") }),
|
this.setState({ alwaysShowTimestamps: value as boolean }),
|
||||||
|
),
|
||||||
|
SettingsStore.watchSetting("showTwelveHourTimestamps", null, (...[,,, value]) =>
|
||||||
|
this.setState({ showTwelveHourTimestamps: value as boolean }),
|
||||||
|
),
|
||||||
|
SettingsStore.watchSetting("readMarkerInViewThresholdMs", null, (...[,,, value]) =>
|
||||||
|
this.setState({ readMarkerInViewThresholdMs: value as number }),
|
||||||
|
),
|
||||||
|
SettingsStore.watchSetting("readMarkerOutOfViewThresholdMs", null, (...[,,, value]) =>
|
||||||
|
this.setState({ readMarkerOutOfViewThresholdMs: value as number }),
|
||||||
|
),
|
||||||
|
SettingsStore.watchSetting("showHiddenEventsInTimeline", null, (...[,,, value]) =>
|
||||||
|
this.setState({ showHiddenEventsInTimeline: value as boolean }),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -337,30 +357,20 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
// Add watchers for each of the settings we just looked up
|
// Add watchers for each of the settings we just looked up
|
||||||
this.settingWatchers = this.settingWatchers.concat([
|
this.settingWatchers = this.settingWatchers.concat([
|
||||||
SettingsStore.watchSetting("showReadReceipts", null, () =>
|
SettingsStore.watchSetting("showReadReceipts", roomId, (...[,,, value]) =>
|
||||||
this.setState({
|
this.setState({ showReadReceipts: value as boolean }),
|
||||||
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
SettingsStore.watchSetting("showRedactions", null, () =>
|
SettingsStore.watchSetting("showRedactions", roomId, (...[,,, value]) =>
|
||||||
this.setState({
|
this.setState({ showRedactions: value as boolean }),
|
||||||
showRedactions: SettingsStore.getValue("showRedactions", roomId),
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
SettingsStore.watchSetting("showJoinLeaves", null, () =>
|
SettingsStore.watchSetting("showJoinLeaves", roomId, (...[,,, value]) =>
|
||||||
this.setState({
|
this.setState({ showJoinLeaves: value as boolean }),
|
||||||
showJoinLeaves: SettingsStore.getValue("showJoinLeaves", roomId),
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
SettingsStore.watchSetting("showAvatarChanges", null, () =>
|
SettingsStore.watchSetting("showAvatarChanges", roomId, (...[,,, value]) =>
|
||||||
this.setState({
|
this.setState({ showAvatarChanges: value as boolean }),
|
||||||
showAvatarChanges: SettingsStore.getValue("showAvatarChanges", roomId),
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
SettingsStore.watchSetting("showDisplaynameChanges", null, () =>
|
SettingsStore.watchSetting("showDisplaynameChanges", roomId, (...[,,, value]) =>
|
||||||
this.setState({
|
this.setState({ showDisplaynameChanges: value as boolean }),
|
||||||
showDisplaynameChanges: SettingsStore.getValue("showDisplaynameChanges", roomId),
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -1730,7 +1740,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
onJoinClick={this.onJoinButtonClicked}
|
onJoinClick={this.onJoinButtonClicked}
|
||||||
onForgetClick={this.onForgetClick}
|
onForgetClick={this.onForgetClick}
|
||||||
onRejectClick={this.onRejectThreepidInviteButtonClicked}
|
onRejectClick={this.onRejectThreepidInviteButtonClicked}
|
||||||
canPreview={false} error={this.state.roomLoadError}
|
canPreview={false}
|
||||||
|
error={this.state.roomLoadError}
|
||||||
roomAlias={roomAlias}
|
roomAlias={roomAlias}
|
||||||
joining={this.state.joining}
|
joining={this.state.joining}
|
||||||
inviterName={inviterName}
|
inviterName={inviterName}
|
||||||
|
|
|
@ -136,8 +136,8 @@ export default class SearchBox extends React.Component {
|
||||||
key="button"
|
key="button"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className="mx_SearchBox_closeButton"
|
className="mx_SearchBox_closeButton"
|
||||||
onClick={() => {this._clearSearch("button"); }}>
|
onClick={() => {this._clearSearch("button"); }}
|
||||||
</AccessibleButton>) : undefined;
|
/>) : undefined;
|
||||||
|
|
||||||
// show a shorter placeholder when blurred, if requested
|
// show a shorter placeholder when blurred, if requested
|
||||||
// this is used for the room filter field that has
|
// this is used for the room filter field that has
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import React, { RefObject, useContext, useRef, useState } from "react";
|
import React, { RefObject, useContext, useRef, useState } from "react";
|
||||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
import { Preset } from "matrix-js-sdk/src/@types/partials";
|
import { Preset, JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { EventSubscription } from "fbemitter";
|
import { EventSubscription } from "fbemitter";
|
||||||
|
|
||||||
|
@ -66,7 +66,6 @@ import Modal from "../../Modal";
|
||||||
import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog";
|
import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog";
|
||||||
import SdkConfig from "../../SdkConfig";
|
import SdkConfig from "../../SdkConfig";
|
||||||
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
|
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
|
||||||
import { JoinRule } from "../views/settings/tabs/room/SecurityRoomSettingsTab";
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
space: Room;
|
space: Room;
|
||||||
|
@ -101,12 +100,14 @@ export const SpaceFeedbackPrompt = ({ onClick }: { onClick?: () => void }) => {
|
||||||
<hr />
|
<hr />
|
||||||
<div>
|
<div>
|
||||||
<span className="mx_SpaceFeedbackPrompt_text">{ _t("Spaces are a beta feature.") }</span>
|
<span className="mx_SpaceFeedbackPrompt_text">{ _t("Spaces are a beta feature.") }</span>
|
||||||
<AccessibleButton kind="link" onClick={() => {
|
<AccessibleButton
|
||||||
if (onClick) onClick();
|
kind="link"
|
||||||
Modal.createTrackedDialog("Beta Feedback", "feature_spaces", BetaFeedbackDialog, {
|
onClick={() => {
|
||||||
|
if (onClick) onClick();
|
||||||
|
Modal.createTrackedDialog("Beta Feedback", "feature_spaces", BetaFeedbackDialog, {
|
||||||
featureId: "feature_spaces",
|
featureId: "feature_spaces",
|
||||||
});
|
});
|
||||||
}}>
|
}}>
|
||||||
{ _t("Feedback") }
|
{ _t("Feedback") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
|
@ -307,7 +308,6 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
|
||||||
};
|
};
|
||||||
|
|
||||||
const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => {
|
const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => {
|
||||||
const cli = useContext(MatrixClientContext);
|
|
||||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
|
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
|
||||||
|
|
||||||
let contextMenu;
|
let contextMenu;
|
||||||
|
@ -330,7 +330,7 @@ const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
closeMenu();
|
closeMenu();
|
||||||
|
|
||||||
if (await showCreateNewRoom(cli, space)) {
|
if (await showCreateNewRoom(space)) {
|
||||||
onNewRoomAdded();
|
onNewRoomAdded();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
@ -343,7 +343,7 @@ const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
closeMenu();
|
closeMenu();
|
||||||
|
|
||||||
const [added] = await showAddExistingRooms(cli, space);
|
const [added] = await showAddExistingRooms(space);
|
||||||
if (added) {
|
if (added) {
|
||||||
onNewRoomAdded();
|
onNewRoomAdded();
|
||||||
}
|
}
|
||||||
|
@ -397,11 +397,11 @@ const SpaceLanding = ({ space }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
let settingsButton;
|
let settingsButton;
|
||||||
if (shouldShowSpaceSettings(cli, space)) {
|
if (shouldShowSpaceSettings(space)) {
|
||||||
settingsButton = <AccessibleTooltipButton
|
settingsButton = <AccessibleTooltipButton
|
||||||
className="mx_SpaceRoomView_landing_settingsButton"
|
className="mx_SpaceRoomView_landing_settingsButton"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
showSpaceSettings(cli, space);
|
showSpaceSettings(space);
|
||||||
}}
|
}}
|
||||||
title={_t("Settings")}
|
title={_t("Settings")}
|
||||||
/>;
|
/>;
|
||||||
|
@ -553,9 +553,7 @@ const SpaceAddExistingRooms = ({ space, onFinished }) => {
|
||||||
onFinished={onFinished}
|
onFinished={onFinished}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="mx_SpaceRoomView_buttons">
|
<div className="mx_SpaceRoomView_buttons" />
|
||||||
|
|
||||||
</div>
|
|
||||||
<SpaceFeedbackPrompt />
|
<SpaceFeedbackPrompt />
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
|
@ -665,8 +665,8 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
private readMarkerTimeout(readMarkerPosition: number): number {
|
private readMarkerTimeout(readMarkerPosition: number): number {
|
||||||
return readMarkerPosition === 0 ?
|
return readMarkerPosition === 0 ?
|
||||||
this.state.readMarkerInViewThresholdMs :
|
this.context?.readMarkerInViewThresholdMs ?? this.state.readMarkerInViewThresholdMs :
|
||||||
this.state.readMarkerOutOfViewThresholdMs;
|
this.context?.readMarkerOutOfViewThresholdMs ?? this.state.readMarkerOutOfViewThresholdMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateReadMarkerOnUserActivity(): Promise<void> {
|
private async updateReadMarkerOnUserActivity(): Promise<void> {
|
||||||
|
@ -1493,8 +1493,12 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
onUserScroll={this.props.onUserScroll}
|
onUserScroll={this.props.onUserScroll}
|
||||||
onFillRequest={this.onMessageListFillRequest}
|
onFillRequest={this.onMessageListFillRequest}
|
||||||
onUnfillRequest={this.onMessageListUnfillRequest}
|
onUnfillRequest={this.onMessageListUnfillRequest}
|
||||||
isTwelveHour={this.state.isTwelveHour}
|
isTwelveHour={this.context?.showTwelveHourTimestamps ?? this.state.isTwelveHour}
|
||||||
alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.alwaysShowTimestamps}
|
alwaysShowTimestamps={
|
||||||
|
this.props.alwaysShowTimestamps ??
|
||||||
|
this.context?.alwaysShowTimestamps ??
|
||||||
|
this.state.alwaysShowTimestamps
|
||||||
|
}
|
||||||
className={this.props.className}
|
className={this.props.className}
|
||||||
tileShape={this.props.tileShape}
|
tileShape={this.props.tileShape}
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
|
|
|
@ -315,7 +315,10 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
||||||
{ _t("An email has been sent to %(emailAddress)s. Once you've followed the " +
|
{ _t("An email has been sent to %(emailAddress)s. Once you've followed the " +
|
||||||
"link it contains, click below.", { emailAddress: this.state.email }) }
|
"link it contains, click below.", { emailAddress: this.state.email }) }
|
||||||
<br />
|
<br />
|
||||||
<input className="mx_Login_submit" type="button" onClick={this.onVerify}
|
<input
|
||||||
|
className="mx_Login_submit"
|
||||||
|
type="button"
|
||||||
|
onClick={this.onVerify}
|
||||||
value={_t('I have verified my email address')} />
|
value={_t('I have verified my email address')} />
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
@ -328,7 +331,10 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
||||||
"push notifications. To re-enable notifications, sign in again on each " +
|
"push notifications. To re-enable notifications, sign in again on each " +
|
||||||
"device.",
|
"device.",
|
||||||
) }</p>
|
) }</p>
|
||||||
<input className="mx_Login_submit" type="button" onClick={this.props.onComplete}
|
<input
|
||||||
|
className="mx_Login_submit"
|
||||||
|
type="button"
|
||||||
|
onClick={this.props.onComplete}
|
||||||
value={_t('Return to login screen')} />
|
value={_t('Return to login screen')} />
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -463,7 +463,9 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
||||||
"Either use HTTPS or <a>enable unsafe scripts</a>.", {},
|
"Either use HTTPS or <a>enable unsafe scripts</a>.", {},
|
||||||
{
|
{
|
||||||
'a': (sub) => {
|
'a': (sub) => {
|
||||||
return <a target="_blank" rel="noreferrer noopener"
|
return <a
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer noopener"
|
||||||
href="https://www.google.com/search?&q=enable%20unsafe%20scripts"
|
href="https://www.google.com/search?&q=enable%20unsafe%20scripts"
|
||||||
>
|
>
|
||||||
{ sub }
|
{ sub }
|
||||||
|
|
|
@ -557,12 +557,16 @@ export default class Registration extends React.Component<IProps, IState> {
|
||||||
loggedInUserId: this.state.differentLoggedInUserId,
|
loggedInUserId: this.state.differentLoggedInUserId,
|
||||||
},
|
},
|
||||||
) }</p>
|
) }</p>
|
||||||
<p><AccessibleButton element="span" className="mx_linkButton" onClick={async event => {
|
<p><AccessibleButton
|
||||||
const sessionLoaded = await this.onLoginClickWithCheck(event);
|
element="span"
|
||||||
if (sessionLoaded) {
|
className="mx_linkButton"
|
||||||
dis.dispatch({ action: "view_welcome_page" });
|
onClick={async event => {
|
||||||
}
|
const sessionLoaded = await this.onLoginClickWithCheck(event);
|
||||||
}}>
|
if (sessionLoaded) {
|
||||||
|
dis.dispatch({ action: "view_welcome_page" });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
{ _t("Continue with previous account") }
|
{ _t("Continue with previous account") }
|
||||||
</AccessibleButton></p>
|
</AccessibleButton></p>
|
||||||
</div>;
|
</div>;
|
||||||
|
|
|
@ -14,9 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Playback, PlaybackState } from "../../../voice/Playback";
|
|
||||||
import React, { createRef, ReactNode, RefObject } from "react";
|
import React, { createRef, ReactNode, RefObject } from "react";
|
||||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
|
||||||
import PlayPauseButton from "./PlayPauseButton";
|
import PlayPauseButton from "./PlayPauseButton";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { formatBytes } from "../../../utils/FormattingUtils";
|
import { formatBytes } from "../../../utils/FormattingUtils";
|
||||||
|
@ -25,44 +23,13 @@ import { Key } from "../../../Keyboard";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import SeekBar from "./SeekBar";
|
import SeekBar from "./SeekBar";
|
||||||
import PlaybackClock from "./PlaybackClock";
|
import PlaybackClock from "./PlaybackClock";
|
||||||
|
import AudioPlayerBase from "./AudioPlayerBase";
|
||||||
interface IProps {
|
|
||||||
// Playback instance to render. Cannot change during component lifecycle: create
|
|
||||||
// an all-new component instead.
|
|
||||||
playback: Playback;
|
|
||||||
|
|
||||||
mediaName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IState {
|
|
||||||
playbackPhase: PlaybackState;
|
|
||||||
}
|
|
||||||
|
|
||||||
@replaceableComponent("views.audio_messages.AudioPlayer")
|
@replaceableComponent("views.audio_messages.AudioPlayer")
|
||||||
export default class AudioPlayer extends React.PureComponent<IProps, IState> {
|
export default class AudioPlayer extends AudioPlayerBase {
|
||||||
private playPauseRef: RefObject<PlayPauseButton> = createRef();
|
private playPauseRef: RefObject<PlayPauseButton> = createRef();
|
||||||
private seekRef: RefObject<SeekBar> = createRef();
|
private seekRef: RefObject<SeekBar> = createRef();
|
||||||
|
|
||||||
constructor(props: IProps) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
playbackPhase: PlaybackState.Decoding, // default assumption
|
|
||||||
};
|
|
||||||
|
|
||||||
// We don't need to de-register: the class handles this for us internally
|
|
||||||
this.props.playback.on(UPDATE_EVENT, this.onPlaybackUpdate);
|
|
||||||
|
|
||||||
// Don't wait for the promise to complete - it will emit a progress update when it
|
|
||||||
// is done, and it's not meant to take long anyhow.
|
|
||||||
// noinspection JSIgnoredPromiseFromCall
|
|
||||||
this.props.playback.prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
private onPlaybackUpdate = (ev: PlaybackState) => {
|
|
||||||
this.setState({ playbackPhase: ev });
|
|
||||||
};
|
|
||||||
|
|
||||||
private onKeyDown = (ev: React.KeyboardEvent) => {
|
private onKeyDown = (ev: React.KeyboardEvent) => {
|
||||||
// stopPropagation() prevents the FocusComposer catch-all from triggering,
|
// stopPropagation() prevents the FocusComposer catch-all from triggering,
|
||||||
// but we need to do it on key down instead of press (even though the user
|
// but we need to do it on key down instead of press (even though the user
|
||||||
|
@ -88,37 +55,39 @@ export default class AudioPlayer extends React.PureComponent<IProps, IState> {
|
||||||
return `(${formatBytes(bytes)})`;
|
return `(${formatBytes(bytes)})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): ReactNode {
|
protected renderComponent(): ReactNode {
|
||||||
// tabIndex=0 to ensure that the whole component becomes a tab stop, where we handle keyboard
|
// tabIndex=0 to ensure that the whole component becomes a tab stop, where we handle keyboard
|
||||||
// events for accessibility
|
// events for accessibility
|
||||||
return <div className='mx_MediaBody mx_AudioPlayer_container' tabIndex={0} onKeyDown={this.onKeyDown}>
|
return (
|
||||||
<div className='mx_AudioPlayer_primaryContainer'>
|
<div className='mx_MediaBody mx_AudioPlayer_container' tabIndex={0} onKeyDown={this.onKeyDown}>
|
||||||
<PlayPauseButton
|
<div className='mx_AudioPlayer_primaryContainer'>
|
||||||
playback={this.props.playback}
|
<PlayPauseButton
|
||||||
playbackPhase={this.state.playbackPhase}
|
playback={this.props.playback}
|
||||||
tabIndex={-1} // prevent tabbing into the button
|
playbackPhase={this.state.playbackPhase}
|
||||||
ref={this.playPauseRef}
|
tabIndex={-1} // prevent tabbing into the button
|
||||||
/>
|
ref={this.playPauseRef}
|
||||||
<div className='mx_AudioPlayer_mediaInfo'>
|
/>
|
||||||
<span className='mx_AudioPlayer_mediaName'>
|
<div className='mx_AudioPlayer_mediaInfo'>
|
||||||
{ this.props.mediaName || _t("Unnamed audio") }
|
<span className='mx_AudioPlayer_mediaName'>
|
||||||
</span>
|
{ this.props.mediaName || _t("Unnamed audio") }
|
||||||
<div className='mx_AudioPlayer_byline'>
|
</span>
|
||||||
<DurationClock playback={this.props.playback} />
|
<div className='mx_AudioPlayer_byline'>
|
||||||
{ /* easiest way to introduce a gap between the components */ }
|
<DurationClock playback={this.props.playback} />
|
||||||
{ this.renderFileSize() }
|
{ /* easiest way to introduce a gap between the components */ }
|
||||||
|
{ this.renderFileSize() }
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className='mx_AudioPlayer_seek'>
|
||||||
|
<SeekBar
|
||||||
|
playback={this.props.playback}
|
||||||
|
tabIndex={-1} // prevent tabbing into the bar
|
||||||
|
playbackPhase={this.state.playbackPhase}
|
||||||
|
ref={this.seekRef}
|
||||||
|
/>
|
||||||
|
<PlaybackClock playback={this.props.playback} defaultDisplaySeconds={0} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_AudioPlayer_seek'>
|
);
|
||||||
<SeekBar
|
|
||||||
playback={this.props.playback}
|
|
||||||
tabIndex={-1} // prevent tabbing into the bar
|
|
||||||
playbackPhase={this.state.playbackPhase}
|
|
||||||
ref={this.seekRef}
|
|
||||||
/>
|
|
||||||
<PlaybackClock playback={this.props.playback} defaultDisplaySeconds={0} />
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
70
src/components/views/audio_messages/AudioPlayerBase.tsx
Normal file
70
src/components/views/audio_messages/AudioPlayerBase.tsx
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Playback, PlaybackState } from "../../../audio/Playback";
|
||||||
|
import { TileShape } from "../rooms/EventTile";
|
||||||
|
import React, { ReactNode } from "react";
|
||||||
|
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||||
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
// Playback instance to render. Cannot change during component lifecycle: create
|
||||||
|
// an all-new component instead.
|
||||||
|
playback: Playback;
|
||||||
|
|
||||||
|
mediaName?: string;
|
||||||
|
tileShape?: TileShape;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
playbackPhase: PlaybackState;
|
||||||
|
error?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("views.audio_messages.AudioPlayerBase")
|
||||||
|
export default abstract class AudioPlayerBase extends React.PureComponent<IProps, IState> {
|
||||||
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
playbackPhase: PlaybackState.Decoding, // default assumption
|
||||||
|
};
|
||||||
|
|
||||||
|
// We don't need to de-register: the class handles this for us internally
|
||||||
|
this.props.playback.on(UPDATE_EVENT, this.onPlaybackUpdate);
|
||||||
|
|
||||||
|
// Don't wait for the promise to complete - it will emit a progress update when it
|
||||||
|
// is done, and it's not meant to take long anyhow.
|
||||||
|
this.props.playback.prepare().catch(e => {
|
||||||
|
console.error("Error processing audio file:", e);
|
||||||
|
this.setState({ error: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private onPlaybackUpdate = (ev: PlaybackState) => {
|
||||||
|
this.setState({ playbackPhase: ev });
|
||||||
|
};
|
||||||
|
|
||||||
|
protected abstract renderComponent(): ReactNode;
|
||||||
|
|
||||||
|
public render(): ReactNode {
|
||||||
|
return <>
|
||||||
|
{ this.renderComponent() }
|
||||||
|
{ this.state.error && <div className="text-warning">{ _t("Error downloading audio") }</div> }
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import Clock from "./Clock";
|
import Clock from "./Clock";
|
||||||
import { Playback } from "../../../voice/Playback";
|
import { Playback } from "../../../audio/Playback";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
playback: Playback;
|
playback: Playback;
|
||||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { IRecordingUpdate, VoiceRecording } from "../../../voice/VoiceRecording";
|
import { IRecordingUpdate, VoiceRecording } from "../../../audio/VoiceRecording";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import Clock from "./Clock";
|
import Clock from "./Clock";
|
||||||
import { MarkedExecution } from "../../../utils/MarkedExecution";
|
import { MarkedExecution } from "../../../utils/MarkedExecution";
|
||||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording } from "../../../voice/VoiceRecording";
|
import { IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording } from "../../../audio/VoiceRecording";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { arrayFastResample } from "../../../utils/arrays";
|
import { arrayFastResample } from "../../../utils/arrays";
|
||||||
import { percentageOf } from "../../../utils/numbers";
|
import { percentageOf } from "../../../utils/numbers";
|
||||||
|
|
|
@ -18,7 +18,7 @@ import React, { ReactNode } from "react";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { Playback, PlaybackState } from "../../../voice/Playback";
|
import { Playback, PlaybackState } from "../../../audio/Playback";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
// omitted props are handled by render function
|
// omitted props are handled by render function
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import Clock from "./Clock";
|
import Clock from "./Clock";
|
||||||
import { Playback, PlaybackState } from "../../../voice/Playback";
|
import { Playback, PlaybackState } from "../../../audio/Playback";
|
||||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
|
|
|
@ -18,7 +18,7 @@ import React from "react";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { arraySeed, arrayTrimFill } from "../../../utils/arrays";
|
import { arraySeed, arrayTrimFill } from "../../../utils/arrays";
|
||||||
import Waveform from "./Waveform";
|
import Waveform from "./Waveform";
|
||||||
import { Playback, PLAYBACK_WAVEFORM_SAMPLES } from "../../../voice/Playback";
|
import { Playback, PLAYBACK_WAVEFORM_SAMPLES } from "../../../audio/Playback";
|
||||||
import { percentageOf } from "../../../utils/numbers";
|
import { percentageOf } from "../../../utils/numbers";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
|
|
|
@ -14,61 +14,30 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Playback, PlaybackState } from "../../../voice/Playback";
|
|
||||||
import React, { ReactNode } from "react";
|
import React, { ReactNode } from "react";
|
||||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
|
||||||
import PlayPauseButton from "./PlayPauseButton";
|
import PlayPauseButton from "./PlayPauseButton";
|
||||||
import PlaybackClock from "./PlaybackClock";
|
import PlaybackClock from "./PlaybackClock";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { TileShape } from "../rooms/EventTile";
|
import { TileShape } from "../rooms/EventTile";
|
||||||
import PlaybackWaveform from "./PlaybackWaveform";
|
import PlaybackWaveform from "./PlaybackWaveform";
|
||||||
|
import AudioPlayerBase from "./AudioPlayerBase";
|
||||||
interface IProps {
|
|
||||||
// Playback instance to render. Cannot change during component lifecycle: create
|
|
||||||
// an all-new component instead.
|
|
||||||
playback: Playback;
|
|
||||||
|
|
||||||
tileShape?: TileShape;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IState {
|
|
||||||
playbackPhase: PlaybackState;
|
|
||||||
}
|
|
||||||
|
|
||||||
@replaceableComponent("views.audio_messages.RecordingPlayback")
|
@replaceableComponent("views.audio_messages.RecordingPlayback")
|
||||||
export default class RecordingPlayback extends React.PureComponent<IProps, IState> {
|
export default class RecordingPlayback extends AudioPlayerBase {
|
||||||
constructor(props: IProps) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
playbackPhase: PlaybackState.Decoding, // default assumption
|
|
||||||
};
|
|
||||||
|
|
||||||
// We don't need to de-register: the class handles this for us internally
|
|
||||||
this.props.playback.on(UPDATE_EVENT, this.onPlaybackUpdate);
|
|
||||||
|
|
||||||
// Don't wait for the promise to complete - it will emit a progress update when it
|
|
||||||
// is done, and it's not meant to take long anyhow.
|
|
||||||
// noinspection JSIgnoredPromiseFromCall
|
|
||||||
this.props.playback.prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
private get isWaveformable(): boolean {
|
private get isWaveformable(): boolean {
|
||||||
return this.props.tileShape !== TileShape.Notif
|
return this.props.tileShape !== TileShape.Notif
|
||||||
&& this.props.tileShape !== TileShape.FileGrid
|
&& this.props.tileShape !== TileShape.FileGrid
|
||||||
&& this.props.tileShape !== TileShape.Pinned;
|
&& this.props.tileShape !== TileShape.Pinned;
|
||||||
}
|
}
|
||||||
|
|
||||||
private onPlaybackUpdate = (ev: PlaybackState) => {
|
protected renderComponent(): ReactNode {
|
||||||
this.setState({ playbackPhase: ev });
|
|
||||||
};
|
|
||||||
|
|
||||||
public render(): ReactNode {
|
|
||||||
const shapeClass = !this.isWaveformable ? 'mx_VoiceMessagePrimaryContainer_noWaveform' : '';
|
const shapeClass = !this.isWaveformable ? 'mx_VoiceMessagePrimaryContainer_noWaveform' : '';
|
||||||
return <div className={'mx_MediaBody mx_VoiceMessagePrimaryContainer ' + shapeClass}>
|
return (
|
||||||
<PlayPauseButton playback={this.props.playback} playbackPhase={this.state.playbackPhase} />
|
<div className={'mx_MediaBody mx_VoiceMessagePrimaryContainer ' + shapeClass}>
|
||||||
<PlaybackClock playback={this.props.playback} />
|
<PlayPauseButton playback={this.props.playback} playbackPhase={this.state.playbackPhase} />
|
||||||
{ this.isWaveformable && <PlaybackWaveform playback={this.props.playback} /> }
|
<PlaybackClock playback={this.props.playback} />
|
||||||
</div>;
|
{ this.isWaveformable && <PlaybackWaveform playback={this.props.playback} /> }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Playback, PlaybackState } from "../../../voice/Playback";
|
import { Playback, PlaybackState } from "../../../audio/Playback";
|
||||||
import React, { ChangeEvent, CSSProperties, ReactNode } from "react";
|
import React, { ChangeEvent, CSSProperties, ReactNode } from "react";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { MarkedExecution } from "../../../utils/MarkedExecution";
|
import { MarkedExecution } from "../../../utils/MarkedExecution";
|
||||||
|
|
|
@ -54,9 +54,13 @@ export default class Waveform extends React.PureComponent<IProps, IState> {
|
||||||
'mx_Waveform_bar': true,
|
'mx_Waveform_bar': true,
|
||||||
'mx_Waveform_bar_100pct': isCompleteBar,
|
'mx_Waveform_bar_100pct': isCompleteBar,
|
||||||
});
|
});
|
||||||
return <span key={i} style={{
|
return <span
|
||||||
"--barHeight": h,
|
key={i}
|
||||||
} as WaveformCSSProperties} className={classes} />;
|
style={{
|
||||||
|
"--barHeight": h,
|
||||||
|
} as WaveformCSSProperties}
|
||||||
|
className={classes}
|
||||||
|
/>;
|
||||||
}) }
|
}) }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -416,8 +416,10 @@ export class TermsAuthEntry extends React.Component<ITermsAuthEntryProps, ITerms
|
||||||
let submitButton;
|
let submitButton;
|
||||||
if (this.props.showContinue !== false) {
|
if (this.props.showContinue !== false) {
|
||||||
// XXX: button classes
|
// XXX: button classes
|
||||||
submitButton = <button className="mx_InteractiveAuthEntryComponents_termsSubmit mx_GeneralButton"
|
submitButton = <button
|
||||||
onClick={this.trySubmit} disabled={!allChecked}>{ _t("Accept") }</button>;
|
className="mx_InteractiveAuthEntryComponents_termsSubmit mx_GeneralButton"
|
||||||
|
onClick={this.trySubmit}
|
||||||
|
disabled={!allChecked}>{ _t("Accept") }</button>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -616,7 +618,9 @@ export class MsisdnAuthEntry extends React.Component<IMsisdnAuthEntryProps, IMsi
|
||||||
aria-label={_t("Code")}
|
aria-label={_t("Code")}
|
||||||
/>
|
/>
|
||||||
<br />
|
<br />
|
||||||
<input type="submit" value={_t("Submit")}
|
<input
|
||||||
|
type="submit"
|
||||||
|
value={_t("Submit")}
|
||||||
className={submitClasses}
|
className={submitClasses}
|
||||||
disabled={!enableSubmit}
|
disabled={!enableSubmit}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -187,7 +187,8 @@ const BaseAvatar = (props: IProps) => {
|
||||||
width: toPx(width),
|
width: toPx(width),
|
||||||
height: toPx(height),
|
height: toPx(height),
|
||||||
}}
|
}}
|
||||||
title={title} alt={_t("Avatar")}
|
title={title}
|
||||||
|
alt={_t("Avatar")}
|
||||||
inputRef={inputRef}
|
inputRef={inputRef}
|
||||||
{...otherProps} />
|
{...otherProps} />
|
||||||
);
|
);
|
||||||
|
@ -201,7 +202,8 @@ const BaseAvatar = (props: IProps) => {
|
||||||
width: toPx(width),
|
width: toPx(width),
|
||||||
height: toPx(height),
|
height: toPx(height),
|
||||||
}}
|
}}
|
||||||
title={title} alt=""
|
title={title}
|
||||||
|
alt=""
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
{...otherProps} />
|
{...otherProps} />
|
||||||
);
|
);
|
||||||
|
|
|
@ -102,8 +102,12 @@ export default class MemberAvatar extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseAvatar {...otherProps} name={this.state.name} title={this.state.title}
|
<BaseAvatar {...otherProps}
|
||||||
idName={userId} url={this.state.imageUrl} onClick={onClick} />
|
name={this.state.name}
|
||||||
|
title={this.state.title}
|
||||||
|
idName={userId}
|
||||||
|
url={this.state.imageUrl}
|
||||||
|
onClick={onClick} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,9 +13,11 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { ComponentProps } from 'react';
|
import React, { ComponentProps } from 'react';
|
||||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||||
import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
|
import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
import BaseAvatar from './BaseAvatar';
|
import BaseAvatar from './BaseAvatar';
|
||||||
import ImageView from '../elements/ImageView';
|
import ImageView from '../elements/ImageView';
|
||||||
|
@ -32,11 +34,14 @@ interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idNam
|
||||||
// oobData.avatarUrl should be set (else there
|
// oobData.avatarUrl should be set (else there
|
||||||
// would be nowhere to get the avatar from)
|
// would be nowhere to get the avatar from)
|
||||||
room?: Room;
|
room?: Room;
|
||||||
oobData?: IOOBData;
|
oobData?: IOOBData & {
|
||||||
|
roomId?: string;
|
||||||
|
};
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
resizeMethod?: ResizeMethod;
|
resizeMethod?: ResizeMethod;
|
||||||
viewAvatarOnClick?: boolean;
|
viewAvatarOnClick?: boolean;
|
||||||
|
className?: string;
|
||||||
onClick?(): void;
|
onClick?(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,15 +134,19 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const { room, oobData, viewAvatarOnClick, onClick, ...otherProps } = this.props;
|
const { room, oobData, viewAvatarOnClick, onClick, className, ...otherProps } = this.props;
|
||||||
|
|
||||||
const roomName = room ? room.name : oobData.name;
|
const roomName = room ? room.name : oobData.name;
|
||||||
// If the room is a DM, we use the other user's ID for the color hash
|
// If the room is a DM, we use the other user's ID for the color hash
|
||||||
// in order to match the room avatar with their avatar
|
// in order to match the room avatar with their avatar
|
||||||
const idName = room ? (DMRoomMap.shared().getUserIdForRoomId(room.roomId) ?? room.roomId) : null;
|
const idName = room ? (DMRoomMap.shared().getUserIdForRoomId(room.roomId) ?? room.roomId) : oobData.roomId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseAvatar {...otherProps}
|
<BaseAvatar
|
||||||
|
{...otherProps}
|
||||||
|
className={classNames(className, {
|
||||||
|
mx_RoomAvatar_isSpaceRoom: room?.isSpaceRoom(),
|
||||||
|
})}
|
||||||
name={roomName}
|
name={roomName}
|
||||||
idName={idName}
|
idName={idName}
|
||||||
urls={this.state.urls}
|
urls={this.state.urls}
|
||||||
|
|
|
@ -60,8 +60,10 @@ export default class DialpadContextMenu extends React.Component<IProps, IState>
|
||||||
<AccessibleButton className="mx_DialPadContextMenu_cancel" onClick={this.onCancelClick} />
|
<AccessibleButton className="mx_DialPadContextMenu_cancel" onClick={this.onCancelClick} />
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_DialPadContextMenu_header">
|
<div className="mx_DialPadContextMenu_header">
|
||||||
<Field className="mx_DialPadContextMenu_dialled"
|
<Field
|
||||||
value={this.state.value} autoFocus={true}
|
className="mx_DialPadContextMenu_dialled"
|
||||||
|
value={this.state.value}
|
||||||
|
autoFocus={true}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -109,8 +109,10 @@ export default class StatusMessageContextMenu extends React.Component {
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_submit"
|
actionButton = <AccessibleButton
|
||||||
disabled={!this.state.message} onClick={this._onSubmit}
|
className="mx_StatusMessageContextMenu_submit"
|
||||||
|
disabled={!this.state.message}
|
||||||
|
onClick={this._onSubmit}
|
||||||
>
|
>
|
||||||
<span>{ _t("Set status") }</span>
|
<span>{ _t("Set status") }</span>
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
|
@ -121,12 +123,19 @@ export default class StatusMessageContextMenu extends React.Component {
|
||||||
spinner = <Spinner w="24" h="24" />;
|
spinner = <Spinner w="24" h="24" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const form = <form className="mx_StatusMessageContextMenu_form"
|
const form = <form
|
||||||
autoComplete="off" onSubmit={this._onSubmit}
|
className="mx_StatusMessageContextMenu_form"
|
||||||
|
autoComplete="off"
|
||||||
|
onSubmit={this._onSubmit}
|
||||||
>
|
>
|
||||||
<input type="text" className="mx_StatusMessageContextMenu_message"
|
<input
|
||||||
key="message" placeholder={_t("Set a new status...")}
|
type="text"
|
||||||
autoFocus={true} maxLength="60" value={this.state.message}
|
className="mx_StatusMessageContextMenu_message"
|
||||||
|
key="message"
|
||||||
|
placeholder={_t("Set a new status...")}
|
||||||
|
autoFocus={true}
|
||||||
|
maxLength="60"
|
||||||
|
value={this.state.message}
|
||||||
onChange={this._onStatusChange}
|
onChange={this._onStatusChange}
|
||||||
/>
|
/>
|
||||||
<div className="mx_StatusMessageContextMenu_actionContainer">
|
<div className="mx_StatusMessageContextMenu_actionContainer">
|
||||||
|
|
|
@ -76,7 +76,8 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
||||||
onFinished();
|
onFinished();
|
||||||
};
|
};
|
||||||
streamAudioStreamButton = <IconizedContextMenuOption
|
streamAudioStreamButton = <IconizedContextMenuOption
|
||||||
onClick={onStreamAudioClick} label={_t("Start audio stream")}
|
onClick={onStreamAudioClick}
|
||||||
|
label={_t("Start audio stream")}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
||||||
import React, { ReactNode, 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 { sleep } from "matrix-js-sdk/src/utils";
|
import { sleep } from "matrix-js-sdk/src/utils";
|
||||||
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
@ -44,9 +43,8 @@ import EntityTile from "../rooms/EntityTile";
|
||||||
import BaseAvatar from "../avatars/BaseAvatar";
|
import BaseAvatar from "../avatars/BaseAvatar";
|
||||||
|
|
||||||
interface IProps extends IDialogProps {
|
interface IProps extends IDialogProps {
|
||||||
matrixClient: MatrixClient;
|
|
||||||
space: Room;
|
space: Room;
|
||||||
onCreateRoomClick(cli: MatrixClient, space: Room): void;
|
onCreateRoomClick(space: Room): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Entry = ({ room, checked, onChange }) => {
|
const Entry = ({ room, checked, onChange }) => {
|
||||||
|
@ -211,10 +209,16 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
|
||||||
function overflowTile(overflowCount, totalCount) {
|
function overflowTile(overflowCount, totalCount) {
|
||||||
const text = _t("and %(count)s others...", { count: overflowCount });
|
const text = _t("and %(count)s others...", { count: overflowCount });
|
||||||
return (
|
return (
|
||||||
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
|
<EntityTile
|
||||||
<BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} />
|
className="mx_EntityTile_ellipsis"
|
||||||
} name={text} presenceState="online" suppressOnHover={true}
|
avatarJsx={
|
||||||
onClick={() => setTruncateAt(totalCount)} />
|
<BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} />
|
||||||
|
}
|
||||||
|
name={text}
|
||||||
|
presenceState="online"
|
||||||
|
suppressOnHover={true}
|
||||||
|
onClick={() => setTruncateAt(totalCount)}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -295,7 +299,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => {
|
const AddExistingToSpaceDialog: React.FC<IProps> = ({ space, onCreateRoomClick, onFinished }) => {
|
||||||
const [selectedSpace, setSelectedSpace] = useState(space);
|
const [selectedSpace, setSelectedSpace] = useState(space);
|
||||||
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
|
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
|
||||||
|
|
||||||
|
@ -344,13 +348,13 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
|
||||||
onFinished={onFinished}
|
onFinished={onFinished}
|
||||||
fixedWidth={false}
|
fixedWidth={false}
|
||||||
>
|
>
|
||||||
<MatrixClientContext.Provider value={cli}>
|
<MatrixClientContext.Provider value={space.client}>
|
||||||
<AddExistingToSpace
|
<AddExistingToSpace
|
||||||
space={space}
|
space={space}
|
||||||
onFinished={onFinished}
|
onFinished={onFinished}
|
||||||
footerPrompt={<>
|
footerPrompt={<>
|
||||||
<div>{ _t("Want to add a new room instead?") }</div>
|
<div>{ _t("Want to add a new room instead?") }</div>
|
||||||
<AccessibleButton onClick={() => onCreateRoomClick(cli, space)} kind="link">
|
<AccessibleButton onClick={() => onCreateRoomClick(space)} kind="link">
|
||||||
{ _t("Create a new room") }
|
{ _t("Create a new room") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</>}
|
</>}
|
||||||
|
|
|
@ -18,14 +18,12 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createRef } from 'react';
|
import React, { createRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { sleep } from "matrix-js-sdk/src/utils";
|
import { sleep } from "matrix-js-sdk/src/utils";
|
||||||
|
|
||||||
import { _t, _td } from '../../../languageHandler';
|
import { _t, _td } from '../../../languageHandler';
|
||||||
import * as sdk from '../../../index';
|
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import { addressTypes, getAddressType } from '../../../UserAddress';
|
import { AddressType, addressTypes, getAddressType, IUserAddress } from '../../../UserAddress';
|
||||||
import GroupStore from '../../../stores/GroupStore';
|
import GroupStore from '../../../stores/GroupStore';
|
||||||
import * as Email from '../../../email';
|
import * as Email from '../../../email';
|
||||||
import IdentityAuthClient from '../../../IdentityAuthClient';
|
import IdentityAuthClient from '../../../IdentityAuthClient';
|
||||||
|
@ -34,6 +32,10 @@ import { abbreviateUrl } from '../../../utils/UrlUtils';
|
||||||
import { Key } from "../../../Keyboard";
|
import { Key } from "../../../Keyboard";
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import AddressSelector from '../elements/AddressSelector';
|
||||||
|
import AddressTile from '../elements/AddressTile';
|
||||||
|
import BaseDialog from "./BaseDialog";
|
||||||
|
import DialogButtons from "../elements/DialogButtons";
|
||||||
|
|
||||||
const TRUNCATE_QUERY_LIST = 40;
|
const TRUNCATE_QUERY_LIST = 40;
|
||||||
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
|
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
|
||||||
|
@ -44,29 +46,64 @@ const addressTypeName = {
|
||||||
'email': _td("email address"),
|
'email': _td("email address"),
|
||||||
};
|
};
|
||||||
|
|
||||||
@replaceableComponent("views.dialogs.AddressPickerDialog")
|
interface IResult {
|
||||||
export default class AddressPickerDialog extends React.Component {
|
user_id: string; // eslint-disable-line camelcase
|
||||||
static propTypes = {
|
room_id?: string; // eslint-disable-line camelcase
|
||||||
title: PropTypes.string.isRequired,
|
name?: string;
|
||||||
description: PropTypes.node,
|
display_name?: string; // eslint-disable-line camelcase
|
||||||
// Extra node inserted after picker input, dropdown and errors
|
avatar_url?: string;// eslint-disable-line camelcase
|
||||||
extraNode: PropTypes.node,
|
}
|
||||||
value: PropTypes.string,
|
|
||||||
placeholder: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
|
||||||
roomId: PropTypes.string,
|
|
||||||
button: PropTypes.string,
|
|
||||||
focus: PropTypes.bool,
|
|
||||||
validAddressTypes: PropTypes.arrayOf(PropTypes.oneOf(addressTypes)),
|
|
||||||
onFinished: PropTypes.func.isRequired,
|
|
||||||
groupId: PropTypes.string,
|
|
||||||
// The type of entity to search for. Default: 'user'.
|
|
||||||
pickerType: PropTypes.oneOf(['user', 'room']),
|
|
||||||
// Whether the current user should be included in the addresses returned. Only
|
|
||||||
// applicable when pickerType is `user`. Default: false.
|
|
||||||
includeSelf: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
interface IProps {
|
||||||
|
title: string;
|
||||||
|
description?: JSX.Element;
|
||||||
|
// Extra node inserted after picker input, dropdown and errors
|
||||||
|
extraNode?: JSX.Element;
|
||||||
|
value?: string;
|
||||||
|
placeholder?: ((validAddressTypes: any) => string) | string;
|
||||||
|
roomId?: string;
|
||||||
|
button?: string;
|
||||||
|
focus?: boolean;
|
||||||
|
validAddressTypes?: AddressType[];
|
||||||
|
onFinished: (success: boolean, list?: IUserAddress[]) => void;
|
||||||
|
groupId?: string;
|
||||||
|
// The type of entity to search for. Default: 'user'.
|
||||||
|
pickerType?: 'user' | 'room';
|
||||||
|
// Whether the current user should be included in the addresses returned. Only
|
||||||
|
// applicable when pickerType is `user`. Default: false.
|
||||||
|
includeSelf?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
// Whether to show an error message because of an invalid address
|
||||||
|
invalidAddressError: boolean;
|
||||||
|
// List of UserAddressType objects representing
|
||||||
|
// the list of addresses we're going to invite
|
||||||
|
selectedList: IUserAddress[];
|
||||||
|
// Whether a search is ongoing
|
||||||
|
busy: boolean;
|
||||||
|
// An error message generated during the user directory search
|
||||||
|
searchError: string;
|
||||||
|
// Whether the server supports the user_directory API
|
||||||
|
serverSupportsUserDirectory: boolean;
|
||||||
|
// The query being searched for
|
||||||
|
query: string;
|
||||||
|
// List of UserAddressType objects representing the set of
|
||||||
|
// auto-completion results for the current search query.
|
||||||
|
suggestedList: IUserAddress[];
|
||||||
|
// List of address types initialised from props, but may change while the
|
||||||
|
// dialog is open and represents the supported list of address types at this time.
|
||||||
|
validAddressTypes: AddressType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("views.dialogs.AddressPickerDialog")
|
||||||
|
export default class AddressPickerDialog extends React.Component<IProps, IState> {
|
||||||
|
private textinput = createRef<HTMLTextAreaElement>();
|
||||||
|
private addressSelector = createRef<AddressSelector>();
|
||||||
|
private queryChangedDebouncer: number;
|
||||||
|
private cancelThreepidLookup: () => void;
|
||||||
|
|
||||||
|
static defaultProps: Partial<IProps> = {
|
||||||
value: "",
|
value: "",
|
||||||
focus: true,
|
focus: true,
|
||||||
validAddressTypes: addressTypes,
|
validAddressTypes: addressTypes,
|
||||||
|
@ -74,36 +111,23 @@ export default class AddressPickerDialog extends React.Component {
|
||||||
includeSelf: false,
|
includeSelf: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this._textinput = createRef();
|
|
||||||
|
|
||||||
let validAddressTypes = this.props.validAddressTypes;
|
let validAddressTypes = this.props.validAddressTypes;
|
||||||
// Remove email from validAddressTypes if no IS is configured. It may be added at a later stage by the user
|
// Remove email from validAddressTypes if no IS is configured. It may be added at a later stage by the user
|
||||||
if (!MatrixClientPeg.get().getIdentityServerUrl() && validAddressTypes.includes("email")) {
|
if (!MatrixClientPeg.get().getIdentityServerUrl() && validAddressTypes.includes(AddressType.Email)) {
|
||||||
validAddressTypes = validAddressTypes.filter(type => type !== "email");
|
validAddressTypes = validAddressTypes.filter(type => type !== AddressType.Email);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
// Whether to show an error message because of an invalid address
|
|
||||||
invalidAddressError: false,
|
invalidAddressError: false,
|
||||||
// List of UserAddressType objects representing
|
|
||||||
// the list of addresses we're going to invite
|
|
||||||
selectedList: [],
|
selectedList: [],
|
||||||
// Whether a search is ongoing
|
|
||||||
busy: false,
|
busy: false,
|
||||||
// An error message generated during the user directory search
|
|
||||||
searchError: null,
|
searchError: null,
|
||||||
// Whether the server supports the user_directory API
|
|
||||||
serverSupportsUserDirectory: true,
|
serverSupportsUserDirectory: true,
|
||||||
// The query being searched for
|
|
||||||
query: "",
|
query: "",
|
||||||
// List of UserAddressType objects representing the set of
|
|
||||||
// auto-completion results for the current search query.
|
|
||||||
suggestedList: [],
|
suggestedList: [],
|
||||||
// List of address types initialised from props, but may change while the
|
|
||||||
// dialog is open and represents the supported list of address types at this time.
|
|
||||||
validAddressTypes,
|
validAddressTypes,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -111,11 +135,11 @@ export default class AddressPickerDialog extends React.Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (this.props.focus) {
|
if (this.props.focus) {
|
||||||
// Set the cursor at the end of the text input
|
// Set the cursor at the end of the text input
|
||||||
this._textinput.current.value = this.props.value;
|
this.textinput.current.value = this.props.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getPlaceholder() {
|
private getPlaceholder(): string {
|
||||||
const { placeholder } = this.props;
|
const { placeholder } = this.props;
|
||||||
if (typeof placeholder === "string") {
|
if (typeof placeholder === "string") {
|
||||||
return placeholder;
|
return placeholder;
|
||||||
|
@ -124,23 +148,23 @@ export default class AddressPickerDialog extends React.Component {
|
||||||
return placeholder(this.state.validAddressTypes);
|
return placeholder(this.state.validAddressTypes);
|
||||||
}
|
}
|
||||||
|
|
||||||
onButtonClick = () => {
|
private onButtonClick = (): void => {
|
||||||
let selectedList = this.state.selectedList.slice();
|
let selectedList = this.state.selectedList.slice();
|
||||||
// Check the text input field to see if user has an unconverted address
|
// Check the text input field to see if user has an unconverted address
|
||||||
// If there is and it's valid add it to the local selectedList
|
// If there is and it's valid add it to the local selectedList
|
||||||
if (this._textinput.current.value !== '') {
|
if (this.textinput.current.value !== '') {
|
||||||
selectedList = this._addAddressesToList([this._textinput.current.value]);
|
selectedList = this.addAddressesToList([this.textinput.current.value]);
|
||||||
if (selectedList === null) return;
|
if (selectedList === null) return;
|
||||||
}
|
}
|
||||||
this.props.onFinished(true, selectedList);
|
this.props.onFinished(true, selectedList);
|
||||||
};
|
};
|
||||||
|
|
||||||
onCancel = () => {
|
private onCancel = (): void => {
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
onKeyDown = e => {
|
private onKeyDown = (e: React.KeyboardEvent): void => {
|
||||||
const textInput = this._textinput.current ? this._textinput.current.value : undefined;
|
const textInput = this.textinput.current ? this.textinput.current.value : undefined;
|
||||||
|
|
||||||
if (e.key === Key.ESCAPE) {
|
if (e.key === Key.ESCAPE) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
@ -149,15 +173,15 @@ export default class AddressPickerDialog extends React.Component {
|
||||||
} else if (e.key === Key.ARROW_UP) {
|
} else if (e.key === Key.ARROW_UP) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (this.addressSelector) this.addressSelector.moveSelectionUp();
|
if (this.addressSelector.current) this.addressSelector.current.moveSelectionUp();
|
||||||
} else if (e.key === Key.ARROW_DOWN) {
|
} else if (e.key === Key.ARROW_DOWN) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (this.addressSelector) this.addressSelector.moveSelectionDown();
|
if (this.addressSelector.current) this.addressSelector.current.moveSelectionDown();
|
||||||
} else if (this.state.suggestedList.length > 0 && [Key.COMMA, Key.ENTER, Key.TAB].includes(e.key)) {
|
} else if (this.state.suggestedList.length > 0 && [Key.COMMA, Key.ENTER, Key.TAB].includes(e.key)) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (this.addressSelector) this.addressSelector.chooseSelection();
|
if (this.addressSelector.current) this.addressSelector.current.chooseSelection();
|
||||||
} else if (textInput.length === 0 && this.state.selectedList.length && e.key === Key.BACKSPACE) {
|
} else if (textInput.length === 0 && this.state.selectedList.length && e.key === Key.BACKSPACE) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -169,17 +193,17 @@ export default class AddressPickerDialog extends React.Component {
|
||||||
// if there's nothing in the input box, submit the form
|
// if there's nothing in the input box, submit the form
|
||||||
this.onButtonClick();
|
this.onButtonClick();
|
||||||
} else {
|
} else {
|
||||||
this._addAddressesToList([textInput]);
|
this.addAddressesToList([textInput]);
|
||||||
}
|
}
|
||||||
} else if (textInput && (e.key === Key.COMMA || e.key === Key.TAB)) {
|
} else if (textInput && (e.key === Key.COMMA || e.key === Key.TAB)) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this._addAddressesToList([textInput]);
|
this.addAddressesToList([textInput]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onQueryChanged = ev => {
|
private onQueryChanged = (ev: React.ChangeEvent): void => {
|
||||||
const query = ev.target.value;
|
const query = (ev.target as HTMLTextAreaElement).value;
|
||||||
if (this.queryChangedDebouncer) {
|
if (this.queryChangedDebouncer) {
|
||||||
clearTimeout(this.queryChangedDebouncer);
|
clearTimeout(this.queryChangedDebouncer);
|
||||||
}
|
}
|
||||||
|
@ -188,17 +212,17 @@ export default class AddressPickerDialog extends React.Component {
|
||||||
this.queryChangedDebouncer = setTimeout(() => {
|
this.queryChangedDebouncer = setTimeout(() => {
|
||||||
if (this.props.pickerType === 'user') {
|
if (this.props.pickerType === 'user') {
|
||||||
if (this.props.groupId) {
|
if (this.props.groupId) {
|
||||||
this._doNaiveGroupSearch(query);
|
this.doNaiveGroupSearch(query);
|
||||||
} else if (this.state.serverSupportsUserDirectory) {
|
} else if (this.state.serverSupportsUserDirectory) {
|
||||||
this._doUserDirectorySearch(query);
|
this.doUserDirectorySearch(query);
|
||||||
} else {
|
} else {
|
||||||
this._doLocalSearch(query);
|
this.doLocalSearch(query);
|
||||||
}
|
}
|
||||||
} else if (this.props.pickerType === 'room') {
|
} else if (this.props.pickerType === 'room') {
|
||||||
if (this.props.groupId) {
|
if (this.props.groupId) {
|
||||||
this._doNaiveGroupRoomSearch(query);
|
this.doNaiveGroupRoomSearch(query);
|
||||||
} else {
|
} else {
|
||||||
this._doRoomSearch(query);
|
this.doRoomSearch(query);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error('Unknown pickerType', this.props.pickerType);
|
console.error('Unknown pickerType', this.props.pickerType);
|
||||||
|
@ -213,7 +237,7 @@ export default class AddressPickerDialog extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onDismissed = index => () => {
|
private onDismissed = (index: number) => () => {
|
||||||
const selectedList = this.state.selectedList.slice();
|
const selectedList = this.state.selectedList.slice();
|
||||||
selectedList.splice(index, 1);
|
selectedList.splice(index, 1);
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -221,25 +245,21 @@ export default class AddressPickerDialog extends React.Component {
|
||||||
suggestedList: [],
|
suggestedList: [],
|
||||||
query: "",
|
query: "",
|
||||||
});
|
});
|
||||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
if (this.cancelThreepidLookup) this.cancelThreepidLookup();
|
||||||
};
|
};
|
||||||
|
|
||||||
onClick = index => () => {
|
private onSelected = (index: number): void => {
|
||||||
this.onSelected(index);
|
|
||||||
};
|
|
||||||
|
|
||||||
onSelected = index => {
|
|
||||||
const selectedList = this.state.selectedList.slice();
|
const selectedList = this.state.selectedList.slice();
|
||||||
selectedList.push(this._getFilteredSuggestions()[index]);
|
selectedList.push(this.getFilteredSuggestions()[index]);
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedList,
|
selectedList,
|
||||||
suggestedList: [],
|
suggestedList: [],
|
||||||
query: "",
|
query: "",
|
||||||
});
|
});
|
||||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
if (this.cancelThreepidLookup) this.cancelThreepidLookup();
|
||||||
};
|
};
|
||||||
|
|
||||||
_doNaiveGroupSearch(query) {
|
private doNaiveGroupSearch(query: string): void {
|
||||||
const lowerCaseQuery = query.toLowerCase();
|
const lowerCaseQuery = query.toLowerCase();
|
||||||
this.setState({
|
this.setState({
|
||||||
busy: true,
|
busy: true,
|
||||||
|
@ -260,7 +280,7 @@ export default class AddressPickerDialog extends React.Component {
|
||||||
display_name: u.displayname,
|
display_name: u.displayname,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
this._processResults(results, query);
|
this.processResults(results, query);
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.error('Error whilst searching group rooms: ', err);
|
console.error('Error whilst searching group rooms: ', err);
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -273,7 +293,7 @@ export default class AddressPickerDialog extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_doNaiveGroupRoomSearch(query) {
|
private doNaiveGroupRoomSearch(query: string): void {
|
||||||
const lowerCaseQuery = query.toLowerCase();
|
const lowerCaseQuery = query.toLowerCase();
|
||||||
const results = [];
|
const results = [];
|
||||||
GroupStore.getGroupRooms(this.props.groupId).forEach((r) => {
|
GroupStore.getGroupRooms(this.props.groupId).forEach((r) => {
|
||||||
|
@ -289,13 +309,13 @@ export default class AddressPickerDialog extends React.Component {
|
||||||
name: r.name || r.canonical_alias,
|
name: r.name || r.canonical_alias,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
this._processResults(results, query);
|
this.processResults(results, query);
|
||||||
this.setState({
|
this.setState({
|
||||||
busy: false,
|
busy: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_doRoomSearch(query) {
|
private doRoomSearch(query: string): void {
|
||||||
const lowerCaseQuery = query.toLowerCase();
|
const lowerCaseQuery = query.toLowerCase();
|
||||||
const rooms = MatrixClientPeg.get().getRooms();
|
const rooms = MatrixClientPeg.get().getRooms();
|
||||||
const results = [];
|
const results = [];
|
||||||
|
@ -346,13 +366,13 @@ export default class AddressPickerDialog extends React.Component {
|
||||||
return a.rank - b.rank;
|
return a.rank - b.rank;
|
||||||
});
|
});
|
||||||
|
|
||||||
this._processResults(sortedResults, query);
|
this.processResults(sortedResults, query);
|
||||||
this.setState({
|
this.setState({
|
||||||
busy: false,
|
busy: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_doUserDirectorySearch(query) {
|
private doUserDirectorySearch(query: string): void {
|
||||||
this.setState({
|
this.setState({
|
||||||
busy: true,
|
busy: true,
|
||||||
query,
|
query,
|
||||||
|
@ -366,7 +386,7 @@ export default class AddressPickerDialog extends React.Component {
|
||||||
if (this.state.query !== query) {
|
if (this.state.query !== query) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._processResults(resp.results, query);
|
this.processResults(resp.results, query);
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.error('Error whilst searching user directory: ', err);
|
console.error('Error whilst searching user directory: ', err);
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -377,7 +397,7 @@ export default class AddressPickerDialog extends React.Component {
|
||||||
serverSupportsUserDirectory: false,
|
serverSupportsUserDirectory: false,
|
||||||
});
|
});
|
||||||
// Do a local search immediately
|
// Do a local search immediately
|
||||||
this._doLocalSearch(query);
|
this.doLocalSearch(query);
|
||||||
}
|
}
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -386,7 +406,7 @@ export default class AddressPickerDialog extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_doLocalSearch(query) {
|
private doLocalSearch(query: string): void {
|
||||||
this.setState({
|
this.setState({
|
||||||
query,
|
query,
|
||||||
searchError: null,
|
searchError: null,
|
||||||
|
@ -407,10 +427,10 @@ export default class AddressPickerDialog extends React.Component {
|
||||||
avatar_url: user.avatarUrl,
|
avatar_url: user.avatarUrl,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
this._processResults(results, query);
|
this.processResults(results, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
_processResults(results, query) {
|
private processResults(results: IResult[], query: string): void {
|
||||||
const suggestedList = [];
|
const suggestedList = [];
|
||||||
results.forEach((result) => {
|
results.forEach((result) => {
|
||||||
if (result.room_id) {
|
if (result.room_id) {
|
||||||
|
@ -465,27 +485,27 @@ export default class AddressPickerDialog extends React.Component {
|
||||||
address: query,
|
address: query,
|
||||||
isKnown: false,
|
isKnown: false,
|
||||||
});
|
});
|
||||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
if (this.cancelThreepidLookup) this.cancelThreepidLookup();
|
||||||
if (addrType === 'email') {
|
if (addrType === 'email') {
|
||||||
this._lookupThreepid(addrType, query);
|
this.lookupThreepid(addrType, query);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
suggestedList,
|
suggestedList,
|
||||||
invalidAddressError: false,
|
invalidAddressError: false,
|
||||||
}, () => {
|
}, () => {
|
||||||
if (this.addressSelector) this.addressSelector.moveSelectionTop();
|
if (this.addressSelector.current) this.addressSelector.current.moveSelectionTop();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_addAddressesToList(addressTexts) {
|
private addAddressesToList(addressTexts: string[]): IUserAddress[] {
|
||||||
const selectedList = this.state.selectedList.slice();
|
const selectedList = this.state.selectedList.slice();
|
||||||
|
|
||||||
let hasError = false;
|
let hasError = false;
|
||||||
addressTexts.forEach((addressText) => {
|
addressTexts.forEach((addressText) => {
|
||||||
addressText = addressText.trim();
|
addressText = addressText.trim();
|
||||||
const addrType = getAddressType(addressText);
|
const addrType = getAddressType(addressText);
|
||||||
const addrObj = {
|
const addrObj: IUserAddress = {
|
||||||
addressType: addrType,
|
addressType: addrType,
|
||||||
address: addressText,
|
address: addressText,
|
||||||
isKnown: false,
|
isKnown: false,
|
||||||
|
@ -504,7 +524,6 @@ export default class AddressPickerDialog extends React.Component {
|
||||||
const room = MatrixClientPeg.get().getRoom(addrObj.address);
|
const room = MatrixClientPeg.get().getRoom(addrObj.address);
|
||||||
if (room) {
|
if (room) {
|
||||||
addrObj.displayName = room.name;
|
addrObj.displayName = room.name;
|
||||||
addrObj.avatarMxc = room.avatarUrl;
|
|
||||||
addrObj.isKnown = true;
|
addrObj.isKnown = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -518,17 +537,17 @@ export default class AddressPickerDialog extends React.Component {
|
||||||
query: "",
|
query: "",
|
||||||
invalidAddressError: hasError ? true : this.state.invalidAddressError,
|
invalidAddressError: hasError ? true : this.state.invalidAddressError,
|
||||||
});
|
});
|
||||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
if (this.cancelThreepidLookup) this.cancelThreepidLookup();
|
||||||
return hasError ? null : selectedList;
|
return hasError ? null : selectedList;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _lookupThreepid(medium, address) {
|
private async lookupThreepid(medium: AddressType, address: string): Promise<string> {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
// Note that we can't safely remove this after we're done
|
// Note that we can't safely remove this after we're done
|
||||||
// because we don't know that it's the same one, so we just
|
// because we don't know that it's the same one, so we just
|
||||||
// leave it: it's replacing the old one each time so it's
|
// leave it: it's replacing the old one each time so it's
|
||||||
// not like they leak.
|
// not like they leak.
|
||||||
this._cancelThreepidLookup = function() {
|
this.cancelThreepidLookup = function() {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -570,7 +589,7 @@ export default class AddressPickerDialog extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_getFilteredSuggestions() {
|
private getFilteredSuggestions(): IUserAddress[] {
|
||||||
// map addressType => set of addresses to avoid O(n*m) operation
|
// map addressType => set of addresses to avoid O(n*m) operation
|
||||||
const selectedAddresses = {};
|
const selectedAddresses = {};
|
||||||
this.state.selectedList.forEach(({ address, addressType }) => {
|
this.state.selectedList.forEach(({ address, addressType }) => {
|
||||||
|
@ -584,15 +603,15 @@ export default class AddressPickerDialog extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_onPaste = e => {
|
private onPaste = (e: React.ClipboardEvent): void => {
|
||||||
// Prevent the text being pasted into the textarea
|
// Prevent the text being pasted into the textarea
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const text = e.clipboardData.getData("text");
|
const text = e.clipboardData.getData("text");
|
||||||
// Process it as a list of addresses to add instead
|
// Process it as a list of addresses to add instead
|
||||||
this._addAddressesToList(text.split(/[\s,]+/));
|
this.addAddressesToList(text.split(/[\s,]+/));
|
||||||
};
|
};
|
||||||
|
|
||||||
onUseDefaultIdentityServerClick = e => {
|
private onUseDefaultIdentityServerClick = (e: React.MouseEvent): void => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
// Update the IS in account data. Actually using it may trigger terms.
|
// Update the IS in account data. Actually using it may trigger terms.
|
||||||
|
@ -601,22 +620,17 @@ export default class AddressPickerDialog extends React.Component {
|
||||||
|
|
||||||
// Add email as a valid address type.
|
// Add email as a valid address type.
|
||||||
const { validAddressTypes } = this.state;
|
const { validAddressTypes } = this.state;
|
||||||
validAddressTypes.push('email');
|
validAddressTypes.push(AddressType.Email);
|
||||||
this.setState({ validAddressTypes });
|
this.setState({ validAddressTypes });
|
||||||
};
|
};
|
||||||
|
|
||||||
onManageSettingsClick = e => {
|
private onManageSettingsClick = (e: React.MouseEvent): void => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dis.fire(Action.ViewUserSettings);
|
dis.fire(Action.ViewUserSettings);
|
||||||
this.onCancel();
|
this.onCancel();
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
|
||||||
const AddressSelector = sdk.getComponent("elements.AddressSelector");
|
|
||||||
this.scrollElement = null;
|
|
||||||
|
|
||||||
let inputLabel;
|
let inputLabel;
|
||||||
if (this.props.description) {
|
if (this.props.description) {
|
||||||
inputLabel = <div className="mx_AddressPickerDialog_label">
|
inputLabel = <div className="mx_AddressPickerDialog_label">
|
||||||
|
@ -627,7 +641,6 @@ export default class AddressPickerDialog extends React.Component {
|
||||||
const query = [];
|
const query = [];
|
||||||
// create the invite list
|
// create the invite list
|
||||||
if (this.state.selectedList.length > 0) {
|
if (this.state.selectedList.length > 0) {
|
||||||
const AddressTile = sdk.getComponent("elements.AddressTile");
|
|
||||||
for (let i = 0; i < this.state.selectedList.length; i++) {
|
for (let i = 0; i < this.state.selectedList.length; i++) {
|
||||||
query.push(
|
query.push(
|
||||||
<AddressTile
|
<AddressTile
|
||||||
|
@ -644,19 +657,19 @@ export default class AddressPickerDialog extends React.Component {
|
||||||
query.push(
|
query.push(
|
||||||
<textarea
|
<textarea
|
||||||
key={this.state.selectedList.length}
|
key={this.state.selectedList.length}
|
||||||
onPaste={this._onPaste}
|
onPaste={this.onPaste}
|
||||||
rows="1"
|
rows={1}
|
||||||
id="textinput"
|
id="textinput"
|
||||||
ref={this._textinput}
|
ref={this.textinput}
|
||||||
className="mx_AddressPickerDialog_input"
|
className="mx_AddressPickerDialog_input"
|
||||||
onChange={this.onQueryChanged}
|
onChange={this.onQueryChanged}
|
||||||
placeholder={this.getPlaceholder()}
|
placeholder={this.getPlaceholder()}
|
||||||
defaultValue={this.props.value}
|
defaultValue={this.props.value}
|
||||||
autoFocus={this.props.focus}>
|
autoFocus={this.props.focus}
|
||||||
</textarea>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredSuggestedList = this._getFilteredSuggestions();
|
const filteredSuggestedList = this.getFilteredSuggestions();
|
||||||
|
|
||||||
let error;
|
let error;
|
||||||
let addressSelector;
|
let addressSelector;
|
||||||
|
@ -675,7 +688,7 @@ export default class AddressPickerDialog extends React.Component {
|
||||||
error = <div className="mx_AddressPickerDialog_error">{ _t("No results") }</div>;
|
error = <div className="mx_AddressPickerDialog_error">{ _t("No results") }</div>;
|
||||||
} else {
|
} else {
|
||||||
addressSelector = (
|
addressSelector = (
|
||||||
<AddressSelector ref={(ref) => {this.addressSelector = ref;}}
|
<AddressSelector ref={this.addressSelector}
|
||||||
addressList={filteredSuggestedList}
|
addressList={filteredSuggestedList}
|
||||||
showAddress={this.props.pickerType === 'user'}
|
showAddress={this.props.pickerType === 'user'}
|
||||||
onSelected={this.onSelected}
|
onSelected={this.onSelected}
|
||||||
|
@ -686,8 +699,8 @@ export default class AddressPickerDialog extends React.Component {
|
||||||
|
|
||||||
let identityServer;
|
let identityServer;
|
||||||
// If picker cannot currently accept e-mail but should be able to
|
// If picker cannot currently accept e-mail but should be able to
|
||||||
if (this.props.pickerType === 'user' && !this.state.validAddressTypes.includes('email')
|
if (this.props.pickerType === 'user' && !this.state.validAddressTypes.includes(AddressType.Email)
|
||||||
&& this.props.validAddressTypes.includes('email')) {
|
&& this.props.validAddressTypes.includes(AddressType.Email)) {
|
||||||
const defaultIdentityServerUrl = getDefaultIdentityServerUrl();
|
const defaultIdentityServerUrl = getDefaultIdentityServerUrl();
|
||||||
if (defaultIdentityServerUrl) {
|
if (defaultIdentityServerUrl) {
|
||||||
identityServer = <div className="mx_AddressPickerDialog_identityServer">{ _t(
|
identityServer = <div className="mx_AddressPickerDialog_identityServer">{ _t(
|
||||||
|
@ -714,8 +727,12 @@ export default class AddressPickerDialog extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_AddressPickerDialog" onKeyDown={this.onKeyDown}
|
<BaseDialog
|
||||||
onFinished={this.props.onFinished} title={this.props.title}>
|
className="mx_AddressPickerDialog"
|
||||||
|
onKeyDown={this.onKeyDown}
|
||||||
|
onFinished={this.props.onFinished}
|
||||||
|
title={this.props.title}
|
||||||
|
>
|
||||||
{ inputLabel }
|
{ inputLabel }
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<div className="mx_AddressPickerDialog_inputContainer">{ query }</div>
|
<div className="mx_AddressPickerDialog_inputContainer">{ query }</div>
|
|
@ -118,9 +118,7 @@ export default class BaseDialog extends React.Component {
|
||||||
|
|
||||||
let headerImage;
|
let headerImage;
|
||||||
if (this.props.headerImage) {
|
if (this.props.headerImage) {
|
||||||
headerImage = <img className="mx_Dialog_titleImage" src={this.props.headerImage}
|
headerImage = <img className="mx_Dialog_titleImage" src={this.props.headerImage} alt="" />;
|
||||||
alt=""
|
|
||||||
/>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -71,13 +71,16 @@ const BetaFeedbackDialog: React.FC<IProps> = ({ featureId, onFinished }) => {
|
||||||
|
|
||||||
{ _t("Your platform and username will be noted to help us use your feedback as much as we can.") }
|
{ _t("Your platform and username will be noted to help us use your feedback as much as we can.") }
|
||||||
|
|
||||||
<AccessibleButton kind="link" onClick={() => {
|
<AccessibleButton
|
||||||
onFinished(false);
|
kind="link"
|
||||||
defaultDispatcher.dispatch({
|
onClick={() => {
|
||||||
|
onFinished(false);
|
||||||
|
defaultDispatcher.dispatch({
|
||||||
action: Action.ViewUserSettings,
|
action: Action.ViewUserSettings,
|
||||||
initialTabId: UserTab.Labs,
|
initialTabId: UserTab.Labs,
|
||||||
});
|
});
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
{ _t("To leave the beta, visit your settings.") }
|
{ _t("To leave the beta, visit your settings.") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -188,7 +188,9 @@ export default class BugReportDialog extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_BugReportDialog" onFinished={this.onCancel}
|
<BaseDialog
|
||||||
|
className="mx_BugReportDialog"
|
||||||
|
onFinished={this.onCancel}
|
||||||
title={_t('Submit debug logs')}
|
title={_t('Submit debug logs')}
|
||||||
contentId='mx_Dialog_content'
|
contentId='mx_Dialog_content'
|
||||||
>
|
>
|
||||||
|
|
|
@ -205,9 +205,12 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent<
|
||||||
people.push((
|
people.push((
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
onClick={this.onShowMorePeople}
|
onClick={this.onShowMorePeople}
|
||||||
kind="link" key="more"
|
kind="link"
|
||||||
|
key="more"
|
||||||
className="mx_CommunityPrototypeInviteDialog_morePeople"
|
className="mx_CommunityPrototypeInviteDialog_morePeople"
|
||||||
>{ _t("Show more") }</AccessibleButton>
|
>
|
||||||
|
{ _t("Show more") }
|
||||||
|
</AccessibleButton>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -240,10 +243,13 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent<
|
||||||
{ peopleIntro }
|
{ peopleIntro }
|
||||||
{ people }
|
{ people }
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
kind="primary" onClick={this.onSubmit}
|
kind="primary"
|
||||||
|
onClick={this.onSubmit}
|
||||||
disabled={this.state.busy}
|
disabled={this.state.busy}
|
||||||
className="mx_CommunityPrototypeInviteDialog_primaryButton"
|
className="mx_CommunityPrototypeInviteDialog_primaryButton"
|
||||||
>{ buttonText }</AccessibleButton>
|
>
|
||||||
|
{ buttonText }
|
||||||
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|
|
@ -37,8 +37,8 @@ export default class ConfirmRedactDialog extends React.Component<IProps> {
|
||||||
"Note that if you delete a room name or topic change, it could undo the change.")}
|
"Note that if you delete a room name or topic change, it could undo the change.")}
|
||||||
placeholder={_t("Reason (optional)")}
|
placeholder={_t("Reason (optional)")}
|
||||||
focus
|
focus
|
||||||
button={_t("Remove")}>
|
button={_t("Remove")}
|
||||||
</TextInputDialog>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -104,7 +104,9 @@ export default class ConfirmUserActionDialog extends React.Component<IProps> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
|
<BaseDialog
|
||||||
|
className="mx_ConfirmUserActionDialog"
|
||||||
|
onFinished={this.props.onFinished}
|
||||||
title={this.props.title}
|
title={this.props.title}
|
||||||
contentId='mx_Dialog_content'
|
contentId='mx_Dialog_content'
|
||||||
>
|
>
|
||||||
|
|
|
@ -204,8 +204,10 @@ export default class CreateCommunityPrototypeDialog extends React.PureComponent<
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_CreateCommunityPrototypeDialog_colAvatar">
|
<div className="mx_CreateCommunityPrototypeDialog_colAvatar">
|
||||||
<input
|
<input
|
||||||
type="file" style={{ display: "none" }}
|
type="file"
|
||||||
ref={this.avatarUploadRef} accept="image/*"
|
style={{ display: "none" }}
|
||||||
|
ref={this.avatarUploadRef}
|
||||||
|
accept="image/*"
|
||||||
onChange={this.onAvatarChanged}
|
onChange={this.onAvatarChanged}
|
||||||
/>
|
/>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
|
|
|
@ -123,7 +123,9 @@ export default class CreateGroupDialog extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_CreateGroupDialog" onFinished={this.props.onFinished}
|
<BaseDialog
|
||||||
|
className="mx_CreateGroupDialog"
|
||||||
|
onFinished={this.props.onFinished}
|
||||||
title={_t('Create Community')}
|
title={_t('Create Community')}
|
||||||
>
|
>
|
||||||
<form onSubmit={this.onFormSubmit}>
|
<form onSubmit={this.onFormSubmit}>
|
||||||
|
@ -133,8 +135,11 @@ export default class CreateGroupDialog extends React.Component<IProps, IState> {
|
||||||
<label htmlFor="groupname">{ _t('Community Name') }</label>
|
<label htmlFor="groupname">{ _t('Community Name') }</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input id="groupname" className="mx_CreateGroupDialog_input"
|
<input
|
||||||
autoFocus={true} size={64}
|
id="groupname"
|
||||||
|
className="mx_CreateGroupDialog_input"
|
||||||
|
autoFocus={true}
|
||||||
|
size={64}
|
||||||
placeholder={_t('Example')}
|
placeholder={_t('Example')}
|
||||||
onChange={this.onGroupNameChange}
|
onChange={this.onGroupNameChange}
|
||||||
value={this.state.groupName}
|
value={this.state.groupName}
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
|
|
||||||
import React, { ChangeEvent, createRef, KeyboardEvent, SyntheticEvent } from "react";
|
import React, { ChangeEvent, createRef, KeyboardEvent, SyntheticEvent } from "react";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { JoinRule, Preset, Visibility } from "matrix-js-sdk/src/@types/partials";
|
||||||
|
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
import withValidation, { IFieldState } from '../elements/Validation';
|
import withValidation, { IFieldState } from '../elements/Validation';
|
||||||
|
@ -31,7 +32,8 @@ import RoomAliasField from "../elements/RoomAliasField";
|
||||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||||
import DialogButtons from "../elements/DialogButtons";
|
import DialogButtons from "../elements/DialogButtons";
|
||||||
import BaseDialog from "../dialogs/BaseDialog";
|
import BaseDialog from "../dialogs/BaseDialog";
|
||||||
import { Preset, Visibility } from "matrix-js-sdk/src/@types/partials";
|
import Dropdown from "../elements/Dropdown";
|
||||||
|
import SpaceStore from "../../../stores/SpaceStore";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
defaultPublic?: boolean;
|
defaultPublic?: boolean;
|
||||||
|
@ -41,7 +43,7 @@ interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
isPublic: boolean;
|
joinRule: JoinRule;
|
||||||
isEncrypted: boolean;
|
isEncrypted: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
topic: string;
|
topic: string;
|
||||||
|
@ -54,15 +56,25 @@ interface IState {
|
||||||
|
|
||||||
@replaceableComponent("views.dialogs.CreateRoomDialog")
|
@replaceableComponent("views.dialogs.CreateRoomDialog")
|
||||||
export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||||
|
private readonly supportsRestricted: boolean;
|
||||||
private nameField = createRef<Field>();
|
private nameField = createRef<Field>();
|
||||||
private aliasField = createRef<RoomAliasField>();
|
private aliasField = createRef<RoomAliasField>();
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
this.supportsRestricted = this.props.parentSpace && !!SpaceStore.instance.restrictedJoinRuleSupport?.preferred;
|
||||||
|
|
||||||
|
let joinRule = JoinRule.Invite;
|
||||||
|
if (this.props.defaultPublic) {
|
||||||
|
joinRule = JoinRule.Public;
|
||||||
|
} else if (this.supportsRestricted) {
|
||||||
|
joinRule = JoinRule.Restricted;
|
||||||
|
}
|
||||||
|
|
||||||
const config = SdkConfig.get();
|
const config = SdkConfig.get();
|
||||||
this.state = {
|
this.state = {
|
||||||
isPublic: this.props.defaultPublic || false,
|
joinRule,
|
||||||
isEncrypted: privateShouldBeEncrypted(),
|
isEncrypted: privateShouldBeEncrypted(),
|
||||||
name: this.props.defaultName || "",
|
name: this.props.defaultName || "",
|
||||||
topic: "",
|
topic: "",
|
||||||
|
@ -81,13 +93,18 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||||
const opts: IOpts = {};
|
const opts: IOpts = {};
|
||||||
const createOpts: IOpts["createOpts"] = opts.createOpts = {};
|
const createOpts: IOpts["createOpts"] = opts.createOpts = {};
|
||||||
createOpts.name = this.state.name;
|
createOpts.name = this.state.name;
|
||||||
if (this.state.isPublic) {
|
|
||||||
|
if (this.state.joinRule === JoinRule.Public) {
|
||||||
createOpts.visibility = Visibility.Public;
|
createOpts.visibility = Visibility.Public;
|
||||||
createOpts.preset = Preset.PublicChat;
|
createOpts.preset = Preset.PublicChat;
|
||||||
opts.guestAccess = false;
|
opts.guestAccess = false;
|
||||||
const { alias } = this.state;
|
const { alias } = this.state;
|
||||||
createOpts.room_alias_name = alias.substr(1, alias.indexOf(":") - 1);
|
createOpts.room_alias_name = alias.substr(1, alias.indexOf(":") - 1);
|
||||||
|
} else {
|
||||||
|
// If we cannot change encryption we pass `true` for safety, the server should automatically do this for us.
|
||||||
|
opts.encryption = this.state.canChangeEncryption ? this.state.isEncrypted : true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.topic) {
|
if (this.state.topic) {
|
||||||
createOpts.topic = this.state.topic;
|
createOpts.topic = this.state.topic;
|
||||||
}
|
}
|
||||||
|
@ -95,22 +112,13 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||||
createOpts.creation_content = { 'm.federate': false };
|
createOpts.creation_content = { 'm.federate': false };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.state.isPublic) {
|
|
||||||
if (this.state.canChangeEncryption) {
|
|
||||||
opts.encryption = this.state.isEncrypted;
|
|
||||||
} else {
|
|
||||||
// the server should automatically do this for us, but for safety
|
|
||||||
// we'll demand it too.
|
|
||||||
opts.encryption = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
|
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
|
||||||
opts.associatedWithCommunity = CommunityPrototypeStore.instance.getSelectedCommunityId();
|
opts.associatedWithCommunity = CommunityPrototypeStore.instance.getSelectedCommunityId();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.parentSpace) {
|
if (this.props.parentSpace && this.state.joinRule === JoinRule.Restricted) {
|
||||||
opts.parentSpace = this.props.parentSpace;
|
opts.parentSpace = this.props.parentSpace;
|
||||||
|
opts.joinRule = JoinRule.Restricted;
|
||||||
}
|
}
|
||||||
|
|
||||||
return opts;
|
return opts;
|
||||||
|
@ -172,8 +180,8 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||||
this.setState({ topic: ev.target.value });
|
this.setState({ topic: ev.target.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
private onPublicChange = (isPublic: boolean) => {
|
private onJoinRuleChange = (joinRule: JoinRule) => {
|
||||||
this.setState({ isPublic });
|
this.setState({ joinRule });
|
||||||
};
|
};
|
||||||
|
|
||||||
private onEncryptedChange = (isEncrypted: boolean) => {
|
private onEncryptedChange = (isEncrypted: boolean) => {
|
||||||
|
@ -210,7 +218,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let aliasField;
|
let aliasField;
|
||||||
if (this.state.isPublic) {
|
if (this.state.joinRule === JoinRule.Public) {
|
||||||
const domain = MatrixClientPeg.get().getDomain();
|
const domain = MatrixClientPeg.get().getDomain();
|
||||||
aliasField = (
|
aliasField = (
|
||||||
<div className="mx_CreateRoomDialog_aliasContainer">
|
<div className="mx_CreateRoomDialog_aliasContainer">
|
||||||
|
@ -224,19 +232,46 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let publicPrivateLabel = <p>{ _t(
|
let publicPrivateLabel: JSX.Element;
|
||||||
"Private rooms can be found and joined by invitation only. Public rooms can be " +
|
|
||||||
"found and joined by anyone.",
|
|
||||||
) }</p>;
|
|
||||||
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
|
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
|
||||||
publicPrivateLabel = <p>{ _t(
|
publicPrivateLabel = <p>
|
||||||
"Private rooms can be found and joined by invitation only. Public rooms can be " +
|
{ _t(
|
||||||
"found and joined by anyone in this community.",
|
"Private rooms can be found and joined by invitation only. Public rooms can be " +
|
||||||
) }</p>;
|
"found and joined by anyone in this community.",
|
||||||
|
) }
|
||||||
|
</p>;
|
||||||
|
} else if (this.state.joinRule === JoinRule.Restricted) {
|
||||||
|
publicPrivateLabel = <p>
|
||||||
|
{ _t(
|
||||||
|
"Everyone in <SpaceName/> will be able to find and join this room.", {}, {
|
||||||
|
SpaceName: () => <b>{ this.props.parentSpace.name }</b>,
|
||||||
|
},
|
||||||
|
) }
|
||||||
|
|
||||||
|
{ _t("You can change this at any time from room settings.") }
|
||||||
|
</p>;
|
||||||
|
} else if (this.state.joinRule === JoinRule.Public) {
|
||||||
|
publicPrivateLabel = <p>
|
||||||
|
{ _t(
|
||||||
|
"Anyone will be able to find and join this room, not just members of <SpaceName/>.", {}, {
|
||||||
|
SpaceName: () => <b>{ this.props.parentSpace.name }</b>,
|
||||||
|
},
|
||||||
|
) }
|
||||||
|
|
||||||
|
{ _t("You can change this at any time from room settings.") }
|
||||||
|
</p>;
|
||||||
|
} else if (this.state.joinRule === JoinRule.Invite) {
|
||||||
|
publicPrivateLabel = <p>
|
||||||
|
{ _t(
|
||||||
|
"Only people invited will be able to find and join this room.",
|
||||||
|
) }
|
||||||
|
|
||||||
|
{ _t("You can change this at any time from room settings.") }
|
||||||
|
</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let e2eeSection;
|
let e2eeSection;
|
||||||
if (!this.state.isPublic) {
|
if (this.state.joinRule !== JoinRule.Public) {
|
||||||
let microcopy;
|
let microcopy;
|
||||||
if (privateShouldBeEncrypted()) {
|
if (privateShouldBeEncrypted()) {
|
||||||
if (this.state.canChangeEncryption) {
|
if (this.state.canChangeEncryption) {
|
||||||
|
@ -273,15 +308,31 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let title = this.state.isPublic ? _t('Create a public room') : _t('Create a private room');
|
let title = _t("Create a room");
|
||||||
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
|
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
|
||||||
const name = CommunityPrototypeStore.instance.getSelectedCommunityName();
|
const name = CommunityPrototypeStore.instance.getSelectedCommunityName();
|
||||||
title = _t("Create a room in %(communityName)s", { communityName: name });
|
title = _t("Create a room in %(communityName)s", { communityName: name });
|
||||||
|
} else if (!this.props.parentSpace) {
|
||||||
|
title = this.state.joinRule === JoinRule.Public ? _t('Create a public room') : _t('Create a private room');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
<div key={JoinRule.Invite} className="mx_CreateRoomDialog_dropdown_invite">
|
||||||
|
{ _t("Private room (invite only)") }
|
||||||
|
</div>,
|
||||||
|
<div key={JoinRule.Public} className="mx_CreateRoomDialog_dropdown_public">
|
||||||
|
{ _t("Public room") }
|
||||||
|
</div>,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (this.supportsRestricted) {
|
||||||
|
options.unshift(<div key={JoinRule.Restricted} className="mx_CreateRoomDialog_dropdown_restricted">
|
||||||
|
{ _t("Visible to space members") }
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished}
|
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished} title={title}>
|
||||||
title={title}
|
|
||||||
>
|
|
||||||
<form onSubmit={this.onOk} onKeyDown={this.onKeyDown}>
|
<form onSubmit={this.onOk} onKeyDown={this.onKeyDown}>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<Field
|
<Field
|
||||||
|
@ -298,11 +349,18 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||||
value={this.state.topic}
|
value={this.state.topic}
|
||||||
className="mx_CreateRoomDialog_topic"
|
className="mx_CreateRoomDialog_topic"
|
||||||
/>
|
/>
|
||||||
<LabelledToggleSwitch
|
|
||||||
label={_t("Make this room public")}
|
<Dropdown
|
||||||
onChange={this.onPublicChange}
|
id="mx_CreateRoomDialog_typeDropdown"
|
||||||
value={this.state.isPublic}
|
className="mx_CreateRoomDialog_typeDropdown"
|
||||||
/>
|
onOptionChange={this.onJoinRuleChange}
|
||||||
|
menuWidth={448}
|
||||||
|
value={this.state.joinRule}
|
||||||
|
label={_t("Room visibility")}
|
||||||
|
>
|
||||||
|
{ options }
|
||||||
|
</Dropdown>
|
||||||
|
|
||||||
{ publicPrivateLabel }
|
{ publicPrivateLabel }
|
||||||
{ e2eeSection }
|
{ e2eeSection }
|
||||||
{ aliasField }
|
{ aliasField }
|
||||||
|
|
|
@ -72,7 +72,7 @@ const CryptoStoreTooNewDialog: React.FC<IProps> = (props: IProps) => {
|
||||||
hasCancel={false}
|
hasCancel={false}
|
||||||
onPrimaryButtonClick={props.onFinished}
|
onPrimaryButtonClick={props.onFinished}
|
||||||
>
|
>
|
||||||
<button onClick={_onLogoutClicked} >
|
<button onClick={_onLogoutClicked}>
|
||||||
{ _t('Sign out') }
|
{ _t('Sign out') }
|
||||||
</button>
|
</button>
|
||||||
</DialogButtons>
|
</DialogButtons>
|
||||||
|
|
|
@ -182,14 +182,23 @@ export class SendCustomEvent extends GenericEditor<ISendCustomEventProps, ISendC
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<Field id="evContent" label={_t("Event Content")} type="text" className="mx_DevTools_textarea"
|
<Field
|
||||||
autoComplete="off" value={this.state.evContent} onChange={this.onChange} element="textarea" />
|
id="evContent"
|
||||||
|
label={_t("Event Content")}
|
||||||
|
type="text"
|
||||||
|
className="mx_DevTools_textarea"
|
||||||
|
autoComplete="off"
|
||||||
|
value={this.state.evContent}
|
||||||
|
onChange={this.onChange}
|
||||||
|
element="textarea" />
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button onClick={this.onBack}>{ _t('Back') }</button>
|
<button onClick={this.onBack}>{ _t('Back') }</button>
|
||||||
{ !this.state.message && <button onClick={this.send}>{ _t('Send') }</button> }
|
{ !this.state.message && <button onClick={this.send}>{ _t('Send') }</button> }
|
||||||
{ showTglFlip && <div style={{ float: "right" }}>
|
{ showTglFlip && <div style={{ float: "right" }}>
|
||||||
<input id="isStateEvent" className="mx_DevTools_tgl mx_DevTools_tgl-flip"
|
<input
|
||||||
|
id="isStateEvent"
|
||||||
|
className="mx_DevTools_tgl mx_DevTools_tgl-flip"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={this.state.isStateEvent}
|
checked={this.state.isStateEvent}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
|
@ -282,14 +291,24 @@ class SendAccountData extends GenericEditor<ISendAccountDataProps, ISendAccountD
|
||||||
{ this.textInput('eventType', _t('Event Type')) }
|
{ this.textInput('eventType', _t('Event Type')) }
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<Field id="evContent" label={_t("Event Content")} type="text" className="mx_DevTools_textarea"
|
<Field
|
||||||
autoComplete="off" value={this.state.evContent} onChange={this.onChange} element="textarea" />
|
id="evContent"
|
||||||
|
label={_t("Event Content")}
|
||||||
|
type="text"
|
||||||
|
className="mx_DevTools_textarea"
|
||||||
|
autoComplete="off"
|
||||||
|
value={this.state.evContent}
|
||||||
|
onChange={this.onChange}
|
||||||
|
element="textarea"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button onClick={this.onBack}>{ _t('Back') }</button>
|
<button onClick={this.onBack}>{ _t('Back') }</button>
|
||||||
{ !this.state.message && <button onClick={this.send}>{ _t('Send') }</button> }
|
{ !this.state.message && <button onClick={this.send}>{ _t('Send') }</button> }
|
||||||
{ !this.state.message && <div style={{ float: "right" }}>
|
{ !this.state.message && <div style={{ float: "right" }}>
|
||||||
<input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip"
|
<input
|
||||||
|
id="isRoomAccountData"
|
||||||
|
className="mx_DevTools_tgl mx_DevTools_tgl-flip"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={this.state.isRoomAccountData}
|
checked={this.state.isRoomAccountData}
|
||||||
disabled={this.props.forceMode}
|
disabled={this.props.forceMode}
|
||||||
|
@ -371,11 +390,18 @@ class FilteredList extends React.PureComponent<IFilteredListProps, IFilteredList
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <div>
|
return <div>
|
||||||
<Field label={_t('Filter results')} autoFocus={true} size={64}
|
<Field
|
||||||
type="text" autoComplete="off" value={this.props.query} onChange={this.onQuery}
|
label={_t('Filter results')}
|
||||||
|
autoFocus={true}
|
||||||
|
size={64}
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
value={this.props.query}
|
||||||
|
onChange={this.onQuery}
|
||||||
className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query"
|
className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query"
|
||||||
// force re-render so that autoFocus is applied when this component is re-used
|
// force re-render so that autoFocus is applied when this component is re-used
|
||||||
key={this.props.children[0] ? this.props.children[0].key : ''} />
|
key={this.props.children[0] ? this.props.children[0].key : ''}
|
||||||
|
/>
|
||||||
|
|
||||||
<TruncatedList getChildren={this.getChildren}
|
<TruncatedList getChildren={this.getChildren}
|
||||||
getChildCount={this.getChildCount}
|
getChildCount={this.getChildCount}
|
||||||
|
@ -459,11 +485,16 @@ class RoomStateExplorer extends React.PureComponent<IExplorerProps, IRoomStateEx
|
||||||
render() {
|
render() {
|
||||||
if (this.state.event) {
|
if (this.state.event) {
|
||||||
if (this.state.editing) {
|
if (this.state.editing) {
|
||||||
return <SendCustomEvent room={this.props.room} forceStateEvent={true} onBack={this.onBack} inputs={{
|
return <SendCustomEvent
|
||||||
|
room={this.props.room}
|
||||||
|
forceStateEvent={true}
|
||||||
|
onBack={this.onBack}
|
||||||
|
inputs={{
|
||||||
eventType: this.state.event.getType(),
|
eventType: this.state.event.getType(),
|
||||||
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
|
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
|
||||||
stateKey: this.state.event.getStateKey(),
|
stateKey: this.state.event.getStateKey(),
|
||||||
}} />;
|
}}
|
||||||
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className="mx_ViewSource">
|
return <div className="mx_ViewSource">
|
||||||
|
@ -594,7 +625,9 @@ class AccountDataExplorer extends React.PureComponent<IExplorerProps, IAccountDa
|
||||||
inputs={{
|
inputs={{
|
||||||
eventType: this.state.event.getType(),
|
eventType: this.state.event.getType(),
|
||||||
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
|
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
|
||||||
}} forceMode={true} />;
|
}}
|
||||||
|
forceMode={true}
|
||||||
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className="mx_ViewSource">
|
return <div className="mx_ViewSource">
|
||||||
|
@ -631,7 +664,9 @@ class AccountDataExplorer extends React.PureComponent<IExplorerProps, IAccountDa
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button onClick={this.onBack}>{ _t('Back') }</button>
|
<button onClick={this.onBack}>{ _t('Back') }</button>
|
||||||
<div style={{ float: "right" }}>
|
<div style={{ float: "right" }}>
|
||||||
<input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip"
|
<input
|
||||||
|
id="isRoomAccountData"
|
||||||
|
className="mx_DevTools_tgl mx_DevTools_tgl-flip"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={this.state.isRoomAccountData}
|
checked={this.state.isRoomAccountData}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
|
@ -1021,8 +1056,13 @@ class SettingsExplorer extends React.PureComponent<IExplorerProps, ISettingsExpl
|
||||||
<div>
|
<div>
|
||||||
<div className="mx_Dialog_content mx_DevTools_SettingsExplorer">
|
<div className="mx_Dialog_content mx_DevTools_SettingsExplorer">
|
||||||
<Field
|
<Field
|
||||||
label={_t('Filter results')} autoFocus={true} size={64}
|
label={_t('Filter results')}
|
||||||
type="text" autoComplete="off" value={this.state.query} onChange={this.onQueryChange}
|
autoFocus={true}
|
||||||
|
size={64}
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
value={this.state.query}
|
||||||
|
onChange={this.onQueryChange}
|
||||||
className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query"
|
className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query"
|
||||||
/>
|
/>
|
||||||
<table>
|
<table>
|
||||||
|
@ -1040,7 +1080,9 @@ class SettingsExplorer extends React.PureComponent<IExplorerProps, ISettingsExpl
|
||||||
<a href="" onClick={(e) => this.onViewClick(e, i)}>
|
<a href="" onClick={(e) => this.onViewClick(e, i)}>
|
||||||
<code>{ i }</code>
|
<code>{ i }</code>
|
||||||
</a>
|
</a>
|
||||||
<a href="" onClick={(e) => this.onEditClick(e, i)}
|
<a
|
||||||
|
href=""
|
||||||
|
onClick={(e) => this.onEditClick(e, i)}
|
||||||
className='mx_DevTools_SettingsExplorer_edit'
|
className='mx_DevTools_SettingsExplorer_edit'
|
||||||
>
|
>
|
||||||
✏
|
✏
|
||||||
|
@ -1104,18 +1146,26 @@ class SettingsExplorer extends React.PureComponent<IExplorerProps, ISettingsExpl
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Field
|
<Field
|
||||||
id="valExpl" label={_t("Values at explicit levels")} type="text"
|
id="valExpl"
|
||||||
className="mx_DevTools_textarea" element="textarea"
|
label={_t("Values at explicit levels")}
|
||||||
autoComplete="off" value={this.state.explicitValues}
|
type="text"
|
||||||
|
className="mx_DevTools_textarea"
|
||||||
|
element="textarea"
|
||||||
|
autoComplete="off"
|
||||||
|
value={this.state.explicitValues}
|
||||||
onChange={this.onExplValuesEdit}
|
onChange={this.onExplValuesEdit}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Field
|
<Field
|
||||||
id="valExpl" label={_t("Values at explicit levels in this room")} type="text"
|
id="valExpl"
|
||||||
className="mx_DevTools_textarea" element="textarea"
|
label={_t("Values at explicit levels in this room")}
|
||||||
autoComplete="off" value={this.state.explicitRoomValues}
|
type="text"
|
||||||
|
className="mx_DevTools_textarea"
|
||||||
|
element="textarea"
|
||||||
|
autoComplete="off"
|
||||||
|
value={this.state.explicitRoomValues}
|
||||||
onChange={this.onExplRoomValuesEdit}
|
onChange={this.onExplRoomValuesEdit}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -144,8 +144,10 @@ export default class EditCommunityPrototypeDialog extends React.PureComponent<IP
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_EditCommunityPrototypeDialog_rowAvatar">
|
<div className="mx_EditCommunityPrototypeDialog_rowAvatar">
|
||||||
<input
|
<input
|
||||||
type="file" style={{ display: "none" }}
|
type="file"
|
||||||
ref={this.avatarUploadRef} accept="image/*"
|
style={{ display: "none" }}
|
||||||
|
ref={this.avatarUploadRef}
|
||||||
|
accept="image/*"
|
||||||
onChange={this.onAvatarChanged}
|
onChange={this.onAvatarChanged}
|
||||||
/>
|
/>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
|
|
|
@ -106,12 +106,12 @@ const Entry: React.FC<IEntryProps> = ({ room, event, matrixClient: cli, onFinish
|
||||||
className = "mx_ForwardList_sending";
|
className = "mx_ForwardList_sending";
|
||||||
disabled = true;
|
disabled = true;
|
||||||
title = _t("Sending");
|
title = _t("Sending");
|
||||||
icon = <div className="mx_ForwardList_sendIcon" aria-label={title}></div>;
|
icon = <div className="mx_ForwardList_sendIcon" aria-label={title} />;
|
||||||
} else if (sendState === SendState.Sent) {
|
} else if (sendState === SendState.Sent) {
|
||||||
className = "mx_ForwardList_sent";
|
className = "mx_ForwardList_sent";
|
||||||
disabled = true;
|
disabled = true;
|
||||||
title = _t("Sent");
|
title = _t("Sent");
|
||||||
icon = <div className="mx_ForwardList_sendIcon" aria-label={title}></div>;
|
icon = <div className="mx_ForwardList_sendIcon" aria-label={title} />;
|
||||||
} else {
|
} else {
|
||||||
className = "mx_ForwardList_sendFailed";
|
className = "mx_ForwardList_sendFailed";
|
||||||
disabled = true;
|
disabled = true;
|
||||||
|
@ -204,10 +204,16 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
|
||||||
function overflowTile(overflowCount, totalCount) {
|
function overflowTile(overflowCount, totalCount) {
|
||||||
const text = _t("and %(count)s others...", { count: overflowCount });
|
const text = _t("and %(count)s others...", { count: overflowCount });
|
||||||
return (
|
return (
|
||||||
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
|
<EntityTile
|
||||||
<BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} />
|
className="mx_EntityTile_ellipsis"
|
||||||
} name={text} presenceState="online" suppressOnHover={true}
|
avatarJsx={
|
||||||
onClick={() => setTruncateAt(totalCount)} />
|
<BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} />
|
||||||
|
}
|
||||||
|
name={text}
|
||||||
|
presenceState="online"
|
||||||
|
suppressOnHover={true}
|
||||||
|
onClick={() => setTruncateAt(totalCount)}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -133,18 +133,23 @@ export default class IncomingSasDialog extends React.Component {
|
||||||
? mediaFromMxc(oppProfile.avatar_url).getSquareThumbnailHttp(48)
|
? mediaFromMxc(oppProfile.avatar_url).getSquareThumbnailHttp(48)
|
||||||
: null;
|
: null;
|
||||||
profile = <div className="mx_IncomingSasDialog_opponentProfile">
|
profile = <div className="mx_IncomingSasDialog_opponentProfile">
|
||||||
<BaseAvatar name={oppProfile.displayname}
|
<BaseAvatar
|
||||||
|
name={oppProfile.displayname}
|
||||||
idName={this.props.verifier.userId}
|
idName={this.props.verifier.userId}
|
||||||
url={url}
|
url={url}
|
||||||
width={48} height={48} resizeMethod='crop'
|
width={48}
|
||||||
|
height={48}
|
||||||
|
resizeMethod='crop'
|
||||||
/>
|
/>
|
||||||
<h2>{ oppProfile.displayname }</h2>
|
<h2>{ oppProfile.displayname }</h2>
|
||||||
</div>;
|
</div>;
|
||||||
} else if (this.state.opponentProfileError) {
|
} else if (this.state.opponentProfileError) {
|
||||||
profile = <div>
|
profile = <div>
|
||||||
<BaseAvatar name={this.props.verifier.userId.slice(1)}
|
<BaseAvatar
|
||||||
|
name={this.props.verifier.userId.slice(1)}
|
||||||
idName={this.props.verifier.userId}
|
idName={this.props.verifier.userId}
|
||||||
width={48} height={48}
|
width={48}
|
||||||
|
height={48}
|
||||||
/>
|
/>
|
||||||
<h2>{ this.props.verifier.userId }</h2>
|
<h2>{ this.props.verifier.userId }</h2>
|
||||||
</div>;
|
</div>;
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
|
||||||
Copyright 2017 New Vector Ltd.
|
|
||||||
Copyright 2019 Bastian Masanek, Noxware IT <matrix@noxware.de>
|
Copyright 2019 Bastian Masanek, Noxware IT <matrix@noxware.de>
|
||||||
|
Copyright 2015 - 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.
|
||||||
|
@ -16,31 +15,31 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { ReactNode, KeyboardEvent } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import * as sdk from '../../../index';
|
|
||||||
import { _t } from '../../../languageHandler';
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
export default class InfoDialog extends React.Component {
|
import { _t } from '../../../languageHandler';
|
||||||
static propTypes = {
|
import * as sdk from '../../../index';
|
||||||
className: PropTypes.string,
|
import { IDialogProps } from "./IDialogProps";
|
||||||
title: PropTypes.string,
|
|
||||||
description: PropTypes.node,
|
|
||||||
button: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
|
|
||||||
onFinished: PropTypes.func,
|
|
||||||
hasCloseButton: PropTypes.bool,
|
|
||||||
onKeyDown: PropTypes.func,
|
|
||||||
fixedWidth: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
interface IProps extends IDialogProps {
|
||||||
|
title?: string;
|
||||||
|
description?: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
button?: boolean | string;
|
||||||
|
hasCloseButton?: boolean;
|
||||||
|
fixedWidth?: boolean;
|
||||||
|
onKeyDown?(event: KeyboardEvent): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class InfoDialog extends React.Component<IProps> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
title: '',
|
title: '',
|
||||||
description: '',
|
description: '',
|
||||||
hasCloseButton: false,
|
hasCloseButton: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
onFinished = () => {
|
private onFinished = () => {
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -63,8 +62,7 @@ export default class InfoDialog extends React.Component {
|
||||||
{ this.props.button !== false && <DialogButtons primaryButton={this.props.button || _t('OK')}
|
{ this.props.button !== false && <DialogButtons primaryButton={this.props.button || _t('OK')}
|
||||||
onPrimaryButtonClick={this.onFinished}
|
onPrimaryButtonClick={this.onFinished}
|
||||||
hasCancel={false}
|
hasCancel={false}
|
||||||
>
|
/> }
|
||||||
</DialogButtons> }
|
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -196,7 +196,9 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
||||||
? <img
|
? <img
|
||||||
className='mx_InviteDialog_userTile_avatar mx_InviteDialog_userTile_threepidAvatar'
|
className='mx_InviteDialog_userTile_avatar mx_InviteDialog_userTile_threepidAvatar'
|
||||||
src={require("../../../../res/img/icon-email-pill-avatar.svg")}
|
src={require("../../../../res/img/icon-email-pill-avatar.svg")}
|
||||||
width={avatarSize} height={avatarSize} />
|
width={avatarSize}
|
||||||
|
height={avatarSize}
|
||||||
|
/>
|
||||||
: <BaseAvatar
|
: <BaseAvatar
|
||||||
className='mx_InviteDialog_userTile_avatar'
|
className='mx_InviteDialog_userTile_avatar'
|
||||||
url={this.props.member.getMxcAvatarUrl()
|
url={this.props.member.getMxcAvatarUrl()
|
||||||
|
@ -214,8 +216,11 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
||||||
className='mx_InviteDialog_userTile_remove'
|
className='mx_InviteDialog_userTile_remove'
|
||||||
onClick={this.onRemove}
|
onClick={this.onRemove}
|
||||||
>
|
>
|
||||||
<img src={require("../../../../res/img/icon-pill-remove.svg")}
|
<img
|
||||||
alt={_t('Remove')} width={8} height={8}
|
src={require("../../../../res/img/icon-pill-remove.svg")}
|
||||||
|
alt={_t('Remove')}
|
||||||
|
width={8}
|
||||||
|
height={8}
|
||||||
/>
|
/>
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
|
@ -297,7 +302,9 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
||||||
const avatar = (this.props.member as ThreepidMember).isEmail
|
const avatar = (this.props.member as ThreepidMember).isEmail
|
||||||
? <img
|
? <img
|
||||||
src={require("../../../../res/img/icon-email-pill-avatar.svg")}
|
src={require("../../../../res/img/icon-email-pill-avatar.svg")}
|
||||||
width={avatarSize} height={avatarSize} />
|
width={avatarSize}
|
||||||
|
height={avatarSize}
|
||||||
|
/>
|
||||||
: <BaseAvatar
|
: <BaseAvatar
|
||||||
url={this.props.member.getMxcAvatarUrl()
|
url={this.props.member.getMxcAvatarUrl()
|
||||||
? mediaFromMxc(this.props.member.getMxcAvatarUrl()).getSquareThumbnailHttp(avatarSize)
|
? mediaFromMxc(this.props.member.getMxcAvatarUrl()).getSquareThumbnailHttp(avatarSize)
|
||||||
|
@ -1458,7 +1465,8 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
<p className='mx_InviteDialog_helpText'>
|
<p className='mx_InviteDialog_helpText'>
|
||||||
<img
|
<img
|
||||||
src={require("../../../../res/img/element-icons/info.svg")}
|
src={require("../../../../res/img/element-icons/info.svg")}
|
||||||
width={14} height={14} />
|
width={14}
|
||||||
|
height={14} />
|
||||||
{ " " + _t("Invited people will be able to read old messages.") }
|
{ " " + _t("Invited people will be able to read old messages.") }
|
||||||
</p>;
|
</p>;
|
||||||
}
|
}
|
||||||
|
@ -1534,14 +1542,18 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
// Only show the backspace button if the field has content
|
// Only show the backspace button if the field has content
|
||||||
let dialPadField;
|
let dialPadField;
|
||||||
if (this.state.dialPadValue.length !== 0) {
|
if (this.state.dialPadValue.length !== 0) {
|
||||||
dialPadField = <Field className="mx_InviteDialog_dialPadField" id="dialpad_number"
|
dialPadField = <Field
|
||||||
|
className="mx_InviteDialog_dialPadField"
|
||||||
|
id="dialpad_number"
|
||||||
value={this.state.dialPadValue}
|
value={this.state.dialPadValue}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
onChange={this.onDialChange}
|
onChange={this.onDialChange}
|
||||||
postfixComponent={backspaceButton}
|
postfixComponent={backspaceButton}
|
||||||
/>;
|
/>;
|
||||||
} else {
|
} else {
|
||||||
dialPadField = <Field className="mx_InviteDialog_dialPadField" id="dialpad_number"
|
dialPadField = <Field
|
||||||
|
className="mx_InviteDialog_dialPadField"
|
||||||
|
id="dialpad_number"
|
||||||
value={this.state.dialPadValue}
|
value={this.state.dialPadValue}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
onChange={this.onDialChange}
|
onChange={this.onDialChange}
|
||||||
|
@ -1552,14 +1564,19 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
<form onSubmit={this.onDialFormSubmit}>
|
<form onSubmit={this.onDialFormSubmit}>
|
||||||
{ dialPadField }
|
{ dialPadField }
|
||||||
</form>
|
</form>
|
||||||
<Dialpad hasDial={false}
|
<Dialpad
|
||||||
onDigitPress={this.onDigitPress} onDeletePress={this.onDeletePress}
|
hasDial={false}
|
||||||
|
onDigitPress={this.onDigitPress}
|
||||||
|
onDeletePress={this.onDeletePress}
|
||||||
/>
|
/>
|
||||||
</div>;
|
</div>;
|
||||||
tabs.push(new Tab(TabId.DialPad, _td("Dial pad"), 'mx_InviteDialog_dialPadIcon', dialPadSection));
|
tabs.push(new Tab(TabId.DialPad, _td("Dial pad"), 'mx_InviteDialog_dialPadIcon', dialPadSection));
|
||||||
dialogContent = <React.Fragment>
|
dialogContent = <React.Fragment>
|
||||||
<TabbedView tabs={tabs} initialTabId={this.state.currentTabId}
|
<TabbedView
|
||||||
tabLocation={TabLocation.TOP} onChange={this.onTabChange}
|
tabs={tabs}
|
||||||
|
initialTabId={this.state.currentTabId}
|
||||||
|
tabLocation={TabLocation.TOP}
|
||||||
|
onChange={this.onTabChange}
|
||||||
/>
|
/>
|
||||||
{ consultConnectSection }
|
{ consultConnectSection }
|
||||||
</React.Fragment>;
|
</React.Fragment>;
|
||||||
|
|
192
src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx
Normal file
192
src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useMemo, useState } from "react";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
import { IDialogProps } from "./IDialogProps";
|
||||||
|
import BaseDialog from "./BaseDialog";
|
||||||
|
import SearchBox from "../../structures/SearchBox";
|
||||||
|
import SpaceStore from "../../../stores/SpaceStore";
|
||||||
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
|
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||||
|
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||||
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
|
||||||
|
interface IProps extends IDialogProps {
|
||||||
|
room: Room;
|
||||||
|
selected?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const Entry = ({ room, checked, onChange }) => {
|
||||||
|
const localRoom = room instanceof Room;
|
||||||
|
|
||||||
|
let description;
|
||||||
|
if (localRoom) {
|
||||||
|
description = _t("%(count)s members", { count: room.getJoinedMemberCount() });
|
||||||
|
const numChildRooms = SpaceStore.instance.getChildRooms(room.roomId).length;
|
||||||
|
if (numChildRooms > 0) {
|
||||||
|
description += " · " + _t("%(count)s rooms", { count: numChildRooms });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <label className="mx_ManageRestrictedJoinRuleDialog_entry">
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
{ localRoom
|
||||||
|
? <RoomAvatar room={room} height={20} width={20} />
|
||||||
|
: <RoomAvatar oobData={room} height={20} width={20} />
|
||||||
|
}
|
||||||
|
<span className="mx_ManageRestrictedJoinRuleDialog_entry_name">{ room.name }</span>
|
||||||
|
</div>
|
||||||
|
{ description && <div className="mx_ManageRestrictedJoinRuleDialog_entry_description">
|
||||||
|
{ description }
|
||||||
|
</div> }
|
||||||
|
</div>
|
||||||
|
<StyledCheckbox
|
||||||
|
onChange={onChange ? (e) => onChange(e.target.checked) : null}
|
||||||
|
checked={checked}
|
||||||
|
disabled={!onChange}
|
||||||
|
/>
|
||||||
|
</label>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ManageRestrictedJoinRuleDialog: React.FC<IProps> = ({ room, selected = [], onFinished }) => {
|
||||||
|
const cli = room.client;
|
||||||
|
const [newSelected, setNewSelected] = useState(new Set<string>(selected));
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
const lcQuery = query.toLowerCase().trim();
|
||||||
|
|
||||||
|
const [spacesContainingRoom, otherEntries] = useMemo(() => {
|
||||||
|
const spaces = cli.getVisibleRooms().filter(r => r.getMyMembership() === "join" && r.isSpaceRoom());
|
||||||
|
return [
|
||||||
|
spaces.filter(r => SpaceStore.instance.getSpaceFilteredRoomIds(r).has(room.roomId)),
|
||||||
|
selected.map(roomId => {
|
||||||
|
const room = cli.getRoom(roomId);
|
||||||
|
if (!room) {
|
||||||
|
return { roomId, name: roomId } as Room;
|
||||||
|
}
|
||||||
|
if (room.getMyMembership() !== "join" || !room.isSpaceRoom()) {
|
||||||
|
return room;
|
||||||
|
}
|
||||||
|
}).filter(Boolean),
|
||||||
|
];
|
||||||
|
}, [cli, selected, room.roomId]);
|
||||||
|
|
||||||
|
const [filteredSpacesContainingRooms, filteredOtherEntries] = useMemo(() => [
|
||||||
|
spacesContainingRoom.filter(r => r.name.toLowerCase().includes(lcQuery)),
|
||||||
|
otherEntries.filter(r => r.name.toLowerCase().includes(lcQuery)),
|
||||||
|
], [spacesContainingRoom, otherEntries, lcQuery]);
|
||||||
|
|
||||||
|
const onChange = (checked: boolean, room: Room): void => {
|
||||||
|
if (checked) {
|
||||||
|
newSelected.add(room.roomId);
|
||||||
|
} else {
|
||||||
|
newSelected.delete(room.roomId);
|
||||||
|
}
|
||||||
|
setNewSelected(new Set(newSelected));
|
||||||
|
};
|
||||||
|
|
||||||
|
let inviteOnlyWarning;
|
||||||
|
if (newSelected.size < 1) {
|
||||||
|
inviteOnlyWarning = <div className="mx_ManageRestrictedJoinRuleDialog_section_info">
|
||||||
|
{ _t("You're removing all spaces. Access will default to invite only") }
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <BaseDialog
|
||||||
|
title={_t("Select spaces")}
|
||||||
|
className="mx_ManageRestrictedJoinRuleDialog"
|
||||||
|
onFinished={onFinished}
|
||||||
|
fixedWidth={false}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
{ _t("Decide which spaces can access this room. " +
|
||||||
|
"If a space is selected, its members can find and join <RoomName/>.", {}, {
|
||||||
|
RoomName: () => <b>{ room.name }</b>,
|
||||||
|
}) }
|
||||||
|
</p>
|
||||||
|
<MatrixClientContext.Provider value={cli}>
|
||||||
|
<SearchBox
|
||||||
|
className="mx_textinput_icon mx_textinput_search"
|
||||||
|
placeholder={_t("Search spaces")}
|
||||||
|
onSearch={setQuery}
|
||||||
|
autoComplete={true}
|
||||||
|
autoFocus={true}
|
||||||
|
/>
|
||||||
|
<AutoHideScrollbar className="mx_ManageRestrictedJoinRuleDialog_content">
|
||||||
|
{ filteredSpacesContainingRooms.length > 0 ? (
|
||||||
|
<div className="mx_ManageRestrictedJoinRuleDialog_section">
|
||||||
|
<h3>{ _t("Spaces you know that contain this room") }</h3>
|
||||||
|
{ filteredSpacesContainingRooms.map(space => {
|
||||||
|
return <Entry
|
||||||
|
key={space.roomId}
|
||||||
|
room={space}
|
||||||
|
checked={newSelected.has(space.roomId)}
|
||||||
|
onChange={(checked: boolean) => {
|
||||||
|
onChange(checked, space);
|
||||||
|
}}
|
||||||
|
/>;
|
||||||
|
}) }
|
||||||
|
</div>
|
||||||
|
) : undefined }
|
||||||
|
|
||||||
|
{ filteredOtherEntries.length > 0 ? (
|
||||||
|
<div className="mx_ManageRestrictedJoinRuleDialog_section">
|
||||||
|
<h3>{ _t("Other spaces or rooms you might not know") }</h3>
|
||||||
|
<div className="mx_ManageRestrictedJoinRuleDialog_section_info">
|
||||||
|
<div>{ _t("These are likely ones other room admins are a part of.") }</div>
|
||||||
|
</div>
|
||||||
|
{ filteredOtherEntries.map(space => {
|
||||||
|
return <Entry
|
||||||
|
key={space.roomId}
|
||||||
|
room={space}
|
||||||
|
checked={newSelected.has(space.roomId)}
|
||||||
|
onChange={(checked: boolean) => {
|
||||||
|
onChange(checked, space);
|
||||||
|
}}
|
||||||
|
/>;
|
||||||
|
}) }
|
||||||
|
</div>
|
||||||
|
) : null }
|
||||||
|
|
||||||
|
{ filteredSpacesContainingRooms.length + filteredOtherEntries.length < 1
|
||||||
|
? <span className="mx_ManageRestrictedJoinRuleDialog_noResults">
|
||||||
|
{ _t("No results") }
|
||||||
|
</span>
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
</AutoHideScrollbar>
|
||||||
|
|
||||||
|
<div className="mx_ManageRestrictedJoinRuleDialog_footer">
|
||||||
|
{ inviteOnlyWarning }
|
||||||
|
<div className="mx_ManageRestrictedJoinRuleDialog_footer_buttons">
|
||||||
|
<AccessibleButton kind="primary_outline" onClick={() => onFinished()}>
|
||||||
|
{ _t("Cancel") }
|
||||||
|
</AccessibleButton>
|
||||||
|
<AccessibleButton kind="primary" onClick={() => onFinished(Array.from(newSelected))}>
|
||||||
|
{ _t("Confirm") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MatrixClientContext.Provider>
|
||||||
|
</BaseDialog>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ManageRestrictedJoinRuleDialog;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2018 New Vector Ltd
|
Copyright 2018 - 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.
|
||||||
|
@ -15,19 +15,29 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import * as sdk from '../../../index';
|
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { upgradeRoom } from "../../../utils/RoomUpgrade";
|
||||||
|
import { IDialogProps } from "./IDialogProps";
|
||||||
|
import BaseDialog from "./BaseDialog";
|
||||||
|
import ErrorDialog from './ErrorDialog';
|
||||||
|
import DialogButtons from '../elements/DialogButtons';
|
||||||
|
import Spinner from "../elements/Spinner";
|
||||||
|
|
||||||
|
interface IProps extends IDialogProps {
|
||||||
|
room: Room;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
busy: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.dialogs.RoomUpgradeDialog")
|
@replaceableComponent("views.dialogs.RoomUpgradeDialog")
|
||||||
export default class RoomUpgradeDialog extends React.Component {
|
export default class RoomUpgradeDialog extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
private targetVersion: string;
|
||||||
room: PropTypes.object.isRequired,
|
|
||||||
onFinished: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
busy: true,
|
busy: true,
|
||||||
|
@ -35,20 +45,19 @@ export default class RoomUpgradeDialog extends React.Component {
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
const recommended = await this.props.room.getRecommendedVersion();
|
const recommended = await this.props.room.getRecommendedVersion();
|
||||||
this._targetVersion = recommended.version;
|
this.targetVersion = recommended.version;
|
||||||
this.setState({ busy: false });
|
this.setState({ busy: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
_onCancelClick = () => {
|
private onCancelClick = (): void => {
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
_onUpgradeClick = () => {
|
private onUpgradeClick = (): void => {
|
||||||
this.setState({ busy: true });
|
this.setState({ busy: true });
|
||||||
MatrixClientPeg.get().upgradeRoom(this.props.room.roomId, this._targetVersion).then(() => {
|
upgradeRoom(this.props.room, this.targetVersion, false, false).then(() => {
|
||||||
this.props.onFinished(true);
|
this.props.onFinished(true);
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
Modal.createTrackedDialog('Failed to upgrade room', '', ErrorDialog, {
|
Modal.createTrackedDialog('Failed to upgrade room', '', ErrorDialog, {
|
||||||
title: _t("Failed to upgrade room"),
|
title: _t("Failed to upgrade room"),
|
||||||
description: ((err && err.message) ? err.message : _t("The room upgrade could not be completed")),
|
description: ((err && err.message) ? err.message : _t("The room upgrade could not be completed")),
|
||||||
|
@ -59,29 +68,22 @@ export default class RoomUpgradeDialog extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
|
||||||
const Spinner = sdk.getComponent('views.elements.Spinner');
|
|
||||||
|
|
||||||
let buttons;
|
let buttons;
|
||||||
if (this.state.busy) {
|
if (this.state.busy) {
|
||||||
buttons = <Spinner />;
|
buttons = <Spinner />;
|
||||||
} else {
|
} else {
|
||||||
buttons = <DialogButtons
|
buttons = <DialogButtons
|
||||||
primaryButton={_t(
|
primaryButton={_t('Upgrade this room to version %(version)s', { version: this.targetVersion })}
|
||||||
'Upgrade this room to version %(version)s',
|
|
||||||
{ version: this._targetVersion },
|
|
||||||
)}
|
|
||||||
primaryButtonClass="danger"
|
primaryButtonClass="danger"
|
||||||
hasCancel={true}
|
hasCancel={true}
|
||||||
onPrimaryButtonClick={this._onUpgradeClick}
|
onPrimaryButtonClick={this.onUpgradeClick}
|
||||||
focus={this.props.focus}
|
onCancel={this.onCancelClick}
|
||||||
onCancel={this._onCancelClick}
|
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_RoomUpgradeDialog"
|
<BaseDialog
|
||||||
|
className="mx_RoomUpgradeDialog"
|
||||||
onFinished={this.props.onFinished}
|
onFinished={this.props.onFinished}
|
||||||
title={_t("Upgrade Room Version")}
|
title={_t("Upgrade Room Version")}
|
||||||
contentId='mx_Dialog_content'
|
contentId='mx_Dialog_content'
|
||||||
|
@ -97,8 +99,10 @@ export default class RoomUpgradeDialog extends React.Component {
|
||||||
<ol>
|
<ol>
|
||||||
<li>{ _t("Create a new room with the same name, description and avatar") }</li>
|
<li>{ _t("Create a new room with the same name, description and avatar") }</li>
|
||||||
<li>{ _t("Update any local room aliases to point to the new room") }</li>
|
<li>{ _t("Update any local room aliases to point to the new room") }</li>
|
||||||
<li>{ _t("Stop users from speaking in the old version of the room, and post a message advising users to move to the new room") }</li>
|
<li>{ _t("Stop users from speaking in the old version of the room, " +
|
||||||
<li>{ _t("Put a link back to the old room at the start of the new room so people can see old messages") }</li>
|
"and post a message advising users to move to the new room") }</li>
|
||||||
|
<li>{ _t("Put a link back to the old room at the start of the new room " +
|
||||||
|
"so people can see old messages") }</li>
|
||||||
</ol>
|
</ol>
|
||||||
{ buttons }
|
{ buttons }
|
||||||
</BaseDialog>
|
</BaseDialog>
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
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,73 +14,82 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import { EventType } from 'matrix-js-sdk/src/@types/event';
|
||||||
|
import { JoinRule } from 'matrix-js-sdk/src/@types/partials';
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import SdkConfig from "../../../SdkConfig";
|
import SdkConfig from "../../../SdkConfig";
|
||||||
import * as sdk from "../../../index";
|
|
||||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { IDialogProps } from "./IDialogProps";
|
||||||
|
import BugReportDialog from './BugReportDialog';
|
||||||
|
import BaseDialog from "./BaseDialog";
|
||||||
|
import DialogButtons from "../elements/DialogButtons";
|
||||||
|
|
||||||
|
interface IProps extends IDialogProps {
|
||||||
|
roomId: string;
|
||||||
|
targetVersion: string;
|
||||||
|
description?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
inviteUsersToNewRoom: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.dialogs.RoomUpgradeWarningDialog")
|
@replaceableComponent("views.dialogs.RoomUpgradeWarningDialog")
|
||||||
export default class RoomUpgradeWarningDialog extends React.Component {
|
export default class RoomUpgradeWarningDialog extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
private readonly isPrivate: boolean;
|
||||||
onFinished: PropTypes.func.isRequired,
|
private readonly currentVersion: string;
|
||||||
roomId: PropTypes.string.isRequired,
|
|
||||||
targetVersion: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||||
const joinRules = room ? room.currentState.getStateEvents("m.room.join_rules", "") : null;
|
const joinRules = room?.currentState.getStateEvents(EventType.RoomJoinRules, "");
|
||||||
const isPrivate = joinRules ? joinRules.getContent()['join_rule'] !== 'public' : true;
|
this.isPrivate = joinRules?.getContent()['join_rule'] !== JoinRule.Public ?? true;
|
||||||
|
this.currentVersion = room?.getVersion() || "1";
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
currentVersion: room ? room.getVersion() : "1",
|
|
||||||
isPrivate,
|
|
||||||
inviteUsersToNewRoom: true,
|
inviteUsersToNewRoom: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
_onContinue = () => {
|
private onContinue = () => {
|
||||||
this.props.onFinished({ continue: true, invite: this.state.isPrivate && this.state.inviteUsersToNewRoom });
|
this.props.onFinished({ continue: true, invite: this.isPrivate && this.state.inviteUsersToNewRoom });
|
||||||
};
|
};
|
||||||
|
|
||||||
_onCancel = () => {
|
private onCancel = () => {
|
||||||
this.props.onFinished({ continue: false, invite: false });
|
this.props.onFinished({ continue: false, invite: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
_onInviteUsersToggle = (newVal) => {
|
private onInviteUsersToggle = (inviteUsersToNewRoom: boolean) => {
|
||||||
this.setState({ inviteUsersToNewRoom: newVal });
|
this.setState({ inviteUsersToNewRoom });
|
||||||
};
|
};
|
||||||
|
|
||||||
_openBugReportDialog = (e) => {
|
private openBugReportDialog = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
|
|
||||||
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {});
|
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const brand = SdkConfig.get().brand;
|
const brand = SdkConfig.get().brand;
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
|
||||||
|
|
||||||
let inviteToggle = null;
|
let inviteToggle = null;
|
||||||
if (this.state.isPrivate) {
|
if (this.isPrivate) {
|
||||||
inviteToggle = (
|
inviteToggle = (
|
||||||
<LabelledToggleSwitch
|
<LabelledToggleSwitch
|
||||||
value={this.state.inviteUsersToNewRoom}
|
value={this.state.inviteUsersToNewRoom}
|
||||||
onChange={this._onInviteUsersToggle}
|
onChange={this.onInviteUsersToggle}
|
||||||
label={_t("Automatically invite users")} />
|
label={_t("Automatically invite members from this room to the new one")} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = this.state.isPrivate ? _t("Upgrade private room") : _t("Upgrade public room");
|
const title = this.isPrivate ? _t("Upgrade private room") : _t("Upgrade public room");
|
||||||
|
|
||||||
let bugReports = (
|
let bugReports = (
|
||||||
<p>
|
<p>
|
||||||
|
@ -101,7 +110,7 @@ export default class RoomUpgradeWarningDialog extends React.Component {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"a": (sub) => {
|
"a": (sub) => {
|
||||||
return <a href='#' onClick={this._openBugReportDialog}>{ sub }</a>;
|
return <a href='#' onClick={this.openBugReportDialog}>{ sub }</a>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
) }
|
) }
|
||||||
|
@ -119,18 +128,26 @@ export default class RoomUpgradeWarningDialog extends React.Component {
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
{ _t(
|
{ this.props.description || _t(
|
||||||
"Upgrading a room is an advanced action and is usually recommended when a room " +
|
"Upgrading a room is an advanced action and is usually recommended when a room " +
|
||||||
"is unstable due to bugs, missing features or security vulnerabilities.",
|
"is unstable due to bugs, missing features or security vulnerabilities.",
|
||||||
) }
|
) }
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
{ _t(
|
||||||
|
"<b>Please note upgrading will make a new version of the room</b>. " +
|
||||||
|
"All current messages will stay in this archived room.", {}, {
|
||||||
|
b: sub => <b>{ sub }</b>,
|
||||||
|
},
|
||||||
|
) }
|
||||||
|
</p>
|
||||||
{ bugReports }
|
{ bugReports }
|
||||||
<p>
|
<p>
|
||||||
{ _t(
|
{ _t(
|
||||||
"You'll upgrade this room from <oldVersion /> to <newVersion />.",
|
"You'll upgrade this room from <oldVersion /> to <newVersion />.",
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
oldVersion: () => <code>{ this.state.currentVersion }</code>,
|
oldVersion: () => <code>{ this.currentVersion }</code>,
|
||||||
newVersion: () => <code>{ this.props.targetVersion }</code>,
|
newVersion: () => <code>{ this.props.targetVersion }</code>,
|
||||||
},
|
},
|
||||||
) }
|
) }
|
||||||
|
@ -139,9 +156,9 @@ export default class RoomUpgradeWarningDialog extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
<DialogButtons
|
<DialogButtons
|
||||||
primaryButton={_t("Upgrade")}
|
primaryButton={_t("Upgrade")}
|
||||||
onPrimaryButtonClick={this._onContinue}
|
onPrimaryButtonClick={this.onContinue}
|
||||||
cancelButton={_t("Cancel")}
|
cancelButton={_t("Cancel")}
|
||||||
onCancel={this._onCancel}
|
onCancel={this.onCancel}
|
||||||
/>
|
/>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
|
@ -85,7 +85,9 @@ export default class SessionRestoreErrorDialog extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
|
<BaseDialog
|
||||||
|
className="mx_ErrorDialog"
|
||||||
|
onFinished={this.props.onFinished}
|
||||||
title={_t('Unable to restore session')}
|
title={_t('Unable to restore session')}
|
||||||
contentId='mx_Dialog_content'
|
contentId='mx_Dialog_content'
|
||||||
hasCancel={false}
|
hasCancel={false}
|
||||||
|
|
|
@ -54,7 +54,9 @@ export default class StorageEvictedDialog extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
|
<BaseDialog
|
||||||
|
className="mx_ErrorDialog"
|
||||||
|
onFinished={this.props.onFinished}
|
||||||
title={_t('Missing session data')}
|
title={_t('Missing session data')}
|
||||||
contentId='mx_Dialog_content'
|
contentId='mx_Dialog_content'
|
||||||
hasCancel={false}
|
hasCancel={false}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue