Merge branch 'develop' of https://github.com/matrix-org/matrix-react-sdk into export-conversations
This commit is contained in:
commit
b04bfeda33
196 changed files with 5000 additions and 3423 deletions
|
@ -87,6 +87,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";
|
||||||
|
@ -160,10 +161,10 @@
|
||||||
@import "./views/groups/_GroupPublicityToggle.scss";
|
@import "./views/groups/_GroupPublicityToggle.scss";
|
||||||
@import "./views/groups/_GroupRoomList.scss";
|
@import "./views/groups/_GroupRoomList.scss";
|
||||||
@import "./views/groups/_GroupUserSettings.scss";
|
@import "./views/groups/_GroupUserSettings.scss";
|
||||||
|
@import "./views/messages/_CallEvent.scss";
|
||||||
@import "./views/messages/_CreateEvent.scss";
|
@import "./views/messages/_CreateEvent.scss";
|
||||||
@import "./views/messages/_DateSeparator.scss";
|
@import "./views/messages/_DateSeparator.scss";
|
||||||
@import "./views/messages/_EventTileBubble.scss";
|
@import "./views/messages/_EventTileBubble.scss";
|
||||||
@import "./views/messages/_CallEvent.scss";
|
|
||||||
@import "./views/messages/_MEmoteBody.scss";
|
@import "./views/messages/_MEmoteBody.scss";
|
||||||
@import "./views/messages/_MFileBody.scss";
|
@import "./views/messages/_MFileBody.scss";
|
||||||
@import "./views/messages/_MImageBody.scss";
|
@import "./views/messages/_MImageBody.scss";
|
||||||
|
@ -173,7 +174,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";
|
||||||
|
@ -202,8 +202,8 @@
|
||||||
@import "./views/rooms/_E2EIcon.scss";
|
@import "./views/rooms/_E2EIcon.scss";
|
||||||
@import "./views/rooms/_EditMessageComposer.scss";
|
@import "./views/rooms/_EditMessageComposer.scss";
|
||||||
@import "./views/rooms/_EntityTile.scss";
|
@import "./views/rooms/_EntityTile.scss";
|
||||||
@import "./views/rooms/_EventTile.scss";
|
|
||||||
@import "./views/rooms/_EventBubbleTile.scss";
|
@import "./views/rooms/_EventBubbleTile.scss";
|
||||||
|
@import "./views/rooms/_EventTile.scss";
|
||||||
@import "./views/rooms/_GroupLayout.scss";
|
@import "./views/rooms/_GroupLayout.scss";
|
||||||
@import "./views/rooms/_IRCLayout.scss";
|
@import "./views/rooms/_IRCLayout.scss";
|
||||||
@import "./views/rooms/_JumpToBottomButton.scss";
|
@import "./views/rooms/_JumpToBottomButton.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,23 +16,15 @@ limitations under the License.
|
||||||
|
|
||||||
$timelineImageBorderRadius: 4px;
|
$timelineImageBorderRadius: 4px;
|
||||||
|
|
||||||
.mx_MImageBody {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_MImageBody_thumbnail {
|
.mx_MImageBody_thumbnail {
|
||||||
position: absolute;
|
object-fit: contain;
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
border-radius: $timelineImageBorderRadius;
|
border-radius: $timelineImageBorderRadius;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
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,10 +132,15 @@ $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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& ~ .mx_EventListSummary .mx_EventTile_line {
|
||||||
|
padding-left: calc($left-gutter);
|
||||||
|
}
|
||||||
|
|
||||||
&.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line {
|
&.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line {
|
||||||
padding-left: calc($left-gutter + 18px - $hover-select-border);
|
padding-left: calc($left-gutter + 18px - $hover-select-border);
|
||||||
}
|
}
|
||||||
|
@ -208,43 +213,11 @@ $hover-select-border: 4px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* all the overflow-y: hidden; are to trap Zalgos -
|
|
||||||
but they introduce an implicit overflow-x: auto.
|
|
||||||
so make that explicitly hidden too to avoid random
|
|
||||||
horizontal scrollbars occasionally appearing, like in
|
|
||||||
https://github.com/vector-im/vector-web/issues/1154
|
|
||||||
*/
|
|
||||||
.mx_EventTile_content {
|
|
||||||
display: block;
|
|
||||||
overflow-y: hidden;
|
|
||||||
overflow-x: hidden;
|
|
||||||
margin-right: 34px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* De-zalgoing */
|
/* De-zalgoing */
|
||||||
.mx_EventTile_body {
|
.mx_EventTile_body {
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Spoiler stuff */
|
|
||||||
.mx_EventTile_spoiler {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_spoiler_reason {
|
|
||||||
color: $event-timestamp-color;
|
|
||||||
font-size: $font-11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_spoiler_content {
|
|
||||||
filter: blur(5px) saturate(0.1) sepia(1);
|
|
||||||
transition-duration: 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_spoiler.visible > .mx_EventTile_spoiler_content {
|
|
||||||
filter: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover.mx_EventTile_verified .mx_EventTile_line,
|
&:hover.mx_EventTile_verified .mx_EventTile_line,
|
||||||
&:hover.mx_EventTile_unverified .mx_EventTile_line,
|
&:hover.mx_EventTile_unverified .mx_EventTile_line,
|
||||||
&:hover.mx_EventTile_unknown .mx_EventTile_line {
|
&:hover.mx_EventTile_unknown .mx_EventTile_line {
|
||||||
|
@ -307,6 +280,36 @@ $hover-select-border: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* all the overflow-y: hidden; are to trap Zalgos -
|
||||||
|
but they introduce an implicit overflow-x: auto.
|
||||||
|
so make that explicitly hidden too to avoid random
|
||||||
|
horizontal scrollbars occasionally appearing, like in
|
||||||
|
https://github.com/vector-im/vector-web/issues/1154 */
|
||||||
|
.mx_EventTile_content {
|
||||||
|
overflow-y: hidden;
|
||||||
|
overflow-x: hidden;
|
||||||
|
margin-right: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Spoiler stuff */
|
||||||
|
.mx_EventTile_spoiler {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_spoiler_reason {
|
||||||
|
color: $event-timestamp-color;
|
||||||
|
font-size: $font-11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_spoiler_content {
|
||||||
|
filter: blur(5px) saturate(0.1) sepia(1);
|
||||||
|
transition-duration: 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_spoiler.visible > .mx_EventTile_spoiler_content {
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_RoomView_timeline_rr_enabled {
|
.mx_RoomView_timeline_rr_enabled {
|
||||||
|
|
||||||
.mx_EventTile:not([data-layout=bubble]) {
|
.mx_EventTile:not([data-layout=bubble]) {
|
||||||
|
@ -469,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,6 +44,11 @@ limitations under the License.
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 0 16px 9px 16px;
|
padding: 0 16px 9px 16px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
display: flow-root;
|
||||||
|
|
||||||
|
.mx_EventTile[data-layout=bubble] {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_EventTile_msgOption {
|
.mx_EventTile_msgOption {
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -154,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;
|
||||||
|
@ -210,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)\/(.+?)\/(.+?)(?:[?/]|$)/;
|
||||||
|
|
||||||
|
@ -79,8 +79,8 @@ function mightContainEmoji(str: string): boolean {
|
||||||
* @return {String} The shortcode (such as :thumbup:)
|
* @return {String} The shortcode (such as :thumbup:)
|
||||||
*/
|
*/
|
||||||
export function unicodeToShortcode(char: string): string {
|
export function unicodeToShortcode(char: string): string {
|
||||||
const shortcodes = getEmojiFromUnicode(char).shortcodes;
|
const shortcodes = getEmojiFromUnicode(char)?.shortcodes;
|
||||||
return shortcodes.length > 0 ? `:${shortcodes[0]}:` : '';
|
return shortcodes?.length ? `:${shortcodes[0]}:` : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function processHtmlForSending(html: string): string {
|
export function processHtmlForSending(html: string): string {
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
|
@ -15,8 +15,10 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import * as sdk from '../../../../index';
|
|
||||||
import PropTypes from 'prop-types';
|
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
|
||||||
|
import Spinner from "../../../../components/views/elements/Spinner";
|
||||||
|
import DialogButtons from "../../../../components/views/elements/DialogButtons";
|
||||||
import dis from "../../../../dispatcher/dispatcher";
|
import dis from "../../../../dispatcher/dispatcher";
|
||||||
import { _t } from '../../../../languageHandler';
|
import { _t } from '../../../../languageHandler';
|
||||||
|
|
||||||
|
@ -24,46 +26,44 @@ import SettingsStore from "../../../../settings/SettingsStore";
|
||||||
import EventIndexPeg from "../../../../indexing/EventIndexPeg";
|
import EventIndexPeg from "../../../../indexing/EventIndexPeg";
|
||||||
import { Action } from "../../../../dispatcher/actions";
|
import { Action } from "../../../../dispatcher/actions";
|
||||||
import { SettingLevel } from "../../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../../settings/SettingLevel";
|
||||||
|
interface IProps {
|
||||||
|
onFinished: (success: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
disabling: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Allows the user to disable the Event Index.
|
* Allows the user to disable the Event Index.
|
||||||
*/
|
*/
|
||||||
export default class DisableEventIndexDialog extends React.Component {
|
export default class DisableEventIndexDialog extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
constructor(props: IProps) {
|
||||||
onFinished: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
disabling: false,
|
disabling: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
_onDisable = async () => {
|
private onDisable = async (): Promise<void> => {
|
||||||
this.setState({
|
this.setState({
|
||||||
disabling: true,
|
disabling: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, false);
|
await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, false);
|
||||||
await EventIndexPeg.deleteEventIndex();
|
await EventIndexPeg.deleteEventIndex();
|
||||||
this.props.onFinished();
|
this.props.onFinished(true);
|
||||||
dis.fire(Action.ViewUserSettings);
|
dis.fire(Action.ViewUserSettings);
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
|
||||||
const Spinner = sdk.getComponent('elements.Spinner');
|
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
|
||||||
|
|
||||||
|
public render(): React.ReactNode {
|
||||||
return (
|
return (
|
||||||
<BaseDialog onFinished={this.props.onFinished} title={_t("Are you sure?")}>
|
<BaseDialog onFinished={this.props.onFinished} title={_t("Are you sure?")}>
|
||||||
{ _t("If disabled, messages from encrypted rooms won't appear in search results.") }
|
{ _t("If disabled, messages from encrypted rooms won't appear in search results.") }
|
||||||
{ this.state.disabling ? <Spinner /> : <div /> }
|
{ this.state.disabling ? <Spinner /> : <div /> }
|
||||||
<DialogButtons
|
<DialogButtons
|
||||||
primaryButton={_t('Disable')}
|
primaryButton={_t('Disable')}
|
||||||
onPrimaryButtonClick={this._onDisable}
|
onPrimaryButtonClick={this.onDisable}
|
||||||
primaryButtonClass="danger"
|
primaryButtonClass="danger"
|
||||||
cancelButtonClass="warning"
|
cancelButtonClass="warning"
|
||||||
onCancel={this.props.onFinished}
|
onCancel={this.props.onFinished}
|
|
@ -134,8 +134,9 @@ export default class ManageEventIndexDialog extends React.Component<IProps, ISta
|
||||||
}
|
}
|
||||||
|
|
||||||
private onDisable = async () => {
|
private onDisable = async () => {
|
||||||
Modal.createTrackedDialogAsync("Disable message search", "Disable message search",
|
const DisableEventIndexDialog = (await import("./DisableEventIndexDialog")).default;
|
||||||
import("./DisableEventIndexDialog"),
|
Modal.createTrackedDialog("Disable message search", "Disable message search",
|
||||||
|
DisableEventIndexDialog,
|
||||||
null, null, /* priority = */ false, /* static = */ true,
|
null, null, /* priority = */ false, /* static = */ true,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
@replaceableComponent("views.auth.AuthBody")
|
@replaceableComponent("views.auth.AuthBody")
|
||||||
export default class AuthBody extends React.PureComponent {
|
export default class AuthBody extends React.PureComponent {
|
||||||
render() {
|
public render(): React.ReactNode {
|
||||||
return <div className="mx_AuthBody">
|
return <div className="mx_AuthBody">
|
||||||
{ this.props.children }
|
{ this.props.children }
|
||||||
</div>;
|
</div>;
|
|
@ -22,7 +22,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
@replaceableComponent("views.auth.AuthFooter")
|
@replaceableComponent("views.auth.AuthFooter")
|
||||||
export default class AuthFooter extends React.Component {
|
export default class AuthFooter extends React.Component {
|
||||||
render() {
|
public render(): React.ReactNode {
|
||||||
return (
|
return (
|
||||||
<div className="mx_AuthFooter">
|
<div className="mx_AuthFooter">
|
||||||
<a href="https://matrix.org" target="_blank" rel="noreferrer noopener">{ _t("powered by Matrix") }</a>
|
<a href="https://matrix.org" target="_blank" rel="noreferrer noopener">{ _t("powered by Matrix") }</a>
|
|
@ -16,20 +16,17 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import * as sdk from '../../../index';
|
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import AuthHeaderLogo from "./AuthHeaderLogo";
|
||||||
|
import LanguageSelector from "./LanguageSelector";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
disableLanguageSelector?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.auth.AuthHeader")
|
@replaceableComponent("views.auth.AuthHeader")
|
||||||
export default class AuthHeader extends React.Component {
|
export default class AuthHeader extends React.Component<IProps> {
|
||||||
static propTypes = {
|
public render(): React.ReactNode {
|
||||||
disableLanguageSelector: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const AuthHeaderLogo = sdk.getComponent('auth.AuthHeaderLogo');
|
|
||||||
const LanguageSelector = sdk.getComponent('views.auth.LanguageSelector');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_AuthHeader">
|
<div className="mx_AuthHeader">
|
||||||
<AuthHeaderLogo />
|
<AuthHeaderLogo />
|
|
@ -19,7 +19,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
@replaceableComponent("views.auth.AuthHeaderLogo")
|
@replaceableComponent("views.auth.AuthHeaderLogo")
|
||||||
export default class AuthHeaderLogo extends React.PureComponent {
|
export default class AuthHeaderLogo extends React.PureComponent {
|
||||||
render() {
|
public render(): React.ReactNode {
|
||||||
return <div className="mx_AuthHeaderLogo">
|
return <div className="mx_AuthHeaderLogo">
|
||||||
Matrix
|
Matrix
|
||||||
</div>;
|
</div>;
|
|
@ -17,14 +17,12 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import * as sdk from '../../../index';
|
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import AuthFooter from "./AuthFooter";
|
||||||
|
|
||||||
@replaceableComponent("views.auth.AuthPage")
|
@replaceableComponent("views.auth.AuthPage")
|
||||||
export default class AuthPage extends React.PureComponent {
|
export default class AuthPage extends React.PureComponent {
|
||||||
render() {
|
public render(): React.ReactNode {
|
||||||
const AuthFooter = sdk.getComponent('auth.AuthFooter');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_AuthPage">
|
<div className="mx_AuthPage">
|
||||||
<div className="mx_AuthPage_modal">
|
<div className="mx_AuthPage_modal">
|
|
@ -19,7 +19,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
@replaceableComponent("views.auth.CompleteSecurityBody")
|
@replaceableComponent("views.auth.CompleteSecurityBody")
|
||||||
export default class CompleteSecurityBody extends React.PureComponent {
|
export default class CompleteSecurityBody extends React.PureComponent {
|
||||||
render() {
|
public render(): React.ReactNode {
|
||||||
return <div className="mx_CompleteSecurityBody">
|
return <div className="mx_CompleteSecurityBody">
|
||||||
{ this.props.children }
|
{ this.props.children }
|
||||||
</div>;
|
</div>;
|
|
@ -15,21 +15,19 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import * as sdk from '../../../index';
|
import { COUNTRIES, getEmojiFlag, PhoneNumberCountryDefinition } from '../../../phonenumber';
|
||||||
|
|
||||||
import { COUNTRIES, getEmojiFlag } from '../../../phonenumber';
|
|
||||||
import SdkConfig from "../../../SdkConfig";
|
import SdkConfig from "../../../SdkConfig";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import Dropdown from "../elements/Dropdown";
|
||||||
|
|
||||||
const COUNTRIES_BY_ISO2 = {};
|
const COUNTRIES_BY_ISO2 = {};
|
||||||
for (const c of COUNTRIES) {
|
for (const c of COUNTRIES) {
|
||||||
COUNTRIES_BY_ISO2[c.iso2] = c;
|
COUNTRIES_BY_ISO2[c.iso2] = c;
|
||||||
}
|
}
|
||||||
|
|
||||||
function countryMatchesSearchQuery(query, country) {
|
function countryMatchesSearchQuery(query: string, country: PhoneNumberCountryDefinition): boolean {
|
||||||
// Remove '+' if present (when searching for a prefix)
|
// Remove '+' if present (when searching for a prefix)
|
||||||
if (query[0] === '+') {
|
if (query[0] === '+') {
|
||||||
query = query.slice(1);
|
query = query.slice(1);
|
||||||
|
@ -41,15 +39,26 @@ function countryMatchesSearchQuery(query, country) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.auth.CountryDropdown")
|
interface IProps {
|
||||||
export default class CountryDropdown extends React.Component {
|
value?: string;
|
||||||
constructor(props) {
|
onOptionChange: (country: PhoneNumberCountryDefinition) => void;
|
||||||
super(props);
|
isSmall: boolean; // if isSmall, show +44 in the selected value
|
||||||
this._onSearchChange = this._onSearchChange.bind(this);
|
showPrefix: boolean;
|
||||||
this._onOptionChange = this._onOptionChange.bind(this);
|
className?: string;
|
||||||
this._getShortOption = this._getShortOption.bind(this);
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
let defaultCountry = COUNTRIES[0];
|
interface IState {
|
||||||
|
searchQuery: string;
|
||||||
|
defaultCountry: PhoneNumberCountryDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("views.auth.CountryDropdown")
|
||||||
|
export default class CountryDropdown extends React.Component<IProps, IState> {
|
||||||
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
let defaultCountry: PhoneNumberCountryDefinition = COUNTRIES[0];
|
||||||
const defaultCountryCode = SdkConfig.get()["defaultCountryCode"];
|
const defaultCountryCode = SdkConfig.get()["defaultCountryCode"];
|
||||||
if (defaultCountryCode) {
|
if (defaultCountryCode) {
|
||||||
const country = COUNTRIES.find(c => c.iso2 === defaultCountryCode.toUpperCase());
|
const country = COUNTRIES.find(c => c.iso2 === defaultCountryCode.toUpperCase());
|
||||||
|
@ -62,7 +71,7 @@ export default class CountryDropdown extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount(): void {
|
||||||
if (!this.props.value) {
|
if (!this.props.value) {
|
||||||
// If no value is given, we start with the default
|
// If no value is given, we start with the default
|
||||||
// country selected, but our parent component
|
// country selected, but our parent component
|
||||||
|
@ -71,21 +80,21 @@ export default class CountryDropdown extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSearchChange(search) {
|
private onSearchChange = (search: string): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
searchQuery: search,
|
searchQuery: search,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
_onOptionChange(iso2) {
|
private onOptionChange = (iso2: string): void => {
|
||||||
this.props.onOptionChange(COUNTRIES_BY_ISO2[iso2]);
|
this.props.onOptionChange(COUNTRIES_BY_ISO2[iso2]);
|
||||||
}
|
};
|
||||||
|
|
||||||
_flagImgForIso2(iso2) {
|
private flagImgForIso2(iso2: string): React.ReactNode {
|
||||||
return <div className="mx_Dropdown_option_emoji">{ getEmojiFlag(iso2) }</div>;
|
return <div className="mx_Dropdown_option_emoji">{ getEmojiFlag(iso2) }</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
_getShortOption(iso2) {
|
private getShortOption = (iso2: string): React.ReactNode => {
|
||||||
if (!this.props.isSmall) {
|
if (!this.props.isSmall) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -94,14 +103,12 @@ export default class CountryDropdown extends React.Component {
|
||||||
countryPrefix = '+' + COUNTRIES_BY_ISO2[iso2].prefix;
|
countryPrefix = '+' + COUNTRIES_BY_ISO2[iso2].prefix;
|
||||||
}
|
}
|
||||||
return <span className="mx_CountryDropdown_shortOption">
|
return <span className="mx_CountryDropdown_shortOption">
|
||||||
{ this._flagImgForIso2(iso2) }
|
{ this.flagImgForIso2(iso2) }
|
||||||
{ countryPrefix }
|
{ countryPrefix }
|
||||||
</span>;
|
</span>;
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
|
||||||
const Dropdown = sdk.getComponent('elements.Dropdown');
|
|
||||||
|
|
||||||
|
public render(): React.ReactNode {
|
||||||
let displayedCountries;
|
let displayedCountries;
|
||||||
if (this.state.searchQuery) {
|
if (this.state.searchQuery) {
|
||||||
displayedCountries = COUNTRIES.filter(
|
displayedCountries = COUNTRIES.filter(
|
||||||
|
@ -124,7 +131,7 @@ export default class CountryDropdown extends React.Component {
|
||||||
|
|
||||||
const options = displayedCountries.map((country) => {
|
const options = displayedCountries.map((country) => {
|
||||||
return <div className="mx_CountryDropdown_option" key={country.iso2}>
|
return <div className="mx_CountryDropdown_option" key={country.iso2}>
|
||||||
{ this._flagImgForIso2(country.iso2) }
|
{ this.flagImgForIso2(country.iso2) }
|
||||||
{ _t(country.name) } (+{ country.prefix })
|
{ _t(country.name) } (+{ country.prefix })
|
||||||
</div>;
|
</div>;
|
||||||
});
|
});
|
||||||
|
@ -136,10 +143,10 @@ export default class CountryDropdown extends React.Component {
|
||||||
return <Dropdown
|
return <Dropdown
|
||||||
id="mx_CountryDropdown"
|
id="mx_CountryDropdown"
|
||||||
className={this.props.className + " mx_CountryDropdown"}
|
className={this.props.className + " mx_CountryDropdown"}
|
||||||
onOptionChange={this._onOptionChange}
|
onOptionChange={this.onOptionChange}
|
||||||
onSearchChange={this._onSearchChange}
|
onSearchChange={this.onSearchChange}
|
||||||
menuWidth={298}
|
menuWidth={298}
|
||||||
getShortOption={this._getShortOption}
|
getShortOption={this.getShortOption}
|
||||||
value={value}
|
value={value}
|
||||||
searchEnabled={true}
|
searchEnabled={true}
|
||||||
disabled={this.props.disabled}
|
disabled={this.props.disabled}
|
||||||
|
@ -149,13 +156,3 @@ export default class CountryDropdown extends React.Component {
|
||||||
</Dropdown>;
|
</Dropdown>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CountryDropdown.propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
isSmall: PropTypes.bool,
|
|
||||||
// if isSmall, show +44 in the selected value
|
|
||||||
showPrefix: PropTypes.bool,
|
|
||||||
onOptionChange: PropTypes.func.isRequired,
|
|
||||||
value: PropTypes.string,
|
|
||||||
disabled: PropTypes.bool,
|
|
||||||
};
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -18,21 +18,23 @@ import SdkConfig from "../../../SdkConfig";
|
||||||
import { getCurrentLanguage } from "../../../languageHandler";
|
import { getCurrentLanguage } from "../../../languageHandler";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import PlatformPeg from "../../../PlatformPeg";
|
import PlatformPeg from "../../../PlatformPeg";
|
||||||
import * as sdk from '../../../index';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||||
|
import LanguageDropdown from "../elements/LanguageDropdown";
|
||||||
|
|
||||||
function onChange(newLang) {
|
function onChange(newLang: string): void {
|
||||||
if (getCurrentLanguage() !== newLang) {
|
if (getCurrentLanguage() !== newLang) {
|
||||||
SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang);
|
SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang);
|
||||||
PlatformPeg.get().reload();
|
PlatformPeg.get().reload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LanguageSelector({ disabled }) {
|
interface IProps {
|
||||||
if (SdkConfig.get()['disable_login_language_selector']) return <div />;
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown');
|
export default function LanguageSelector({ disabled }: IProps): JSX.Element {
|
||||||
|
if (SdkConfig.get()['disable_login_language_selector']) return <div />;
|
||||||
return <LanguageDropdown
|
return <LanguageDropdown
|
||||||
className="mx_AuthBody_language"
|
className="mx_AuthBody_language"
|
||||||
onOptionChange={onChange}
|
onOptionChange={onChange}
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from "../../../index";
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
import AuthPage from "./AuthPage";
|
import AuthPage from "./AuthPage";
|
||||||
import { _td } from "../../../languageHandler";
|
import { _td } from "../../../languageHandler";
|
||||||
|
@ -25,21 +25,26 @@ import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { UIFeature } from "../../../settings/UIFeature";
|
import { UIFeature } from "../../../settings/UIFeature";
|
||||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import LanguageSelector from "./LanguageSelector";
|
||||||
|
|
||||||
// translatable strings for Welcome pages
|
// translatable strings for Welcome pages
|
||||||
_td("Sign in with SSO");
|
_td("Sign in with SSO");
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.auth.Welcome")
|
@replaceableComponent("views.auth.Welcome")
|
||||||
export default class Welcome extends React.PureComponent {
|
export default class Welcome extends React.PureComponent<IProps> {
|
||||||
constructor(props) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
CountlyAnalytics.instance.track("onboarding_welcome");
|
CountlyAnalytics.instance.track("onboarding_welcome");
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
public render(): React.ReactNode {
|
||||||
const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage');
|
// FIXME: Using an import will result in wrench-element-tests failures
|
||||||
const LanguageSelector = sdk.getComponent('auth.LanguageSelector');
|
const EmbeddedPage = sdk.getComponent("structures.EmbeddedPage");
|
||||||
|
|
||||||
const pagesConfig = SdkConfig.get().embeddedPages;
|
const pagesConfig = SdkConfig.get().embeddedPages;
|
||||||
let pageUrl = null;
|
let pageUrl = null;
|
|
@ -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} />
|
||||||
);
|
);
|
||||||
|
|
|
@ -106,8 +106,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>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue