Merge branch 'develop' of https://github.com/matrix-org/matrix-react-sdk into export-conversations

This commit is contained in:
Jaiwanth 2021-07-26 00:18:56 +05:30
commit b04bfeda33
196 changed files with 5000 additions and 3423 deletions

View file

@ -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";

View file

@ -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 {

View file

@ -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;

View file

@ -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 {

View file

@ -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;

View file

@ -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;

View file

@ -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 {

View file

@ -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;
}
}
} }

View file

@ -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;
} }

View 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;
}
}
}
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;
} }
} }

View file

@ -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;
}

View file

@ -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;
} }
} }

View file

@ -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:

View file

@ -26,6 +26,7 @@ $left-gutter: 64px;
> .mx_EventTile_avatar { > .mx_EventTile_avatar {
position: absolute; position: absolute;
z-index: 9;
} }
.mx_MessageTimestamp { .mx_MessageTimestamp {

View file

@ -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 {

View file

@ -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;

View file

@ -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;
} }

View file

@ -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;
}
} }

View file

@ -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 {

View file

@ -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

View file

@ -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;
}

View file

@ -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;

View file

@ -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;

View file

@ -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.

View file

@ -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;
} }

View file

@ -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 {

View file

@ -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) {

View file

@ -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());

View file

@ -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)) {

View file

@ -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}

View file

@ -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,
); );
}; };

View file

@ -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>

View file

@ -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("Well generate a Security Key for you to store somewhere safe, like a password manager or a safe.") }</div> <div>{ _t("Well 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}
> >

View file

@ -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>

View file

@ -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}>

View 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();
}
}

View file

@ -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*

View file

@ -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) {

View 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;
}
}

View file

@ -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;
} }
} }

View file

@ -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}>

View file

@ -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>
); );

View file

@ -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>,
); );
} }

View file

@ -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>,
); );

View file

@ -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') }

View file

@ -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 {

View file

@ -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.') }

View file

@ -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}

View file

@ -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

View file

@ -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>;
}; };

View file

@ -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}

View file

@ -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>;
} }

View file

@ -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 }

View file

@ -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>;

View file

@ -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'>
&nbsp; { /* easiest way to introduce a gap between the components */ } <DurationClock playback={this.props.playback} />
{ this.renderFileSize() } &nbsp; { /* 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>;
} }
} }

View 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> }
</>;
}
}

View file

@ -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;

View file

@ -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";

View file

@ -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";

View file

@ -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

View file

@ -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 {

View file

@ -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 {

View file

@ -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>
);
} }
} }

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { 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";

View file

@ -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>;
} }

View file

@ -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>;

View file

@ -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>

View file

@ -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 />

View file

@ -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>;

View file

@ -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">

View file

@ -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>;

View file

@ -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,
};

View file

@ -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}
/> />

View file

@ -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}

View file

@ -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;

View file

@ -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} />
); );

View file

@ -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} />
); );
} }
} }

View file

@ -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}

View file

@ -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>

View file

@ -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">

View file

@ -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")}
/>; />;
} }

View file

@ -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>
</>} </>}

View file

@ -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>

View file

@ -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 (

View file

@ -71,13 +71,16 @@ const BetaFeedbackDialog: React.FC<IProps> = ({ featureId, onFinished }) => {
&nbsp; &nbsp;
{ _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>

View file

@ -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'
> >

View file

@ -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>

View file

@ -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> />
); );
} }
} }

View file

@ -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'
> >

View file

@ -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

View file

@ -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}

View file

@ -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>,
},
) }
&nbsp;
{ _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>,
},
) }
&nbsp;
{ _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.",
) }
&nbsp;
{ _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 }

View file

@ -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