Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
Šimon Brandner 2021-07-25 08:10:01 +02:00
commit b762303102
No known key found for this signature in database
GPG key ID: 55C211A1226CB17D
181 changed files with 4794 additions and 3252 deletions

View file

@ -14,9 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { Playback, PlaybackState } from "../../../voice/Playback";
import React, { createRef, ReactNode, RefObject } from "react";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import PlayPauseButton from "./PlayPauseButton";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { formatBytes } from "../../../utils/FormattingUtils";
@ -25,44 +23,13 @@ import { Key } from "../../../Keyboard";
import { _t } from "../../../languageHandler";
import SeekBar from "./SeekBar";
import PlaybackClock from "./PlaybackClock";
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;
}
import AudioPlayerBase from "./AudioPlayerBase";
@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 seekRef: RefObject<SeekBar> = createRef();
constructor(props: IProps) {
super(props);
this.state = {
playbackPhase: PlaybackState.Decoding, // default assumption
};
// We don't need to de-register: the class handles this for us internally
this.props.playback.on(UPDATE_EVENT, this.onPlaybackUpdate);
// Don't wait for the promise to complete - it will emit a progress update when it
// is done, and it's not meant to take long anyhow.
// noinspection JSIgnoredPromiseFromCall
this.props.playback.prepare();
}
private onPlaybackUpdate = (ev: PlaybackState) => {
this.setState({ playbackPhase: ev });
};
private onKeyDown = (ev: React.KeyboardEvent) => {
// stopPropagation() prevents the FocusComposer catch-all from triggering,
// but we need to do it on key down instead of press (even though the user
@ -88,37 +55,39 @@ export default class AudioPlayer extends React.PureComponent<IProps, IState> {
return `(${formatBytes(bytes)})`;
}
public render(): ReactNode {
protected renderComponent(): ReactNode {
// tabIndex=0 to ensure that the whole component becomes a tab stop, where we handle keyboard
// events for accessibility
return <div className='mx_MediaBody mx_AudioPlayer_container' tabIndex={0} onKeyDown={this.onKeyDown}>
<div className='mx_AudioPlayer_primaryContainer'>
<PlayPauseButton
playback={this.props.playback}
playbackPhase={this.state.playbackPhase}
tabIndex={-1} // prevent tabbing into the button
ref={this.playPauseRef}
/>
<div className='mx_AudioPlayer_mediaInfo'>
<span className='mx_AudioPlayer_mediaName'>
{ this.props.mediaName || _t("Unnamed audio") }
</span>
<div className='mx_AudioPlayer_byline'>
<DurationClock playback={this.props.playback} />
&nbsp; { /* easiest way to introduce a gap between the components */ }
{ this.renderFileSize() }
return (
<div className='mx_MediaBody mx_AudioPlayer_container' tabIndex={0} onKeyDown={this.onKeyDown}>
<div className='mx_AudioPlayer_primaryContainer'>
<PlayPauseButton
playback={this.props.playback}
playbackPhase={this.state.playbackPhase}
tabIndex={-1} // prevent tabbing into the button
ref={this.playPauseRef}
/>
<div className='mx_AudioPlayer_mediaInfo'>
<span className='mx_AudioPlayer_mediaName'>
{ this.props.mediaName || _t("Unnamed audio") }
</span>
<div className='mx_AudioPlayer_byline'>
<DurationClock playback={this.props.playback} />
&nbsp; { /* easiest way to introduce a gap between the components */ }
{ this.renderFileSize() }
</div>
</div>
</div>
<div className='mx_AudioPlayer_seek'>
<SeekBar
playback={this.props.playback}
tabIndex={-1} // prevent tabbing into the bar
playbackPhase={this.state.playbackPhase}
ref={this.seekRef}
/>
<PlaybackClock playback={this.props.playback} defaultDisplaySeconds={0} />
</div>
</div>
<div className='mx_AudioPlayer_seek'>
<SeekBar
playback={this.props.playback}
tabIndex={-1} // prevent tabbing into the bar
playbackPhase={this.state.playbackPhase}
ref={this.seekRef}
/>
<PlaybackClock playback={this.props.playback} defaultDisplaySeconds={0} />
</div>
</div>;
);
}
}

View file

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

View file

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

View file

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

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
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 { arrayFastResample } from "../../../utils/arrays";
import { percentageOf } from "../../../utils/numbers";

View file

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

View file

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

View file

@ -18,7 +18,7 @@ import React from "react";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { arraySeed, arrayTrimFill } from "../../../utils/arrays";
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";
interface IProps {

View file

@ -14,61 +14,30 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { Playback, PlaybackState } from "../../../voice/Playback";
import React, { ReactNode } from "react";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import PlayPauseButton from "./PlayPauseButton";
import PlaybackClock from "./PlaybackClock";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { TileShape } from "../rooms/EventTile";
import PlaybackWaveform from "./PlaybackWaveform";
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;
}
import AudioPlayerBase from "./AudioPlayerBase";
@replaceableComponent("views.audio_messages.RecordingPlayback")
export default class RecordingPlayback 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.
// noinspection JSIgnoredPromiseFromCall
this.props.playback.prepare();
}
export default class RecordingPlayback extends AudioPlayerBase {
private get isWaveformable(): boolean {
return this.props.tileShape !== TileShape.Notif
&& this.props.tileShape !== TileShape.FileGrid
&& this.props.tileShape !== TileShape.Pinned;
}
private onPlaybackUpdate = (ev: PlaybackState) => {
this.setState({ playbackPhase: ev });
};
public render(): ReactNode {
protected renderComponent(): ReactNode {
const shapeClass = !this.isWaveformable ? 'mx_VoiceMessagePrimaryContainer_noWaveform' : '';
return <div className={'mx_MediaBody mx_VoiceMessagePrimaryContainer ' + shapeClass}>
<PlayPauseButton playback={this.props.playback} playbackPhase={this.state.playbackPhase} />
<PlaybackClock playback={this.props.playback} />
{ this.isWaveformable && <PlaybackWaveform playback={this.props.playback} /> }
</div>;
return (
<div className={'mx_MediaBody mx_VoiceMessagePrimaryContainer ' + shapeClass}>
<PlayPauseButton playback={this.props.playback} playbackPhase={this.state.playbackPhase} />
<PlaybackClock playback={this.props.playback} />
{ this.isWaveformable && <PlaybackWaveform playback={this.props.playback} /> }
</div>
);
}
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { Playback, PlaybackState } from "../../../voice/Playback";
import { Playback, PlaybackState } from "../../../audio/Playback";
import React, { ChangeEvent, CSSProperties, ReactNode } from "react";
import { replaceableComponent } from "../../../utils/replaceableComponent";
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_100pct': isCompleteBar,
});
return <span key={i} style={{
"--barHeight": h,
} as WaveformCSSProperties} className={classes} />;
return <span
key={i}
style={{
"--barHeight": h,
} as WaveformCSSProperties}
className={classes}
/>;
}) }
</div>;
}

View file

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

View file

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

View file

@ -102,8 +102,12 @@ export default class MemberAvatar extends React.Component<IProps, IState> {
}
return (
<BaseAvatar {...otherProps} name={this.state.name} title={this.state.title}
idName={userId} url={this.state.imageUrl} onClick={onClick} />
<BaseAvatar {...otherProps}
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
limitations under the License.
*/
import React, { ComponentProps } from 'react';
import { Room } from 'matrix-js-sdk/src/models/room';
import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
import classNames from "classnames";
import BaseAvatar from './BaseAvatar';
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
// would be nowhere to get the avatar from)
room?: Room;
oobData?: IOOBData;
oobData?: IOOBData & {
roomId?: string;
};
width?: number;
height?: number;
resizeMethod?: ResizeMethod;
viewAvatarOnClick?: boolean;
className?: string;
onClick?(): void;
}
@ -129,15 +134,19 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
};
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;
// 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
const idName = room ? (DMRoomMap.shared().getUserIdForRoomId(room.roomId) ?? room.roomId) : null;
const idName = room ? (DMRoomMap.shared().getUserIdForRoomId(room.roomId) ?? room.roomId) : oobData.roomId;
return (
<BaseAvatar {...otherProps}
<BaseAvatar
{...otherProps}
className={classNames(className, {
mx_RoomAvatar_isSpaceRoom: room?.isSpaceRoom(),
})}
name={roomName}
idName={idName}
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} />
</div>
<div className="mx_DialPadContextMenu_header">
<Field className="mx_DialPadContextMenu_dialled"
value={this.state.value} autoFocus={true}
<Field
className="mx_DialPadContextMenu_dialled"
value={this.state.value}
autoFocus={true}
onChange={this.onChange}
/>
</div>

View file

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

View file

@ -76,7 +76,8 @@ const WidgetContextMenu: React.FC<IProps> = ({
onFinished();
};
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 classNames from "classnames";
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 { _t } from '../../../languageHandler';
@ -44,9 +43,8 @@ import EntityTile from "../rooms/EntityTile";
import BaseAvatar from "../avatars/BaseAvatar";
interface IProps extends IDialogProps {
matrixClient: MatrixClient;
space: Room;
onCreateRoomClick(cli: MatrixClient, space: Room): void;
onCreateRoomClick(space: Room): void;
}
const Entry = ({ room, checked, onChange }) => {
@ -211,10 +209,16 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
function overflowTile(overflowCount, totalCount) {
const text = _t("and %(count)s others...", { count: overflowCount });
return (
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
<BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} />
} name={text} presenceState="online" suppressOnHover={true}
onClick={() => setTruncateAt(totalCount)} />
<EntityTile
className="mx_EntityTile_ellipsis"
avatarJsx={
<BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} />
}
name={text}
presenceState="online"
suppressOnHover={true}
onClick={() => setTruncateAt(totalCount)}
/>
);
}
@ -295,7 +299,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
</div>;
};
const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => {
const AddExistingToSpaceDialog: React.FC<IProps> = ({ space, onCreateRoomClick, onFinished }) => {
const [selectedSpace, setSelectedSpace] = useState(space);
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
@ -344,13 +348,13 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
onFinished={onFinished}
fixedWidth={false}
>
<MatrixClientContext.Provider value={cli}>
<MatrixClientContext.Provider value={space.client}>
<AddExistingToSpace
space={space}
onFinished={onFinished}
footerPrompt={<>
<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") }
</AccessibleButton>
</>}

View file

@ -18,14 +18,12 @@ limitations under the License.
*/
import React, { createRef } from 'react';
import PropTypes from 'prop-types';
import { sleep } from "matrix-js-sdk/src/utils";
import { _t, _td } from '../../../languageHandler';
import * as sdk from '../../../index';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import dis from '../../../dispatcher/dispatcher';
import { addressTypes, getAddressType } from '../../../UserAddress';
import { AddressType, addressTypes, getAddressType, IUserAddress } from '../../../UserAddress';
import GroupStore from '../../../stores/GroupStore';
import * as Email from '../../../email';
import IdentityAuthClient from '../../../IdentityAuthClient';
@ -34,6 +32,10 @@ import { abbreviateUrl } from '../../../utils/UrlUtils';
import { Key } from "../../../Keyboard";
import { Action } from "../../../dispatcher/actions";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import AddressSelector from '../elements/AddressSelector';
import AddressTile from '../elements/AddressTile';
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
const TRUNCATE_QUERY_LIST = 40;
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
@ -44,29 +46,64 @@ const addressTypeName = {
'email': _td("email address"),
};
@replaceableComponent("views.dialogs.AddressPickerDialog")
export default class AddressPickerDialog extends React.Component {
static propTypes = {
title: PropTypes.string.isRequired,
description: PropTypes.node,
// Extra node inserted after picker input, dropdown and errors
extraNode: PropTypes.node,
value: PropTypes.string,
placeholder: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
roomId: PropTypes.string,
button: PropTypes.string,
focus: PropTypes.bool,
validAddressTypes: PropTypes.arrayOf(PropTypes.oneOf(addressTypes)),
onFinished: PropTypes.func.isRequired,
groupId: PropTypes.string,
// The type of entity to search for. Default: 'user'.
pickerType: PropTypes.oneOf(['user', 'room']),
// Whether the current user should be included in the addresses returned. Only
// applicable when pickerType is `user`. Default: false.
includeSelf: PropTypes.bool,
};
interface IResult {
user_id: string; // eslint-disable-line camelcase
room_id?: string; // eslint-disable-line camelcase
name?: string;
display_name?: string; // eslint-disable-line camelcase
avatar_url?: string;// eslint-disable-line camelcase
}
static defaultProps = {
interface IProps {
title: string;
description?: JSX.Element;
// Extra node inserted after picker input, dropdown and errors
extraNode?: JSX.Element;
value?: string;
placeholder?: ((validAddressTypes: any) => string) | string;
roomId?: string;
button?: string;
focus?: boolean;
validAddressTypes?: AddressType[];
onFinished: (success: boolean, list?: IUserAddress[]) => void;
groupId?: string;
// The type of entity to search for. Default: 'user'.
pickerType?: 'user' | 'room';
// Whether the current user should be included in the addresses returned. Only
// applicable when pickerType is `user`. Default: false.
includeSelf?: boolean;
}
interface IState {
// Whether to show an error message because of an invalid address
invalidAddressError: boolean;
// List of UserAddressType objects representing
// the list of addresses we're going to invite
selectedList: IUserAddress[];
// Whether a search is ongoing
busy: boolean;
// An error message generated during the user directory search
searchError: string;
// Whether the server supports the user_directory API
serverSupportsUserDirectory: boolean;
// The query being searched for
query: string;
// List of UserAddressType objects representing the set of
// auto-completion results for the current search query.
suggestedList: IUserAddress[];
// List of address types initialised from props, but may change while the
// dialog is open and represents the supported list of address types at this time.
validAddressTypes: AddressType[];
}
@replaceableComponent("views.dialogs.AddressPickerDialog")
export default class AddressPickerDialog extends React.Component<IProps, IState> {
private textinput = createRef<HTMLTextAreaElement>();
private addressSelector = createRef<AddressSelector>();
private queryChangedDebouncer: number;
private cancelThreepidLookup: () => void;
static defaultProps: Partial<IProps> = {
value: "",
focus: true,
validAddressTypes: addressTypes,
@ -74,36 +111,23 @@ export default class AddressPickerDialog extends React.Component {
includeSelf: false,
};
constructor(props) {
constructor(props: IProps) {
super(props);
this._textinput = createRef();
let validAddressTypes = this.props.validAddressTypes;
// Remove email from validAddressTypes if no IS is configured. It may be added at a later stage by the user
if (!MatrixClientPeg.get().getIdentityServerUrl() && validAddressTypes.includes("email")) {
validAddressTypes = validAddressTypes.filter(type => type !== "email");
if (!MatrixClientPeg.get().getIdentityServerUrl() && validAddressTypes.includes(AddressType.Email)) {
validAddressTypes = validAddressTypes.filter(type => type !== AddressType.Email);
}
this.state = {
// Whether to show an error message because of an invalid address
invalidAddressError: false,
// List of UserAddressType objects representing
// the list of addresses we're going to invite
selectedList: [],
// Whether a search is ongoing
busy: false,
// An error message generated during the user directory search
searchError: null,
// Whether the server supports the user_directory API
serverSupportsUserDirectory: true,
// The query being searched for
query: "",
// List of UserAddressType objects representing the set of
// auto-completion results for the current search query.
suggestedList: [],
// List of address types initialised from props, but may change while the
// dialog is open and represents the supported list of address types at this time.
validAddressTypes,
};
}
@ -111,11 +135,11 @@ export default class AddressPickerDialog extends React.Component {
componentDidMount() {
if (this.props.focus) {
// Set the cursor at the end of the text input
this._textinput.current.value = this.props.value;
this.textinput.current.value = this.props.value;
}
}
getPlaceholder() {
private getPlaceholder(): string {
const { placeholder } = this.props;
if (typeof placeholder === "string") {
return placeholder;
@ -124,23 +148,23 @@ export default class AddressPickerDialog extends React.Component {
return placeholder(this.state.validAddressTypes);
}
onButtonClick = () => {
private onButtonClick = (): void => {
let selectedList = this.state.selectedList.slice();
// Check the text input field to see if user has an unconverted address
// If there is and it's valid add it to the local selectedList
if (this._textinput.current.value !== '') {
selectedList = this._addAddressesToList([this._textinput.current.value]);
if (this.textinput.current.value !== '') {
selectedList = this.addAddressesToList([this.textinput.current.value]);
if (selectedList === null) return;
}
this.props.onFinished(true, selectedList);
};
onCancel = () => {
private onCancel = (): void => {
this.props.onFinished(false);
};
onKeyDown = e => {
const textInput = this._textinput.current ? this._textinput.current.value : undefined;
private onKeyDown = (e: React.KeyboardEvent): void => {
const textInput = this.textinput.current ? this.textinput.current.value : undefined;
if (e.key === Key.ESCAPE) {
e.stopPropagation();
@ -149,15 +173,15 @@ export default class AddressPickerDialog extends React.Component {
} else if (e.key === Key.ARROW_UP) {
e.stopPropagation();
e.preventDefault();
if (this.addressSelector) this.addressSelector.moveSelectionUp();
if (this.addressSelector.current) this.addressSelector.current.moveSelectionUp();
} else if (e.key === Key.ARROW_DOWN) {
e.stopPropagation();
e.preventDefault();
if (this.addressSelector) this.addressSelector.moveSelectionDown();
if (this.addressSelector.current) this.addressSelector.current.moveSelectionDown();
} else if (this.state.suggestedList.length > 0 && [Key.COMMA, Key.ENTER, Key.TAB].includes(e.key)) {
e.stopPropagation();
e.preventDefault();
if (this.addressSelector) this.addressSelector.chooseSelection();
if (this.addressSelector.current) this.addressSelector.current.chooseSelection();
} else if (textInput.length === 0 && this.state.selectedList.length && e.key === Key.BACKSPACE) {
e.stopPropagation();
e.preventDefault();
@ -169,17 +193,17 @@ export default class AddressPickerDialog extends React.Component {
// if there's nothing in the input box, submit the form
this.onButtonClick();
} else {
this._addAddressesToList([textInput]);
this.addAddressesToList([textInput]);
}
} else if (textInput && (e.key === Key.COMMA || e.key === Key.TAB)) {
e.stopPropagation();
e.preventDefault();
this._addAddressesToList([textInput]);
this.addAddressesToList([textInput]);
}
};
onQueryChanged = ev => {
const query = ev.target.value;
private onQueryChanged = (ev: React.ChangeEvent): void => {
const query = (ev.target as HTMLTextAreaElement).value;
if (this.queryChangedDebouncer) {
clearTimeout(this.queryChangedDebouncer);
}
@ -188,17 +212,17 @@ export default class AddressPickerDialog extends React.Component {
this.queryChangedDebouncer = setTimeout(() => {
if (this.props.pickerType === 'user') {
if (this.props.groupId) {
this._doNaiveGroupSearch(query);
this.doNaiveGroupSearch(query);
} else if (this.state.serverSupportsUserDirectory) {
this._doUserDirectorySearch(query);
this.doUserDirectorySearch(query);
} else {
this._doLocalSearch(query);
this.doLocalSearch(query);
}
} else if (this.props.pickerType === 'room') {
if (this.props.groupId) {
this._doNaiveGroupRoomSearch(query);
this.doNaiveGroupRoomSearch(query);
} else {
this._doRoomSearch(query);
this.doRoomSearch(query);
}
} else {
console.error('Unknown pickerType', this.props.pickerType);
@ -213,7 +237,7 @@ export default class AddressPickerDialog extends React.Component {
}
};
onDismissed = index => () => {
private onDismissed = (index: number) => () => {
const selectedList = this.state.selectedList.slice();
selectedList.splice(index, 1);
this.setState({
@ -221,25 +245,21 @@ export default class AddressPickerDialog extends React.Component {
suggestedList: [],
query: "",
});
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
if (this.cancelThreepidLookup) this.cancelThreepidLookup();
};
onClick = index => () => {
this.onSelected(index);
};
onSelected = index => {
private onSelected = (index: number): void => {
const selectedList = this.state.selectedList.slice();
selectedList.push(this._getFilteredSuggestions()[index]);
selectedList.push(this.getFilteredSuggestions()[index]);
this.setState({
selectedList,
suggestedList: [],
query: "",
});
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
if (this.cancelThreepidLookup) this.cancelThreepidLookup();
};
_doNaiveGroupSearch(query) {
private doNaiveGroupSearch(query: string): void {
const lowerCaseQuery = query.toLowerCase();
this.setState({
busy: true,
@ -260,7 +280,7 @@ export default class AddressPickerDialog extends React.Component {
display_name: u.displayname,
});
});
this._processResults(results, query);
this.processResults(results, query);
}).catch((err) => {
console.error('Error whilst searching group rooms: ', err);
this.setState({
@ -273,7 +293,7 @@ export default class AddressPickerDialog extends React.Component {
});
}
_doNaiveGroupRoomSearch(query) {
private doNaiveGroupRoomSearch(query: string): void {
const lowerCaseQuery = query.toLowerCase();
const results = [];
GroupStore.getGroupRooms(this.props.groupId).forEach((r) => {
@ -289,13 +309,13 @@ export default class AddressPickerDialog extends React.Component {
name: r.name || r.canonical_alias,
});
});
this._processResults(results, query);
this.processResults(results, query);
this.setState({
busy: false,
});
}
_doRoomSearch(query) {
private doRoomSearch(query: string): void {
const lowerCaseQuery = query.toLowerCase();
const rooms = MatrixClientPeg.get().getRooms();
const results = [];
@ -346,13 +366,13 @@ export default class AddressPickerDialog extends React.Component {
return a.rank - b.rank;
});
this._processResults(sortedResults, query);
this.processResults(sortedResults, query);
this.setState({
busy: false,
});
}
_doUserDirectorySearch(query) {
private doUserDirectorySearch(query: string): void {
this.setState({
busy: true,
query,
@ -366,7 +386,7 @@ export default class AddressPickerDialog extends React.Component {
if (this.state.query !== query) {
return;
}
this._processResults(resp.results, query);
this.processResults(resp.results, query);
}).catch((err) => {
console.error('Error whilst searching user directory: ', err);
this.setState({
@ -377,7 +397,7 @@ export default class AddressPickerDialog extends React.Component {
serverSupportsUserDirectory: false,
});
// Do a local search immediately
this._doLocalSearch(query);
this.doLocalSearch(query);
}
}).then(() => {
this.setState({
@ -386,7 +406,7 @@ export default class AddressPickerDialog extends React.Component {
});
}
_doLocalSearch(query) {
private doLocalSearch(query: string): void {
this.setState({
query,
searchError: null,
@ -407,10 +427,10 @@ export default class AddressPickerDialog extends React.Component {
avatar_url: user.avatarUrl,
});
});
this._processResults(results, query);
this.processResults(results, query);
}
_processResults(results, query) {
private processResults(results: IResult[], query: string): void {
const suggestedList = [];
results.forEach((result) => {
if (result.room_id) {
@ -465,27 +485,27 @@ export default class AddressPickerDialog extends React.Component {
address: query,
isKnown: false,
});
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
if (this.cancelThreepidLookup) this.cancelThreepidLookup();
if (addrType === 'email') {
this._lookupThreepid(addrType, query);
this.lookupThreepid(addrType, query);
}
}
this.setState({
suggestedList,
invalidAddressError: false,
}, () => {
if (this.addressSelector) this.addressSelector.moveSelectionTop();
if (this.addressSelector.current) this.addressSelector.current.moveSelectionTop();
});
}
_addAddressesToList(addressTexts) {
private addAddressesToList(addressTexts: string[]): IUserAddress[] {
const selectedList = this.state.selectedList.slice();
let hasError = false;
addressTexts.forEach((addressText) => {
addressText = addressText.trim();
const addrType = getAddressType(addressText);
const addrObj = {
const addrObj: IUserAddress = {
addressType: addrType,
address: addressText,
isKnown: false,
@ -504,7 +524,6 @@ export default class AddressPickerDialog extends React.Component {
const room = MatrixClientPeg.get().getRoom(addrObj.address);
if (room) {
addrObj.displayName = room.name;
addrObj.avatarMxc = room.avatarUrl;
addrObj.isKnown = true;
}
}
@ -518,17 +537,17 @@ export default class AddressPickerDialog extends React.Component {
query: "",
invalidAddressError: hasError ? true : this.state.invalidAddressError,
});
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
if (this.cancelThreepidLookup) this.cancelThreepidLookup();
return hasError ? null : selectedList;
}
async _lookupThreepid(medium, address) {
private async lookupThreepid(medium: AddressType, address: string): Promise<string> {
let cancelled = false;
// Note that we can't safely remove this after we're done
// because we don't know that it's the same one, so we just
// leave it: it's replacing the old one each time so it's
// not like they leak.
this._cancelThreepidLookup = function() {
this.cancelThreepidLookup = function() {
cancelled = true;
};
@ -570,7 +589,7 @@ export default class AddressPickerDialog extends React.Component {
}
}
_getFilteredSuggestions() {
private getFilteredSuggestions(): IUserAddress[] {
// map addressType => set of addresses to avoid O(n*m) operation
const selectedAddresses = {};
this.state.selectedList.forEach(({ address, addressType }) => {
@ -584,15 +603,15 @@ export default class AddressPickerDialog extends React.Component {
});
}
_onPaste = e => {
private onPaste = (e: React.ClipboardEvent): void => {
// Prevent the text being pasted into the textarea
e.preventDefault();
const text = e.clipboardData.getData("text");
// Process it as a list of addresses to add instead
this._addAddressesToList(text.split(/[\s,]+/));
this.addAddressesToList(text.split(/[\s,]+/));
};
onUseDefaultIdentityServerClick = e => {
private onUseDefaultIdentityServerClick = (e: React.MouseEvent): void => {
e.preventDefault();
// Update the IS in account data. Actually using it may trigger terms.
@ -601,22 +620,17 @@ export default class AddressPickerDialog extends React.Component {
// Add email as a valid address type.
const { validAddressTypes } = this.state;
validAddressTypes.push('email');
validAddressTypes.push(AddressType.Email);
this.setState({ validAddressTypes });
};
onManageSettingsClick = e => {
private onManageSettingsClick = (e: React.MouseEvent): void => {
e.preventDefault();
dis.fire(Action.ViewUserSettings);
this.onCancel();
};
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AddressSelector = sdk.getComponent("elements.AddressSelector");
this.scrollElement = null;
let inputLabel;
if (this.props.description) {
inputLabel = <div className="mx_AddressPickerDialog_label">
@ -627,7 +641,6 @@ export default class AddressPickerDialog extends React.Component {
const query = [];
// create the invite list
if (this.state.selectedList.length > 0) {
const AddressTile = sdk.getComponent("elements.AddressTile");
for (let i = 0; i < this.state.selectedList.length; i++) {
query.push(
<AddressTile
@ -644,19 +657,19 @@ export default class AddressPickerDialog extends React.Component {
query.push(
<textarea
key={this.state.selectedList.length}
onPaste={this._onPaste}
rows="1"
onPaste={this.onPaste}
rows={1}
id="textinput"
ref={this._textinput}
ref={this.textinput}
className="mx_AddressPickerDialog_input"
onChange={this.onQueryChanged}
placeholder={this.getPlaceholder()}
defaultValue={this.props.value}
autoFocus={this.props.focus}>
</textarea>,
autoFocus={this.props.focus}
/>,
);
const filteredSuggestedList = this._getFilteredSuggestions();
const filteredSuggestedList = this.getFilteredSuggestions();
let error;
let addressSelector;
@ -675,7 +688,7 @@ export default class AddressPickerDialog extends React.Component {
error = <div className="mx_AddressPickerDialog_error">{ _t("No results") }</div>;
} else {
addressSelector = (
<AddressSelector ref={(ref) => {this.addressSelector = ref;}}
<AddressSelector ref={this.addressSelector}
addressList={filteredSuggestedList}
showAddress={this.props.pickerType === 'user'}
onSelected={this.onSelected}
@ -686,8 +699,8 @@ export default class AddressPickerDialog extends React.Component {
let identityServer;
// If picker cannot currently accept e-mail but should be able to
if (this.props.pickerType === 'user' && !this.state.validAddressTypes.includes('email')
&& this.props.validAddressTypes.includes('email')) {
if (this.props.pickerType === 'user' && !this.state.validAddressTypes.includes(AddressType.Email)
&& this.props.validAddressTypes.includes(AddressType.Email)) {
const defaultIdentityServerUrl = getDefaultIdentityServerUrl();
if (defaultIdentityServerUrl) {
identityServer = <div className="mx_AddressPickerDialog_identityServer">{ _t(
@ -714,8 +727,12 @@ export default class AddressPickerDialog extends React.Component {
}
return (
<BaseDialog className="mx_AddressPickerDialog" onKeyDown={this.onKeyDown}
onFinished={this.props.onFinished} title={this.props.title}>
<BaseDialog
className="mx_AddressPickerDialog"
onKeyDown={this.onKeyDown}
onFinished={this.props.onFinished}
title={this.props.title}
>
{ inputLabel }
<div className="mx_Dialog_content">
<div className="mx_AddressPickerDialog_inputContainer">{ query }</div>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -17,6 +17,7 @@ limitations under the License.
import React, { ChangeEvent, createRef, KeyboardEvent, SyntheticEvent } from "react";
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 withValidation, { IFieldState } from '../elements/Validation';
@ -31,7 +32,8 @@ import RoomAliasField from "../elements/RoomAliasField";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import DialogButtons from "../elements/DialogButtons";
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 {
defaultPublic?: boolean;
@ -41,7 +43,7 @@ interface IProps {
}
interface IState {
isPublic: boolean;
joinRule: JoinRule;
isEncrypted: boolean;
name: string;
topic: string;
@ -54,15 +56,25 @@ interface IState {
@replaceableComponent("views.dialogs.CreateRoomDialog")
export default class CreateRoomDialog extends React.Component<IProps, IState> {
private readonly supportsRestricted: boolean;
private nameField = createRef<Field>();
private aliasField = createRef<RoomAliasField>();
constructor(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();
this.state = {
isPublic: this.props.defaultPublic || false,
joinRule,
isEncrypted: privateShouldBeEncrypted(),
name: this.props.defaultName || "",
topic: "",
@ -81,13 +93,18 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
const opts: IOpts = {};
const createOpts: IOpts["createOpts"] = opts.createOpts = {};
createOpts.name = this.state.name;
if (this.state.isPublic) {
if (this.state.joinRule === JoinRule.Public) {
createOpts.visibility = Visibility.Public;
createOpts.preset = Preset.PublicChat;
opts.guestAccess = false;
const { alias } = this.state;
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) {
createOpts.topic = this.state.topic;
}
@ -95,22 +112,13 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
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()) {
opts.associatedWithCommunity = CommunityPrototypeStore.instance.getSelectedCommunityId();
}
if (this.props.parentSpace) {
if (this.props.parentSpace && this.state.joinRule === JoinRule.Restricted) {
opts.parentSpace = this.props.parentSpace;
opts.joinRule = JoinRule.Restricted;
}
return opts;
@ -172,8 +180,8 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
this.setState({ topic: ev.target.value });
};
private onPublicChange = (isPublic: boolean) => {
this.setState({ isPublic });
private onJoinRuleChange = (joinRule: JoinRule) => {
this.setState({ joinRule });
};
private onEncryptedChange = (isEncrypted: boolean) => {
@ -210,7 +218,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
render() {
let aliasField;
if (this.state.isPublic) {
if (this.state.joinRule === JoinRule.Public) {
const domain = MatrixClientPeg.get().getDomain();
aliasField = (
<div className="mx_CreateRoomDialog_aliasContainer">
@ -224,19 +232,46 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
);
}
let publicPrivateLabel = <p>{ _t(
"Private rooms can be found and joined by invitation only. Public rooms can be " +
"found and joined by anyone.",
) }</p>;
let publicPrivateLabel: JSX.Element;
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
publicPrivateLabel = <p>{ _t(
"Private rooms can be found and joined by invitation only. Public rooms can be " +
"found and joined by anyone in this community.",
) }</p>;
publicPrivateLabel = <p>
{ _t(
"Private rooms can be found and joined by invitation only. Public rooms can be " +
"found and joined by anyone in this community.",
) }
</p>;
} else if (this.state.joinRule === JoinRule.Restricted) {
publicPrivateLabel = <p>
{ _t(
"Everyone in <SpaceName/> will be able to find and join this room.", {}, {
SpaceName: () => <b>{ this.props.parentSpace.name }</b>,
},
) }
&nbsp;
{ _t("You can change this at any time from room settings.") }
</p>;
} else if (this.state.joinRule === JoinRule.Public) {
publicPrivateLabel = <p>
{ _t(
"Anyone will be able to find and join this room, not just members of <SpaceName/>.", {}, {
SpaceName: () => <b>{ this.props.parentSpace.name }</b>,
},
) }
&nbsp;
{ _t("You can change this at any time from room settings.") }
</p>;
} else if (this.state.joinRule === JoinRule.Invite) {
publicPrivateLabel = <p>
{ _t(
"Only people invited will be able to find and join this room.",
) }
&nbsp;
{ _t("You can change this at any time from room settings.") }
</p>;
}
let e2eeSection;
if (!this.state.isPublic) {
if (this.state.joinRule !== JoinRule.Public) {
let microcopy;
if (privateShouldBeEncrypted()) {
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()) {
const name = CommunityPrototypeStore.instance.getSelectedCommunityName();
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 (
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished}
title={title}
>
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished} title={title}>
<form onSubmit={this.onOk} onKeyDown={this.onKeyDown}>
<div className="mx_Dialog_content">
<Field
@ -298,11 +349,18 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
value={this.state.topic}
className="mx_CreateRoomDialog_topic"
/>
<LabelledToggleSwitch
label={_t("Make this room public")}
onChange={this.onPublicChange}
value={this.state.isPublic}
/>
<Dropdown
id="mx_CreateRoomDialog_typeDropdown"
className="mx_CreateRoomDialog_typeDropdown"
onOptionChange={this.onJoinRuleChange}
menuWidth={448}
value={this.state.joinRule}
label={_t("Room visibility")}
>
{ options }
</Dropdown>
{ publicPrivateLabel }
{ e2eeSection }
{ aliasField }

View file

@ -72,7 +72,7 @@ const CryptoStoreTooNewDialog: React.FC<IProps> = (props: IProps) => {
hasCancel={false}
onPrimaryButtonClick={props.onFinished}
>
<button onClick={_onLogoutClicked} >
<button onClick={_onLogoutClicked}>
{ _t('Sign out') }
</button>
</DialogButtons>

View file

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

View file

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

View file

@ -106,12 +106,12 @@ const Entry: React.FC<IEntryProps> = ({ room, event, matrixClient: cli, onFinish
className = "mx_ForwardList_sending";
disabled = true;
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) {
className = "mx_ForwardList_sent";
disabled = true;
title = _t("Sent");
icon = <div className="mx_ForwardList_sendIcon" aria-label={title}></div>;
icon = <div className="mx_ForwardList_sendIcon" aria-label={title} />;
} else {
className = "mx_ForwardList_sendFailed";
disabled = true;
@ -204,10 +204,16 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
function overflowTile(overflowCount, totalCount) {
const text = _t("and %(count)s others...", { count: overflowCount });
return (
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
<BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} />
} name={text} presenceState="online" suppressOnHover={true}
onClick={() => setTruncateAt(totalCount)} />
<EntityTile
className="mx_EntityTile_ellipsis"
avatarJsx={
<BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} />
}
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)
: null;
profile = <div className="mx_IncomingSasDialog_opponentProfile">
<BaseAvatar name={oppProfile.displayname}
<BaseAvatar
name={oppProfile.displayname}
idName={this.props.verifier.userId}
url={url}
width={48} height={48} resizeMethod='crop'
width={48}
height={48}
resizeMethod='crop'
/>
<h2>{ oppProfile.displayname }</h2>
</div>;
} else if (this.state.opponentProfileError) {
profile = <div>
<BaseAvatar name={this.props.verifier.userId.slice(1)}
<BaseAvatar
name={this.props.verifier.userId.slice(1)}
idName={this.props.verifier.userId}
width={48} height={48}
width={48}
height={48}
/>
<h2>{ this.props.verifier.userId }</h2>
</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 2015 - 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.
@ -16,31 +15,31 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import React, { ReactNode, KeyboardEvent } from 'react';
import classNames from "classnames";
export default class InfoDialog extends React.Component {
static propTypes = {
className: PropTypes.string,
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,
};
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import { IDialogProps } from "./IDialogProps";
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 = {
title: '',
description: '',
hasCloseButton: false,
};
onFinished = () => {
private 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')}
onPrimaryButtonClick={this.onFinished}
hasCancel={false}
>
</DialogButtons> }
/> }
</BaseDialog>
);
}

View file

@ -196,7 +196,9 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
? <img
className='mx_InviteDialog_userTile_avatar mx_InviteDialog_userTile_threepidAvatar'
src={require("../../../../res/img/icon-email-pill-avatar.svg")}
width={avatarSize} height={avatarSize} />
width={avatarSize}
height={avatarSize}
/>
: <BaseAvatar
className='mx_InviteDialog_userTile_avatar'
url={this.props.member.getMxcAvatarUrl()
@ -214,8 +216,11 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
className='mx_InviteDialog_userTile_remove'
onClick={this.onRemove}
>
<img src={require("../../../../res/img/icon-pill-remove.svg")}
alt={_t('Remove')} width={8} height={8}
<img
src={require("../../../../res/img/icon-pill-remove.svg")}
alt={_t('Remove')}
width={8}
height={8}
/>
</AccessibleButton>
);
@ -297,7 +302,9 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
const avatar = (this.props.member as ThreepidMember).isEmail
? <img
src={require("../../../../res/img/icon-email-pill-avatar.svg")}
width={avatarSize} height={avatarSize} />
width={avatarSize}
height={avatarSize}
/>
: <BaseAvatar
url={this.props.member.getMxcAvatarUrl()
? mediaFromMxc(this.props.member.getMxcAvatarUrl()).getSquareThumbnailHttp(avatarSize)
@ -1458,7 +1465,8 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
<p className='mx_InviteDialog_helpText'>
<img
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.") }
</p>;
}
@ -1534,14 +1542,18 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
// Only show the backspace button if the field has content
let dialPadField;
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}
autoFocus={true}
onChange={this.onDialChange}
postfixComponent={backspaceButton}
/>;
} else {
dialPadField = <Field className="mx_InviteDialog_dialPadField" id="dialpad_number"
dialPadField = <Field
className="mx_InviteDialog_dialPadField"
id="dialpad_number"
value={this.state.dialPadValue}
autoFocus={true}
onChange={this.onDialChange}
@ -1552,14 +1564,19 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
<form onSubmit={this.onDialFormSubmit}>
{ dialPadField }
</form>
<Dialpad hasDial={false}
onDigitPress={this.onDigitPress} onDeletePress={this.onDeletePress}
<Dialpad
hasDial={false}
onDigitPress={this.onDigitPress}
onDeletePress={this.onDeletePress}
/>
</div>;
tabs.push(new Tab(TabId.DialPad, _td("Dial pad"), 'mx_InviteDialog_dialPadIcon', dialPadSection));
dialogContent = <React.Fragment>
<TabbedView tabs={tabs} initialTabId={this.state.currentTabId}
tabLocation={TabLocation.TOP} onChange={this.onTabChange}
<TabbedView
tabs={tabs}
initialTabId={this.state.currentTabId}
tabLocation={TabLocation.TOP}
onChange={this.onTabChange}
/>
{ consultConnectSection }
</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");
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 PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { Room } from "matrix-js-sdk/src/models/room";
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
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")
export default class RoomUpgradeDialog extends React.Component {
static propTypes = {
room: PropTypes.object.isRequired,
onFinished: PropTypes.func.isRequired,
};
export default class RoomUpgradeDialog extends React.Component<IProps, IState> {
private targetVersion: string;
state = {
busy: true,
@ -35,20 +45,19 @@ export default class RoomUpgradeDialog extends React.Component {
async componentDidMount() {
const recommended = await this.props.room.getRecommendedVersion();
this._targetVersion = recommended.version;
this.targetVersion = recommended.version;
this.setState({ busy: false });
}
_onCancelClick = () => {
private onCancelClick = (): void => {
this.props.onFinished(false);
};
_onUpgradeClick = () => {
private onUpgradeClick = (): void => {
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);
}).catch((err) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to upgrade room', '', ErrorDialog, {
title: _t("Failed to upgrade room"),
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() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const Spinner = sdk.getComponent('views.elements.Spinner');
let buttons;
if (this.state.busy) {
buttons = <Spinner />;
} else {
buttons = <DialogButtons
primaryButton={_t(
'Upgrade this room to version %(version)s',
{ version: this._targetVersion },
)}
primaryButton={_t('Upgrade this room to version %(version)s', { version: this.targetVersion })}
primaryButtonClass="danger"
hasCancel={true}
onPrimaryButtonClick={this._onUpgradeClick}
focus={this.props.focus}
onCancel={this._onCancelClick}
onPrimaryButtonClick={this.onUpgradeClick}
onCancel={this.onCancelClick}
/>;
}
return (
<BaseDialog className="mx_RoomUpgradeDialog"
<BaseDialog
className="mx_RoomUpgradeDialog"
onFinished={this.props.onFinished}
title={_t("Upgrade Room Version")}
contentId='mx_Dialog_content'
@ -97,8 +99,10 @@ export default class RoomUpgradeDialog extends React.Component {
<ol>
<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("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("Put a link back to the old room at the start of the new room so people can see old messages") }</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("Put a link back to the old room at the start of the new room " +
"so people can see old messages") }</li>
</ol>
{ buttons }
</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");
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.
*/
import React from 'react';
import PropTypes from 'prop-types';
import React, { ReactNode } from 'react';
import { EventType } from 'matrix-js-sdk/src/@types/event';
import { JoinRule } from 'matrix-js-sdk/src/@types/partials';
import { _t } from "../../../languageHandler";
import SdkConfig from "../../../SdkConfig";
import * as sdk from "../../../index";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import Modal from "../../../Modal";
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")
export default class RoomUpgradeWarningDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
roomId: PropTypes.string.isRequired,
targetVersion: PropTypes.string.isRequired,
};
export default class RoomUpgradeWarningDialog extends React.Component<IProps, IState> {
private readonly isPrivate: boolean;
private readonly currentVersion: string;
constructor(props) {
super(props);
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
const joinRules = room ? room.currentState.getStateEvents("m.room.join_rules", "") : null;
const isPrivate = joinRules ? joinRules.getContent()['join_rule'] !== 'public' : true;
const joinRules = room?.currentState.getStateEvents(EventType.RoomJoinRules, "");
this.isPrivate = joinRules?.getContent()['join_rule'] !== JoinRule.Public ?? true;
this.currentVersion = room?.getVersion() || "1";
this.state = {
currentVersion: room ? room.getVersion() : "1",
isPrivate,
inviteUsersToNewRoom: true,
};
}
_onContinue = () => {
this.props.onFinished({ continue: true, invite: this.state.isPrivate && this.state.inviteUsersToNewRoom });
private onContinue = () => {
this.props.onFinished({ continue: true, invite: this.isPrivate && this.state.inviteUsersToNewRoom });
};
_onCancel = () => {
private onCancel = () => {
this.props.onFinished({ continue: false, invite: false });
};
_onInviteUsersToggle = (newVal) => {
this.setState({ inviteUsersToNewRoom: newVal });
private onInviteUsersToggle = (inviteUsersToNewRoom: boolean) => {
this.setState({ inviteUsersToNewRoom });
};
_openBugReportDialog = (e) => {
private openBugReportDialog = (e) => {
e.preventDefault();
e.stopPropagation();
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {});
};
render() {
const brand = SdkConfig.get().brand;
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
let inviteToggle = null;
if (this.state.isPrivate) {
if (this.isPrivate) {
inviteToggle = (
<LabelledToggleSwitch
value={this.state.inviteUsersToNewRoom}
onChange={this._onInviteUsersToggle}
label={_t("Automatically invite users")} />
onChange={this.onInviteUsersToggle}
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 = (
<p>
@ -101,7 +110,7 @@ export default class RoomUpgradeWarningDialog extends React.Component {
},
{
"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>
<p>
{ _t(
{ this.props.description || _t(
"Upgrading a room is an advanced action and is usually recommended when a room " +
"is unstable due to bugs, missing features or security vulnerabilities.",
) }
</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 }
<p>
{ _t(
"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>,
},
) }
@ -139,9 +156,9 @@ export default class RoomUpgradeWarningDialog extends React.Component {
</div>
<DialogButtons
primaryButton={_t("Upgrade")}
onPrimaryButtonClick={this._onContinue}
onPrimaryButtonClick={this.onContinue}
cancelButton={_t("Cancel")}
onCancel={this._onCancel}
onCancel={this.onCancel}
/>
</BaseDialog>
);

View file

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

View file

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

View file

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

View file

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

View file

@ -15,56 +15,62 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import AccessibleButton from './AccessibleButton';
import dis from '../../../dispatcher/dispatcher';
import * as sdk from '../../../index';
import Analytics from '../../../Analytics';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Tooltip from './Tooltip';
interface IProps {
size?: string;
tooltip?: boolean;
action: string;
mouseOverAction?: string;
label: string;
iconPath?: string;
className?: string;
children?: JSX.Element;
}
interface IState {
showTooltip: boolean;
}
@replaceableComponent("views.elements.ActionButton")
export default class ActionButton extends React.Component {
static propTypes = {
size: PropTypes.string,
tooltip: PropTypes.bool,
action: PropTypes.string.isRequired,
mouseOverAction: PropTypes.string,
label: PropTypes.string.isRequired,
iconPath: PropTypes.string,
className: PropTypes.string,
children: PropTypes.node,
};
static defaultProps = {
export default class ActionButton extends React.Component<IProps, IState> {
static defaultProps: Partial<IProps> = {
size: "25",
tooltip: false,
};
state = {
showTooltip: false,
};
constructor(props: IProps) {
super(props);
_onClick = (ev) => {
this.state = {
showTooltip: false,
};
}
private onClick = (ev: React.MouseEvent): void => {
ev.stopPropagation();
Analytics.trackEvent('Action Button', 'click', this.props.action);
dis.dispatch({ action: this.props.action });
};
_onMouseEnter = () => {
private onMouseEnter = (): void => {
if (this.props.tooltip) this.setState({ showTooltip: true });
if (this.props.mouseOverAction) {
dis.dispatch({ action: this.props.mouseOverAction });
}
};
_onMouseLeave = () => {
private onMouseLeave = (): void => {
this.setState({ showTooltip: false });
};
render() {
let tooltip;
if (this.state.showTooltip) {
const Tooltip = sdk.getComponent("elements.Tooltip");
tooltip = <Tooltip className="mx_RoleButton_tooltip" label={this.props.label} />;
}
@ -80,9 +86,9 @@ export default class ActionButton extends React.Component {
return (
<AccessibleButton
className={classNames.join(" ")}
onClick={this._onClick}
onMouseEnter={this._onMouseEnter}
onMouseLeave={this._onMouseLeave}
onClick={this.onClick}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
aria-label={this.props.label}
>
{ icon }

View file

@ -15,30 +15,37 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import React, { createRef } from 'react';
import classNames from 'classnames';
import { UserAddressType } from '../../../UserAddress';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { IUserAddress } from '../../../UserAddress';
import AddressTile from './AddressTile';
interface IProps {
onSelected: (index: number) => void;
// List of the addresses to display
addressList: IUserAddress[];
// Whether to show the address on the address tiles
showAddress?: boolean;
truncateAt: number;
selected?: number;
// Element to put as a header on top of the list
header?: JSX.Element;
}
interface IState {
selected: number;
hover: boolean;
}
@replaceableComponent("views.elements.AddressSelector")
export default class AddressSelector extends React.Component {
static propTypes = {
onSelected: PropTypes.func.isRequired,
export default class AddressSelector extends React.Component<IProps, IState> {
private scrollElement = createRef<HTMLDivElement>();
private addressListElement = createRef<HTMLDivElement>();
// List of the addresses to display
addressList: PropTypes.arrayOf(UserAddressType).isRequired,
// Whether to show the address on the address tiles
showAddress: PropTypes.bool,
truncateAt: PropTypes.number.isRequired,
selected: PropTypes.number,
// Element to put as a header on top of the list
header: PropTypes.node,
};
constructor(props) {
constructor(props: IProps) {
super(props);
this.state = {
@ -48,10 +55,10 @@ export default class AddressSelector extends React.Component {
}
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps(props) { // eslint-disable-line camelcase
UNSAFE_componentWillReceiveProps(props: IProps) { // eslint-disable-line
// Make sure the selected item isn't outside the list bounds
const selected = this.state.selected;
const maxSelected = this._maxSelected(props.addressList);
const maxSelected = this.maxSelected(props.addressList);
if (selected > maxSelected) {
this.setState({ selected: maxSelected });
}
@ -60,13 +67,13 @@ export default class AddressSelector extends React.Component {
componentDidUpdate() {
// As the user scrolls with the arrow keys keep the selected item
// at the top of the window.
if (this.scrollElement && this.props.addressList.length > 0 && !this.state.hover) {
const elementHeight = this.addressListElement.getBoundingClientRect().height;
this.scrollElement.scrollTop = (this.state.selected * elementHeight) - elementHeight;
if (this.scrollElement.current && this.props.addressList.length > 0 && !this.state.hover) {
const elementHeight = this.addressListElement.current.getBoundingClientRect().height;
this.scrollElement.current.scrollTop = (this.state.selected * elementHeight) - elementHeight;
}
}
moveSelectionTop = () => {
public moveSelectionTop = (): void => {
if (this.state.selected > 0) {
this.setState({
selected: 0,
@ -75,7 +82,7 @@ export default class AddressSelector extends React.Component {
}
};
moveSelectionUp = () => {
public moveSelectionUp = (): void => {
if (this.state.selected > 0) {
this.setState({
selected: this.state.selected - 1,
@ -84,8 +91,8 @@ export default class AddressSelector extends React.Component {
}
};
moveSelectionDown = () => {
if (this.state.selected < this._maxSelected(this.props.addressList)) {
public moveSelectionDown = (): void => {
if (this.state.selected < this.maxSelected(this.props.addressList)) {
this.setState({
selected: this.state.selected + 1,
hover: false,
@ -93,26 +100,26 @@ export default class AddressSelector extends React.Component {
}
};
chooseSelection = () => {
public chooseSelection = (): void => {
this.selectAddress(this.state.selected);
};
onClick = index => {
private onClick = (index: number): void => {
this.selectAddress(index);
};
onMouseEnter = index => {
private onMouseEnter = (index: number): void => {
this.setState({
selected: index,
hover: true,
});
};
onMouseLeave = () => {
private onMouseLeave = (): void => {
this.setState({ hover: false });
};
selectAddress = index => {
private selectAddress = (index: number): void => {
// Only try to select an address if one exists
if (this.props.addressList.length !== 0) {
this.props.onSelected(index);
@ -120,9 +127,8 @@ export default class AddressSelector extends React.Component {
}
};
createAddressListTiles() {
const AddressTile = sdk.getComponent("elements.AddressTile");
const maxSelected = this._maxSelected(this.props.addressList);
private createAddressListTiles(): JSX.Element[] {
const maxSelected = this.maxSelected(this.props.addressList);
const addressList = [];
// Only create the address elements if there are address
@ -143,14 +149,12 @@ export default class AddressSelector extends React.Component {
onMouseEnter={this.onMouseEnter.bind(this, i)}
onMouseLeave={this.onMouseLeave}
key={this.props.addressList[i].addressType + "/" + this.props.addressList[i].address}
ref={(ref) => { this.addressListElement = ref; }}
ref={this.addressListElement}
>
<AddressTile
address={this.props.addressList[i]}
showAddress={this.props.showAddress}
justified={true}
networkName="vector"
networkUrl={require("../../../../res/img/search-icon-vector.svg")}
/>
</div>,
);
@ -159,7 +163,7 @@ export default class AddressSelector extends React.Component {
return addressList;
}
_maxSelected(list) {
private maxSelected(list: IUserAddress[]): number {
const listSize = list.length === 0 ? 0 : list.length - 1;
const maxSelected = listSize > (this.props.truncateAt - 1) ? (this.props.truncateAt - 1) : listSize;
return maxSelected;
@ -172,7 +176,7 @@ export default class AddressSelector extends React.Component {
});
return (
<div className={classes} ref={(ref) => {this.scrollElement = ref;}}>
<div className={classes} ref={this.scrollElement}>
{ this.props.header }
{ this.createAddressListTiles() }
</div>

View file

@ -16,24 +16,25 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import * as sdk from "../../../index";
import { _t } from '../../../languageHandler';
import { UserAddressType } from '../../../UserAddress';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
import { IUserAddress } from '../../../UserAddress';
import BaseAvatar from '../avatars/BaseAvatar';
import EmailUserIcon from "../../../../res/img/icon-email-user.svg";
interface IProps {
address: IUserAddress;
canDismiss?: boolean;
onDismissed?: () => void;
justified?: boolean;
showAddress?: boolean;
}
@replaceableComponent("views.elements.AddressTile")
export default class AddressTile extends React.Component {
static propTypes = {
address: UserAddressType.isRequired,
canDismiss: PropTypes.bool,
onDismissed: PropTypes.func,
justified: PropTypes.bool,
};
static defaultProps = {
export default class AddressTile extends React.Component<IProps> {
static defaultProps: Partial<IProps> = {
canDismiss: false,
onDismissed: function() {}, // NOP
justified: false,
@ -49,11 +50,9 @@ export default class AddressTile extends React.Component {
if (isMatrixAddress && address.avatarMxc) {
imgUrls.push(mediaFromMxc(address.avatarMxc).getSquareThumbnailHttp(25));
} else if (address.addressType === 'email') {
imgUrls.push(require("../../../../res/img/icon-email-user.svg"));
imgUrls.push(EmailUserIcon);
}
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const nameClasses = classNames({
"mx_AddressTile_name": true,
"mx_AddressTile_justified": this.props.justified,
@ -70,9 +69,10 @@ export default class AddressTile extends React.Component {
info = (
<div className="mx_AddressTile_mx">
<div className={nameClasses}>{ name }</div>
{ this.props.showAddress ?
<div className={idClasses}>{ address.address }</div> :
<div />
{
this.props.showAddress
? <div className={idClasses}>{ address.address }</div>
: <div />
}
</div>
);
@ -122,7 +122,7 @@ export default class AddressTile extends React.Component {
let dismiss;
if (this.props.canDismiss) {
dismiss = (
<div className="mx_AddressTile_dismiss" onClick={this.props.onDismissed} >
<div className="mx_AddressTile_dismiss" onClick={this.props.onDismissed}>
<img src={require("../../../../res/img/icon-address-delete.svg")} width="9" height="9" />
</div>
);

View file

@ -17,30 +17,39 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import url from 'url';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import WidgetUtils from "../../../utils/WidgetUtils";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
import MemberAvatar from '../avatars/MemberAvatar';
import BaseAvatar from '../avatars/BaseAvatar';
import AccessibleButton from './AccessibleButton';
import TextWithTooltip from "./TextWithTooltip";
interface IProps {
url: string;
creatorUserId: string;
roomId: string;
onPermissionGranted: () => void;
isRoomEncrypted?: boolean;
}
interface IState {
roomMember: RoomMember;
isWrapped: boolean;
widgetDomain: string;
}
@replaceableComponent("views.elements.AppPermission")
export default class AppPermission extends React.Component {
static propTypes = {
url: PropTypes.string.isRequired,
creatorUserId: PropTypes.string.isRequired,
roomId: PropTypes.string.isRequired,
onPermissionGranted: PropTypes.func.isRequired,
isRoomEncrypted: PropTypes.bool,
};
static defaultProps = {
export default class AppPermission extends React.Component<IProps, IState> {
static defaultProps: Partial<IProps> = {
onPermissionGranted: () => {},
};
constructor(props) {
constructor(props: IProps) {
super(props);
// The first step is to pick apart the widget so we can render information about it
@ -55,16 +64,18 @@ export default class AppPermission extends React.Component {
this.state = {
...urlInfo,
roomMember,
isWrapped: null,
widgetDomain: null,
};
}
parseWidgetUrl() {
private parseWidgetUrl(): { isWrapped: boolean, widgetDomain: string } {
const widgetUrl = url.parse(this.props.url);
const params = new URLSearchParams(widgetUrl.search);
// HACK: We're relying on the query params when we should be relying on the widget's `data`.
// This is a workaround for Scalar.
if (WidgetUtils.isScalarUrl(widgetUrl) && params && params.get('url')) {
if (WidgetUtils.isScalarUrl(this.props.url) && params && params.get('url')) {
const unwrappedUrl = url.parse(params.get('url'));
return {
widgetDomain: unwrappedUrl.host || unwrappedUrl.hostname,
@ -80,10 +91,6 @@ export default class AppPermission extends React.Component {
render() {
const brand = SdkConfig.get().brand;
const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton");
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
const TextWithTooltip = sdk.getComponent("views.elements.TextWithTooltip");
const displayName = this.state.roomMember ? this.state.roomMember.name : this.props.creatorUserId;
const userId = displayName === this.props.creatorUserId ? null : this.props.creatorUserId;

View file

@ -1,7 +1,6 @@
/*
Copyright 2017 Vector Creations Ltd
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");
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.
*/
import React, { createRef } from 'react';
import PropTypes from 'prop-types';
import React, { ChangeEvent, createRef, CSSProperties, ReactElement, ReactNode, Ref } from 'react';
import classnames from 'classnames';
import AccessibleButton from './AccessibleButton';
import { _t } from '../../../languageHandler';
import { Key } from "../../../Keyboard";
import { replaceableComponent } from "../../../utils/replaceableComponent";
class MenuOption extends React.Component {
constructor(props) {
super(props);
this._onMouseEnter = this._onMouseEnter.bind(this);
this._onClick = this._onClick.bind(this);
}
interface IMenuOptionProps {
children: ReactElement;
highlighted?: boolean;
dropdownKey: string;
id?: string;
inputRef?: Ref<HTMLDivElement>;
onClick(dropdownKey: string): void;
onMouseEnter(dropdownKey: string): void;
}
class MenuOption extends React.Component<IMenuOptionProps> {
static defaultProps = {
disabled: false,
};
_onMouseEnter() {
private onMouseEnter = () => {
this.props.onMouseEnter(this.props.dropdownKey);
}
};
_onClick(e) {
private onClick = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
this.props.onClick(this.props.dropdownKey);
}
};
render() {
const optClasses = classnames({
@ -54,8 +57,8 @@ class MenuOption extends React.Component {
return <div
id={this.props.id}
className={optClasses}
onClick={this._onClick}
onMouseEnter={this._onMouseEnter}
onClick={this.onClick}
onMouseEnter={this.onMouseEnter}
role="option"
aria-selected={this.props.highlighted}
ref={this.props.inputRef}
@ -65,91 +68,97 @@ class MenuOption extends React.Component {
}
}
MenuOption.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]),
highlighted: PropTypes.bool,
dropdownKey: PropTypes.string,
onClick: PropTypes.func.isRequired,
onMouseEnter: PropTypes.func.isRequired,
inputRef: PropTypes.any,
};
interface IProps {
id: string;
// ARIA label
label: string;
value?: string;
className?: string;
children: ReactElement[];
// negative for consistency with HTML
disabled?: boolean;
// 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,
* but somewhat simpler as react-select is 79KB of minified
* javascript.
*
* TODO: Port NetworkDropdown to use this.
*/
@replaceableComponent("views.elements.Dropdown")
export default class Dropdown extends React.Component {
constructor(props) {
export default class Dropdown extends React.Component<IProps, IState> {
private readonly buttonRef = createRef<HTMLDivElement>();
private dropdownRootElement: HTMLDivElement = null;
private ignoreEvent: MouseEvent = null;
private childrenByKey: Record<string, ReactNode> = {};
constructor(props: IProps) {
super(props);
this.dropdownRootElement = null;
this.ignoreEvent = null;
this.reindexChildren(this.props.children);
this._onInputClick = this._onInputClick.bind(this);
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];
const firstChild = React.Children.toArray(props.children)[0] as ReactElement;
this.state = {
// True if the menu is dropped-down
expanded: false,
// The key of the highlighted option
// (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
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
// menu when the user clicks somewhere else
document.addEventListener('click', this._onDocumentClick, false);
document.addEventListener('click', this.onDocumentClick, false);
}
componentWillUnmount() {
document.removeEventListener('click', this._onDocumentClick, false);
document.removeEventListener('click', this.onDocumentClick, false);
}
// 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) {
return;
}
this._reindexChildren(nextProps.children);
this.reindexChildren(nextProps.children);
const firstChild = nextProps.children[0];
this.setState({
highlightedOption: firstChild ? firstChild.key : null,
});
}
_reindexChildren(children) {
private reindexChildren(children: ReactElement[]): void {
this.childrenByKey = {};
React.Children.forEach(children, (child) => {
this.childrenByKey[child.key] = child;
});
}
_onDocumentClick(ev) {
private onDocumentClick = (ev: MouseEvent) => {
// Close the dropdown if the user clicks anywhere that isn't
// within our root element
if (ev !== this.ignoreEvent) {
@ -157,9 +166,9 @@ export default class Dropdown extends React.Component {
expanded: false,
});
}
}
};
_onRootClick(ev) {
private onRootClick = (ev: MouseEvent) => {
// This captures any clicks that happen within our elements,
// such that we can then ignore them when they're seen by 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
// doesn't reach the React onClick().
this.ignoreEvent = ev;
}
};
_onInputClick(ev) {
private onInputClick = (ev: React.MouseEvent) => {
if (this.props.disabled) return;
if (!this.state.expanded) {
@ -178,24 +187,24 @@ export default class Dropdown extends React.Component {
});
ev.preventDefault();
}
}
};
_close() {
private close() {
this.setState({
expanded: false,
});
// their focus was on the input, its getting unmounted, move it to the button
if (this._button.current) {
this._button.current.focus();
if (this.buttonRef.current) {
this.buttonRef.current.focus();
}
}
_onMenuOptionClick(dropdownKey) {
this._close();
private onMenuOptionClick = (dropdownKey: string) => {
this.close();
this.props.onOptionChange(dropdownKey);
}
};
_onInputKeyDown = (e) => {
private onInputKeyDown = (e: React.KeyboardEvent) => {
let handled = true;
// 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);
// fallthrough
case Key.ESCAPE:
this._close();
this.close();
break;
case Key.ARROW_DOWN:
this.setState({
highlightedOption: this._nextOption(this.state.highlightedOption),
highlightedOption: this.nextOption(this.state.highlightedOption),
});
break;
case Key.ARROW_UP:
this.setState({
highlightedOption: this._prevOption(this.state.highlightedOption),
highlightedOption: this.prevOption(this.state.highlightedOption),
});
break;
default:
@ -224,53 +233,46 @@ export default class Dropdown extends React.Component {
e.preventDefault();
e.stopPropagation();
}
}
};
_onInputChange(e) {
private onInputChange = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({
searchQuery: e.target.value,
searchQuery: e.currentTarget.value,
});
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) {
this.dropdownRootElement.removeEventListener(
'click', this._onRootClick, false,
);
this.dropdownRootElement.removeEventListener('click', this.onRootClick, false);
}
if (e) {
e.addEventListener('click', this._onRootClick, false);
e.addEventListener('click', this.onRootClick, false);
}
this.dropdownRootElement = e;
}
};
_collectInputTextBox(e) {
this.inputTextBox = e;
if (e) e.focus();
}
_setHighlightedOption(optionKey) {
private setHighlightedOption = (optionKey: string) => {
this.setState({
highlightedOption: optionKey,
});
}
};
_nextOption(optionKey) {
private nextOption(optionKey: string): string {
const keys = Object.keys(this.childrenByKey);
const index = keys.indexOf(optionKey);
return keys[(index + 1) % keys.length];
}
_prevOption(optionKey) {
private prevOption(optionKey: string): string {
const keys = Object.keys(this.childrenByKey);
const index = keys.indexOf(optionKey);
return keys[(index - 1) % keys.length];
}
_scrollIntoView(node) {
private scrollIntoView(node: Element) {
if (node) {
node.scrollIntoView({
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 highlighted = this.state.highlightedOption === child.key;
return (
<MenuOption
id={`${this.props.id}__${child.key}`}
key={child.key}
dropdownKey={child.key}
dropdownKey={child.key as string}
highlighted={highlighted}
onMouseEnter={this._setHighlightedOption}
onClick={this._onMenuOptionClick}
inputRef={highlighted ? this._scrollIntoView : undefined}
onMouseEnter={this.setHighlightedOption}
onClick={this.onMenuOptionClick}
inputRef={highlighted ? this.scrollIntoView : undefined}
>
{ child }
</MenuOption>
@ -307,7 +309,7 @@ export default class Dropdown extends React.Component {
render() {
let currentValue;
const menuStyle = {};
const menuStyle: CSSProperties = {};
if (this.props.menuWidth) menuStyle.width = this.props.menuWidth;
let menu;
@ -316,10 +318,10 @@ export default class Dropdown extends React.Component {
currentValue = (
<input
type="text"
autoFocus={true}
className="mx_Dropdown_option"
ref={this._collectInputTextBox}
onKeyDown={this._onInputKeyDown}
onChange={this._onInputChange}
onKeyDown={this.onInputKeyDown}
onChange={this.onInputChange}
value={this.state.searchQuery}
role="combobox"
aria-autocomplete="list"
@ -332,7 +334,7 @@ export default class Dropdown extends React.Component {
}
menu = (
<div className="mx_Dropdown_menu" style={menuStyle} role="listbox" id={`${this.props.id}_listbox`}>
{ this._getMenuOptions() }
{ this.getMenuOptions() }
</div>
);
}
@ -356,14 +358,14 @@ export default class Dropdown extends React.Component {
// Note the menu sits inside the AccessibleButton div so it's anchored
// 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
className="mx_Dropdown_input mx_no_textinput"
onClick={this._onInputClick}
onClick={this.onInputClick}
aria-haspopup="listbox"
aria-expanded={this.state.expanded}
disabled={this.props.disabled}
inputRef={this._button}
inputRef={this.buttonRef}
aria-label={this.props.label}
aria-describedby={`${this.props.id}_value`}
>
@ -374,28 +376,3 @@ export default class Dropdown extends React.Component {
</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

@ -160,41 +160,80 @@ export default class ImageView extends React.Component<IProps, IState> {
});
};
private zoom(delta: number) {
const newZoom = this.state.zoom + delta;
private zoomDelta(delta: number, anchorX?: number, anchorY?: number) {
this.zoom(this.state.zoom + delta, anchorX, anchorY);
}
private zoom(zoomLevel: number, anchorX?: number, anchorY?: number) {
const oldZoom = this.state.zoom;
const newZoom = Math.min(zoomLevel, this.state.maxZoom);
if (newZoom <= this.state.minZoom) {
// Zoom out fully
this.setState({
zoom: this.state.minZoom,
translationX: 0,
translationY: 0,
});
return;
}
if (newZoom >= this.state.maxZoom) {
this.setState({ zoom: this.state.maxZoom });
return;
}
} else if (typeof anchorX !== "number" && typeof anchorY !== "number") {
// Zoom relative to the center of the view
this.setState({
zoom: newZoom,
translationX: this.state.translationX * newZoom / oldZoom,
translationY: this.state.translationY * newZoom / oldZoom,
});
} else {
// Zoom relative to the given point on the image.
// First we need to figure out the offset of the anchor point
// relative to the center of the image, accounting for rotation.
let offsetX;
let offsetY;
// The modulo operator can return negative values for some
// rotations, so we have to do some extra work to normalize it
switch (((this.state.rotation % 360) + 360) % 360) {
case 0:
offsetX = this.image.current.clientWidth / 2 - anchorX;
offsetY = this.image.current.clientHeight / 2 - anchorY;
break;
case 90:
offsetX = anchorY - this.image.current.clientHeight / 2;
offsetY = this.image.current.clientWidth / 2 - anchorX;
break;
case 180:
offsetX = anchorX - this.image.current.clientWidth / 2;
offsetY = anchorY - this.image.current.clientHeight / 2;
break;
case 270:
offsetX = this.image.current.clientHeight / 2 - anchorY;
offsetY = anchorX - this.image.current.clientWidth / 2;
}
this.setState({
zoom: newZoom,
});
// Apply the zoom and offset
this.setState({
zoom: newZoom,
translationX: this.state.translationX + (newZoom - oldZoom) * offsetX,
translationY: this.state.translationY + (newZoom - oldZoom) * offsetY,
});
}
}
private onWheel = (ev: WheelEvent) => {
ev.stopPropagation();
ev.preventDefault();
if (ev.target === this.image.current) {
ev.stopPropagation();
ev.preventDefault();
const { deltaY } = normalizeWheelEvent(ev);
this.zoom(-(deltaY * ZOOM_COEFFICIENT));
const { deltaY } = normalizeWheelEvent(ev);
// Zoom in on the point on the image targeted by the cursor
this.zoomDelta(-deltaY * ZOOM_COEFFICIENT, ev.offsetX, ev.offsetY);
}
};
private onZoomInClick = () => {
this.zoom(ZOOM_STEP);
this.zoomDelta(ZOOM_STEP);
};
private onZoomOutClick = () => {
this.zoom(-ZOOM_STEP);
this.zoomDelta(-ZOOM_STEP);
};
private onKeyDown = (ev: KeyboardEvent) => {
@ -259,7 +298,7 @@ export default class ImageView extends React.Component<IProps, IState> {
// Zoom in if we are completely zoomed out
if (this.state.zoom === this.state.minZoom) {
this.setState({ zoom: this.state.maxZoom });
this.zoom(this.state.maxZoom, ev.nativeEvent.offsetX, ev.nativeEvent.offsetY);
return;
}
@ -289,11 +328,7 @@ export default class ImageView extends React.Component<IProps, IState> {
Math.abs(this.state.translationX - this.previousX) < ZOOM_DISTANCE &&
Math.abs(this.state.translationY - this.previousY) < ZOOM_DISTANCE
) {
this.setState({
zoom: this.state.minZoom,
translationX: 0,
translationY: 0,
});
this.zoom(this.state.minZoom);
this.initX = 0;
this.initY = 0;
}
@ -384,7 +419,8 @@ export default class ImageView extends React.Component<IProps, IState> {
const avatar = (
<MemberAvatar
member={mxEvent.sender}
width={32} height={32}
width={32}
height={32}
viewUserOnClick={true}
/>
);
@ -403,7 +439,7 @@ export default class ImageView extends React.Component<IProps, IState> {
// an empty div here, since the panel uses space-between
// and we want the same placement of elements
info = (
<div></div>
<div />
);
}
@ -427,15 +463,15 @@ export default class ImageView extends React.Component<IProps, IState> {
<AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_zoomOut"
title={_t("Zoom out")}
onClick={this.onZoomOutClick}>
</AccessibleTooltipButton>
onClick={this.onZoomOutClick}
/>
);
zoomInButton = (
<AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_zoomIn"
title={_t("Zoom in")}
onClick={this.onZoomInClick}>
</AccessibleTooltipButton>
onClick={this.onZoomInClick}
/>
);
}
@ -457,24 +493,24 @@ export default class ImageView extends React.Component<IProps, IState> {
<AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_rotateCCW"
title={_t("Rotate Left")}
onClick={this.onRotateCounterClockwiseClick}>
</AccessibleTooltipButton>
onClick={this.onRotateCounterClockwiseClick}
/>
<AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_rotateCW"
title={_t("Rotate Right")}
onClick={this.onRotateClockwiseClick}>
</AccessibleTooltipButton>
onClick={this.onRotateClockwiseClick}
/>
<AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_download"
title={_t("Download")}
onClick={this.onDownloadClick}>
</AccessibleTooltipButton>
onClick={this.onDownloadClick}
/>
{ contextMenuButton }
<AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_close"
title={_t("Close")}
onClick={this.props.onFinished}>
</AccessibleTooltipButton>
onClick={this.props.onFinished}
/>
{ this.renderContextMenu() }
</div>
</div>

View file

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

View file

@ -258,7 +258,10 @@ class Pill extends React.Component {
linkText = groupId;
if (this.props.shouldShowPillAvatar) {
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} />;
}
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;
if (this.state.custom) {
picker = (
<Field type="number"
label={label} max={this.props.maxValue}
<Field
type="number"
label={label}
max={this.props.maxValue}
onBlur={this.onCustomBlur}
onKeyDown={this.onCustomKeyDown}
onChange={this.onCustomChange}
@ -157,9 +159,12 @@ export default class PowerSelector extends React.Component {
});
picker = (
<Field element="select"
label={label} onChange={this.onSelectChange}
value={String(this.state.selectValue)} disabled={this.props.disabled}
<Field
element="select"
label={label}
onChange={this.onSelectChange}
value={String(this.state.selectValue)}
disabled={this.props.disabled}
>
{ options }
</Field>

View file

@ -14,17 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import React, { ReactNode } from "react";
import classNames from "classnames";
import StyledRadioButton from "./StyledRadioButton";
interface IDefinition<T extends string> {
export interface IDefinition<T extends string> {
value: T;
className?: string;
disabled?: boolean;
label: React.ReactChild;
description?: React.ReactChild;
label: ReactNode;
description?: ReactNode;
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}
name={name}
value={d.value}
disabled={disabled || d.disabled}
disabled={d.disabled ?? disabled}
outlined={outlined}
>
{ d.label }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -16,13 +16,14 @@ limitations under the License.
import React from "react";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { Playback } from "../../../voice/Playback";
import { Playback } from "../../../audio/Playback";
import InlineSpinner from '../elements/InlineSpinner';
import { _t } from "../../../languageHandler";
import AudioPlayer from "../audio_messages/AudioPlayer";
import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent";
import MFileBody from "./MFileBody";
import { IBodyProps } from "./IBodyProps";
import { PlaybackManager } from "../../../audio/PlaybackManager";
interface IState {
error?: Error;
@ -62,7 +63,7 @@ export default class MAudioBody extends React.PureComponent<IBodyProps, IState>
const waveform = content?.["org.matrix.msc1767.audio"]?.waveform?.map(p => p / 1024);
// We should have a buffer to work with now: let's set it up
const playback = new Playback(buffer, waveform);
const playback = PlaybackManager.instance.createPlaybackInstance(buffer, waveform);
playback.clockInfo.populatePlaceholdersFrom(this.props.mxEvent);
this.setState({ playback });
@ -75,7 +76,6 @@ export default class MAudioBody extends React.PureComponent<IBodyProps, IState>
public render() {
if (this.state.error) {
// TODO: @@TR: Verify error state
return (
<span className="mx_MAudioBody">
<img src={require("../../../../res/img/warning.svg")} width="16" height="16" />
@ -85,7 +85,6 @@ export default class MAudioBody extends React.PureComponent<IBodyProps, IState>
}
if (!this.state.playback) {
// TODO: @@TR: Verify loading/decrypting state
return (
<span className="mx_MAudioBody">
<InlineSpinner />

View file

@ -141,7 +141,7 @@ export default class MFileBody extends React.Component<IProps, IState> {
let placeholder = null;
if (this.props.showGenericPlaceholder) {
placeholder = (
<div className="mx_MFileBody_info">
<div className="mx_MediaBody mx_MFileBody_info">
<span className="mx_MFileBody_info_icon" />
<span className="mx_MFileBody_info_filename">
{ presentableTextForFile(content, _t("Attachment"), false) }

View file

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

View file

@ -267,8 +267,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
width={width}
poster={poster}
onPlay={this.videoOnPlay}
>
</video>
/>
{ this.props.tileShape && <MFileBody {...this.props} showGenericPlaceholder={false} /> }
</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.
public render() {
if (this.state.error) {
// TODO: @@TR: Verify error state
return (
<span className="mx_MVoiceMessageBody">
<img src={require("../../../../res/img/warning.svg")} width="16" height="16" />
@ -37,7 +36,6 @@ export default class MVoiceMessageBody extends MAudioBody {
}
if (!this.state.playback) {
// TODO: @@TR: Verify loading/decrypting state
return (
<span className="mx_MVoiceMessageBody">
<InlineSpinner />

View file

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

View file

@ -514,7 +514,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
switch (content.msgtype) {
case MsgType.Emote:
return (
<span className="mx_MEmoteBody mx_EventTile_content">
<div className="mx_MEmoteBody mx_EventTile_content">
*&nbsp;
<span
className="mx_MEmoteBody_sender"
@ -525,21 +525,21 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
&nbsp;
{ body }
{ widgets }
</span>
</div>
);
case MsgType.Notice:
return (
<span className="mx_MNoticeBody mx_EventTile_content">
<div className="mx_MNoticeBody mx_EventTile_content">
{ body }
{ widgets }
</span>
</div>
);
default: // including "m.text"
return (
<span className="mx_MTextBody mx_EventTile_content">
<div className="mx_MTextBody mx_EventTile_content">
{ body }
{ widgets }
</span>
</div>
);
}
}

View file

@ -205,7 +205,7 @@ function DeviceItem({ userId, device }: {userId: string, device: IDevice}) {
if (isVerified) {
return (
<div className={classes} title={device.deviceId} >
<div className={classes} title={device.deviceId}>
<div className={iconClasses} />
<div className="mx_UserInfo_device_name">{ deviceName }</div>
<div className="mx_UserInfo_device_trusted">{ trustedLabel }</div>
@ -1353,13 +1353,16 @@ const BasicUserInfo: React.FC<{
if (hasCrossSigningKeys !== undefined) {
// Note: mx_UserInfo_verifyButton is for the end-to-end tests
verifyButton = (
<AccessibleButton className="mx_UserInfo_field mx_UserInfo_verifyButton" onClick={() => {
if (hasCrossSigningKeys) {
verifyUser(member as User);
} else {
legacyVerifyUser(member as User);
}
}}>
<AccessibleButton
className="mx_UserInfo_field mx_UserInfo_verifyButton"
onClick={() => {
if (hasCrossSigningKeys) {
verifyUser(member as User);
} else {
legacyVerifyUser(member as User);
}
}}
>
{ _t("Verify") }
</AccessibleButton>
);
@ -1374,12 +1377,15 @@ const BasicUserInfo: React.FC<{
let editDevices;
if (member.userId == cli.getUserId()) {
editDevices = (<p>
<AccessibleButton className="mx_UserInfo_field" onClick={() => {
dis.dispatch({
<AccessibleButton
className="mx_UserInfo_field"
onClick={() => {
dis.dispatch({
action: Action.ViewUserSettings,
initialTabId: UserTab.Security,
});
}}>
});
}}
>
{ _t("Edit devices") }
</AccessibleButton>
</p>);

View file

@ -711,9 +711,12 @@ export default class EventTile extends React.Component<IProps, IState> {
// add to the start so the most recent is on the end (ie. ends up rightmost)
avatars.unshift(
<ReadReceiptMarker key={userId} member={receipt.roomMember}
<ReadReceiptMarker
key={userId}
member={receipt.roomMember}
fallbackUserId={userId}
leftOffset={left} hidden={hidden}
leftOffset={left}
hidden={hidden}
readReceiptInfo={readReceiptInfo}
checkUnmounting={this.props.checkUnmounting}
suppressAnimation={this.suppressReadReceiptAnimation}
@ -949,8 +952,10 @@ export default class EventTile extends React.Component<IProps, IState> {
}
avatar = (
<div className="mx_EventTile_avatar">
<MemberAvatar member={member}
width={avatarSize} height={avatarSize}
<MemberAvatar
member={member}
width={avatarSize}
height={avatarSize}
viewUserOnClick={true}
/>
</div>
@ -1142,6 +1147,7 @@ export default class EventTile extends React.Component<IProps, IState> {
{ ircTimestamp }
{ sender }
{ ircPadlock }
{ avatar }
<div className="mx_EventTile_line" key="mx_EventTile_line">
{ groupTimestamp }
{ groupPadlock }
@ -1162,7 +1168,6 @@ export default class EventTile extends React.Component<IProps, IState> {
</div>
{ reactionsRow }
{ msgOption }
{ avatar }
</>)
);
}

View file

@ -28,10 +28,11 @@ export default (props) => {
badge = (<div className="mx_JumpToBottomButton_badge">{ props.numUnreadMessages }</div>);
}
return (<div className={className}>
<AccessibleButton className="mx_JumpToBottomButton_scrollDown"
<AccessibleButton
className="mx_JumpToBottomButton_scrollDown"
title={_t("Scroll to most recent messages")}
onClick={props.onScrollToBottomClick}>
</AccessibleButton>
onClick={props.onScrollToBottomClick}
/>
{ badge }
</div>);
};

View file

@ -306,10 +306,16 @@ export default class MemberList extends React.Component<IProps, IState> {
// For now we'll pretend this is any entity. It should probably be a separate tile.
const text = _t("and %(count)s others...", { count: overflowCount });
return (
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
<BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} />
} name={text} presenceState="online" suppressOnHover={true}
onClick={onClick} />
<EntityTile
className="mx_EntityTile_ellipsis"
avatarJsx={
<BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} />
}
name={text}
presenceState="online"
suppressOnHover={true}
onClick={onClick}
/>
);
};
@ -465,8 +471,12 @@ export default class MemberList extends React.Component<IProps, IState> {
return <MemberTile key={m.userId} member={m} ref={m.userId} showPresence={this.showPresence} />;
} else {
// Is a 3pid invite
return <EntityTile key={m.getStateKey()} name={m.getContent().display_name} suppressOnHover={true}
onClick={() => this.onPending3pidInviteClick(m)} />;
return <EntityTile
key={m.getStateKey()}
name={m.getContent().display_name}
suppressOnHover={true}
onClick={() => this.onPending3pidInviteClick(m)}
/>;
}
});
}

View file

@ -35,7 +35,7 @@ import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import VoiceRecordComposerTile from "./VoiceRecordComposerTile";
import { VoiceRecordingStore } from "../../../stores/VoiceRecordingStore";
import { RecordingState } from "../../../voice/VoiceRecording";
import { RecordingState } from "../../../audio/VoiceRecording";
import Tooltip, { Alignment } from "../elements/Tooltip";
import ResizeNotifier from "../../../utils/ResizeNotifier";
import { E2EStatus } from '../../../utils/ShieldUtils';
@ -98,9 +98,7 @@ const EmojiButton = ({ addEmoji }) => {
isExpanded={menuDisplayed}
title={_t('Emoji picker')}
inputRef={button}
>
</ContextMenuTooltipButton>
/>
{ contextMenu }
</React.Fragment>;
@ -439,7 +437,8 @@ export default class MessageComposer extends React.Component<IProps, IState> {
if (secondsLeft) {
recordingTooltip = <Tooltip
label={_t("%(seconds)ss left", { seconds: secondsLeft })}
alignment={Alignment.Top} yOffset={-50}
alignment={Alignment.Top}
yOffset={-50}
/>;
}

View file

@ -58,13 +58,18 @@ const NewRoomIntro = () => {
const member = room?.getMember(dmPartner);
const displayName = member?.rawDisplayName || dmPartner;
body = <React.Fragment>
<RoomAvatar room={room} width={AVATAR_SIZE} height={AVATAR_SIZE} onClick={() => {
defaultDispatcher.dispatch<ViewUserPayload>({
<RoomAvatar
room={room}
width={AVATAR_SIZE}
height={AVATAR_SIZE}
onClick={() => {
defaultDispatcher.dispatch<ViewUserPayload>({
action: Action.ViewUser,
// XXX: We should be using a real member object and not assuming what the receiver wants.
member: member || { userId: dmPartner } as User,
});
}} />
});
}}
/>
<h2>{ room.name }</h2>

View file

@ -192,7 +192,9 @@ export default class ReadReceiptMarker extends React.PureComponent {
member={this.props.member}
fallbackUserId={this.props.fallbackUserId}
aria-hidden="true"
width={14} height={14} resizeMethod="crop"
width={14}
height={14}
resizeMethod="crop"
style={style}
title={title}
onClick={this.props.onClick}

View file

@ -105,7 +105,9 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
// NOTE: The CSSTransition timeout MUST match the timeout in our CSS!
return (
<CSSTransition
appear={true} in={this.state.doAnimation} timeout={640}
appear={true}
in={this.state.doAnimation}
timeout={640}
classNames='mx_RoomBreadcrumbs'
>
<Toolbar className='mx_RoomBreadcrumbs' aria-label={_t("Recently visited rooms")}>

View file

@ -105,8 +105,12 @@ export default class RoomDetailRow extends React.Component {
return <tr key={room.roomId} onClick={this.onClick} onMouseDown={this.props.onMouseDown}>
<td className="mx_RoomDirectory_roomAvatar">
<BaseAvatar width={24} height={24} resizeMethod='crop'
name={name} idName={name}
<BaseAvatar
width={24}
height={24}
resizeMethod='crop'
name={name}
idName={name}
url={avatarUrl} />
</td>
<td className="mx_RoomDirectory_roomDescription">

View file

@ -68,7 +68,7 @@ interface IState {
suggestedRooms: ISuggestedRoom[];
}
const TAG_ORDER: TagID[] = [
export const TAG_ORDER: TagID[] = [
DefaultTagID.Invite,
DefaultTagID.Favourite,
DefaultTagID.DM,
@ -140,7 +140,7 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
e.preventDefault();
e.stopPropagation();
onFinished();
showCreateNewRoom(MatrixClientPeg.get(), SpaceStore.instance.activeSpace);
showCreateNewRoom(SpaceStore.instance.activeSpace);
}}
disabled={!canAddRooms}
tooltip={canAddRooms ? undefined
@ -153,7 +153,7 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
e.preventDefault();
e.stopPropagation();
onFinished();
showAddExistingRooms(MatrixClientPeg.get(), SpaceStore.instance.activeSpace);
showAddExistingRooms(SpaceStore.instance.activeSpace);
}}
disabled={!canAddRooms}
tooltip={canAddRooms ? undefined
@ -428,7 +428,9 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
groupId={g.groupId}
groupName={g.name}
groupAvatarUrl={g.avatarUrl}
width={32} height={32} resizeMethod='crop'
width={32}
height={32}
resizeMethod='crop'
/>
);
const openGroup = () => {

View file

@ -536,8 +536,10 @@ export default class RoomPreviewBar extends React.Component {
"If you think you're seeing this message in error, please " +
"<issueLink>submit a bug report</issueLink>.",
{ errcode: this.props.error.errcode },
{ issueLink: label => <a href="https://github.com/vector-im/element-web/issues/new/choose"
target="_blank" rel="noreferrer noopener">{ label }</a> },
{ issueLink: label => <a
href="https://github.com/vector-im/element-web/issues/new/choose"
target="_blank"
rel="noreferrer noopener">{ label }</a> },
),
];
break;

View file

@ -419,7 +419,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
>
<IconizedContextMenuOptionList first>
<IconizedContextMenuRadio
label={_t("Global")}
label={_t("Use default")}
active={state === ALL_MESSAGES}
iconClassName="mx_RoomTile_iconBell"
onClick={this.onClickAllNotifs}

View file

@ -514,13 +514,11 @@ export default class SendMessageComposer extends React.Component<IProps> {
private onPaste = (event: ClipboardEvent<HTMLDivElement>): boolean => {
const { clipboardData } = event;
// Prioritize text on the clipboard over files as Office on macOS puts a bitmap
// in the clipboard as well as the content being copied.
if (clipboardData.files.length && !clipboardData.types.some(t => t === "text/plain")) {
// This actually not so much for 'files' as such (at time of writing
// neither chrome nor firefox let you paste a plain file copied
// from Finder) but more images copied from a different website
// / word processor etc.
// Prioritize text on the clipboard over files if RTF is present as Office on macOS puts a bitmap
// in the clipboard as well as the content being copied. Modern versions of Office seem to not do this anymore.
// We check text/rtf instead of text/plain as when copy+pasting a file from Finder or Gnome Image Viewer
// it puts the filename in as text/plain which we want to ignore.
if (clipboardData.files.length && !clipboardData.types.includes("text/rtf")) {
ContentMessages.sharedInstance().sendContentListToRoom(
Array.from(clipboardData.files), this.props.room.roomId, this.context,
);

View file

@ -35,13 +35,15 @@ export default class SimpleRoomHeader extends React.Component {
let icon;
if (this.props.icon) {
icon = <img
className="mx_RoomHeader_icon" src={this.props.icon}
width="25" height="25"
className="mx_RoomHeader_icon"
src={this.props.icon}
width="25"
height="25"
/>;
}
return (
<div className="mx_RoomHeader mx_RoomHeader_wrapper" >
<div className="mx_RoomHeader mx_RoomHeader_wrapper">
<div className="mx_RoomHeader_simpleHeader">
{ icon }
{ this.props.title }

View file

@ -403,8 +403,7 @@ export default class Stickerpicker extends React.PureComponent {
onClick={this._onHideStickersClick}
active={this.state.showStickers.toString()}
title={_t("Hide Stickers")}
>
</AccessibleButton>;
/>;
const GenericElementContextMenu = sdk.getComponent('context_menus.GenericElementContextMenu');
stickerPicker = <ContextMenu
@ -431,8 +430,7 @@ export default class Stickerpicker extends React.PureComponent {
className="mx_MessageComposer_button mx_MessageComposer_stickers"
onClick={this._onShowStickersClick}
title={_t("Show Stickers")}
>
</AccessibleTooltipButton>;
/>;
}
return <React.Fragment>
{ stickersButton }

View file

@ -32,14 +32,16 @@ export default class TopUnreadMessagesBar extends React.Component {
render() {
return (
<div className="mx_TopUnreadMessagesBar">
<AccessibleButton className="mx_TopUnreadMessagesBar_scrollUp"
<AccessibleButton
className="mx_TopUnreadMessagesBar_scrollUp"
title={_t('Jump to first unread message.')}
onClick={this.props.onScrollUpClick}>
</AccessibleButton>
<AccessibleButton className="mx_TopUnreadMessagesBar_markAsRead"
onClick={this.props.onScrollUpClick}
/>
<AccessibleButton
className="mx_TopUnreadMessagesBar_markAsRead"
title={_t('Mark all as read')}
onClick={this.props.onCloseClick}>
</AccessibleButton>
onClick={this.props.onCloseClick}
/>
</div>
);
}

View file

@ -20,7 +20,7 @@ import React, { ReactNode } from "react";
import {
RecordingState,
VoiceRecording,
} from "../../../voice/VoiceRecording";
} from "../../../audio/VoiceRecording";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import classNames from "classnames";
@ -68,37 +68,49 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
}
await this.state.recorder.stop();
const upload = await this.state.recorder.upload(this.props.room.roomId);
MatrixClientPeg.get().sendMessage(this.props.room.roomId, {
"body": "Voice message",
//"msgtype": "org.matrix.msc2516.voice",
"msgtype": MsgType.Audio,
"url": upload.mxc,
"file": upload.encrypted,
"info": {
duration: Math.round(this.state.recorder.durationSeconds * 1000),
mimetype: this.state.recorder.contentType,
size: this.state.recorder.contentLength,
},
// MSC1767 + Ideals of MSC2516 as MSC3245
// https://github.com/matrix-org/matrix-doc/pull/3245
"org.matrix.msc1767.text": "Voice message",
"org.matrix.msc1767.file": {
url: upload.mxc,
file: upload.encrypted,
name: "Voice message.ogg",
mimetype: this.state.recorder.contentType,
size: this.state.recorder.contentLength,
},
"org.matrix.msc1767.audio": {
duration: Math.round(this.state.recorder.durationSeconds * 1000),
try {
const upload = await this.state.recorder.upload(this.props.room.roomId);
// https://github.com/matrix-org/matrix-doc/pull/3246
waveform: this.state.recorder.getPlayback().thumbnailWaveform.map(v => Math.round(v * 1024)),
},
"org.matrix.msc3245.voice": {}, // No content, this is a rendering hint
});
// noinspection ES6MissingAwait - we don't care if it fails, it'll get queued.
MatrixClientPeg.get().sendMessage(this.props.room.roomId, {
"body": "Voice message",
//"msgtype": "org.matrix.msc2516.voice",
"msgtype": MsgType.Audio,
"url": upload.mxc,
"file": upload.encrypted,
"info": {
duration: Math.round(this.state.recorder.durationSeconds * 1000),
mimetype: this.state.recorder.contentType,
size: this.state.recorder.contentLength,
},
// MSC1767 + Ideals of MSC2516 as MSC3245
// https://github.com/matrix-org/matrix-doc/pull/3245
"org.matrix.msc1767.text": "Voice message",
"org.matrix.msc1767.file": {
url: upload.mxc,
file: upload.encrypted,
name: "Voice message.ogg",
mimetype: this.state.recorder.contentType,
size: this.state.recorder.contentLength,
},
"org.matrix.msc1767.audio": {
duration: Math.round(this.state.recorder.durationSeconds * 1000),
// https://github.com/matrix-org/matrix-doc/pull/3246
waveform: this.state.recorder.getPlayback().thumbnailWaveform.map(v => Math.round(v * 1024)),
},
"org.matrix.msc3245.voice": {}, // No content, this is a rendering hint
});
} catch (e) {
console.error("Error sending/uploading voice message:", e);
Modal.createTrackedDialog('Upload failed', '', ErrorDialog, {
title: _t('Upload Failed'),
description: _t("The voice message failed to upload."),
});
return; // don't dispose the recording so the user can retry, maybe
}
await this.disposeRecording();
}
@ -177,7 +189,6 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
if (!this.state.recorder) return null; // no recorder means we're not recording: no waveform
if (this.state.recordingPhase !== RecordingState.Started) {
// TODO: @@ TR: Should we disable this during upload? What does a failed upload look like?
return <RecordingPlayback playback={this.state.recorder.getPlayback()} />;
}

View file

@ -124,7 +124,7 @@ export default class BridgeTile extends React.PureComponent<IProps> {
url={avatarUrl}
/>;
} else {
networkIcon = <div className="noProtocolIcon"></div>;
networkIcon = <div className="noProtocolIcon" />;
}
let networkItem = null;
if (network) {

View file

@ -148,13 +148,22 @@ export default class ChangeAvatar extends React.Component {
if (this.props.room && !this.avatarSet) {
const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
avatarImg = <RoomAvatar
room={this.props.room} width={this.props.width} height={this.props.height} resizeMethod='crop'
room={this.props.room}
width={this.props.width}
height={this.props.height}
resizeMethod='crop'
/>;
} else {
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
// XXX: FIXME: once we track in the JS what our own displayname is(!) then use it here rather than ?
avatarImg = <BaseAvatar width={this.props.width} height={this.props.height} resizeMethod='crop'
name='?' idName={MatrixClientPeg.get().getUserIdLocalpart()} url={this.state.avatarUrl} />;
avatarImg = <BaseAvatar
width={this.props.width}
height={this.props.height}
resizeMethod='crop'
name='?'
idName={MatrixClientPeg.get().getUserIdLocalpart()}
url={this.state.avatarUrl}
/>;
}
let uploadSection;

View file

@ -178,8 +178,11 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
"appear in search results.",
) }</div>
<div>
<AccessibleButton kind="primary" disabled={this.state.enabling}
onClick={this.onEnable}>
<AccessibleButton
kind="primary"
disabled={this.state.enabling}
onClick={this.onEnable}
>
{ _t("Enable") }
</AccessibleButton>
{ this.state.enabling ? <InlineSpinner /> : <div /> }
@ -203,8 +206,10 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
brand,
},
{
nativeLink: sub => <a href={nativeLink}
target="_blank" rel="noreferrer noopener"
nativeLink: sub => <a
href={nativeLink}
target="_blank"
rel="noreferrer noopener"
>{ sub }</a>,
},
) }</div>
@ -219,8 +224,10 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
brand,
},
{
desktopLink: sub => <a href="https://element.io/get-started"
target="_blank" rel="noreferrer noopener"
desktopLink: sub => <a
href="https://element.io/get-started"
target="_blank"
rel="noreferrer noopener"
>{ sub }</a>,
},
) }</div>

View file

@ -172,7 +172,8 @@ export default class ProfileSettings extends React.Component {
>
<input
type="file"
ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload"
ref={this._avatarUpload}
className="mx_ProfileSettings_avatarUpload"
onChange={this._onAvatarChanged}
accept="image/*"
/>
@ -181,7 +182,8 @@ export default class ProfileSettings extends React.Component {
<span className="mx_SettingsTab_subheading">{ _t("Profile") }</span>
<Field
label={_t("Display Name")}
type="text" value={this.state.displayName}
type="text"
value={this.state.displayName}
autoComplete="off"
onChange={this._onDisplayNameChanged}
/>

View file

@ -426,7 +426,9 @@ export default class SetIdServer extends React.Component<IProps, IState> {
disabled={this.state.busy}
forceValidity={this.state.error ? false : null}
/>
<AccessibleButton type="submit" kind="primary_sm"
<AccessibleButton
type="submit"
kind="primary_sm"
onClick={this.checkIdServer}
disabled={!this.idServerChangeEnabled()}
>{ _t("Change") }</AccessibleButton>

View file

@ -97,9 +97,12 @@ export default class GeneralRoomSettingsTab extends React.Component {
<div className="mx_SettingsTab_heading">{ _t("Room Addresses") }</div>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<AliasSettings roomId={this.props.roomId}
canSetCanonicalAlias={canSetCanonical} canSetAliases={canSetAliases}
canonicalAliasEvent={canonicalAliasEv} />
<AliasSettings
roomId={this.props.roomId}
canSetCanonicalAlias={canSetCanonical}
canSetAliases={canSetAliases}
canonicalAliasEvent={canonicalAliasEv}
/>
</div>
<div className="mx_SettingsTab_heading">{ _t("Other") }</div>
{ flairSection }

View file

@ -346,8 +346,11 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
let bannedBy = member.events.member.getSender(); // start by falling back to mxid
if (sender) bannedBy = sender.name;
return (
<BannedUser key={member.userId} canUnban={canBanUsers}
member={member} reason={banEvent.reason}
<BannedUser
key={member.userId}
canUnban={canBanUsers}
member={member}
reason={banEvent.reason}
by={bannedBy}
/>
);

View file

@ -15,52 +15,43 @@ limitations under the License.
*/
import React from 'react';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { GuestAccess, HistoryVisibility, JoinRule, RestrictedAllowType } from "matrix-js-sdk/src/@types/partials";
import { IContent, MatrixEvent } from "matrix-js-sdk/src/models/event";
import { EventType } from 'matrix-js-sdk/src/@types/event';
import { _t } from "../../../../../languageHandler";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
import Modal from "../../../../../Modal";
import QuestionDialog from "../../../dialogs/QuestionDialog";
import StyledRadioGroup from '../../../elements/StyledRadioGroup';
import StyledRadioGroup, { IDefinition } from '../../../elements/StyledRadioGroup';
import { SettingLevel } from "../../../../../settings/SettingLevel";
import SettingsStore from "../../../../../settings/SettingsStore";
import { UIFeature } from "../../../../../settings/UIFeature";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
import AccessibleButton from "../../../elements/AccessibleButton";
import SpaceStore from "../../../../../stores/SpaceStore";
import RoomAvatar from "../../../avatars/RoomAvatar";
import ManageRestrictedJoinRuleDialog from '../../../dialogs/ManageRestrictedJoinRuleDialog';
import RoomUpgradeWarningDialog from '../../../dialogs/RoomUpgradeWarningDialog';
import { upgradeRoom } from "../../../../../utils/RoomUpgrade";
import { arrayHasDiff } from "../../../../../utils/arrays";
import SettingsFlag from '../../../elements/SettingsFlag';
// Knock and private are reserved keywords which are not yet implemented.
export enum JoinRule {
Public = "public",
Knock = "knock",
Invite = "invite",
/**
* @deprecated Reserved. Should not be used.
*/
Private = "private",
}
export enum GuestAccess {
CanJoin = "can_join",
Forbidden = "forbidden",
}
export enum HistoryVisibility {
Invited = "invited",
Joined = "joined",
Shared = "shared",
WorldReadable = "world_readable",
}
interface IProps {
roomId: string;
}
interface IState {
joinRule: JoinRule;
restrictedAllowRoomIds?: string[];
guestAccess: GuestAccess;
history: HistoryVisibility;
hasAliases: boolean;
encrypted: boolean;
roomSupportsRestricted?: boolean;
preferredRestrictionVersion?: string;
showAdvancedSection: boolean;
}
@replaceableComponent("views.settings.tabs.room.SecurityRoomSettingsTab")
@ -70,44 +61,58 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
this.state = {
joinRule: JoinRule.Invite,
guestAccess: GuestAccess.CanJoin,
guestAccess: GuestAccess.Forbidden,
history: HistoryVisibility.Shared,
hasAliases: false,
encrypted: false,
showAdvancedSection: false,
};
}
// TODO: [REACT-WARNING] Move this to constructor
async UNSAFE_componentWillMount() { // eslint-disable-line
MatrixClientPeg.get().on("RoomState.events", this.onStateEvent);
UNSAFE_componentWillMount() { // eslint-disable-line
const cli = MatrixClientPeg.get();
cli.on("RoomState.events", this.onStateEvent);
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
const room = cli.getRoom(this.props.roomId);
const state = room.currentState;
const joinRule: JoinRule = this.pullContentPropertyFromEvent(
state.getStateEvents("m.room.join_rules", ""),
const joinRuleEvent = state.getStateEvents(EventType.RoomJoinRules, "");
const joinRule: JoinRule = this.pullContentPropertyFromEvent<JoinRule>(
joinRuleEvent,
'join_rule',
JoinRule.Invite,
);
const guestAccess: GuestAccess = this.pullContentPropertyFromEvent(
state.getStateEvents("m.room.guest_access", ""),
const restrictedAllowRoomIds = joinRule === JoinRule.Restricted
? joinRuleEvent?.getContent().allow
?.filter(a => a.type === RestrictedAllowType.RoomMembership)
?.map(a => a.room_id)
: undefined;
const guestAccess: GuestAccess = this.pullContentPropertyFromEvent<GuestAccess>(
state.getStateEvents(EventType.RoomGuestAccess, ""),
'guest_access',
GuestAccess.Forbidden,
);
const history: HistoryVisibility = this.pullContentPropertyFromEvent(
state.getStateEvents("m.room.history_visibility", ""),
const history: HistoryVisibility = this.pullContentPropertyFromEvent<HistoryVisibility>(
state.getStateEvents(EventType.RoomHistoryVisibility, ""),
'history_visibility',
HistoryVisibility.Shared,
);
const encrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.roomId);
this.setState({ joinRule, guestAccess, history, encrypted });
const hasAliases = await this.hasAliases();
this.setState({ hasAliases });
const restrictedRoomCapabilities = SpaceStore.instance.restrictedJoinRuleSupport;
const roomSupportsRestricted = Array.isArray(restrictedRoomCapabilities?.support)
&& restrictedRoomCapabilities.support.includes(room.getVersion());
const preferredRestrictionVersion = roomSupportsRestricted ? undefined : restrictedRoomCapabilities?.preferred;
this.setState({ joinRule, restrictedAllowRoomIds, guestAccess, history, encrypted,
roomSupportsRestricted, preferredRestrictionVersion });
this.hasAliases().then(hasAliases => this.setState({ hasAliases }));
}
private pullContentPropertyFromEvent<T>(event: MatrixEvent, key: string, defaultValue: T): T {
if (!event || !event.getContent()) return defaultValue;
return event.getContent()[key] || defaultValue;
return event?.getContent()[key] || defaultValue;
}
componentWillUnmount() {
@ -115,13 +120,13 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
}
private onStateEvent = (e: MatrixEvent) => {
const refreshWhenTypes = [
'm.room.join_rules',
'm.room.guest_access',
'm.room.history_visibility',
'm.room.encryption',
const refreshWhenTypes: EventType[] = [
EventType.RoomJoinRules,
EventType.RoomGuestAccess,
EventType.RoomHistoryVisibility,
EventType.RoomEncryption,
];
if (refreshWhenTypes.includes(e.getType())) this.forceUpdate();
if (refreshWhenTypes.includes(e.getType() as EventType)) this.forceUpdate();
};
private onEncryptionChange = () => {
@ -133,8 +138,10 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
"may prevent many bots and bridges from working correctly. <a>Learn more about encryption.</a>",
{},
{
a: sub => <a href="https://element.io/help#encryption"
rel="noreferrer noopener" target="_blank"
a: sub => <a
href="https://element.io/help#encryption"
rel="noreferrer noopener"
target="_blank"
>{ sub }</a>,
},
),
@ -147,7 +154,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
const beforeEncrypted = this.state.encrypted;
this.setState({ encrypted: true });
MatrixClientPeg.get().sendStateEvent(
this.props.roomId, "m.room.encryption",
this.props.roomId, EventType.RoomEncryption,
{ algorithm: "m.megolm.v1.aes-sha2" },
).catch((e) => {
console.error(e);
@ -157,89 +164,91 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
});
};
private fixGuestAccess = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
const joinRule = JoinRule.Invite;
const guestAccess = GuestAccess.CanJoin;
private onJoinRuleChange = async (joinRule: JoinRule) => {
const beforeJoinRule = this.state.joinRule;
const beforeGuestAccess = this.state.guestAccess;
this.setState({ joinRule, guestAccess });
let restrictedAllowRoomIds: string[];
if (joinRule === JoinRule.Restricted) {
const matrixClient = MatrixClientPeg.get();
const roomId = this.props.roomId;
const room = matrixClient.getRoom(roomId);
if (beforeJoinRule === JoinRule.Restricted || this.state.roomSupportsRestricted) {
// Have the user pick which spaces to allow joins from
restrictedAllowRoomIds = await this.editRestrictedRoomIds();
if (!Array.isArray(restrictedAllowRoomIds)) return;
} else if (this.state.preferredRestrictionVersion) {
// Block this action on a room upgrade otherwise it'd make their room unjoinable
const targetVersion = this.state.preferredRestrictionVersion;
Modal.createTrackedDialog('Restricted join rule upgrade', '', RoomUpgradeWarningDialog, {
roomId,
targetVersion,
description: _t("This upgrade will allow members of selected spaces " +
"access to this room without an invite."),
onFinished: (resp) => {
if (!resp?.continue) return;
upgradeRoom(room, targetVersion, resp.invite);
},
});
return;
}
}
if (beforeJoinRule === joinRule && !restrictedAllowRoomIds) return;
const content: IContent = {
join_rule: joinRule,
};
// pre-set the accepted spaces with the currently viewed one as per the microcopy
if (joinRule === JoinRule.Restricted) {
content.allow = restrictedAllowRoomIds.map(roomId => ({
"type": RestrictedAllowType.RoomMembership,
"room_id": roomId,
}));
}
this.setState({ joinRule, restrictedAllowRoomIds });
const client = MatrixClientPeg.get();
client.sendStateEvent(
this.props.roomId,
"m.room.join_rules",
{ join_rule: joinRule },
"",
).catch((e) => {
client.sendStateEvent(this.props.roomId, EventType.RoomJoinRules, content, "").catch((e) => {
console.error(e);
this.setState({ joinRule: beforeJoinRule });
});
client.sendStateEvent(
this.props.roomId,
"m.room.guest_access",
{ guest_access: guestAccess },
"",
).catch((e) => {
console.error(e);
this.setState({ guestAccess: beforeGuestAccess });
this.setState({
joinRule: beforeJoinRule,
restrictedAllowRoomIds: undefined,
});
});
};
private onRoomAccessRadioToggle = (roomAccess: string) => {
// join_rule
// INVITE | PUBLIC
// ----------------------+----------------
// guest CAN_JOIN | inv_only | pub_with_guest
// access ----------------------+----------------
// FORBIDDEN | inv_only | pub_no_guest
// ----------------------+----------------
// we always set guests can_join here as it makes no sense to have
// an invite-only room that guests can't join. If you explicitly
// invite them, you clearly want them to join, whether they're a
// guest or not. In practice, guest_access should probably have
// been implemented as part of the join_rules enum.
let joinRule = JoinRule.Invite;
let guestAccess = GuestAccess.CanJoin;
switch (roomAccess) {
case "invite_only":
// no change - use defaults above
break;
case "public_no_guests":
joinRule = JoinRule.Public;
guestAccess = GuestAccess.Forbidden;
break;
case "public_with_guests":
joinRule = JoinRule.Public;
guestAccess = GuestAccess.CanJoin;
break;
}
const beforeJoinRule = this.state.joinRule;
const beforeGuestAccess = this.state.guestAccess;
this.setState({ joinRule, guestAccess });
private onRestrictedRoomIdsChange = (restrictedAllowRoomIds: string[]) => {
const beforeRestrictedAllowRoomIds = this.state.restrictedAllowRoomIds;
if (!arrayHasDiff(beforeRestrictedAllowRoomIds || [], restrictedAllowRoomIds)) return;
this.setState({ restrictedAllowRoomIds });
const client = MatrixClientPeg.get();
client.sendStateEvent(
this.props.roomId,
"m.room.join_rules",
{ join_rule: joinRule },
"",
).catch((e) => {
client.sendStateEvent(this.props.roomId, EventType.RoomJoinRules, {
join_rule: JoinRule.Restricted,
allow: restrictedAllowRoomIds.map(roomId => ({
"type": RestrictedAllowType.RoomMembership,
"room_id": roomId,
})),
}, "").catch((e) => {
console.error(e);
this.setState({ joinRule: beforeJoinRule });
this.setState({ restrictedAllowRoomIds: beforeRestrictedAllowRoomIds });
});
client.sendStateEvent(
this.props.roomId,
"m.room.guest_access",
{ guest_access: guestAccess },
"",
).catch((e) => {
};
private onGuestAccessChange = (allowed: boolean) => {
const guestAccess = allowed ? GuestAccess.CanJoin : GuestAccess.Forbidden;
const beforeGuestAccess = this.state.guestAccess;
if (beforeGuestAccess === guestAccess) return;
this.setState({ guestAccess });
const client = MatrixClientPeg.get();
client.sendStateEvent(this.props.roomId, EventType.RoomGuestAccess, {
guest_access: guestAccess,
}, "").catch((e) => {
console.error(e);
this.setState({ guestAccess: beforeGuestAccess });
});
@ -247,8 +256,10 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
private onHistoryRadioToggle = (history: HistoryVisibility) => {
const beforeHistory = this.state.history;
if (beforeHistory === history) return;
this.setState({ history: history });
MatrixClientPeg.get().sendStateEvent(this.props.roomId, "m.room.history_visibility", {
MatrixClientPeg.get().sendStateEvent(this.props.roomId, EventType.RoomHistoryVisibility, {
history_visibility: history,
}, "").catch((e) => {
console.error(e);
@ -268,36 +279,48 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
return Array.isArray(localAliases) && localAliases.length !== 0;
} else {
const room = cli.getRoom(this.props.roomId);
const aliasEvents = room.currentState.getStateEvents("m.room.aliases") || [];
const aliasEvents = room.currentState.getStateEvents(EventType.RoomAliases) || [];
const hasAliases = !!aliasEvents.find((ev) => (ev.getContent().aliases || []).length > 0);
return hasAliases;
}
}
private renderRoomAccess() {
private editRestrictedRoomIds = async (): Promise<string[] | undefined> => {
let selected = this.state.restrictedAllowRoomIds;
if (!selected?.length && SpaceStore.instance.activeSpace) {
selected = [SpaceStore.instance.activeSpace.roomId];
}
const matrixClient = MatrixClientPeg.get();
const { finished } = Modal.createTrackedDialog('Edit restricted', '', ManageRestrictedJoinRuleDialog, {
matrixClient,
room: matrixClient.getRoom(this.props.roomId),
selected,
}, "mx_ManageRestrictedJoinRuleDialog_wrapper");
const [restrictedAllowRoomIds] = await finished;
return restrictedAllowRoomIds;
};
private onEditRestrictedClick = async () => {
const restrictedAllowRoomIds = await this.editRestrictedRoomIds();
if (!Array.isArray(restrictedAllowRoomIds)) return;
if (restrictedAllowRoomIds.length > 0) {
this.onRestrictedRoomIdsChange(restrictedAllowRoomIds);
} else {
this.onJoinRuleChange(JoinRule.Invite);
}
};
private renderJoinRule() {
const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId);
const joinRule = this.state.joinRule;
const guestAccess = this.state.guestAccess;
const canChangeAccess = room.currentState.mayClientSendStateEvent("m.room.join_rules", client)
&& room.currentState.mayClientSendStateEvent("m.room.guest_access", client);
let guestWarning = null;
if (joinRule !== 'public' && guestAccess === 'forbidden') {
guestWarning = (
<div className='mx_SecurityRoomSettingsTab_warning'>
<img src={require("../../../../../../res/img/warning.svg")} width={15} height={15} />
<span>
{ _t("Guests cannot join this room even if explicitly invited.") }&nbsp;
<a href="" onClick={this.fixGuestAccess}>{ _t("Click here to fix") }</a>
</span>
</div>
);
}
const canChangeJoinRule = room.currentState.mayClientSendStateEvent(EventType.RoomJoinRules, client);
let aliasWarning = null;
if (joinRule === 'public' && !this.state.hasAliases) {
if (joinRule === JoinRule.Public && !this.state.hasAliases) {
aliasWarning = (
<div className='mx_SecurityRoomSettingsTab_warning'>
<img src={require("../../../../../../res/img/warning.svg")} width={15} height={15} />
@ -308,34 +331,107 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
);
}
const radioDefinitions: IDefinition<JoinRule>[] = [{
value: JoinRule.Invite,
label: _t("Private (invite only)"),
description: _t("Only invited people can join."),
checked: this.state.joinRule === JoinRule.Invite
|| (this.state.joinRule === JoinRule.Restricted && !this.state.restrictedAllowRoomIds?.length),
}, {
value: JoinRule.Public,
label: _t("Public"),
description: _t("Anyone can find and join."),
}];
if (this.state.roomSupportsRestricted ||
this.state.preferredRestrictionVersion ||
joinRule === JoinRule.Restricted
) {
let upgradeRequiredPill;
if (this.state.preferredRestrictionVersion) {
upgradeRequiredPill = <span className="mx_SecurityRoomSettingsTab_upgradeRequired">
{ _t("Upgrade required") }
</span>;
}
let description;
if (joinRule === JoinRule.Restricted && this.state.restrictedAllowRoomIds?.length) {
const shownSpaces = this.state.restrictedAllowRoomIds
.map(roomId => client.getRoom(roomId))
.filter(room => room?.isSpaceRoom())
.slice(0, 4);
let moreText;
if (shownSpaces.length < this.state.restrictedAllowRoomIds.length) {
if (shownSpaces.length > 0) {
moreText = _t("& %(count)s more", {
count: this.state.restrictedAllowRoomIds.length - shownSpaces.length,
});
} else {
moreText = _t("Currently, %(count)s spaces have access", {
count: this.state.restrictedAllowRoomIds.length,
});
}
}
description = <div>
<span>
{ _t("Anyone in a space can find and join. <a>Edit which spaces can access here.</a>", {}, {
a: sub => <AccessibleButton
disabled={!canChangeJoinRule}
onClick={this.onEditRestrictedClick}
kind="link"
>
{ sub }
</AccessibleButton>,
}) }
</span>
<div className="mx_SecurityRoomSettingsTab_spacesWithAccess">
<h4>{ _t("Spaces with access") }</h4>
{ shownSpaces.map(room => {
return <span key={room.roomId}>
<RoomAvatar room={room} height={32} width={32} />
{ room.name }
</span>;
}) }
{ moreText && <span>{ moreText }</span> }
</div>
</div>;
} else if (SpaceStore.instance.activeSpace) {
description = _t("Anyone in %(spaceName)s can find and join. You can select other spaces too.", {
spaceName: SpaceStore.instance.activeSpace.name,
});
} else {
description = _t("Anyone in a space can find and join. You can select multiple spaces.");
}
radioDefinitions.splice(1, 0, {
value: JoinRule.Restricted,
label: <>
{ _t("Space members") }
{ upgradeRequiredPill }
</>,
description,
// if there are 0 allowed spaces then render it as invite only instead
checked: this.state.joinRule === JoinRule.Restricted && !!this.state.restrictedAllowRoomIds?.length,
});
}
return (
<div>
{ guestWarning }
<div className="mx_SecurityRoomSettingsTab_joinRule">
<div className="mx_SettingsTab_subsectionText">
<span>{ _t("Decide who can join %(roomName)s.", {
roomName: client.getRoom(this.props.roomId)?.name,
}) }</span>
</div>
{ aliasWarning }
<StyledRadioGroup
name="roomVis"
name="joinRule"
value={joinRule}
onChange={this.onRoomAccessRadioToggle}
definitions={[
{
value: "invite_only",
disabled: !canChangeAccess,
label: _t('Only people who have been invited'),
checked: joinRule !== "public",
},
{
value: "public_no_guests",
disabled: !canChangeAccess,
label: _t('Anyone who knows the room\'s link, apart from guests'),
checked: joinRule === "public" && guestAccess !== "can_join",
},
{
value: "public_with_guests",
disabled: !canChangeAccess,
label: _t("Anyone who knows the room's link, including guests"),
checked: joinRule === "public" && guestAccess === "can_join",
},
]}
onChange={this.onJoinRuleChange}
definitions={radioDefinitions}
disabled={!canChangeJoinRule}
/>
</div>
);
@ -345,7 +441,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
const client = MatrixClientPeg.get();
const history = this.state.history;
const state = client.getRoom(this.props.roomId).currentState;
const canChangeHistory = state.mayClientSendStateEvent('m.room.history_visibility', client);
const canChangeHistory = state.mayClientSendStateEvent(EventType.RoomHistoryVisibility, client);
const options = [
{
@ -387,11 +483,35 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
);
}
private toggleAdvancedSection = () => {
this.setState({ showAdvancedSection: !this.state.showAdvancedSection });
};
private renderAdvanced() {
const client = MatrixClientPeg.get();
const guestAccess = this.state.guestAccess;
const state = client.getRoom(this.props.roomId).currentState;
const canSetGuestAccess = state.mayClientSendStateEvent(EventType.RoomGuestAccess, client);
return <div className="mx_SettingsTab_section">
<LabelledToggleSwitch
value={guestAccess === GuestAccess.CanJoin}
onChange={this.onGuestAccessChange}
disabled={!canSetGuestAccess}
label={_t("Enable guest access")}
/>
<p>
{ _t("People with supported clients will be able to join " +
"the room without having a registered account.") }
</p>
</div>;
}
render() {
const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId);
const isEncrypted = this.state.encrypted;
const hasEncryptionPermission = room.currentState.mayClientSendStateEvent("m.room.encryption", client);
const hasEncryptionPermission = room.currentState.mayClientSendStateEvent(EventType.RoomEncryption, client);
const canEnableEncryption = !isEncrypted && hasEncryptionPermission;
let encryptionSettings = null;
@ -424,18 +544,30 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
<div className='mx_SettingsTab_subsectionText'>
<span>{ _t("Once enabled, encryption cannot be disabled.") }</span>
</div>
<LabelledToggleSwitch value={isEncrypted} onChange={this.onEncryptionChange}
label={_t("Encrypted")} disabled={!canEnableEncryption}
<LabelledToggleSwitch
value={isEncrypted}
onChange={this.onEncryptionChange}
label={_t("Encrypted")}
disabled={!canEnableEncryption}
/>
</div>
{ encryptionSettings }
</div>
<span className='mx_SettingsTab_subheading'>{ _t("Who can access this room?") }</span>
<span className='mx_SettingsTab_subheading'>{ _t("Access") }</span>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
{ this.renderRoomAccess() }
{ this.renderJoinRule() }
</div>
<AccessibleButton
onClick={this.toggleAdvancedSection}
kind="link"
className="mx_SettingsTab_showAdvanced"
>
{ this.state.showAdvancedSection ? _t("Hide advanced") : _t("Show advanced") }
</AccessibleButton>
{ this.state.showAdvancedSection && this.renderAdvanced() }
{ historySection }
</div>
);

View file

@ -303,9 +303,12 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
/>
<AccessibleButton
onClick={this.onAddCustomTheme}
type="submit" kind="primary_sm"
type="submit"
kind="primary_sm"
disabled={!this.state.customThemeUrl.trim()}
>{ _t("Add theme") }</AccessibleButton>
>
{ _t("Add theme") }
</AccessibleButton>
{ messageElement }
</form>
</div>
@ -393,7 +396,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
<span className="mx_SettingsTab_subheading">{ _t("Message layout") }</span>
<div className="mx_AppearanceUserSettingsTab_Layout_RadioButtons">
<div className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
<label className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: this.state.layout == Layout.IRC,
})}>
<EventTilePreview
@ -412,9 +415,8 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
>
{ _t("IRC") }
</StyledRadioButton>
</div>
<div className="mx_AppearanceUserSettingsTab_spacer" />
<div className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
</label>
<label className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: this.state.layout == Layout.Group,
})}>
<EventTilePreview
@ -433,9 +435,8 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
>
{ _t("Modern") }
</StyledRadioButton>
</div>
<div className="mx_AppearanceUserSettingsTab_spacer" />
<div className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
</label>
<label className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: this.state.layout === Layout.Bubble,
})}>
<EventTilePreview
@ -454,7 +455,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
>
{ _t("Message bubbles") }
</StyledRadioButton>
</div>
</label>
</div>
</div>;
};

View file

@ -426,9 +426,13 @@ export default class GeneralUserSettingsTab extends React.Component {
const supportsMultiLanguageSpellCheck = plaf.supportsMultiLanguageSpellCheck();
const discoWarning = this.state.requiredPolicyInfo.hasTerms
? <img className='mx_GeneralUserSettingsTab_warningIcon'
? <img
className='mx_GeneralUserSettingsTab_warningIcon'
src={require("../../../../../../res/img/feather-customised/warning-triangle.svg")}
width="18" height="18" alt={_t("Warning")} />
width="18"
height="18"
alt={_t("Warning")}
/>
: null;
let accountManagementSection;

View file

@ -134,28 +134,39 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
<span className='mx_SettingsTab_subheading'>{ _t("Credits") }</span>
<ul>
<li>
The <a href="themes/element/img/backgrounds/lake.jpg" rel="noreferrer noopener"
target="_blank">default cover photo</a> is ©&nbsp;
<a href="https://www.flickr.com/golan" rel="noreferrer noopener"
target="_blank">Jesús Roncero</a> used under the terms of&nbsp;
<a href="https://creativecommons.org/licenses/by-sa/4.0/" rel="noreferrer noopener"
target="_blank">CC-BY-SA 4.0</a>.
The <a href="themes/element/img/backgrounds/lake.jpg" rel="noreferrer noopener" target="_blank">
default cover photo
</a> is ©&nbsp;
<a href="https://www.flickr.com/golan" rel="noreferrer noopener" target="_blank">
Jesús Roncero
</a> used under the terms of&nbsp;
<a href="https://creativecommons.org/licenses/by-sa/4.0/" rel="noreferrer noopener" target="_blank">
CC-BY-SA 4.0
</a>.
</li>
<li>
The <a href="https://github.com/matrix-org/twemoji-colr" rel="noreferrer noopener"
target="_blank">twemoji-colr</a> font is ©&nbsp;
<a href="https://mozilla.org" rel="noreferrer noopener"
target="_blank">Mozilla Foundation</a> used under the terms of&nbsp;
<a href="http://www.apache.org/licenses/LICENSE-2.0" rel="noreferrer noopener"
target="_blank">Apache 2.0</a>.
The <a
href="https://github.com/matrix-org/twemoji-colr"
rel="noreferrer noopener"
target="_blank"
>
twemoji-colr
</a> font is ©&nbsp;
<a href="https://mozilla.org" rel="noreferrer noopener" target="_blank">
Mozilla Foundation
</a> used under the terms of&nbsp;
<a href="http://www.apache.org/licenses/LICENSE-2.0" rel="noreferrer noopener" target="_blank">Apache 2.0</a>.
</li>
<li>
The <a href="https://twemoji.twitter.com/" rel="noreferrer noopener"
target="_blank">Twemoji</a> emoji art is ©&nbsp;
<a href="https://twemoji.twitter.com/" rel="noreferrer noopener"
target="_blank">Twitter, Inc and other contributors</a> used under the terms of&nbsp;
<a href="https://creativecommons.org/licenses/by/4.0/" rel="noreferrer noopener"
target="_blank">CC-BY 4.0</a>.
The <a href="https://twemoji.twitter.com/" rel="noreferrer noopener" target="_blank">
Twemoji
</a> emoji art is ©&nbsp;
<a href="https://twemoji.twitter.com/" rel="noreferrer noopener" target="_blank">
Twitter, Inc and other contributors
</a> used under the terms of&nbsp;
<a href="https://creativecommons.org/licenses/by/4.0/" rel="noreferrer noopener" target="_blank">
CC-BY 4.0
</a>.
</li>
</ul>
</div>
@ -254,7 +265,8 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
"<a>Security Disclosure Policy</a>.", {},
{
a: sub => <a href="https://matrix.org/security-disclosure-policy/"
rel="noreferrer noopener" target="_blank"
rel="noreferrer noopener"
target="_blank"
>{ sub }</a>,
},
) }

View file

@ -86,8 +86,11 @@ export default class LabsUserSettingsTab extends React.Component {
'test out new features and help shape them before they actually launch. ' +
'<a>Learn more</a>.', {}, {
'a': (sub) => {
return <a href="https://github.com/vector-im/element-web/blob/develop/docs/labs.md"
rel='noreferrer noopener' target='_blank'>{ sub }</a>;
return <a
href="https://github.com/vector-im/element-web/blob/develop/docs/labs.md"
rel='noreferrer noopener'
target='_blank'
>{ sub }</a>;
},
})
}

View file

@ -57,11 +57,15 @@ export const SpaceAvatar = ({
src={avatar}
alt=""
/>
<AccessibleButton onClick={() => {
avatarUploadRef.current.value = "";
setAvatarDataUrl(undefined);
setAvatar(undefined);
}} kind="link" className="mx_SpaceBasicSettings_avatar_remove">
<AccessibleButton
onClick={() => {
avatarUploadRef.current.value = "";
setAvatarDataUrl(undefined);
setAvatar(undefined);
}}
kind="link"
className="mx_SpaceBasicSettings_avatar_remove"
>
{ _t("Delete") }
</AccessibleButton>
</React.Fragment>;
@ -77,16 +81,21 @@ export const SpaceAvatar = ({
return <div className="mx_SpaceBasicSettings_avatarContainer">
{ avatarSection }
<input type="file" ref={avatarUploadRef} onChange={(e) => {
if (!e.target.files?.length) return;
const file = e.target.files[0];
setAvatar(file);
const reader = new FileReader();
reader.onload = (ev) => {
setAvatarDataUrl(ev.target.result as string);
};
reader.readAsDataURL(file);
}} accept="image/*" />
<input
type="file"
ref={avatarUploadRef}
onChange={(e) => {
if (!e.target.files?.length) return;
const file = e.target.files[0];
setAvatar(file);
const reader = new FileReader();
reader.onload = (ev) => {
setAvatarDataUrl(ev.target.result as string);
};
reader.readAsDataURL(file);
}}
accept="image/*"
/>
</div>;
};

View file

@ -76,7 +76,11 @@ const SpaceButton: React.FC<IButtonProps> = ({
let notifBadge;
if (notificationState) {
notifBadge = <div className="mx_SpacePanel_badgeContainer">
<NotificationBadge forceCount={false} notification={notificationState} />
<NotificationBadge
onClick={() => SpaceStore.instance.setActiveRoomInSpace(space)}
forceCount={false}
notification={notificationState}
/>
</div>;
}

View file

@ -18,13 +18,13 @@ import React, { useState } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { GuestAccess, HistoryVisibility, JoinRule } from "matrix-js-sdk/src/@types/partials";
import { _t } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
import AliasSettings from "../room_settings/AliasSettings";
import { useStateToggle } from "../../../hooks/useStateToggle";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import { GuestAccess, HistoryVisibility, JoinRule } from "../settings/tabs/room/SecurityRoomSettingsTab";
import StyledRadioGroup from "../elements/StyledRadioGroup";
interface IProps {

View file

@ -203,7 +203,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
ev.preventDefault();
ev.stopPropagation();
showSpaceSettings(this.context, this.props.space);
showSpaceSettings(this.props.space);
this.setState({ contextMenuPosition: null }); // also close the menu
};
@ -222,7 +222,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
ev.preventDefault();
ev.stopPropagation();
showCreateNewRoom(this.context, this.props.space);
showCreateNewRoom(this.props.space);
this.setState({ contextMenuPosition: null }); // also close the menu
};
@ -230,7 +230,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
ev.preventDefault();
ev.stopPropagation();
showAddExistingRooms(this.context, this.props.space);
showAddExistingRooms(this.props.space);
this.setState({ contextMenuPosition: null }); // also close the menu
};
@ -285,7 +285,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
let settingsOption;
let leaveSection;
if (shouldShowSpaceSettings(this.context, this.props.space)) {
if (shouldShowSpaceSettings(this.props.space)) {
settingsOption = (
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconSettings"
@ -401,7 +401,11 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
let notifBadge;
if (notificationState) {
notifBadge = <div className="mx_SpacePanel_badgeContainer">
<NotificationBadge forceCount={false} notification={notificationState} />
<NotificationBadge
onClick={() => SpaceStore.instance.setActiveRoomInSpace(space)}
forceCount={false}
notification={notificationState}
/>
</div>;
}

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