Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/17021

This commit is contained in:
Michael Telatynski 2021-05-06 08:05:14 +01:00
commit 90538c95aa
153 changed files with 2506 additions and 981 deletions

View file

@ -15,7 +15,6 @@ module.exports = {
"prefer-promise-reject-errors": "off", "prefer-promise-reject-errors": "off",
"no-async-promise-executor": "off", "no-async-promise-executor": "off",
"quotes": "off", "quotes": "off",
"indent": "off",
}, },
overrides: [{ overrides: [{

View file

@ -28,7 +28,7 @@ Platform Targets:
* WebRTC features (VoIP and Video calling) are only available in Chrome & Firefox. * 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 * Mobile Web is not currently a target platform - instead please use the native
iOS (https://github.com/matrix-org/matrix-ios-kit) and Android 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. All code lands on the `develop` branch - `master` is only used for stable releases.
**Please file PRs against `develop`!!** **Please file PRs against `develop`!!**

View file

@ -162,6 +162,7 @@
@import "./views/messages/_MStickerBody.scss"; @import "./views/messages/_MStickerBody.scss";
@import "./views/messages/_MTextBody.scss"; @import "./views/messages/_MTextBody.scss";
@import "./views/messages/_MVideoBody.scss"; @import "./views/messages/_MVideoBody.scss";
@import "./views/messages/_MVoiceMessageBody.scss";
@import "./views/messages/_MessageActionBar.scss"; @import "./views/messages/_MessageActionBar.scss";
@import "./views/messages/_MessageTimestamp.scss"; @import "./views/messages/_MessageTimestamp.scss";
@import "./views/messages/_MjolnirBody.scss"; @import "./views/messages/_MjolnirBody.scss";

View file

@ -79,6 +79,10 @@ $activeBorderColor: $secondary-fg-color;
.mx_SpaceItem { .mx_SpaceItem {
display: inline-flex; display: inline-flex;
flex-flow: wrap; flex-flow: wrap;
&.mx_SpaceItem_narrow {
align-self: baseline;
}
} }
.mx_SpaceItem.collapsed { .mx_SpaceItem.collapsed {
@ -275,7 +279,7 @@ $activeBorderColor: $secondary-fg-color;
.mx_SpaceButton:hover, .mx_SpaceButton:hover,
.mx_SpaceButton:focus-within, .mx_SpaceButton:focus-within,
.mx_SpaceButton_hasMenuOpen { .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 // Hide the badge container on hover because it'll be a menu button
.mx_SpacePanel_badgeContainer { .mx_SpacePanel_badgeContainer {
width: 0; width: 0;

View file

@ -101,7 +101,7 @@ limitations under the License.
.mx_BaseAvatar { .mx_BaseAvatar {
display: inline-flex; display: inline-flex;
margin: 5px 16px 5px 5px; margin: auto 16px auto 5px;
vertical-align: middle; 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 { .mx_AddExistingToSpace {
display: contents; display: contents;
} }
.mx_AddExistingToSpaceDialog_footer { .mx_AddExistingToSpaceDialog_footer {
display: flex; display: flex;
margin-top: 32px; margin-top: 20px;
> span { > span {
flex-grow: 1; flex-grow: 1;
font-size: $font-14px; font-size: $font-12px;
line-height: $font-15px; line-height: $font-15px;
font-weight: $font-semi-bold; color: $secondary-fg-color;
.mx_AccessibleButton { .mx_ProgressBar {
font-size: inherit; height: 8px;
display: inline-block; 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 { .mx_AccessibleButton {
display: inline-block; 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 { .mx_AccessibleButton_kind_link {

View file

@ -21,7 +21,7 @@ progress.mx_ProgressBar {
appearance: none; appearance: none;
border: none; border: none;
@mixin ProgressBarBorderRadius "6px"; @mixin ProgressBarBorderRadius 6px;
@mixin ProgressBarColour $progressbar-fg-color; @mixin ProgressBarColour $progressbar-fg-color;
@mixin ProgressBarBgColour $progressbar-bg-color; @mixin ProgressBarBgColour $progressbar-bg-color;
::-webkit-progress-value { ::-webkit-progress-value {

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

View file

@ -34,6 +34,10 @@ limitations under the License.
border-color: $reaction-row-button-selected-border-color; border-color: $reaction-row-button-selected-border-color;
} }
&.mx_AccessibleButton_disabled {
cursor: not-allowed;
}
.mx_ReactionsRowButton_content { .mx_ReactionsRowButton_content {
max-width: 100px; max-width: 100px;
overflow: hidden; overflow: hidden;

View file

@ -39,14 +39,14 @@ limitations under the License.
width: 14px; // w&h are size of icon width: 14px; // w&h are size of icon
height: 18px; height: 18px;
vertical-align: middle; vertical-align: middle;
margin-right: 7px; // distance from left edge of waveform container (container has some margin too) margin-right: 11px; // distance from left edge of waveform container (container has some margin too)
background-color: $muted-fg-color; background-color: $voice-record-icon-color;
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-size: contain; mask-size: contain;
mask-image: url('$(res)/img/element-icons/trashcan.svg'); 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. // Note: remaining class properties are in the PlayerContainer CSS.
margin: 6px; // force the composer area to put a gutter around us 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 position: relative; // important for the live circle
&.mx_VoiceRecordComposerTile_recording { &.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 { &::before {
animation: recording-pulse 2s infinite; animation: recording-pulse 2s infinite;
@ -65,8 +67,8 @@ limitations under the License.
width: 10px; width: 10px;
height: 10px; height: 10px;
position: absolute; position: absolute;
left: 8px; left: 12px; // 12px from the left edge for container padding
top: 16px; // vertically center top: 18px; // vertically center (middle align with clock)
border-radius: 10px; border-radius: 10px;
} }
} }

View file

@ -22,3 +22,34 @@ limitations under the License.
.mx_HelpUserSettingsTab span.mx_AccessibleButton { .mx_HelpUserSettingsTab span.mx_AccessibleButton {
word-break: break-word; 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;
}

View file

@ -19,12 +19,12 @@ limitations under the License.
width: 32px; width: 32px;
height: 32px; height: 32px;
border-radius: 32px; border-radius: 32px;
background-color: $primary-bg-color; background-color: $voice-playback-button-bg-color;
&::before { &::before {
content: ''; content: '';
position: absolute; // sizing varies by icon position: absolute; // sizing varies by icon
background-color: $muted-fg-color; background-color: $voice-playback-button-fg-color;
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-size: contain; mask-size: contain;
} }

View file

@ -19,8 +19,9 @@ limitations under the License.
// Container for live recording and playback controls // Container for live recording and playback controls
.mx_VoiceMessagePrimaryContainer { .mx_VoiceMessagePrimaryContainer {
padding: 6px; // makes us 4px taller than the send/stop button // 7px top and bottom for visual design. 12px left & right, but the waveform (right)
padding-right: 5px; // there's 1px from the waveform itself, so account for that // has a 1px padding on it that we want to account for.
padding: 7px 12px 7px 11px;
background-color: $voice-record-waveform-bg-color; background-color: $voice-record-waveform-bg-color;
border-radius: 12px; border-radius: 12px;
@ -30,11 +31,9 @@ limitations under the License.
color: $voice-record-waveform-fg-color; color: $voice-record-waveform-fg-color;
font-size: $font-14px; font-size: $font-14px;
line-height: $font-24px;
.mx_Waveform { .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 { .mx_Waveform_bar {
background-color: $voice-record-waveform-incomplete-fg-color; background-color: $voice-record-waveform-incomplete-fg-color;
@ -47,8 +46,8 @@ limitations under the License.
} }
.mx_Clock { .mx_Clock {
padding-right: 4px; // isolate from waveform width: 42px; // we're not using a monospace font, so fake it
padding-left: 8px; // isolate from live circle padding-right: 6px; // with the fixed width this ends up as a visual 8px most of the time, as intended.
width: 40px; // we're not using a monospace font, so fake it padding-left: 8px; // isolate from recording circle / play control
} }
} }

View file

@ -9,6 +9,7 @@ $header-panel-text-primary-color: #B9BEC6;
$header-panel-text-secondary-color: #c8c8cd; $header-panel-text-secondary-color: #c8c8cd;
$text-primary-color: #ffffff; $text-primary-color: #ffffff;
$text-secondary-color: #B9BEC6; $text-secondary-color: #B9BEC6;
$quaternary-fg-color: #6F7882;
$search-bg-color: #181b21; $search-bg-color: #181b21;
$search-placeholder-color: #61708b; $search-placeholder-color: #61708b;
$room-highlight-color: #343a46; $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); $groupFilterPanel-bg-color: rgba(38, 39, 43, 0.82);
$inverted-bg-color: $base-color; $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 // used by AddressSelector
$selected-color: $room-highlight-color; $selected-color: $room-highlight-color;

View file

@ -124,6 +124,15 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%)
$groupFilterPanel-divider-color: $roomlist-header-color; $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-preview-color: #9e9e9e;
$roomtile-default-badge-bg-color: #61708b; $roomtile-default-badge-bg-color: #61708b;
$roomtile-selected-bg-color: #1A1D23; $roomtile-selected-bg-color: #1A1D23;

View file

@ -192,12 +192,15 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%)
$groupFilterPanel-divider-color: $roomlist-header-color; $groupFilterPanel-divider-color: $roomlist-header-color;
// See non-legacy _light for variable information // See non-legacy _light for variable information
$voice-record-stop-border-color: #E3E8F0;
$voice-record-stop-symbol-color: #ff4b55; $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-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-preview-color: #9e9e9e;
$roomtile-default-badge-bg-color: #61708b; $roomtile-default-badge-bg-color: #61708b;

View file

@ -21,6 +21,7 @@ $notice-primary-bg-color: rgba(255, 75, 85, 0.16);
$primary-fg-color: #2e2f32; $primary-fg-color: #2e2f32;
$secondary-fg-color: #737D8C; $secondary-fg-color: #737D8C;
$tertiary-fg-color: #8D99A5; $tertiary-fg-color: #8D99A5;
$quaternary-fg-color: #C1C6CD;
$header-panel-bg-color: #f3f8fd; $header-panel-bg-color: #f3f8fd;
// typical text (dark-on-white in light skin) // 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; $groupFilterPanel-divider-color: $roomlist-header-color;
$voice-record-stop-border-color: #E3E8F0; // These two don't change between themes. They are the $warning-color, but we don't
$voice-record-stop-symbol-color: #ff4b55; // $warning-color, but without letting people change it in themes // want custom themes to affect them by accident.
$voice-record-waveform-bg-color: #E3E8F0; $voice-record-stop-symbol-color: #ff4b55;
$voice-record-waveform-fg-color: $muted-fg-color; $voice-record-live-circle-color: #ff4b55;
$voice-record-waveform-incomplete-fg-color: #C1C6CD;
$voice-record-live-circle-color: #ff4b55; // $warning-color, but without letting people change it in themes $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-preview-color: $secondary-fg-color;
$roomtile-default-badge-bg-color: #61708b; $roomtile-default-badge-bg-color: #61708b;

View file

@ -148,13 +148,15 @@ function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog( Modal.createTrackedDialog(
'Failed to add the following room to the group', 'Failed to add the following room to the group',
'', ErrorDialog, '',
{ ErrorDialog,
title: _t( {
"Failed to add the following rooms to %(groupId)s:", title: _t(
{groupId}, "Failed to add the following rooms to %(groupId)s:",
), {groupId},
description: errorList.join(", "), ),
}); description: errorList.join(", "),
},
);
}); });
} }

View file

@ -163,7 +163,7 @@ export default class IdentityAuthClient {
</div> </div>
), ),
button: _t("Trust"), button: _t("Trust"),
}); });
const [confirmed] = await finished; const [confirmed] = await finished;
if (confirmed) { if (confirmed) {
// eslint-disable-next-line react-hooks/rules-of-hooks // eslint-disable-next-line react-hooks/rules-of-hooks

View file

@ -54,7 +54,7 @@ export default class PasswordReset {
return res; return res;
}, function(err) { }, function(err) {
if (err.errcode === 'M_THREEPID_NOT_FOUND') { 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) { } else if (err.httpStatus) {
err.message = err.message + ` (Status ${err.httpStatus})`; err.message = err.message + ` (Status ${err.httpStatus})`;
} }

View file

@ -547,17 +547,23 @@ function textForMjolnirEvent(event) {
// else the entity !== prevEntity - count as a removal & add // else the entity !== prevEntity - count as a removal & add
if (USER_RULE_TYPES.includes(event.getType())) { 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", "%(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())) { } 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", "%(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())) { } 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", "%(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. // Unknown type. We'll say something but we shouldn't end up here.

View file

@ -310,7 +310,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <form onSubmit={this._onPassPhraseConfirmNextClick}> return <form onSubmit={this._onPassPhraseConfirmNextClick}>
<p>{_t( <p>{_t(
"Please enter your Security Phrase a second time to confirm.", "Enter your Security Phrase a second time to confirm it.",
)}</p> )}</p>
<div className="mx_CreateKeyBackupDialog_primaryContainer"> <div className="mx_CreateKeyBackupDialog_primaryContainer">
<div className="mx_CreateKeyBackupDialog_passPhraseContainer"> <div className="mx_CreateKeyBackupDialog_passPhraseContainer">
@ -498,9 +498,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
title={this._titleForPhase(this.state.phase)} title={this._titleForPhase(this.state.phase)}
hasCancel={[PHASE_PASSPHRASE, PHASE_DONE].includes(this.state.phase)} hasCancel={[PHASE_PASSPHRASE, PHASE_DONE].includes(this.state.phase)}
> >
<div> <div>
{content} {content}
</div> </div>
</BaseDialog> </BaseDialog>
); );
} }

View file

@ -647,7 +647,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
} }
return <form onSubmit={this._onPassPhraseConfirmNextClick}> return <form onSubmit={this._onPassPhraseConfirmNextClick}>
<p>{_t( <p>{_t(
"Enter your recovery passphrase a second time to confirm it.", "Enter your Security Phrase a second time to confirm it.",
)}</p> )}</p>
<div className="mx_CreateSecretStorageDialog_passPhraseContainer"> <div className="mx_CreateSecretStorageDialog_passPhraseContainer">
<Field <Field
@ -655,7 +655,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
onChange={this._onPassPhraseConfirmChange} onChange={this._onPassPhraseConfirmChange}
value={this.state.passPhraseConfirm} value={this.state.passPhraseConfirm}
className="mx_CreateSecretStorageDialog_passPhraseField" className="mx_CreateSecretStorageDialog_passPhraseField"
label={_t("Confirm your recovery passphrase")} label={_t("Confirm your Security Phrase")}
autoFocus={true} autoFocus={true}
autoComplete="new-password" autoComplete="new-password"
/> />
@ -856,9 +856,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
hasCancel={this.props.hasCancel && [PHASE_PASSPHRASE].includes(this.state.phase)} hasCancel={this.props.hasCancel && [PHASE_PASSPHRASE].includes(this.state.phase)}
fixedWidth={false} fixedWidth={false}
> >
<div> <div>
{content} {content}
</div> </div>
</BaseDialog> </BaseDialog>
); );
} }

View file

@ -170,8 +170,11 @@ export default class ExportE2eKeysDialog extends React.Component {
</div> </div>
</div> </div>
<div className='mx_Dialog_buttons'> <div className='mx_Dialog_buttons'>
<input className='mx_Dialog_primary' type='submit' value={_t('Export')} <input
disabled={disableForm} className='mx_Dialog_primary'
type='submit'
value={_t('Export')}
disabled={disableForm}
/> />
<button onClick={this._onCancelClick} disabled={disableForm}> <button onClick={this._onCancelClick} disabled={disableForm}>
{ _t("Cancel") } { _t("Cancel") }

View file

@ -140,36 +140,36 @@ export default class ImportE2eKeysDialog extends React.Component {
</div> </div>
<div className='mx_E2eKeysDialog_inputTable'> <div className='mx_E2eKeysDialog_inputTable'>
<div className='mx_E2eKeysDialog_inputRow'> <div className='mx_E2eKeysDialog_inputRow'>
<div className='mx_E2eKeysDialog_inputLabel'> <div className='mx_E2eKeysDialog_inputLabel'>
<label htmlFor='importFile'> <label htmlFor='importFile'>
{ _t("File to import") } { _t("File to import") }
</label> </label>
</div> </div>
<div className='mx_E2eKeysDialog_inputCell'> <div className='mx_E2eKeysDialog_inputCell'>
<input <input
ref={this._file} ref={this._file}
id='importFile' id='importFile'
type='file' type='file'
autoFocus={true} autoFocus={true}
onChange={this._onFormChange} onChange={this._onFormChange}
disabled={disableForm} /> disabled={disableForm} />
</div> </div>
</div> </div>
<div className='mx_E2eKeysDialog_inputRow'> <div className='mx_E2eKeysDialog_inputRow'>
<div className='mx_E2eKeysDialog_inputLabel'> <div className='mx_E2eKeysDialog_inputLabel'>
<label htmlFor='passphrase'> <label htmlFor='passphrase'>
{ _t("Enter passphrase") } { _t("Enter passphrase") }
</label> </label>
</div> </div>
<div className='mx_E2eKeysDialog_inputCell'> <div className='mx_E2eKeysDialog_inputCell'>
<input <input
ref={this._passphrase} ref={this._passphrase}
id='passphrase' id='passphrase'
size='64' size='64'
type='password' type='password'
onChange={this._onFormChange} onChange={this._onFormChange}
disabled={disableForm} /> disabled={disableForm} />
</div> </div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -200,10 +200,10 @@ class FilePanel extends React.Component {
previousPhase={RightPanelPhases.RoomSummary} previousPhase={RightPanelPhases.RoomSummary}
> >
<div className="mx_RoomView_empty"> <div className="mx_RoomView_empty">
{ _t("You must <a>register</a> to use this functionality", { _t("You must <a>register</a> to use this functionality",
{}, {},
{ 'a': (sub) => <a href="#/register" key="sub">{ sub }</a> }) { 'a': (sub) => <a href="#/register" key="sub">{ sub }</a> })
} }
</div> </div>
</BaseCard>; </BaseCard>;
} else if (this.noRoom) { } else if (this.noRoom) {

View file

@ -160,17 +160,17 @@ class GroupFilterPanel extends React.Component {
type="draggable-TagTile" type="draggable-TagTile"
> >
{ (provided, snapshot) => ( { (provided, snapshot) => (
<div <div
className="mx_GroupFilterPanel_tagTileContainer" className="mx_GroupFilterPanel_tagTileContainer"
ref={provided.innerRef} ref={provided.innerRef}
> >
{ this.renderGlobalIcon() } { this.renderGlobalIcon() }
{ tags } { tags }
<div> <div>
{createButton} {createButton}
</div>
{ provided.placeholder }
</div> </div>
{ provided.placeholder }
</div>
) } ) }
</Droppable> </Droppable>
</AutoHideScrollbar> </AutoHideScrollbar>

View file

@ -43,7 +43,7 @@ import {mediaFromMxc} from "../../customisations/Media";
import {replaceableComponent} from "../../utils/replaceableComponent"; import {replaceableComponent} from "../../utils/replaceableComponent";
const LONG_DESC_PLACEHOLDER = _td( const LONG_DESC_PLACEHOLDER = _td(
`<h1>HTML for your community's page</h1> `<h1>HTML for your community's page</h1>
<p> <p>
Use the long description to introduce new members to the community, or distribute Use the long description to introduce new members to the community, or distribute
some important <a href="foo">links</a> some important <a href="foo">links</a>
@ -110,14 +110,16 @@ class CategoryRoomList extends React.Component {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog( Modal.createTrackedDialog(
'Failed to add the following room to the group summary', 'Failed to add the following room to the group summary',
'', ErrorDialog, '',
{ ErrorDialog,
title: _t( {
"Failed to add the following rooms to the summary of %(groupId)s:", title: _t(
{groupId: this.props.groupId}, "Failed to add the following rooms to the summary of %(groupId)s:",
), {groupId: this.props.groupId},
description: errorList.join(", "), ),
}); description: errorList.join(", "),
},
);
}); });
}, },
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
@ -146,8 +148,8 @@ class CategoryRoomList extends React.Component {
let catHeader = <div />; let catHeader = <div />;
if (this.props.category && this.props.category.profile) { if (this.props.category && this.props.category.profile) {
catHeader = <div className="mx_GroupView_featuredThings_category"> catHeader = <div className="mx_GroupView_featuredThings_category">
{ this.props.category.profile.name } { this.props.category.profile.name }
</div>; </div>;
} }
return <div className="mx_GroupView_featuredThings_container"> return <div className="mx_GroupView_featuredThings_container">
{ catHeader } { catHeader }
@ -190,13 +192,14 @@ class FeaturedRoom extends React.Component {
Modal.createTrackedDialog( Modal.createTrackedDialog(
'Failed to remove room from group summary', 'Failed to remove room from group summary',
'', ErrorDialog, '', ErrorDialog,
{ {
title: _t( title: _t(
"Failed to remove the room from the summary of %(groupId)s", "Failed to remove the room from the summary of %(groupId)s",
{groupId: this.props.groupId}, {groupId: this.props.groupId},
), ),
description: _t("The room '%(roomName)s' could not be removed from the summary.", {roomName}), 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( Modal.createTrackedDialog(
'Failed to add the following users to the community summary', 'Failed to add the following users to the community summary',
'', ErrorDialog, '', ErrorDialog,
{ {
title: _t( title: _t(
"Failed to add the following users to the summary of %(groupId)s:", "Failed to add the following users to the summary of %(groupId)s:",
{groupId: this.props.groupId}, {groupId: this.props.groupId},
), ),
description: errorList.join(", "), description: errorList.join(", "),
}); },
);
}); });
}, },
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
@ -299,11 +303,11 @@ class RoleUserList extends React.Component {
const TintableSvg = sdk.getComponent("elements.TintableSvg"); const TintableSvg = sdk.getComponent("elements.TintableSvg");
const addButton = this.props.editing ? const addButton = this.props.editing ?
(<AccessibleButton className="mx_GroupView_featuredThings_addButton" onClick={this.onAddUsersClicked}> (<AccessibleButton className="mx_GroupView_featuredThings_addButton" onClick={this.onAddUsersClicked}>
<TintableSvg src={require("../../../res/img/icons-create-room.svg")} width="64" height="64" /> <TintableSvg src={require("../../../res/img/icons-create-room.svg")} width="64" height="64" />
<div className="mx_GroupView_featuredThings_addButton_label"> <div className="mx_GroupView_featuredThings_addButton_label">
{ _t('Add a User') } { _t('Add a User') }
</div> </div>
</AccessibleButton>) : <div />; </AccessibleButton>) : <div />;
const userNodes = this.props.users.map((u) => { const userNodes = this.props.users.map((u) => {
return <FeaturedUser return <FeaturedUser
key={u.user_id} key={u.user_id}
@ -352,14 +356,16 @@ class FeaturedUser extends React.Component {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog( Modal.createTrackedDialog(
'Failed to remove user from community summary', 'Failed to remove user from community summary',
'', ErrorDialog, '',
{ ErrorDialog,
title: _t( {
"Failed to remove a user from the summary of %(groupId)s", title: _t(
{groupId: this.props.groupId}, "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}), ),
}); 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"), title: _t("Leave Community"),
description: ( description: (
<span> <span>
{ _t("Leave %(groupName)s?", {groupName: this.props.groupId}) } { _t("Leave %(groupName)s?", {groupName: this.props.groupId}) }
{ warnings } { warnings }
</span> </span>
), ),
button: _t("Leave"), button: _t("Leave"),
@ -1055,10 +1061,11 @@ export default class GroupView extends React.Component {
return null; return null;
} }
const membershipButtonClasses = classnames([ const membershipButtonClasses = classnames(
'mx_RoomHeader_textButton', [
'mx_GroupView_textButton', 'mx_RoomHeader_textButton',
], 'mx_GroupView_textButton',
],
membershipButtonExtraClasses, membershipButtonExtraClasses,
); );

View file

@ -347,7 +347,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
if (element) { if (element) {
classes = element.classList; classes = element.classList;
} }
} while (element && !cssClasses.some(c => classes.contains(c))); } while (element && (!cssClasses.some(c => classes.contains(c)) || element.offsetParent === null));
if (element) { if (element) {
element.focus(); element.focus();
@ -416,7 +416,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
const roomList = <RoomList const roomList = <RoomList
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}
resizeNotifier={null} resizeNotifier={this.props.resizeNotifier}
onFocus={this.onFocus} onFocus={this.onFocus}
onBlur={this.onBlur} onBlur={this.onBlur}
isMinimized={this.props.isMinimized} isMinimized={this.props.isMinimized}

View file

@ -427,8 +427,10 @@ export default class MessagePanel extends React.Component {
// we get a new DOM node (restarting the animation) when the ghost // we get a new DOM node (restarting the animation) when the ghost
// moves to a different event. // moves to a different event.
return ( return (
<li key={"_readuptoghost_"+eventId} <li
className="mx_RoomView_myReadMarker_container"> key={"_readuptoghost_"+eventId}
className="mx_RoomView_myReadMarker_container"
>
{ hr } { hr }
</li> </li>
); );
@ -1014,13 +1016,13 @@ class CreationGrouper {
ret.push( ret.push(
<EventListSummary <EventListSummary
key="roomcreationsummary" key="roomcreationsummary"
events={this.events} events={this.events}
onToggle={panel._onHeightChanged} // Update scroll state onToggle={panel._onHeightChanged} // Update scroll state
summaryMembers={[ev.sender]} summaryMembers={[ev.sender]}
summaryText={summaryText} summaryText={summaryText}
> >
{ eventTiles } { eventTiles }
</EventListSummary>, </EventListSummary>,
); );
@ -1222,11 +1224,11 @@ class MemberGrouper {
ret.push( ret.push(
<MemberEventListSummary key={key} <MemberEventListSummary key={key}
events={this.events} events={this.events}
onToggle={panel._onHeightChanged} // Update scroll state onToggle={panel._onHeightChanged} // Update scroll state
startExpanded={highlightInMels} startExpanded={highlightInMels}
> >
{ eventTiles } { eventTiles }
</MemberEventListSummary>, </MemberEventListSummary>,
); );

View file

@ -17,6 +17,8 @@ limitations under the License.
import * as React from "react"; import * as React from "react";
import { createRef } from "react"; import { createRef } from "react";
import classNames from "classnames"; import classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room";
import defaultDispatcher from "../../dispatcher/dispatcher"; import defaultDispatcher from "../../dispatcher/dispatcher";
import { _t } from "../../languageHandler"; import { _t } from "../../languageHandler";
import { ActionPayload } from "../../dispatcher/payloads"; 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 { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition";
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager"; import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
import {replaceableComponent} from "../../utils/replaceableComponent"; 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 { interface IProps {
isMinimized: boolean; isMinimized: boolean;
@ -40,6 +42,7 @@ interface IProps {
interface IState { interface IState {
query: string; query: string;
focused: boolean; focused: boolean;
inSpaces: boolean;
} }
@replaceableComponent("structures.RoomSearch") @replaceableComponent("structures.RoomSearch")
@ -54,11 +57,13 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
this.state = { this.state = {
query: "", query: "",
focused: false, focused: false,
inSpaces: false,
}; };
this.dispatcherRef = defaultDispatcher.register(this.onAction); this.dispatcherRef = defaultDispatcher.register(this.onAction);
// clear filter when changing spaces, in future we may wish to maintain a filter per-space // 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_SELECTED_SPACE, this.clearInput);
SpaceStore.instance.on(UPDATE_TOP_LEVEL_SPACES, this.onSpaces);
} }
public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>): void { public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>): void {
@ -79,8 +84,15 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
public componentWillUnmount() { public componentWillUnmount() {
defaultDispatcher.unregister(this.dispatcherRef); defaultDispatcher.unregister(this.dispatcherRef);
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.clearInput); 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) => { private onAction = (payload: ActionPayload) => {
if (payload.action === 'view_room' && payload.clear_search) { if (payload.action === 'view_room' && payload.clear_search) {
this.clearInput(); this.clearInput();
@ -152,6 +164,11 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
'mx_RoomSearch_inputExpanded': this.state.query || this.state.focused, 'mx_RoomSearch_inputExpanded': this.state.query || this.state.focused,
}); });
let placeholder = _t("Filter");
if (this.state.inSpaces) {
placeholder = _t("Filter all spaces");
}
let icon = ( let icon = (
<div className='mx_RoomSearch_icon' /> <div className='mx_RoomSearch_icon' />
); );
@ -165,7 +182,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
onBlur={this.onBlur} onBlur={this.onBlur}
onChange={this.onChange} onChange={this.onChange}
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}
placeholder={_t("Filter")} placeholder={placeholder}
autoComplete="off" autoComplete="off"
/> />
); );

View file

@ -200,20 +200,22 @@ export default class RoomStatusBar extends React.Component {
} else if (resourceLimitError) { } else if (resourceLimitError) {
title = messageForResourceLimitError( title = messageForResourceLimitError(
resourceLimitError.data.limit_type, resourceLimitError.data.limit_type,
resourceLimitError.data.admin_contact, { resourceLimitError.data.admin_contact,
'monthly_active_user': _td( {
"Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. " + 'monthly_active_user': _td(
"Please <a>contact your service administrator</a> to continue using the service.", "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. " + 'hs_disabled': _td(
"Please <a>contact your service administrator</a> to continue using the service.", "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. " + '': _td(
"Please <a>contact your service administrator</a> to continue using the service.", "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 { } else {
title = _t('Some of your messages have not been sent'); 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 role="alert">
<div className="mx_RoomStatusBar_connectionLostBar"> <div className="mx_RoomStatusBar_connectionLostBar">
<img src={require("../../../res/img/feather-customised/warning-triangle.svg")} width="24" <img src={require("../../../res/img/feather-customised/warning-triangle.svg")} width="24"
height="24" title="/!\ " alt="/!\ " /> height="24" title="/!\ " alt="/!\ " />
<div> <div>
<div className="mx_RoomStatusBar_connectionLostBar_title"> <div className="mx_RoomStatusBar_connectionLostBar_title">
{_t('Connectivity to the server has been lost.')} {_t('Connectivity to the server has been lost.')}

View file

@ -190,6 +190,9 @@ export interface IState {
rejectError?: Error; rejectError?: Error;
hasPinnedWidgets?: boolean; hasPinnedWidgets?: boolean;
dragCounter: number; 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") @replaceableComponent("structures.RoomView")
@ -326,6 +329,7 @@ export default class RoomView extends React.Component<IProps, IState> {
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(), shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId), showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId),
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId), showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
wasContextSwitch: RoomViewStore.getWasContextSwitch(),
}; };
if (!initial && this.state.shouldPeek && !newState.shouldPeek) { 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()} timelineSet={this.state.room.getUnfilteredTimelineSet()}
showReadReceipts={this.state.showReadReceipts} showReadReceipts={this.state.showReadReceipts}
manageReadReceipts={!this.state.isPeeking} manageReadReceipts={!this.state.isPeeking}
sendReadReceiptOnLoad={!this.state.wasContextSwitch}
manageReadMarkers={!this.state.isPeeking} manageReadMarkers={!this.state.isPeeking}
hidden={hideMessagePanel} hidden={hideMessagePanel}
highlightedEventId={highlightedEventId} highlightedEventId={highlightedEventId}

View file

@ -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 // 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 // list-style-type: none; is no longer a list
return (<AutoHideScrollbar wrappedRef={this._collectScroll} return (
<AutoHideScrollbar
wrappedRef={this._collectScroll}
onScroll={this.onScroll} onScroll={this.onScroll}
className={`mx_ScrollPanel ${this.props.className}`} style={this.props.style}> className={`mx_ScrollPanel ${this.props.className}`}
{ this.props.fixedChildren } style={this.props.style}
<div className="mx_RoomView_messageListWrapper"> >
<ol ref={this._itemlist} className="mx_RoomView_MessageList" aria-live="polite" role="list"> { this.props.fixedChildren }
{ this.props.children } <div className="mx_RoomView_messageListWrapper">
</ol> <ol ref={this._itemlist} className="mx_RoomView_MessageList" aria-live="polite" role="list">
</div> { this.props.children }
</AutoHideScrollbar> </ol>
); </div>
</AutoHideScrollbar>
);
} }
} }

View file

@ -39,6 +39,7 @@ import {mediaFromMxc} from "../../customisations/Media";
import InfoTooltip from "../views/elements/InfoTooltip"; import InfoTooltip from "../views/elements/InfoTooltip";
import TextWithTooltip from "../views/elements/TextWithTooltip"; import TextWithTooltip from "../views/elements/TextWithTooltip";
import {useStateToggle} from "../../hooks/useStateToggle"; import {useStateToggle} from "../../hooks/useStateToggle";
import {getOrder} from "../../stores/SpaceStore";
interface IHierarchyProps { interface IHierarchyProps {
space: Room; space: Room;
@ -254,7 +255,11 @@ export const HierarchyLevel = ({
const space = cli.getRoom(spaceId); const space = cli.getRoom(spaceId);
const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId()); 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 [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => {
const roomId = ev.state_key; const roomId = ev.state_key;
if (!rooms.has(roomId)) return result; if (!rooms.has(roomId)) return result;

View file

@ -52,7 +52,7 @@ import {useStateToggle} from "../../hooks/useStateToggle";
import SpaceStore from "../../stores/SpaceStore"; import SpaceStore from "../../stores/SpaceStore";
import FacePile from "../views/elements/FacePile"; import FacePile from "../views/elements/FacePile";
import {AddExistingToSpace} from "../views/dialogs/AddExistingToSpaceDialog"; import {AddExistingToSpace} from "../views/dialogs/AddExistingToSpaceDialog";
import {allSettled} from "../../utils/promise"; import {sleep} from "../../utils/promise";
import {calculateRoomVia} from "../../utils/permalinks/Permalinks"; import {calculateRoomVia} from "../../utils/permalinks/Permalinks";
import {BetaPill} from "../views/beta/BetaCard"; import {BetaPill} from "../views/beta/BetaCard";
import {USER_LABS_TAB} from "../views/dialogs/UserSettingsDialog"; import {USER_LABS_TAB} from "../views/dialogs/UserSettingsDialog";
@ -434,15 +434,24 @@ const SpaceAddExistingRooms = ({ space, onFinished }) => {
let buttonLabel = _t("Skip for now"); let buttonLabel = _t("Skip for now");
if (selectedToAdd.size > 0) { if (selectedToAdd.size > 0) {
onClick = async () => { onClick = async () => {
// TODO rate limiting
setBusy(true); setBusy(true);
try {
await allSettled(Array.from(selectedToAdd).map((room) => for (const room of selectedToAdd) {
SpaceStore.instance.addRoomToSpace(space, room.roomId, calculateRoomVia(room)))); const via = calculateRoomVia(room);
onFinished(true); try {
} catch (e) { await SpaceStore.instance.addRoomToSpace(space, room.roomId, via).catch(async e => {
console.error("Failed to add rooms to space", e); if (e.errcode === "M_LIMIT_EXCEEDED") {
setError(_t("Failed to add rooms to space")); 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); setBusy(false);
}; };

View file

@ -68,6 +68,7 @@ class TimelinePanel extends React.Component {
showReadReceipts: PropTypes.bool, showReadReceipts: PropTypes.bool,
// Enable managing RRs and RMs. These require the timelineSet to have a room. // Enable managing RRs and RMs. These require the timelineSet to have a room.
manageReadReceipts: PropTypes.bool, manageReadReceipts: PropTypes.bool,
sendReadReceiptOnLoad: PropTypes.bool,
manageReadMarkers: PropTypes.bool, manageReadMarkers: PropTypes.bool,
// true to give the component a 'display: none' style. // true to give the component a 'display: none' style.
@ -126,6 +127,7 @@ class TimelinePanel extends React.Component {
// event tile heights. (See _unpaginateEvents) // event tile heights. (See _unpaginateEvents)
timelineCap: Number.MAX_VALUE, timelineCap: Number.MAX_VALUE,
className: 'mx_RoomView_messagePanel', className: 'mx_RoomView_messagePanel',
sendReadReceiptOnLoad: true,
}; };
constructor(props) { constructor(props) {
@ -785,8 +787,10 @@ class TimelinePanel extends React.Component {
return; return;
} }
const lastDisplayedEvent = this.state.events[lastDisplayedIndex]; const lastDisplayedEvent = this.state.events[lastDisplayedIndex];
this._setReadMarker(lastDisplayedEvent.getId(), this._setReadMarker(
lastDisplayedEvent.getTs()); lastDisplayedEvent.getId(),
lastDisplayedEvent.getTs(),
);
// the read-marker should become invisible, so that if the user scrolls // the read-marker should become invisible, so that if the user scrolls
// down, they don't see it. // 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 messagepanel knows where the RM is, so we must have loaded
// the relevant event. // the relevant event.
this._messagePanel.current.scrollToEvent(this.state.readMarkerEventId, this._messagePanel.current.scrollToEvent(this.state.readMarkerEventId,
0, 1/3); 0, 1/3);
return; return;
} }
@ -1044,12 +1048,14 @@ class TimelinePanel extends React.Component {
} }
if (eventId) { if (eventId) {
this._messagePanel.current.scrollToEvent(eventId, pixelOffset, this._messagePanel.current.scrollToEvent(eventId, pixelOffset,
offsetBase); offsetBase);
} else { } else {
this._messagePanel.current.scrollToBottom(); 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) ['PREPARED', 'CATCHUP'].includes(this.state.clientSyncState)
); );
const events = this.state.firstVisibleEventIndex const events = this.state.firstVisibleEventIndex
? this.state.events.slice(this.state.firstVisibleEventIndex) ? this.state.events.slice(this.state.firstVisibleEventIndex)
: this.state.events; : this.state.events;
return ( return (
<MessagePanel <MessagePanel
ref={this._messagePanel} ref={this._messagePanel}

View file

@ -169,7 +169,7 @@ export class PasswordAuthEntry extends React.Component {
{ submitButtonOrSpinner } { submitButtonOrSpinner }
</div> </div>
</form> </form>
{ errorSection } { errorSection }
</div> </div>
); );
} }
@ -375,7 +375,7 @@ export class TermsAuthEntry extends React.Component {
if (this.props.showContinue !== false) { if (this.props.showContinue !== false) {
// XXX: button classes // XXX: button classes
submitButton = <button className="mx_InteractiveAuthEntryComponents_termsSubmit mx_GeneralButton" submitButton = <button className="mx_InteractiveAuthEntryComponents_termsSubmit mx_GeneralButton"
onClick={this._trySubmit} disabled={!allChecked}>{_t("Accept")}</button>; onClick={this._trySubmit} disabled={!allChecked}>{_t("Accept")}</button>;
} }
return ( return (

View file

@ -350,7 +350,7 @@ export default class MessageContextMenu extends React.Component {
> >
{ _t('Source URL') } { _t('Source URL') }
</MenuItem> </MenuItem>
); );
} }
if (this.props.collapseReplyThread) { if (this.props.collapseReplyThread) {

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, {useContext, useState} from "react"; import React, {useContext, useMemo, useState} from "react";
import classNames from "classnames"; import classNames from "classnames";
import {Room} from "matrix-js-sdk/src/models/room"; import {Room} from "matrix-js-sdk/src/models/room";
import {MatrixClient} from "matrix-js-sdk/src/client"; import {MatrixClient} from "matrix-js-sdk/src/client";
@ -29,11 +29,13 @@ import RoomAvatar from "../avatars/RoomAvatar";
import {getDisplayAliasForRoom} from "../../../Rooms"; import {getDisplayAliasForRoom} from "../../../Rooms";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import {allSettled} from "../../../utils/promise"; import {sleep} from "../../../utils/promise";
import DMRoomMap from "../../../utils/DMRoomMap"; import DMRoomMap from "../../../utils/DMRoomMap";
import {calculateRoomVia} from "../../../utils/permalinks/Permalinks"; import {calculateRoomVia} from "../../../utils/permalinks/Permalinks";
import StyledCheckbox from "../elements/StyledCheckbox"; import StyledCheckbox from "../elements/StyledCheckbox";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
import ProgressBar from "../elements/ProgressBar";
interface IProps extends IDialogProps { interface IProps extends IDialogProps {
matrixClient: MatrixClient; matrixClient: MatrixClient;
@ -45,7 +47,11 @@ const Entry = ({ room, checked, onChange }) => {
return <label className="mx_AddExistingToSpace_entry"> return <label className="mx_AddExistingToSpace_entry">
<RoomAvatar room={room} height={32} width={32} /> <RoomAvatar room={room} height={32} width={32} />
<span className="mx_AddExistingToSpace_entry_name">{ room.name }</span> <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>; </label>;
}; };
@ -57,6 +63,8 @@ interface IAddExistingToSpaceProps {
export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space, selected, onChange }) => { export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space, selected, onChange }) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const visibleRooms = useMemo(() => sortRooms(cli.getVisibleRooms()), [cli]);
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const lcQuery = query.toLowerCase(); 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 existingRoomsSet = new Set(SpaceStore.instance.getChildRooms(space.roomId));
const joinRule = space.getJoinRule(); 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.getMyMembership() !== "join") return arr;
if (!room.name.toLowerCase().includes(lcQuery)) return arr; if (!room.name.toLowerCase().includes(lcQuery)) return arr;
@ -101,9 +109,9 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space,
key={room.roomId} key={room.roomId}
room={room} room={room}
checked={selected.has(room)} checked={selected.has(room)}
onChange={(checked) => { onChange={onChange ? (checked) => {
onChange(checked, room); onChange(checked, room);
}} } : null}
/>; />;
}) } }) }
</div> </div>
@ -117,9 +125,9 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space,
key={space.roomId} key={space.roomId}
room={space} room={space}
checked={selected.has(space)} checked={selected.has(space)}
onChange={(checked) => { onChange={onChange ? (checked) => {
onChange(checked, space); onChange(checked, space);
}} } : null}
/>; />;
}) } }) }
</div> </div>
@ -133,9 +141,9 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space,
key={room.roomId} key={room.roomId}
room={room} room={room}
checked={selected.has(room)} checked={selected.has(room)}
onChange={(checked) => { onChange={onChange ? (checked) => {
onChange(checked, room); onChange(checked, room);
}} } : null}
/>; />;
}) } }) }
</div> </div>
@ -153,8 +161,8 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId); const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>()); const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
const [busy, setBusy] = useState(false); const [progress, setProgress] = useState<number>(null);
const [error, setError] = useState(""); const [error, setError] = useState<Error>(null);
let spaceOptionSection; let spaceOptionSection;
if (existingSubspaces.length > 0) { if (existingSubspaces.length > 0) {
@ -194,6 +202,82 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
</div> </div>
</React.Fragment>; </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 return <BaseDialog
title={title} title={title}
className="mx_AddExistingToSpaceDialog" className="mx_AddExistingToSpaceDialog"
@ -201,50 +285,23 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
onFinished={onFinished} onFinished={onFinished}
fixedWidth={false} fixedWidth={false}
> >
{ error && <div className="mx_AddExistingToSpaceDialog_errorText">{ error }</div> }
<MatrixClientContext.Provider value={cli}> <MatrixClientContext.Provider value={cli}>
<AddExistingToSpace <AddExistingToSpace
space={space} space={space}
selected={selectedToAdd} selected={selectedToAdd}
onChange={(checked, room) => { onChange={!busy && !error ? (checked, room) => {
if (checked) { if (checked) {
selectedToAdd.add(room); selectedToAdd.add(room);
} else { } else {
selectedToAdd.delete(room); selectedToAdd.delete(room);
} }
setSelectedToAdd(new Set(selectedToAdd)); setSelectedToAdd(new Set(selectedToAdd));
}} } : null}
/> />
</MatrixClientContext.Provider> </MatrixClientContext.Provider>
<div className="mx_AddExistingToSpaceDialog_footer"> <div className="mx_AddExistingToSpaceDialog_footer">
<span> { footer }
<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>
</div> </div>
</BaseDialog>; </BaseDialog>;
}; };

View file

@ -184,7 +184,7 @@ export default class BugReportDialog extends React.Component {
return ( return (
<BaseDialog className="mx_BugReportDialog" onFinished={this._onCancel} <BaseDialog className="mx_BugReportDialog" onFinished={this._onCancel}
title={_t('Submit debug logs')} title={_t('Submit debug logs')}
contentId='mx_Dialog_content' contentId='mx_Dialog_content'
> >
<div className="mx_Dialog_content" id='mx_Dialog_content'> <div className="mx_Dialog_content" id='mx_Dialog_content'>

View file

@ -95,7 +95,7 @@ export default class ChangelogDialog extends React.Component {
description={content} description={content}
button={_t("Update")} button={_t("Update")}
onFinished={this.props.onFinished} onFinished={this.props.onFinished}
/> />
); );
} }
} }

View file

@ -39,9 +39,12 @@ export default class ConfirmWipeDeviceDialog extends React.Component {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return ( return (
<BaseDialog className='mx_ConfirmWipeDeviceDialog' hasCancel={true} <BaseDialog
onFinished={this.props.onFinished} className='mx_ConfirmWipeDeviceDialog'
title={_t("Clear all data in this session?")}> hasCancel={true}
onFinished={this.props.onFinished}
title={_t("Clear all data in this session?")}
>
<div className='mx_ConfirmWipeDeviceDialog_content'> <div className='mx_ConfirmWipeDeviceDialog_content'>
<p> <p>
{_t( {_t(

View file

@ -70,8 +70,16 @@ class GenericEditor extends React.PureComponent {
} }
textInput(id, label) { textInput(id, label) {
return <Field id={id} label={label} size="42" autoFocus={true} type="text" autoComplete="on" return <Field
value={this.state[id]} onChange={this._onChange} />; 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 /> <br />
<Field id="evContent" label={_t("Event Content")} type="text" className="mx_DevTools_textarea" <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>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button onClick={this.onBack}>{ _t('Back') }</button> <button onClick={this.onBack}>{ _t('Back') }</button>
@ -239,7 +247,7 @@ class SendAccountData extends GenericEditor {
<br /> <br />
<Field id="evContent" label={_t("Event Content")} type="text" className="mx_DevTools_textarea" <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>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button onClick={this.onBack}>{ _t('Back') }</button> <button onClick={this.onBack}>{ _t('Back') }</button>
@ -315,15 +323,15 @@ class FilteredList extends React.PureComponent {
const TruncatedList = sdk.getComponent("elements.TruncatedList"); const TruncatedList = sdk.getComponent("elements.TruncatedList");
return <div> return <div>
<Field label={_t('Filter results')} autoFocus={true} size={64} <Field label={_t('Filter results')} autoFocus={true} size={64}
type="text" autoComplete="off" value={this.props.query} onChange={this.onQuery} type="text" autoComplete="off" value={this.props.query} onChange={this.onQuery}
className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query" className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query"
// force re-render so that autoFocus is applied when this component is re-used // force re-render so that autoFocus is applied when this component is re-used
key={this.props.children[0] ? this.props.children[0].key : ''} /> key={this.props.children[0] ? this.props.children[0].key : ''} />
<TruncatedList getChildren={this.getChildren} <TruncatedList getChildren={this.getChildren}
getChildCount={this.getChildCount} getChildCount={this.getChildCount}
truncateAt={this.state.truncateAt} truncateAt={this.state.truncateAt}
createOverflowElement={this.createOverflowElement} /> createOverflowElement={this.createOverflowElement} />
</div>; </div>;
} }
} }
@ -647,7 +655,7 @@ function VerificationRequest({txnId, request}) {
/* Note that request.timeout is a getter, so its value changes */ /* Note that request.timeout is a getter, so its value changes */
const id = setInterval(() => { const id = setInterval(() => {
setRequestTimeout(request.timeout); setRequestTimeout(request.timeout);
}, 500); }, 500);
return () => { clearInterval(id); }; return () => { clearInterval(id); };
@ -941,35 +949,35 @@ class SettingsExplorer extends React.Component {
/> />
<table> <table>
<thead> <thead>
<tr> <tr>
<th>{_t("Setting ID")}</th> <th>{_t("Setting ID")}</th>
<th>{_t("Value")}</th> <th>{_t("Value")}</th>
<th>{_t("Value in this room")}</th> <th>{_t("Value in this room")}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{allSettings.map(i => ( {allSettings.map(i => (
<tr key={i}> <tr key={i}>
<td> <td>
<a href="" onClick={(e) => this.onViewClick(e, i)}> <a href="" onClick={(e) => this.onViewClick(e, i)}>
<code>{i}</code> <code>{i}</code>
</a> </a>
<a href="" onClick={(e) => this.onEditClick(e, i)} <a href="" onClick={(e) => this.onEditClick(e, i)}
className='mx_DevTools_SettingsExplorer_edit' className='mx_DevTools_SettingsExplorer_edit'
> >
</a> </a>
</td> </td>
<td> <td>
<code>{this.renderSettingValue(SettingsStore.getValue(i))}</code> <code>{this.renderSettingValue(SettingsStore.getValue(i))}</code>
</td> </td>
<td> <td>
<code> <code>
{this.renderSettingValue(SettingsStore.getValue(i, room.roomId))} {this.renderSettingValue(SettingsStore.getValue(i, room.roomId))}
</code> </code>
</td> </td>
</tr> </tr>
))} ))}
</tbody> </tbody>
</table> </table>
</div> </div>
@ -998,11 +1006,11 @@ class SettingsExplorer extends React.Component {
<div> <div>
<table> <table>
<thead> <thead>
<tr> <tr>
<th>{_t("Level")}</th> <th>{_t("Level")}</th>
<th>{_t("Settable at global")}</th> <th>{_t("Settable at global")}</th>
<th>{_t("Settable at room")}</th> <th>{_t("Settable at room")}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{LEVEL_ORDER.map(lvl => ( {LEVEL_ORDER.map(lvl => (

View file

@ -42,9 +42,12 @@ export default class IntegrationsDisabledDialog extends React.Component {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return ( return (
<BaseDialog className='mx_IntegrationsDisabledDialog' hasCancel={true} <BaseDialog
onFinished={this.props.onFinished} className='mx_IntegrationsDisabledDialog'
title={_t("Integrations are disabled")}> hasCancel={true}
onFinished={this.props.onFinished}
title={_t("Integrations are disabled")}
>
<div className='mx_IntegrationsDisabledDialog_content'> <div className='mx_IntegrationsDisabledDialog_content'>
<p>{_t("Enable 'Manage Integrations' in Settings to do this.")}</p> <p>{_t("Enable 'Manage Integrations' in Settings to do this.")}</p>
</div> </div>

View file

@ -37,9 +37,12 @@ export default class IntegrationsImpossibleDialog extends React.Component {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return ( return (
<BaseDialog className='mx_IntegrationsImpossibleDialog' hasCancel={false} <BaseDialog
onFinished={this.props.onFinished} className='mx_IntegrationsImpossibleDialog'
title={_t("Integrations not allowed")}> hasCancel={false}
onFinished={this.props.onFinished}
title={_t("Integrations not allowed")}
>
<div className='mx_IntegrationsImpossibleDialog_content'> <div className='mx_IntegrationsImpossibleDialog_content'>
<p> <p>
{_t( {_t(

View file

@ -24,7 +24,7 @@ export default function KeySignatureUploadFailedDialog({
source, source,
continuation, continuation,
onFinished, onFinished,
}) { }) {
const RETRIES = 2; const RETRIES = 2;
const BaseDialog = sdk.getComponent('dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
@ -84,10 +84,10 @@ export default function KeySignatureUploadFailedDialog({
} else { } else {
body = (<div> body = (<div>
{success ? {success ?
<span>{_t("Upload completed")}</span> : <span>{_t("Upload completed")}</span> :
cancelled ? cancelled ?
<span>{_t("Cancelled signature upload")}</span> : <span>{_t("Cancelled signature upload")}</span> :
<span>{_t("Unable to upload")}</span>} <span>{_t("Unable to upload")}</span>}
<DialogButtons <DialogButtons
primaryButton={_t("OK")} primaryButton={_t("OK")}
hasCancel={false} hasCancel={false}

View file

@ -164,8 +164,12 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
} }
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return ( return (
<BaseDialog className='mx_MessageEditHistoryDialog' hasCancel={true} <BaseDialog
onFinished={this.props.onFinished} title={_t("Message edits")}> className='mx_MessageEditHistoryDialog'
hasCancel={true}
onFinished={this.props.onFinished}
title={_t("Message edits")}
>
{content} {content}
</BaseDialog> </BaseDialog>
); );

View file

@ -116,8 +116,12 @@ export default class RoomSettingsDialog extends React.Component {
const roomName = MatrixClientPeg.get().getRoom(this.props.roomId).name; const roomName = MatrixClientPeg.get().getRoom(this.props.roomId).name;
return ( return (
<BaseDialog className='mx_RoomSettingsDialog' hasCancel={true} <BaseDialog
onFinished={this.props.onFinished} title={_t("Room Settings - %(roomName)s", {roomName})}> className='mx_RoomSettingsDialog'
hasCancel={true}
onFinished={this.props.onFinished}
title={_t("Room Settings - %(roomName)s", {roomName})}
>
<div className='mx_SettingsDialog_content'> <div className='mx_SettingsDialog_content'>
<TabbedView tabs={this._getTabs()} /> <TabbedView tabs={this._getTabs()} />
</div> </div>

View file

@ -98,7 +98,7 @@ export default class SessionRestoreErrorDialog extends React.Component {
"may be incompatible with this version. Close this window and return " + "may be incompatible with this version. Close this window and return " +
"to the more recent version.", "to the more recent version.",
{ brand }, { brand },
) }</p> ) }</p>
<p>{ _t( <p>{ _t(
"Clearing your browser's storage may fix the problem, but will sign you " + "Clearing your browser's storage may fix the problem, but will sign you " +

View file

@ -45,10 +45,12 @@ export default class StorageEvictedDialog extends React.Component {
let logRequest; let logRequest;
if (SdkConfig.get().bug_report_endpoint_url) { if (SdkConfig.get().bug_report_endpoint_url) {
logRequest = _t( logRequest = _t(
"To help us prevent this in future, please <a>send us logs</a>.", {}, "To help us prevent this in future, please <a>send us logs</a>.",
{ {},
a: text => <a href="#" onClick={this._sendBugReport}>{text}</a>, {
}); a: text => <a href="#" onClick={this._sendBugReport}>{text}</a>,
},
);
} }
return ( return (

View file

@ -158,8 +158,12 @@ export default class UserSettingsDialog extends React.Component {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return ( return (
<BaseDialog className='mx_UserSettingsDialog' hasCancel={true} <BaseDialog
onFinished={this.props.onFinished} title={_t("Settings")}> className='mx_UserSettingsDialog'
hasCancel={true}
onFinished={this.props.onFinished}
title={_t("Settings")}
>
<div className='mx_SettingsDialog_content'> <div className='mx_SettingsDialog_content'>
<TabbedView tabs={this._getTabs()} initialTabId={this.props.initialTabId} /> <TabbedView tabs={this._getTabs()} initialTabId={this.props.initialTabId} />
</div> </div>

View file

@ -52,11 +52,13 @@ export default class VerificationRequestDialog extends React.Component {
const title = request && request.isSelfVerification ? const title = request && request.isSelfVerification ?
_t("Verify other login") : _t("Verification Request"); _t("Verify other login") : _t("Verification Request");
return <BaseDialog className="mx_InfoDialog" onFinished={this.props.onFinished} return <BaseDialog
contentId="mx_Dialog_content" className="mx_InfoDialog"
title={title} onFinished={this.props.onFinished}
hasCancel={true} contentId="mx_Dialog_content"
> title={title}
hasCancel={true}
>
<EncryptionPanel <EncryptionPanel
layout="dialog" layout="dialog"
verificationRequest={this.props.verificationRequest} verificationRequest={this.props.verificationRequest}

View file

@ -70,9 +70,12 @@ export default class WidgetOpenIDPermissionsDialog extends React.Component {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return ( return (
<BaseDialog className='mx_WidgetOpenIDPermissionsDialog' hasCancel={true} <BaseDialog
onFinished={this.props.onFinished} className='mx_WidgetOpenIDPermissionsDialog'
title={_t("Allow this widget to verify your identity")}> hasCancel={true}
onFinished={this.props.onFinished}
title={_t("Allow this widget to verify your identity")}
>
<div className='mx_WidgetOpenIDPermissionsDialog_content'> <div className='mx_WidgetOpenIDPermissionsDialog_content'>
<p> <p>
{_t("The widget will verify your user ID, but won't be able to perform actions for you:")} {_t("The widget will verify your user ID, but won't be able to perform actions for you:")}

View file

@ -40,10 +40,11 @@ export default class ConfirmDestroyCrossSigningDialog extends React.Component {
return ( return (
<BaseDialog <BaseDialog
className='mx_ConfirmDestroyCrossSigningDialog' className='mx_ConfirmDestroyCrossSigningDialog'
hasCancel={true} hasCancel={true}
onFinished={this.props.onFinished} onFinished={this.props.onFinished}
title={_t("Destroy cross-signing keys?")}> title={_t("Destroy cross-signing keys?")}
>
<div className='mx_ConfirmDestroyCrossSigningDialog_content'> <div className='mx_ConfirmDestroyCrossSigningDialog_content'>
<p> <p>
{_t( {_t(

View file

@ -373,21 +373,24 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
{_t( {_t(
"If you've forgotten your Security Phrase you can "+ "If you've forgotten your Security Phrase you can "+
"<button1>use your Security Key</button1> or " + "<button1>use your Security Key</button1> or " +
"<button2>set up new recovery options</button2>" "<button2>set up new recovery options</button2>",
, {}, { {},
button1: s => <AccessibleButton className="mx_linkButton" {
element="span" button1: s => <AccessibleButton
onClick={this._onUseRecoveryKeyClick} className="mx_linkButton"
> element="span"
{s} onClick={this._onUseRecoveryKeyClick}
</AccessibleButton>, >
button2: s => <AccessibleButton className="mx_linkButton" {s}
element="span" </AccessibleButton>,
onClick={this._onResetRecoveryClick} button2: s => <AccessibleButton
> className="mx_linkButton"
{s} element="span"
</AccessibleButton>, onClick={this._onResetRecoveryClick}
})} >
{s}
</AccessibleButton>,
})}
</div>; </div>;
} else { } else {
title = _t("Enter Security Key"); title = _t("Enter Security Key");
@ -435,15 +438,17 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
</div> </div>
{_t( {_t(
"If you've forgotten your Security Key you can "+ "If you've forgotten your Security Key you can "+
"<button>set up new recovery options</button>" "<button>set up new recovery options</button>",
, {}, { {},
button: s => <AccessibleButton className="mx_linkButton" {
element="span" button: s => <AccessibleButton className="mx_linkButton"
onClick={this._onResetRecoveryClick} element="span"
> onClick={this._onResetRecoveryClick}
{s} >
</AccessibleButton>, {s}
})} </AccessibleButton>,
},
)}
</div>; </div>;
} }
@ -452,9 +457,9 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
onFinished={this.props.onFinished} onFinished={this.props.onFinished}
title={title} title={title}
> >
<div className='mx_RestoreKeyBackupDialog_content'> <div className='mx_RestoreKeyBackupDialog_content'>
{content} {content}
</div> </div>
</BaseDialog> </BaseDialog>
); );
} }

View file

@ -71,8 +71,8 @@ export default class ActionButton extends React.Component {
} }
const icon = this.props.iconPath ? const icon = this.props.iconPath ?
(<TintableSvg src={this.props.iconPath} width={this.props.size} height={this.props.size} />) : (<TintableSvg src={this.props.iconPath} width={this.props.size} height={this.props.size} />) :
undefined; undefined;
const classNames = ["mx_RoleButton"]; const classNames = ["mx_RoleButton"];
if (this.props.className) { if (this.props.className) {

View file

@ -109,7 +109,7 @@ export default class AppTile extends React.Component {
const childContentProtocol = u.protocol; const childContentProtocol = u.protocol;
if (parentContentProtocol === 'https:' && childContentProtocol !== 'https:') { if (parentContentProtocol === 'https:' && childContentProtocol !== 'https:') {
console.warn("Refusing to load mixed-content app:", 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 true;
} }
return false; return false;

View file

@ -65,12 +65,18 @@ export class EditableItem extends React.Component {
<span className="mx_EditableItem_promptText"> <span className="mx_EditableItem_promptText">
{_t("Are you sure?")} {_t("Are you sure?")}
</span> </span>
<AccessibleButton onClick={this._onActuallyRemove} kind="primary_sm" <AccessibleButton
className="mx_EditableItem_confirmBtn"> onClick={this._onActuallyRemove}
kind="primary_sm"
className="mx_EditableItem_confirmBtn"
>
{_t("Yes")} {_t("Yes")}
</AccessibleButton> </AccessibleButton>
<AccessibleButton onClick={this._onDontRemove} kind="danger_sm" <AccessibleButton
className="mx_EditableItem_confirmBtn"> onClick={this._onDontRemove}
kind="danger_sm"
className="mx_EditableItem_confirmBtn"
>
{_t("No")} {_t("No")}
</AccessibleButton> </AccessibleButton>
</div> </div>
@ -121,11 +127,15 @@ export default class EditableItemList extends React.Component {
_renderNewItemField() { _renderNewItemField() {
return ( return (
<form onSubmit={this._onItemAdded} autoComplete="off" <form
noValidate={true} className="mx_EditableItemList_newItem"> onSubmit={this._onItemAdded}
autoComplete="off"
noValidate={true}
className="mx_EditableItemList_newItem"
>
<Field label={this.props.placeholder} type="text" <Field label={this.props.placeholder} type="text"
autoComplete="off" value={this.props.newItem || ""} onChange={this._onNewItemChanged} autoComplete="off" value={this.props.newItem || ""} onChange={this._onNewItemChanged}
list={this.props.suggestionsListId} /> list={this.props.suggestionsListId} />
<AccessibleButton onClick={this._onItemAdded} kind="primary" type="submit" disabled={!this.props.newItem}> <AccessibleButton onClick={this._onItemAdded} kind="primary" type="submit" disabled={!this.props.newItem}>
{_t("Add")} {_t("Add")}
</AccessibleButton> </AccessibleButton>

View file

@ -221,13 +221,15 @@ export default class EditableText extends React.Component {
</div>; </div>;
} else { } else {
// show the content editable div, but manually manage its contents as react and contentEditable don't play nice together // 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} editableEl = <div
contentEditable={true} ref={this._editable_div}
className={className} contentEditable={true}
onKeyDown={this.onKeyDown} className={className}
onKeyUp={this.onKeyUp} onKeyDown={this.onKeyDown}
onFocus={this.onFocus} onKeyUp={this.onKeyUp}
onBlur={this.onBlur} />; onFocus={this.onFocus}
onBlur={this.onBlur}
/>;
} }
return editableEl; return editableEl;

View file

@ -114,6 +114,8 @@ export default class ImageView extends React.Component<IProps, IState> {
componentWillUnmount() { componentWillUnmount() {
this.focusLock.current.removeEventListener('wheel', this.onWheel); this.focusLock.current.removeEventListener('wheel', this.onWheel);
window.removeEventListener("resize", this.calculateZoom);
this.image.current.removeEventListener("load", this.calculateZoom);
} }
private calculateZoom = () => { private calculateZoom = () => {

View file

@ -46,8 +46,12 @@ export default class LabelledToggleSwitch extends React.Component {
// This is a minimal version of a SettingsFlag // This is a minimal version of a SettingsFlag
let firstPart = <span className="mx_SettingsFlag_label">{this.props.label}</span>; let firstPart = <span className="mx_SettingsFlag_label">{this.props.label}</span>;
let secondPart = <ToggleSwitch checked={this.props.value} disabled={this.props.disabled} let secondPart = <ToggleSwitch
onChange={this.props.onChange} aria-label={this.props.label} />; checked={this.props.value}
disabled={this.props.disabled}
onChange={this.props.onChange}
aria-label={this.props.label}
/>;
if (this.props.toggleInFront) { if (this.props.toggleInFront) {
const temp = firstPart; const temp = firstPart;

View file

@ -60,10 +60,10 @@ export default class LanguageDropdown extends React.Component {
// doesn't know this, therefore we do this. // doesn't know this, therefore we do this.
const language = SettingsStore.getValue("language", null, /*excludeDefault:*/true); const language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
if (language) { if (language) {
this.props.onOptionChange(language); this.props.onOptionChange(language);
} else { } else {
const language = languageHandler.normalizeLanguageKey(languageHandler.getLanguageFromBrowser()); const language = languageHandler.normalizeLanguageKey(languageHandler.getLanguageFromBrowser());
this.props.onOptionChange(language); this.props.onOptionChange(language);
} }
} }
} }

View file

@ -225,19 +225,19 @@ class Pill extends React.Component {
} }
break; break;
case Pill.TYPE_USER_MENTION: { case Pill.TYPE_USER_MENTION: {
// If this user is not a member of this room, default to the empty member // If this user is not a member of this room, default to the empty member
const member = this.state.member; const member = this.state.member;
if (member) { if (member) {
userId = member.userId; userId = member.userId;
member.rawDisplayName = member.rawDisplayName || ''; member.rawDisplayName = member.rawDisplayName || '';
linkText = member.rawDisplayName; linkText = member.rawDisplayName;
if (this.props.shouldShowPillAvatar) { if (this.props.shouldShowPillAvatar) {
avatar = <MemberAvatar member={member} width={16} height={16} aria-hidden="true" />; avatar = <MemberAvatar member={member} width={16} height={16} aria-hidden="true" />;
}
pillClass = 'mx_UserPill';
href = null;
onClick = this.onUserPillClicked;
} }
pillClass = 'mx_UserPill';
href = null;
onClick = this.onUserPillClicked;
}
} }
break; break;
case Pill.TYPE_ROOM_MENTION: { case Pill.TYPE_ROOM_MENTION: {

View file

@ -135,9 +135,13 @@ export default class PowerSelector extends React.Component {
if (this.state.custom) { if (this.state.custom) {
picker = ( picker = (
<Field type="number" <Field type="number"
label={label} max={this.props.maxValue} label={label} max={this.props.maxValue}
onBlur={this.onCustomBlur} onKeyDown={this.onCustomKeyDown} onChange={this.onCustomChange} onBlur={this.onCustomBlur}
value={String(this.state.customValue)} disabled={this.props.disabled} /> onKeyDown={this.onCustomKeyDown}
onChange={this.onCustomChange}
value={String(this.state.customValue)}
disabled={this.props.disabled}
/>
); );
} else { } else {
// Each level must have a definition in this.state.levelRoleMap // Each level must have a definition in this.state.levelRoleMap
@ -154,8 +158,9 @@ export default class PowerSelector extends React.Component {
picker = ( picker = (
<Field element="select" <Field element="select"
label={label} onChange={this.onSelectChange} label={label} onChange={this.onSelectChange}
value={String(this.state.selectValue)} disabled={this.props.disabled}> value={String(this.state.selectValue)} disabled={this.props.disabled}
>
{options} {options}
</Field> </Field>
); );

View file

@ -46,17 +46,18 @@ export default class RoomAliasField extends React.PureComponent {
const domain = (<span title={aliasPostfix}>{aliasPostfix}</span>); const domain = (<span title={aliasPostfix}>{aliasPostfix}</span>);
const maxlength = 255 - this.props.domain.length - 2; // 2 for # and : const maxlength = 255 - this.props.domain.length - 2; // 2 for # and :
return ( return (
<Field <Field
label={_t("Room address")} label={_t("Room address")}
className="mx_RoomAliasField" className="mx_RoomAliasField"
prefixComponent={poundSign} prefixComponent={poundSign}
postfixComponent={domain} postfixComponent={domain}
ref={ref => this._fieldRef = ref} ref={ref => this._fieldRef = ref}
onValidate={this._onValidate} onValidate={this._onValidate}
placeholder={_t("e.g. my-room")} placeholder={_t("e.g. my-room")}
onChange={this._onChange} onChange={this._onChange}
value={this.props.value.substring(1, this.props.value.length - this.props.domain.length - 1)} value={this.props.value.substring(1, this.props.value.length - this.props.domain.length - 1)}
maxLength={maxlength} /> maxLength={maxlength}
/>
); );
} }

View file

@ -59,13 +59,13 @@ class TintableSvg extends React.Component {
render() { render() {
return ( return (
<object className={"mx_TintableSvg " + (this.props.className ? this.props.className : "")} <object className={"mx_TintableSvg " + (this.props.className ? this.props.className : "")}
type="image/svg+xml" type="image/svg+xml"
data={this.props.src} data={this.props.src}
width={this.props.width} width={this.props.width}
height={this.props.height} height={this.props.height}
onLoad={this.onLoad} onLoad={this.onLoad}
tabIndex="-1" tabIndex="-1"
/> />
); );
} }
} }

View file

@ -178,9 +178,15 @@ export default class GroupMemberList extends React.Component {
} }
const inputBox = ( const inputBox = (
<input className="mx_GroupMemberList_query mx_textinput" id="mx_GroupMemberList_query" type="text" <input
onChange={this.onSearchQueryChanged} value={this.state.searchQuery} className="mx_GroupMemberList_query mx_textinput"
placeholder={_t('Filter community members')} autoComplete="off" /> 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"> const joined = this.state.members ? <div className="mx_MemberList_joined">

View file

@ -67,11 +67,11 @@ export default class GroupPublicityToggle extends React.Component {
const GroupTile = sdk.getComponent('groups.GroupTile'); const GroupTile = sdk.getComponent('groups.GroupTile');
return <div className="mx_GroupPublicity_toggle"> return <div className="mx_GroupPublicity_toggle">
<GroupTile groupId={this.props.groupId} showDescription={false} <GroupTile groupId={this.props.groupId} showDescription={false}
avatarHeight={40} draggable={false} avatarHeight={40} draggable={false}
/> />
<ToggleSwitch checked={this.state.isGroupPublicised} <ToggleSwitch checked={this.state.isGroupPublicised}
disabled={!this.state.ready || this.state.busy} disabled={!this.state.ready || this.state.busy}
onChange={this._onPublicityToggle} /> onChange={this._onPublicityToggle} />
</div>; </div>;
} }
} }

View file

@ -141,9 +141,14 @@ export default class GroupRoomList extends React.Component {
); );
} }
const inputBox = ( const inputBox = (
<input className="mx_GroupRoomList_query mx_textinput" id="mx_GroupRoomList_query" type="text" <input
onChange={this.onSearchQueryChanged} value={this.state.searchQuery} className="mx_GroupRoomList_query mx_textinput" id="mx_GroupRoomList_query"
placeholder={_t('Filter community rooms')} autoComplete="off" /> type="text"
onChange={this.onSearchQueryChanged}
value={this.state.searchQuery}
placeholder={_t('Filter community rooms')}
autoComplete="off"
/>
); );
const TruncatedList = sdk.getComponent("elements.TruncatedList"); const TruncatedList = sdk.getComponent("elements.TruncatedList");
@ -152,7 +157,7 @@ export default class GroupRoomList extends React.Component {
{ inviteButton } { inviteButton }
<AutoHideScrollbar className="mx_GroupRoomList_joined mx_GroupRoomList_outerWrapper"> <AutoHideScrollbar className="mx_GroupRoomList_joined mx_GroupRoomList_outerWrapper">
<TruncatedList className="mx_GroupRoomList_wrapper" truncateAt={this.state.truncateAt} <TruncatedList className="mx_GroupRoomList_wrapper" truncateAt={this.state.truncateAt}
createOverflowElement={this._createOverflowTile}> createOverflowElement={this._createOverflowTile}>
{ this.makeGroupRoomTiles(this.state.searchQuery) } { this.makeGroupRoomTiles(this.state.searchQuery) }
</TruncatedList> </TruncatedList>
</AutoHideScrollbar> </AutoHideScrollbar>

View file

@ -125,9 +125,9 @@ export default class MImageBody extends React.Component {
_isGif() { _isGif() {
const content = this.props.mxEvent.getContent(); const content = this.props.mxEvent.getContent();
return ( return (
content && content &&
content.info && content.info &&
content.info.mimetype === "image/gif" content.info.mimetype === "image/gif"
); );
} }
@ -346,9 +346,9 @@ export default class MImageBody extends React.Component {
} else { } else {
imageElement = ( imageElement = (
<img style={{display: 'none'}} src={thumbUrl} ref={this._image} <img style={{display: 'none'}} src={thumbUrl} ref={this._image}
alt={content.body} alt={content.body}
onError={this.onImageError} onError={this.onImageError}
onLoad={this.onImageLoad} onLoad={this.onImageLoad}
/> />
); );
} }
@ -384,12 +384,12 @@ export default class MImageBody extends React.Component {
// mx_MImageBody_thumbnail resizes img to exactly container size // mx_MImageBody_thumbnail resizes img to exactly container size
img = ( img = (
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref={this._image} <img className="mx_MImageBody_thumbnail" src={thumbUrl} ref={this._image}
style={{ maxWidth: maxWidth + "px" }} style={{ maxWidth: maxWidth + "px" }}
alt={content.body} alt={content.body}
onError={this.onImageError} onError={this.onImageError}
onLoad={this.onImageLoad} onLoad={this.onImageLoad}
onMouseEnter={this.onImageEnter} onMouseEnter={this.onImageEnter}
onMouseLeave={this.onImageLeave} /> onMouseLeave={this.onImageLeave} />
); );
} }
@ -467,9 +467,9 @@ export default class MImageBody extends React.Component {
const contentUrl = this._getContentUrl(); const contentUrl = this._getContentUrl();
let thumbUrl; let thumbUrl;
if (this._isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) { if (this._isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) {
thumbUrl = contentUrl; thumbUrl = contentUrl;
} else { } else {
thumbUrl = this._getThumbUrl(); thumbUrl = this._getThumbUrl();
} }
const thumbnail = this._messageContent(contentUrl, thumbUrl, content); const thumbnail = this._messageContent(contentUrl, thumbUrl, content);

View file

@ -82,9 +82,7 @@ export default class MKeyVerificationConclusion extends React.Component {
} }
// User isn't actually verified // User isn't actually verified
if (!MatrixClientPeg.get() if (!MatrixClientPeg.get().checkUserTrust(request.otherUserId).isCrossSigningVerified()) {
.checkUserTrust(request.otherUserId)
.isCrossSigningVerified()) {
return false; return false;
} }

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

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

View file

@ -72,12 +72,8 @@ export default class MessageEvent extends React.Component {
'm.emote': sdk.getComponent('messages.TextualBody'), 'm.emote': sdk.getComponent('messages.TextualBody'),
'm.image': sdk.getComponent('messages.MImageBody'), 'm.image': sdk.getComponent('messages.MImageBody'),
'm.file': sdk.getComponent('messages.MFileBody'), 'm.file': sdk.getComponent('messages.MFileBody'),
'm.audio': sdk.getComponent('messages.MAudioBody'), 'm.audio': sdk.getComponent('messages.MVoiceOrAudioBody'),
'm.video': sdk.getComponent('messages.MVideoBody'), '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 = { const evTypes = {
'm.sticker': sdk.getComponent('messages.MStickerBody'), 'm.sticker': sdk.getComponent('messages.MStickerBody'),

View file

@ -129,12 +129,13 @@ export default class ReactionsRowButton extends React.PureComponent {
}, },
); );
} }
const isPeeking = room.getMyMembership() !== "join";
const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return <AccessibleButton return <AccessibleButton
className={classes} className={classes}
aria-label={label} aria-label={label}
onClick={this.onClick} onClick={this.onClick}
disabled={isPeeking}
onMouseOver={this.onMouseOver} onMouseOver={this.onMouseOver}
onMouseLeave={this.onMouseLeave} onMouseLeave={this.onMouseLeave}
> >

View file

@ -521,11 +521,12 @@ export default class TextualBody extends React.Component {
const LinkPreviewWidget = sdk.getComponent('rooms.LinkPreviewWidget'); const LinkPreviewWidget = sdk.getComponent('rooms.LinkPreviewWidget');
widgets = this.state.links.map((link)=>{ widgets = this.state.links.map((link)=>{
return <LinkPreviewWidget return <LinkPreviewWidget
key={link} key={link}
link={link} link={link}
mxEvent={this.props.mxEvent} mxEvent={this.props.mxEvent}
onCancelClick={this.onCancelClick} onCancelClick={this.onCancelClick}
onHeightChanged={this.props.onHeightChanged} />; onHeightChanged={this.props.onHeightChanged}
/>;
}); });
} }

View file

@ -310,9 +310,14 @@ export default class AliasSettings extends React.Component {
let found = false; let found = false;
const canonicalValue = this.state.canonicalAlias || ""; const canonicalValue = this.state.canonicalAlias || "";
const canonicalAliasSection = ( const canonicalAliasSection = (
<Field onChange={this.onCanonicalAliasChange} value={canonicalValue} <Field
disabled={this.state.updatingCanonicalAlias || !this.props.canSetCanonicalAlias} onChange={this.onCanonicalAliasChange}
element='select' id='canonicalAlias' label={_t('Main address')}> 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> <option value="" key="unset">{ _t('not specified') }</option>
{ {
this._getAliases().map((alias, i) => { this._getAliases().map((alias, i) => {
@ -326,9 +331,9 @@ export default class AliasSettings extends React.Component {
} }
{ {
found || !this.state.canonicalAlias ? '' : found || !this.state.canonicalAlias ? '' :
<option value={ this.state.canonicalAlias } key='arbitrary'> <option value={ this.state.canonicalAlias } key='arbitrary'>
{ this.state.canonicalAlias } { this.state.canonicalAlias }
</option> </option>
} }
</Field> </Field>
); );

View file

@ -205,16 +205,34 @@ export default class RoomProfileSettings extends React.Component {
noValidate={true} noValidate={true}
className="mx_ProfileSettings_profileForm" className="mx_ProfileSettings_profileForm"
> >
<input type="file" ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload" <input
onChange={this._onAvatarChanged} accept="image/*" /> type="file"
ref={this._avatarUpload}
className="mx_ProfileSettings_avatarUpload"
onChange={this._onAvatarChanged}
accept="image/*"
/>
<div className="mx_ProfileSettings_profile"> <div className="mx_ProfileSettings_profile">
<div className="mx_ProfileSettings_controls"> <div className="mx_ProfileSettings_controls">
<Field label={_t("Room Name")} <Field
type="text" value={this.state.displayName} autoComplete="off" label={_t("Room Name")}
onChange={this._onDisplayNameChanged} disabled={!this.state.canSetName} /> type="text"
<Field className="mx_ProfileSettings_controls_topic" id="profileTopic" label={_t("Room Topic")} disabled={!this.state.canSetTopic} value={this.state.displayName}
type="text" value={this.state.topic} autoComplete="off" autoComplete="off"
onChange={this._onTopicChanged} element="textarea" /> 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> </div>
<AvatarSetting <AvatarSetting
avatarUrl={this.state.avatarUrl} avatarUrl={this.state.avatarUrl}

View file

@ -68,10 +68,12 @@ export default class UrlPreviewSettings extends React.Component {
if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, "room")) { if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, "room")) {
previewsForRoom = ( previewsForRoom = (
<label> <label>
<SettingsFlag name="urlPreviewsEnabled" <SettingsFlag
level={SettingLevel.ROOM} name="urlPreviewsEnabled"
roomId={roomId} level={SettingLevel.ROOM}
isExplicit={true} /> roomId={roomId}
isExplicit={true}
/>
</label> </label>
); );
} else { } 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 const previewsForRoomAccount = ( // in an e2ee room we use a special key to enforce per-room opt-in
<SettingsFlag name={isEncrypted ? 'urlPreviewsEnabled_e2ee' : 'urlPreviewsEnabled'} <SettingsFlag name={isEncrypted ? 'urlPreviewsEnabled_e2ee' : 'urlPreviewsEnabled'}
level={SettingLevel.ROOM_ACCOUNT} level={SettingLevel.ROOM_ACCOUNT}
roomId={roomId} /> roomId={roomId} />
); );
return ( return (

View file

@ -176,8 +176,11 @@ class EntityTile extends React.Component {
// The wrapping div is required to make the magic mouse listener work, for some reason. // The wrapping div is required to make the magic mouse listener work, for some reason.
return ( return (
<div ref={(c) => this.container = c} > <div ref={(c) => this.container = c} >
<AccessibleButton className={classNames(mainClassNames)} title={this.props.title} <AccessibleButton
onClick={this.props.onClick}> className={classNames(mainClassNames)}
title={this.props.title}
onClick={this.props.onClick}
>
<div className="mx_EntityTile_avatar"> <div className="mx_EntityTile_avatar">
{ av } { av }
{ e2eIcon } { e2eIcon }

View file

@ -128,8 +128,8 @@ export default class LinkPreviewWidget extends React.Component {
let img; let img;
if (image) { if (image) {
img = <div className="mx_LinkPreviewWidget_image" style={{ height: thumbHeight }}> img = <div className="mx_LinkPreviewWidget_image" style={{ height: thumbHeight }}>
<img style={{ maxWidth: imageMaxWidth, maxHeight: imageMaxHeight }} src={image} onClick={this.onImageClick} /> <img style={{ maxWidth: imageMaxWidth, maxHeight: imageMaxHeight }} src={image} onClick={this.onImageClick} />
</div>; </div>;
} }
// The description includes &-encoded HTML entities, we decode those as React treats the thing as an // The description includes &-encoded HTML entities, we decode those as React treats the thing as an

View file

@ -53,9 +53,9 @@ export default class PinnedEventTile extends React.Component {
if (index !== -1) { if (index !== -1) {
pinned.splice(index, 1); pinned.splice(index, 1);
MatrixClientPeg.get().sendStateEvent(this.props.mxRoom.roomId, 'm.room.pinned_events', {pinned}, '') MatrixClientPeg.get().sendStateEvent(this.props.mxRoom.roomId, 'm.room.pinned_events', {pinned}, '')
.then(() => { .then(() => {
if (this.props.onUnpinned) this.props.onUnpinned(); if (this.props.onUnpinned) this.props.onUnpinned();
}); });
} else 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())) } { formatFullDate(new Date(this.props.mxEvent.getTs())) }
</span> </span>
<div className="mx_PinnedEventTile_message"> <div className="mx_PinnedEventTile_message">
<MessageEvent mxEvent={this.props.mxEvent} className="mx_PinnedEventTile_body" maxImageHeight={150} <MessageEvent
onHeightChanged={() => {}} // we need to give this, apparently mxEvent={this.props.mxEvent}
className="mx_PinnedEventTile_body"
maxImageHeight={150}
onHeightChanged={() => {}} // we need to give this, apparently
/> />
</div> </div>
</div> </div>

View file

@ -64,10 +64,10 @@ export default class PinnedEventsPanel extends React.Component {
pinnedEvents.getContent().pinned.map((eventId) => { pinnedEvents.getContent().pinned.map((eventId) => {
promises.push(cli.getEventTimeline(this.props.room.getUnfilteredTimelineSet(), eventId, 0).then( promises.push(cli.getEventTimeline(this.props.room.getUnfilteredTimelineSet(), eventId, 0).then(
(timeline) => { (timeline) => {
const event = timeline.getEvents().find((e) => e.getId() === eventId); const event = timeline.getEvents().find((e) => e.getId() === eventId);
return {eventId, timeline, event}; return {eventId, timeline, event};
}).catch((err) => { }).catch((err) => {
console.error("Error looking up pinned event " + eventId + " in room " + this.props.room.roomId); console.error("Error looking up pinned event " + eventId + " in room " + this.props.room.roomId);
console.error(err); console.error(err);
return null; // return lack of context to avoid unhandled errors 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 this.state.pinned.map((context) => {
return (<PinnedEventTile key={context.event.getId()} return (
mxRoom={this.props.room} <PinnedEventTile
mxEvent={context.event} key={context.event.getId()}
onUnpinned={this._updatePinnedMessages} />); mxRoom={this.props.room}
mxEvent={context.event}
onUnpinned={this._updatePinnedMessages}
/>
);
}); });
} }

View file

@ -187,8 +187,7 @@ export default class ReadReceiptMarker extends React.PureComponent {
} }
return ( return (
<NodeAnimator <NodeAnimator startStyles={this.state.startStyles}>
startStyles={this.state.startStyles} >
<MemberAvatar <MemberAvatar
member={this.props.member} member={this.props.member}
fallbackUserId={this.props.fallbackUserId} fallbackUserId={this.props.fallbackUserId}

View file

@ -79,8 +79,13 @@ export default class ReplyPreview extends React.Component {
{ _t('Replying') } { _t('Replying') }
</div> </div>
<div className="mx_ReplyPreview_header mx_ReplyPreview_cancel"> <div className="mx_ReplyPreview_header mx_ReplyPreview_cancel">
<img className="mx_filterFlipColor" src={require("../../../../res/img/cancel.svg")} width="18" height="18" <img
onClick={cancelQuoting} /> className="mx_filterFlipColor"
src={require("../../../../res/img/cancel.svg")}
width="18"
height="18"
onClick={cancelQuoting}
/>
</div> </div>
<div className="mx_ReplyPreview_clear" /> <div className="mx_ReplyPreview_clear" />
<EventTile <EventTile

View file

@ -88,11 +88,11 @@ export default class RoomDetailRow extends React.Component {
const name = room.name || getDisplayAliasForRoom(room) || _t('Unnamed room'); const name = room.name || getDisplayAliasForRoom(room) || _t('Unnamed room');
const guestRead = room.worldReadable ? ( const guestRead = room.worldReadable ? (
<div className="mx_RoomDirectory_perm">{ _t('World readable') }</div> <div className="mx_RoomDirectory_perm">{ _t('World readable') }</div>
) : <div />; ) : <div />;
const guestJoin = room.guestCanJoin ? ( const guestJoin = room.guestCanJoin ? (
<div className="mx_RoomDirectory_perm">{ _t('Guests can join') }</div> <div className="mx_RoomDirectory_perm">{ _t('Guests can join') }</div>
) : <div />; ) : <div />;
const perms = (guestRead || guestJoin) ? (<div className="mx_RoomDirectory_perms"> const perms = (guestRead || guestJoin) ? (<div className="mx_RoomDirectory_perms">
{ guestRead }&nbsp; { guestRead }&nbsp;

View file

@ -539,6 +539,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
onResize={this.props.onResize} onResize={this.props.onResize}
showSkeleton={showSkeleton} showSkeleton={showSkeleton}
extraTiles={extraTiles} extraTiles={extraTiles}
resizeNotifier={this.props.resizeNotifier}
alwaysVisible={ALWAYS_VISIBLE_TAGS.includes(orderedTagId)} alwaysVisible={ALWAYS_VISIBLE_TAGS.includes(orderedTagId)}
/> />
}); });

View file

@ -19,6 +19,7 @@ import React, {useState} from "react";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore"; import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
import {useEventEmitter} from "../../../hooks/useEventEmitter"; import {useEventEmitter} from "../../../hooks/useEventEmitter";
import SpaceStore from "../../../stores/SpaceStore";
const RoomListNumResults: React.FC = () => { const RoomListNumResults: React.FC = () => {
const [count, setCount] = useState<number>(null); const [count, setCount] = useState<number>(null);
@ -34,7 +35,10 @@ const RoomListNumResults: React.FC = () => {
if (typeof count !== "number") return null; if (typeof count !== "number") return null;
return <div className="mx_LeftPanel_roomListFilterCount"> 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>; </div>;
}; };

View file

@ -44,6 +44,7 @@ import { ActionPayload } from "../../../dispatcher/payloads";
import { Enable, Resizable } from "re-resizable"; import { Enable, Resizable } from "re-resizable";
import { Direction } from "re-resizable/lib/resizer"; import { Direction } from "re-resizable/lib/resizer";
import { polyfillTouchEvent } from "../../../@types/polyfill"; import { polyfillTouchEvent } from "../../../@types/polyfill";
import { ResizeNotifier } from "../../../utils/ResizeNotifier";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore"; import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore";
import { arrayFastClone, arrayHasOrderChange } from "../../../utils/arrays"; import { arrayFastClone, arrayHasOrderChange } from "../../../utils/arrays";
@ -75,7 +76,7 @@ interface IProps {
onResize: () => void; onResize: () => void;
showSkeleton?: boolean; showSkeleton?: boolean;
alwaysVisible?: boolean; alwaysVisible?: boolean;
resizeNotifier: ResizeNotifier;
extraTiles?: ReactComponentElement<typeof ExtraTile>[]; extraTiles?: ReactComponentElement<typeof ExtraTile>[];
// TODO: Account for https://github.com/vector-im/element-web/issues/14179 // 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 tiles.push(<RoomTile
room={room} room={room}
key={`room-${room.roomId}`} key={`room-${room.roomId}`}
resizeNotifier={this.props.resizeNotifier}
showMessagePreview={this.layout.showPreviews} showMessagePreview={this.layout.showPreviews}
isMinimized={this.props.isMinimized} isMinimized={this.props.isMinimized}
tag={this.props.tagId} tag={this.props.tagId}

View file

@ -53,12 +53,14 @@ import { CommunityPrototypeStore, IRoomProfile } from "../../../stores/Community
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { getUnsentMessages } from "../../structures/RoomStatusBar"; import { getUnsentMessages } from "../../structures/RoomStatusBar";
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
import { ResizeNotifier } from "../../../utils/ResizeNotifier";
interface IProps { interface IProps {
room: Room; room: Room;
showMessagePreview: boolean; showMessagePreview: boolean;
isMinimized: boolean; isMinimized: boolean;
tag: TagID; tag: TagID;
resizeNotifier: ResizeNotifier;
} }
type PartialDOMRect = Pick<DOMRect, "left" | "bottom">; 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.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room);
this.roomProps = EchoChamber.forRoom(this.props.room); this.roomProps = EchoChamber.forRoom(this.props.room);
if (this.props.resizeNotifier) {
this.props.resizeNotifier.on("middlePanelResized", this.onResize);
}
} }
private countUnsentEvents(): number { private countUnsentEvents(): number {
@ -116,6 +121,12 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
this.forceUpdate(); // notification state changed - update 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) => { private onLocalEchoUpdated = (ev: MatrixEvent, room: Room) => {
if (!room?.roomId === this.props.room.roomId) return; if (!room?.roomId === this.props.room.roomId) return;
this.setState({hasUnsentEvents: this.countUnsentEvents() > 0}); 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); 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); ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
defaultDispatcher.unregister(this.dispatcherRef); defaultDispatcher.unregister(this.dispatcherRef);
this.notificationState.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate); this.notificationState.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);

View file

@ -266,25 +266,25 @@ export default class Stickerpicker extends React.Component {
width: this.popoverWidth, width: this.popoverWidth,
}} }}
> >
<PersistedElement persistKey={PERSISTED_ELEMENT_KEY} zIndex={STICKERPICKER_Z_INDEX}> <PersistedElement persistKey={PERSISTED_ELEMENT_KEY} zIndex={STICKERPICKER_Z_INDEX}>
<AppTile <AppTile
app={stickerApp} app={stickerApp}
room={this.props.room} room={this.props.room}
fullWidth={true} fullWidth={true}
userId={MatrixClientPeg.get().credentials.userId} userId={MatrixClientPeg.get().credentials.userId}
creatorUserId={stickerpickerWidget.sender || MatrixClientPeg.get().credentials.userId} creatorUserId={stickerpickerWidget.sender || MatrixClientPeg.get().credentials.userId}
waitForIframeLoad={true} waitForIframeLoad={true}
showMenubar={true} showMenubar={true}
onEditClick={this._launchManageIntegrations} onEditClick={this._launchManageIntegrations}
onDeleteClick={this._removeStickerpickerWidgets} onDeleteClick={this._removeStickerpickerWidgets}
showTitle={false} showTitle={false}
showCancel={false} showCancel={false}
showPopout={false} showPopout={false}
onMinimiseClick={this._onHideStickersClick} onMinimiseClick={this._onHideStickersClick}
handleMinimisePointerEvents={true} handleMinimisePointerEvents={true}
userWidget={true} userWidget={true}
/> />
</PersistedElement> </PersistedElement>
</div> </div>
</div> </div>
); );

View file

@ -27,6 +27,10 @@ import LiveRecordingClock from "../voice_messages/LiveRecordingClock";
import {VoiceRecordingStore} from "../../../stores/VoiceRecordingStore"; import {VoiceRecordingStore} from "../../../stores/VoiceRecordingStore";
import {UPDATE_EVENT} from "../../../stores/AsyncStore"; import {UPDATE_EVENT} from "../../../stores/AsyncStore";
import RecordingPlayback from "../voice_messages/RecordingPlayback"; 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 { interface IProps {
room: Room; room: Room;
@ -64,8 +68,8 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
const mxc = await this.state.recorder.upload(); const mxc = await this.state.recorder.upload();
MatrixClientPeg.get().sendMessage(this.props.room.roomId, { MatrixClientPeg.get().sendMessage(this.props.room.roomId, {
"body": "Voice message", "body": "Voice message",
"msgtype": "org.matrix.msc2516.voice", //"msgtype": "org.matrix.msc2516.voice",
//"msgtype": MsgType.Audio, "msgtype": MsgType.Audio,
"url": mxc, "url": mxc,
"info": { "info": {
duration: Math.round(this.state.recorder.durationSeconds * 1000), duration: Math.round(this.state.recorder.durationSeconds * 1000),
@ -83,10 +87,6 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
}, },
"org.matrix.msc1767.audio": { "org.matrix.msc1767.audio": {
duration: Math.round(this.state.recorder.durationSeconds * 1000), 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 // 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 // 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. // We're expecting about one data point per second of audio.
waveform: this.state.recorder.getPlayback().waveform.map(v => Math.round(v * 1024)), 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(); await this.disposeRecording();
} }
@ -115,16 +116,59 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
await this.state.recorder.stop(); await this.state.recorder.stop();
return; 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. // The "microphone access error" dialogs are used a lot, so let's functionify them
recorder.on(UPDATE_EVENT, (ev: RecordingState) => { const accessError = () => {
if (ev === RecordingState.EndingSoon) return; // ignore this state: it has no UI purpose here Modal.createTrackedDialog('Microphone Access Error', '', ErrorDialog, {
this.setState({recordingPhase: ev}); 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 { private renderWaveformArea(): ReactNode {

View file

@ -154,7 +154,7 @@ export default class ChangeAvatar extends React.Component {
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); 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 ? // 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' 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; let uploadSection;

View file

@ -206,7 +206,7 @@ export default class ChangePassword extends React.Component {
test: ({ value, allowEmpty }) => allowEmpty || !!value, test: ({ value, allowEmpty }) => allowEmpty || !!value,
invalid: () => _t("Passwords can't be empty"), invalid: () => _t("Passwords can't be empty"),
}, },
], ],
}); });
onChangeNewPassword = (ev) => { onChangeNewPassword = (ev) => {
@ -245,7 +245,7 @@ export default class ChangePassword extends React.Component {
}, },
invalid: () => _t("Passwords don't match"), invalid: () => _t("Passwords don't match"),
}, },
], ],
}); });
onClickChange = async (ev) => { onClickChange = async (ev) => {

View file

@ -259,7 +259,7 @@ export default class CrossSigningPanel extends React.PureComponent {
<td>{_t("Homeserver feature support:")}</td> <td>{_t("Homeserver feature support:")}</td>
<td>{homeserverSupportsCrossSigning ? _t("exists") : _t("not found")}</td> <td>{homeserverSupportsCrossSigning ? _t("exists") : _t("not found")}</td>
</tr> </tr>
</tbody></table> </tbody></table>
</details> </details>
{errorSection} {errorSection}
{actionRow} {actionRow}

View file

@ -214,7 +214,7 @@ export default class DevicesPanel extends React.Component {
const deleteButton = this.state.deleting ? const deleteButton = this.state.deleting ?
<Spinner w={22} h={22} /> : <Spinner w={22} h={22} /> :
<AccessibleButton onClick={this._onDeleteClick} kind="danger_sm"> <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>; </AccessibleButton>;
const classes = classNames(this.props.className, "mx_DevicesPanel"); const classes = classNames(this.props.className, "mx_DevicesPanel");

View file

@ -100,7 +100,7 @@ export default class Notifications extends React.Component {
MatrixClientPeg.get().setPushRuleEnabled( MatrixClientPeg.get().setPushRuleEnabled(
'global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked, 'global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked,
).then(function() { ).then(function() {
self._refreshFromServer(); self._refreshFromServer();
}); });
}; };
@ -580,12 +580,12 @@ export default class Notifications extends React.Component {
"vectorRuleId": "_keywords", "vectorRuleId": "_keywords",
"description": ( "description": (
<span> <span>
{ _t('Messages containing <span>keywords</span>', { _t('Messages containing <span>keywords</span>',
{}, {},
{ 'span': (sub) => { 'span': (sub) =>
<span className="mx_UserNotifSettings_keywords" onClick={ self.onKeywordsClicked }>{sub}</span>, <span className="mx_UserNotifSettings_keywords" onClick={ self.onKeywordsClicked }>{sub}</span>,
}, },
)} )}
</span> </span>
), ),
"vectorState": self.state.vectorContentRules.vectorState, "vectorState": self.state.vectorContentRules.vectorState,
@ -743,8 +743,8 @@ export default class Notifications extends React.Component {
emailNotificationsRow(address, label) { emailNotificationsRow(address, label) {
return <LabelledToggleSwitch value={this.hasEmailPusher(this.state.pushers, address)} return <LabelledToggleSwitch value={this.hasEmailPusher(this.state.pushers, address)}
onChange={this.onEnableEmailNotificationsChange.bind(this, address)} onChange={this.onEnableEmailNotificationsChange.bind(this, address)}
label={label} key={`emailNotif_${label}`} />; label={label} key={`emailNotif_${label}`} />;
} }
render() { render() {
@ -757,8 +757,8 @@ export default class Notifications extends React.Component {
let masterPushRuleDiv; let masterPushRuleDiv;
if (this.state.masterPushRule) { if (this.state.masterPushRule) {
masterPushRuleDiv = <LabelledToggleSwitch value={!this.state.masterPushRule.enabled} masterPushRuleDiv = <LabelledToggleSwitch value={!this.state.masterPushRule.enabled}
onChange={this.onEnableNotificationsChange} onChange={this.onEnableNotificationsChange}
label={_t('Enable notifications for this account')} />; label={_t('Enable notifications for this account')} />;
} }
let clearNotificationsButton; let clearNotificationsButton;
@ -874,16 +874,16 @@ export default class Notifications extends React.Component {
{ spinner } { spinner }
<LabelledToggleSwitch value={SettingsStore.getValue("notificationsEnabled")} <LabelledToggleSwitch value={SettingsStore.getValue("notificationsEnabled")}
onChange={this.onEnableDesktopNotificationsChange} onChange={this.onEnableDesktopNotificationsChange}
label={_t('Enable desktop notifications for this session')} /> label={_t('Enable desktop notifications for this session')} />
<LabelledToggleSwitch value={SettingsStore.getValue("notificationBodyEnabled")} <LabelledToggleSwitch value={SettingsStore.getValue("notificationBodyEnabled")}
onChange={this.onEnableDesktopNotificationBodyChange} onChange={this.onEnableDesktopNotificationBodyChange}
label={_t('Show message in desktop notification')} /> label={_t('Show message in desktop notification')} />
<LabelledToggleSwitch value={SettingsStore.getValue("audioNotificationsEnabled")} <LabelledToggleSwitch value={SettingsStore.getValue("audioNotificationsEnabled")}
onChange={this.onEnableAudioNotificationsChange} onChange={this.onEnableAudioNotificationsChange}
label={_t('Enable audible notifications for this session')} /> label={_t('Enable audible notifications for this session')} />
{ emailNotificationsRows } { emailNotificationsRows }

View file

@ -170,8 +170,12 @@ export default class ProfileSettings extends React.Component {
noValidate={true} noValidate={true}
className="mx_ProfileSettings_profileForm" className="mx_ProfileSettings_profileForm"
> >
<input type="file" ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload" <input
onChange={this._onAvatarChanged} accept="image/*" /> type="file"
ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload"
onChange={this._onAvatarChanged}
accept="image/*"
/>
<div className="mx_ProfileSettings_profile"> <div className="mx_ProfileSettings_profile">
<div className="mx_ProfileSettings_controls"> <div className="mx_ProfileSettings_controls">
<span className="mx_SettingsTab_subheading">{_t("Profile")}</span> <span className="mx_SettingsTab_subheading">{_t("Profile")}</span>

View file

@ -90,12 +90,18 @@ export class ExistingEmailAddress extends React.Component {
<span className="mx_ExistingEmailAddress_promptText"> <span className="mx_ExistingEmailAddress_promptText">
{_t("Remove %(email)s?", {email: this.props.email.address} )} {_t("Remove %(email)s?", {email: this.props.email.address} )}
</span> </span>
<AccessibleButton onClick={this._onActuallyRemove} kind="danger_sm" <AccessibleButton
className="mx_ExistingEmailAddress_confirmBtn"> onClick={this._onActuallyRemove}
kind="danger_sm"
className="mx_ExistingEmailAddress_confirmBtn"
>
{_t("Remove")} {_t("Remove")}
</AccessibleButton> </AccessibleButton>
<AccessibleButton onClick={this._onDontRemove} kind="link_sm" <AccessibleButton
className="mx_ExistingEmailAddress_confirmBtn"> onClick={this._onDontRemove}
kind="link_sm"
className="mx_ExistingEmailAddress_confirmBtn"
>
{_t("Cancel")} {_t("Cancel")}
</AccessibleButton> </AccessibleButton>
</div> </div>
@ -228,21 +234,28 @@ export default class EmailAddresses extends React.Component {
); );
if (this.state.verifying) { if (this.state.verifying) {
addButton = ( addButton = (
<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> <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" <AccessibleButton
disabled={this.state.continueDisabled}> onClick={this._onContinueClick}
{_t("Continue")} kind="primary"
</AccessibleButton> disabled={this.state.continueDisabled}
</div> >
{_t("Continue")}
</AccessibleButton>
</div>
); );
} }
return ( return (
<div className="mx_EmailAddresses"> <div className="mx_EmailAddresses">
{existingEmailElements} {existingEmailElements}
<form onSubmit={this._onAddClick} autoComplete="off" <form
noValidate={true} className="mx_EmailAddresses_new"> onSubmit={this._onAddClick}
autoComplete="off"
noValidate={true}
className="mx_EmailAddresses_new"
>
<Field <Field
type="text" type="text"
label={_t("Email Address")} label={_t("Email Address")}

View file

@ -85,12 +85,18 @@ export class ExistingPhoneNumber extends React.Component {
<span className="mx_ExistingPhoneNumber_promptText"> <span className="mx_ExistingPhoneNumber_promptText">
{_t("Remove %(phone)s?", {phone: this.props.msisdn.address})} {_t("Remove %(phone)s?", {phone: this.props.msisdn.address})}
</span> </span>
<AccessibleButton onClick={this._onActuallyRemove} kind="danger_sm" <AccessibleButton
className="mx_ExistingPhoneNumber_confirmBtn"> onClick={this._onActuallyRemove}
kind="danger_sm"
className="mx_ExistingPhoneNumber_confirmBtn"
>
{_t("Remove")} {_t("Remove")}
</AccessibleButton> </AccessibleButton>
<AccessibleButton onClick={this._onDontRemove} kind="link_sm" <AccessibleButton
className="mx_ExistingPhoneNumber_confirmBtn"> onClick={this._onDontRemove}
kind="link_sm"
className="mx_ExistingPhoneNumber_confirmBtn"
>
{_t("Cancel")} {_t("Cancel")}
</AccessibleButton> </AccessibleButton>
</div> </div>
@ -246,8 +252,11 @@ export default class PhoneNumbers extends React.Component {
value={this.state.newPhoneNumberCode} value={this.state.newPhoneNumberCode}
onChange={this._onChangeNewPhoneNumberCode} onChange={this._onChangeNewPhoneNumberCode}
/> />
<AccessibleButton onClick={this._onContinueClick} kind="primary" <AccessibleButton
disabled={this.state.continueDisabled}> onClick={this._onContinueClick}
kind="primary"
disabled={this.state.continueDisabled}
>
{_t("Continue")} {_t("Continue")}
</AccessibleButton> </AccessibleButton>
</form> </form>

Some files were not shown because too many files have changed in this diff Show more