mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-15 20:54:59 +08:00
Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/17021
This commit is contained in:
commit
90538c95aa
@ -15,7 +15,6 @@ module.exports = {
|
||||
"prefer-promise-reject-errors": "off",
|
||||
"no-async-promise-executor": "off",
|
||||
"quotes": "off",
|
||||
"indent": "off",
|
||||
},
|
||||
|
||||
overrides: [{
|
||||
|
@ -28,7 +28,7 @@ Platform Targets:
|
||||
* WebRTC features (VoIP and Video calling) are only available in Chrome & Firefox.
|
||||
* Mobile Web is not currently a target platform - instead please use the native
|
||||
iOS (https://github.com/matrix-org/matrix-ios-kit) and Android
|
||||
(https://github.com/matrix-org/matrix-android-sdk) SDKs.
|
||||
(https://github.com/matrix-org/matrix-android-sdk2) SDKs.
|
||||
|
||||
All code lands on the `develop` branch - `master` is only used for stable releases.
|
||||
**Please file PRs against `develop`!!**
|
||||
|
@ -162,6 +162,7 @@
|
||||
@import "./views/messages/_MStickerBody.scss";
|
||||
@import "./views/messages/_MTextBody.scss";
|
||||
@import "./views/messages/_MVideoBody.scss";
|
||||
@import "./views/messages/_MVoiceMessageBody.scss";
|
||||
@import "./views/messages/_MessageActionBar.scss";
|
||||
@import "./views/messages/_MessageTimestamp.scss";
|
||||
@import "./views/messages/_MjolnirBody.scss";
|
||||
|
@ -79,6 +79,10 @@ $activeBorderColor: $secondary-fg-color;
|
||||
.mx_SpaceItem {
|
||||
display: inline-flex;
|
||||
flex-flow: wrap;
|
||||
|
||||
&.mx_SpaceItem_narrow {
|
||||
align-self: baseline;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SpaceItem.collapsed {
|
||||
@ -275,7 +279,7 @@ $activeBorderColor: $secondary-fg-color;
|
||||
.mx_SpaceButton:hover,
|
||||
.mx_SpaceButton:focus-within,
|
||||
.mx_SpaceButton_hasMenuOpen {
|
||||
&:not(.mx_SpaceButton_home) {
|
||||
&:not(.mx_SpaceButton_home):not(.mx_SpaceButton_invite) {
|
||||
// Hide the badge container on hover because it'll be a menu button
|
||||
.mx_SpacePanel_badgeContainer {
|
||||
width: 0;
|
||||
|
@ -101,7 +101,7 @@ limitations under the License.
|
||||
|
||||
.mx_BaseAvatar {
|
||||
display: inline-flex;
|
||||
margin: 5px 16px 5px 5px;
|
||||
margin: auto 16px auto 5px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@ -160,31 +160,32 @@ limitations under the License.
|
||||
}
|
||||
}
|
||||
|
||||
.mx_AddExistingToSpaceDialog_errorText {
|
||||
font-weight: $font-semi-bold;
|
||||
font-size: $font-12px;
|
||||
line-height: $font-15px;
|
||||
color: $notice-primary-color;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.mx_AddExistingToSpace {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.mx_AddExistingToSpaceDialog_footer {
|
||||
display: flex;
|
||||
margin-top: 32px;
|
||||
margin-top: 20px;
|
||||
|
||||
> span {
|
||||
flex-grow: 1;
|
||||
font-size: $font-14px;
|
||||
font-size: $font-12px;
|
||||
line-height: $font-15px;
|
||||
font-weight: $font-semi-bold;
|
||||
color: $secondary-fg-color;
|
||||
|
||||
.mx_AccessibleButton {
|
||||
font-size: inherit;
|
||||
display: inline-block;
|
||||
.mx_ProgressBar {
|
||||
height: 8px;
|
||||
width: 100%;
|
||||
|
||||
@mixin ProgressBarBorderRadius 8px;
|
||||
}
|
||||
|
||||
.mx_AddExistingToSpaceDialog_progressText {
|
||||
margin-top: 8px;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-24px;
|
||||
color: $primary-fg-color;
|
||||
}
|
||||
|
||||
> * {
|
||||
@ -192,8 +193,54 @@ limitations under the License.
|
||||
}
|
||||
}
|
||||
|
||||
.mx_AddExistingToSpaceDialog_error {
|
||||
padding-left: 12px;
|
||||
|
||||
> img {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.mx_AddExistingToSpaceDialog_errorHeading {
|
||||
font-weight: $font-semi-bold;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-18px;
|
||||
color: $notice-primary-color;
|
||||
}
|
||||
|
||||
.mx_AddExistingToSpaceDialog_errorCaption {
|
||||
margin-top: 4px;
|
||||
font-size: $font-12px;
|
||||
line-height: $font-15px;
|
||||
color: $primary-fg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_AccessibleButton {
|
||||
display: inline-block;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton_kind_primary {
|
||||
padding: 8px 36px;
|
||||
}
|
||||
|
||||
.mx_AddExistingToSpaceDialog_retryButton {
|
||||
margin-left: 12px;
|
||||
padding-left: 24px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
background-color: $primary-fg-color;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
mask-image: url('$(res)/img/element-icons/retry.svg');
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_AccessibleButton_kind_link {
|
||||
|
@ -21,7 +21,7 @@ progress.mx_ProgressBar {
|
||||
appearance: none;
|
||||
border: none;
|
||||
|
||||
@mixin ProgressBarBorderRadius "6px";
|
||||
@mixin ProgressBarBorderRadius 6px;
|
||||
@mixin ProgressBarColour $progressbar-fg-color;
|
||||
@mixin ProgressBarBgColour $progressbar-bg-color;
|
||||
::-webkit-progress-value {
|
||||
|
19
res/css/views/messages/_MVoiceMessageBody.scss
Normal file
19
res/css/views/messages/_MVoiceMessageBody.scss
Normal file
@ -0,0 +1,19 @@
|
||||
/*
|
||||
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_MVoiceMessageBody {
|
||||
display: inline-block; // makes the playback controls magically line up
|
||||
}
|
@ -34,6 +34,10 @@ limitations under the License.
|
||||
border-color: $reaction-row-button-selected-border-color;
|
||||
}
|
||||
|
||||
&.mx_AccessibleButton_disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.mx_ReactionsRowButton_content {
|
||||
max-width: 100px;
|
||||
overflow: hidden;
|
||||
|
@ -39,14 +39,14 @@ limitations under the License.
|
||||
width: 14px; // w&h are size of icon
|
||||
height: 18px;
|
||||
vertical-align: middle;
|
||||
margin-right: 7px; // distance from left edge of waveform container (container has some margin too)
|
||||
background-color: $muted-fg-color;
|
||||
margin-right: 11px; // distance from left edge of waveform container (container has some margin too)
|
||||
background-color: $voice-record-icon-color;
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
mask-image: url('$(res)/img/element-icons/trashcan.svg');
|
||||
}
|
||||
|
||||
.mx_VoiceMessagePrimaryContainer {
|
||||
.mx_VoiceRecordComposerTile_recording.mx_VoiceMessagePrimaryContainer {
|
||||
// Note: remaining class properties are in the PlayerContainer CSS.
|
||||
|
||||
margin: 6px; // force the composer area to put a gutter around us
|
||||
@ -55,7 +55,9 @@ limitations under the License.
|
||||
position: relative; // important for the live circle
|
||||
|
||||
&.mx_VoiceRecordComposerTile_recording {
|
||||
padding-left: 16px; // +10px for the live circle, +6px for regular padding
|
||||
// We are putting the circle in this padding, so we need +10px from the regular
|
||||
// padding on the left side.
|
||||
padding-left: 22px;
|
||||
|
||||
&::before {
|
||||
animation: recording-pulse 2s infinite;
|
||||
@ -65,8 +67,8 @@ limitations under the License.
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 16px; // vertically center
|
||||
left: 12px; // 12px from the left edge for container padding
|
||||
top: 18px; // vertically center (middle align with clock)
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
@ -22,3 +22,34 @@ limitations under the License.
|
||||
.mx_HelpUserSettingsTab span.mx_AccessibleButton {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.mx_HelpUserSettingsTab code {
|
||||
word-break: break-all;
|
||||
user-select: all;
|
||||
}
|
||||
|
||||
.mx_HelpUserSettingsTab_accessToken {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-radius: 5px;
|
||||
border: solid 1px $light-fg-color;
|
||||
margin-bottom: 10px;
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.mx_HelpUserSettingsTab_accessToken_copy {
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
margin-left: 20px;
|
||||
display: inherit;
|
||||
}
|
||||
|
||||
.mx_HelpUserSettingsTab_accessToken_copy > div {
|
||||
mask-image: url($copy-button-url);
|
||||
background-color: $message-action-bar-fg-color;
|
||||
margin-left: 5px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
@ -19,12 +19,12 @@ limitations under the License.
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 32px;
|
||||
background-color: $primary-bg-color;
|
||||
background-color: $voice-playback-button-bg-color;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute; // sizing varies by icon
|
||||
background-color: $muted-fg-color;
|
||||
background-color: $voice-playback-button-fg-color;
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
}
|
||||
|
@ -19,8 +19,9 @@ limitations under the License.
|
||||
|
||||
// Container for live recording and playback controls
|
||||
.mx_VoiceMessagePrimaryContainer {
|
||||
padding: 6px; // makes us 4px taller than the send/stop button
|
||||
padding-right: 5px; // there's 1px from the waveform itself, so account for that
|
||||
// 7px top and bottom for visual design. 12px left & right, but the waveform (right)
|
||||
// has a 1px padding on it that we want to account for.
|
||||
padding: 7px 12px 7px 11px;
|
||||
background-color: $voice-record-waveform-bg-color;
|
||||
border-radius: 12px;
|
||||
|
||||
@ -30,11 +31,9 @@ limitations under the License.
|
||||
|
||||
color: $voice-record-waveform-fg-color;
|
||||
font-size: $font-14px;
|
||||
line-height: $font-24px;
|
||||
|
||||
.mx_Waveform {
|
||||
// We want the bars to be 2px shorter than the play/pause button in the waveform control
|
||||
height: 28px; // default is 30px, so we're subtracting the 2px border off the bars
|
||||
|
||||
.mx_Waveform_bar {
|
||||
background-color: $voice-record-waveform-incomplete-fg-color;
|
||||
|
||||
@ -47,8 +46,8 @@ limitations under the License.
|
||||
}
|
||||
|
||||
.mx_Clock {
|
||||
padding-right: 4px; // isolate from waveform
|
||||
padding-left: 8px; // isolate from live circle
|
||||
width: 40px; // we're not using a monospace font, so fake it
|
||||
width: 42px; // we're not using a monospace font, so fake it
|
||||
padding-right: 6px; // with the fixed width this ends up as a visual 8px most of the time, as intended.
|
||||
padding-left: 8px; // isolate from recording circle / play control
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ $header-panel-text-primary-color: #B9BEC6;
|
||||
$header-panel-text-secondary-color: #c8c8cd;
|
||||
$text-primary-color: #ffffff;
|
||||
$text-secondary-color: #B9BEC6;
|
||||
$quaternary-fg-color: #6F7882;
|
||||
$search-bg-color: #181b21;
|
||||
$search-placeholder-color: #61708b;
|
||||
$room-highlight-color: #343a46;
|
||||
@ -42,6 +43,14 @@ $preview-bar-bg-color: $header-panel-bg-color;
|
||||
$groupFilterPanel-bg-color: rgba(38, 39, 43, 0.82);
|
||||
$inverted-bg-color: $base-color;
|
||||
|
||||
$voice-record-stop-border-color: $quaternary-fg-color;
|
||||
$voice-record-waveform-bg-color: #394049; // "Dark Tile"
|
||||
$voice-record-waveform-fg-color: $secondary-fg-color;
|
||||
$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color;
|
||||
$voice-record-icon-color: $quaternary-fg-color;
|
||||
$voice-playback-button-bg-color: $tertiary-fg-color;
|
||||
$voice-playback-button-fg-color: #21262C; // "Separator"
|
||||
|
||||
// used by AddressSelector
|
||||
$selected-color: $room-highlight-color;
|
||||
|
||||
|
@ -124,6 +124,15 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%)
|
||||
|
||||
$groupFilterPanel-divider-color: $roomlist-header-color;
|
||||
|
||||
// See non-legacy dark for variable information
|
||||
$voice-record-stop-border-color: #6F7882;
|
||||
$voice-record-waveform-bg-color: #394049;
|
||||
$voice-record-waveform-fg-color: $secondary-fg-color;
|
||||
$voice-record-waveform-incomplete-fg-color: #6F7882;
|
||||
$voice-record-icon-color: #6F7882;
|
||||
$voice-playback-button-bg-color: $tertiary-fg-color;
|
||||
$voice-playback-button-fg-color: #21262C;
|
||||
|
||||
$roomtile-preview-color: #9e9e9e;
|
||||
$roomtile-default-badge-bg-color: #61708b;
|
||||
$roomtile-selected-bg-color: #1A1D23;
|
||||
|
@ -192,12 +192,15 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%)
|
||||
$groupFilterPanel-divider-color: $roomlist-header-color;
|
||||
|
||||
// See non-legacy _light for variable information
|
||||
$voice-record-stop-border-color: #E3E8F0;
|
||||
$voice-record-stop-symbol-color: #ff4b55;
|
||||
$voice-record-waveform-bg-color: #E3E8F0;
|
||||
$voice-record-waveform-fg-color: $muted-fg-color;
|
||||
$voice-record-waveform-incomplete-fg-color: #C1C6CD;
|
||||
$voice-record-live-circle-color: #ff4b55;
|
||||
$voice-record-stop-border-color: #E3E8F0;
|
||||
$voice-record-waveform-bg-color: #E3E8F0;
|
||||
$voice-record-waveform-fg-color: $secondary-fg-color;
|
||||
$voice-record-waveform-incomplete-fg-color: #C1C6CD;
|
||||
$voice-record-icon-color: $tertiary-fg-color;
|
||||
$voice-playback-button-bg-color: $primary-bg-color;
|
||||
$voice-playback-button-fg-color: $secondary-fg-color;
|
||||
|
||||
$roomtile-preview-color: #9e9e9e;
|
||||
$roomtile-default-badge-bg-color: #61708b;
|
||||
|
@ -21,6 +21,7 @@ $notice-primary-bg-color: rgba(255, 75, 85, 0.16);
|
||||
$primary-fg-color: #2e2f32;
|
||||
$secondary-fg-color: #737D8C;
|
||||
$tertiary-fg-color: #8D99A5;
|
||||
$quaternary-fg-color: #C1C6CD;
|
||||
$header-panel-bg-color: #f3f8fd;
|
||||
|
||||
// typical text (dark-on-white in light skin)
|
||||
@ -182,12 +183,18 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%)
|
||||
|
||||
$groupFilterPanel-divider-color: $roomlist-header-color;
|
||||
|
||||
$voice-record-stop-border-color: #E3E8F0;
|
||||
$voice-record-stop-symbol-color: #ff4b55; // $warning-color, but without letting people change it in themes
|
||||
$voice-record-waveform-bg-color: #E3E8F0;
|
||||
$voice-record-waveform-fg-color: $muted-fg-color;
|
||||
$voice-record-waveform-incomplete-fg-color: #C1C6CD;
|
||||
$voice-record-live-circle-color: #ff4b55; // $warning-color, but without letting people change it in themes
|
||||
// These two don't change between themes. They are the $warning-color, but we don't
|
||||
// want custom themes to affect them by accident.
|
||||
$voice-record-stop-symbol-color: #ff4b55;
|
||||
$voice-record-live-circle-color: #ff4b55;
|
||||
|
||||
$voice-record-stop-border-color: #E3E8F0; // "Separator"
|
||||
$voice-record-waveform-bg-color: #E3E8F0; // "Separator"
|
||||
$voice-record-waveform-fg-color: $secondary-fg-color;
|
||||
$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color;
|
||||
$voice-record-icon-color: $tertiary-fg-color;
|
||||
$voice-playback-button-bg-color: $primary-bg-color;
|
||||
$voice-playback-button-fg-color: $secondary-fg-color;
|
||||
|
||||
$roomtile-preview-color: $secondary-fg-color;
|
||||
$roomtile-default-badge-bg-color: #61708b;
|
||||
|
@ -148,13 +148,15 @@ function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog(
|
||||
'Failed to add the following room to the group',
|
||||
'', ErrorDialog,
|
||||
{
|
||||
title: _t(
|
||||
"Failed to add the following rooms to %(groupId)s:",
|
||||
{groupId},
|
||||
),
|
||||
description: errorList.join(", "),
|
||||
});
|
||||
'',
|
||||
ErrorDialog,
|
||||
{
|
||||
title: _t(
|
||||
"Failed to add the following rooms to %(groupId)s:",
|
||||
{groupId},
|
||||
),
|
||||
description: errorList.join(", "),
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -163,7 +163,7 @@ export default class IdentityAuthClient {
|
||||
</div>
|
||||
),
|
||||
button: _t("Trust"),
|
||||
});
|
||||
});
|
||||
const [confirmed] = await finished;
|
||||
if (confirmed) {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
|
@ -54,7 +54,7 @@ export default class PasswordReset {
|
||||
return res;
|
||||
}, function(err) {
|
||||
if (err.errcode === 'M_THREEPID_NOT_FOUND') {
|
||||
err.message = _t('This email address was not found');
|
||||
err.message = _t('This email address was not found');
|
||||
} else if (err.httpStatus) {
|
||||
err.message = err.message + ` (Status ${err.httpStatus})`;
|
||||
}
|
||||
|
@ -547,17 +547,23 @@ function textForMjolnirEvent(event) {
|
||||
|
||||
// else the entity !== prevEntity - count as a removal & add
|
||||
if (USER_RULE_TYPES.includes(event.getType())) {
|
||||
return _t("%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching " +
|
||||
return _t(
|
||||
"%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching " +
|
||||
"%(newGlob)s for %(reason)s",
|
||||
{senderName, oldGlob: prevEntity, newGlob: entity, reason});
|
||||
{senderName, oldGlob: prevEntity, newGlob: entity, reason},
|
||||
);
|
||||
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
|
||||
return _t("%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching " +
|
||||
return _t(
|
||||
"%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching " +
|
||||
"%(newGlob)s for %(reason)s",
|
||||
{senderName, oldGlob: prevEntity, newGlob: entity, reason});
|
||||
{senderName, oldGlob: prevEntity, newGlob: entity, reason},
|
||||
);
|
||||
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
|
||||
return _t("%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching " +
|
||||
return _t(
|
||||
"%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching " +
|
||||
"%(newGlob)s for %(reason)s",
|
||||
{senderName, oldGlob: prevEntity, newGlob: entity, reason});
|
||||
{senderName, oldGlob: prevEntity, newGlob: entity, reason},
|
||||
);
|
||||
}
|
||||
|
||||
// Unknown type. We'll say something but we shouldn't end up here.
|
||||
|
@ -310,7 +310,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return <form onSubmit={this._onPassPhraseConfirmNextClick}>
|
||||
<p>{_t(
|
||||
"Please enter your Security Phrase a second time to confirm.",
|
||||
"Enter your Security Phrase a second time to confirm it.",
|
||||
)}</p>
|
||||
<div className="mx_CreateKeyBackupDialog_primaryContainer">
|
||||
<div className="mx_CreateKeyBackupDialog_passPhraseContainer">
|
||||
@ -498,9 +498,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||
title={this._titleForPhase(this.state.phase)}
|
||||
hasCancel={[PHASE_PASSPHRASE, PHASE_DONE].includes(this.state.phase)}
|
||||
>
|
||||
<div>
|
||||
{content}
|
||||
</div>
|
||||
<div>
|
||||
{content}
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
|
@ -647,7 +647,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||
}
|
||||
return <form onSubmit={this._onPassPhraseConfirmNextClick}>
|
||||
<p>{_t(
|
||||
"Enter your recovery passphrase a second time to confirm it.",
|
||||
"Enter your Security Phrase a second time to confirm it.",
|
||||
)}</p>
|
||||
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
|
||||
<Field
|
||||
@ -655,7 +655,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||
onChange={this._onPassPhraseConfirmChange}
|
||||
value={this.state.passPhraseConfirm}
|
||||
className="mx_CreateSecretStorageDialog_passPhraseField"
|
||||
label={_t("Confirm your recovery passphrase")}
|
||||
label={_t("Confirm your Security Phrase")}
|
||||
autoFocus={true}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
@ -856,9 +856,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||
hasCancel={this.props.hasCancel && [PHASE_PASSPHRASE].includes(this.state.phase)}
|
||||
fixedWidth={false}
|
||||
>
|
||||
<div>
|
||||
{content}
|
||||
</div>
|
||||
<div>
|
||||
{content}
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
|
@ -170,8 +170,11 @@ export default class ExportE2eKeysDialog extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
<div className='mx_Dialog_buttons'>
|
||||
<input className='mx_Dialog_primary' type='submit' value={_t('Export')}
|
||||
disabled={disableForm}
|
||||
<input
|
||||
className='mx_Dialog_primary'
|
||||
type='submit'
|
||||
value={_t('Export')}
|
||||
disabled={disableForm}
|
||||
/>
|
||||
<button onClick={this._onCancelClick} disabled={disableForm}>
|
||||
{ _t("Cancel") }
|
||||
|
@ -140,36 +140,36 @@ export default class ImportE2eKeysDialog extends React.Component {
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputTable'>
|
||||
<div className='mx_E2eKeysDialog_inputRow'>
|
||||
<div className='mx_E2eKeysDialog_inputLabel'>
|
||||
<label htmlFor='importFile'>
|
||||
{ _t("File to import") }
|
||||
</label>
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputCell'>
|
||||
<input
|
||||
ref={this._file}
|
||||
id='importFile'
|
||||
type='file'
|
||||
autoFocus={true}
|
||||
onChange={this._onFormChange}
|
||||
disabled={disableForm} />
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputLabel'>
|
||||
<label htmlFor='importFile'>
|
||||
{ _t("File to import") }
|
||||
</label>
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputCell'>
|
||||
<input
|
||||
ref={this._file}
|
||||
id='importFile'
|
||||
type='file'
|
||||
autoFocus={true}
|
||||
onChange={this._onFormChange}
|
||||
disabled={disableForm} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputRow'>
|
||||
<div className='mx_E2eKeysDialog_inputLabel'>
|
||||
<label htmlFor='passphrase'>
|
||||
{ _t("Enter passphrase") }
|
||||
</label>
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputCell'>
|
||||
<input
|
||||
ref={this._passphrase}
|
||||
id='passphrase'
|
||||
size='64'
|
||||
type='password'
|
||||
onChange={this._onFormChange}
|
||||
disabled={disableForm} />
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputLabel'>
|
||||
<label htmlFor='passphrase'>
|
||||
{ _t("Enter passphrase") }
|
||||
</label>
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputCell'>
|
||||
<input
|
||||
ref={this._passphrase}
|
||||
id='passphrase'
|
||||
size='64'
|
||||
type='password'
|
||||
onChange={this._onFormChange}
|
||||
disabled={disableForm} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -200,10 +200,10 @@ class FilePanel extends React.Component {
|
||||
previousPhase={RightPanelPhases.RoomSummary}
|
||||
>
|
||||
<div className="mx_RoomView_empty">
|
||||
{ _t("You must <a>register</a> to use this functionality",
|
||||
{},
|
||||
{ 'a': (sub) => <a href="#/register" key="sub">{ sub }</a> })
|
||||
}
|
||||
{ _t("You must <a>register</a> to use this functionality",
|
||||
{},
|
||||
{ 'a': (sub) => <a href="#/register" key="sub">{ sub }</a> })
|
||||
}
|
||||
</div>
|
||||
</BaseCard>;
|
||||
} else if (this.noRoom) {
|
||||
|
@ -160,17 +160,17 @@ class GroupFilterPanel extends React.Component {
|
||||
type="draggable-TagTile"
|
||||
>
|
||||
{ (provided, snapshot) => (
|
||||
<div
|
||||
className="mx_GroupFilterPanel_tagTileContainer"
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
{ this.renderGlobalIcon() }
|
||||
{ tags }
|
||||
<div>
|
||||
{createButton}
|
||||
</div>
|
||||
{ provided.placeholder }
|
||||
<div
|
||||
className="mx_GroupFilterPanel_tagTileContainer"
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
{ this.renderGlobalIcon() }
|
||||
{ tags }
|
||||
<div>
|
||||
{createButton}
|
||||
</div>
|
||||
{ provided.placeholder }
|
||||
</div>
|
||||
) }
|
||||
</Droppable>
|
||||
</AutoHideScrollbar>
|
||||
|
@ -43,7 +43,7 @@ import {mediaFromMxc} from "../../customisations/Media";
|
||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||
|
||||
const LONG_DESC_PLACEHOLDER = _td(
|
||||
`<h1>HTML for your community's page</h1>
|
||||
`<h1>HTML for your community's page</h1>
|
||||
<p>
|
||||
Use the long description to introduce new members to the community, or distribute
|
||||
some important <a href="foo">links</a>
|
||||
@ -110,14 +110,16 @@ class CategoryRoomList extends React.Component {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog(
|
||||
'Failed to add the following room to the group summary',
|
||||
'', ErrorDialog,
|
||||
{
|
||||
title: _t(
|
||||
"Failed to add the following rooms to the summary of %(groupId)s:",
|
||||
{groupId: this.props.groupId},
|
||||
),
|
||||
description: errorList.join(", "),
|
||||
});
|
||||
'',
|
||||
ErrorDialog,
|
||||
{
|
||||
title: _t(
|
||||
"Failed to add the following rooms to the summary of %(groupId)s:",
|
||||
{groupId: this.props.groupId},
|
||||
),
|
||||
description: errorList.join(", "),
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
|
||||
@ -146,8 +148,8 @@ class CategoryRoomList extends React.Component {
|
||||
let catHeader = <div />;
|
||||
if (this.props.category && this.props.category.profile) {
|
||||
catHeader = <div className="mx_GroupView_featuredThings_category">
|
||||
{ this.props.category.profile.name }
|
||||
</div>;
|
||||
{ this.props.category.profile.name }
|
||||
</div>;
|
||||
}
|
||||
return <div className="mx_GroupView_featuredThings_container">
|
||||
{ catHeader }
|
||||
@ -190,13 +192,14 @@ class FeaturedRoom extends React.Component {
|
||||
Modal.createTrackedDialog(
|
||||
'Failed to remove room from group summary',
|
||||
'', ErrorDialog,
|
||||
{
|
||||
title: _t(
|
||||
"Failed to remove the room from the summary of %(groupId)s",
|
||||
{groupId: this.props.groupId},
|
||||
),
|
||||
description: _t("The room '%(roomName)s' could not be removed from the summary.", {roomName}),
|
||||
});
|
||||
{
|
||||
title: _t(
|
||||
"Failed to remove the room from the summary of %(groupId)s",
|
||||
{groupId: this.props.groupId},
|
||||
),
|
||||
description: _t("The room '%(roomName)s' could not be removed from the summary.", {roomName}),
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@ -283,13 +286,14 @@ class RoleUserList extends React.Component {
|
||||
Modal.createTrackedDialog(
|
||||
'Failed to add the following users to the community summary',
|
||||
'', ErrorDialog,
|
||||
{
|
||||
title: _t(
|
||||
"Failed to add the following users to the summary of %(groupId)s:",
|
||||
{groupId: this.props.groupId},
|
||||
),
|
||||
description: errorList.join(", "),
|
||||
});
|
||||
{
|
||||
title: _t(
|
||||
"Failed to add the following users to the summary of %(groupId)s:",
|
||||
{groupId: this.props.groupId},
|
||||
),
|
||||
description: errorList.join(", "),
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
|
||||
@ -299,11 +303,11 @@ class RoleUserList extends React.Component {
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const addButton = this.props.editing ?
|
||||
(<AccessibleButton className="mx_GroupView_featuredThings_addButton" onClick={this.onAddUsersClicked}>
|
||||
<TintableSvg src={require("../../../res/img/icons-create-room.svg")} width="64" height="64" />
|
||||
<div className="mx_GroupView_featuredThings_addButton_label">
|
||||
{ _t('Add a User') }
|
||||
</div>
|
||||
</AccessibleButton>) : <div />;
|
||||
<TintableSvg src={require("../../../res/img/icons-create-room.svg")} width="64" height="64" />
|
||||
<div className="mx_GroupView_featuredThings_addButton_label">
|
||||
{ _t('Add a User') }
|
||||
</div>
|
||||
</AccessibleButton>) : <div />;
|
||||
const userNodes = this.props.users.map((u) => {
|
||||
return <FeaturedUser
|
||||
key={u.user_id}
|
||||
@ -352,14 +356,16 @@ class FeaturedUser extends React.Component {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog(
|
||||
'Failed to remove user from community summary',
|
||||
'', ErrorDialog,
|
||||
{
|
||||
title: _t(
|
||||
"Failed to remove a user from the summary of %(groupId)s",
|
||||
{groupId: this.props.groupId},
|
||||
),
|
||||
description: _t("The user '%(displayName)s' could not be removed from the summary.", {displayName}),
|
||||
});
|
||||
'',
|
||||
ErrorDialog,
|
||||
{
|
||||
title: _t(
|
||||
"Failed to remove a user from the summary of %(groupId)s",
|
||||
{groupId: this.props.groupId},
|
||||
),
|
||||
description: _t("The user '%(displayName)s' could not be removed from the summary.", {displayName}),
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@ -767,8 +773,8 @@ export default class GroupView extends React.Component {
|
||||
title: _t("Leave Community"),
|
||||
description: (
|
||||
<span>
|
||||
{ _t("Leave %(groupName)s?", {groupName: this.props.groupId}) }
|
||||
{ warnings }
|
||||
{ _t("Leave %(groupName)s?", {groupName: this.props.groupId}) }
|
||||
{ warnings }
|
||||
</span>
|
||||
),
|
||||
button: _t("Leave"),
|
||||
@ -1055,10 +1061,11 @@ export default class GroupView extends React.Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
const membershipButtonClasses = classnames([
|
||||
'mx_RoomHeader_textButton',
|
||||
'mx_GroupView_textButton',
|
||||
],
|
||||
const membershipButtonClasses = classnames(
|
||||
[
|
||||
'mx_RoomHeader_textButton',
|
||||
'mx_GroupView_textButton',
|
||||
],
|
||||
membershipButtonExtraClasses,
|
||||
);
|
||||
|
||||
|
@ -347,7 +347,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||
if (element) {
|
||||
classes = element.classList;
|
||||
}
|
||||
} while (element && !cssClasses.some(c => classes.contains(c)));
|
||||
} while (element && (!cssClasses.some(c => classes.contains(c)) || element.offsetParent === null));
|
||||
|
||||
if (element) {
|
||||
element.focus();
|
||||
@ -416,7 +416,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||
|
||||
const roomList = <RoomList
|
||||
onKeyDown={this.onKeyDown}
|
||||
resizeNotifier={null}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
isMinimized={this.props.isMinimized}
|
||||
|
@ -427,8 +427,10 @@ export default class MessagePanel extends React.Component {
|
||||
// we get a new DOM node (restarting the animation) when the ghost
|
||||
// moves to a different event.
|
||||
return (
|
||||
<li key={"_readuptoghost_"+eventId}
|
||||
className="mx_RoomView_myReadMarker_container">
|
||||
<li
|
||||
key={"_readuptoghost_"+eventId}
|
||||
className="mx_RoomView_myReadMarker_container"
|
||||
>
|
||||
{ hr }
|
||||
</li>
|
||||
);
|
||||
@ -1014,13 +1016,13 @@ class CreationGrouper {
|
||||
|
||||
ret.push(
|
||||
<EventListSummary
|
||||
key="roomcreationsummary"
|
||||
events={this.events}
|
||||
onToggle={panel._onHeightChanged} // Update scroll state
|
||||
summaryMembers={[ev.sender]}
|
||||
summaryText={summaryText}
|
||||
key="roomcreationsummary"
|
||||
events={this.events}
|
||||
onToggle={panel._onHeightChanged} // Update scroll state
|
||||
summaryMembers={[ev.sender]}
|
||||
summaryText={summaryText}
|
||||
>
|
||||
{ eventTiles }
|
||||
{ eventTiles }
|
||||
</EventListSummary>,
|
||||
);
|
||||
|
||||
@ -1222,11 +1224,11 @@ class MemberGrouper {
|
||||
|
||||
ret.push(
|
||||
<MemberEventListSummary key={key}
|
||||
events={this.events}
|
||||
onToggle={panel._onHeightChanged} // Update scroll state
|
||||
startExpanded={highlightInMels}
|
||||
events={this.events}
|
||||
onToggle={panel._onHeightChanged} // Update scroll state
|
||||
startExpanded={highlightInMels}
|
||||
>
|
||||
{ eventTiles }
|
||||
{ eventTiles }
|
||||
</MemberEventListSummary>,
|
||||
);
|
||||
|
||||
|
@ -17,6 +17,8 @@ limitations under the License.
|
||||
import * as React from "react";
|
||||
import { createRef } from "react";
|
||||
import classNames from "classnames";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import { _t } from "../../languageHandler";
|
||||
import { ActionPayload } from "../../dispatcher/payloads";
|
||||
@ -26,7 +28,7 @@ import RoomListStore from "../../stores/room-list/RoomListStore";
|
||||
import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition";
|
||||
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
|
||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||
import SpaceStore, {UPDATE_SELECTED_SPACE} from "../../stores/SpaceStore";
|
||||
import SpaceStore, {UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES} from "../../stores/SpaceStore";
|
||||
|
||||
interface IProps {
|
||||
isMinimized: boolean;
|
||||
@ -40,6 +42,7 @@ interface IProps {
|
||||
interface IState {
|
||||
query: string;
|
||||
focused: boolean;
|
||||
inSpaces: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.RoomSearch")
|
||||
@ -54,11 +57,13 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
||||
this.state = {
|
||||
query: "",
|
||||
focused: false,
|
||||
inSpaces: false,
|
||||
};
|
||||
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
// clear filter when changing spaces, in future we may wish to maintain a filter per-space
|
||||
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.clearInput);
|
||||
SpaceStore.instance.on(UPDATE_TOP_LEVEL_SPACES, this.onSpaces);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>): void {
|
||||
@ -79,8 +84,15 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
||||
public componentWillUnmount() {
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.clearInput);
|
||||
SpaceStore.instance.off(UPDATE_TOP_LEVEL_SPACES, this.onSpaces);
|
||||
}
|
||||
|
||||
private onSpaces = (spaces: Room[]) => {
|
||||
this.setState({
|
||||
inSpaces: spaces.length > 0,
|
||||
});
|
||||
};
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
if (payload.action === 'view_room' && payload.clear_search) {
|
||||
this.clearInput();
|
||||
@ -152,6 +164,11 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
||||
'mx_RoomSearch_inputExpanded': this.state.query || this.state.focused,
|
||||
});
|
||||
|
||||
let placeholder = _t("Filter");
|
||||
if (this.state.inSpaces) {
|
||||
placeholder = _t("Filter all spaces");
|
||||
}
|
||||
|
||||
let icon = (
|
||||
<div className='mx_RoomSearch_icon' />
|
||||
);
|
||||
@ -165,7 +182,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
||||
onBlur={this.onBlur}
|
||||
onChange={this.onChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
placeholder={_t("Filter")}
|
||||
placeholder={placeholder}
|
||||
autoComplete="off"
|
||||
/>
|
||||
);
|
||||
|
@ -200,20 +200,22 @@ export default class RoomStatusBar extends React.Component {
|
||||
} else if (resourceLimitError) {
|
||||
title = messageForResourceLimitError(
|
||||
resourceLimitError.data.limit_type,
|
||||
resourceLimitError.data.admin_contact, {
|
||||
'monthly_active_user': _td(
|
||||
"Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. " +
|
||||
"Please <a>contact your service administrator</a> to continue using the service.",
|
||||
),
|
||||
'hs_disabled': _td(
|
||||
"Your message wasn't sent because this homeserver has been blocked by it's administrator. " +
|
||||
"Please <a>contact your service administrator</a> to continue using the service.",
|
||||
),
|
||||
'': _td(
|
||||
"Your message wasn't sent because this homeserver has exceeded a resource limit. " +
|
||||
"Please <a>contact your service administrator</a> to continue using the service.",
|
||||
),
|
||||
});
|
||||
resourceLimitError.data.admin_contact,
|
||||
{
|
||||
'monthly_active_user': _td(
|
||||
"Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. " +
|
||||
"Please <a>contact your service administrator</a> to continue using the service.",
|
||||
),
|
||||
'hs_disabled': _td(
|
||||
"Your message wasn't sent because this homeserver has been blocked by it's administrator. " +
|
||||
"Please <a>contact your service administrator</a> to continue using the service.",
|
||||
),
|
||||
'': _td(
|
||||
"Your message wasn't sent because this homeserver has exceeded a resource limit. " +
|
||||
"Please <a>contact your service administrator</a> to continue using the service.",
|
||||
),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
title = _t('Some of your messages have not been sent');
|
||||
}
|
||||
@ -265,7 +267,7 @@ export default class RoomStatusBar extends React.Component {
|
||||
<div role="alert">
|
||||
<div className="mx_RoomStatusBar_connectionLostBar">
|
||||
<img src={require("../../../res/img/feather-customised/warning-triangle.svg")} width="24"
|
||||
height="24" title="/!\ " alt="/!\ " />
|
||||
height="24" title="/!\ " alt="/!\ " />
|
||||
<div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||
{_t('Connectivity to the server has been lost.')}
|
||||
|
@ -190,6 +190,9 @@ export interface IState {
|
||||
rejectError?: Error;
|
||||
hasPinnedWidgets?: boolean;
|
||||
dragCounter: number;
|
||||
// whether or not a spaces context switch brought us here,
|
||||
// if it did we don't want the room to be marked as read as soon as it is loaded.
|
||||
wasContextSwitch?: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.RoomView")
|
||||
@ -326,6 +329,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
|
||||
showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId),
|
||||
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
|
||||
wasContextSwitch: RoomViewStore.getWasContextSwitch(),
|
||||
};
|
||||
|
||||
if (!initial && this.state.shouldPeek && !newState.shouldPeek) {
|
||||
@ -2014,6 +2018,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||
timelineSet={this.state.room.getUnfilteredTimelineSet()}
|
||||
showReadReceipts={this.state.showReadReceipts}
|
||||
manageReadReceipts={!this.state.isPeeking}
|
||||
sendReadReceiptOnLoad={!this.state.wasContextSwitch}
|
||||
manageReadMarkers={!this.state.isPeeking}
|
||||
hidden={hideMessagePanel}
|
||||
highlightedEventId={highlightedEventId}
|
||||
|
@ -884,16 +884,20 @@ export default class ScrollPanel extends React.Component {
|
||||
|
||||
// give the <ol> an explicit role=list because Safari+VoiceOver seems to think an ordered-list with
|
||||
// list-style-type: none; is no longer a list
|
||||
return (<AutoHideScrollbar wrappedRef={this._collectScroll}
|
||||
return (
|
||||
<AutoHideScrollbar
|
||||
wrappedRef={this._collectScroll}
|
||||
onScroll={this.onScroll}
|
||||
className={`mx_ScrollPanel ${this.props.className}`} style={this.props.style}>
|
||||
{ this.props.fixedChildren }
|
||||
<div className="mx_RoomView_messageListWrapper">
|
||||
<ol ref={this._itemlist} className="mx_RoomView_MessageList" aria-live="polite" role="list">
|
||||
{ this.props.children }
|
||||
</ol>
|
||||
</div>
|
||||
</AutoHideScrollbar>
|
||||
);
|
||||
className={`mx_ScrollPanel ${this.props.className}`}
|
||||
style={this.props.style}
|
||||
>
|
||||
{ this.props.fixedChildren }
|
||||
<div className="mx_RoomView_messageListWrapper">
|
||||
<ol ref={this._itemlist} className="mx_RoomView_MessageList" aria-live="polite" role="list">
|
||||
{ this.props.children }
|
||||
</ol>
|
||||
</div>
|
||||
</AutoHideScrollbar>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -39,6 +39,7 @@ import {mediaFromMxc} from "../../customisations/Media";
|
||||
import InfoTooltip from "../views/elements/InfoTooltip";
|
||||
import TextWithTooltip from "../views/elements/TextWithTooltip";
|
||||
import {useStateToggle} from "../../hooks/useStateToggle";
|
||||
import {getOrder} from "../../stores/SpaceStore";
|
||||
|
||||
interface IHierarchyProps {
|
||||
space: Room;
|
||||
@ -254,7 +255,11 @@ export const HierarchyLevel = ({
|
||||
const space = cli.getRoom(spaceId);
|
||||
const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId());
|
||||
|
||||
const sortedChildren = sortBy([...(relations.get(spaceId)?.values() || [])], ev => ev.content.order || null);
|
||||
const children = Array.from(relations.get(spaceId)?.values() || []);
|
||||
const sortedChildren = sortBy(children, ev => {
|
||||
// XXX: Space Summary API doesn't give the child origin_server_ts but once it does we should use it for sorting
|
||||
return getOrder(ev.content.order, null, ev.state_key);
|
||||
});
|
||||
const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => {
|
||||
const roomId = ev.state_key;
|
||||
if (!rooms.has(roomId)) return result;
|
||||
|
@ -52,7 +52,7 @@ import {useStateToggle} from "../../hooks/useStateToggle";
|
||||
import SpaceStore from "../../stores/SpaceStore";
|
||||
import FacePile from "../views/elements/FacePile";
|
||||
import {AddExistingToSpace} from "../views/dialogs/AddExistingToSpaceDialog";
|
||||
import {allSettled} from "../../utils/promise";
|
||||
import {sleep} from "../../utils/promise";
|
||||
import {calculateRoomVia} from "../../utils/permalinks/Permalinks";
|
||||
import {BetaPill} from "../views/beta/BetaCard";
|
||||
import {USER_LABS_TAB} from "../views/dialogs/UserSettingsDialog";
|
||||
@ -434,15 +434,24 @@ const SpaceAddExistingRooms = ({ space, onFinished }) => {
|
||||
let buttonLabel = _t("Skip for now");
|
||||
if (selectedToAdd.size > 0) {
|
||||
onClick = async () => {
|
||||
// TODO rate limiting
|
||||
setBusy(true);
|
||||
try {
|
||||
await allSettled(Array.from(selectedToAdd).map((room) =>
|
||||
SpaceStore.instance.addRoomToSpace(space, room.roomId, calculateRoomVia(room))));
|
||||
onFinished(true);
|
||||
} catch (e) {
|
||||
console.error("Failed to add rooms to space", e);
|
||||
setError(_t("Failed to add rooms to space"));
|
||||
|
||||
for (const room of selectedToAdd) {
|
||||
const via = calculateRoomVia(room);
|
||||
try {
|
||||
await SpaceStore.instance.addRoomToSpace(space, room.roomId, via).catch(async e => {
|
||||
if (e.errcode === "M_LIMIT_EXCEEDED") {
|
||||
await sleep(e.data.retry_after_ms);
|
||||
return SpaceStore.instance.addRoomToSpace(space, room.roomId, via); // retry
|
||||
}
|
||||
|
||||
throw e;
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Failed to add rooms to space", e);
|
||||
setError(_t("Failed to add rooms to space"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
setBusy(false);
|
||||
};
|
||||
|
@ -68,6 +68,7 @@ class TimelinePanel extends React.Component {
|
||||
showReadReceipts: PropTypes.bool,
|
||||
// Enable managing RRs and RMs. These require the timelineSet to have a room.
|
||||
manageReadReceipts: PropTypes.bool,
|
||||
sendReadReceiptOnLoad: PropTypes.bool,
|
||||
manageReadMarkers: PropTypes.bool,
|
||||
|
||||
// true to give the component a 'display: none' style.
|
||||
@ -126,6 +127,7 @@ class TimelinePanel extends React.Component {
|
||||
// event tile heights. (See _unpaginateEvents)
|
||||
timelineCap: Number.MAX_VALUE,
|
||||
className: 'mx_RoomView_messagePanel',
|
||||
sendReadReceiptOnLoad: true,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
@ -785,8 +787,10 @@ class TimelinePanel extends React.Component {
|
||||
return;
|
||||
}
|
||||
const lastDisplayedEvent = this.state.events[lastDisplayedIndex];
|
||||
this._setReadMarker(lastDisplayedEvent.getId(),
|
||||
lastDisplayedEvent.getTs());
|
||||
this._setReadMarker(
|
||||
lastDisplayedEvent.getId(),
|
||||
lastDisplayedEvent.getTs(),
|
||||
);
|
||||
|
||||
// the read-marker should become invisible, so that if the user scrolls
|
||||
// down, they don't see it.
|
||||
@ -872,7 +876,7 @@ class TimelinePanel extends React.Component {
|
||||
// The messagepanel knows where the RM is, so we must have loaded
|
||||
// the relevant event.
|
||||
this._messagePanel.current.scrollToEvent(this.state.readMarkerEventId,
|
||||
0, 1/3);
|
||||
0, 1/3);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1044,12 +1048,14 @@ class TimelinePanel extends React.Component {
|
||||
}
|
||||
if (eventId) {
|
||||
this._messagePanel.current.scrollToEvent(eventId, pixelOffset,
|
||||
offsetBase);
|
||||
offsetBase);
|
||||
} else {
|
||||
this._messagePanel.current.scrollToBottom();
|
||||
}
|
||||
|
||||
this.sendReadReceipt();
|
||||
if (this.props.sendReadReceiptOnLoad) {
|
||||
this.sendReadReceipt();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@ -1418,8 +1424,8 @@ class TimelinePanel extends React.Component {
|
||||
['PREPARED', 'CATCHUP'].includes(this.state.clientSyncState)
|
||||
);
|
||||
const events = this.state.firstVisibleEventIndex
|
||||
? this.state.events.slice(this.state.firstVisibleEventIndex)
|
||||
: this.state.events;
|
||||
? this.state.events.slice(this.state.firstVisibleEventIndex)
|
||||
: this.state.events;
|
||||
return (
|
||||
<MessagePanel
|
||||
ref={this._messagePanel}
|
||||
|
@ -169,7 +169,7 @@ export class PasswordAuthEntry extends React.Component {
|
||||
{ submitButtonOrSpinner }
|
||||
</div>
|
||||
</form>
|
||||
{ errorSection }
|
||||
{ errorSection }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -375,7 +375,7 @@ export class TermsAuthEntry extends React.Component {
|
||||
if (this.props.showContinue !== false) {
|
||||
// XXX: button classes
|
||||
submitButton = <button className="mx_InteractiveAuthEntryComponents_termsSubmit mx_GeneralButton"
|
||||
onClick={this._trySubmit} disabled={!allChecked}>{_t("Accept")}</button>;
|
||||
onClick={this._trySubmit} disabled={!allChecked}>{_t("Accept")}</button>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -350,7 +350,7 @@ export default class MessageContextMenu extends React.Component {
|
||||
>
|
||||
{ _t('Source URL') }
|
||||
</MenuItem>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.collapseReplyThread) {
|
||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useContext, useState} from "react";
|
||||
import React, {useContext, useMemo, useState} from "react";
|
||||
import classNames from "classnames";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
@ -29,11 +29,13 @@ import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import {getDisplayAliasForRoom} from "../../../Rooms";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import {allSettled} from "../../../utils/promise";
|
||||
import {sleep} from "../../../utils/promise";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import {calculateRoomVia} from "../../../utils/permalinks/Permalinks";
|
||||
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
|
||||
import ProgressBar from "../elements/ProgressBar";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
matrixClient: MatrixClient;
|
||||
@ -45,7 +47,11 @@ const Entry = ({ room, checked, onChange }) => {
|
||||
return <label className="mx_AddExistingToSpace_entry">
|
||||
<RoomAvatar room={room} height={32} width={32} />
|
||||
<span className="mx_AddExistingToSpace_entry_name">{ room.name }</span>
|
||||
<StyledCheckbox onChange={(e) => onChange(e.target.checked)} checked={checked} />
|
||||
<StyledCheckbox
|
||||
onChange={onChange ? (e) => onChange(e.target.checked) : null}
|
||||
checked={checked}
|
||||
disabled={!onChange}
|
||||
/>
|
||||
</label>;
|
||||
};
|
||||
|
||||
@ -57,6 +63,8 @@ interface IAddExistingToSpaceProps {
|
||||
|
||||
export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space, selected, onChange }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const visibleRooms = useMemo(() => sortRooms(cli.getVisibleRooms()), [cli]);
|
||||
|
||||
const [query, setQuery] = useState("");
|
||||
const lcQuery = query.toLowerCase();
|
||||
|
||||
@ -65,7 +73,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space,
|
||||
const existingRoomsSet = new Set(SpaceStore.instance.getChildRooms(space.roomId));
|
||||
|
||||
const joinRule = space.getJoinRule();
|
||||
const [spaces, rooms, dms] = cli.getVisibleRooms().reduce((arr, room) => {
|
||||
const [spaces, rooms, dms] = visibleRooms.reduce((arr, room) => {
|
||||
if (room.getMyMembership() !== "join") return arr;
|
||||
if (!room.name.toLowerCase().includes(lcQuery)) return arr;
|
||||
|
||||
@ -101,9 +109,9 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space,
|
||||
key={room.roomId}
|
||||
room={room}
|
||||
checked={selected.has(room)}
|
||||
onChange={(checked) => {
|
||||
onChange={onChange ? (checked) => {
|
||||
onChange(checked, room);
|
||||
}}
|
||||
} : null}
|
||||
/>;
|
||||
}) }
|
||||
</div>
|
||||
@ -117,9 +125,9 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space,
|
||||
key={space.roomId}
|
||||
room={space}
|
||||
checked={selected.has(space)}
|
||||
onChange={(checked) => {
|
||||
onChange={onChange ? (checked) => {
|
||||
onChange(checked, space);
|
||||
}}
|
||||
} : null}
|
||||
/>;
|
||||
}) }
|
||||
</div>
|
||||
@ -133,9 +141,9 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space,
|
||||
key={room.roomId}
|
||||
room={room}
|
||||
checked={selected.has(room)}
|
||||
onChange={(checked) => {
|
||||
onChange={onChange ? (checked) => {
|
||||
onChange(checked, room);
|
||||
}}
|
||||
} : null}
|
||||
/>;
|
||||
}) }
|
||||
</div>
|
||||
@ -153,8 +161,8 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
|
||||
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
|
||||
const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
|
||||
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [progress, setProgress] = useState<number>(null);
|
||||
const [error, setError] = useState<Error>(null);
|
||||
|
||||
let spaceOptionSection;
|
||||
if (existingSubspaces.length > 0) {
|
||||
@ -194,6 +202,82 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
|
||||
const addRooms = async () => {
|
||||
setError(null);
|
||||
setProgress(0);
|
||||
|
||||
let error;
|
||||
|
||||
for (const room of selectedToAdd) {
|
||||
const via = calculateRoomVia(room);
|
||||
try {
|
||||
await SpaceStore.instance.addRoomToSpace(space, room.roomId, via).catch(async e => {
|
||||
if (e.errcode === "M_LIMIT_EXCEEDED") {
|
||||
await sleep(e.data.retry_after_ms);
|
||||
return SpaceStore.instance.addRoomToSpace(space, room.roomId, via); // retry
|
||||
}
|
||||
|
||||
throw e;
|
||||
});
|
||||
setProgress(i => i + 1);
|
||||
} catch (e) {
|
||||
console.error("Failed to add rooms to space", e);
|
||||
setError(error = e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!error) {
|
||||
onFinished(true);
|
||||
}
|
||||
};
|
||||
|
||||
const busy = progress !== null;
|
||||
|
||||
let footer;
|
||||
if (error) {
|
||||
footer = <>
|
||||
<img
|
||||
src={require("../../../../res/img/element-icons/warning-badge.svg")}
|
||||
height="24"
|
||||
width="24"
|
||||
alt=""
|
||||
/>
|
||||
|
||||
<span className="mx_AddExistingToSpaceDialog_error">
|
||||
<div className="mx_AddExistingToSpaceDialog_errorHeading">{ _t("Not all selected were added") }</div>
|
||||
<div className="mx_AddExistingToSpaceDialog_errorCaption">{ _t("Try again") }</div>
|
||||
</span>
|
||||
|
||||
<AccessibleButton className="mx_AddExistingToSpaceDialog_retryButton" onClick={addRooms}>
|
||||
{ _t("Retry") }
|
||||
</AccessibleButton>
|
||||
</>;
|
||||
} else if (busy) {
|
||||
footer = <span>
|
||||
<ProgressBar value={progress} max={selectedToAdd.size} />
|
||||
<div className="mx_AddExistingToSpaceDialog_progressText">
|
||||
{ _t("Adding rooms... (%(progress)s out of %(count)s)", {
|
||||
count: selectedToAdd.size,
|
||||
progress,
|
||||
}) }
|
||||
</div>
|
||||
</span>;
|
||||
} else {
|
||||
footer = <>
|
||||
<span>
|
||||
<div>{ _t("Want to add a new room instead?") }</div>
|
||||
<AccessibleButton onClick={() => onCreateRoomClick(cli, space)} kind="link">
|
||||
{ _t("Create a new room") }
|
||||
</AccessibleButton>
|
||||
</span>
|
||||
|
||||
<AccessibleButton kind="primary" disabled={selectedToAdd.size < 1} onClick={addRooms}>
|
||||
{ _t("Add") }
|
||||
</AccessibleButton>
|
||||
</>;
|
||||
}
|
||||
|
||||
return <BaseDialog
|
||||
title={title}
|
||||
className="mx_AddExistingToSpaceDialog"
|
||||
@ -201,50 +285,23 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
|
||||
onFinished={onFinished}
|
||||
fixedWidth={false}
|
||||
>
|
||||
{ error && <div className="mx_AddExistingToSpaceDialog_errorText">{ error }</div> }
|
||||
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<AddExistingToSpace
|
||||
space={space}
|
||||
selected={selectedToAdd}
|
||||
onChange={(checked, room) => {
|
||||
onChange={!busy && !error ? (checked, room) => {
|
||||
if (checked) {
|
||||
selectedToAdd.add(room);
|
||||
} else {
|
||||
selectedToAdd.delete(room);
|
||||
}
|
||||
setSelectedToAdd(new Set(selectedToAdd));
|
||||
}}
|
||||
} : null}
|
||||
/>
|
||||
</MatrixClientContext.Provider>
|
||||
|
||||
<div className="mx_AddExistingToSpaceDialog_footer">
|
||||
<span>
|
||||
<div>{ _t("Don't want to add an existing room?") }</div>
|
||||
<AccessibleButton onClick={() => onCreateRoomClick(cli, space)} kind="link">
|
||||
{ _t("Create a new room") }
|
||||
</AccessibleButton>
|
||||
</span>
|
||||
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
disabled={busy || selectedToAdd.size < 1}
|
||||
onClick={async () => {
|
||||
// TODO rate limiting
|
||||
setBusy(true);
|
||||
try {
|
||||
await allSettled(Array.from(selectedToAdd).map((room) =>
|
||||
SpaceStore.instance.addRoomToSpace(space, room.roomId, calculateRoomVia(room))));
|
||||
onFinished(true);
|
||||
} catch (e) {
|
||||
console.error("Failed to add rooms to space", e);
|
||||
setError(_t("Failed to add rooms to space"));
|
||||
}
|
||||
setBusy(false);
|
||||
}}
|
||||
>
|
||||
{ busy ? _t("Adding...") : _t("Add") }
|
||||
</AccessibleButton>
|
||||
{ footer }
|
||||
</div>
|
||||
</BaseDialog>;
|
||||
};
|
||||
|
@ -184,7 +184,7 @@ export default class BugReportDialog extends React.Component {
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_BugReportDialog" onFinished={this._onCancel}
|
||||
title={_t('Submit debug logs')}
|
||||
title={_t('Submit debug logs')}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
|
@ -95,7 +95,7 @@ export default class ChangelogDialog extends React.Component {
|
||||
description={content}
|
||||
button={_t("Update")}
|
||||
onFinished={this.props.onFinished}
|
||||
/>
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -39,9 +39,12 @@ export default class ConfirmWipeDeviceDialog extends React.Component {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
||||
return (
|
||||
<BaseDialog className='mx_ConfirmWipeDeviceDialog' hasCancel={true}
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("Clear all data in this session?")}>
|
||||
<BaseDialog
|
||||
className='mx_ConfirmWipeDeviceDialog'
|
||||
hasCancel={true}
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("Clear all data in this session?")}
|
||||
>
|
||||
<div className='mx_ConfirmWipeDeviceDialog_content'>
|
||||
<p>
|
||||
{_t(
|
||||
|
@ -70,8 +70,16 @@ class GenericEditor extends React.PureComponent {
|
||||
}
|
||||
|
||||
textInput(id, label) {
|
||||
return <Field id={id} label={label} size="42" autoFocus={true} type="text" autoComplete="on"
|
||||
value={this.state[id]} onChange={this._onChange} />;
|
||||
return <Field
|
||||
id={id}
|
||||
label={label}
|
||||
size="42"
|
||||
autoFocus={true}
|
||||
type="text"
|
||||
autoComplete="on"
|
||||
value={this.state[id]}
|
||||
onChange={this._onChange}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
@ -155,7 +163,7 @@ export class SendCustomEvent extends GenericEditor {
|
||||
<br />
|
||||
|
||||
<Field id="evContent" label={_t("Event Content")} type="text" className="mx_DevTools_textarea"
|
||||
autoComplete="off" value={this.state.evContent} onChange={this._onChange} element="textarea" />
|
||||
autoComplete="off" value={this.state.evContent} onChange={this._onChange} element="textarea" />
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onBack}>{ _t('Back') }</button>
|
||||
@ -239,7 +247,7 @@ class SendAccountData extends GenericEditor {
|
||||
<br />
|
||||
|
||||
<Field id="evContent" label={_t("Event Content")} type="text" className="mx_DevTools_textarea"
|
||||
autoComplete="off" value={this.state.evContent} onChange={this._onChange} element="textarea" />
|
||||
autoComplete="off" value={this.state.evContent} onChange={this._onChange} element="textarea" />
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onBack}>{ _t('Back') }</button>
|
||||
@ -315,15 +323,15 @@ class FilteredList extends React.PureComponent {
|
||||
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
||||
return <div>
|
||||
<Field label={_t('Filter results')} autoFocus={true} size={64}
|
||||
type="text" autoComplete="off" value={this.props.query} onChange={this.onQuery}
|
||||
className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query"
|
||||
// force re-render so that autoFocus is applied when this component is re-used
|
||||
key={this.props.children[0] ? this.props.children[0].key : ''} />
|
||||
type="text" autoComplete="off" value={this.props.query} onChange={this.onQuery}
|
||||
className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query"
|
||||
// force re-render so that autoFocus is applied when this component is re-used
|
||||
key={this.props.children[0] ? this.props.children[0].key : ''} />
|
||||
|
||||
<TruncatedList getChildren={this.getChildren}
|
||||
getChildCount={this.getChildCount}
|
||||
truncateAt={this.state.truncateAt}
|
||||
createOverflowElement={this.createOverflowElement} />
|
||||
getChildCount={this.getChildCount}
|
||||
truncateAt={this.state.truncateAt}
|
||||
createOverflowElement={this.createOverflowElement} />
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
@ -647,7 +655,7 @@ function VerificationRequest({txnId, request}) {
|
||||
|
||||
/* Note that request.timeout is a getter, so its value changes */
|
||||
const id = setInterval(() => {
|
||||
setRequestTimeout(request.timeout);
|
||||
setRequestTimeout(request.timeout);
|
||||
}, 500);
|
||||
|
||||
return () => { clearInterval(id); };
|
||||
@ -941,35 +949,35 @@ class SettingsExplorer extends React.Component {
|
||||
/>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{_t("Setting ID")}</th>
|
||||
<th>{_t("Value")}</th>
|
||||
<th>{_t("Value in this room")}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{_t("Setting ID")}</th>
|
||||
<th>{_t("Value")}</th>
|
||||
<th>{_t("Value in this room")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{allSettings.map(i => (
|
||||
<tr key={i}>
|
||||
<td>
|
||||
<a href="" onClick={(e) => this.onViewClick(e, i)}>
|
||||
<code>{i}</code>
|
||||
</a>
|
||||
<a href="" onClick={(e) => this.onEditClick(e, i)}
|
||||
className='mx_DevTools_SettingsExplorer_edit'
|
||||
>
|
||||
{allSettings.map(i => (
|
||||
<tr key={i}>
|
||||
<td>
|
||||
<a href="" onClick={(e) => this.onViewClick(e, i)}>
|
||||
<code>{i}</code>
|
||||
</a>
|
||||
<a href="" onClick={(e) => this.onEditClick(e, i)}
|
||||
className='mx_DevTools_SettingsExplorer_edit'
|
||||
>
|
||||
✏
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<code>{this.renderSettingValue(SettingsStore.getValue(i))}</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>
|
||||
{this.renderSettingValue(SettingsStore.getValue(i, room.roomId))}
|
||||
</code>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<code>{this.renderSettingValue(SettingsStore.getValue(i))}</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>
|
||||
{this.renderSettingValue(SettingsStore.getValue(i, room.roomId))}
|
||||
</code>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@ -998,11 +1006,11 @@ class SettingsExplorer extends React.Component {
|
||||
<div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{_t("Level")}</th>
|
||||
<th>{_t("Settable at global")}</th>
|
||||
<th>{_t("Settable at room")}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{_t("Level")}</th>
|
||||
<th>{_t("Settable at global")}</th>
|
||||
<th>{_t("Settable at room")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{LEVEL_ORDER.map(lvl => (
|
||||
|
@ -42,9 +42,12 @@ export default class IntegrationsDisabledDialog extends React.Component {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
||||
return (
|
||||
<BaseDialog className='mx_IntegrationsDisabledDialog' hasCancel={true}
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("Integrations are disabled")}>
|
||||
<BaseDialog
|
||||
className='mx_IntegrationsDisabledDialog'
|
||||
hasCancel={true}
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("Integrations are disabled")}
|
||||
>
|
||||
<div className='mx_IntegrationsDisabledDialog_content'>
|
||||
<p>{_t("Enable 'Manage Integrations' in Settings to do this.")}</p>
|
||||
</div>
|
||||
|
@ -37,9 +37,12 @@ export default class IntegrationsImpossibleDialog extends React.Component {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
||||
return (
|
||||
<BaseDialog className='mx_IntegrationsImpossibleDialog' hasCancel={false}
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("Integrations not allowed")}>
|
||||
<BaseDialog
|
||||
className='mx_IntegrationsImpossibleDialog'
|
||||
hasCancel={false}
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("Integrations not allowed")}
|
||||
>
|
||||
<div className='mx_IntegrationsImpossibleDialog_content'>
|
||||
<p>
|
||||
{_t(
|
||||
|
@ -24,7 +24,7 @@ export default function KeySignatureUploadFailedDialog({
|
||||
source,
|
||||
continuation,
|
||||
onFinished,
|
||||
}) {
|
||||
}) {
|
||||
const RETRIES = 2;
|
||||
const BaseDialog = sdk.getComponent('dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
@ -84,10 +84,10 @@ export default function KeySignatureUploadFailedDialog({
|
||||
} else {
|
||||
body = (<div>
|
||||
{success ?
|
||||
<span>{_t("Upload completed")}</span> :
|
||||
cancelled ?
|
||||
<span>{_t("Cancelled signature upload")}</span> :
|
||||
<span>{_t("Unable to upload")}</span>}
|
||||
<span>{_t("Upload completed")}</span> :
|
||||
cancelled ?
|
||||
<span>{_t("Cancelled signature upload")}</span> :
|
||||
<span>{_t("Unable to upload")}</span>}
|
||||
<DialogButtons
|
||||
primaryButton={_t("OK")}
|
||||
hasCancel={false}
|
||||
|
@ -164,8 +164,12 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
|
||||
}
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<BaseDialog className='mx_MessageEditHistoryDialog' hasCancel={true}
|
||||
onFinished={this.props.onFinished} title={_t("Message edits")}>
|
||||
<BaseDialog
|
||||
className='mx_MessageEditHistoryDialog'
|
||||
hasCancel={true}
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("Message edits")}
|
||||
>
|
||||
{content}
|
||||
</BaseDialog>
|
||||
);
|
||||
|
@ -116,8 +116,12 @@ export default class RoomSettingsDialog extends React.Component {
|
||||
|
||||
const roomName = MatrixClientPeg.get().getRoom(this.props.roomId).name;
|
||||
return (
|
||||
<BaseDialog className='mx_RoomSettingsDialog' hasCancel={true}
|
||||
onFinished={this.props.onFinished} title={_t("Room Settings - %(roomName)s", {roomName})}>
|
||||
<BaseDialog
|
||||
className='mx_RoomSettingsDialog'
|
||||
hasCancel={true}
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("Room Settings - %(roomName)s", {roomName})}
|
||||
>
|
||||
<div className='mx_SettingsDialog_content'>
|
||||
<TabbedView tabs={this._getTabs()} />
|
||||
</div>
|
||||
|
@ -98,7 +98,7 @@ export default class SessionRestoreErrorDialog extends React.Component {
|
||||
"may be incompatible with this version. Close this window and return " +
|
||||
"to the more recent version.",
|
||||
{ brand },
|
||||
) }</p>
|
||||
) }</p>
|
||||
|
||||
<p>{ _t(
|
||||
"Clearing your browser's storage may fix the problem, but will sign you " +
|
||||
|
@ -45,10 +45,12 @@ export default class StorageEvictedDialog extends React.Component {
|
||||
let logRequest;
|
||||
if (SdkConfig.get().bug_report_endpoint_url) {
|
||||
logRequest = _t(
|
||||
"To help us prevent this in future, please <a>send us logs</a>.", {},
|
||||
{
|
||||
a: text => <a href="#" onClick={this._sendBugReport}>{text}</a>,
|
||||
});
|
||||
"To help us prevent this in future, please <a>send us logs</a>.",
|
||||
{},
|
||||
{
|
||||
a: text => <a href="#" onClick={this._sendBugReport}>{text}</a>,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -158,8 +158,12 @@ export default class UserSettingsDialog extends React.Component {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
return (
|
||||
<BaseDialog className='mx_UserSettingsDialog' hasCancel={true}
|
||||
onFinished={this.props.onFinished} title={_t("Settings")}>
|
||||
<BaseDialog
|
||||
className='mx_UserSettingsDialog'
|
||||
hasCancel={true}
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("Settings")}
|
||||
>
|
||||
<div className='mx_SettingsDialog_content'>
|
||||
<TabbedView tabs={this._getTabs()} initialTabId={this.props.initialTabId} />
|
||||
</div>
|
||||
|
@ -52,11 +52,13 @@ export default class VerificationRequestDialog extends React.Component {
|
||||
const title = request && request.isSelfVerification ?
|
||||
_t("Verify other login") : _t("Verification Request");
|
||||
|
||||
return <BaseDialog className="mx_InfoDialog" onFinished={this.props.onFinished}
|
||||
contentId="mx_Dialog_content"
|
||||
title={title}
|
||||
hasCancel={true}
|
||||
>
|
||||
return <BaseDialog
|
||||
className="mx_InfoDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
contentId="mx_Dialog_content"
|
||||
title={title}
|
||||
hasCancel={true}
|
||||
>
|
||||
<EncryptionPanel
|
||||
layout="dialog"
|
||||
verificationRequest={this.props.verificationRequest}
|
||||
|
@ -70,9 +70,12 @@ export default class WidgetOpenIDPermissionsDialog extends React.Component {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
||||
return (
|
||||
<BaseDialog className='mx_WidgetOpenIDPermissionsDialog' hasCancel={true}
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("Allow this widget to verify your identity")}>
|
||||
<BaseDialog
|
||||
className='mx_WidgetOpenIDPermissionsDialog'
|
||||
hasCancel={true}
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("Allow this widget to verify your identity")}
|
||||
>
|
||||
<div className='mx_WidgetOpenIDPermissionsDialog_content'>
|
||||
<p>
|
||||
{_t("The widget will verify your user ID, but won't be able to perform actions for you:")}
|
||||
|
@ -40,10 +40,11 @@ export default class ConfirmDestroyCrossSigningDialog extends React.Component {
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className='mx_ConfirmDestroyCrossSigningDialog'
|
||||
hasCancel={true}
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("Destroy cross-signing keys?")}>
|
||||
className='mx_ConfirmDestroyCrossSigningDialog'
|
||||
hasCancel={true}
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("Destroy cross-signing keys?")}
|
||||
>
|
||||
<div className='mx_ConfirmDestroyCrossSigningDialog_content'>
|
||||
<p>
|
||||
{_t(
|
||||
|
@ -373,21 +373,24 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
||||
{_t(
|
||||
"If you've forgotten your Security Phrase you can "+
|
||||
"<button1>use your Security Key</button1> or " +
|
||||
"<button2>set up new recovery options</button2>"
|
||||
, {}, {
|
||||
button1: s => <AccessibleButton className="mx_linkButton"
|
||||
element="span"
|
||||
onClick={this._onUseRecoveryKeyClick}
|
||||
>
|
||||
{s}
|
||||
</AccessibleButton>,
|
||||
button2: s => <AccessibleButton className="mx_linkButton"
|
||||
element="span"
|
||||
onClick={this._onResetRecoveryClick}
|
||||
>
|
||||
{s}
|
||||
</AccessibleButton>,
|
||||
})}
|
||||
"<button2>set up new recovery options</button2>",
|
||||
{},
|
||||
{
|
||||
button1: s => <AccessibleButton
|
||||
className="mx_linkButton"
|
||||
element="span"
|
||||
onClick={this._onUseRecoveryKeyClick}
|
||||
>
|
||||
{s}
|
||||
</AccessibleButton>,
|
||||
button2: s => <AccessibleButton
|
||||
className="mx_linkButton"
|
||||
element="span"
|
||||
onClick={this._onResetRecoveryClick}
|
||||
>
|
||||
{s}
|
||||
</AccessibleButton>,
|
||||
})}
|
||||
</div>;
|
||||
} else {
|
||||
title = _t("Enter Security Key");
|
||||
@ -435,15 +438,17 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
||||
</div>
|
||||
{_t(
|
||||
"If you've forgotten your Security Key you can "+
|
||||
"<button>set up new recovery options</button>"
|
||||
, {}, {
|
||||
button: s => <AccessibleButton className="mx_linkButton"
|
||||
element="span"
|
||||
onClick={this._onResetRecoveryClick}
|
||||
>
|
||||
{s}
|
||||
</AccessibleButton>,
|
||||
})}
|
||||
"<button>set up new recovery options</button>",
|
||||
{},
|
||||
{
|
||||
button: s => <AccessibleButton className="mx_linkButton"
|
||||
element="span"
|
||||
onClick={this._onResetRecoveryClick}
|
||||
>
|
||||
{s}
|
||||
</AccessibleButton>,
|
||||
},
|
||||
)}
|
||||
</div>;
|
||||
}
|
||||
|
||||
@ -452,9 +457,9 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
||||
onFinished={this.props.onFinished}
|
||||
title={title}
|
||||
>
|
||||
<div className='mx_RestoreKeyBackupDialog_content'>
|
||||
{content}
|
||||
</div>
|
||||
<div className='mx_RestoreKeyBackupDialog_content'>
|
||||
{content}
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
|
@ -71,8 +71,8 @@ export default class ActionButton extends React.Component {
|
||||
}
|
||||
|
||||
const icon = this.props.iconPath ?
|
||||
(<TintableSvg src={this.props.iconPath} width={this.props.size} height={this.props.size} />) :
|
||||
undefined;
|
||||
(<TintableSvg src={this.props.iconPath} width={this.props.size} height={this.props.size} />) :
|
||||
undefined;
|
||||
|
||||
const classNames = ["mx_RoleButton"];
|
||||
if (this.props.className) {
|
||||
|
@ -109,7 +109,7 @@ export default class AppTile extends React.Component {
|
||||
const childContentProtocol = u.protocol;
|
||||
if (parentContentProtocol === 'https:' && childContentProtocol !== 'https:') {
|
||||
console.warn("Refusing to load mixed-content app:",
|
||||
parentContentProtocol, childContentProtocol, window.location, this.props.app.url);
|
||||
parentContentProtocol, childContentProtocol, window.location, this.props.app.url);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
@ -65,12 +65,18 @@ export class EditableItem extends React.Component {
|
||||
<span className="mx_EditableItem_promptText">
|
||||
{_t("Are you sure?")}
|
||||
</span>
|
||||
<AccessibleButton onClick={this._onActuallyRemove} kind="primary_sm"
|
||||
className="mx_EditableItem_confirmBtn">
|
||||
<AccessibleButton
|
||||
onClick={this._onActuallyRemove}
|
||||
kind="primary_sm"
|
||||
className="mx_EditableItem_confirmBtn"
|
||||
>
|
||||
{_t("Yes")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton onClick={this._onDontRemove} kind="danger_sm"
|
||||
className="mx_EditableItem_confirmBtn">
|
||||
<AccessibleButton
|
||||
onClick={this._onDontRemove}
|
||||
kind="danger_sm"
|
||||
className="mx_EditableItem_confirmBtn"
|
||||
>
|
||||
{_t("No")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
@ -121,11 +127,15 @@ export default class EditableItemList extends React.Component {
|
||||
|
||||
_renderNewItemField() {
|
||||
return (
|
||||
<form onSubmit={this._onItemAdded} autoComplete="off"
|
||||
noValidate={true} className="mx_EditableItemList_newItem">
|
||||
<form
|
||||
onSubmit={this._onItemAdded}
|
||||
autoComplete="off"
|
||||
noValidate={true}
|
||||
className="mx_EditableItemList_newItem"
|
||||
>
|
||||
<Field label={this.props.placeholder} type="text"
|
||||
autoComplete="off" value={this.props.newItem || ""} onChange={this._onNewItemChanged}
|
||||
list={this.props.suggestionsListId} />
|
||||
autoComplete="off" value={this.props.newItem || ""} onChange={this._onNewItemChanged}
|
||||
list={this.props.suggestionsListId} />
|
||||
<AccessibleButton onClick={this._onItemAdded} kind="primary" type="submit" disabled={!this.props.newItem}>
|
||||
{_t("Add")}
|
||||
</AccessibleButton>
|
||||
|
@ -221,13 +221,15 @@ export default class EditableText extends React.Component {
|
||||
</div>;
|
||||
} else {
|
||||
// show the content editable div, but manually manage its contents as react and contentEditable don't play nice together
|
||||
editableEl = <div ref={this._editable_div}
|
||||
contentEditable={true}
|
||||
className={className}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onKeyUp={this.onKeyUp}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur} />;
|
||||
editableEl = <div
|
||||
ref={this._editable_div}
|
||||
contentEditable={true}
|
||||
className={className}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onKeyUp={this.onKeyUp}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
/>;
|
||||
}
|
||||
|
||||
return editableEl;
|
||||
|
@ -114,6 +114,8 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||
|
||||
componentWillUnmount() {
|
||||
this.focusLock.current.removeEventListener('wheel', this.onWheel);
|
||||
window.removeEventListener("resize", this.calculateZoom);
|
||||
this.image.current.removeEventListener("load", this.calculateZoom);
|
||||
}
|
||||
|
||||
private calculateZoom = () => {
|
||||
|
@ -46,8 +46,12 @@ export default class LabelledToggleSwitch extends React.Component {
|
||||
// This is a minimal version of a SettingsFlag
|
||||
|
||||
let firstPart = <span className="mx_SettingsFlag_label">{this.props.label}</span>;
|
||||
let secondPart = <ToggleSwitch checked={this.props.value} disabled={this.props.disabled}
|
||||
onChange={this.props.onChange} aria-label={this.props.label} />;
|
||||
let secondPart = <ToggleSwitch
|
||||
checked={this.props.value}
|
||||
disabled={this.props.disabled}
|
||||
onChange={this.props.onChange}
|
||||
aria-label={this.props.label}
|
||||
/>;
|
||||
|
||||
if (this.props.toggleInFront) {
|
||||
const temp = firstPart;
|
||||
|
@ -60,10 +60,10 @@ export default class LanguageDropdown extends React.Component {
|
||||
// doesn't know this, therefore we do this.
|
||||
const language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
|
||||
if (language) {
|
||||
this.props.onOptionChange(language);
|
||||
this.props.onOptionChange(language);
|
||||
} else {
|
||||
const language = languageHandler.normalizeLanguageKey(languageHandler.getLanguageFromBrowser());
|
||||
this.props.onOptionChange(language);
|
||||
const language = languageHandler.normalizeLanguageKey(languageHandler.getLanguageFromBrowser());
|
||||
this.props.onOptionChange(language);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -225,19 +225,19 @@ class Pill extends React.Component {
|
||||
}
|
||||
break;
|
||||
case Pill.TYPE_USER_MENTION: {
|
||||
// If this user is not a member of this room, default to the empty member
|
||||
const member = this.state.member;
|
||||
if (member) {
|
||||
userId = member.userId;
|
||||
member.rawDisplayName = member.rawDisplayName || '';
|
||||
linkText = member.rawDisplayName;
|
||||
if (this.props.shouldShowPillAvatar) {
|
||||
avatar = <MemberAvatar member={member} width={16} height={16} aria-hidden="true" />;
|
||||
}
|
||||
pillClass = 'mx_UserPill';
|
||||
href = null;
|
||||
onClick = this.onUserPillClicked;
|
||||
// If this user is not a member of this room, default to the empty member
|
||||
const member = this.state.member;
|
||||
if (member) {
|
||||
userId = member.userId;
|
||||
member.rawDisplayName = member.rawDisplayName || '';
|
||||
linkText = member.rawDisplayName;
|
||||
if (this.props.shouldShowPillAvatar) {
|
||||
avatar = <MemberAvatar member={member} width={16} height={16} aria-hidden="true" />;
|
||||
}
|
||||
pillClass = 'mx_UserPill';
|
||||
href = null;
|
||||
onClick = this.onUserPillClicked;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Pill.TYPE_ROOM_MENTION: {
|
||||
|
@ -135,9 +135,13 @@ export default class PowerSelector extends React.Component {
|
||||
if (this.state.custom) {
|
||||
picker = (
|
||||
<Field type="number"
|
||||
label={label} max={this.props.maxValue}
|
||||
onBlur={this.onCustomBlur} onKeyDown={this.onCustomKeyDown} onChange={this.onCustomChange}
|
||||
value={String(this.state.customValue)} disabled={this.props.disabled} />
|
||||
label={label} max={this.props.maxValue}
|
||||
onBlur={this.onCustomBlur}
|
||||
onKeyDown={this.onCustomKeyDown}
|
||||
onChange={this.onCustomChange}
|
||||
value={String(this.state.customValue)}
|
||||
disabled={this.props.disabled}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
// Each level must have a definition in this.state.levelRoleMap
|
||||
@ -154,8 +158,9 @@ export default class PowerSelector extends React.Component {
|
||||
|
||||
picker = (
|
||||
<Field element="select"
|
||||
label={label} onChange={this.onSelectChange}
|
||||
value={String(this.state.selectValue)} disabled={this.props.disabled}>
|
||||
label={label} onChange={this.onSelectChange}
|
||||
value={String(this.state.selectValue)} disabled={this.props.disabled}
|
||||
>
|
||||
{options}
|
||||
</Field>
|
||||
);
|
||||
|
@ -46,17 +46,18 @@ export default class RoomAliasField extends React.PureComponent {
|
||||
const domain = (<span title={aliasPostfix}>{aliasPostfix}</span>);
|
||||
const maxlength = 255 - this.props.domain.length - 2; // 2 for # and :
|
||||
return (
|
||||
<Field
|
||||
label={_t("Room address")}
|
||||
className="mx_RoomAliasField"
|
||||
prefixComponent={poundSign}
|
||||
postfixComponent={domain}
|
||||
ref={ref => this._fieldRef = ref}
|
||||
onValidate={this._onValidate}
|
||||
placeholder={_t("e.g. my-room")}
|
||||
onChange={this._onChange}
|
||||
value={this.props.value.substring(1, this.props.value.length - this.props.domain.length - 1)}
|
||||
maxLength={maxlength} />
|
||||
<Field
|
||||
label={_t("Room address")}
|
||||
className="mx_RoomAliasField"
|
||||
prefixComponent={poundSign}
|
||||
postfixComponent={domain}
|
||||
ref={ref => this._fieldRef = ref}
|
||||
onValidate={this._onValidate}
|
||||
placeholder={_t("e.g. my-room")}
|
||||
onChange={this._onChange}
|
||||
value={this.props.value.substring(1, this.props.value.length - this.props.domain.length - 1)}
|
||||
maxLength={maxlength}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -59,13 +59,13 @@ class TintableSvg extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<object className={"mx_TintableSvg " + (this.props.className ? this.props.className : "")}
|
||||
type="image/svg+xml"
|
||||
data={this.props.src}
|
||||
width={this.props.width}
|
||||
height={this.props.height}
|
||||
onLoad={this.onLoad}
|
||||
tabIndex="-1"
|
||||
/>
|
||||
type="image/svg+xml"
|
||||
data={this.props.src}
|
||||
width={this.props.width}
|
||||
height={this.props.height}
|
||||
onLoad={this.onLoad}
|
||||
tabIndex="-1"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -178,9 +178,15 @@ export default class GroupMemberList extends React.Component {
|
||||
}
|
||||
|
||||
const inputBox = (
|
||||
<input className="mx_GroupMemberList_query mx_textinput" id="mx_GroupMemberList_query" type="text"
|
||||
onChange={this.onSearchQueryChanged} value={this.state.searchQuery}
|
||||
placeholder={_t('Filter community members')} autoComplete="off" />
|
||||
<input
|
||||
className="mx_GroupMemberList_query mx_textinput"
|
||||
id="mx_GroupMemberList_query"
|
||||
type="text"
|
||||
onChange={this.onSearchQueryChanged}
|
||||
value={this.state.searchQuery}
|
||||
placeholder={_t('Filter community members')}
|
||||
autoComplete="off"
|
||||
/>
|
||||
);
|
||||
|
||||
const joined = this.state.members ? <div className="mx_MemberList_joined">
|
||||
|
@ -67,11 +67,11 @@ export default class GroupPublicityToggle extends React.Component {
|
||||
const GroupTile = sdk.getComponent('groups.GroupTile');
|
||||
return <div className="mx_GroupPublicity_toggle">
|
||||
<GroupTile groupId={this.props.groupId} showDescription={false}
|
||||
avatarHeight={40} draggable={false}
|
||||
avatarHeight={40} draggable={false}
|
||||
/>
|
||||
<ToggleSwitch checked={this.state.isGroupPublicised}
|
||||
disabled={!this.state.ready || this.state.busy}
|
||||
onChange={this._onPublicityToggle} />
|
||||
disabled={!this.state.ready || this.state.busy}
|
||||
onChange={this._onPublicityToggle} />
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
@ -141,9 +141,14 @@ export default class GroupRoomList extends React.Component {
|
||||
);
|
||||
}
|
||||
const inputBox = (
|
||||
<input className="mx_GroupRoomList_query mx_textinput" id="mx_GroupRoomList_query" type="text"
|
||||
onChange={this.onSearchQueryChanged} value={this.state.searchQuery}
|
||||
placeholder={_t('Filter community rooms')} autoComplete="off" />
|
||||
<input
|
||||
className="mx_GroupRoomList_query mx_textinput" id="mx_GroupRoomList_query"
|
||||
type="text"
|
||||
onChange={this.onSearchQueryChanged}
|
||||
value={this.state.searchQuery}
|
||||
placeholder={_t('Filter community rooms')}
|
||||
autoComplete="off"
|
||||
/>
|
||||
);
|
||||
|
||||
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
||||
@ -152,7 +157,7 @@ export default class GroupRoomList extends React.Component {
|
||||
{ inviteButton }
|
||||
<AutoHideScrollbar className="mx_GroupRoomList_joined mx_GroupRoomList_outerWrapper">
|
||||
<TruncatedList className="mx_GroupRoomList_wrapper" truncateAt={this.state.truncateAt}
|
||||
createOverflowElement={this._createOverflowTile}>
|
||||
createOverflowElement={this._createOverflowTile}>
|
||||
{ this.makeGroupRoomTiles(this.state.searchQuery) }
|
||||
</TruncatedList>
|
||||
</AutoHideScrollbar>
|
||||
|
@ -125,9 +125,9 @@ export default class MImageBody extends React.Component {
|
||||
_isGif() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
return (
|
||||
content &&
|
||||
content.info &&
|
||||
content.info.mimetype === "image/gif"
|
||||
content &&
|
||||
content.info &&
|
||||
content.info.mimetype === "image/gif"
|
||||
);
|
||||
}
|
||||
|
||||
@ -346,9 +346,9 @@ export default class MImageBody extends React.Component {
|
||||
} else {
|
||||
imageElement = (
|
||||
<img style={{display: 'none'}} src={thumbUrl} ref={this._image}
|
||||
alt={content.body}
|
||||
onError={this.onImageError}
|
||||
onLoad={this.onImageLoad}
|
||||
alt={content.body}
|
||||
onError={this.onImageError}
|
||||
onLoad={this.onImageLoad}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -384,12 +384,12 @@ export default class MImageBody extends React.Component {
|
||||
// mx_MImageBody_thumbnail resizes img to exactly container size
|
||||
img = (
|
||||
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref={this._image}
|
||||
style={{ maxWidth: maxWidth + "px" }}
|
||||
alt={content.body}
|
||||
onError={this.onImageError}
|
||||
onLoad={this.onImageLoad}
|
||||
onMouseEnter={this.onImageEnter}
|
||||
onMouseLeave={this.onImageLeave} />
|
||||
style={{ maxWidth: maxWidth + "px" }}
|
||||
alt={content.body}
|
||||
onError={this.onImageError}
|
||||
onLoad={this.onImageLoad}
|
||||
onMouseEnter={this.onImageEnter}
|
||||
onMouseLeave={this.onImageLeave} />
|
||||
);
|
||||
}
|
||||
|
||||
@ -467,9 +467,9 @@ export default class MImageBody extends React.Component {
|
||||
const contentUrl = this._getContentUrl();
|
||||
let thumbUrl;
|
||||
if (this._isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||
thumbUrl = contentUrl;
|
||||
thumbUrl = contentUrl;
|
||||
} else {
|
||||
thumbUrl = this._getThumbUrl();
|
||||
thumbUrl = this._getThumbUrl();
|
||||
}
|
||||
|
||||
const thumbnail = this._messageContent(contentUrl, thumbUrl, content);
|
||||
|
@ -82,9 +82,7 @@ export default class MKeyVerificationConclusion extends React.Component {
|
||||
}
|
||||
|
||||
// User isn't actually verified
|
||||
if (!MatrixClientPeg.get()
|
||||
.checkUserTrust(request.otherUserId)
|
||||
.isCrossSigningVerified()) {
|
||||
if (!MatrixClientPeg.get().checkUserTrust(request.otherUserId).isCrossSigningVerified()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
106
src/components/views/messages/MVoiceMessageBody.tsx
Normal file
106
src/components/views/messages/MVoiceMessageBody.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {Playback} from "../../../voice/Playback";
|
||||
import MFileBody from "./MFileBody";
|
||||
import InlineSpinner from '../elements/InlineSpinner';
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {mediaFromContent} from "../../../customisations/Media";
|
||||
import {decryptFile} from "../../../utils/DecryptFile";
|
||||
import RecordingPlayback from "../voice_messages/RecordingPlayback";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
error?: Error;
|
||||
playback?: Playback;
|
||||
decryptedBlob?: Blob;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.MVoiceMessageBody")
|
||||
export default class MVoiceMessageBody extends React.PureComponent<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
let buffer: ArrayBuffer;
|
||||
const content = this.props.mxEvent.getContent();
|
||||
const media = mediaFromContent(content);
|
||||
if (media.isEncrypted) {
|
||||
try {
|
||||
const blob = await decryptFile(content.file);
|
||||
buffer = await blob.arrayBuffer();
|
||||
this.setState({decryptedBlob: blob});
|
||||
} catch (e) {
|
||||
this.setState({error: e});
|
||||
console.warn("Unable to decrypt voice message", e);
|
||||
return; // stop processing the audio file
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
buffer = await media.downloadSource().then(r => r.blob()).then(r => r.arrayBuffer());
|
||||
} catch (e) {
|
||||
this.setState({error: e});
|
||||
console.warn("Unable to download voice message", e);
|
||||
return; // stop processing the audio file
|
||||
}
|
||||
}
|
||||
|
||||
const waveform = content?.["org.matrix.msc1767.audio"]?.waveform?.map(p => p / 1024);
|
||||
|
||||
// We should have a buffer to work with now: let's set it up
|
||||
const playback = new Playback(buffer, waveform);
|
||||
this.setState({playback});
|
||||
// Note: the RecordingPlayback component will handle preparing the Playback class for us.
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (this.state.error) {
|
||||
// TODO: @@TR: Verify error state
|
||||
return (
|
||||
<span className="mx_MVoiceMessageBody">
|
||||
<img src={require("../../../../res/img/warning.svg")} width="16" height="16" />
|
||||
{ _t("Error processing voice message") }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.state.playback) {
|
||||
// TODO: @@TR: Verify loading/decrypting state
|
||||
return (
|
||||
<span className="mx_MVoiceMessageBody">
|
||||
<InlineSpinner />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// At this point we should have a playable state
|
||||
return (
|
||||
<span className="mx_MVoiceMessageBody">
|
||||
<RecordingPlayback playback={this.state.playback} />
|
||||
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
39
src/components/views/messages/MVoiceOrAudioBody.tsx
Normal file
39
src/components/views/messages/MVoiceOrAudioBody.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
import MAudioBody from "./MAudioBody";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import MVoiceMessageBody from "./MVoiceMessageBody";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.MVoiceOrAudioBody")
|
||||
export default class MVoiceOrAudioBody extends React.PureComponent<IProps> {
|
||||
public render() {
|
||||
const isVoiceMessage = !!this.props.mxEvent.getContent()['org.matrix.msc2516.voice'];
|
||||
const voiceMessagesEnabled = SettingsStore.getValue("feature_voice_messages");
|
||||
if (isVoiceMessage && voiceMessagesEnabled) {
|
||||
return <MVoiceMessageBody {...this.props} />;
|
||||
} else {
|
||||
return <MAudioBody {...this.props} />;
|
||||
}
|
||||
}
|
||||
}
|
@ -72,12 +72,8 @@ export default class MessageEvent extends React.Component {
|
||||
'm.emote': sdk.getComponent('messages.TextualBody'),
|
||||
'm.image': sdk.getComponent('messages.MImageBody'),
|
||||
'm.file': sdk.getComponent('messages.MFileBody'),
|
||||
'm.audio': sdk.getComponent('messages.MAudioBody'),
|
||||
'm.audio': sdk.getComponent('messages.MVoiceOrAudioBody'),
|
||||
'm.video': sdk.getComponent('messages.MVideoBody'),
|
||||
|
||||
// TODO: @@ TravisR: Use labs flag determination.
|
||||
// MSC: https://github.com/matrix-org/matrix-doc/pull/2516
|
||||
'org.matrix.msc2516.voice': sdk.getComponent('messages.MAudioBody'),
|
||||
};
|
||||
const evTypes = {
|
||||
'm.sticker': sdk.getComponent('messages.MStickerBody'),
|
||||
|
@ -129,12 +129,13 @@ export default class ReactionsRowButton extends React.PureComponent {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const isPeeking = room.getMyMembership() !== "join";
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return <AccessibleButton
|
||||
className={classes}
|
||||
aria-label={label}
|
||||
onClick={this.onClick}
|
||||
disabled={isPeeking}
|
||||
onMouseOver={this.onMouseOver}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
>
|
||||
|
@ -521,11 +521,12 @@ export default class TextualBody extends React.Component {
|
||||
const LinkPreviewWidget = sdk.getComponent('rooms.LinkPreviewWidget');
|
||||
widgets = this.state.links.map((link)=>{
|
||||
return <LinkPreviewWidget
|
||||
key={link}
|
||||
link={link}
|
||||
mxEvent={this.props.mxEvent}
|
||||
onCancelClick={this.onCancelClick}
|
||||
onHeightChanged={this.props.onHeightChanged} />;
|
||||
key={link}
|
||||
link={link}
|
||||
mxEvent={this.props.mxEvent}
|
||||
onCancelClick={this.onCancelClick}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
/>;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -310,9 +310,14 @@ export default class AliasSettings extends React.Component {
|
||||
let found = false;
|
||||
const canonicalValue = this.state.canonicalAlias || "";
|
||||
const canonicalAliasSection = (
|
||||
<Field onChange={this.onCanonicalAliasChange} value={canonicalValue}
|
||||
disabled={this.state.updatingCanonicalAlias || !this.props.canSetCanonicalAlias}
|
||||
element='select' id='canonicalAlias' label={_t('Main address')}>
|
||||
<Field
|
||||
onChange={this.onCanonicalAliasChange}
|
||||
value={canonicalValue}
|
||||
disabled={this.state.updatingCanonicalAlias || !this.props.canSetCanonicalAlias}
|
||||
element='select'
|
||||
id='canonicalAlias'
|
||||
label={_t('Main address')}
|
||||
>
|
||||
<option value="" key="unset">{ _t('not specified') }</option>
|
||||
{
|
||||
this._getAliases().map((alias, i) => {
|
||||
@ -326,9 +331,9 @@ export default class AliasSettings extends React.Component {
|
||||
}
|
||||
{
|
||||
found || !this.state.canonicalAlias ? '' :
|
||||
<option value={ this.state.canonicalAlias } key='arbitrary'>
|
||||
{ this.state.canonicalAlias }
|
||||
</option>
|
||||
<option value={ this.state.canonicalAlias } key='arbitrary'>
|
||||
{ this.state.canonicalAlias }
|
||||
</option>
|
||||
}
|
||||
</Field>
|
||||
);
|
||||
|
@ -205,16 +205,34 @@ export default class RoomProfileSettings extends React.Component {
|
||||
noValidate={true}
|
||||
className="mx_ProfileSettings_profileForm"
|
||||
>
|
||||
<input type="file" ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload"
|
||||
onChange={this._onAvatarChanged} accept="image/*" />
|
||||
<input
|
||||
type="file"
|
||||
ref={this._avatarUpload}
|
||||
className="mx_ProfileSettings_avatarUpload"
|
||||
onChange={this._onAvatarChanged}
|
||||
accept="image/*"
|
||||
/>
|
||||
<div className="mx_ProfileSettings_profile">
|
||||
<div className="mx_ProfileSettings_controls">
|
||||
<Field label={_t("Room Name")}
|
||||
type="text" value={this.state.displayName} autoComplete="off"
|
||||
onChange={this._onDisplayNameChanged} disabled={!this.state.canSetName} />
|
||||
<Field className="mx_ProfileSettings_controls_topic" id="profileTopic" label={_t("Room Topic")} disabled={!this.state.canSetTopic}
|
||||
type="text" value={this.state.topic} autoComplete="off"
|
||||
onChange={this._onTopicChanged} element="textarea" />
|
||||
<Field
|
||||
label={_t("Room Name")}
|
||||
type="text"
|
||||
value={this.state.displayName}
|
||||
autoComplete="off"
|
||||
onChange={this._onDisplayNameChanged}
|
||||
disabled={!this.state.canSetName}
|
||||
/>
|
||||
<Field
|
||||
className="mx_ProfileSettings_controls_topic"
|
||||
id="profileTopic"
|
||||
label={_t("Room Topic")}
|
||||
disabled={!this.state.canSetTopic}
|
||||
type="text"
|
||||
value={this.state.topic}
|
||||
autoComplete="off"
|
||||
onChange={this._onTopicChanged}
|
||||
element="textarea"
|
||||
/>
|
||||
</div>
|
||||
<AvatarSetting
|
||||
avatarUrl={this.state.avatarUrl}
|
||||
|
@ -68,10 +68,12 @@ export default class UrlPreviewSettings extends React.Component {
|
||||
if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, "room")) {
|
||||
previewsForRoom = (
|
||||
<label>
|
||||
<SettingsFlag name="urlPreviewsEnabled"
|
||||
level={SettingLevel.ROOM}
|
||||
roomId={roomId}
|
||||
isExplicit={true} />
|
||||
<SettingsFlag
|
||||
name="urlPreviewsEnabled"
|
||||
level={SettingLevel.ROOM}
|
||||
roomId={roomId}
|
||||
isExplicit={true}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
} else {
|
||||
@ -91,8 +93,8 @@ export default class UrlPreviewSettings extends React.Component {
|
||||
|
||||
const previewsForRoomAccount = ( // in an e2ee room we use a special key to enforce per-room opt-in
|
||||
<SettingsFlag name={isEncrypted ? 'urlPreviewsEnabled_e2ee' : 'urlPreviewsEnabled'}
|
||||
level={SettingLevel.ROOM_ACCOUNT}
|
||||
roomId={roomId} />
|
||||
level={SettingLevel.ROOM_ACCOUNT}
|
||||
roomId={roomId} />
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -176,8 +176,11 @@ class EntityTile extends React.Component {
|
||||
// The wrapping div is required to make the magic mouse listener work, for some reason.
|
||||
return (
|
||||
<div ref={(c) => this.container = c} >
|
||||
<AccessibleButton className={classNames(mainClassNames)} title={this.props.title}
|
||||
onClick={this.props.onClick}>
|
||||
<AccessibleButton
|
||||
className={classNames(mainClassNames)}
|
||||
title={this.props.title}
|
||||
onClick={this.props.onClick}
|
||||
>
|
||||
<div className="mx_EntityTile_avatar">
|
||||
{ av }
|
||||
{ e2eIcon }
|
||||
|
@ -128,8 +128,8 @@ export default class LinkPreviewWidget extends React.Component {
|
||||
let img;
|
||||
if (image) {
|
||||
img = <div className="mx_LinkPreviewWidget_image" style={{ height: thumbHeight }}>
|
||||
<img style={{ maxWidth: imageMaxWidth, maxHeight: imageMaxHeight }} src={image} onClick={this.onImageClick} />
|
||||
</div>;
|
||||
<img style={{ maxWidth: imageMaxWidth, maxHeight: imageMaxHeight }} src={image} onClick={this.onImageClick} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
// The description includes &-encoded HTML entities, we decode those as React treats the thing as an
|
||||
|
@ -53,9 +53,9 @@ export default class PinnedEventTile extends React.Component {
|
||||
if (index !== -1) {
|
||||
pinned.splice(index, 1);
|
||||
MatrixClientPeg.get().sendStateEvent(this.props.mxRoom.roomId, 'm.room.pinned_events', {pinned}, '')
|
||||
.then(() => {
|
||||
if (this.props.onUnpinned) this.props.onUnpinned();
|
||||
});
|
||||
.then(() => {
|
||||
if (this.props.onUnpinned) this.props.onUnpinned();
|
||||
});
|
||||
} else if (this.props.onUnpinned) this.props.onUnpinned();
|
||||
}
|
||||
};
|
||||
@ -98,8 +98,11 @@ export default class PinnedEventTile extends React.Component {
|
||||
{ formatFullDate(new Date(this.props.mxEvent.getTs())) }
|
||||
</span>
|
||||
<div className="mx_PinnedEventTile_message">
|
||||
<MessageEvent mxEvent={this.props.mxEvent} className="mx_PinnedEventTile_body" maxImageHeight={150}
|
||||
onHeightChanged={() => {}} // we need to give this, apparently
|
||||
<MessageEvent
|
||||
mxEvent={this.props.mxEvent}
|
||||
className="mx_PinnedEventTile_body"
|
||||
maxImageHeight={150}
|
||||
onHeightChanged={() => {}} // we need to give this, apparently
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -64,10 +64,10 @@ export default class PinnedEventsPanel extends React.Component {
|
||||
|
||||
pinnedEvents.getContent().pinned.map((eventId) => {
|
||||
promises.push(cli.getEventTimeline(this.props.room.getUnfilteredTimelineSet(), eventId, 0).then(
|
||||
(timeline) => {
|
||||
const event = timeline.getEvents().find((e) => e.getId() === eventId);
|
||||
return {eventId, timeline, event};
|
||||
}).catch((err) => {
|
||||
(timeline) => {
|
||||
const event = timeline.getEvents().find((e) => e.getId() === eventId);
|
||||
return {eventId, timeline, event};
|
||||
}).catch((err) => {
|
||||
console.error("Error looking up pinned event " + eventId + " in room " + this.props.room.roomId);
|
||||
console.error(err);
|
||||
return null; // return lack of context to avoid unhandled errors
|
||||
@ -113,10 +113,14 @@ export default class PinnedEventsPanel extends React.Component {
|
||||
}
|
||||
|
||||
return this.state.pinned.map((context) => {
|
||||
return (<PinnedEventTile key={context.event.getId()}
|
||||
mxRoom={this.props.room}
|
||||
mxEvent={context.event}
|
||||
onUnpinned={this._updatePinnedMessages} />);
|
||||
return (
|
||||
<PinnedEventTile
|
||||
key={context.event.getId()}
|
||||
mxRoom={this.props.room}
|
||||
mxEvent={context.event}
|
||||
onUnpinned={this._updatePinnedMessages}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -187,8 +187,7 @@ export default class ReadReceiptMarker extends React.PureComponent {
|
||||
}
|
||||
|
||||
return (
|
||||
<NodeAnimator
|
||||
startStyles={this.state.startStyles} >
|
||||
<NodeAnimator startStyles={this.state.startStyles}>
|
||||
<MemberAvatar
|
||||
member={this.props.member}
|
||||
fallbackUserId={this.props.fallbackUserId}
|
||||
|
@ -79,8 +79,13 @@ export default class ReplyPreview extends React.Component {
|
||||
{ _t('Replying') }
|
||||
</div>
|
||||
<div className="mx_ReplyPreview_header mx_ReplyPreview_cancel">
|
||||
<img className="mx_filterFlipColor" src={require("../../../../res/img/cancel.svg")} width="18" height="18"
|
||||
onClick={cancelQuoting} />
|
||||
<img
|
||||
className="mx_filterFlipColor"
|
||||
src={require("../../../../res/img/cancel.svg")}
|
||||
width="18"
|
||||
height="18"
|
||||
onClick={cancelQuoting}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_ReplyPreview_clear" />
|
||||
<EventTile
|
||||
|
@ -88,11 +88,11 @@ export default class RoomDetailRow extends React.Component {
|
||||
const name = room.name || getDisplayAliasForRoom(room) || _t('Unnamed room');
|
||||
|
||||
const guestRead = room.worldReadable ? (
|
||||
<div className="mx_RoomDirectory_perm">{ _t('World readable') }</div>
|
||||
) : <div />;
|
||||
<div className="mx_RoomDirectory_perm">{ _t('World readable') }</div>
|
||||
) : <div />;
|
||||
const guestJoin = room.guestCanJoin ? (
|
||||
<div className="mx_RoomDirectory_perm">{ _t('Guests can join') }</div>
|
||||
) : <div />;
|
||||
<div className="mx_RoomDirectory_perm">{ _t('Guests can join') }</div>
|
||||
) : <div />;
|
||||
|
||||
const perms = (guestRead || guestJoin) ? (<div className="mx_RoomDirectory_perms">
|
||||
{ guestRead }
|
||||
|
@ -539,6 +539,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||
onResize={this.props.onResize}
|
||||
showSkeleton={showSkeleton}
|
||||
extraTiles={extraTiles}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
alwaysVisible={ALWAYS_VISIBLE_TAGS.includes(orderedTagId)}
|
||||
/>
|
||||
});
|
||||
|
@ -19,6 +19,7 @@ import React, {useState} from "react";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import SpaceStore from "../../../stores/SpaceStore";
|
||||
|
||||
const RoomListNumResults: React.FC = () => {
|
||||
const [count, setCount] = useState<number>(null);
|
||||
@ -34,7 +35,10 @@ const RoomListNumResults: React.FC = () => {
|
||||
if (typeof count !== "number") return null;
|
||||
|
||||
return <div className="mx_LeftPanel_roomListFilterCount">
|
||||
{_t("%(count)s results", { count })}
|
||||
{ SpaceStore.instance.spacePanelSpaces.length
|
||||
? _t("%(count)s results in all spaces", { count })
|
||||
: _t("%(count)s results", { count })
|
||||
}
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
@ -44,6 +44,7 @@ import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import { Enable, Resizable } from "re-resizable";
|
||||
import { Direction } from "re-resizable/lib/resizer";
|
||||
import { polyfillTouchEvent } from "../../../@types/polyfill";
|
||||
import { ResizeNotifier } from "../../../utils/ResizeNotifier";
|
||||
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
||||
import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore";
|
||||
import { arrayFastClone, arrayHasOrderChange } from "../../../utils/arrays";
|
||||
@ -75,7 +76,7 @@ interface IProps {
|
||||
onResize: () => void;
|
||||
showSkeleton?: boolean;
|
||||
alwaysVisible?: boolean;
|
||||
|
||||
resizeNotifier: ResizeNotifier;
|
||||
extraTiles?: ReactComponentElement<typeof ExtraTile>[];
|
||||
|
||||
// TODO: Account for https://github.com/vector-im/element-web/issues/14179
|
||||
@ -528,6 +529,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||
tiles.push(<RoomTile
|
||||
room={room}
|
||||
key={`room-${room.roomId}`}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
showMessagePreview={this.layout.showPreviews}
|
||||
isMinimized={this.props.isMinimized}
|
||||
tag={this.props.tagId}
|
||||
|
@ -53,12 +53,14 @@ import { CommunityPrototypeStore, IRoomProfile } from "../../../stores/Community
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { getUnsentMessages } from "../../structures/RoomStatusBar";
|
||||
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
|
||||
import { ResizeNotifier } from "../../../utils/ResizeNotifier";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
showMessagePreview: boolean;
|
||||
isMinimized: boolean;
|
||||
tag: TagID;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
}
|
||||
|
||||
type PartialDOMRect = Pick<DOMRect, "left" | "bottom">;
|
||||
@ -102,6 +104,9 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||
};
|
||||
this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room);
|
||||
this.roomProps = EchoChamber.forRoom(this.props.room);
|
||||
if (this.props.resizeNotifier) {
|
||||
this.props.resizeNotifier.on("middlePanelResized", this.onResize);
|
||||
}
|
||||
}
|
||||
|
||||
private countUnsentEvents(): number {
|
||||
@ -116,6 +121,12 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||
this.forceUpdate(); // notification state changed - update
|
||||
};
|
||||
|
||||
private onResize = () => {
|
||||
if (this.showMessagePreview && !this.state.messagePreview) {
|
||||
this.setState({messagePreview: this.generatePreview()});
|
||||
}
|
||||
};
|
||||
|
||||
private onLocalEchoUpdated = (ev: MatrixEvent, room: Room) => {
|
||||
if (!room?.roomId === this.props.room.roomId) return;
|
||||
this.setState({hasUnsentEvents: this.countUnsentEvents() > 0});
|
||||
@ -195,6 +206,9 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||
);
|
||||
this.props.room.off("Room.name", this.onRoomNameUpdate);
|
||||
}
|
||||
if (this.props.resizeNotifier) {
|
||||
this.props.resizeNotifier.off("middlePanelResized", this.onResize);
|
||||
}
|
||||
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
this.notificationState.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
|
||||
|
@ -266,25 +266,25 @@ export default class Stickerpicker extends React.Component {
|
||||
width: this.popoverWidth,
|
||||
}}
|
||||
>
|
||||
<PersistedElement persistKey={PERSISTED_ELEMENT_KEY} zIndex={STICKERPICKER_Z_INDEX}>
|
||||
<AppTile
|
||||
app={stickerApp}
|
||||
room={this.props.room}
|
||||
fullWidth={true}
|
||||
userId={MatrixClientPeg.get().credentials.userId}
|
||||
creatorUserId={stickerpickerWidget.sender || MatrixClientPeg.get().credentials.userId}
|
||||
waitForIframeLoad={true}
|
||||
showMenubar={true}
|
||||
onEditClick={this._launchManageIntegrations}
|
||||
onDeleteClick={this._removeStickerpickerWidgets}
|
||||
showTitle={false}
|
||||
showCancel={false}
|
||||
showPopout={false}
|
||||
onMinimiseClick={this._onHideStickersClick}
|
||||
handleMinimisePointerEvents={true}
|
||||
userWidget={true}
|
||||
/>
|
||||
</PersistedElement>
|
||||
<PersistedElement persistKey={PERSISTED_ELEMENT_KEY} zIndex={STICKERPICKER_Z_INDEX}>
|
||||
<AppTile
|
||||
app={stickerApp}
|
||||
room={this.props.room}
|
||||
fullWidth={true}
|
||||
userId={MatrixClientPeg.get().credentials.userId}
|
||||
creatorUserId={stickerpickerWidget.sender || MatrixClientPeg.get().credentials.userId}
|
||||
waitForIframeLoad={true}
|
||||
showMenubar={true}
|
||||
onEditClick={this._launchManageIntegrations}
|
||||
onDeleteClick={this._removeStickerpickerWidgets}
|
||||
showTitle={false}
|
||||
showCancel={false}
|
||||
showPopout={false}
|
||||
onMinimiseClick={this._onHideStickersClick}
|
||||
handleMinimisePointerEvents={true}
|
||||
userWidget={true}
|
||||
/>
|
||||
</PersistedElement>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -27,6 +27,10 @@ import LiveRecordingClock from "../voice_messages/LiveRecordingClock";
|
||||
import {VoiceRecordingStore} from "../../../stores/VoiceRecordingStore";
|
||||
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
|
||||
import RecordingPlayback from "../voice_messages/RecordingPlayback";
|
||||
import {MsgType} from "matrix-js-sdk/src/@types/event";
|
||||
import Modal from "../../../Modal";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import CallMediaHandler from "../../../CallMediaHandler";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
@ -64,8 +68,8 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||
const mxc = await this.state.recorder.upload();
|
||||
MatrixClientPeg.get().sendMessage(this.props.room.roomId, {
|
||||
"body": "Voice message",
|
||||
"msgtype": "org.matrix.msc2516.voice",
|
||||
//"msgtype": MsgType.Audio,
|
||||
//"msgtype": "org.matrix.msc2516.voice",
|
||||
"msgtype": MsgType.Audio,
|
||||
"url": mxc,
|
||||
"info": {
|
||||
duration: Math.round(this.state.recorder.durationSeconds * 1000),
|
||||
@ -83,10 +87,6 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||
},
|
||||
"org.matrix.msc1767.audio": {
|
||||
duration: Math.round(this.state.recorder.durationSeconds * 1000),
|
||||
// TODO: @@ TravisR: Waveform? (MSC1767 decision)
|
||||
},
|
||||
"org.matrix.experimental.msc2516.voice": { // MSC2516+MSC1767 experiment
|
||||
duration: Math.round(this.state.recorder.durationSeconds * 1000),
|
||||
|
||||
// Events can't have floats, so we try to maintain resolution by using 1024
|
||||
// as a maximum value. The waveform contains values between zero and 1, so this
|
||||
@ -95,6 +95,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||
// We're expecting about one data point per second of audio.
|
||||
waveform: this.state.recorder.getPlayback().waveform.map(v => Math.round(v * 1024)),
|
||||
},
|
||||
"org.matrix.msc2516.voice": {}, // No content, this is a rendering hint
|
||||
});
|
||||
await this.disposeRecording();
|
||||
}
|
||||
@ -115,16 +116,59 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||
await this.state.recorder.stop();
|
||||
return;
|
||||
}
|
||||
const recorder = VoiceRecordingStore.instance.startRecording();
|
||||
await recorder.start();
|
||||
|
||||
// We don't need to remove the listener: the recorder will clean that up for us.
|
||||
recorder.on(UPDATE_EVENT, (ev: RecordingState) => {
|
||||
if (ev === RecordingState.EndingSoon) return; // ignore this state: it has no UI purpose here
|
||||
this.setState({recordingPhase: ev});
|
||||
});
|
||||
// The "microphone access error" dialogs are used a lot, so let's functionify them
|
||||
const accessError = () => {
|
||||
Modal.createTrackedDialog('Microphone Access Error', '', ErrorDialog, {
|
||||
title: _t("Unable to access your microphone"),
|
||||
description: <>
|
||||
<p>{_t(
|
||||
"We were unable to access your microphone. Please check your browser settings and try again.",
|
||||
)}</p>
|
||||
</>,
|
||||
});
|
||||
};
|
||||
|
||||
this.setState({recorder, recordingPhase: RecordingState.Started});
|
||||
// Do a sanity test to ensure we're about to grab a valid microphone reference. Things might
|
||||
// change between this and recording, but at least we will have tried.
|
||||
try {
|
||||
const devices = await CallMediaHandler.getDevices();
|
||||
if (!devices?.['audioinput']?.length) {
|
||||
Modal.createTrackedDialog('No Microphone Error', '', ErrorDialog, {
|
||||
title: _t("No microphone found"),
|
||||
description: <>
|
||||
<p>{_t(
|
||||
"We didn't find a microphone on your device. Please check your settings and try again.",
|
||||
)}</p>
|
||||
</>,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// else we probably have a device that is good enough
|
||||
} catch (e) {
|
||||
console.error("Error getting devices: ", e);
|
||||
accessError();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const recorder = VoiceRecordingStore.instance.startRecording();
|
||||
await recorder.start();
|
||||
|
||||
// We don't need to remove the listener: the recorder will clean that up for us.
|
||||
recorder.on(UPDATE_EVENT, (ev: RecordingState) => {
|
||||
if (ev === RecordingState.EndingSoon) return; // ignore this state: it has no UI purpose here
|
||||
this.setState({recordingPhase: ev});
|
||||
});
|
||||
|
||||
this.setState({recorder, recordingPhase: RecordingState.Started});
|
||||
} catch (e) {
|
||||
console.error("Error starting recording: ", e);
|
||||
accessError();
|
||||
|
||||
// noinspection ES6MissingAwait - if this goes wrong we don't want it to affect the call stack
|
||||
VoiceRecordingStore.instance.disposeRecording();
|
||||
}
|
||||
};
|
||||
|
||||
private renderWaveformArea(): ReactNode {
|
||||
|
@ -154,7 +154,7 @@ export default class ChangeAvatar extends React.Component {
|
||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
// XXX: FIXME: once we track in the JS what our own displayname is(!) then use it here rather than ?
|
||||
avatarImg = <BaseAvatar width={this.props.width} height={this.props.height} resizeMethod='crop'
|
||||
name='?' idName={MatrixClientPeg.get().getUserIdLocalpart()} url={this.state.avatarUrl} />;
|
||||
name='?' idName={MatrixClientPeg.get().getUserIdLocalpart()} url={this.state.avatarUrl} />;
|
||||
}
|
||||
|
||||
let uploadSection;
|
||||
|
@ -206,7 +206,7 @@ export default class ChangePassword extends React.Component {
|
||||
test: ({ value, allowEmpty }) => allowEmpty || !!value,
|
||||
invalid: () => _t("Passwords can't be empty"),
|
||||
},
|
||||
],
|
||||
],
|
||||
});
|
||||
|
||||
onChangeNewPassword = (ev) => {
|
||||
@ -245,7 +245,7 @@ export default class ChangePassword extends React.Component {
|
||||
},
|
||||
invalid: () => _t("Passwords don't match"),
|
||||
},
|
||||
],
|
||||
],
|
||||
});
|
||||
|
||||
onClickChange = async (ev) => {
|
||||
|
@ -259,7 +259,7 @@ export default class CrossSigningPanel extends React.PureComponent {
|
||||
<td>{_t("Homeserver feature support:")}</td>
|
||||
<td>{homeserverSupportsCrossSigning ? _t("exists") : _t("not found")}</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
</tbody></table>
|
||||
</details>
|
||||
{errorSection}
|
||||
{actionRow}
|
||||
|
@ -214,7 +214,7 @@ export default class DevicesPanel extends React.Component {
|
||||
const deleteButton = this.state.deleting ?
|
||||
<Spinner w={22} h={22} /> :
|
||||
<AccessibleButton onClick={this._onDeleteClick} kind="danger_sm">
|
||||
{ _t("Delete %(count)s sessions", {count: this.state.selectedDevices.length}) }
|
||||
{ _t("Delete %(count)s sessions", {count: this.state.selectedDevices.length})}
|
||||
</AccessibleButton>;
|
||||
|
||||
const classes = classNames(this.props.className, "mx_DevicesPanel");
|
||||
|
@ -100,7 +100,7 @@ export default class Notifications extends React.Component {
|
||||
MatrixClientPeg.get().setPushRuleEnabled(
|
||||
'global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked,
|
||||
).then(function() {
|
||||
self._refreshFromServer();
|
||||
self._refreshFromServer();
|
||||
});
|
||||
};
|
||||
|
||||
@ -580,12 +580,12 @@ export default class Notifications extends React.Component {
|
||||
"vectorRuleId": "_keywords",
|
||||
"description": (
|
||||
<span>
|
||||
{ _t('Messages containing <span>keywords</span>',
|
||||
{},
|
||||
{ 'span': (sub) =>
|
||||
<span className="mx_UserNotifSettings_keywords" onClick={ self.onKeywordsClicked }>{sub}</span>,
|
||||
},
|
||||
)}
|
||||
{ _t('Messages containing <span>keywords</span>',
|
||||
{},
|
||||
{ 'span': (sub) =>
|
||||
<span className="mx_UserNotifSettings_keywords" onClick={ self.onKeywordsClicked }>{sub}</span>,
|
||||
},
|
||||
)}
|
||||
</span>
|
||||
),
|
||||
"vectorState": self.state.vectorContentRules.vectorState,
|
||||
@ -743,8 +743,8 @@ export default class Notifications extends React.Component {
|
||||
|
||||
emailNotificationsRow(address, label) {
|
||||
return <LabelledToggleSwitch value={this.hasEmailPusher(this.state.pushers, address)}
|
||||
onChange={this.onEnableEmailNotificationsChange.bind(this, address)}
|
||||
label={label} key={`emailNotif_${label}`} />;
|
||||
onChange={this.onEnableEmailNotificationsChange.bind(this, address)}
|
||||
label={label} key={`emailNotif_${label}`} />;
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -757,8 +757,8 @@ export default class Notifications extends React.Component {
|
||||
let masterPushRuleDiv;
|
||||
if (this.state.masterPushRule) {
|
||||
masterPushRuleDiv = <LabelledToggleSwitch value={!this.state.masterPushRule.enabled}
|
||||
onChange={this.onEnableNotificationsChange}
|
||||
label={_t('Enable notifications for this account')} />;
|
||||
onChange={this.onEnableNotificationsChange}
|
||||
label={_t('Enable notifications for this account')} />;
|
||||
}
|
||||
|
||||
let clearNotificationsButton;
|
||||
@ -874,16 +874,16 @@ export default class Notifications extends React.Component {
|
||||
{ spinner }
|
||||
|
||||
<LabelledToggleSwitch value={SettingsStore.getValue("notificationsEnabled")}
|
||||
onChange={this.onEnableDesktopNotificationsChange}
|
||||
label={_t('Enable desktop notifications for this session')} />
|
||||
onChange={this.onEnableDesktopNotificationsChange}
|
||||
label={_t('Enable desktop notifications for this session')} />
|
||||
|
||||
<LabelledToggleSwitch value={SettingsStore.getValue("notificationBodyEnabled")}
|
||||
onChange={this.onEnableDesktopNotificationBodyChange}
|
||||
label={_t('Show message in desktop notification')} />
|
||||
onChange={this.onEnableDesktopNotificationBodyChange}
|
||||
label={_t('Show message in desktop notification')} />
|
||||
|
||||
<LabelledToggleSwitch value={SettingsStore.getValue("audioNotificationsEnabled")}
|
||||
onChange={this.onEnableAudioNotificationsChange}
|
||||
label={_t('Enable audible notifications for this session')} />
|
||||
onChange={this.onEnableAudioNotificationsChange}
|
||||
label={_t('Enable audible notifications for this session')} />
|
||||
|
||||
{ emailNotificationsRows }
|
||||
|
||||
|
@ -170,8 +170,12 @@ export default class ProfileSettings extends React.Component {
|
||||
noValidate={true}
|
||||
className="mx_ProfileSettings_profileForm"
|
||||
>
|
||||
<input type="file" ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload"
|
||||
onChange={this._onAvatarChanged} accept="image/*" />
|
||||
<input
|
||||
type="file"
|
||||
ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload"
|
||||
onChange={this._onAvatarChanged}
|
||||
accept="image/*"
|
||||
/>
|
||||
<div className="mx_ProfileSettings_profile">
|
||||
<div className="mx_ProfileSettings_controls">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Profile")}</span>
|
||||
|
@ -90,12 +90,18 @@ export class ExistingEmailAddress extends React.Component {
|
||||
<span className="mx_ExistingEmailAddress_promptText">
|
||||
{_t("Remove %(email)s?", {email: this.props.email.address} )}
|
||||
</span>
|
||||
<AccessibleButton onClick={this._onActuallyRemove} kind="danger_sm"
|
||||
className="mx_ExistingEmailAddress_confirmBtn">
|
||||
<AccessibleButton
|
||||
onClick={this._onActuallyRemove}
|
||||
kind="danger_sm"
|
||||
className="mx_ExistingEmailAddress_confirmBtn"
|
||||
>
|
||||
{_t("Remove")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton onClick={this._onDontRemove} kind="link_sm"
|
||||
className="mx_ExistingEmailAddress_confirmBtn">
|
||||
<AccessibleButton
|
||||
onClick={this._onDontRemove}
|
||||
kind="link_sm"
|
||||
className="mx_ExistingEmailAddress_confirmBtn"
|
||||
>
|
||||
{_t("Cancel")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
@ -228,21 +234,28 @@ export default class EmailAddresses extends React.Component {
|
||||
);
|
||||
if (this.state.verifying) {
|
||||
addButton = (
|
||||
<div>
|
||||
<div>{_t("We've sent you an email to verify your address. Please follow the instructions there and then click the button below.")}</div>
|
||||
<AccessibleButton onClick={this._onContinueClick} kind="primary"
|
||||
disabled={this.state.continueDisabled}>
|
||||
{_t("Continue")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<div>
|
||||
<div>{_t("We've sent you an email to verify your address. Please follow the instructions there and then click the button below.")}</div>
|
||||
<AccessibleButton
|
||||
onClick={this._onContinueClick}
|
||||
kind="primary"
|
||||
disabled={this.state.continueDisabled}
|
||||
>
|
||||
{_t("Continue")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_EmailAddresses">
|
||||
{existingEmailElements}
|
||||
<form onSubmit={this._onAddClick} autoComplete="off"
|
||||
noValidate={true} className="mx_EmailAddresses_new">
|
||||
<form
|
||||
onSubmit={this._onAddClick}
|
||||
autoComplete="off"
|
||||
noValidate={true}
|
||||
className="mx_EmailAddresses_new"
|
||||
>
|
||||
<Field
|
||||
type="text"
|
||||
label={_t("Email Address")}
|
||||
|
@ -85,12 +85,18 @@ export class ExistingPhoneNumber extends React.Component {
|
||||
<span className="mx_ExistingPhoneNumber_promptText">
|
||||
{_t("Remove %(phone)s?", {phone: this.props.msisdn.address})}
|
||||
</span>
|
||||
<AccessibleButton onClick={this._onActuallyRemove} kind="danger_sm"
|
||||
className="mx_ExistingPhoneNumber_confirmBtn">
|
||||
<AccessibleButton
|
||||
onClick={this._onActuallyRemove}
|
||||
kind="danger_sm"
|
||||
className="mx_ExistingPhoneNumber_confirmBtn"
|
||||
>
|
||||
{_t("Remove")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton onClick={this._onDontRemove} kind="link_sm"
|
||||
className="mx_ExistingPhoneNumber_confirmBtn">
|
||||
<AccessibleButton
|
||||
onClick={this._onDontRemove}
|
||||
kind="link_sm"
|
||||
className="mx_ExistingPhoneNumber_confirmBtn"
|
||||
>
|
||||
{_t("Cancel")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
@ -246,8 +252,11 @@ export default class PhoneNumbers extends React.Component {
|
||||
value={this.state.newPhoneNumberCode}
|
||||
onChange={this._onChangeNewPhoneNumberCode}
|
||||
/>
|
||||
<AccessibleButton onClick={this._onContinueClick} kind="primary"
|
||||
disabled={this.state.continueDisabled}>
|
||||
<AccessibleButton
|
||||
onClick={this._onContinueClick}
|
||||
kind="primary"
|
||||
disabled={this.state.continueDisabled}
|
||||
>
|
||||
{_t("Continue")}
|
||||
</AccessibleButton>
|
||||
</form>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user