Merge branch 'develop' into gsouquet/fix-18132

This commit is contained in:
Germain Souquet 2021-07-26 15:49:55 +02:00
commit 486d576b23
151 changed files with 2280 additions and 1058 deletions

View file

@ -86,6 +86,7 @@
@import "./views/dialogs/_IncomingSasDialog.scss"; @import "./views/dialogs/_IncomingSasDialog.scss";
@import "./views/dialogs/_InviteDialog.scss"; @import "./views/dialogs/_InviteDialog.scss";
@import "./views/dialogs/_KeyboardShortcutsDialog.scss"; @import "./views/dialogs/_KeyboardShortcutsDialog.scss";
@import "./views/dialogs/_ManageRestrictedJoinRuleDialog.scss";
@import "./views/dialogs/_MessageEditHistoryDialog.scss"; @import "./views/dialogs/_MessageEditHistoryDialog.scss";
@import "./views/dialogs/_ModalWidgetDialog.scss"; @import "./views/dialogs/_ModalWidgetDialog.scss";
@import "./views/dialogs/_NewSessionReviewDialog.scss"; @import "./views/dialogs/_NewSessionReviewDialog.scss";

View file

@ -234,6 +234,9 @@ $SpaceRoomViewInnerWidth: 428px;
} }
.mx_SpaceRoomView_landing { .mx_SpaceRoomView_landing {
display: flex;
flex-direction: column;
> .mx_BaseAvatar_image, > .mx_BaseAvatar_image,
> .mx_BaseAvatar > .mx_BaseAvatar_image { > .mx_BaseAvatar > .mx_BaseAvatar_image {
border-radius: 12px; border-radius: 12px;
@ -340,6 +343,7 @@ $SpaceRoomViewInnerWidth: 428px;
.mx_SearchBox { .mx_SearchBox {
margin: 0 0 20px; margin: 0 0 20px;
flex: 0;
} }
.mx_SpaceFeedbackPrompt { .mx_SpaceFeedbackPrompt {
@ -350,6 +354,11 @@ $SpaceRoomViewInnerWidth: 428px;
display: none; display: none;
} }
} }
.mx_SpaceRoomDirectory_list {
// we don't want this container to get forced into the flexbox layout
display: contents;
}
} }
.mx_SpaceRoomView_privateScope { .mx_SpaceRoomView_privateScope {

View file

@ -27,7 +27,6 @@ limitations under the License.
// https://bugzilla.mozilla.org/show_bug.cgi?id=255139 // https://bugzilla.mozilla.org/show_bug.cgi?id=255139
display: inline-block; display: inline-block;
user-select: none; user-select: none;
line-height: 1;
} }
.mx_BaseAvatar_initial { .mx_BaseAvatar_initial {

View file

@ -65,7 +65,7 @@ limitations under the License.
.mx_CreateRoomDialog_aliasContainer { .mx_CreateRoomDialog_aliasContainer {
display: flex; display: flex;
// put margin on container so it can collapse with siblings // put margin on container so it can collapse with siblings
margin: 10px 0; margin: 24px 0 10px;
.mx_RoomAliasField { .mx_RoomAliasField {
margin: 0; margin: 0;
@ -101,10 +101,6 @@ limitations under the License.
margin-left: 30px; margin-left: 30px;
} }
.mx_CreateRoomDialog_topic {
margin-bottom: 36px;
}
.mx_Dialog_content > .mx_SettingsFlag { .mx_Dialog_content > .mx_SettingsFlag {
margin-top: 24px; margin-top: 24px;
} }
@ -113,5 +109,56 @@ limitations under the License.
margin: 0 85px 0 0; margin: 0 85px 0 0;
font-size: $font-12px; font-size: $font-12px;
} }
.mx_Dropdown {
margin-bottom: 8px;
font-weight: normal;
font-family: $font-family;
font-size: $font-14px;
color: $primary-fg-color;
.mx_Dropdown_input {
border: 1px solid $input-border-color;
}
.mx_Dropdown_option {
font-size: $font-14px;
line-height: $font-32px;
height: 32px;
min-height: 32px;
> div {
padding-left: 30px;
position: relative;
&::before {
content: "";
position: absolute;
height: 16px;
width: 16px;
left: 6px;
top: 8px;
mask-repeat: no-repeat;
mask-position: center;
background-color: $secondary-fg-color;
}
}
}
.mx_CreateRoomDialog_dropdown_invite::before {
mask-image: url('$(res)/img/element-icons/lock.svg');
mask-size: contain;
}
.mx_CreateRoomDialog_dropdown_public::before {
mask-image: url('$(res)/img/globe.svg');
mask-size: 12px;
}
.mx_CreateRoomDialog_dropdown_restricted::before {
mask-image: url('$(res)/img/element-icons/community-members.svg');
mask-size: contain;
}
}
} }

View file

@ -0,0 +1,150 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_ManageRestrictedJoinRuleDialog_wrapper {
.mx_Dialog {
display: flex;
flex-direction: column;
}
}
.mx_ManageRestrictedJoinRuleDialog {
width: 480px;
color: $primary-fg-color;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
min-height: 0;
height: 60vh;
.mx_SearchBox {
// To match the space around the title
margin: 0 0 15px 0;
flex-grow: 0;
}
.mx_ManageRestrictedJoinRuleDialog_content {
flex-grow: 1;
}
.mx_ManageRestrictedJoinRuleDialog_noResults {
display: block;
margin-top: 24px;
}
.mx_ManageRestrictedJoinRuleDialog_section {
&:not(:first-child) {
margin-top: 24px;
}
> h3 {
margin: 0;
color: $secondary-fg-color;
font-size: $font-12px;
font-weight: $font-semi-bold;
line-height: $font-15px;
}
.mx_ManageRestrictedJoinRuleDialog_entry {
display: flex;
margin-top: 12px;
> div {
flex-grow: 1;
}
img.mx_RoomAvatar_isSpaceRoom,
.mx_RoomAvatar_isSpaceRoom img {
border-radius: 4px;
}
.mx_ManageRestrictedJoinRuleDialog_entry_name {
margin: 0 8px;
font-size: $font-15px;
line-height: 30px;
flex-grow: 1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.mx_ManageRestrictedJoinRuleDialog_entry_description {
margin-top: 8px;
font-size: $font-12px;
line-height: $font-15px;
color: $tertiary-fg-color;
}
.mx_Checkbox {
align-items: center;
}
}
}
.mx_ManageRestrictedJoinRuleDialog_section_spaces {
.mx_BaseAvatar {
margin-right: 12px;
}
.mx_BaseAvatar_image {
border-radius: 8px;
}
}
.mx_ManageRestrictedJoinRuleDialog_section_info {
position: relative;
border-radius: 8px;
margin: 12px 0;
padding: 8px 8px 8px 42px;
background-color: $header-panel-bg-color;
font-size: $font-12px;
line-height: $font-15px;
color: $secondary-fg-color;
&::before {
content: '';
position: absolute;
left: 10px;
top: calc(50% - 8px); // vertical centering
height: 16px;
width: 16px;
background-color: $secondary-fg-color;
mask-repeat: no-repeat;
mask-size: contain;
mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
mask-position: center;
}
}
.mx_ManageRestrictedJoinRuleDialog_footer {
margin-top: 20px;
.mx_ManageRestrictedJoinRuleDialog_footer_buttons {
display: flex;
width: max-content;
margin-left: auto;
.mx_AccessibleButton {
display: inline-block;
& + .mx_AccessibleButton {
margin-left: 24px;
}
}
}
}
}

View file

@ -27,7 +27,7 @@ limitations under the License.
display: flex; display: flex;
align-items: center; align-items: center;
position: relative; position: relative;
border-radius: 3px; border-radius: 4px;
border: 1px solid $strong-input-border-color; border: 1px solid $strong-input-border-color;
font-size: $font-12px; font-size: $font-12px;
user-select: none; user-select: none;
@ -109,7 +109,7 @@ input.mx_Dropdown_option:focus {
z-index: 2; z-index: 2;
margin: 0; margin: 0;
padding: 0px; padding: 0px;
border-radius: 3px; border-radius: 4px;
border: 1px solid $input-focused-border-color; border: 1px solid $input-focused-border-color;
background-color: $primary-bg-color; background-color: $primary-bg-color;
max-height: 200px; max-height: 200px;

View file

@ -16,10 +16,6 @@ limitations under the License.
$timelineImageBorderRadius: 4px; $timelineImageBorderRadius: 4px;
.mx_MImageBody {
display: block;
}
.mx_MImageBody_thumbnail { .mx_MImageBody_thumbnail {
object-fit: contain; object-fit: contain;
border-radius: $timelineImageBorderRadius; border-radius: $timelineImageBorderRadius;
@ -28,7 +24,7 @@ $timelineImageBorderRadius: 4px;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
> canvas { > div > canvas {
border-radius: $timelineImageBorderRadius; border-radius: $timelineImageBorderRadius;
} }
} }

View file

@ -38,7 +38,8 @@ limitations under the License.
padding-top: 0; padding-top: 0;
} }
&:hover { &:hover,
&.mx_EventTile_selected {
&::before { &::before {
content: ''; content: '';
position: absolute; position: absolute;
@ -155,12 +156,24 @@ limitations under the License.
position: absolute; position: absolute;
top: 0; top: 0;
line-height: 1; line-height: 1;
z-index: 9;
img { img {
box-shadow: 0 0 0 3px $eventbubble-avatar-outline; box-shadow: 0 0 0 3px $eventbubble-avatar-outline;
border-radius: 50%; border-radius: 50%;
} }
} }
&.mx_EventTile_noSender {
.mx_EventTile_avatar {
top: -19px;
}
}
.mx_BaseAvatar,
.mx_EventTile_avatar {
line-height: 1;
}
&[data-has-reply=true] { &[data-has-reply=true] {
> .mx_EventTile_line { > .mx_EventTile_line {
flex-direction: column; flex-direction: column;
@ -219,6 +232,8 @@ limitations under the License.
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center;
padding: 5px 0;
.mx_EventTile_avatar { .mx_EventTile_avatar {
position: static; position: static;
@ -289,7 +304,7 @@ limitations under the License.
& + .mx_EventListSummary { & + .mx_EventListSummary {
.mx_EventTile { .mx_EventTile {
margin-top: 0; margin-top: 0;
padding: 0; padding: 2px 0;
} }
} }

View file

@ -132,7 +132,8 @@ $hover-select-border: 4px;
} }
} }
&.mx_EventTile_info .mx_EventTile_line { &.mx_EventTile_info .mx_EventTile_line,
& ~ .mx_EventListSummary .mx_EventTile_avatar ~ .mx_EventTile_line {
padding-left: calc($left-gutter + 18px); padding-left: calc($left-gutter + 18px);
} }

View file

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

View file

@ -116,6 +116,11 @@ $irc-line-height: $font-18px;
.mx_EditMessageComposer_buttons { .mx_EditMessageComposer_buttons {
position: relative; position: relative;
} }
.mx_ReactionsRow {
padding-left: 0;
padding-right: 0;
}
} }
.mx_EventTile_emote { .mx_EventTile_emote {

View file

@ -34,7 +34,7 @@ limitations under the License.
.mx_LinkPreviewWidget_caption { .mx_LinkPreviewWidget_caption {
margin-left: 15px; margin-left: 15px;
flex: 1 1 auto; flex: 1 1 auto;
overflow-x: hidden; // cause it to wrap rather than clip overflow: hidden; // cause it to wrap rather than clip
} }
.mx_LinkPreviewWidget_title { .mx_LinkPreviewWidget_title {

View file

@ -47,14 +47,14 @@ limitations under the License.
color: $settings-subsection-fg-color; color: $settings-subsection-fg-color;
font-size: $font-14px; font-size: $font-14px;
display: block; display: block;
margin: 10px 100px 10px 0; // Align with the rest of the view margin: 10px 80px 10px 0; // Align with the rest of the view
} }
.mx_SettingsTab_section { .mx_SettingsTab_section {
margin-bottom: 24px; margin-bottom: 24px;
.mx_SettingsFlag { .mx_SettingsFlag {
margin-right: 100px; margin-right: 80px;
margin-bottom: 10px; margin-bottom: 10px;
} }

View file

@ -14,6 +14,44 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_SecurityRoomSettingsTab {
.mx_SettingsTab_showAdvanced {
padding: 0;
margin-bottom: 16px;
}
.mx_SecurityRoomSettingsTab_spacesWithAccess {
> h4 {
color: $secondary-fg-color;
font-weight: $font-semi-bold;
font-size: $font-12px;
line-height: $font-15px;
text-transform: uppercase;
}
> span {
font-weight: 500;
font-size: $font-14px;
line-height: 32px; // matches height of avatar for v-align
color: $secondary-fg-color;
display: inline-block;
img.mx_RoomAvatar_isSpaceRoom,
.mx_RoomAvatar_isSpaceRoom img {
border-radius: 8px;
}
.mx_BaseAvatar {
margin-right: 8px;
}
& + span {
margin-left: 16px;
}
}
}
}
.mx_SecurityRoomSettingsTab_warning { .mx_SecurityRoomSettingsTab_warning {
display: block; display: block;
@ -26,5 +64,51 @@ limitations under the License.
} }
.mx_SecurityRoomSettingsTab_encryptionSection { .mx_SecurityRoomSettingsTab_encryptionSection {
margin-bottom: 25px; padding-bottom: 24px;
border-bottom: 1px solid $menu-border-color;
margin-bottom: 32px;
}
.mx_SecurityRoomSettingsTab_upgradeRequired {
margin-left: 16px;
padding: 4px 16px;
border: 1px solid $accent-color;
border-radius: 8px;
color: $accent-color;
font-size: $font-12px;
line-height: $font-15px;
}
.mx_SecurityRoomSettingsTab_joinRule {
.mx_RadioButton {
padding-top: 16px;
margin-bottom: 8px;
.mx_RadioButton_content {
margin-left: 14px;
font-weight: $font-semi-bold;
font-size: $font-15px;
line-height: $font-24px;
color: $primary-fg-color;
display: block;
}
}
> span {
display: inline-block;
margin-left: 34px;
margin-bottom: 16px;
font-size: $font-15px;
line-height: $font-24px;
color: $secondary-fg-color;
& + .mx_RadioButton {
border-top: 1px solid $menu-border-color;
}
}
.mx_AccessibleButton_kind_link {
padding: 0;
font-size: inherit;
}
} }

View file

@ -1,7 +0,0 @@
<svg height="12" viewBox="0 0 12 12" width="12" xmlns="http://www.w3.org/2000/svg">
<g style="stroke:#454545;stroke-width:.8;fill:none;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round" transform="translate(1 1)">
<circle cx="5" cy="5" r="5"/>
<path d="m0 5h10"/>
<path d="m5 0c1.25064019 1.36917645 1.96137638 3.14601693 2 5-.03862362 1.85398307-.74935981 3.63082355-2 5-1.25064019-1.36917645-1.96137638-3.14601693-2-5 .03862362-1.85398307.74935981-3.63082355 2-5z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 524 B

View file

@ -51,10 +51,15 @@ export async function startAnyRegistrationFlow(options) {
description: _t("Use your account or create a new one to continue."), description: _t("Use your account or create a new one to continue."),
button: _t("Create Account"), button: _t("Create Account"),
extraButtons: [ extraButtons: [
<button key="start_login" onClick={() => { <button
key="start_login"
onClick={() => {
modal.close(); modal.close();
dis.dispatch({ action: 'start_login', screenAfterLogin: options.screen_after }); dis.dispatch({ action: 'start_login', screenAfterLogin: options.screen_after });
}}>{ _t('Sign In') }</button>, }}
>
{ _t('Sign In') }
</button>,
], ],
onFinished: (proceed) => { onFinished: (proceed) => {
if (proceed) { if (proceed) {

View file

@ -34,7 +34,6 @@ import { getAddressType } from './UserAddress';
import { abbreviateUrl } from './utils/UrlUtils'; import { abbreviateUrl } from './utils/UrlUtils';
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from './utils/IdentityServerUtils'; import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from './utils/IdentityServerUtils';
import { isPermalinkHost, parsePermalink } from "./utils/permalinks/Permalinks"; import { isPermalinkHost, parsePermalink } from "./utils/permalinks/Permalinks";
import { inviteUsersToRoom } from "./RoomInvite";
import { WidgetType } from "./widgets/WidgetType"; import { WidgetType } from "./widgets/WidgetType";
import { Jitsi } from "./widgets/Jitsi"; import { Jitsi } from "./widgets/Jitsi";
import { parseFragment as parseHtml, Element as ChildElement } from "parse5"; import { parseFragment as parseHtml, Element as ChildElement } from "parse5";
@ -49,6 +48,7 @@ import { UIFeature } from "./settings/UIFeature";
import { CHAT_EFFECTS } from "./effects"; import { CHAT_EFFECTS } from "./effects";
import CallHandler from "./CallHandler"; import CallHandler from "./CallHandler";
import { guessAndSetDMRoom } from "./Rooms"; import { guessAndSetDMRoom } from "./Rooms";
import { upgradeRoom } from './utils/RoomUpgrade';
import UploadConfirmDialog from './components/views/dialogs/UploadConfirmDialog'; import UploadConfirmDialog from './components/views/dialogs/UploadConfirmDialog';
import ErrorDialog from './components/views/dialogs/ErrorDialog'; import ErrorDialog from './components/views/dialogs/ErrorDialog';
import DevtoolsDialog from './components/views/dialogs/DevtoolsDialog'; import DevtoolsDialog from './components/views/dialogs/DevtoolsDialog';
@ -277,50 +277,8 @@ export const Commands = [
/*isPriority=*/false, /*isStatic=*/true); /*isPriority=*/false, /*isStatic=*/true);
return success(finished.then(async ([resp]) => { return success(finished.then(async ([resp]) => {
if (!resp.continue) return; if (!resp?.continue) return;
await upgradeRoom(room, args, resp.invite);
let checkForUpgradeFn;
try {
const upgradePromise = cli.upgradeRoom(roomId, args);
// We have to wait for the js-sdk to give us the room back so
// we can more effectively abuse the MultiInviter behaviour
// which heavily relies on the Room object being available.
if (resp.invite) {
checkForUpgradeFn = async (newRoom) => {
// The upgradePromise should be done by the time we await it here.
const { replacement_room: newRoomId } = await upgradePromise;
if (newRoom.roomId !== newRoomId) return;
const toInvite = [
...room.getMembersWithMembership("join"),
...room.getMembersWithMembership("invite"),
].map(m => m.userId).filter(m => m !== cli.getUserId());
if (toInvite.length > 0) {
// Errors are handled internally to this function
await inviteUsersToRoom(newRoomId, toInvite);
}
cli.removeListener('Room', checkForUpgradeFn);
};
cli.on('Room', checkForUpgradeFn);
}
// We have to await after so that the checkForUpgradesFn has a proper reference
// to the new room's ID.
await upgradePromise;
} catch (e) {
console.error(e);
if (checkForUpgradeFn) cli.removeListener('Room', checkForUpgradeFn);
Modal.createTrackedDialog('Slash Commands', 'room upgrade error', ErrorDialog, {
title: _t('Error upgrading room'),
description: _t(
'Double check that your server supports the room version chosen and try again.'),
});
}
})); }));
} }
return reject(this.getUsage()); return reject(this.getUsage());

View file

@ -474,7 +474,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
outlined outlined
> >
<div className="mx_CreateSecretStorageDialog_optionTitle"> <div className="mx_CreateSecretStorageDialog_optionTitle">
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_secureBackup"></span> <span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_secureBackup" />
{ _t("Generate a Security Key") } { _t("Generate a Security Key") }
</div> </div>
<div>{ _t("Well generate a Security Key for you to store somewhere safe, like a password manager or a safe.") }</div> <div>{ _t("Well generate a Security Key for you to store somewhere safe, like a password manager or a safe.") }</div>
@ -493,7 +493,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
outlined outlined
> >
<div className="mx_CreateSecretStorageDialog_optionTitle"> <div className="mx_CreateSecretStorageDialog_optionTitle">
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_securePhrase"></span> <span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_securePhrase" />
{ _t("Enter a Security Phrase") } { _t("Enter a Security Phrase") }
</div> </div>
<div>{ _t("Use a secret phrase only you know, and optionally save a Security Key to use for backup.") }</div> <div>{ _t("Use a secret phrase only you know, and optionally save a Security Key to use for backup.") }</div>
@ -701,7 +701,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
<code ref={this._collectRecoveryKeyNode}>{ this._recoveryKey.encodedPrivateKey }</code> <code ref={this._collectRecoveryKeyNode}>{ this._recoveryKey.encodedPrivateKey }</code>
</div> </div>
<div className="mx_CreateSecretStorageDialog_recoveryKeyButtons"> <div className="mx_CreateSecretStorageDialog_recoveryKeyButtons">
<AccessibleButton kind='primary' className="mx_Dialog_primary" <AccessibleButton kind='primary'
className="mx_Dialog_primary"
onClick={this._onDownloadClick} onClick={this._onDownloadClick}
disabled={this.state.phase === PHASE_STORING} disabled={this.state.phase === PHASE_STORING}
> >

View file

@ -148,8 +148,12 @@ export default class ExportE2eKeysDialog extends React.Component {
</label> </label>
</div> </div>
<div className='mx_E2eKeysDialog_inputCell'> <div className='mx_E2eKeysDialog_inputCell'>
<input ref={this._passphrase1} id='passphrase1' <input
autoFocus={true} size='64' type='password' ref={this._passphrase1}
id='passphrase1'
autoFocus={true}
size='64'
type='password'
disabled={disableForm} disabled={disableForm}
/> />
</div> </div>
@ -161,8 +165,10 @@ export default class ExportE2eKeysDialog extends React.Component {
</label> </label>
</div> </div>
<div className='mx_E2eKeysDialog_inputCell'> <div className='mx_E2eKeysDialog_inputCell'>
<input ref={this._passphrase2} id='passphrase2' <input ref={this._passphrase2}
size='64' type='password' id='passphrase2'
size='64'
type='password'
disabled={disableForm} disabled={disableForm}
/> />
</div> </div>

View file

@ -174,7 +174,10 @@ export default class ImportE2eKeysDialog extends React.Component {
</div> </div>
</div> </div>
<div className='mx_Dialog_buttons'> <div className='mx_Dialog_buttons'>
<input className='mx_Dialog_primary' type='submit' value={_t('Import')} <input
className='mx_Dialog_primary'
type='submit'
value={_t('Import')}
disabled={!this.state.enableSubmit || disableForm} disabled={!this.state.enableSubmit || disableForm}
/> />
<button onClick={this._onCancelClick} disabled={disableForm}> <button onClick={this._onCancelClick} disabled={disableForm}>

View file

@ -120,8 +120,7 @@ export default class EmbeddedPage extends React.PureComponent {
const content = <div className={`${className}_body`} const content = <div className={`${className}_body`}
dangerouslySetInnerHTML={{ __html: this.state.page }} dangerouslySetInnerHTML={{ __html: this.state.page }}
> />;
</div>;
if (this.props.scrollbar) { if (this.props.scrollbar) {
return <AutoHideScrollbar className={classes}> return <AutoHideScrollbar className={classes}>

View file

@ -1185,10 +1185,13 @@ export default class GroupView extends React.Component {
avatarImage = <Spinner />; avatarImage = <Spinner />;
} else { } else {
const GroupAvatar = sdk.getComponent('avatars.GroupAvatar'); const GroupAvatar = sdk.getComponent('avatars.GroupAvatar');
avatarImage = <GroupAvatar groupId={this.props.groupId} avatarImage = <GroupAvatar
groupId={this.props.groupId}
groupName={this.state.profileForm.name} groupName={this.state.profileForm.name}
groupAvatarUrl={this.state.profileForm.avatar_url} groupAvatarUrl={this.state.profileForm.avatar_url}
width={28} height={28} resizeMethod='crop' width={28}
height={28}
resizeMethod='crop'
/>; />;
} }
@ -1199,9 +1202,12 @@ export default class GroupView extends React.Component {
</label> </label>
<div className="mx_GroupView_avatarPicker_edit"> <div className="mx_GroupView_avatarPicker_edit">
<label htmlFor="avatarInput" className="mx_GroupView_avatarPicker_label"> <label htmlFor="avatarInput" className="mx_GroupView_avatarPicker_label">
<img src={require("../../../res/img/camera.svg")} <img
alt={_t("Upload avatar")} title={_t("Upload avatar")} src={require("../../../res/img/camera.svg")}
width="17" height="15" /> alt={_t("Upload avatar")}
title={_t("Upload avatar")}
width="17"
height="15" />
</label> </label>
<input id="avatarInput" className="mx_GroupView_uploadInput" type="file" onChange={this._onAvatarSelected} /> <input id="avatarInput" className="mx_GroupView_uploadInput" type="file" onChange={this._onAvatarSelected} />
</div> </div>
@ -1238,7 +1244,8 @@ export default class GroupView extends React.Component {
groupAvatarUrl={groupAvatarUrl} groupAvatarUrl={groupAvatarUrl}
groupName={groupName} groupName={groupName}
onClick={onGroupHeaderItemClick} onClick={onGroupHeaderItemClick}
width={28} height={28} width={28}
height={28}
/>; />;
if (summary.profile && summary.profile.name) { if (summary.profile && summary.profile.name) {
nameNode = <div onClick={onGroupHeaderItemClick}> nameNode = <div onClick={onGroupHeaderItemClick}>
@ -1269,28 +1276,32 @@ export default class GroupView extends React.Component {
key="_cancelButton" key="_cancelButton"
onClick={this._onCancelClick} onClick={this._onCancelClick}
> >
<img src={require("../../../res/img/cancel.svg")} className="mx_filterFlipColor" <img
width="18" height="18" alt={_t("Cancel")} /> src={require("../../../res/img/cancel.svg")}
className="mx_filterFlipColor"
width="18"
height="18"
alt={_t("Cancel")} />
</AccessibleButton>, </AccessibleButton>,
); );
} else { } else {
if (summary.user && summary.user.membership === 'join') { if (summary.user && summary.user.membership === 'join') {
rightButtons.push( rightButtons.push(
<AccessibleButton className="mx_GroupHeader_button mx_GroupHeader_editButton" <AccessibleButton
className="mx_GroupHeader_button mx_GroupHeader_editButton"
key="_editButton" key="_editButton"
onClick={this._onEditClick} onClick={this._onEditClick}
title={_t("Community Settings")} title={_t("Community Settings")}
> />,
</AccessibleButton>,
); );
} }
rightButtons.push( rightButtons.push(
<AccessibleButton className="mx_GroupHeader_button mx_GroupHeader_shareButton" <AccessibleButton
className="mx_GroupHeader_button mx_GroupHeader_shareButton"
key="_shareButton" key="_shareButton"
onClick={this._onShareClick} onClick={this._onShareClick}
title={_t('Share Community')} title={_t('Share Community')}
> />,
</AccessibleButton>,
); );
} }

View file

@ -109,8 +109,7 @@ export default class MyGroups extends React.Component {
<SimpleRoomHeader title={_t("Communities")} icon={require("../../../res/img/icons-groups.svg")} /> <SimpleRoomHeader title={_t("Communities")} icon={require("../../../res/img/icons-groups.svg")} />
<div className='mx_MyGroups_header'> <div className='mx_MyGroups_header'>
<div className="mx_MyGroups_headerCard"> <div className="mx_MyGroups_headerCard">
<AccessibleButton className='mx_MyGroups_headerCard_button' onClick={this._onCreateGroupClick}> <AccessibleButton className='mx_MyGroups_headerCard_button' onClick={this._onCreateGroupClick} />
</AccessibleButton>
<div className="mx_MyGroups_headerCard_content"> <div className="mx_MyGroups_headerCard_content">
<div className="mx_MyGroups_headerCard_header"> <div className="mx_MyGroups_headerCard_header">
{ _t('Create a new community') } { _t('Create a new community') }

View file

@ -266,8 +266,12 @@ export default class RoomStatusBar extends React.PureComponent {
<div className="mx_RoomStatusBar"> <div className="mx_RoomStatusBar">
<div role="alert"> <div role="alert">
<div className="mx_RoomStatusBar_connectionLostBar"> <div className="mx_RoomStatusBar_connectionLostBar">
<img src={require("../../../res/img/feather-customised/warning-triangle.svg")} width="24" <img
height="24" title="/!\ " alt="/!\ " /> src={require("../../../res/img/feather-customised/warning-triangle.svg")}
width="24"
height="24"
title="/!\ "
alt="/!\ " />
<div> <div>
<div className="mx_RoomStatusBar_connectionLostBar_title"> <div className="mx_RoomStatusBar_connectionLostBar_title">
{ _t('Connectivity to the server has been lost.') } { _t('Connectivity to the server has been lost.') }

View file

@ -1740,7 +1740,8 @@ export default class RoomView extends React.Component<IProps, IState> {
onJoinClick={this.onJoinButtonClicked} onJoinClick={this.onJoinButtonClicked}
onForgetClick={this.onForgetClick} onForgetClick={this.onForgetClick}
onRejectClick={this.onRejectThreepidInviteButtonClicked} onRejectClick={this.onRejectThreepidInviteButtonClicked}
canPreview={false} error={this.state.roomLoadError} canPreview={false}
error={this.state.roomLoadError}
roomAlias={roomAlias} roomAlias={roomAlias}
joining={this.state.joining} joining={this.state.joining}
inviterName={inviterName} inviterName={inviterName}

View file

@ -136,8 +136,8 @@ export default class SearchBox extends React.Component {
key="button" key="button"
tabIndex={-1} tabIndex={-1}
className="mx_SearchBox_closeButton" className="mx_SearchBox_closeButton"
onClick={() => {this._clearSearch("button"); }}> onClick={() => {this._clearSearch("button"); }}
</AccessibleButton>) : undefined; />) : undefined;
// show a shorter placeholder when blurred, if requested // show a shorter placeholder when blurred, if requested
// this is used for the room filter field that has // this is used for the room filter field that has

View file

@ -16,7 +16,7 @@ limitations under the License.
import React, { RefObject, useContext, useRef, useState } from "react"; import React, { RefObject, useContext, useRef, useState } from "react";
import { EventType } from "matrix-js-sdk/src/@types/event"; import { EventType } from "matrix-js-sdk/src/@types/event";
import { Preset } from "matrix-js-sdk/src/@types/partials"; import { Preset, JoinRule } from "matrix-js-sdk/src/@types/partials";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { EventSubscription } from "fbemitter"; import { EventSubscription } from "fbemitter";
@ -66,7 +66,6 @@ import Modal from "../../Modal";
import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog"; import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog";
import SdkConfig from "../../SdkConfig"; import SdkConfig from "../../SdkConfig";
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership"; import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
import { JoinRule } from "../views/settings/tabs/room/SecurityRoomSettingsTab";
interface IProps { interface IProps {
space: Room; space: Room;
@ -101,7 +100,9 @@ export const SpaceFeedbackPrompt = ({ onClick }: { onClick?: () => void }) => {
<hr /> <hr />
<div> <div>
<span className="mx_SpaceFeedbackPrompt_text">{ _t("Spaces are a beta feature.") }</span> <span className="mx_SpaceFeedbackPrompt_text">{ _t("Spaces are a beta feature.") }</span>
<AccessibleButton kind="link" onClick={() => { <AccessibleButton
kind="link"
onClick={() => {
if (onClick) onClick(); if (onClick) onClick();
Modal.createTrackedDialog("Beta Feedback", "feature_spaces", BetaFeedbackDialog, { Modal.createTrackedDialog("Beta Feedback", "feature_spaces", BetaFeedbackDialog, {
featureId: "feature_spaces", featureId: "feature_spaces",
@ -307,7 +308,6 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
}; };
const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => { const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => {
const cli = useContext(MatrixClientContext);
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(); const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
let contextMenu; let contextMenu;
@ -330,7 +330,7 @@ const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => {
e.stopPropagation(); e.stopPropagation();
closeMenu(); closeMenu();
if (await showCreateNewRoom(cli, space)) { if (await showCreateNewRoom(space)) {
onNewRoomAdded(); onNewRoomAdded();
} }
}} }}
@ -343,7 +343,7 @@ const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => {
e.stopPropagation(); e.stopPropagation();
closeMenu(); closeMenu();
const [added] = await showAddExistingRooms(cli, space); const [added] = await showAddExistingRooms(space);
if (added) { if (added) {
onNewRoomAdded(); onNewRoomAdded();
} }
@ -397,11 +397,11 @@ const SpaceLanding = ({ space }) => {
} }
let settingsButton; let settingsButton;
if (shouldShowSpaceSettings(cli, space)) { if (shouldShowSpaceSettings(space)) {
settingsButton = <AccessibleTooltipButton settingsButton = <AccessibleTooltipButton
className="mx_SpaceRoomView_landing_settingsButton" className="mx_SpaceRoomView_landing_settingsButton"
onClick={() => { onClick={() => {
showSpaceSettings(cli, space); showSpaceSettings(space);
}} }}
title={_t("Settings")} title={_t("Settings")}
/>; />;
@ -553,9 +553,7 @@ const SpaceAddExistingRooms = ({ space, onFinished }) => {
onFinished={onFinished} onFinished={onFinished}
/> />
<div className="mx_SpaceRoomView_buttons"> <div className="mx_SpaceRoomView_buttons" />
</div>
<SpaceFeedbackPrompt /> <SpaceFeedbackPrompt />
</div>; </div>;
}; };

View file

@ -315,7 +315,10 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
{ _t("An email has been sent to %(emailAddress)s. Once you've followed the " + { _t("An email has been sent to %(emailAddress)s. Once you've followed the " +
"link it contains, click below.", { emailAddress: this.state.email }) } "link it contains, click below.", { emailAddress: this.state.email }) }
<br /> <br />
<input className="mx_Login_submit" type="button" onClick={this.onVerify} <input
className="mx_Login_submit"
type="button"
onClick={this.onVerify}
value={_t('I have verified my email address')} /> value={_t('I have verified my email address')} />
</div>; </div>;
} }
@ -328,7 +331,10 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
"push notifications. To re-enable notifications, sign in again on each " + "push notifications. To re-enable notifications, sign in again on each " +
"device.", "device.",
) }</p> ) }</p>
<input className="mx_Login_submit" type="button" onClick={this.props.onComplete} <input
className="mx_Login_submit"
type="button"
onClick={this.props.onComplete}
value={_t('Return to login screen')} /> value={_t('Return to login screen')} />
</div>; </div>;
} }

View file

@ -463,7 +463,9 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
"Either use HTTPS or <a>enable unsafe scripts</a>.", {}, "Either use HTTPS or <a>enable unsafe scripts</a>.", {},
{ {
'a': (sub) => { 'a': (sub) => {
return <a target="_blank" rel="noreferrer noopener" return <a
target="_blank"
rel="noreferrer noopener"
href="https://www.google.com/search?&q=enable%20unsafe%20scripts" href="https://www.google.com/search?&q=enable%20unsafe%20scripts"
> >
{ sub } { sub }

View file

@ -557,12 +557,16 @@ export default class Registration extends React.Component<IProps, IState> {
loggedInUserId: this.state.differentLoggedInUserId, loggedInUserId: this.state.differentLoggedInUserId,
}, },
) }</p> ) }</p>
<p><AccessibleButton element="span" className="mx_linkButton" onClick={async event => { <p><AccessibleButton
element="span"
className="mx_linkButton"
onClick={async event => {
const sessionLoaded = await this.onLoginClickWithCheck(event); const sessionLoaded = await this.onLoginClickWithCheck(event);
if (sessionLoaded) { if (sessionLoaded) {
dis.dispatch({ action: "view_welcome_page" }); dis.dispatch({ action: "view_welcome_page" });
} }
}}> }}
>
{ _t("Continue with previous account") } { _t("Continue with previous account") }
</AccessibleButton></p> </AccessibleButton></p>
</div>; </div>;

View file

@ -14,9 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { Playback, PlaybackState } from "../../../voice/Playback";
import React, { createRef, ReactNode, RefObject } from "react"; import React, { createRef, ReactNode, RefObject } from "react";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import PlayPauseButton from "./PlayPauseButton"; import PlayPauseButton from "./PlayPauseButton";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { formatBytes } from "../../../utils/FormattingUtils"; import { formatBytes } from "../../../utils/FormattingUtils";
@ -25,47 +23,13 @@ import { Key } from "../../../Keyboard";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import SeekBar from "./SeekBar"; import SeekBar from "./SeekBar";
import PlaybackClock from "./PlaybackClock"; import PlaybackClock from "./PlaybackClock";
import AudioPlayerBase from "./AudioPlayerBase";
interface IProps {
// Playback instance to render. Cannot change during component lifecycle: create
// an all-new component instead.
playback: Playback;
mediaName: string;
}
interface IState {
playbackPhase: PlaybackState;
error?: boolean;
}
@replaceableComponent("views.audio_messages.AudioPlayer") @replaceableComponent("views.audio_messages.AudioPlayer")
export default class AudioPlayer extends React.PureComponent<IProps, IState> { export default class AudioPlayer extends AudioPlayerBase {
private playPauseRef: RefObject<PlayPauseButton> = createRef(); private playPauseRef: RefObject<PlayPauseButton> = createRef();
private seekRef: RefObject<SeekBar> = createRef(); private seekRef: RefObject<SeekBar> = createRef();
constructor(props: IProps) {
super(props);
this.state = {
playbackPhase: PlaybackState.Decoding, // default assumption
};
// We don't need to de-register: the class handles this for us internally
this.props.playback.on(UPDATE_EVENT, this.onPlaybackUpdate);
// Don't wait for the promise to complete - it will emit a progress update when it
// is done, and it's not meant to take long anyhow.
this.props.playback.prepare().catch(e => {
console.error("Error processing audio file:", e);
this.setState({ error: true });
});
}
private onPlaybackUpdate = (ev: PlaybackState) => {
this.setState({ playbackPhase: ev });
};
private onKeyDown = (ev: React.KeyboardEvent) => { private onKeyDown = (ev: React.KeyboardEvent) => {
// stopPropagation() prevents the FocusComposer catch-all from triggering, // stopPropagation() prevents the FocusComposer catch-all from triggering,
// but we need to do it on key down instead of press (even though the user // but we need to do it on key down instead of press (even though the user
@ -91,10 +55,10 @@ export default class AudioPlayer extends React.PureComponent<IProps, IState> {
return `(${formatBytes(bytes)})`; return `(${formatBytes(bytes)})`;
} }
public render(): ReactNode { protected renderComponent(): ReactNode {
// tabIndex=0 to ensure that the whole component becomes a tab stop, where we handle keyboard // tabIndex=0 to ensure that the whole component becomes a tab stop, where we handle keyboard
// events for accessibility // events for accessibility
return <> return (
<div className='mx_MediaBody mx_AudioPlayer_container' tabIndex={0} onKeyDown={this.onKeyDown}> <div className='mx_MediaBody mx_AudioPlayer_container' tabIndex={0} onKeyDown={this.onKeyDown}>
<div className='mx_AudioPlayer_primaryContainer'> <div className='mx_AudioPlayer_primaryContainer'>
<PlayPauseButton <PlayPauseButton
@ -124,7 +88,6 @@ export default class AudioPlayer extends React.PureComponent<IProps, IState> {
<PlaybackClock playback={this.props.playback} defaultDisplaySeconds={0} /> <PlaybackClock playback={this.props.playback} defaultDisplaySeconds={0} />
</div> </div>
</div> </div>
{ this.state.error && <div className="text-warning">{ _t("Error downloading audio") }</div> } );
</>;
} }
} }

View file

@ -0,0 +1,70 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Playback, PlaybackState } from "../../../audio/Playback";
import { TileShape } from "../rooms/EventTile";
import React, { ReactNode } from "react";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { _t } from "../../../languageHandler";
interface IProps {
// Playback instance to render. Cannot change during component lifecycle: create
// an all-new component instead.
playback: Playback;
mediaName?: string;
tileShape?: TileShape;
}
interface IState {
playbackPhase: PlaybackState;
error?: boolean;
}
@replaceableComponent("views.audio_messages.AudioPlayerBase")
export default abstract class AudioPlayerBase extends React.PureComponent<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
playbackPhase: PlaybackState.Decoding, // default assumption
};
// We don't need to de-register: the class handles this for us internally
this.props.playback.on(UPDATE_EVENT, this.onPlaybackUpdate);
// Don't wait for the promise to complete - it will emit a progress update when it
// is done, and it's not meant to take long anyhow.
this.props.playback.prepare().catch(e => {
console.error("Error processing audio file:", e);
this.setState({ error: true });
});
}
private onPlaybackUpdate = (ev: PlaybackState) => {
this.setState({ playbackPhase: ev });
};
protected abstract renderComponent(): ReactNode;
public render(): ReactNode {
return <>
{ this.renderComponent() }
{ this.state.error && <div className="text-warning">{ _t("Error downloading audio") }</div> }
</>;
}
}

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from "react"; import React from "react";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import Clock from "./Clock"; import Clock from "./Clock";
import { Playback } from "../../../voice/Playback"; import { Playback } from "../../../audio/Playback";
interface IProps { interface IProps {
playback: Playback; playback: Playback;

View file

@ -15,7 +15,7 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import { IRecordingUpdate, VoiceRecording } from "../../../voice/VoiceRecording"; import { IRecordingUpdate, VoiceRecording } from "../../../audio/VoiceRecording";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import Clock from "./Clock"; import Clock from "./Clock";
import { MarkedExecution } from "../../../utils/MarkedExecution"; import { MarkedExecution } from "../../../utils/MarkedExecution";

View file

@ -15,7 +15,7 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import { IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording } from "../../../voice/VoiceRecording"; import { IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording } from "../../../audio/VoiceRecording";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { arrayFastResample } from "../../../utils/arrays"; import { arrayFastResample } from "../../../utils/arrays";
import { percentageOf } from "../../../utils/numbers"; import { percentageOf } from "../../../utils/numbers";

View file

@ -18,7 +18,7 @@ import React, { ReactNode } from "react";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { Playback, PlaybackState } from "../../../voice/Playback"; import { Playback, PlaybackState } from "../../../audio/Playback";
import classNames from "classnames"; import classNames from "classnames";
// omitted props are handled by render function // omitted props are handled by render function

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from "react"; import React from "react";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import Clock from "./Clock"; import Clock from "./Clock";
import { Playback, PlaybackState } from "../../../voice/Playback"; import { Playback, PlaybackState } from "../../../audio/Playback";
import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import { UPDATE_EVENT } from "../../../stores/AsyncStore";
interface IProps { interface IProps {

View file

@ -18,7 +18,7 @@ import React from "react";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { arraySeed, arrayTrimFill } from "../../../utils/arrays"; import { arraySeed, arrayTrimFill } from "../../../utils/arrays";
import Waveform from "./Waveform"; import Waveform from "./Waveform";
import { Playback, PLAYBACK_WAVEFORM_SAMPLES } from "../../../voice/Playback"; import { Playback, PLAYBACK_WAVEFORM_SAMPLES } from "../../../audio/Playback";
import { percentageOf } from "../../../utils/numbers"; import { percentageOf } from "../../../utils/numbers";
interface IProps { interface IProps {

View file

@ -14,68 +14,30 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { Playback, PlaybackState } from "../../../voice/Playback";
import React, { ReactNode } from "react"; import React, { ReactNode } from "react";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import PlayPauseButton from "./PlayPauseButton"; import PlayPauseButton from "./PlayPauseButton";
import PlaybackClock from "./PlaybackClock"; import PlaybackClock from "./PlaybackClock";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { TileShape } from "../rooms/EventTile"; import { TileShape } from "../rooms/EventTile";
import PlaybackWaveform from "./PlaybackWaveform"; import PlaybackWaveform from "./PlaybackWaveform";
import { _t } from "../../../languageHandler"; import AudioPlayerBase from "./AudioPlayerBase";
interface IProps {
// Playback instance to render. Cannot change during component lifecycle: create
// an all-new component instead.
playback: Playback;
tileShape?: TileShape;
}
interface IState {
playbackPhase: PlaybackState;
error?: boolean;
}
@replaceableComponent("views.audio_messages.RecordingPlayback") @replaceableComponent("views.audio_messages.RecordingPlayback")
export default class RecordingPlayback extends React.PureComponent<IProps, IState> { export default class RecordingPlayback extends AudioPlayerBase {
constructor(props: IProps) {
super(props);
this.state = {
playbackPhase: PlaybackState.Decoding, // default assumption
};
// We don't need to de-register: the class handles this for us internally
this.props.playback.on(UPDATE_EVENT, this.onPlaybackUpdate);
// Don't wait for the promise to complete - it will emit a progress update when it
// is done, and it's not meant to take long anyhow.
this.props.playback.prepare().catch(e => {
console.error("Error processing audio file:", e);
this.setState({ error: true });
});
}
private get isWaveformable(): boolean { private get isWaveformable(): boolean {
return this.props.tileShape !== TileShape.Notif return this.props.tileShape !== TileShape.Notif
&& this.props.tileShape !== TileShape.FileGrid && this.props.tileShape !== TileShape.FileGrid
&& this.props.tileShape !== TileShape.Pinned; && this.props.tileShape !== TileShape.Pinned;
} }
private onPlaybackUpdate = (ev: PlaybackState) => { protected renderComponent(): ReactNode {
this.setState({ playbackPhase: ev });
};
public render(): ReactNode {
const shapeClass = !this.isWaveformable ? 'mx_VoiceMessagePrimaryContainer_noWaveform' : ''; const shapeClass = !this.isWaveformable ? 'mx_VoiceMessagePrimaryContainer_noWaveform' : '';
return <> return (
<div className={'mx_MediaBody mx_VoiceMessagePrimaryContainer ' + shapeClass}> <div className={'mx_MediaBody mx_VoiceMessagePrimaryContainer ' + shapeClass}>
<PlayPauseButton playback={this.props.playback} playbackPhase={this.state.playbackPhase} /> <PlayPauseButton playback={this.props.playback} playbackPhase={this.state.playbackPhase} />
<PlaybackClock playback={this.props.playback} /> <PlaybackClock playback={this.props.playback} />
{ this.isWaveformable && <PlaybackWaveform playback={this.props.playback} /> } { this.isWaveformable && <PlaybackWaveform playback={this.props.playback} /> }
</div> </div>
{ this.state.error && <div className="text-warning">{ _t("Error downloading audio") }</div> } );
</>;
} }
} }

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { Playback, PlaybackState } from "../../../voice/Playback"; import { Playback, PlaybackState } from "../../../audio/Playback";
import React, { ChangeEvent, CSSProperties, ReactNode } from "react"; import React, { ChangeEvent, CSSProperties, ReactNode } from "react";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { MarkedExecution } from "../../../utils/MarkedExecution"; import { MarkedExecution } from "../../../utils/MarkedExecution";

View file

@ -54,9 +54,13 @@ export default class Waveform extends React.PureComponent<IProps, IState> {
'mx_Waveform_bar': true, 'mx_Waveform_bar': true,
'mx_Waveform_bar_100pct': isCompleteBar, 'mx_Waveform_bar_100pct': isCompleteBar,
}); });
return <span key={i} style={{ return <span
key={i}
style={{
"--barHeight": h, "--barHeight": h,
} as WaveformCSSProperties} className={classes} />; } as WaveformCSSProperties}
className={classes}
/>;
}) } }) }
</div>; </div>;
} }

View file

@ -416,8 +416,10 @@ export class TermsAuthEntry extends React.Component<ITermsAuthEntryProps, ITerms
let submitButton; let submitButton;
if (this.props.showContinue !== false) { if (this.props.showContinue !== false) {
// XXX: button classes // XXX: button classes
submitButton = <button className="mx_InteractiveAuthEntryComponents_termsSubmit mx_GeneralButton" submitButton = <button
onClick={this.trySubmit} disabled={!allChecked}>{ _t("Accept") }</button>; className="mx_InteractiveAuthEntryComponents_termsSubmit mx_GeneralButton"
onClick={this.trySubmit}
disabled={!allChecked}>{ _t("Accept") }</button>;
} }
return ( return (
@ -616,7 +618,9 @@ export class MsisdnAuthEntry extends React.Component<IMsisdnAuthEntryProps, IMsi
aria-label={_t("Code")} aria-label={_t("Code")}
/> />
<br /> <br />
<input type="submit" value={_t("Submit")} <input
type="submit"
value={_t("Submit")}
className={submitClasses} className={submitClasses}
disabled={!enableSubmit} disabled={!enableSubmit}
/> />

View file

@ -187,7 +187,8 @@ const BaseAvatar = (props: IProps) => {
width: toPx(width), width: toPx(width),
height: toPx(height), height: toPx(height),
}} }}
title={title} alt={_t("Avatar")} title={title}
alt={_t("Avatar")}
inputRef={inputRef} inputRef={inputRef}
{...otherProps} /> {...otherProps} />
); );
@ -201,7 +202,8 @@ const BaseAvatar = (props: IProps) => {
width: toPx(width), width: toPx(width),
height: toPx(height), height: toPx(height),
}} }}
title={title} alt="" title={title}
alt=""
ref={inputRef} ref={inputRef}
{...otherProps} /> {...otherProps} />
); );

View file

@ -102,8 +102,12 @@ export default class MemberAvatar extends React.Component<IProps, IState> {
} }
return ( return (
<BaseAvatar {...otherProps} name={this.state.name} title={this.state.title} <BaseAvatar {...otherProps}
idName={userId} url={this.state.imageUrl} onClick={onClick} /> name={this.state.name}
title={this.state.title}
idName={userId}
url={this.state.imageUrl}
onClick={onClick} />
); );
} }
} }

View file

@ -13,9 +13,11 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { ComponentProps } from 'react'; import React, { ComponentProps } from 'react';
import { Room } from 'matrix-js-sdk/src/models/room'; import { Room } from 'matrix-js-sdk/src/models/room';
import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials'; import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
import classNames from "classnames";
import BaseAvatar from './BaseAvatar'; import BaseAvatar from './BaseAvatar';
import ImageView from '../elements/ImageView'; import ImageView from '../elements/ImageView';
@ -32,11 +34,14 @@ interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idNam
// oobData.avatarUrl should be set (else there // oobData.avatarUrl should be set (else there
// would be nowhere to get the avatar from) // would be nowhere to get the avatar from)
room?: Room; room?: Room;
oobData?: IOOBData; oobData?: IOOBData & {
roomId?: string;
};
width?: number; width?: number;
height?: number; height?: number;
resizeMethod?: ResizeMethod; resizeMethod?: ResizeMethod;
viewAvatarOnClick?: boolean; viewAvatarOnClick?: boolean;
className?: string;
onClick?(): void; onClick?(): void;
} }
@ -129,15 +134,19 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
}; };
public render() { public render() {
const { room, oobData, viewAvatarOnClick, onClick, ...otherProps } = this.props; const { room, oobData, viewAvatarOnClick, onClick, className, ...otherProps } = this.props;
const roomName = room ? room.name : oobData.name; const roomName = room ? room.name : oobData.name;
// If the room is a DM, we use the other user's ID for the color hash // If the room is a DM, we use the other user's ID for the color hash
// in order to match the room avatar with their avatar // in order to match the room avatar with their avatar
const idName = room ? (DMRoomMap.shared().getUserIdForRoomId(room.roomId) ?? room.roomId) : null; const idName = room ? (DMRoomMap.shared().getUserIdForRoomId(room.roomId) ?? room.roomId) : oobData.roomId;
return ( return (
<BaseAvatar {...otherProps} <BaseAvatar
{...otherProps}
className={classNames(className, {
mx_RoomAvatar_isSpaceRoom: room?.isSpaceRoom(),
})}
name={roomName} name={roomName}
idName={idName} idName={idName}
urls={this.state.urls} urls={this.state.urls}

View file

@ -60,8 +60,10 @@ export default class DialpadContextMenu extends React.Component<IProps, IState>
<AccessibleButton className="mx_DialPadContextMenu_cancel" onClick={this.onCancelClick} /> <AccessibleButton className="mx_DialPadContextMenu_cancel" onClick={this.onCancelClick} />
</div> </div>
<div className="mx_DialPadContextMenu_header"> <div className="mx_DialPadContextMenu_header">
<Field className="mx_DialPadContextMenu_dialled" <Field
value={this.state.value} autoFocus={true} className="mx_DialPadContextMenu_dialled"
value={this.state.value}
autoFocus={true}
onChange={this.onChange} onChange={this.onChange}
/> />
</div> </div>

View file

@ -109,8 +109,10 @@ export default class StatusMessageContextMenu extends React.Component {
</AccessibleButton>; </AccessibleButton>;
} }
} else { } else {
actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_submit" actionButton = <AccessibleButton
disabled={!this.state.message} onClick={this._onSubmit} className="mx_StatusMessageContextMenu_submit"
disabled={!this.state.message}
onClick={this._onSubmit}
> >
<span>{ _t("Set status") }</span> <span>{ _t("Set status") }</span>
</AccessibleButton>; </AccessibleButton>;
@ -121,12 +123,19 @@ export default class StatusMessageContextMenu extends React.Component {
spinner = <Spinner w="24" h="24" />; spinner = <Spinner w="24" h="24" />;
} }
const form = <form className="mx_StatusMessageContextMenu_form" const form = <form
autoComplete="off" onSubmit={this._onSubmit} className="mx_StatusMessageContextMenu_form"
autoComplete="off"
onSubmit={this._onSubmit}
> >
<input type="text" className="mx_StatusMessageContextMenu_message" <input
key="message" placeholder={_t("Set a new status...")} type="text"
autoFocus={true} maxLength="60" value={this.state.message} className="mx_StatusMessageContextMenu_message"
key="message"
placeholder={_t("Set a new status...")}
autoFocus={true}
maxLength="60"
value={this.state.message}
onChange={this._onStatusChange} onChange={this._onStatusChange}
/> />
<div className="mx_StatusMessageContextMenu_actionContainer"> <div className="mx_StatusMessageContextMenu_actionContainer">

View file

@ -76,7 +76,8 @@ const WidgetContextMenu: React.FC<IProps> = ({
onFinished(); onFinished();
}; };
streamAudioStreamButton = <IconizedContextMenuOption streamAudioStreamButton = <IconizedContextMenuOption
onClick={onStreamAudioClick} label={_t("Start audio stream")} onClick={onStreamAudioClick}
label={_t("Start audio stream")}
/>; />;
} }

View file

@ -17,7 +17,6 @@ limitations under the License.
import React, { ReactNode, useContext, useMemo, useState } from "react"; import React, { ReactNode, useContext, useMemo, useState } from "react";
import classNames from "classnames"; import classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { sleep } from "matrix-js-sdk/src/utils"; import { sleep } from "matrix-js-sdk/src/utils";
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
@ -44,9 +43,8 @@ import EntityTile from "../rooms/EntityTile";
import BaseAvatar from "../avatars/BaseAvatar"; import BaseAvatar from "../avatars/BaseAvatar";
interface IProps extends IDialogProps { interface IProps extends IDialogProps {
matrixClient: MatrixClient;
space: Room; space: Room;
onCreateRoomClick(cli: MatrixClient, space: Room): void; onCreateRoomClick(space: Room): void;
} }
const Entry = ({ room, checked, onChange }) => { const Entry = ({ room, checked, onChange }) => {
@ -211,10 +209,16 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
function overflowTile(overflowCount, totalCount) { function overflowTile(overflowCount, totalCount) {
const text = _t("and %(count)s others...", { count: overflowCount }); const text = _t("and %(count)s others...", { count: overflowCount });
return ( return (
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={ <EntityTile
className="mx_EntityTile_ellipsis"
avatarJsx={
<BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} /> <BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} />
} name={text} presenceState="online" suppressOnHover={true} }
onClick={() => setTruncateAt(totalCount)} /> name={text}
presenceState="online"
suppressOnHover={true}
onClick={() => setTruncateAt(totalCount)}
/>
); );
} }
@ -295,7 +299,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
</div>; </div>;
}; };
const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => { const AddExistingToSpaceDialog: React.FC<IProps> = ({ space, onCreateRoomClick, onFinished }) => {
const [selectedSpace, setSelectedSpace] = useState(space); const [selectedSpace, setSelectedSpace] = useState(space);
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId); const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
@ -344,13 +348,13 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
onFinished={onFinished} onFinished={onFinished}
fixedWidth={false} fixedWidth={false}
> >
<MatrixClientContext.Provider value={cli}> <MatrixClientContext.Provider value={space.client}>
<AddExistingToSpace <AddExistingToSpace
space={space} space={space}
onFinished={onFinished} onFinished={onFinished}
footerPrompt={<> footerPrompt={<>
<div>{ _t("Want to add a new room instead?") }</div> <div>{ _t("Want to add a new room instead?") }</div>
<AccessibleButton onClick={() => onCreateRoomClick(cli, space)} kind="link"> <AccessibleButton onClick={() => onCreateRoomClick(space)} kind="link">
{ _t("Create a new room") } { _t("Create a new room") }
</AccessibleButton> </AccessibleButton>
</>} </>}

View file

@ -665,8 +665,8 @@ export default class AddressPickerDialog extends React.Component<IProps, IState>
onChange={this.onQueryChanged} onChange={this.onQueryChanged}
placeholder={this.getPlaceholder()} placeholder={this.getPlaceholder()}
defaultValue={this.props.value} defaultValue={this.props.value}
autoFocus={this.props.focus}> autoFocus={this.props.focus}
</textarea>, />,
); );
const filteredSuggestedList = this.getFilteredSuggestions(); const filteredSuggestedList = this.getFilteredSuggestions();
@ -727,8 +727,12 @@ export default class AddressPickerDialog extends React.Component<IProps, IState>
} }
return ( return (
<BaseDialog className="mx_AddressPickerDialog" onKeyDown={this.onKeyDown} <BaseDialog
onFinished={this.props.onFinished} title={this.props.title}> className="mx_AddressPickerDialog"
onKeyDown={this.onKeyDown}
onFinished={this.props.onFinished}
title={this.props.title}
>
{ inputLabel } { inputLabel }
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
<div className="mx_AddressPickerDialog_inputContainer">{ query }</div> <div className="mx_AddressPickerDialog_inputContainer">{ query }</div>

View file

@ -118,9 +118,7 @@ export default class BaseDialog extends React.Component {
let headerImage; let headerImage;
if (this.props.headerImage) { if (this.props.headerImage) {
headerImage = <img className="mx_Dialog_titleImage" src={this.props.headerImage} headerImage = <img className="mx_Dialog_titleImage" src={this.props.headerImage} alt="" />;
alt=""
/>;
} }
return ( return (

View file

@ -71,13 +71,16 @@ const BetaFeedbackDialog: React.FC<IProps> = ({ featureId, onFinished }) => {
&nbsp; &nbsp;
{ _t("Your platform and username will be noted to help us use your feedback as much as we can.") } { _t("Your platform and username will be noted to help us use your feedback as much as we can.") }
<AccessibleButton kind="link" onClick={() => { <AccessibleButton
kind="link"
onClick={() => {
onFinished(false); onFinished(false);
defaultDispatcher.dispatch({ defaultDispatcher.dispatch({
action: Action.ViewUserSettings, action: Action.ViewUserSettings,
initialTabId: UserTab.Labs, initialTabId: UserTab.Labs,
}); });
}}> }}
>
{ _t("To leave the beta, visit your settings.") } { _t("To leave the beta, visit your settings.") }
</AccessibleButton> </AccessibleButton>
</div> </div>

View file

@ -188,7 +188,9 @@ export default class BugReportDialog extends React.Component<IProps, IState> {
} }
return ( return (
<BaseDialog className="mx_BugReportDialog" onFinished={this.onCancel} <BaseDialog
className="mx_BugReportDialog"
onFinished={this.onCancel}
title={_t('Submit debug logs')} title={_t('Submit debug logs')}
contentId='mx_Dialog_content' contentId='mx_Dialog_content'
> >

View file

@ -205,9 +205,12 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent<
people.push(( people.push((
<AccessibleButton <AccessibleButton
onClick={this.onShowMorePeople} onClick={this.onShowMorePeople}
kind="link" key="more" kind="link"
key="more"
className="mx_CommunityPrototypeInviteDialog_morePeople" className="mx_CommunityPrototypeInviteDialog_morePeople"
>{ _t("Show more") }</AccessibleButton> >
{ _t("Show more") }
</AccessibleButton>
)); ));
} }
} }
@ -240,10 +243,13 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent<
{ peopleIntro } { peopleIntro }
{ people } { people }
<AccessibleButton <AccessibleButton
kind="primary" onClick={this.onSubmit} kind="primary"
onClick={this.onSubmit}
disabled={this.state.busy} disabled={this.state.busy}
className="mx_CommunityPrototypeInviteDialog_primaryButton" className="mx_CommunityPrototypeInviteDialog_primaryButton"
>{ buttonText }</AccessibleButton> >
{ buttonText }
</AccessibleButton>
</div> </div>
</form> </form>
</BaseDialog> </BaseDialog>

View file

@ -37,8 +37,8 @@ export default class ConfirmRedactDialog extends React.Component<IProps> {
"Note that if you delete a room name or topic change, it could undo the change.")} "Note that if you delete a room name or topic change, it could undo the change.")}
placeholder={_t("Reason (optional)")} placeholder={_t("Reason (optional)")}
focus focus
button={_t("Remove")}> button={_t("Remove")}
</TextInputDialog> />
); );
} }
} }

View file

@ -104,7 +104,9 @@ export default class ConfirmUserActionDialog extends React.Component<IProps> {
} }
return ( return (
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished} <BaseDialog
className="mx_ConfirmUserActionDialog"
onFinished={this.props.onFinished}
title={this.props.title} title={this.props.title}
contentId='mx_Dialog_content' contentId='mx_Dialog_content'
> >

View file

@ -204,8 +204,10 @@ export default class CreateCommunityPrototypeDialog extends React.PureComponent<
</div> </div>
<div className="mx_CreateCommunityPrototypeDialog_colAvatar"> <div className="mx_CreateCommunityPrototypeDialog_colAvatar">
<input <input
type="file" style={{ display: "none" }} type="file"
ref={this.avatarUploadRef} accept="image/*" style={{ display: "none" }}
ref={this.avatarUploadRef}
accept="image/*"
onChange={this.onAvatarChanged} onChange={this.onAvatarChanged}
/> />
<AccessibleButton <AccessibleButton

View file

@ -123,7 +123,9 @@ export default class CreateGroupDialog extends React.Component<IProps, IState> {
} }
return ( return (
<BaseDialog className="mx_CreateGroupDialog" onFinished={this.props.onFinished} <BaseDialog
className="mx_CreateGroupDialog"
onFinished={this.props.onFinished}
title={_t('Create Community')} title={_t('Create Community')}
> >
<form onSubmit={this.onFormSubmit}> <form onSubmit={this.onFormSubmit}>
@ -133,8 +135,11 @@ export default class CreateGroupDialog extends React.Component<IProps, IState> {
<label htmlFor="groupname">{ _t('Community Name') }</label> <label htmlFor="groupname">{ _t('Community Name') }</label>
</div> </div>
<div> <div>
<input id="groupname" className="mx_CreateGroupDialog_input" <input
autoFocus={true} size={64} id="groupname"
className="mx_CreateGroupDialog_input"
autoFocus={true}
size={64}
placeholder={_t('Example')} placeholder={_t('Example')}
onChange={this.onGroupNameChange} onChange={this.onGroupNameChange}
value={this.state.groupName} value={this.state.groupName}

View file

@ -17,6 +17,7 @@ limitations under the License.
import React, { ChangeEvent, createRef, KeyboardEvent, SyntheticEvent } from "react"; import React, { ChangeEvent, createRef, KeyboardEvent, SyntheticEvent } from "react";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { JoinRule, Preset, Visibility } from "matrix-js-sdk/src/@types/partials";
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import withValidation, { IFieldState } from '../elements/Validation'; import withValidation, { IFieldState } from '../elements/Validation';
@ -31,7 +32,8 @@ import RoomAliasField from "../elements/RoomAliasField";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import DialogButtons from "../elements/DialogButtons"; import DialogButtons from "../elements/DialogButtons";
import BaseDialog from "../dialogs/BaseDialog"; import BaseDialog from "../dialogs/BaseDialog";
import { Preset, Visibility } from "matrix-js-sdk/src/@types/partials"; import Dropdown from "../elements/Dropdown";
import SpaceStore from "../../../stores/SpaceStore";
interface IProps { interface IProps {
defaultPublic?: boolean; defaultPublic?: boolean;
@ -41,7 +43,7 @@ interface IProps {
} }
interface IState { interface IState {
isPublic: boolean; joinRule: JoinRule;
isEncrypted: boolean; isEncrypted: boolean;
name: string; name: string;
topic: string; topic: string;
@ -54,15 +56,25 @@ interface IState {
@replaceableComponent("views.dialogs.CreateRoomDialog") @replaceableComponent("views.dialogs.CreateRoomDialog")
export default class CreateRoomDialog extends React.Component<IProps, IState> { export default class CreateRoomDialog extends React.Component<IProps, IState> {
private readonly supportsRestricted: boolean;
private nameField = createRef<Field>(); private nameField = createRef<Field>();
private aliasField = createRef<RoomAliasField>(); private aliasField = createRef<RoomAliasField>();
constructor(props) { constructor(props) {
super(props); super(props);
this.supportsRestricted = this.props.parentSpace && !!SpaceStore.instance.restrictedJoinRuleSupport?.preferred;
let joinRule = JoinRule.Invite;
if (this.props.defaultPublic) {
joinRule = JoinRule.Public;
} else if (this.supportsRestricted) {
joinRule = JoinRule.Restricted;
}
const config = SdkConfig.get(); const config = SdkConfig.get();
this.state = { this.state = {
isPublic: this.props.defaultPublic || false, joinRule,
isEncrypted: privateShouldBeEncrypted(), isEncrypted: privateShouldBeEncrypted(),
name: this.props.defaultName || "", name: this.props.defaultName || "",
topic: "", topic: "",
@ -81,13 +93,18 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
const opts: IOpts = {}; const opts: IOpts = {};
const createOpts: IOpts["createOpts"] = opts.createOpts = {}; const createOpts: IOpts["createOpts"] = opts.createOpts = {};
createOpts.name = this.state.name; createOpts.name = this.state.name;
if (this.state.isPublic) {
if (this.state.joinRule === JoinRule.Public) {
createOpts.visibility = Visibility.Public; createOpts.visibility = Visibility.Public;
createOpts.preset = Preset.PublicChat; createOpts.preset = Preset.PublicChat;
opts.guestAccess = false; opts.guestAccess = false;
const { alias } = this.state; const { alias } = this.state;
createOpts.room_alias_name = alias.substr(1, alias.indexOf(":") - 1); createOpts.room_alias_name = alias.substr(1, alias.indexOf(":") - 1);
} else {
// If we cannot change encryption we pass `true` for safety, the server should automatically do this for us.
opts.encryption = this.state.canChangeEncryption ? this.state.isEncrypted : true;
} }
if (this.state.topic) { if (this.state.topic) {
createOpts.topic = this.state.topic; createOpts.topic = this.state.topic;
} }
@ -95,22 +112,13 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
createOpts.creation_content = { 'm.federate': false }; createOpts.creation_content = { 'm.federate': false };
} }
if (!this.state.isPublic) {
if (this.state.canChangeEncryption) {
opts.encryption = this.state.isEncrypted;
} else {
// the server should automatically do this for us, but for safety
// we'll demand it too.
opts.encryption = true;
}
}
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) { if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
opts.associatedWithCommunity = CommunityPrototypeStore.instance.getSelectedCommunityId(); opts.associatedWithCommunity = CommunityPrototypeStore.instance.getSelectedCommunityId();
} }
if (this.props.parentSpace) { if (this.props.parentSpace && this.state.joinRule === JoinRule.Restricted) {
opts.parentSpace = this.props.parentSpace; opts.parentSpace = this.props.parentSpace;
opts.joinRule = JoinRule.Restricted;
} }
return opts; return opts;
@ -172,8 +180,8 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
this.setState({ topic: ev.target.value }); this.setState({ topic: ev.target.value });
}; };
private onPublicChange = (isPublic: boolean) => { private onJoinRuleChange = (joinRule: JoinRule) => {
this.setState({ isPublic }); this.setState({ joinRule });
}; };
private onEncryptedChange = (isEncrypted: boolean) => { private onEncryptedChange = (isEncrypted: boolean) => {
@ -210,7 +218,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
render() { render() {
let aliasField; let aliasField;
if (this.state.isPublic) { if (this.state.joinRule === JoinRule.Public) {
const domain = MatrixClientPeg.get().getDomain(); const domain = MatrixClientPeg.get().getDomain();
aliasField = ( aliasField = (
<div className="mx_CreateRoomDialog_aliasContainer"> <div className="mx_CreateRoomDialog_aliasContainer">
@ -224,19 +232,46 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
); );
} }
let publicPrivateLabel = <p>{ _t( let publicPrivateLabel: JSX.Element;
"Private rooms can be found and joined by invitation only. Public rooms can be " +
"found and joined by anyone.",
) }</p>;
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) { if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
publicPrivateLabel = <p>{ _t( publicPrivateLabel = <p>
{ _t(
"Private rooms can be found and joined by invitation only. Public rooms can be " + "Private rooms can be found and joined by invitation only. Public rooms can be " +
"found and joined by anyone in this community.", "found and joined by anyone in this community.",
) }</p>; ) }
</p>;
} else if (this.state.joinRule === JoinRule.Restricted) {
publicPrivateLabel = <p>
{ _t(
"Everyone in <SpaceName/> will be able to find and join this room.", {}, {
SpaceName: () => <b>{ this.props.parentSpace.name }</b>,
},
) }
&nbsp;
{ _t("You can change this at any time from room settings.") }
</p>;
} else if (this.state.joinRule === JoinRule.Public) {
publicPrivateLabel = <p>
{ _t(
"Anyone will be able to find and join this room, not just members of <SpaceName/>.", {}, {
SpaceName: () => <b>{ this.props.parentSpace.name }</b>,
},
) }
&nbsp;
{ _t("You can change this at any time from room settings.") }
</p>;
} else if (this.state.joinRule === JoinRule.Invite) {
publicPrivateLabel = <p>
{ _t(
"Only people invited will be able to find and join this room.",
) }
&nbsp;
{ _t("You can change this at any time from room settings.") }
</p>;
} }
let e2eeSection; let e2eeSection;
if (!this.state.isPublic) { if (this.state.joinRule !== JoinRule.Public) {
let microcopy; let microcopy;
if (privateShouldBeEncrypted()) { if (privateShouldBeEncrypted()) {
if (this.state.canChangeEncryption) { if (this.state.canChangeEncryption) {
@ -273,15 +308,31 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
); );
} }
let title = this.state.isPublic ? _t('Create a public room') : _t('Create a private room'); let title = _t("Create a room");
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) { if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
const name = CommunityPrototypeStore.instance.getSelectedCommunityName(); const name = CommunityPrototypeStore.instance.getSelectedCommunityName();
title = _t("Create a room in %(communityName)s", { communityName: name }); title = _t("Create a room in %(communityName)s", { communityName: name });
} else if (!this.props.parentSpace) {
title = this.state.joinRule === JoinRule.Public ? _t('Create a public room') : _t('Create a private room');
} }
const options = [
<div key={JoinRule.Invite} className="mx_CreateRoomDialog_dropdown_invite">
{ _t("Private room (invite only)") }
</div>,
<div key={JoinRule.Public} className="mx_CreateRoomDialog_dropdown_public">
{ _t("Public room") }
</div>,
];
if (this.supportsRestricted) {
options.unshift(<div key={JoinRule.Restricted} className="mx_CreateRoomDialog_dropdown_restricted">
{ _t("Visible to space members") }
</div>);
}
return ( return (
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished} <BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished} title={title}>
title={title}
>
<form onSubmit={this.onOk} onKeyDown={this.onKeyDown}> <form onSubmit={this.onOk} onKeyDown={this.onKeyDown}>
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
<Field <Field
@ -298,11 +349,18 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
value={this.state.topic} value={this.state.topic}
className="mx_CreateRoomDialog_topic" className="mx_CreateRoomDialog_topic"
/> />
<LabelledToggleSwitch
label={_t("Make this room public")} <Dropdown
onChange={this.onPublicChange} id="mx_CreateRoomDialog_typeDropdown"
value={this.state.isPublic} className="mx_CreateRoomDialog_typeDropdown"
/> onOptionChange={this.onJoinRuleChange}
menuWidth={448}
value={this.state.joinRule}
label={_t("Room visibility")}
>
{ options }
</Dropdown>
{ publicPrivateLabel } { publicPrivateLabel }
{ e2eeSection } { e2eeSection }
{ aliasField } { aliasField }

View file

@ -182,14 +182,23 @@ export class SendCustomEvent extends GenericEditor<ISendCustomEventProps, ISendC
<br /> <br />
<Field id="evContent" label={_t("Event Content")} type="text" className="mx_DevTools_textarea" <Field
autoComplete="off" value={this.state.evContent} onChange={this.onChange} element="textarea" /> id="evContent"
label={_t("Event Content")}
type="text"
className="mx_DevTools_textarea"
autoComplete="off"
value={this.state.evContent}
onChange={this.onChange}
element="textarea" />
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button onClick={this.onBack}>{ _t('Back') }</button> <button onClick={this.onBack}>{ _t('Back') }</button>
{ !this.state.message && <button onClick={this.send}>{ _t('Send') }</button> } { !this.state.message && <button onClick={this.send}>{ _t('Send') }</button> }
{ showTglFlip && <div style={{ float: "right" }}> { showTglFlip && <div style={{ float: "right" }}>
<input id="isStateEvent" className="mx_DevTools_tgl mx_DevTools_tgl-flip" <input
id="isStateEvent"
className="mx_DevTools_tgl mx_DevTools_tgl-flip"
type="checkbox" type="checkbox"
checked={this.state.isStateEvent} checked={this.state.isStateEvent}
onChange={this.onChange} onChange={this.onChange}
@ -282,14 +291,24 @@ class SendAccountData extends GenericEditor<ISendAccountDataProps, ISendAccountD
{ this.textInput('eventType', _t('Event Type')) } { this.textInput('eventType', _t('Event Type')) }
<br /> <br />
<Field id="evContent" label={_t("Event Content")} type="text" className="mx_DevTools_textarea" <Field
autoComplete="off" value={this.state.evContent} onChange={this.onChange} element="textarea" /> id="evContent"
label={_t("Event Content")}
type="text"
className="mx_DevTools_textarea"
autoComplete="off"
value={this.state.evContent}
onChange={this.onChange}
element="textarea"
/>
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button onClick={this.onBack}>{ _t('Back') }</button> <button onClick={this.onBack}>{ _t('Back') }</button>
{ !this.state.message && <button onClick={this.send}>{ _t('Send') }</button> } { !this.state.message && <button onClick={this.send}>{ _t('Send') }</button> }
{ !this.state.message && <div style={{ float: "right" }}> { !this.state.message && <div style={{ float: "right" }}>
<input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip" <input
id="isRoomAccountData"
className="mx_DevTools_tgl mx_DevTools_tgl-flip"
type="checkbox" type="checkbox"
checked={this.state.isRoomAccountData} checked={this.state.isRoomAccountData}
disabled={this.props.forceMode} disabled={this.props.forceMode}
@ -371,11 +390,18 @@ class FilteredList extends React.PureComponent<IFilteredListProps, IFilteredList
render() { render() {
return <div> return <div>
<Field label={_t('Filter results')} autoFocus={true} size={64} <Field
type="text" autoComplete="off" value={this.props.query} onChange={this.onQuery} label={_t('Filter results')}
autoFocus={true}
size={64}
type="text"
autoComplete="off"
value={this.props.query}
onChange={this.onQuery}
className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query" className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query"
// force re-render so that autoFocus is applied when this component is re-used // force re-render so that autoFocus is applied when this component is re-used
key={this.props.children[0] ? this.props.children[0].key : ''} /> key={this.props.children[0] ? this.props.children[0].key : ''}
/>
<TruncatedList getChildren={this.getChildren} <TruncatedList getChildren={this.getChildren}
getChildCount={this.getChildCount} getChildCount={this.getChildCount}
@ -459,11 +485,16 @@ class RoomStateExplorer extends React.PureComponent<IExplorerProps, IRoomStateEx
render() { render() {
if (this.state.event) { if (this.state.event) {
if (this.state.editing) { if (this.state.editing) {
return <SendCustomEvent room={this.props.room} forceStateEvent={true} onBack={this.onBack} inputs={{ return <SendCustomEvent
room={this.props.room}
forceStateEvent={true}
onBack={this.onBack}
inputs={{
eventType: this.state.event.getType(), eventType: this.state.event.getType(),
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'), evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
stateKey: this.state.event.getStateKey(), stateKey: this.state.event.getStateKey(),
}} />; }}
/>;
} }
return <div className="mx_ViewSource"> return <div className="mx_ViewSource">
@ -594,7 +625,9 @@ class AccountDataExplorer extends React.PureComponent<IExplorerProps, IAccountDa
inputs={{ inputs={{
eventType: this.state.event.getType(), eventType: this.state.event.getType(),
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'), evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
}} forceMode={true} />; }}
forceMode={true}
/>;
} }
return <div className="mx_ViewSource"> return <div className="mx_ViewSource">
@ -631,7 +664,9 @@ class AccountDataExplorer extends React.PureComponent<IExplorerProps, IAccountDa
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button onClick={this.onBack}>{ _t('Back') }</button> <button onClick={this.onBack}>{ _t('Back') }</button>
<div style={{ float: "right" }}> <div style={{ float: "right" }}>
<input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip" <input
id="isRoomAccountData"
className="mx_DevTools_tgl mx_DevTools_tgl-flip"
type="checkbox" type="checkbox"
checked={this.state.isRoomAccountData} checked={this.state.isRoomAccountData}
onChange={this.onChange} onChange={this.onChange}
@ -1021,8 +1056,13 @@ class SettingsExplorer extends React.PureComponent<IExplorerProps, ISettingsExpl
<div> <div>
<div className="mx_Dialog_content mx_DevTools_SettingsExplorer"> <div className="mx_Dialog_content mx_DevTools_SettingsExplorer">
<Field <Field
label={_t('Filter results')} autoFocus={true} size={64} label={_t('Filter results')}
type="text" autoComplete="off" value={this.state.query} onChange={this.onQueryChange} autoFocus={true}
size={64}
type="text"
autoComplete="off"
value={this.state.query}
onChange={this.onQueryChange}
className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query" className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query"
/> />
<table> <table>
@ -1040,7 +1080,9 @@ class SettingsExplorer extends React.PureComponent<IExplorerProps, ISettingsExpl
<a href="" onClick={(e) => this.onViewClick(e, i)}> <a href="" onClick={(e) => this.onViewClick(e, i)}>
<code>{ i }</code> <code>{ i }</code>
</a> </a>
<a href="" onClick={(e) => this.onEditClick(e, i)} <a
href=""
onClick={(e) => this.onEditClick(e, i)}
className='mx_DevTools_SettingsExplorer_edit' className='mx_DevTools_SettingsExplorer_edit'
> >
@ -1104,18 +1146,26 @@ class SettingsExplorer extends React.PureComponent<IExplorerProps, ISettingsExpl
<div> <div>
<Field <Field
id="valExpl" label={_t("Values at explicit levels")} type="text" id="valExpl"
className="mx_DevTools_textarea" element="textarea" label={_t("Values at explicit levels")}
autoComplete="off" value={this.state.explicitValues} type="text"
className="mx_DevTools_textarea"
element="textarea"
autoComplete="off"
value={this.state.explicitValues}
onChange={this.onExplValuesEdit} onChange={this.onExplValuesEdit}
/> />
</div> </div>
<div> <div>
<Field <Field
id="valExpl" label={_t("Values at explicit levels in this room")} type="text" id="valExpl"
className="mx_DevTools_textarea" element="textarea" label={_t("Values at explicit levels in this room")}
autoComplete="off" value={this.state.explicitRoomValues} type="text"
className="mx_DevTools_textarea"
element="textarea"
autoComplete="off"
value={this.state.explicitRoomValues}
onChange={this.onExplRoomValuesEdit} onChange={this.onExplRoomValuesEdit}
/> />
</div> </div>

View file

@ -144,8 +144,10 @@ export default class EditCommunityPrototypeDialog extends React.PureComponent<IP
</div> </div>
<div className="mx_EditCommunityPrototypeDialog_rowAvatar"> <div className="mx_EditCommunityPrototypeDialog_rowAvatar">
<input <input
type="file" style={{ display: "none" }} type="file"
ref={this.avatarUploadRef} accept="image/*" style={{ display: "none" }}
ref={this.avatarUploadRef}
accept="image/*"
onChange={this.onAvatarChanged} onChange={this.onAvatarChanged}
/> />
<AccessibleButton <AccessibleButton

View file

@ -106,12 +106,12 @@ const Entry: React.FC<IEntryProps> = ({ room, event, matrixClient: cli, onFinish
className = "mx_ForwardList_sending"; className = "mx_ForwardList_sending";
disabled = true; disabled = true;
title = _t("Sending"); title = _t("Sending");
icon = <div className="mx_ForwardList_sendIcon" aria-label={title}></div>; icon = <div className="mx_ForwardList_sendIcon" aria-label={title} />;
} else if (sendState === SendState.Sent) { } else if (sendState === SendState.Sent) {
className = "mx_ForwardList_sent"; className = "mx_ForwardList_sent";
disabled = true; disabled = true;
title = _t("Sent"); title = _t("Sent");
icon = <div className="mx_ForwardList_sendIcon" aria-label={title}></div>; icon = <div className="mx_ForwardList_sendIcon" aria-label={title} />;
} else { } else {
className = "mx_ForwardList_sendFailed"; className = "mx_ForwardList_sendFailed";
disabled = true; disabled = true;
@ -204,10 +204,16 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
function overflowTile(overflowCount, totalCount) { function overflowTile(overflowCount, totalCount) {
const text = _t("and %(count)s others...", { count: overflowCount }); const text = _t("and %(count)s others...", { count: overflowCount });
return ( return (
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={ <EntityTile
className="mx_EntityTile_ellipsis"
avatarJsx={
<BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} /> <BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} />
} name={text} presenceState="online" suppressOnHover={true} }
onClick={() => setTruncateAt(totalCount)} /> name={text}
presenceState="online"
suppressOnHover={true}
onClick={() => setTruncateAt(totalCount)}
/>
); );
} }

View file

@ -133,18 +133,23 @@ export default class IncomingSasDialog extends React.Component {
? mediaFromMxc(oppProfile.avatar_url).getSquareThumbnailHttp(48) ? mediaFromMxc(oppProfile.avatar_url).getSquareThumbnailHttp(48)
: null; : null;
profile = <div className="mx_IncomingSasDialog_opponentProfile"> profile = <div className="mx_IncomingSasDialog_opponentProfile">
<BaseAvatar name={oppProfile.displayname} <BaseAvatar
name={oppProfile.displayname}
idName={this.props.verifier.userId} idName={this.props.verifier.userId}
url={url} url={url}
width={48} height={48} resizeMethod='crop' width={48}
height={48}
resizeMethod='crop'
/> />
<h2>{ oppProfile.displayname }</h2> <h2>{ oppProfile.displayname }</h2>
</div>; </div>;
} else if (this.state.opponentProfileError) { } else if (this.state.opponentProfileError) {
profile = <div> profile = <div>
<BaseAvatar name={this.props.verifier.userId.slice(1)} <BaseAvatar
name={this.props.verifier.userId.slice(1)}
idName={this.props.verifier.userId} idName={this.props.verifier.userId}
width={48} height={48} width={48}
height={48}
/> />
<h2>{ this.props.verifier.userId }</h2> <h2>{ this.props.verifier.userId }</h2>
</div>; </div>;

View file

@ -1,7 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd.
Copyright 2019 Bastian Masanek, Noxware IT <matrix@noxware.de> Copyright 2019 Bastian Masanek, Noxware IT <matrix@noxware.de>
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -16,31 +15,31 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React, { ReactNode, KeyboardEvent } from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import classNames from "classnames"; import classNames from "classnames";
export default class InfoDialog extends React.Component { import { _t } from '../../../languageHandler';
static propTypes = { import * as sdk from '../../../index';
className: PropTypes.string, import { IDialogProps } from "./IDialogProps";
title: PropTypes.string,
description: PropTypes.node,
button: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
onFinished: PropTypes.func,
hasCloseButton: PropTypes.bool,
onKeyDown: PropTypes.func,
fixedWidth: PropTypes.bool,
};
interface IProps extends IDialogProps {
title?: string;
description?: ReactNode;
className?: string;
button?: boolean | string;
hasCloseButton?: boolean;
fixedWidth?: boolean;
onKeyDown?(event: KeyboardEvent): void;
}
export default class InfoDialog extends React.Component<IProps> {
static defaultProps = { static defaultProps = {
title: '', title: '',
description: '', description: '',
hasCloseButton: false, hasCloseButton: false,
}; };
onFinished = () => { private onFinished = () => {
this.props.onFinished(); this.props.onFinished();
}; };
@ -63,8 +62,7 @@ export default class InfoDialog extends React.Component {
{ this.props.button !== false && <DialogButtons primaryButton={this.props.button || _t('OK')} { this.props.button !== false && <DialogButtons primaryButton={this.props.button || _t('OK')}
onPrimaryButtonClick={this.onFinished} onPrimaryButtonClick={this.onFinished}
hasCancel={false} hasCancel={false}
> /> }
</DialogButtons> }
</BaseDialog> </BaseDialog>
); );
} }

View file

@ -196,7 +196,9 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
? <img ? <img
className='mx_InviteDialog_userTile_avatar mx_InviteDialog_userTile_threepidAvatar' className='mx_InviteDialog_userTile_avatar mx_InviteDialog_userTile_threepidAvatar'
src={require("../../../../res/img/icon-email-pill-avatar.svg")} src={require("../../../../res/img/icon-email-pill-avatar.svg")}
width={avatarSize} height={avatarSize} /> width={avatarSize}
height={avatarSize}
/>
: <BaseAvatar : <BaseAvatar
className='mx_InviteDialog_userTile_avatar' className='mx_InviteDialog_userTile_avatar'
url={this.props.member.getMxcAvatarUrl() url={this.props.member.getMxcAvatarUrl()
@ -214,8 +216,11 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
className='mx_InviteDialog_userTile_remove' className='mx_InviteDialog_userTile_remove'
onClick={this.onRemove} onClick={this.onRemove}
> >
<img src={require("../../../../res/img/icon-pill-remove.svg")} <img
alt={_t('Remove')} width={8} height={8} src={require("../../../../res/img/icon-pill-remove.svg")}
alt={_t('Remove')}
width={8}
height={8}
/> />
</AccessibleButton> </AccessibleButton>
); );
@ -297,7 +302,9 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
const avatar = (this.props.member as ThreepidMember).isEmail const avatar = (this.props.member as ThreepidMember).isEmail
? <img ? <img
src={require("../../../../res/img/icon-email-pill-avatar.svg")} src={require("../../../../res/img/icon-email-pill-avatar.svg")}
width={avatarSize} height={avatarSize} /> width={avatarSize}
height={avatarSize}
/>
: <BaseAvatar : <BaseAvatar
url={this.props.member.getMxcAvatarUrl() url={this.props.member.getMxcAvatarUrl()
? mediaFromMxc(this.props.member.getMxcAvatarUrl()).getSquareThumbnailHttp(avatarSize) ? mediaFromMxc(this.props.member.getMxcAvatarUrl()).getSquareThumbnailHttp(avatarSize)
@ -1458,7 +1465,8 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
<p className='mx_InviteDialog_helpText'> <p className='mx_InviteDialog_helpText'>
<img <img
src={require("../../../../res/img/element-icons/info.svg")} src={require("../../../../res/img/element-icons/info.svg")}
width={14} height={14} /> width={14}
height={14} />
{ " " + _t("Invited people will be able to read old messages.") } { " " + _t("Invited people will be able to read old messages.") }
</p>; </p>;
} }
@ -1534,14 +1542,18 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
// Only show the backspace button if the field has content // Only show the backspace button if the field has content
let dialPadField; let dialPadField;
if (this.state.dialPadValue.length !== 0) { if (this.state.dialPadValue.length !== 0) {
dialPadField = <Field className="mx_InviteDialog_dialPadField" id="dialpad_number" dialPadField = <Field
className="mx_InviteDialog_dialPadField"
id="dialpad_number"
value={this.state.dialPadValue} value={this.state.dialPadValue}
autoFocus={true} autoFocus={true}
onChange={this.onDialChange} onChange={this.onDialChange}
postfixComponent={backspaceButton} postfixComponent={backspaceButton}
/>; />;
} else { } else {
dialPadField = <Field className="mx_InviteDialog_dialPadField" id="dialpad_number" dialPadField = <Field
className="mx_InviteDialog_dialPadField"
id="dialpad_number"
value={this.state.dialPadValue} value={this.state.dialPadValue}
autoFocus={true} autoFocus={true}
onChange={this.onDialChange} onChange={this.onDialChange}
@ -1552,14 +1564,19 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
<form onSubmit={this.onDialFormSubmit}> <form onSubmit={this.onDialFormSubmit}>
{ dialPadField } { dialPadField }
</form> </form>
<Dialpad hasDial={false} <Dialpad
onDigitPress={this.onDigitPress} onDeletePress={this.onDeletePress} hasDial={false}
onDigitPress={this.onDigitPress}
onDeletePress={this.onDeletePress}
/> />
</div>; </div>;
tabs.push(new Tab(TabId.DialPad, _td("Dial pad"), 'mx_InviteDialog_dialPadIcon', dialPadSection)); tabs.push(new Tab(TabId.DialPad, _td("Dial pad"), 'mx_InviteDialog_dialPadIcon', dialPadSection));
dialogContent = <React.Fragment> dialogContent = <React.Fragment>
<TabbedView tabs={tabs} initialTabId={this.state.currentTabId} <TabbedView
tabLocation={TabLocation.TOP} onChange={this.onTabChange} tabs={tabs}
initialTabId={this.state.currentTabId}
tabLocation={TabLocation.TOP}
onChange={this.onTabChange}
/> />
{ consultConnectSection } { consultConnectSection }
</React.Fragment>; </React.Fragment>;

View file

@ -0,0 +1,192 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useMemo, useState } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { _t } from '../../../languageHandler';
import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog";
import SearchBox from "../../structures/SearchBox";
import SpaceStore from "../../../stores/SpaceStore";
import RoomAvatar from "../avatars/RoomAvatar";
import AccessibleButton from "../elements/AccessibleButton";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import StyledCheckbox from "../elements/StyledCheckbox";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
interface IProps extends IDialogProps {
room: Room;
selected?: string[];
}
const Entry = ({ room, checked, onChange }) => {
const localRoom = room instanceof Room;
let description;
if (localRoom) {
description = _t("%(count)s members", { count: room.getJoinedMemberCount() });
const numChildRooms = SpaceStore.instance.getChildRooms(room.roomId).length;
if (numChildRooms > 0) {
description += " · " + _t("%(count)s rooms", { count: numChildRooms });
}
}
return <label className="mx_ManageRestrictedJoinRuleDialog_entry">
<div>
<div>
{ localRoom
? <RoomAvatar room={room} height={20} width={20} />
: <RoomAvatar oobData={room} height={20} width={20} />
}
<span className="mx_ManageRestrictedJoinRuleDialog_entry_name">{ room.name }</span>
</div>
{ description && <div className="mx_ManageRestrictedJoinRuleDialog_entry_description">
{ description }
</div> }
</div>
<StyledCheckbox
onChange={onChange ? (e) => onChange(e.target.checked) : null}
checked={checked}
disabled={!onChange}
/>
</label>;
};
const ManageRestrictedJoinRuleDialog: React.FC<IProps> = ({ room, selected = [], onFinished }) => {
const cli = room.client;
const [newSelected, setNewSelected] = useState(new Set<string>(selected));
const [query, setQuery] = useState("");
const lcQuery = query.toLowerCase().trim();
const [spacesContainingRoom, otherEntries] = useMemo(() => {
const spaces = cli.getVisibleRooms().filter(r => r.getMyMembership() === "join" && r.isSpaceRoom());
return [
spaces.filter(r => SpaceStore.instance.getSpaceFilteredRoomIds(r).has(room.roomId)),
selected.map(roomId => {
const room = cli.getRoom(roomId);
if (!room) {
return { roomId, name: roomId } as Room;
}
if (room.getMyMembership() !== "join" || !room.isSpaceRoom()) {
return room;
}
}).filter(Boolean),
];
}, [cli, selected, room.roomId]);
const [filteredSpacesContainingRooms, filteredOtherEntries] = useMemo(() => [
spacesContainingRoom.filter(r => r.name.toLowerCase().includes(lcQuery)),
otherEntries.filter(r => r.name.toLowerCase().includes(lcQuery)),
], [spacesContainingRoom, otherEntries, lcQuery]);
const onChange = (checked: boolean, room: Room): void => {
if (checked) {
newSelected.add(room.roomId);
} else {
newSelected.delete(room.roomId);
}
setNewSelected(new Set(newSelected));
};
let inviteOnlyWarning;
if (newSelected.size < 1) {
inviteOnlyWarning = <div className="mx_ManageRestrictedJoinRuleDialog_section_info">
{ _t("You're removing all spaces. Access will default to invite only") }
</div>;
}
return <BaseDialog
title={_t("Select spaces")}
className="mx_ManageRestrictedJoinRuleDialog"
onFinished={onFinished}
fixedWidth={false}
>
<p>
{ _t("Decide which spaces can access this room. " +
"If a space is selected, its members can find and join <RoomName/>.", {}, {
RoomName: () => <b>{ room.name }</b>,
}) }
</p>
<MatrixClientContext.Provider value={cli}>
<SearchBox
className="mx_textinput_icon mx_textinput_search"
placeholder={_t("Search spaces")}
onSearch={setQuery}
autoComplete={true}
autoFocus={true}
/>
<AutoHideScrollbar className="mx_ManageRestrictedJoinRuleDialog_content">
{ filteredSpacesContainingRooms.length > 0 ? (
<div className="mx_ManageRestrictedJoinRuleDialog_section">
<h3>{ _t("Spaces you know that contain this room") }</h3>
{ filteredSpacesContainingRooms.map(space => {
return <Entry
key={space.roomId}
room={space}
checked={newSelected.has(space.roomId)}
onChange={(checked: boolean) => {
onChange(checked, space);
}}
/>;
}) }
</div>
) : undefined }
{ filteredOtherEntries.length > 0 ? (
<div className="mx_ManageRestrictedJoinRuleDialog_section">
<h3>{ _t("Other spaces or rooms you might not know") }</h3>
<div className="mx_ManageRestrictedJoinRuleDialog_section_info">
<div>{ _t("These are likely ones other room admins are a part of.") }</div>
</div>
{ filteredOtherEntries.map(space => {
return <Entry
key={space.roomId}
room={space}
checked={newSelected.has(space.roomId)}
onChange={(checked: boolean) => {
onChange(checked, space);
}}
/>;
}) }
</div>
) : null }
{ filteredSpacesContainingRooms.length + filteredOtherEntries.length < 1
? <span className="mx_ManageRestrictedJoinRuleDialog_noResults">
{ _t("No results") }
</span>
: undefined
}
</AutoHideScrollbar>
<div className="mx_ManageRestrictedJoinRuleDialog_footer">
{ inviteOnlyWarning }
<div className="mx_ManageRestrictedJoinRuleDialog_footer_buttons">
<AccessibleButton kind="primary_outline" onClick={() => onFinished()}>
{ _t("Cancel") }
</AccessibleButton>
<AccessibleButton kind="primary" onClick={() => onFinished(Array.from(newSelected))}>
{ _t("Confirm") }
</AccessibleButton>
</div>
</div>
</MatrixClientContext.Provider>
</BaseDialog>;
};
export default ManageRestrictedJoinRuleDialog;

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2018 New Vector Ltd Copyright 2018 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,19 +15,29 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import { Room } from "matrix-js-sdk/src/models/room";
import * as sdk from '../../../index';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { upgradeRoom } from "../../../utils/RoomUpgrade";
import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog";
import ErrorDialog from './ErrorDialog';
import DialogButtons from '../elements/DialogButtons';
import Spinner from "../elements/Spinner";
interface IProps extends IDialogProps {
room: Room;
}
interface IState {
busy: boolean;
}
@replaceableComponent("views.dialogs.RoomUpgradeDialog") @replaceableComponent("views.dialogs.RoomUpgradeDialog")
export default class RoomUpgradeDialog extends React.Component { export default class RoomUpgradeDialog extends React.Component<IProps, IState> {
static propTypes = { private targetVersion: string;
room: PropTypes.object.isRequired,
onFinished: PropTypes.func.isRequired,
};
state = { state = {
busy: true, busy: true,
@ -35,20 +45,19 @@ export default class RoomUpgradeDialog extends React.Component {
async componentDidMount() { async componentDidMount() {
const recommended = await this.props.room.getRecommendedVersion(); const recommended = await this.props.room.getRecommendedVersion();
this._targetVersion = recommended.version; this.targetVersion = recommended.version;
this.setState({ busy: false }); this.setState({ busy: false });
} }
_onCancelClick = () => { private onCancelClick = (): void => {
this.props.onFinished(false); this.props.onFinished(false);
}; };
_onUpgradeClick = () => { private onUpgradeClick = (): void => {
this.setState({ busy: true }); this.setState({ busy: true });
MatrixClientPeg.get().upgradeRoom(this.props.room.roomId, this._targetVersion).then(() => { upgradeRoom(this.props.room, this.targetVersion, false, false).then(() => {
this.props.onFinished(true); this.props.onFinished(true);
}).catch((err) => { }).catch((err) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to upgrade room', '', ErrorDialog, { Modal.createTrackedDialog('Failed to upgrade room', '', ErrorDialog, {
title: _t("Failed to upgrade room"), title: _t("Failed to upgrade room"),
description: ((err && err.message) ? err.message : _t("The room upgrade could not be completed")), description: ((err && err.message) ? err.message : _t("The room upgrade could not be completed")),
@ -59,29 +68,22 @@ export default class RoomUpgradeDialog extends React.Component {
}; };
render() { render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const Spinner = sdk.getComponent('views.elements.Spinner');
let buttons; let buttons;
if (this.state.busy) { if (this.state.busy) {
buttons = <Spinner />; buttons = <Spinner />;
} else { } else {
buttons = <DialogButtons buttons = <DialogButtons
primaryButton={_t( primaryButton={_t('Upgrade this room to version %(version)s', { version: this.targetVersion })}
'Upgrade this room to version %(version)s',
{ version: this._targetVersion },
)}
primaryButtonClass="danger" primaryButtonClass="danger"
hasCancel={true} hasCancel={true}
onPrimaryButtonClick={this._onUpgradeClick} onPrimaryButtonClick={this.onUpgradeClick}
focus={this.props.focus} onCancel={this.onCancelClick}
onCancel={this._onCancelClick}
/>; />;
} }
return ( return (
<BaseDialog className="mx_RoomUpgradeDialog" <BaseDialog
className="mx_RoomUpgradeDialog"
onFinished={this.props.onFinished} onFinished={this.props.onFinished}
title={_t("Upgrade Room Version")} title={_t("Upgrade Room Version")}
contentId='mx_Dialog_content' contentId='mx_Dialog_content'
@ -97,8 +99,10 @@ export default class RoomUpgradeDialog extends React.Component {
<ol> <ol>
<li>{ _t("Create a new room with the same name, description and avatar") }</li> <li>{ _t("Create a new room with the same name, description and avatar") }</li>
<li>{ _t("Update any local room aliases to point to the new room") }</li> <li>{ _t("Update any local room aliases to point to the new room") }</li>
<li>{ _t("Stop users from speaking in the old version of the room, and post a message advising users to move to the new room") }</li> <li>{ _t("Stop users from speaking in the old version of the room, " +
<li>{ _t("Put a link back to the old room at the start of the new room so people can see old messages") }</li> "and post a message advising users to move to the new room") }</li>
<li>{ _t("Put a link back to the old room at the start of the new room " +
"so people can see old messages") }</li>
</ol> </ol>
{ buttons } { buttons }
</BaseDialog> </BaseDialog>

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2019, 2020 The Matrix.org Foundation C.I.C. Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,73 +14,82 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React, { ReactNode } from 'react';
import PropTypes from 'prop-types'; import { EventType } from 'matrix-js-sdk/src/@types/event';
import { JoinRule } from 'matrix-js-sdk/src/@types/partials';
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import SdkConfig from "../../../SdkConfig"; import SdkConfig from "../../../SdkConfig";
import * as sdk from "../../../index";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { IDialogProps } from "./IDialogProps";
import BugReportDialog from './BugReportDialog';
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
interface IProps extends IDialogProps {
roomId: string;
targetVersion: string;
description?: ReactNode;
}
interface IState {
inviteUsersToNewRoom: boolean;
}
@replaceableComponent("views.dialogs.RoomUpgradeWarningDialog") @replaceableComponent("views.dialogs.RoomUpgradeWarningDialog")
export default class RoomUpgradeWarningDialog extends React.Component { export default class RoomUpgradeWarningDialog extends React.Component<IProps, IState> {
static propTypes = { private readonly isPrivate: boolean;
onFinished: PropTypes.func.isRequired, private readonly currentVersion: string;
roomId: PropTypes.string.isRequired,
targetVersion: PropTypes.string.isRequired,
};
constructor(props) { constructor(props) {
super(props); super(props);
const room = MatrixClientPeg.get().getRoom(this.props.roomId); const room = MatrixClientPeg.get().getRoom(this.props.roomId);
const joinRules = room ? room.currentState.getStateEvents("m.room.join_rules", "") : null; const joinRules = room?.currentState.getStateEvents(EventType.RoomJoinRules, "");
const isPrivate = joinRules ? joinRules.getContent()['join_rule'] !== 'public' : true; this.isPrivate = joinRules?.getContent()['join_rule'] !== JoinRule.Public ?? true;
this.currentVersion = room?.getVersion() || "1";
this.state = { this.state = {
currentVersion: room ? room.getVersion() : "1",
isPrivate,
inviteUsersToNewRoom: true, inviteUsersToNewRoom: true,
}; };
} }
_onContinue = () => { private onContinue = () => {
this.props.onFinished({ continue: true, invite: this.state.isPrivate && this.state.inviteUsersToNewRoom }); this.props.onFinished({ continue: true, invite: this.isPrivate && this.state.inviteUsersToNewRoom });
}; };
_onCancel = () => { private onCancel = () => {
this.props.onFinished({ continue: false, invite: false }); this.props.onFinished({ continue: false, invite: false });
}; };
_onInviteUsersToggle = (newVal) => { private onInviteUsersToggle = (inviteUsersToNewRoom: boolean) => {
this.setState({ inviteUsersToNewRoom: newVal }); this.setState({ inviteUsersToNewRoom });
}; };
_openBugReportDialog = (e) => { private openBugReportDialog = (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {}); Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {});
}; };
render() { render() {
const brand = SdkConfig.get().brand; const brand = SdkConfig.get().brand;
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
let inviteToggle = null; let inviteToggle = null;
if (this.state.isPrivate) { if (this.isPrivate) {
inviteToggle = ( inviteToggle = (
<LabelledToggleSwitch <LabelledToggleSwitch
value={this.state.inviteUsersToNewRoom} value={this.state.inviteUsersToNewRoom}
onChange={this._onInviteUsersToggle} onChange={this.onInviteUsersToggle}
label={_t("Automatically invite users")} /> label={_t("Automatically invite members from this room to the new one")} />
); );
} }
const title = this.state.isPrivate ? _t("Upgrade private room") : _t("Upgrade public room"); const title = this.isPrivate ? _t("Upgrade private room") : _t("Upgrade public room");
let bugReports = ( let bugReports = (
<p> <p>
@ -101,7 +110,7 @@ export default class RoomUpgradeWarningDialog extends React.Component {
}, },
{ {
"a": (sub) => { "a": (sub) => {
return <a href='#' onClick={this._openBugReportDialog}>{ sub }</a>; return <a href='#' onClick={this.openBugReportDialog}>{ sub }</a>;
}, },
}, },
) } ) }
@ -119,18 +128,26 @@ export default class RoomUpgradeWarningDialog extends React.Component {
> >
<div> <div>
<p> <p>
{ _t( { this.props.description || _t(
"Upgrading a room is an advanced action and is usually recommended when a room " + "Upgrading a room is an advanced action and is usually recommended when a room " +
"is unstable due to bugs, missing features or security vulnerabilities.", "is unstable due to bugs, missing features or security vulnerabilities.",
) } ) }
</p> </p>
<p>
{ _t(
"<b>Please note upgrading will make a new version of the room</b>. " +
"All current messages will stay in this archived room.", {}, {
b: sub => <b>{ sub }</b>,
},
) }
</p>
{ bugReports } { bugReports }
<p> <p>
{ _t( { _t(
"You'll upgrade this room from <oldVersion /> to <newVersion />.", "You'll upgrade this room from <oldVersion /> to <newVersion />.",
{}, {},
{ {
oldVersion: () => <code>{ this.state.currentVersion }</code>, oldVersion: () => <code>{ this.currentVersion }</code>,
newVersion: () => <code>{ this.props.targetVersion }</code>, newVersion: () => <code>{ this.props.targetVersion }</code>,
}, },
) } ) }
@ -139,9 +156,9 @@ export default class RoomUpgradeWarningDialog extends React.Component {
</div> </div>
<DialogButtons <DialogButtons
primaryButton={_t("Upgrade")} primaryButton={_t("Upgrade")}
onPrimaryButtonClick={this._onContinue} onPrimaryButtonClick={this.onContinue}
cancelButton={_t("Cancel")} cancelButton={_t("Cancel")}
onCancel={this._onCancel} onCancel={this.onCancel}
/> />
</BaseDialog> </BaseDialog>
); );

View file

@ -85,7 +85,9 @@ export default class SessionRestoreErrorDialog extends React.Component {
} }
return ( return (
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished} <BaseDialog
className="mx_ErrorDialog"
onFinished={this.props.onFinished}
title={_t('Unable to restore session')} title={_t('Unable to restore session')}
contentId='mx_Dialog_content' contentId='mx_Dialog_content'
hasCancel={false} hasCancel={false}

View file

@ -54,7 +54,9 @@ export default class StorageEvictedDialog extends React.Component {
} }
return ( return (
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished} <BaseDialog
className="mx_ErrorDialog"
onFinished={this.props.onFinished}
title={_t('Missing session data')} title={_t('Missing session data')}
contentId='mx_Dialog_content' contentId='mx_Dialog_content'
hasCancel={false} hasCancel={false}

View file

@ -287,7 +287,8 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
<div className="mx_AccessSecretStorageDialog_reset"> <div className="mx_AccessSecretStorageDialog_reset">
{ _t("Forgotten or lost all recovery methods? <a>Reset all</a>", null, { { _t("Forgotten or lost all recovery methods? <a>Reset all</a>", null, {
a: (sub) => <a a: (sub) => <a
href="" onClick={this.onResetAllClick} href=""
onClick={this.onResetAllClick}
className="mx_AccessSecretStorageDialog_reset_link">{ sub }</a>, className="mx_AccessSecretStorageDialog_reset_link">{ sub }</a>,
}) } }) }
</div> </div>

View file

@ -399,7 +399,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
let keyStatus; let keyStatus;
if (this.state.recoveryKey.length === 0) { if (this.state.recoveryKey.length === 0) {
keyStatus = <div className="mx_RestoreKeyBackupDialog_keyStatus"></div>; keyStatus = <div className="mx_RestoreKeyBackupDialog_keyStatus" />;
} else if (this.state.recoveryKeyValid) { } else if (this.state.recoveryKeyValid) {
keyStatus = <div className="mx_RestoreKeyBackupDialog_keyStatus"> keyStatus = <div className="mx_RestoreKeyBackupDialog_keyStatus">
{ "\uD83D\uDC4D " }{ _t("This looks like a valid Security Key!") } { "\uD83D\uDC4D " }{ _t("This looks like a valid Security Key!") }

View file

@ -51,7 +51,8 @@ export class ExistingSource extends React.Component<DesktopCapturerSourceIProps>
<AccessibleButton <AccessibleButton
className="mx_desktopCapturerSourcePicker_stream_button" className="mx_desktopCapturerSourcePicker_stream_button"
title={this.props.source.name} title={this.props.source.name}
onClick={this.onClick} > onClick={this.onClick}
>
<img <img
className="mx_desktopCapturerSourcePicker_stream_thumbnail" className="mx_desktopCapturerSourcePicker_stream_thumbnail"
src={this.props.source.thumbnailURL} src={this.props.source.thumbnailURL}

View file

@ -1,7 +1,6 @@
/* /*
Copyright 2017 Vector Creations Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 The Matrix.org Foundation C.I.C. Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -16,34 +15,38 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { createRef } from 'react'; import React, { ChangeEvent, createRef, CSSProperties, ReactElement, ReactNode, Ref } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import AccessibleButton from './AccessibleButton'; import AccessibleButton from './AccessibleButton';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { Key } from "../../../Keyboard"; import { Key } from "../../../Keyboard";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
class MenuOption extends React.Component { interface IMenuOptionProps {
constructor(props) { children: ReactElement;
super(props); highlighted?: boolean;
this._onMouseEnter = this._onMouseEnter.bind(this); dropdownKey: string;
this._onClick = this._onClick.bind(this); id?: string;
inputRef?: Ref<HTMLDivElement>;
onClick(dropdownKey: string): void;
onMouseEnter(dropdownKey: string): void;
} }
class MenuOption extends React.Component<IMenuOptionProps> {
static defaultProps = { static defaultProps = {
disabled: false, disabled: false,
}; };
_onMouseEnter() { private onMouseEnter = () => {
this.props.onMouseEnter(this.props.dropdownKey); this.props.onMouseEnter(this.props.dropdownKey);
} };
_onClick(e) { private onClick = (e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
this.props.onClick(this.props.dropdownKey); this.props.onClick(this.props.dropdownKey);
} };
render() { render() {
const optClasses = classnames({ const optClasses = classnames({
@ -54,8 +57,8 @@ class MenuOption extends React.Component {
return <div return <div
id={this.props.id} id={this.props.id}
className={optClasses} className={optClasses}
onClick={this._onClick} onClick={this.onClick}
onMouseEnter={this._onMouseEnter} onMouseEnter={this.onMouseEnter}
role="option" role="option"
aria-selected={this.props.highlighted} aria-selected={this.props.highlighted}
ref={this.props.inputRef} ref={this.props.inputRef}
@ -65,91 +68,97 @@ class MenuOption extends React.Component {
} }
} }
MenuOption.propTypes = { interface IProps {
children: PropTypes.oneOfType([ id: string;
PropTypes.arrayOf(PropTypes.node), // ARIA label
PropTypes.node, label: string;
]), value?: string;
highlighted: PropTypes.bool, className?: string;
dropdownKey: PropTypes.string, children: ReactElement[];
onClick: PropTypes.func.isRequired, // negative for consistency with HTML
onMouseEnter: PropTypes.func.isRequired, disabled?: boolean;
inputRef: PropTypes.any, // The width that the dropdown should be. If specified,
}; // the dropped-down part of the menu will be set to this
// width.
menuWidth?: number;
searchEnabled?: boolean;
// Called when the selected option changes
onOptionChange(dropdownKey: string): void;
// Called when the value of the search field changes
onSearchChange?(query: string): void;
// Function that, given the key of an option, returns
// a node representing that option to be displayed in the
// box itself as the currently-selected option (ie. as
// opposed to in the actual dropped-down part). If
// unspecified, the appropriate child element is used as
// in the dropped-down menu.
getShortOption?(value: string): ReactNode;
}
interface IState {
expanded: boolean;
highlightedOption: string | null;
searchQuery: string;
}
/* /*
* Reusable dropdown select control, akin to react-select, * Reusable dropdown select control, akin to react-select,
* but somewhat simpler as react-select is 79KB of minified * but somewhat simpler as react-select is 79KB of minified
* javascript. * javascript.
*
* TODO: Port NetworkDropdown to use this.
*/ */
@replaceableComponent("views.elements.Dropdown") @replaceableComponent("views.elements.Dropdown")
export default class Dropdown extends React.Component { export default class Dropdown extends React.Component<IProps, IState> {
constructor(props) { private readonly buttonRef = createRef<HTMLDivElement>();
private dropdownRootElement: HTMLDivElement = null;
private ignoreEvent: MouseEvent = null;
private childrenByKey: Record<string, ReactNode> = {};
constructor(props: IProps) {
super(props); super(props);
this.dropdownRootElement = null; this.reindexChildren(this.props.children);
this.ignoreEvent = null;
this._onInputClick = this._onInputClick.bind(this); const firstChild = React.Children.toArray(props.children)[0] as ReactElement;
this._onRootClick = this._onRootClick.bind(this);
this._onDocumentClick = this._onDocumentClick.bind(this);
this._onMenuOptionClick = this._onMenuOptionClick.bind(this);
this._onInputChange = this._onInputChange.bind(this);
this._collectRoot = this._collectRoot.bind(this);
this._collectInputTextBox = this._collectInputTextBox.bind(this);
this._setHighlightedOption = this._setHighlightedOption.bind(this);
this.inputTextBox = null;
this._reindexChildren(this.props.children);
const firstChild = React.Children.toArray(props.children)[0];
this.state = { this.state = {
// True if the menu is dropped-down // True if the menu is dropped-down
expanded: false, expanded: false,
// The key of the highlighted option // The key of the highlighted option
// (the option that would become selected if you pressed enter) // (the option that would become selected if you pressed enter)
highlightedOption: firstChild ? firstChild.key : null, highlightedOption: firstChild ? firstChild.key as string : null,
// the current search query // the current search query
searchQuery: '', searchQuery: '',
}; };
}
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount() { // eslint-disable-line camelcase
this._button = createRef();
// Listen for all clicks on the document so we can close the // Listen for all clicks on the document so we can close the
// menu when the user clicks somewhere else // menu when the user clicks somewhere else
document.addEventListener('click', this._onDocumentClick, false); document.addEventListener('click', this.onDocumentClick, false);
} }
componentWillUnmount() { componentWillUnmount() {
document.removeEventListener('click', this._onDocumentClick, false); document.removeEventListener('click', this.onDocumentClick, false);
} }
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line
if (!nextProps.children || nextProps.children.length === 0) { if (!nextProps.children || nextProps.children.length === 0) {
return; return;
} }
this._reindexChildren(nextProps.children); this.reindexChildren(nextProps.children);
const firstChild = nextProps.children[0]; const firstChild = nextProps.children[0];
this.setState({ this.setState({
highlightedOption: firstChild ? firstChild.key : null, highlightedOption: firstChild ? firstChild.key : null,
}); });
} }
_reindexChildren(children) { private reindexChildren(children: ReactElement[]): void {
this.childrenByKey = {}; this.childrenByKey = {};
React.Children.forEach(children, (child) => { React.Children.forEach(children, (child) => {
this.childrenByKey[child.key] = child; this.childrenByKey[child.key] = child;
}); });
} }
_onDocumentClick(ev) { private onDocumentClick = (ev: MouseEvent) => {
// Close the dropdown if the user clicks anywhere that isn't // Close the dropdown if the user clicks anywhere that isn't
// within our root element // within our root element
if (ev !== this.ignoreEvent) { if (ev !== this.ignoreEvent) {
@ -157,9 +166,9 @@ export default class Dropdown extends React.Component {
expanded: false, expanded: false,
}); });
} }
} };
_onRootClick(ev) { private onRootClick = (ev: MouseEvent) => {
// This captures any clicks that happen within our elements, // This captures any clicks that happen within our elements,
// such that we can then ignore them when they're seen by the // such that we can then ignore them when they're seen by the
// click listener on the document handler, ie. not close the // click listener on the document handler, ie. not close the
@ -167,9 +176,9 @@ export default class Dropdown extends React.Component {
// NB. We can't just stopPropagation() because then the event // NB. We can't just stopPropagation() because then the event
// doesn't reach the React onClick(). // doesn't reach the React onClick().
this.ignoreEvent = ev; this.ignoreEvent = ev;
} };
_onInputClick(ev) { private onInputClick = (ev: React.MouseEvent) => {
if (this.props.disabled) return; if (this.props.disabled) return;
if (!this.state.expanded) { if (!this.state.expanded) {
@ -178,24 +187,24 @@ export default class Dropdown extends React.Component {
}); });
ev.preventDefault(); ev.preventDefault();
} }
} };
_close() { private close() {
this.setState({ this.setState({
expanded: false, expanded: false,
}); });
// their focus was on the input, its getting unmounted, move it to the button // their focus was on the input, its getting unmounted, move it to the button
if (this._button.current) { if (this.buttonRef.current) {
this._button.current.focus(); this.buttonRef.current.focus();
} }
} }
_onMenuOptionClick(dropdownKey) { private onMenuOptionClick = (dropdownKey: string) => {
this._close(); this.close();
this.props.onOptionChange(dropdownKey); this.props.onOptionChange(dropdownKey);
} };
_onInputKeyDown = (e) => { private onInputKeyDown = (e: React.KeyboardEvent) => {
let handled = true; let handled = true;
// These keys don't generate keypress events and so needs to be on keyup // These keys don't generate keypress events and so needs to be on keyup
@ -204,16 +213,16 @@ export default class Dropdown extends React.Component {
this.props.onOptionChange(this.state.highlightedOption); this.props.onOptionChange(this.state.highlightedOption);
// fallthrough // fallthrough
case Key.ESCAPE: case Key.ESCAPE:
this._close(); this.close();
break; break;
case Key.ARROW_DOWN: case Key.ARROW_DOWN:
this.setState({ this.setState({
highlightedOption: this._nextOption(this.state.highlightedOption), highlightedOption: this.nextOption(this.state.highlightedOption),
}); });
break; break;
case Key.ARROW_UP: case Key.ARROW_UP:
this.setState({ this.setState({
highlightedOption: this._prevOption(this.state.highlightedOption), highlightedOption: this.prevOption(this.state.highlightedOption),
}); });
break; break;
default: default:
@ -224,53 +233,46 @@ export default class Dropdown extends React.Component {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
} }
} };
_onInputChange(e) { private onInputChange = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({ this.setState({
searchQuery: e.target.value, searchQuery: e.currentTarget.value,
}); });
if (this.props.onSearchChange) { if (this.props.onSearchChange) {
this.props.onSearchChange(e.target.value); this.props.onSearchChange(e.currentTarget.value);
}
} }
};
_collectRoot(e) { private collectRoot = (e: HTMLDivElement) => {
if (this.dropdownRootElement) { if (this.dropdownRootElement) {
this.dropdownRootElement.removeEventListener( this.dropdownRootElement.removeEventListener('click', this.onRootClick, false);
'click', this._onRootClick, false,
);
} }
if (e) { if (e) {
e.addEventListener('click', this._onRootClick, false); e.addEventListener('click', this.onRootClick, false);
} }
this.dropdownRootElement = e; this.dropdownRootElement = e;
} };
_collectInputTextBox(e) { private setHighlightedOption = (optionKey: string) => {
this.inputTextBox = e;
if (e) e.focus();
}
_setHighlightedOption(optionKey) {
this.setState({ this.setState({
highlightedOption: optionKey, highlightedOption: optionKey,
}); });
} };
_nextOption(optionKey) { private nextOption(optionKey: string): string {
const keys = Object.keys(this.childrenByKey); const keys = Object.keys(this.childrenByKey);
const index = keys.indexOf(optionKey); const index = keys.indexOf(optionKey);
return keys[(index + 1) % keys.length]; return keys[(index + 1) % keys.length];
} }
_prevOption(optionKey) { private prevOption(optionKey: string): string {
const keys = Object.keys(this.childrenByKey); const keys = Object.keys(this.childrenByKey);
const index = keys.indexOf(optionKey); const index = keys.indexOf(optionKey);
return keys[(index - 1) % keys.length]; return keys[(index - 1) % keys.length];
} }
_scrollIntoView(node) { private scrollIntoView(node: Element) {
if (node) { if (node) {
node.scrollIntoView({ node.scrollIntoView({
block: "nearest", block: "nearest",
@ -279,18 +281,18 @@ export default class Dropdown extends React.Component {
} }
} }
_getMenuOptions() { private getMenuOptions() {
const options = React.Children.map(this.props.children, (child) => { const options = React.Children.map(this.props.children, (child) => {
const highlighted = this.state.highlightedOption === child.key; const highlighted = this.state.highlightedOption === child.key;
return ( return (
<MenuOption <MenuOption
id={`${this.props.id}__${child.key}`} id={`${this.props.id}__${child.key}`}
key={child.key} key={child.key}
dropdownKey={child.key} dropdownKey={child.key as string}
highlighted={highlighted} highlighted={highlighted}
onMouseEnter={this._setHighlightedOption} onMouseEnter={this.setHighlightedOption}
onClick={this._onMenuOptionClick} onClick={this.onMenuOptionClick}
inputRef={highlighted ? this._scrollIntoView : undefined} inputRef={highlighted ? this.scrollIntoView : undefined}
> >
{ child } { child }
</MenuOption> </MenuOption>
@ -307,7 +309,7 @@ export default class Dropdown extends React.Component {
render() { render() {
let currentValue; let currentValue;
const menuStyle = {}; const menuStyle: CSSProperties = {};
if (this.props.menuWidth) menuStyle.width = this.props.menuWidth; if (this.props.menuWidth) menuStyle.width = this.props.menuWidth;
let menu; let menu;
@ -316,10 +318,10 @@ export default class Dropdown extends React.Component {
currentValue = ( currentValue = (
<input <input
type="text" type="text"
autoFocus={true}
className="mx_Dropdown_option" className="mx_Dropdown_option"
ref={this._collectInputTextBox} onKeyDown={this.onInputKeyDown}
onKeyDown={this._onInputKeyDown} onChange={this.onInputChange}
onChange={this._onInputChange}
value={this.state.searchQuery} value={this.state.searchQuery}
role="combobox" role="combobox"
aria-autocomplete="list" aria-autocomplete="list"
@ -332,7 +334,7 @@ export default class Dropdown extends React.Component {
} }
menu = ( menu = (
<div className="mx_Dropdown_menu" style={menuStyle} role="listbox" id={`${this.props.id}_listbox`}> <div className="mx_Dropdown_menu" style={menuStyle} role="listbox" id={`${this.props.id}_listbox`}>
{ this._getMenuOptions() } { this.getMenuOptions() }
</div> </div>
); );
} }
@ -356,14 +358,14 @@ export default class Dropdown extends React.Component {
// Note the menu sits inside the AccessibleButton div so it's anchored // Note the menu sits inside the AccessibleButton div so it's anchored
// to the input, but overflows below it. The root contains both. // to the input, but overflows below it. The root contains both.
return <div className={classnames(dropdownClasses)} ref={this._collectRoot}> return <div className={classnames(dropdownClasses)} ref={this.collectRoot}>
<AccessibleButton <AccessibleButton
className="mx_Dropdown_input mx_no_textinput" className="mx_Dropdown_input mx_no_textinput"
onClick={this._onInputClick} onClick={this.onInputClick}
aria-haspopup="listbox" aria-haspopup="listbox"
aria-expanded={this.state.expanded} aria-expanded={this.state.expanded}
disabled={this.props.disabled} disabled={this.props.disabled}
inputRef={this._button} inputRef={this.buttonRef}
aria-label={this.props.label} aria-label={this.props.label}
aria-describedby={`${this.props.id}_value`} aria-describedby={`${this.props.id}_value`}
> >
@ -374,28 +376,3 @@ export default class Dropdown extends React.Component {
</div>; </div>;
} }
} }
Dropdown.propTypes = {
id: PropTypes.string.isRequired,
// The width that the dropdown should be. If specified,
// the dropped-down part of the menu will be set to this
// width.
menuWidth: PropTypes.number,
// Called when the selected option changes
onOptionChange: PropTypes.func.isRequired,
// Called when the value of the search field changes
onSearchChange: PropTypes.func,
searchEnabled: PropTypes.bool,
// Function that, given the key of an option, returns
// a node representing that option to be displayed in the
// box itself as the currently-selected option (ie. as
// opposed to in the actual dropped-down part). If
// unspecified, the appropriate child element is used as
// in the dropped-down menu.
getShortOption: PropTypes.func,
value: PropTypes.string,
// negative for consistency with HTML
disabled: PropTypes.bool,
// ARIA label
label: PropTypes.string.isRequired,
};

View file

@ -419,7 +419,8 @@ export default class ImageView extends React.Component<IProps, IState> {
const avatar = ( const avatar = (
<MemberAvatar <MemberAvatar
member={mxEvent.sender} member={mxEvent.sender}
width={32} height={32} width={32}
height={32}
viewUserOnClick={true} viewUserOnClick={true}
/> />
); );
@ -438,7 +439,7 @@ export default class ImageView extends React.Component<IProps, IState> {
// an empty div here, since the panel uses space-between // an empty div here, since the panel uses space-between
// and we want the same placement of elements // and we want the same placement of elements
info = ( info = (
<div></div> <div />
); );
} }
@ -462,15 +463,15 @@ export default class ImageView extends React.Component<IProps, IState> {
<AccessibleTooltipButton <AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_zoomOut" className="mx_ImageView_button mx_ImageView_button_zoomOut"
title={_t("Zoom out")} title={_t("Zoom out")}
onClick={this.onZoomOutClick}> onClick={this.onZoomOutClick}
</AccessibleTooltipButton> />
); );
zoomInButton = ( zoomInButton = (
<AccessibleTooltipButton <AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_zoomIn" className="mx_ImageView_button mx_ImageView_button_zoomIn"
title={_t("Zoom in")} title={_t("Zoom in")}
onClick={this.onZoomInClick}> onClick={this.onZoomInClick}
</AccessibleTooltipButton> />
); );
} }
@ -492,24 +493,24 @@ export default class ImageView extends React.Component<IProps, IState> {
<AccessibleTooltipButton <AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_rotateCCW" className="mx_ImageView_button mx_ImageView_button_rotateCCW"
title={_t("Rotate Left")} title={_t("Rotate Left")}
onClick={this.onRotateCounterClockwiseClick}> onClick={this.onRotateCounterClockwiseClick}
</AccessibleTooltipButton> />
<AccessibleTooltipButton <AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_rotateCW" className="mx_ImageView_button mx_ImageView_button_rotateCW"
title={_t("Rotate Right")} title={_t("Rotate Right")}
onClick={this.onRotateClockwiseClick}> onClick={this.onRotateClockwiseClick}
</AccessibleTooltipButton> />
<AccessibleTooltipButton <AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_download" className="mx_ImageView_button mx_ImageView_button_download"
title={_t("Download")} title={_t("Download")}
onClick={this.onDownloadClick}> onClick={this.onDownloadClick}
</AccessibleTooltipButton> />
{ contextMenuButton } { contextMenuButton }
<AccessibleTooltipButton <AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_close" className="mx_ImageView_button mx_ImageView_button_close"
title={_t("Close")} title={_t("Close")}
onClick={this.props.onFinished}> onClick={this.props.onFinished}
</AccessibleTooltipButton> />
{ this.renderContextMenu() } { this.renderContextMenu() }
</div> </div>
</div> </div>

View file

@ -92,7 +92,7 @@ const MiniAvatarUploader: React.FC<IProps> = ({ hasAvatar, hasAvatarLabel, noAva
<div className="mx_MiniAvatarUploader_indicator"> <div className="mx_MiniAvatarUploader_indicator">
{ busy ? { busy ?
<Spinner w={20} h={20} /> : <Spinner w={20} h={20} /> :
<div className="mx_MiniAvatarUploader_cameraIcon"></div> } <div className="mx_MiniAvatarUploader_cameraIcon" /> }
</div> </div>
<div className={classNames("mx_Tooltip", { <div className={classNames("mx_Tooltip", {

View file

@ -258,7 +258,10 @@ class Pill extends React.Component {
linkText = groupId; linkText = groupId;
if (this.props.shouldShowPillAvatar) { if (this.props.shouldShowPillAvatar) {
avatar = <BaseAvatar avatar = <BaseAvatar
name={name || groupId} width={16} height={16} aria-hidden="true" name={name || groupId}
width={16}
height={16}
aria-hidden="true"
url={avatarUrl ? mediaFromMxc(avatarUrl).getSquareThumbnailHttp(16) : null} />; url={avatarUrl ? mediaFromMxc(avatarUrl).getSquareThumbnailHttp(16) : null} />;
} }
pillClass = 'mx_GroupPill'; pillClass = 'mx_GroupPill';

View file

@ -134,8 +134,10 @@ export default class PowerSelector extends React.Component {
const label = typeof this.props.label === "undefined" ? _t("Power level") : this.props.label; const label = typeof this.props.label === "undefined" ? _t("Power level") : this.props.label;
if (this.state.custom) { if (this.state.custom) {
picker = ( picker = (
<Field type="number" <Field
label={label} max={this.props.maxValue} type="number"
label={label}
max={this.props.maxValue}
onBlur={this.onCustomBlur} onBlur={this.onCustomBlur}
onKeyDown={this.onCustomKeyDown} onKeyDown={this.onCustomKeyDown}
onChange={this.onCustomChange} onChange={this.onCustomChange}
@ -157,9 +159,12 @@ export default class PowerSelector extends React.Component {
}); });
picker = ( picker = (
<Field element="select" <Field
label={label} onChange={this.onSelectChange} element="select"
value={String(this.state.selectValue)} disabled={this.props.disabled} label={label}
onChange={this.onSelectChange}
value={String(this.state.selectValue)}
disabled={this.props.disabled}
> >
{ options } { options }
</Field> </Field>

View file

@ -14,17 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from "react"; import React, { ReactNode } from "react";
import classNames from "classnames"; import classNames from "classnames";
import StyledRadioButton from "./StyledRadioButton"; import StyledRadioButton from "./StyledRadioButton";
interface IDefinition<T extends string> { export interface IDefinition<T extends string> {
value: T; value: T;
className?: string; className?: string;
disabled?: boolean; disabled?: boolean;
label: React.ReactChild; label: ReactNode;
description?: React.ReactChild; description?: ReactNode;
checked?: boolean; // If provided it will override the value comparison done in the group checked?: boolean; // If provided it will override the value comparison done in the group
} }
@ -59,7 +59,7 @@ function StyledRadioGroup<T extends string>({
checked={d.checked !== undefined ? d.checked : d.value === value} checked={d.checked !== undefined ? d.checked : d.value === value}
name={name} name={name}
value={d.value} value={d.value}
disabled={disabled || d.disabled} disabled={d.disabled ?? disabled}
outlined={outlined} outlined={outlined}
> >
{ d.label } { d.label }

View file

@ -166,8 +166,7 @@ export default class Tooltip extends React.Component<IProps> {
public render() { public render() {
// Render a placeholder // Render a placeholder
return ( return (
<div className={this.props.className}> <div className={this.props.className} />
</div>
); );
} }
} }

View file

@ -101,14 +101,16 @@ class Category extends React.PureComponent<IProps> {
{ name } { name }
</h2> </h2>
<LazyRenderList <LazyRenderList
element="ul" className="mx_EmojiPicker_list" element="ul"
itemHeight={EMOJI_HEIGHT} items={rows} className="mx_EmojiPicker_list"
itemHeight={EMOJI_HEIGHT}
items={rows}
scrollTop={localScrollTop} scrollTop={localScrollTop}
height={localHeight} height={localHeight}
overflowItems={OVERFLOW_ROWS} overflowItems={OVERFLOW_ROWS}
overflowMargin={0} overflowMargin={0}
renderItem={this.renderEmojiRow}> renderItem={this.renderEmojiRow}
</LazyRenderList> />
</section> </section>
); );
} }

View file

@ -86,10 +86,16 @@ export default class GroupMemberList extends React.Component {
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
const text = _t("and %(count)s others...", { count: overflowCount }); const text = _t("and %(count)s others...", { count: overflowCount });
return ( return (
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={ <EntityTile
className="mx_EntityTile_ellipsis"
avatarJsx={
<BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} /> <BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} />
} name={text} presenceState="online" suppressOnHover={true} }
onClick={this._showFullMemberList} /> name={text}
presenceState="online"
suppressOnHover={true}
onClick={this._showFullMemberList}
/>
); );
}; };
@ -152,7 +158,9 @@ export default class GroupMemberList extends React.Component {
); );
}); });
return <TruncatedList className="mx_MemberList_wrapper" truncateAt={this.state.truncateAt} return <TruncatedList
className="mx_MemberList_wrapper"
truncateAt={this.state.truncateAt}
createOverflowElement={this._createOverflowTile} createOverflowElement={this._createOverflowTile}
> >
{ memberTiles } { memberTiles }

View file

@ -56,14 +56,19 @@ export default class GroupMemberTile extends React.Component {
aria-hidden="true" aria-hidden="true"
name={this.props.member.displayname || this.props.member.userId} name={this.props.member.displayname || this.props.member.userId}
idName={this.props.member.userId} idName={this.props.member.userId}
width={36} height={36} width={36}
height={36}
url={avatarUrl} url={avatarUrl}
/> />
); );
return ( return (
<EntityTile name={name} avatarJsx={av} onClick={this.onClick} <EntityTile
suppressOnHover={true} presenceState="online" name={name}
avatarJsx={av}
onClick={this.onClick}
suppressOnHover={true}
presenceState="online"
powerStatus={this.props.member.isPrivileged ? EntityTile.POWER_STATUS_ADMIN : null} powerStatus={this.props.member.isPrivileged ? EntityTile.POWER_STATUS_ADMIN : null}
/> />
); );

View file

@ -76,10 +76,16 @@ export default class GroupRoomList extends React.Component {
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
const text = _t("and %(count)s others...", { count: overflowCount }); const text = _t("and %(count)s others...", { count: overflowCount });
return ( return (
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={ <EntityTile
className="mx_EntityTile_ellipsis"
avatarJsx={
<BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} /> <BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} />
} name={text} presenceState="online" suppressOnHover={true} }
onClick={this._showFullRoomList} /> name={text}
presenceState="online"
suppressOnHover={true}
onClick={this._showFullRoomList}
/>
); );
}; };
@ -142,7 +148,8 @@ export default class GroupRoomList extends React.Component {
} }
const inputBox = ( const inputBox = (
<input <input
className="mx_GroupRoomList_query mx_textinput" id="mx_GroupRoomList_query" className="mx_GroupRoomList_query mx_textinput"
id="mx_GroupRoomList_query"
type="text" type="text"
onChange={this.onSearchQueryChanged} onChange={this.onSearchQueryChanged}
value={this.state.searchQuery} value={this.state.searchQuery}
@ -156,8 +163,11 @@ export default class GroupRoomList extends React.Component {
<div className="mx_GroupRoomList" role="tabpanel"> <div className="mx_GroupRoomList" role="tabpanel">
{ 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
createOverflowElement={this._createOverflowTile}> className="mx_GroupRoomList_wrapper"
truncateAt={this.state.truncateAt}
createOverflowElement={this._createOverflowTile}
>
{ this.makeGroupRoomTiles(this.state.searchQuery) } { this.makeGroupRoomTiles(this.state.searchQuery) }
</TruncatedList> </TruncatedList>
</AutoHideScrollbar> </AutoHideScrollbar>

View file

@ -48,8 +48,10 @@ class GroupRoomTile extends React.Component {
: null; : null;
const av = ( const av = (
<BaseAvatar name={this.props.groupRoom.displayname} <BaseAvatar
width={36} height={36} name={this.props.groupRoom.displayname}
width={36}
height={36}
url={avatarUrl} url={avatarUrl}
/> />
); );

View file

@ -206,7 +206,7 @@ export default class CallEvent extends React.Component<IProps, IState> {
{ sender } { sender }
</div> </div>
<div className="mx_CallEvent_type"> <div className="mx_CallEvent_type">
<div className="mx_CallEvent_type_icon"></div> <div className="mx_CallEvent_type_icon" />
{ callType } { callType }
</div> </div>
</div> </div>

View file

@ -16,14 +16,14 @@ limitations under the License.
import React from "react"; import React from "react";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { Playback } from "../../../voice/Playback"; import { Playback } from "../../../audio/Playback";
import InlineSpinner from '../elements/InlineSpinner'; import InlineSpinner from '../elements/InlineSpinner';
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import AudioPlayer from "../audio_messages/AudioPlayer"; import AudioPlayer from "../audio_messages/AudioPlayer";
import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent"; import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent";
import MFileBody from "./MFileBody"; import MFileBody from "./MFileBody";
import { IBodyProps } from "./IBodyProps"; import { IBodyProps } from "./IBodyProps";
import { PlaybackManager } from "../../../voice/PlaybackManager"; import { PlaybackManager } from "../../../audio/PlaybackManager";
interface IState { interface IState {
error?: Error; error?: Error;
@ -76,7 +76,6 @@ export default class MAudioBody extends React.PureComponent<IBodyProps, IState>
public render() { public render() {
if (this.state.error) { if (this.state.error) {
// TODO: @@TR: Verify error state
return ( return (
<span className="mx_MAudioBody"> <span className="mx_MAudioBody">
<img src={require("../../../../res/img/warning.svg")} width="16" height="16" /> <img src={require("../../../../res/img/warning.svg")} width="16" height="16" />
@ -86,7 +85,6 @@ export default class MAudioBody extends React.PureComponent<IBodyProps, IState>
} }
if (!this.state.playback) { if (!this.state.playback) {
// TODO: @@TR: Verify loading/decrypting state
return ( return (
<span className="mx_MAudioBody"> <span className="mx_MAudioBody">
<InlineSpinner /> <InlineSpinner />

View file

@ -306,7 +306,10 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
imageElement = <HiddenImagePlaceholder />; imageElement = <HiddenImagePlaceholder />;
} 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}
@ -340,8 +343,11 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
// which has the same width as the timeline // which has the same width as the timeline
// 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
style={{ maxWidth: maxWidth + "px" }} className="mx_MImageBody_thumbnail"
src={thumbUrl}
ref={this.image}
style={{ maxWidth: `min(100%, ${maxWidth}px)` }}
alt={content.body} alt={content.body}
onError={this.onImageError} onError={this.onImageError}
onLoad={this.onImageLoad} onLoad={this.onImageLoad}
@ -362,10 +368,13 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
const thumbnail = ( const thumbnail = (
<div className="mx_MImageBody_thumbnail_container" style={{ maxHeight: maxHeight + "px", maxWidth: maxWidth + "px" }}> <div className="mx_MImageBody_thumbnail_container" style={{ maxHeight: maxHeight + "px", maxWidth: maxWidth + "px" }}>
{ showPlaceholder && { showPlaceholder &&
<div className="mx_MImageBody_thumbnail" style={{ <div
className="mx_MImageBody_thumbnail"
style={{
// Constrain width here so that spinner appears central to the loaded thumbnail // Constrain width here so that spinner appears central to the loaded thumbnail
maxWidth: infoWidth + "px", maxWidth: `min(100%, ${infoWidth}px)`,
}}> }}
>
{ placeholder } { placeholder }
</div> </div>
} }
@ -416,10 +425,10 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
if (this.state.error !== null) { if (this.state.error !== null) {
return ( return (
<span className="mx_MImageBody"> <div className="mx_MImageBody">
<img src={require("../../../../res/img/warning.svg")} width="16" height="16" /> <img src={require("../../../../res/img/warning.svg")} width="16" height="16" />
{ _t("Error decrypting image") } { _t("Error decrypting image") }
</span> </div>
); );
} }
@ -434,10 +443,10 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
const thumbnail = this.messageContent(contentUrl, thumbUrl, content); const thumbnail = this.messageContent(contentUrl, thumbUrl, content);
const fileBody = this.getFileBody(); const fileBody = this.getFileBody();
return <span className="mx_MImageBody"> return <div className="mx_MImageBody">
{ thumbnail } { thumbnail }
{ fileBody } { fileBody }
</span>; </div>;
} }
} }
@ -452,7 +461,7 @@ export class HiddenImagePlaceholder extends React.PureComponent<PlaceholderIProp
let className = 'mx_HiddenImagePlaceholder'; let className = 'mx_HiddenImagePlaceholder';
if (this.props.hover) className += ' mx_HiddenImagePlaceholder_hover'; if (this.props.hover) className += ' mx_HiddenImagePlaceholder_hover';
return ( return (
<div className={className} style={{ maxWidth: maxWidth }}> <div className={className} style={{ maxWidth: `min(100%, ${maxWidth}px)` }}>
<div className='mx_HiddenImagePlaceholder_button'> <div className='mx_HiddenImagePlaceholder_button'>
<span className='mx_HiddenImagePlaceholder_eye' /> <span className='mx_HiddenImagePlaceholder_eye' />
<span>{ _t("Show image") }</span> <span>{ _t("Show image") }</span>

View file

@ -267,8 +267,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
width={width} width={width}
poster={poster} poster={poster}
onPlay={this.videoOnPlay} onPlay={this.videoOnPlay}
> />
</video>
{ this.props.tileShape && <MFileBody {...this.props} showGenericPlaceholder={false} /> } { this.props.tileShape && <MFileBody {...this.props} showGenericPlaceholder={false} /> }
</span> </span>
); );

View file

@ -27,7 +27,6 @@ export default class MVoiceMessageBody extends MAudioBody {
// A voice message is an audio file but rendered in a special way. // A voice message is an audio file but rendered in a special way.
public render() { public render() {
if (this.state.error) { if (this.state.error) {
// TODO: @@TR: Verify error state
return ( return (
<span className="mx_MVoiceMessageBody"> <span className="mx_MVoiceMessageBody">
<img src={require("../../../../res/img/warning.svg")} width="16" height="16" /> <img src={require("../../../../res/img/warning.svg")} width="16" height="16" />
@ -37,7 +36,6 @@ export default class MVoiceMessageBody extends MAudioBody {
} }
if (!this.state.playback) { if (!this.state.playback) {
// TODO: @@TR: Verify loading/decrypting state
return ( return (
<span className="mx_MVoiceMessageBody"> <span className="mx_MVoiceMessageBody">
<InlineSpinner /> <InlineSpinner />

View file

@ -78,8 +78,11 @@ export default class RoomAvatarEvent extends React.Component {
{ senderDisplayName: senderDisplayName }, { senderDisplayName: senderDisplayName },
{ {
'img': () => 'img': () =>
<AccessibleButton key="avatar" className="mx_RoomAvatarEvent_avatar" <AccessibleButton
onClick={this.onAvatarClick}> key="avatar"
className="mx_RoomAvatarEvent_avatar"
onClick={this.onAvatarClick}
>
<RoomAvatar width={14} height={14} oobData={oobData} /> <RoomAvatar width={14} height={14} oobData={oobData} />
</AccessibleButton>, </AccessibleButton>,
}) })

View file

@ -1353,13 +1353,16 @@ const BasicUserInfo: React.FC<{
if (hasCrossSigningKeys !== undefined) { if (hasCrossSigningKeys !== undefined) {
// Note: mx_UserInfo_verifyButton is for the end-to-end tests // Note: mx_UserInfo_verifyButton is for the end-to-end tests
verifyButton = ( verifyButton = (
<AccessibleButton className="mx_UserInfo_field mx_UserInfo_verifyButton" onClick={() => { <AccessibleButton
className="mx_UserInfo_field mx_UserInfo_verifyButton"
onClick={() => {
if (hasCrossSigningKeys) { if (hasCrossSigningKeys) {
verifyUser(member as User); verifyUser(member as User);
} else { } else {
legacyVerifyUser(member as User); legacyVerifyUser(member as User);
} }
}}> }}
>
{ _t("Verify") } { _t("Verify") }
</AccessibleButton> </AccessibleButton>
); );
@ -1374,12 +1377,15 @@ const BasicUserInfo: React.FC<{
let editDevices; let editDevices;
if (member.userId == cli.getUserId()) { if (member.userId == cli.getUserId()) {
editDevices = (<p> editDevices = (<p>
<AccessibleButton className="mx_UserInfo_field" onClick={() => { <AccessibleButton
className="mx_UserInfo_field"
onClick={() => {
dis.dispatch({ dis.dispatch({
action: Action.ViewUserSettings, action: Action.ViewUserSettings,
initialTabId: UserTab.Security, initialTabId: UserTab.Security,
}); });
}}> }}
>
{ _t("Edit devices") } { _t("Edit devices") }
</AccessibleButton> </AccessibleButton>
</p>); </p>);

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