Merge remote-tracking branch 'origin/develop' into gsouquet/fix-backdrop-filter
* origin/develop: (1278 commits) Add a little padding Keep number field in focus when pressing dialpad buttons (#6520) Remove old version Fix video call persisting when widget removed Update link to matrix-js-sdk CONTRIBUTING file (#6557) $toast-bg-color -> $system $system-... -> $system Iterate PR based on feedback Remove unnecessary code Use AccessibleTooltipButton Just upload the PR object itself Edit PR Description instead of commenting publish the right directory Fix Netflify builds from fork PRs This doesn't need to be here as it was moved into CallViewButtons Make scrollbar dot transparent Iterate PR based on feedback Don't set hidden RRs labs setting at account level Add a comment for weirdly placed div Add full class names to animations.scss ...
This commit is contained in:
commit
5f9b55eaa9
690 changed files with 26605 additions and 12563 deletions
|
@ -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} />
|
||||
{/* 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} />
|
||||
{ /* 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>;
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
70
src/components/views/audio_messages/AudioPlayerBase.tsx
Normal file
70
src/components/views/audio_messages/AudioPlayerBase.tsx
Normal 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> }
|
||||
</>;
|
||||
}
|
||||
}
|
|
@ -43,6 +43,6 @@ export default class Clock extends React.Component<IProps, IState> {
|
|||
public render() {
|
||||
const minutes = Math.floor(this.props.seconds / 60).toFixed(0).padStart(2, '0');
|
||||
const seconds = Math.floor(this.props.seconds % 60).toFixed(0).padStart(2, '0'); // hide millis
|
||||
return <span className='mx_Clock'>{minutes}:{seconds}</span>;
|
||||
return <span className='mx_Clock'>{ minutes }:{ seconds }</span>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -15,10 +15,9 @@ 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";
|
||||
import { arrayFastResample, arraySeed } from "../../../utils/arrays";
|
||||
import Waveform from "./Waveform";
|
||||
import { MarkedExecution } from "../../../utils/MarkedExecution";
|
||||
|
||||
|
@ -48,18 +47,14 @@ export default class LiveRecordingWaveform extends React.PureComponent<IProps, I
|
|||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
waveform: [],
|
||||
waveform: arraySeed(0, RECORDING_PLAYBACK_SAMPLES),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.recorder.liveData.onUpdate((update: IRecordingUpdate) => {
|
||||
const bars = arrayFastResample(Array.from(update.waveform), RECORDING_PLAYBACK_SAMPLES);
|
||||
// The incoming data is between zero and one, but typically even screaming into a
|
||||
// microphone won't send you over 0.6, so we artificially adjust the gain for the
|
||||
// waveform. This results in a slightly more cinematic/animated waveform for the
|
||||
// user.
|
||||
this.waveform = bars.map(b => percentageOf(b, 0, 0.50));
|
||||
// The incoming data is between zero and one, so we don't need to clamp/rescale it.
|
||||
this.waveform = arrayFastResample(Array.from(update.waveform), RECORDING_PLAYBACK_SAMPLES);
|
||||
this.scheduledUpdate.mark();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -14,51 +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 PlaybackWaveform from "./PlaybackWaveform";
|
||||
import PlayPauseButton from "./PlayPauseButton";
|
||||
import PlaybackClock from "./PlaybackClock";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
// Playback instance to render. Cannot change during component lifecycle: create
|
||||
// an all-new component instead.
|
||||
playback: Playback;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
playbackPhase: PlaybackState;
|
||||
}
|
||||
import { TileShape } from "../rooms/EventTile";
|
||||
import PlaybackWaveform from "./PlaybackWaveform";
|
||||
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 {
|
||||
return <div className='mx_MediaBody mx_VoiceMessagePrimaryContainer'>
|
||||
<PlayPauseButton playback={this.props.playback} playbackPhase={this.state.playbackPhase} />
|
||||
<PlaybackClock playback={this.props.playback} />
|
||||
<PlaybackWaveform playback={this.props.playback} />
|
||||
</div>;
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -47,17 +47,21 @@ export default class Waveform extends React.PureComponent<IProps, IState> {
|
|||
|
||||
public render() {
|
||||
return <div className='mx_Waveform'>
|
||||
{this.props.relHeights.map((h, i) => {
|
||||
{ this.props.relHeights.map((h, i) => {
|
||||
const progress = this.props.progress;
|
||||
const isCompleteBar = (i / this.props.relHeights.length) <= progress && progress > 0;
|
||||
const classes = classNames({
|
||||
'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>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
|||
|
||||
@replaceableComponent("views.auth.AuthBody")
|
||||
export default class AuthBody extends React.PureComponent {
|
||||
render() {
|
||||
public render(): React.ReactNode {
|
||||
return <div className="mx_AuthBody">
|
||||
{ this.props.children }
|
||||
</div>;
|
|
@ -22,7 +22,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
|||
|
||||
@replaceableComponent("views.auth.AuthFooter")
|
||||
export default class AuthFooter extends React.Component {
|
||||
render() {
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<div className="mx_AuthFooter">
|
||||
<a href="https://matrix.org" target="_blank" rel="noreferrer noopener">{ _t("powered by Matrix") }</a>
|
|
@ -16,20 +16,17 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import AuthHeaderLogo from "./AuthHeaderLogo";
|
||||
import LanguageSelector from "./LanguageSelector";
|
||||
|
||||
interface IProps {
|
||||
disableLanguageSelector?: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.AuthHeader")
|
||||
export default class AuthHeader extends React.Component {
|
||||
static propTypes = {
|
||||
disableLanguageSelector: PropTypes.bool,
|
||||
};
|
||||
|
||||
render() {
|
||||
const AuthHeaderLogo = sdk.getComponent('auth.AuthHeaderLogo');
|
||||
const LanguageSelector = sdk.getComponent('views.auth.LanguageSelector');
|
||||
|
||||
export default class AuthHeader extends React.Component<IProps> {
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<div className="mx_AuthHeader">
|
||||
<AuthHeaderLogo />
|
|
@ -19,7 +19,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
|||
|
||||
@replaceableComponent("views.auth.AuthHeaderLogo")
|
||||
export default class AuthHeaderLogo extends React.PureComponent {
|
||||
render() {
|
||||
public render(): React.ReactNode {
|
||||
return <div className="mx_AuthHeaderLogo">
|
||||
Matrix
|
||||
</div>;
|
|
@ -17,18 +17,16 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import * as sdk from '../../../index';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import AuthFooter from "./AuthFooter";
|
||||
|
||||
@replaceableComponent("views.auth.AuthPage")
|
||||
export default class AuthPage extends React.PureComponent {
|
||||
render() {
|
||||
const AuthFooter = sdk.getComponent('auth.AuthFooter');
|
||||
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<div className="mx_AuthPage">
|
||||
<div className="mx_AuthPage_modal">
|
||||
{this.props.children}
|
||||
{ this.props.children }
|
||||
</div>
|
||||
<AuthFooter />
|
||||
</div>
|
|
@ -15,66 +15,74 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, { createRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
const DIV_ID = 'mx_recaptcha';
|
||||
|
||||
interface ICaptchaFormProps {
|
||||
sitePublicKey: string;
|
||||
onCaptchaResponse: (response: string) => void;
|
||||
}
|
||||
|
||||
interface ICaptchaFormState {
|
||||
errorText?: string;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A pure UI component which displays a captcha form.
|
||||
*/
|
||||
@replaceableComponent("views.auth.CaptchaForm")
|
||||
export default class CaptchaForm extends React.Component {
|
||||
static propTypes = {
|
||||
sitePublicKey: PropTypes.string,
|
||||
|
||||
// called with the captcha response
|
||||
onCaptchaResponse: PropTypes.func,
|
||||
};
|
||||
|
||||
export default class CaptchaForm extends React.Component<ICaptchaFormProps, ICaptchaFormState> {
|
||||
static defaultProps = {
|
||||
onCaptchaResponse: () => {},
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
private captchaWidgetId?: string;
|
||||
private recaptchaContainer = createRef<HTMLDivElement>();
|
||||
|
||||
constructor(props: ICaptchaFormProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
errorText: null,
|
||||
errorText: undefined,
|
||||
};
|
||||
|
||||
this._captchaWidgetId = null;
|
||||
|
||||
this._recaptchaContainer = createRef();
|
||||
|
||||
CountlyAnalytics.instance.track("onboarding_grecaptcha_begin");
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Just putting a script tag into the returned jsx doesn't work, annoyingly,
|
||||
// so we do this instead.
|
||||
if (global.grecaptcha) {
|
||||
if (this.isRecaptchaReady()) {
|
||||
// already loaded
|
||||
this._onCaptchaLoaded();
|
||||
this.onCaptchaLoaded();
|
||||
} else {
|
||||
console.log("Loading recaptcha script...");
|
||||
window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded();};
|
||||
window.mxOnRecaptchaLoaded = () => { this.onCaptchaLoaded(); };
|
||||
const scriptTag = document.createElement('script');
|
||||
scriptTag.setAttribute(
|
||||
'src', `https://www.recaptcha.net/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit`,
|
||||
'src', `https://www.recaptcha.net/recaptcha/api.js?onload=mxOnRecaptchaLoaded&render=explicit`,
|
||||
);
|
||||
this._recaptchaContainer.current.appendChild(scriptTag);
|
||||
this.recaptchaContainer.current.appendChild(scriptTag);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._resetRecaptcha();
|
||||
this.resetRecaptcha();
|
||||
}
|
||||
|
||||
_renderRecaptcha(divId) {
|
||||
if (!global.grecaptcha) {
|
||||
// Borrowed directly from: https://github.com/codeep/react-recaptcha-google/commit/e118fa5670fa268426969323b2e7fe77698376ba
|
||||
private isRecaptchaReady(): boolean {
|
||||
return typeof window !== "undefined" &&
|
||||
typeof global.grecaptcha !== "undefined" &&
|
||||
typeof global.grecaptcha.render === 'function';
|
||||
}
|
||||
|
||||
private renderRecaptcha(divId: string) {
|
||||
if (!this.isRecaptchaReady()) {
|
||||
console.error("grecaptcha not loaded!");
|
||||
throw new Error("Recaptcha did not load successfully");
|
||||
}
|
||||
|
@ -84,26 +92,26 @@ export default class CaptchaForm extends React.Component {
|
|||
console.error("No public key for recaptcha!");
|
||||
throw new Error(
|
||||
"This server has not supplied enough information for Recaptcha "
|
||||
+ "authentication");
|
||||
+ "authentication");
|
||||
}
|
||||
|
||||
console.info("Rendering to %s", divId);
|
||||
this._captchaWidgetId = global.grecaptcha.render(divId, {
|
||||
this.captchaWidgetId = global.grecaptcha.render(divId, {
|
||||
sitekey: publicKey,
|
||||
callback: this.props.onCaptchaResponse,
|
||||
});
|
||||
}
|
||||
|
||||
_resetRecaptcha() {
|
||||
if (this._captchaWidgetId !== null) {
|
||||
global.grecaptcha.reset(this._captchaWidgetId);
|
||||
private resetRecaptcha() {
|
||||
if (this.captchaWidgetId) {
|
||||
global?.grecaptcha?.reset(this.captchaWidgetId);
|
||||
}
|
||||
}
|
||||
|
||||
_onCaptchaLoaded() {
|
||||
private onCaptchaLoaded() {
|
||||
console.log("Loaded recaptcha script.");
|
||||
try {
|
||||
this._renderRecaptcha(DIV_ID);
|
||||
this.renderRecaptcha(DIV_ID);
|
||||
// clear error if re-rendered
|
||||
this.setState({
|
||||
errorText: null,
|
||||
|
@ -128,10 +136,10 @@ export default class CaptchaForm extends React.Component {
|
|||
}
|
||||
|
||||
return (
|
||||
<div ref={this._recaptchaContainer}>
|
||||
<p>{_t(
|
||||
<div ref={this.recaptchaContainer}>
|
||||
<p>{ _t(
|
||||
"This homeserver would like to make sure you are not a robot.",
|
||||
)}</p>
|
||||
) }</p>
|
||||
<div id={DIV_ID} />
|
||||
{ error }
|
||||
</div>
|
|
@ -19,7 +19,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
|||
|
||||
@replaceableComponent("views.auth.CompleteSecurityBody")
|
||||
export default class CompleteSecurityBody extends React.PureComponent {
|
||||
render() {
|
||||
public render(): React.ReactNode {
|
||||
return <div className="mx_CompleteSecurityBody">
|
||||
{ this.props.children }
|
||||
</div>;
|
|
@ -15,21 +15,19 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
|
||||
import { COUNTRIES, getEmojiFlag } from '../../../phonenumber';
|
||||
import { COUNTRIES, getEmojiFlag, PhoneNumberCountryDefinition } from '../../../phonenumber';
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Dropdown from "../elements/Dropdown";
|
||||
|
||||
const COUNTRIES_BY_ISO2 = {};
|
||||
for (const c of COUNTRIES) {
|
||||
COUNTRIES_BY_ISO2[c.iso2] = c;
|
||||
}
|
||||
|
||||
function countryMatchesSearchQuery(query, country) {
|
||||
function countryMatchesSearchQuery(query: string, country: PhoneNumberCountryDefinition): boolean {
|
||||
// Remove '+' if present (when searching for a prefix)
|
||||
if (query[0] === '+') {
|
||||
query = query.slice(1);
|
||||
|
@ -41,15 +39,26 @@ function countryMatchesSearchQuery(query, country) {
|
|||
return false;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.CountryDropdown")
|
||||
export default class CountryDropdown extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._onSearchChange = this._onSearchChange.bind(this);
|
||||
this._onOptionChange = this._onOptionChange.bind(this);
|
||||
this._getShortOption = this._getShortOption.bind(this);
|
||||
interface IProps {
|
||||
value?: string;
|
||||
onOptionChange: (country: PhoneNumberCountryDefinition) => void;
|
||||
isSmall: boolean; // if isSmall, show +44 in the selected value
|
||||
showPrefix: boolean;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
let defaultCountry = COUNTRIES[0];
|
||||
interface IState {
|
||||
searchQuery: string;
|
||||
defaultCountry: PhoneNumberCountryDefinition;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.CountryDropdown")
|
||||
export default class CountryDropdown extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
let defaultCountry: PhoneNumberCountryDefinition = COUNTRIES[0];
|
||||
const defaultCountryCode = SdkConfig.get()["defaultCountryCode"];
|
||||
if (defaultCountryCode) {
|
||||
const country = COUNTRIES.find(c => c.iso2 === defaultCountryCode.toUpperCase());
|
||||
|
@ -62,7 +71,7 @@ export default class CountryDropdown extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
public componentDidMount(): void {
|
||||
if (!this.props.value) {
|
||||
// If no value is given, we start with the default
|
||||
// country selected, but our parent component
|
||||
|
@ -71,21 +80,21 @@ export default class CountryDropdown extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_onSearchChange(search) {
|
||||
private onSearchChange = (search: string): void => {
|
||||
this.setState({
|
||||
searchQuery: search,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_onOptionChange(iso2) {
|
||||
private onOptionChange = (iso2: string): void => {
|
||||
this.props.onOptionChange(COUNTRIES_BY_ISO2[iso2]);
|
||||
}
|
||||
};
|
||||
|
||||
_flagImgForIso2(iso2) {
|
||||
private flagImgForIso2(iso2: string): React.ReactNode {
|
||||
return <div className="mx_Dropdown_option_emoji">{ getEmojiFlag(iso2) }</div>;
|
||||
}
|
||||
|
||||
_getShortOption(iso2) {
|
||||
private getShortOption = (iso2: string): React.ReactNode => {
|
||||
if (!this.props.isSmall) {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -94,14 +103,12 @@ export default class CountryDropdown extends React.Component {
|
|||
countryPrefix = '+' + COUNTRIES_BY_ISO2[iso2].prefix;
|
||||
}
|
||||
return <span className="mx_CountryDropdown_shortOption">
|
||||
{ this._flagImgForIso2(iso2) }
|
||||
{ this.flagImgForIso2(iso2) }
|
||||
{ countryPrefix }
|
||||
</span>;
|
||||
}
|
||||
|
||||
render() {
|
||||
const Dropdown = sdk.getComponent('elements.Dropdown');
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
let displayedCountries;
|
||||
if (this.state.searchQuery) {
|
||||
displayedCountries = COUNTRIES.filter(
|
||||
|
@ -124,7 +131,7 @@ export default class CountryDropdown extends React.Component {
|
|||
|
||||
const options = displayedCountries.map((country) => {
|
||||
return <div className="mx_CountryDropdown_option" key={country.iso2}>
|
||||
{ this._flagImgForIso2(country.iso2) }
|
||||
{ this.flagImgForIso2(country.iso2) }
|
||||
{ _t(country.name) } (+{ country.prefix })
|
||||
</div>;
|
||||
});
|
||||
|
@ -136,10 +143,10 @@ export default class CountryDropdown extends React.Component {
|
|||
return <Dropdown
|
||||
id="mx_CountryDropdown"
|
||||
className={this.props.className + " mx_CountryDropdown"}
|
||||
onOptionChange={this._onOptionChange}
|
||||
onSearchChange={this._onSearchChange}
|
||||
onOptionChange={this.onOptionChange}
|
||||
onSearchChange={this.onSearchChange}
|
||||
menuWidth={298}
|
||||
getShortOption={this._getShortOption}
|
||||
getShortOption={this.getShortOption}
|
||||
value={value}
|
||||
searchEnabled={true}
|
||||
disabled={this.props.disabled}
|
||||
|
@ -149,13 +156,3 @@ export default class CountryDropdown extends React.Component {
|
|||
</Dropdown>;
|
||||
}
|
||||
}
|
||||
|
||||
CountryDropdown.propTypes = {
|
||||
className: PropTypes.string,
|
||||
isSmall: PropTypes.bool,
|
||||
// if isSmall, show +44 in the selected value
|
||||
showPrefix: PropTypes.bool,
|
||||
onOptionChange: PropTypes.func.isRequired,
|
||||
value: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
|
@ -41,7 +41,7 @@ import CaptchaForm from "./CaptchaForm";
|
|||
* one HS whilst beign a guest on another).
|
||||
* loginType: the login type of the auth stage being attempted
|
||||
* authSessionId: session id from the server
|
||||
* clientSecret: The client secret in use for ID server auth sessions
|
||||
* clientSecret: The client secret in use for identity server auth sessions
|
||||
* stageParams: params from the server for the stage being attempted
|
||||
* errorText: error message from a previous attempt to authenticate
|
||||
* submitAuthDict: a function which will be called with the new auth dict
|
||||
|
@ -54,8 +54,8 @@ import CaptchaForm from "./CaptchaForm";
|
|||
* Defined keys for stages are:
|
||||
* m.login.email.identity:
|
||||
* * emailSid: string representing the sid of the active
|
||||
* verification session from the ID server, or
|
||||
* null if no session is active.
|
||||
* verification session from the identity server,
|
||||
* or null if no session is active.
|
||||
* fail: a function which should be called with an error object if an
|
||||
* error occurred during the auth stage. This will cause the auth
|
||||
* session to be failed and the process to go back to the start.
|
||||
|
@ -416,13 +416,15 @@ 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 (
|
||||
<div>
|
||||
<p>{_t("Please review and accept the policies of this homeserver:")}</p>
|
||||
<p>{ _t("Please review and accept the policies of this homeserver:") }</p>
|
||||
{ checkboxes }
|
||||
{ errorSection }
|
||||
{ submitButton }
|
||||
|
@ -613,15 +615,17 @@ export class MsisdnAuthEntry extends React.Component<IMsisdnAuthEntryProps, IMsi
|
|||
className="mx_InteractiveAuthEntryComponents_msisdnEntry"
|
||||
value={this.state.token}
|
||||
onChange={this.onTokenChange}
|
||||
aria-label={ _t("Code")}
|
||||
aria-label={_t("Code")}
|
||||
/>
|
||||
<br />
|
||||
<input type="submit" value={_t("Submit")}
|
||||
<input
|
||||
type="submit"
|
||||
value={_t("Submit")}
|
||||
className={submitClasses}
|
||||
disabled={!enableSubmit}
|
||||
/>
|
||||
</form>
|
||||
{errorSection}
|
||||
{ errorSection }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -717,21 +721,21 @@ export class SSOAuthEntry extends React.Component<ISSOAuthEntryProps, ISSOAuthEn
|
|||
<AccessibleButton
|
||||
onClick={this.props.onCancel}
|
||||
kind={this.props.continueKind ? (this.props.continueKind + '_outline') : 'primary_outline'}
|
||||
>{_t("Cancel")}</AccessibleButton>
|
||||
>{ _t("Cancel") }</AccessibleButton>
|
||||
);
|
||||
if (this.state.phase === SSOAuthEntry.PHASE_PREAUTH) {
|
||||
continueButton = (
|
||||
<AccessibleButton
|
||||
onClick={this.onStartAuthClick}
|
||||
kind={this.props.continueKind || 'primary'}
|
||||
>{this.props.continueText || _t("Single Sign On")}</AccessibleButton>
|
||||
>{ this.props.continueText || _t("Single Sign On") }</AccessibleButton>
|
||||
);
|
||||
} else {
|
||||
continueButton = (
|
||||
<AccessibleButton
|
||||
onClick={this.onConfirmClick}
|
||||
kind={this.props.continueKind || 'primary'}
|
||||
>{this.props.continueText || _t("Confirm")}</AccessibleButton>
|
||||
>{ this.props.continueText || _t("Confirm") }</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -753,8 +757,8 @@ export class SSOAuthEntry extends React.Component<ISSOAuthEntryProps, ISSOAuthEn
|
|||
return <React.Fragment>
|
||||
{ errorSection }
|
||||
<div className="mx_InteractiveAuthEntryComponents_sso_buttons">
|
||||
{cancelButton}
|
||||
{continueButton}
|
||||
{ cancelButton }
|
||||
{ continueButton }
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
@ -825,7 +829,7 @@ export class FallbackAuthEntry extends React.Component<IAuthEntryProps> {
|
|||
<a href="" ref={this.fallbackButton} onClick={this.onShowFallbackClick}>{
|
||||
_t("Start authentication")
|
||||
}</a>
|
||||
{errorSection}
|
||||
{ errorSection }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,21 +18,23 @@ import SdkConfig from "../../../SdkConfig";
|
|||
import { getCurrentLanguage } from "../../../languageHandler";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import PlatformPeg from "../../../PlatformPeg";
|
||||
import * as sdk from '../../../index';
|
||||
import React from 'react';
|
||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||
import LanguageDropdown from "../elements/LanguageDropdown";
|
||||
|
||||
function onChange(newLang) {
|
||||
function onChange(newLang: string): void {
|
||||
if (getCurrentLanguage() !== newLang) {
|
||||
SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang);
|
||||
PlatformPeg.get().reload();
|
||||
}
|
||||
}
|
||||
|
||||
export default function LanguageSelector({ disabled }) {
|
||||
if (SdkConfig.get()['disable_login_language_selector']) return <div />;
|
||||
interface IProps {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown');
|
||||
export default function LanguageSelector({ disabled }: IProps): JSX.Element {
|
||||
if (SdkConfig.get()['disable_login_language_selector']) return <div />;
|
||||
return <LanguageDropdown
|
||||
className="mx_AuthBody_language"
|
||||
onOptionChange={onChange}
|
|
@ -416,7 +416,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
|||
kind="link"
|
||||
onClick={this.onForgotPasswordClick}
|
||||
>
|
||||
{_t("Forgot password?")}
|
||||
{ _t("Forgot password?") }
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
|
@ -441,16 +441,16 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
|||
disabled={this.props.disableSubmit}
|
||||
>
|
||||
<option key={LoginField.MatrixId} value={LoginField.MatrixId}>
|
||||
{_t('Username')}
|
||||
{ _t('Username') }
|
||||
</option>
|
||||
<option
|
||||
key={LoginField.Email}
|
||||
value={LoginField.Email}
|
||||
>
|
||||
{_t('Email address')}
|
||||
{ _t('Email address') }
|
||||
</option>
|
||||
<option key={LoginField.Password} value={LoginField.Password}>
|
||||
{_t('Phone')}
|
||||
{ _t('Phone') }
|
||||
</option>
|
||||
</Field>
|
||||
</div>
|
||||
|
@ -460,8 +460,8 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
|||
return (
|
||||
<div>
|
||||
<form onSubmit={this.onSubmitForm}>
|
||||
{loginType}
|
||||
{loginField}
|
||||
{ loginType }
|
||||
{ loginField }
|
||||
<Field
|
||||
className={pwFieldClass}
|
||||
type="password"
|
||||
|
@ -474,7 +474,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
|||
onValidate={this.onPasswordValidate}
|
||||
ref={field => this[LoginField.Password] = field}
|
||||
/>
|
||||
{forgotPasswordJsx}
|
||||
{ forgotPasswordJsx }
|
||||
{ !this.props.busy && <input className="mx_Login_submit"
|
||||
type="submit"
|
||||
value={_t('Sign in')}
|
||||
|
|
|
@ -537,15 +537,15 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
|
|||
<div>
|
||||
<form onSubmit={this.onSubmit}>
|
||||
<div className="mx_AuthBody_fieldRow">
|
||||
{this.renderUsername()}
|
||||
{ this.renderUsername() }
|
||||
</div>
|
||||
<div className="mx_AuthBody_fieldRow">
|
||||
{this.renderPassword()}
|
||||
{this.renderPasswordConfirm()}
|
||||
{ this.renderPassword() }
|
||||
{ this.renderPasswordConfirm() }
|
||||
</div>
|
||||
<div className="mx_AuthBody_fieldRow">
|
||||
{this.renderEmail()}
|
||||
{this.renderPhoneNumber()}
|
||||
{ this.renderEmail() }
|
||||
{ this.renderPhoneNumber() }
|
||||
</div>
|
||||
{ emailHelperText }
|
||||
{ registerButton }
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import classNames from "classnames";
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import * as sdk from "../../../index";
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import AuthPage from "./AuthPage";
|
||||
import { _td } from "../../../languageHandler";
|
||||
|
@ -25,21 +25,26 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import LanguageSelector from "./LanguageSelector";
|
||||
|
||||
// translatable strings for Welcome pages
|
||||
_td("Sign in with SSO");
|
||||
|
||||
interface IProps {
|
||||
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.Welcome")
|
||||
export default class Welcome extends React.PureComponent {
|
||||
constructor(props) {
|
||||
export default class Welcome extends React.PureComponent<IProps> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
CountlyAnalytics.instance.track("onboarding_welcome");
|
||||
}
|
||||
|
||||
render() {
|
||||
const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage');
|
||||
const LanguageSelector = sdk.getComponent('auth.LanguageSelector');
|
||||
public render(): React.ReactNode {
|
||||
// FIXME: Using an import will result in wrench-element-tests failures
|
||||
const EmbeddedPage = sdk.getComponent("structures.EmbeddedPage");
|
||||
|
||||
const pagesConfig = SdkConfig.get().embeddedPages;
|
||||
let pageUrl = null;
|
|
@ -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} />
|
||||
);
|
||||
|
|
|
@ -205,8 +205,8 @@ export default class DecoratedRoomAvatar extends React.PureComponent<IProps, ISt
|
|||
oobData={this.props.oobData}
|
||||
viewAvatarOnClick={this.props.viewAvatarOnClick}
|
||||
/>
|
||||
{icon}
|
||||
{badge}
|
||||
{ icon }
|
||||
{ badge }
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -145,7 +145,7 @@ export default class MemberStatusMessageAvatar extends React.Component {
|
|||
isExpanded={this.state.menuDisplayed}
|
||||
label={_t("User Status")}
|
||||
>
|
||||
{avatar}
|
||||
{ avatar }
|
||||
</ContextMenuButton>
|
||||
|
||||
{ contextMenu }
|
||||
|
|
|
@ -13,15 +13,18 @@ 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';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import Modal from '../../../Modal';
|
||||
import * as Avatar from '../../../Avatar';
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import { IOOBData } from '../../../stores/ThreepidInviteStore';
|
||||
|
@ -31,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;
|
||||
}
|
||||
|
||||
|
@ -128,14 +134,21 @@ 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) : oobData.roomId;
|
||||
|
||||
return (
|
||||
<BaseAvatar {...otherProps}
|
||||
<BaseAvatar
|
||||
{...otherProps}
|
||||
className={classNames(className, {
|
||||
mx_RoomAvatar_isSpaceRoom: room?.isSpaceRoom(),
|
||||
})}
|
||||
name={roomName}
|
||||
idName={room ? room.roomId : null}
|
||||
idName={idName}
|
||||
urls={this.state.urls}
|
||||
onClick={viewAvatarOnClick && this.state.urls[0] ? this.onRoomAvatarClick : onClick}
|
||||
/>
|
||||
|
|
|
@ -27,6 +27,8 @@ import BetaFeedbackDialog from "../dialogs/BetaFeedbackDialog";
|
|||
import SdkConfig from "../../../SdkConfig";
|
||||
import SettingsFlag from "../elements/SettingsFlag";
|
||||
|
||||
// XXX: Keep this around for re-use in future Betas
|
||||
|
||||
interface IProps {
|
||||
title?: string;
|
||||
featureId: string;
|
||||
|
@ -105,7 +107,7 @@ const BetaCard = ({ title: titleOverride, featureId }: IProps) => {
|
|||
</div>
|
||||
<img src={image} alt="" />
|
||||
</div>
|
||||
{ extraSettings && <div className="mx_BetaCard_relatedSettings">
|
||||
{ extraSettings && value && <div className="mx_BetaCard_relatedSettings">
|
||||
{ extraSettings.map(key => (
|
||||
<SettingsFlag key={key} name={key} level={SettingLevel.DEVICE} />
|
||||
)) }
|
||||
|
|
|
@ -53,7 +53,7 @@ export default class CallContextMenu extends React.Component<IProps> {
|
|||
onTransferClick = () => {
|
||||
Modal.createTrackedDialog(
|
||||
'Transfer Call', '', InviteDialog, { kind: KIND_CALL_TRANSFER, call: this.props.call },
|
||||
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
|
||||
/*className=*/"mx_InviteDialog_transferWrapper", /*isPriority=*/false, /*isStatic=*/true,
|
||||
);
|
||||
this.props.onFinished();
|
||||
};
|
||||
|
@ -65,15 +65,15 @@ export default class CallContextMenu extends React.Component<IProps> {
|
|||
let transferItem;
|
||||
if (this.props.call.opponentCanBeTransferred()) {
|
||||
transferItem = <MenuItem className="mx_CallContextMenu_item" onClick={this.onTransferClick}>
|
||||
{_t("Transfer")}
|
||||
{ _t("Transfer") }
|
||||
</MenuItem>;
|
||||
}
|
||||
|
||||
return <ContextMenu {...this.props}>
|
||||
<MenuItem className="mx_CallContextMenu_item" onClick={handler}>
|
||||
{holdUnholdCaption}
|
||||
{ holdUnholdCaption }
|
||||
</MenuItem>
|
||||
{transferItem}
|
||||
{ transferItem }
|
||||
</ContextMenu>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,12 +14,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as React from "react";
|
||||
import { createRef } from "react";
|
||||
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import { ContextMenu, IProps as IContextMenuProps } from '../../structures/ContextMenu';
|
||||
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import Field from "../elements/Field";
|
||||
import Dialpad from '../voip/DialPad';
|
||||
import DialPad from '../voip/DialPad';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps extends IContextMenuProps {
|
||||
|
@ -32,6 +33,8 @@ interface IState {
|
|||
|
||||
@replaceableComponent("views.context_menus.DialpadContextMenu")
|
||||
export default class DialpadContextMenu extends React.Component<IProps, IState> {
|
||||
private numberEntryFieldRef: React.RefObject<Field> = createRef();
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
|
@ -40,9 +43,27 @@ export default class DialpadContextMenu extends React.Component<IProps, IState>
|
|||
};
|
||||
}
|
||||
|
||||
onDigitPress = (digit) => {
|
||||
onDigitPress = (digit: string, ev: ButtonEvent) => {
|
||||
this.props.call.sendDtmfDigit(digit);
|
||||
this.setState({ value: this.state.value + digit });
|
||||
|
||||
// Keep the number field focused so that keyboard entry is still available
|
||||
// However, don't focus if this wasn't the result of directly clicking on the button,
|
||||
// i.e someone using keyboard navigation.
|
||||
if (ev.type === "click") {
|
||||
this.numberEntryFieldRef.current?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
onCancelClick = () => {
|
||||
this.props.onFinished();
|
||||
};
|
||||
|
||||
onKeyDown = (ev) => {
|
||||
// Prevent Backspace and Delete keys from functioning in the entry field
|
||||
if (ev.code === "Backspace" || ev.code === "Delete") {
|
||||
ev.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
onChange = (ev) => {
|
||||
|
@ -51,18 +72,23 @@ export default class DialpadContextMenu extends React.Component<IProps, IState>
|
|||
|
||||
render() {
|
||||
return <ContextMenu {...this.props}>
|
||||
<div className="mx_DialPadContextMenu_header">
|
||||
<div className="mx_DialPadContextMenuWrapper">
|
||||
<div>
|
||||
<span className="mx_DialPadContextMenu_title">{_t("Dial pad")}</span>
|
||||
<AccessibleButton className="mx_DialPadContextMenu_cancel" onClick={this.onCancelClick} />
|
||||
</div>
|
||||
<div className="mx_DialPadContextMenu_header">
|
||||
<Field
|
||||
ref={this.numberEntryFieldRef}
|
||||
className="mx_DialPadContextMenu_dialled"
|
||||
value={this.state.value}
|
||||
autoFocus={true}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_DialPadContextMenu_dialPad">
|
||||
<DialPad onDigitPress={this.onDigitPress} hasDial={false} />
|
||||
</div>
|
||||
<Field className="mx_DialPadContextMenu_dialled"
|
||||
value={this.state.value} autoFocus={true}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_DialPadContextMenu_horizSep" />
|
||||
<div className="mx_DialPadContextMenu_dialPad">
|
||||
<Dialpad onDigitPress={this.onDigitPress} hasDialAndDelete={false} />
|
||||
</div>
|
||||
</ContextMenu>;
|
||||
}
|
||||
|
|
|
@ -64,8 +64,8 @@ export const IconizedContextMenuRadio: React.FC<IRadioProps> = ({
|
|||
label={label}
|
||||
>
|
||||
<span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} />
|
||||
<span className="mx_IconizedContextMenu_label">{label}</span>
|
||||
{active && <span className="mx_IconizedContextMenu_icon mx_IconizedContextMenu_checked" />}
|
||||
<span className="mx_IconizedContextMenu_label">{ label }</span>
|
||||
{ active && <span className="mx_IconizedContextMenu_icon mx_IconizedContextMenu_checked" /> }
|
||||
</MenuItemRadio>;
|
||||
};
|
||||
|
||||
|
@ -85,15 +85,19 @@ export const IconizedContextMenuCheckbox: React.FC<ICheckboxProps> = ({
|
|||
label={label}
|
||||
>
|
||||
<span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} />
|
||||
<span className="mx_IconizedContextMenu_label">{label}</span>
|
||||
{active && <span className="mx_IconizedContextMenu_icon mx_IconizedContextMenu_checked" />}
|
||||
<span className="mx_IconizedContextMenu_label">{ label }</span>
|
||||
<span className={classNames("mx_IconizedContextMenu_icon", {
|
||||
mx_IconizedContextMenu_checked: active,
|
||||
mx_IconizedContextMenu_unchecked: !active,
|
||||
})} />
|
||||
</MenuItemCheckbox>;
|
||||
};
|
||||
|
||||
export const IconizedContextMenuOption: React.FC<IOptionProps> = ({ label, iconClassName, ...props }) => {
|
||||
export const IconizedContextMenuOption: React.FC<IOptionProps> = ({ label, iconClassName, children, ...props }) => {
|
||||
return <MenuItem {...props} label={label}>
|
||||
{ iconClassName && <span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} /> }
|
||||
<span className="mx_IconizedContextMenu_label">{label}</span>
|
||||
<span className="mx_IconizedContextMenu_label">{ label }</span>
|
||||
{ children }
|
||||
</MenuItem>;
|
||||
};
|
||||
|
||||
|
@ -104,7 +108,7 @@ export const IconizedContextMenuOptionList: React.FC<IOptionListProps> = ({ firs
|
|||
});
|
||||
|
||||
return <div className={classes}>
|
||||
{children}
|
||||
{ children }
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
|
|
@ -43,11 +43,15 @@ export function canCancel(eventStatus: EventStatus): boolean {
|
|||
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
|
||||
}
|
||||
|
||||
interface IEventTileOps {
|
||||
export interface IEventTileOps {
|
||||
isWidgetHidden(): boolean;
|
||||
unhideWidget(): void;
|
||||
}
|
||||
|
||||
export interface IOperableEventTile {
|
||||
getEventTileOps(): IEventTileOps;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
/* the MatrixEvent associated with the context menu */
|
||||
mxEvent: MatrixEvent;
|
||||
|
@ -268,7 +272,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
|||
resendReactionsButton = (
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_MessageContextMenu_iconResend"
|
||||
label={ _t('Resend %(unsentCount)s reaction(s)', { unsentCount: unsentReactionsCount }) }
|
||||
label={_t('Resend %(unsentCount)s reaction(s)', { unsentCount: unsentReactionsCount })}
|
||||
onClick={this.onResendReactionsClick}
|
||||
/>
|
||||
);
|
||||
|
@ -298,7 +302,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
|||
pinButton = (
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_MessageContextMenu_iconPin"
|
||||
label={ this.isPinned() ? _t('Unpin') : _t('Pin') }
|
||||
label={this.isPinned() ? _t('Unpin') : _t('Pin')}
|
||||
onClick={this.onPinClick}
|
||||
/>
|
||||
);
|
||||
|
@ -333,7 +337,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
|||
<IconizedContextMenuOption
|
||||
iconClassName="mx_MessageContextMenu_iconPermalink"
|
||||
onClick={this.onPermalinkClick}
|
||||
label= {_t('Share')}
|
||||
label={_t('Share')}
|
||||
element="a"
|
||||
{
|
||||
// XXX: Typescript signature for AccessibleButton doesn't work properly for non-inputs like `a`
|
||||
|
@ -364,7 +368,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
|||
<IconizedContextMenuOption
|
||||
iconClassName="mx_MessageContextMenu_iconLink"
|
||||
onClick={this.closeMenu}
|
||||
label={ _t('Source URL') }
|
||||
label={_t('Source URL')}
|
||||
element="a"
|
||||
{
|
||||
// XXX: Typescript signature for AccessibleButton doesn't work properly for non-inputs like `a`
|
||||
|
|
216
src/components/views/context_menus/SpaceContextMenu.tsx
Normal file
216
src/components/views/context_menus/SpaceContextMenu.tsx
Normal file
|
@ -0,0 +1,216 @@
|
|||
/*
|
||||
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, { useContext } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
import {
|
||||
IProps as IContextMenuProps,
|
||||
} from "../../structures/ContextMenu";
|
||||
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import {
|
||||
leaveSpace,
|
||||
shouldShowSpaceSettings,
|
||||
showAddExistingRooms,
|
||||
showCreateNewRoom,
|
||||
showCreateNewSubspace,
|
||||
showSpaceInvite,
|
||||
showSpaceSettings,
|
||||
} from "../../../utils/space";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import RoomViewStore from "../../../stores/RoomViewStore";
|
||||
import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||
import { BetaPill } from "../beta/BetaCard";
|
||||
|
||||
interface IProps extends IContextMenuProps {
|
||||
space: Room;
|
||||
}
|
||||
|
||||
const SpaceContextMenu = ({ space, onFinished, ...props }: IProps) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const userId = cli.getUserId();
|
||||
|
||||
let inviteOption;
|
||||
if (space.getJoinRule() === "public" || space.canInvite(userId)) {
|
||||
const onInviteClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
showSpaceInvite(space);
|
||||
onFinished();
|
||||
};
|
||||
|
||||
inviteOption = (
|
||||
<IconizedContextMenuOption
|
||||
className="mx_SpacePanel_contextMenu_inviteButton"
|
||||
iconClassName="mx_SpacePanel_iconInvite"
|
||||
label={_t("Invite people")}
|
||||
onClick={onInviteClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let settingsOption;
|
||||
let leaveSection;
|
||||
if (shouldShowSpaceSettings(space)) {
|
||||
const onSettingsClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
showSpaceSettings(space);
|
||||
onFinished();
|
||||
};
|
||||
|
||||
settingsOption = (
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_SpacePanel_iconSettings"
|
||||
label={_t("Settings")}
|
||||
onClick={onSettingsClick}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
const onLeaveClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
leaveSpace(space);
|
||||
onFinished();
|
||||
};
|
||||
|
||||
leaveSection = <IconizedContextMenuOptionList red first>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_SpacePanel_iconLeave"
|
||||
label={_t("Leave space")}
|
||||
onClick={onLeaveClick}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>;
|
||||
}
|
||||
|
||||
const canAddRooms = space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
|
||||
|
||||
let newRoomSection;
|
||||
if (space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) {
|
||||
const onNewRoomClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
showCreateNewRoom(space);
|
||||
onFinished();
|
||||
};
|
||||
|
||||
const onAddExistingRoomClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
showAddExistingRooms(space);
|
||||
onFinished();
|
||||
};
|
||||
|
||||
const onNewSubspaceClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
showCreateNewSubspace(space);
|
||||
onFinished();
|
||||
};
|
||||
|
||||
newRoomSection = <IconizedContextMenuOptionList first>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_SpacePanel_iconPlus"
|
||||
label={_t("Create new room")}
|
||||
onClick={onNewRoomClick}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_SpacePanel_iconHash"
|
||||
label={_t("Add existing room")}
|
||||
onClick={onAddExistingRoomClick}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_SpacePanel_iconPlus"
|
||||
label={_t("Add space")}
|
||||
onClick={onNewSubspaceClick}
|
||||
>
|
||||
<BetaPill />
|
||||
</IconizedContextMenuOption>
|
||||
</IconizedContextMenuOptionList>;
|
||||
}
|
||||
|
||||
const onMembersClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
if (!RoomViewStore.getRoomId()) {
|
||||
defaultDispatcher.dispatch({
|
||||
action: "view_room",
|
||||
room_id: space.roomId,
|
||||
}, true);
|
||||
}
|
||||
|
||||
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: RightPanelPhases.SpaceMemberList,
|
||||
refireParams: { space: space },
|
||||
});
|
||||
onFinished();
|
||||
};
|
||||
|
||||
const onExploreRoomsClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
defaultDispatcher.dispatch({
|
||||
action: "view_room",
|
||||
room_id: space.roomId,
|
||||
});
|
||||
onFinished();
|
||||
};
|
||||
|
||||
return <IconizedContextMenu
|
||||
{...props}
|
||||
onFinished={onFinished}
|
||||
className="mx_SpacePanel_contextMenu"
|
||||
compact
|
||||
>
|
||||
<div className="mx_SpacePanel_contextMenu_header">
|
||||
{ space.name }
|
||||
</div>
|
||||
<IconizedContextMenuOptionList first>
|
||||
{ inviteOption }
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_SpacePanel_iconMembers"
|
||||
label={_t("Members")}
|
||||
onClick={onMembersClick}
|
||||
/>
|
||||
{ settingsOption }
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_SpacePanel_iconExplore"
|
||||
label={canAddRooms ? _t("Manage & explore rooms") : _t("Explore rooms")}
|
||||
onClick={onExploreRoomsClick}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
{ newRoomSection }
|
||||
{ leaveSection }
|
||||
</IconizedContextMenu>;
|
||||
};
|
||||
|
||||
export default SpaceContextMenu;
|
||||
|
|
@ -99,20 +99,22 @@ export default class StatusMessageContextMenu extends React.Component {
|
|||
actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_clear"
|
||||
onClick={this._onClearClick}
|
||||
>
|
||||
<span>{_t("Clear status")}</span>
|
||||
<span>{ _t("Clear status") }</span>
|
||||
</AccessibleButton>;
|
||||
} else {
|
||||
actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_submit"
|
||||
onClick={this._onSubmit}
|
||||
>
|
||||
<span>{_t("Update status")}</span>
|
||||
<span>{ _t("Update status") }</span>
|
||||
</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>
|
||||
<span>{ _t("Set status") }</span>
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
|
@ -121,17 +123,24 @@ 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">
|
||||
{actionButton}
|
||||
{spinner}
|
||||
{ actionButton }
|
||||
{ spinner }
|
||||
</div>
|
||||
</form>;
|
||||
|
||||
|
|
|
@ -24,6 +24,8 @@ import { MenuItem } from "../../structures/ContextMenu";
|
|||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import GroupFilterOrderStore from "../../../stores/GroupFilterOrderStore";
|
||||
import { createSpaceFromCommunity } from "../../../utils/space";
|
||||
import GroupStore from "../../../stores/GroupStore";
|
||||
|
||||
@replaceableComponent("views.context_menus.TagTileContextMenu")
|
||||
export default class TagTileContextMenu extends React.Component {
|
||||
|
@ -49,6 +51,11 @@ export default class TagTileContextMenu extends React.Component {
|
|||
this.props.onFinished();
|
||||
};
|
||||
|
||||
_onCreateSpaceClick = () => {
|
||||
createSpaceFromCommunity(this.context, this.props.tag);
|
||||
this.props.onFinished();
|
||||
};
|
||||
|
||||
_onMoveUp = () => {
|
||||
dis.dispatch(TagOrderActions.moveTag(this.context, this.props.tag, this.props.index - 1));
|
||||
this.props.onFinished();
|
||||
|
@ -77,6 +84,16 @@ export default class TagTileContextMenu extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
let createSpaceOption;
|
||||
if (GroupStore.isUserPrivileged(this.props.tag)) {
|
||||
createSpaceOption = <>
|
||||
<hr className="mx_TagTileContextMenu_separator" role="separator" />
|
||||
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_createSpace" onClick={this._onCreateSpaceClick}>
|
||||
{ _t("Create Space") }
|
||||
</MenuItem>
|
||||
</>;
|
||||
}
|
||||
|
||||
return <div>
|
||||
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_viewCommunity" onClick={this._onViewCommunityClick}>
|
||||
{ _t('View Community') }
|
||||
|
@ -88,6 +105,7 @@ export default class TagTileContextMenu extends React.Component {
|
|||
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_hideCommunity" onClick={this._onRemoveClick}>
|
||||
{ _t("Unpin") }
|
||||
</MenuItem>
|
||||
{ createSpaceOption }
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")}
|
||||
/>;
|
||||
}
|
||||
|
||||
|
|
67
src/components/views/dialogs/AddExistingSubspaceDialog.tsx
Normal file
67
src/components/views/dialogs/AddExistingSubspaceDialog.tsx
Normal file
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
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, { useState } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { AddExistingToSpace, defaultSpacesRenderer, SubspaceSelector } from "./AddExistingToSpaceDialog";
|
||||
|
||||
interface IProps {
|
||||
space: Room;
|
||||
onCreateSubspaceClick(): void;
|
||||
onFinished(added?: boolean): void;
|
||||
}
|
||||
|
||||
const AddExistingSubspaceDialog: React.FC<IProps> = ({ space, onCreateSubspaceClick, onFinished }) => {
|
||||
const [selectedSpace, setSelectedSpace] = useState(space);
|
||||
|
||||
return <BaseDialog
|
||||
title={(
|
||||
<SubspaceSelector
|
||||
title={_t("Add existing space")}
|
||||
space={space}
|
||||
value={selectedSpace}
|
||||
onChange={setSelectedSpace}
|
||||
/>
|
||||
)}
|
||||
className="mx_AddExistingToSpaceDialog"
|
||||
contentId="mx_AddExistingToSpace"
|
||||
onFinished={onFinished}
|
||||
fixedWidth={false}
|
||||
>
|
||||
<MatrixClientContext.Provider value={space.client}>
|
||||
<AddExistingToSpace
|
||||
space={space}
|
||||
onFinished={onFinished}
|
||||
footerPrompt={<>
|
||||
<div>{ _t("Want to add a new space instead?") }</div>
|
||||
<AccessibleButton onClick={onCreateSubspaceClick} kind="link">
|
||||
{ _t("Create a new space") }
|
||||
</AccessibleButton>
|
||||
</>}
|
||||
filterPlaceholder={_t("Search for spaces")}
|
||||
spacesRenderer={defaultSpacesRenderer}
|
||||
/>
|
||||
</MatrixClientContext.Provider>
|
||||
</BaseDialog>;
|
||||
};
|
||||
|
||||
export default AddExistingSubspaceDialog;
|
||||
|
|
@ -17,11 +17,10 @@ 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 { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import Dropdown from "../elements/Dropdown";
|
||||
import SearchBox from "../../structures/SearchBox";
|
||||
|
@ -36,20 +35,20 @@ import StyledCheckbox from "../elements/StyledCheckbox";
|
|||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { sortRooms } from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
|
||||
import ProgressBar from "../elements/ProgressBar";
|
||||
import { SpaceFeedbackPrompt } from "../../structures/SpaceRoomView";
|
||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||
import QueryMatcher from "../../../autocomplete/QueryMatcher";
|
||||
import TruncatedList from "../elements/TruncatedList";
|
||||
import EntityTile from "../rooms/EntityTile";
|
||||
import BaseAvatar from "../avatars/BaseAvatar";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
matrixClient: MatrixClient;
|
||||
interface IProps {
|
||||
space: Room;
|
||||
onCreateRoomClick(cli: MatrixClient, space: Room): void;
|
||||
onCreateRoomClick(): void;
|
||||
onAddSubspaceClick(): void;
|
||||
onFinished(added?: boolean): void;
|
||||
}
|
||||
|
||||
const Entry = ({ room, checked, onChange }) => {
|
||||
export const Entry = ({ room, checked, onChange }) => {
|
||||
return <label className="mx_AddExistingToSpace_entry">
|
||||
{ room?.isSpaceRoom()
|
||||
? <RoomAvatar room={room} height={32} width={32} />
|
||||
|
@ -67,14 +66,36 @@ const Entry = ({ room, checked, onChange }) => {
|
|||
interface IAddExistingToSpaceProps {
|
||||
space: Room;
|
||||
footerPrompt?: ReactNode;
|
||||
filterPlaceholder: string;
|
||||
emptySelectionButton?: ReactNode;
|
||||
onFinished(added: boolean): void;
|
||||
roomsRenderer?(
|
||||
rooms: Room[],
|
||||
selectedToAdd: Set<Room>,
|
||||
onChange: undefined | ((checked: boolean, room: Room) => void),
|
||||
truncateAt: number,
|
||||
overflowTile: (overflowCount: number, totalCount: number) => JSX.Element,
|
||||
): ReactNode;
|
||||
spacesRenderer?(
|
||||
spaces: Room[],
|
||||
selectedToAdd: Set<Room>,
|
||||
onChange?: (checked: boolean, room: Room) => void,
|
||||
): ReactNode;
|
||||
dmsRenderer?(
|
||||
dms: Room[],
|
||||
selectedToAdd: Set<Room>,
|
||||
onChange?: (checked: boolean, room: Room) => void,
|
||||
): ReactNode;
|
||||
}
|
||||
|
||||
export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
|
||||
space,
|
||||
footerPrompt,
|
||||
emptySelectionButton,
|
||||
filterPlaceholder,
|
||||
roomsRenderer,
|
||||
dmsRenderer,
|
||||
spacesRenderer,
|
||||
onFinished,
|
||||
}) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
@ -198,7 +219,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
|
|||
</>;
|
||||
}
|
||||
|
||||
const onChange = !busy && !error ? (checked, room) => {
|
||||
const onChange = !busy && !error ? (checked: boolean, room: Room) => {
|
||||
if (checked) {
|
||||
selectedToAdd.add(room);
|
||||
} else {
|
||||
|
@ -208,83 +229,52 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
|
|||
} : null;
|
||||
|
||||
const [truncateAt, setTruncateAt] = useState(20);
|
||||
function overflowTile(overflowCount, totalCount) {
|
||||
function overflowTile(overflowCount: number, totalCount: number): JSX.Element {
|
||||
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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let noResults = true;
|
||||
if ((roomsRenderer && rooms.length > 0) ||
|
||||
(dmsRenderer && dms.length > 0) ||
|
||||
(!roomsRenderer && !dmsRenderer && spacesRenderer && spaces.length > 0) // only count spaces when alone
|
||||
) {
|
||||
noResults = false;
|
||||
}
|
||||
|
||||
return <div className="mx_AddExistingToSpace">
|
||||
<SearchBox
|
||||
className="mx_textinput_icon mx_textinput_search"
|
||||
placeholder={ _t("Filter your rooms and spaces") }
|
||||
placeholder={filterPlaceholder}
|
||||
onSearch={setQuery}
|
||||
autoComplete={true}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<AutoHideScrollbar className="mx_AddExistingToSpace_content">
|
||||
{ rooms.length > 0 ? (
|
||||
<div className="mx_AddExistingToSpace_section">
|
||||
<h3>{ _t("Rooms") }</h3>
|
||||
<TruncatedList
|
||||
truncateAt={truncateAt}
|
||||
createOverflowElement={overflowTile}
|
||||
getChildren={(start, end) => rooms.slice(start, end).map(room =>
|
||||
<Entry
|
||||
key={room.roomId}
|
||||
room={room}
|
||||
checked={selectedToAdd.has(room)}
|
||||
onChange={onChange ? (checked) => {
|
||||
onChange(checked, room);
|
||||
} : null}
|
||||
/>,
|
||||
)}
|
||||
getChildCount={() => rooms.length}
|
||||
/>
|
||||
</div>
|
||||
{ rooms.length > 0 && roomsRenderer ? (
|
||||
roomsRenderer(rooms, selectedToAdd, onChange, truncateAt, overflowTile)
|
||||
) : undefined }
|
||||
|
||||
{ spaces.length > 0 ? (
|
||||
<div className="mx_AddExistingToSpace_section mx_AddExistingToSpace_section_spaces">
|
||||
<h3>{ _t("Spaces") }</h3>
|
||||
<div className="mx_AddExistingToSpace_section_experimental">
|
||||
<div>{ _t("Feeling experimental?") }</div>
|
||||
<div>{ _t("You can add existing spaces to a space.") }</div>
|
||||
</div>
|
||||
{ spaces.map(space => {
|
||||
return <Entry
|
||||
key={space.roomId}
|
||||
room={space}
|
||||
checked={selectedToAdd.has(space)}
|
||||
onChange={onChange ? (checked) => {
|
||||
onChange(checked, space);
|
||||
} : null}
|
||||
/>;
|
||||
}) }
|
||||
</div>
|
||||
{ spaces.length > 0 && spacesRenderer ? (
|
||||
spacesRenderer(spaces, selectedToAdd, onChange)
|
||||
) : null }
|
||||
|
||||
{ dms.length > 0 ? (
|
||||
<div className="mx_AddExistingToSpace_section">
|
||||
<h3>{ _t("Direct Messages") }</h3>
|
||||
{ dms.map(room => {
|
||||
return <Entry
|
||||
key={room.roomId}
|
||||
room={room}
|
||||
checked={selectedToAdd.has(room)}
|
||||
onChange={onChange ? (checked) => {
|
||||
onChange(checked, room);
|
||||
} : null}
|
||||
/>;
|
||||
}) }
|
||||
</div>
|
||||
{ dms.length > 0 && dmsRenderer ? (
|
||||
dmsRenderer(dms, selectedToAdd, onChange)
|
||||
) : null }
|
||||
|
||||
{ spaces.length + rooms.length + dms.length < 1 ? <span className="mx_AddExistingToSpace_noResults">
|
||||
{ noResults ? <span className="mx_AddExistingToSpace_noResults">
|
||||
{ _t("No results") }
|
||||
</span> : undefined }
|
||||
</AutoHideScrollbar>
|
||||
|
@ -295,69 +285,166 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
|
|||
</div>;
|
||||
};
|
||||
|
||||
const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => {
|
||||
const [selectedSpace, setSelectedSpace] = useState(space);
|
||||
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
|
||||
export const defaultRoomsRenderer: IAddExistingToSpaceProps["roomsRenderer"] = (
|
||||
rooms, selectedToAdd, onChange, truncateAt, overflowTile,
|
||||
) => (
|
||||
<div className="mx_AddExistingToSpace_section">
|
||||
<h3>{ _t("Rooms") }</h3>
|
||||
<TruncatedList
|
||||
truncateAt={truncateAt}
|
||||
createOverflowElement={overflowTile}
|
||||
getChildren={(start, end) => rooms.slice(start, end).map(room =>
|
||||
<Entry
|
||||
key={room.roomId}
|
||||
room={room}
|
||||
checked={selectedToAdd.has(room)}
|
||||
onChange={onChange ? (checked: boolean) => {
|
||||
onChange(checked, room);
|
||||
} : null}
|
||||
/>,
|
||||
)}
|
||||
getChildCount={() => rooms.length}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
let spaceOptionSection;
|
||||
if (existingSubspaces.length > 0) {
|
||||
const options = [space, ...existingSubspaces].map((space) => {
|
||||
const classes = classNames("mx_AddExistingToSpaceDialog_dropdownOption", {
|
||||
mx_AddExistingToSpaceDialog_dropdownOptionActive: space === selectedSpace,
|
||||
});
|
||||
return <div key={space.roomId} className={classes}>
|
||||
<RoomAvatar room={space} width={24} height={24} />
|
||||
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
|
||||
</div>;
|
||||
});
|
||||
export const defaultSpacesRenderer: IAddExistingToSpaceProps["spacesRenderer"] = (spaces, selectedToAdd, onChange) => (
|
||||
<div className="mx_AddExistingToSpace_section">
|
||||
{ spaces.map(space => {
|
||||
return <Entry
|
||||
key={space.roomId}
|
||||
room={space}
|
||||
checked={selectedToAdd.has(space)}
|
||||
onChange={onChange ? (checked) => {
|
||||
onChange(checked, space);
|
||||
} : null}
|
||||
/>;
|
||||
}) }
|
||||
</div>
|
||||
);
|
||||
|
||||
spaceOptionSection = (
|
||||
export const defaultDmsRenderer: IAddExistingToSpaceProps["dmsRenderer"] = (dms, selectedToAdd, onChange) => (
|
||||
<div className="mx_AddExistingToSpace_section">
|
||||
<h3>{ _t("Direct Messages") }</h3>
|
||||
{ dms.map(room => {
|
||||
return <Entry
|
||||
key={room.roomId}
|
||||
room={room}
|
||||
checked={selectedToAdd.has(room)}
|
||||
onChange={onChange ? (checked: boolean) => {
|
||||
onChange(checked, room);
|
||||
} : null}
|
||||
/>;
|
||||
}) }
|
||||
</div>
|
||||
);
|
||||
|
||||
interface ISubspaceSelectorProps {
|
||||
title: string;
|
||||
space: Room;
|
||||
value: Room;
|
||||
onChange(space: Room): void;
|
||||
}
|
||||
|
||||
export const SubspaceSelector = ({ title, space, value, onChange }: ISubspaceSelectorProps) => {
|
||||
const options = useMemo(() => {
|
||||
return [space, ...SpaceStore.instance.getChildSpaces(space.roomId).filter(space => {
|
||||
return space.currentState.maySendStateEvent(EventType.SpaceChild, space.client.credentials.userId);
|
||||
})];
|
||||
}, [space]);
|
||||
|
||||
let body;
|
||||
if (options.length > 1) {
|
||||
body = (
|
||||
<Dropdown
|
||||
id="mx_SpaceSelectDropdown"
|
||||
className="mx_SpaceSelectDropdown"
|
||||
onOptionChange={(key: string) => {
|
||||
setSelectedSpace(existingSubspaces.find(space => space.roomId === key) || space);
|
||||
onChange(options.find(space => space.roomId === key) || space);
|
||||
}}
|
||||
value={selectedSpace.roomId}
|
||||
value={value.roomId}
|
||||
label={_t("Space selection")}
|
||||
>
|
||||
{ options }
|
||||
{ options.map((space) => {
|
||||
const classes = classNames({
|
||||
mx_SubspaceSelector_dropdownOptionActive: space === value,
|
||||
});
|
||||
return <div key={space.roomId} className={classes}>
|
||||
<RoomAvatar room={space} width={24} height={24} />
|
||||
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
|
||||
</div>;
|
||||
}) }
|
||||
</Dropdown>
|
||||
);
|
||||
} else {
|
||||
spaceOptionSection = <div className="mx_AddExistingToSpaceDialog_onlySpace">
|
||||
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
|
||||
</div>;
|
||||
body = (
|
||||
<div className="mx_SubspaceSelector_onlySpace">
|
||||
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const title = <React.Fragment>
|
||||
<RoomAvatar room={selectedSpace} height={40} width={40} />
|
||||
return <div className="mx_SubspaceSelector">
|
||||
<RoomAvatar room={value} height={40} width={40} />
|
||||
<div>
|
||||
<h1>{ _t("Add existing rooms") }</h1>
|
||||
{ spaceOptionSection }
|
||||
<h1>{ title }</h1>
|
||||
{ body }
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
</div>;
|
||||
};
|
||||
|
||||
const AddExistingToSpaceDialog: React.FC<IProps> = ({ space, onCreateRoomClick, onAddSubspaceClick, onFinished }) => {
|
||||
const [selectedSpace, setSelectedSpace] = useState(space);
|
||||
|
||||
return <BaseDialog
|
||||
title={title}
|
||||
title={(
|
||||
<SubspaceSelector
|
||||
title={_t("Add existing rooms")}
|
||||
space={space}
|
||||
value={selectedSpace}
|
||||
onChange={setSelectedSpace}
|
||||
/>
|
||||
)}
|
||||
className="mx_AddExistingToSpaceDialog"
|
||||
contentId="mx_AddExistingToSpace"
|
||||
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
|
||||
kind="link"
|
||||
onClick={() => {
|
||||
onCreateRoomClick();
|
||||
onFinished();
|
||||
}}
|
||||
>
|
||||
{ _t("Create a new room") }
|
||||
</AccessibleButton>
|
||||
</>}
|
||||
filterPlaceholder={_t("Search for rooms")}
|
||||
roomsRenderer={defaultRoomsRenderer}
|
||||
spacesRenderer={() => (
|
||||
<div className="mx_AddExistingToSpace_section">
|
||||
<h3>{ _t("Spaces") }</h3>
|
||||
<AccessibleButton
|
||||
kind="link"
|
||||
onClick={() => {
|
||||
onAddSubspaceClick();
|
||||
onFinished();
|
||||
}}
|
||||
>
|
||||
{ _t("Adding spaces has moved.") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
)}
|
||||
dmsRenderer={defaultDmsRenderer}
|
||||
/>
|
||||
</MatrixClientContext.Provider>
|
||||
|
||||
<SpaceFeedbackPrompt onClick={() => onFinished(false)} />
|
||||
</BaseDialog>;
|
||||
};
|
||||
|
||||
|
|
|
@ -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,33 +620,27 @@ 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">
|
||||
<label htmlFor="textinput">{this.props.description}</label>
|
||||
<label htmlFor="textinput">{ this.props.description }</label>
|
||||
</div>;
|
||||
}
|
||||
|
||||
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,11 +699,11 @@ 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(
|
||||
identityServer = <div className="mx_AddressPickerDialog_identityServer">{ _t(
|
||||
"Use an identity server to invite by email. " +
|
||||
"<default>Use the default (%(defaultIdentityServerName)s)</default> " +
|
||||
"or manage in <settings>Settings</settings>.",
|
||||
|
@ -698,25 +711,29 @@ export default class AddressPickerDialog extends React.Component {
|
|||
defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl),
|
||||
},
|
||||
{
|
||||
default: sub => <a href="#" onClick={this.onUseDefaultIdentityServerClick}>{sub}</a>,
|
||||
settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{sub}</a>,
|
||||
default: sub => <a href="#" onClick={this.onUseDefaultIdentityServerClick}>{ sub }</a>,
|
||||
settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{ sub }</a>,
|
||||
},
|
||||
)}</div>;
|
||||
) }</div>;
|
||||
} else {
|
||||
identityServer = <div className="mx_AddressPickerDialog_identityServer">{_t(
|
||||
identityServer = <div className="mx_AddressPickerDialog_identityServer">{ _t(
|
||||
"Use an identity server to invite by email. " +
|
||||
"Manage in <settings>Settings</settings>.",
|
||||
{}, {
|
||||
settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{sub}</a>,
|
||||
settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{ sub }</a>,
|
||||
},
|
||||
)}</div>;
|
||||
) }</div>;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_AddressPickerDialog" onKeyDown={this.onKeyDown}
|
||||
onFinished={this.props.onFinished} title={this.props.title}>
|
||||
{inputLabel}
|
||||
<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>
|
||||
{ error }
|
|
@ -51,7 +51,7 @@ export default class AskInviteAnywayDialog extends React.Component<IProps> {
|
|||
|
||||
public render() {
|
||||
const errorList = this.props.unknownProfileUsers
|
||||
.map(address => <li key={address.userId}>{address.userId}: {address.errorText}</li>);
|
||||
.map(address => <li key={address.userId}>{ address.userId }: { address.errorText }</li>);
|
||||
|
||||
return (
|
||||
<BaseDialog className='mx_RetryInvitesDialog'
|
||||
|
@ -60,8 +60,8 @@ export default class AskInviteAnywayDialog extends React.Component<IProps> {
|
|||
contentId='mx_Dialog_content'
|
||||
>
|
||||
<div id='mx_Dialog_content'>
|
||||
{/* eslint-disable-next-line */}
|
||||
<p>{_t("Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?")}</p>
|
||||
<p>{ _t("Unable to find profiles for the Matrix IDs listed below - " +
|
||||
"would you like to invite them anyway?") }</p>
|
||||
<ul>
|
||||
{ errorList }
|
||||
</ul>
|
||||
|
|
|
@ -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 (
|
||||
|
@ -149,7 +147,7 @@ export default class BaseDialog extends React.Component {
|
|||
'mx_Dialog_headerWithCancel': !!cancelButton,
|
||||
})}>
|
||||
<div className={classNames('mx_Dialog_title', this.props.titleClass)} id='mx_BaseDialog_title'>
|
||||
{headerImage}
|
||||
{ headerImage }
|
||||
{ this.props.title }
|
||||
</div>
|
||||
{ this.props.headerButton }
|
||||
|
|
|
@ -14,22 +14,18 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState } from "react";
|
||||
import React from "react";
|
||||
|
||||
import QuestionDialog from './QuestionDialog';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Field from "../elements/Field";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { submitFeedback } from "../../../rageshake/submit-rageshake";
|
||||
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||
import Modal from "../../../Modal";
|
||||
import InfoDialog from "./InfoDialog";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { UserTab } from "./UserSettingsDialog";
|
||||
import GenericFeatureFeedbackDialog from "./GenericFeatureFeedbackDialog";
|
||||
|
||||
// XXX: Keep this around for re-use in future Betas
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
featureId: string;
|
||||
|
@ -38,74 +34,28 @@ interface IProps extends IDialogProps {
|
|||
const BetaFeedbackDialog: React.FC<IProps> = ({ featureId, onFinished }) => {
|
||||
const info = SettingsStore.getBetaInfo(featureId);
|
||||
|
||||
const [comment, setComment] = useState("");
|
||||
const [canContact, setCanContact] = useState(false);
|
||||
|
||||
const sendFeedback = async (ok: boolean) => {
|
||||
if (!ok) return onFinished(false);
|
||||
|
||||
const extraData = SettingsStore.getBetaInfo(featureId)?.extraSettings.reduce((o, k) => {
|
||||
o[k] = SettingsStore.getValue(k);
|
||||
return o;
|
||||
}, {});
|
||||
|
||||
submitFeedback(SdkConfig.get().bug_report_endpoint_url, info.feedbackLabel, comment, canContact, extraData);
|
||||
onFinished(true);
|
||||
|
||||
Modal.createTrackedDialog("Beta Dialog Sent", featureId, InfoDialog, {
|
||||
title: _t("Beta feedback"),
|
||||
description: _t("Thank you for your feedback, we really appreciate it."),
|
||||
button: _t("Done"),
|
||||
hasCloseButton: false,
|
||||
fixedWidth: false,
|
||||
});
|
||||
};
|
||||
|
||||
return (<QuestionDialog
|
||||
className="mx_BetaFeedbackDialog"
|
||||
hasCancelButton={true}
|
||||
return <GenericFeatureFeedbackDialog
|
||||
title={_t("%(featureName)s beta feedback", { featureName: info.title })}
|
||||
description={<React.Fragment>
|
||||
<div className="mx_BetaFeedbackDialog_subheading">
|
||||
{ _t(info.feedbackSubheading) }
|
||||
|
||||
{ _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({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Labs,
|
||||
});
|
||||
}}>
|
||||
{ _t("To leave the beta, visit your settings.") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
|
||||
<Field
|
||||
id="feedbackComment"
|
||||
label={_t("Feedback")}
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
value={comment}
|
||||
element="textarea"
|
||||
onChange={(ev) => {
|
||||
setComment(ev.target.value);
|
||||
}}
|
||||
autoFocus={true}
|
||||
/>
|
||||
|
||||
<StyledCheckbox
|
||||
checked={canContact}
|
||||
onClick={e => setCanContact((e.target as HTMLInputElement).checked)}
|
||||
>
|
||||
{ _t("You may contact me if you have any follow up questions") }
|
||||
</StyledCheckbox>
|
||||
</React.Fragment>}
|
||||
button={_t("Send feedback")}
|
||||
buttonDisabled={!comment}
|
||||
onFinished={sendFeedback}
|
||||
/>);
|
||||
subheading={_t(info.feedbackSubheading)}
|
||||
onFinished={onFinished}
|
||||
rageshakeLabel={info.feedbackLabel}
|
||||
rageshakeData={Object.fromEntries((SettingsStore.getBetaInfo(featureId)?.extraSettings || []).map(k => {
|
||||
return SettingsStore.getValue(k);
|
||||
}))}
|
||||
>
|
||||
<AccessibleButton
|
||||
kind="link"
|
||||
onClick={() => {
|
||||
onFinished(false);
|
||||
defaultDispatcher.dispatch({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Labs,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{ _t("To leave the beta, visit your settings.") }
|
||||
</AccessibleButton>
|
||||
</GenericFeatureFeedbackDialog>;
|
||||
};
|
||||
|
||||
export default BetaFeedbackDialog;
|
||||
|
|
|
@ -166,7 +166,7 @@ export default class BugReportDialog extends React.Component<IProps, IState> {
|
|||
let error = null;
|
||||
if (this.state.err) {
|
||||
error = <div className="error">
|
||||
{this.state.err}
|
||||
{ this.state.err }
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
@ -175,7 +175,7 @@ export default class BugReportDialog extends React.Component<IProps, IState> {
|
|||
progress = (
|
||||
<div className="progress">
|
||||
<Spinner />
|
||||
{this.state.progress} ...
|
||||
{ this.state.progress } ...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -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'
|
||||
>
|
||||
|
@ -221,7 +223,7 @@ export default class BugReportDialog extends React.Component<IProps, IState> {
|
|||
<AccessibleButton onClick={this.onDownload} kind="link" disabled={this.state.downloadBusy}>
|
||||
{ _t("Download logs") }
|
||||
</AccessibleButton>
|
||||
{this.state.downloadProgress && <span>{this.state.downloadProgress} ...</span>}
|
||||
{ this.state.downloadProgress && <span>{ this.state.downloadProgress } ...</span> }
|
||||
</div>
|
||||
|
||||
<Field
|
||||
|
@ -246,8 +248,8 @@ export default class BugReportDialog extends React.Component<IProps, IState> {
|
|||
"please include those things here.",
|
||||
)}
|
||||
/>
|
||||
{progress}
|
||||
{error}
|
||||
{ progress }
|
||||
{ error }
|
||||
</div>
|
||||
<DialogButtons primaryButton={_t("Send logs")}
|
||||
onPrimaryButtonClick={this.onSubmit}
|
||||
|
|
|
@ -59,7 +59,7 @@ export default class ChangelogDialog extends React.Component<IProps> {
|
|||
return (
|
||||
<li key={commit.sha} className="mx_ChangelogDialog_li">
|
||||
<a href={commit.html_url} target="_blank" rel="noreferrer noopener">
|
||||
{commit.commit.message.split('\n')[0]}
|
||||
{ commit.commit.message.split('\n')[0] }
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
|
@ -79,15 +79,15 @@ export default class ChangelogDialog extends React.Component<IProps> {
|
|||
}
|
||||
return (
|
||||
<div key={repo}>
|
||||
<h2>{repo}</h2>
|
||||
<ul>{content}</ul>
|
||||
<h2>{ repo }</h2>
|
||||
<ul>{ content }</ul>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const content = (
|
||||
<div className="mx_ChangelogDialog_content">
|
||||
{this.props.version == null || this.props.newVersion == null ? <h2>{_t("Unavailable")}</h2> : logs}
|
||||
{ this.props.version == null || this.props.newVersion == null ? <h2>{ _t("Unavailable") }</h2> : logs }
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
@ -156,8 +156,8 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent<
|
|||
height={avatarSize}
|
||||
/>
|
||||
<div className="mx_CommunityPrototypeInviteDialog_personIdentifiers">
|
||||
<span className="mx_CommunityPrototypeInviteDialog_personName">{person.user.name}</span>
|
||||
<span className="mx_CommunityPrototypeInviteDialog_personId">{person.userId}</span>
|
||||
<span className="mx_CommunityPrototypeInviteDialog_personName">{ person.user.name }</span>
|
||||
<span className="mx_CommunityPrototypeInviteDialog_personId">{ person.userId }</span>
|
||||
</div>
|
||||
<StyledCheckbox onChange={(e) => this.setPersonToggle(person, e.target.checked)} />
|
||||
</div>
|
||||
|
@ -187,7 +187,7 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent<
|
|||
emailAddresses.push((
|
||||
<Field
|
||||
key={emailAddresses.length}
|
||||
value={""}
|
||||
value=""
|
||||
onChange={(e) => this.onAddressChange(e, emailAddresses.length)}
|
||||
label={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")}
|
||||
placeholder={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")}
|
||||
|
@ -205,18 +205,21 @@ 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>
|
||||
));
|
||||
}
|
||||
}
|
||||
if (this.state.people.length > 0) {
|
||||
peopleIntro = (
|
||||
<div className="mx_CommunityPrototypeInviteDialog_people">
|
||||
<span>{_t("People you know on %(brand)s", { brand: SdkConfig.get().brand })}</span>
|
||||
<span>{ _t("People you know on %(brand)s", { brand: SdkConfig.get().brand }) }</span>
|
||||
<AccessibleButton onClick={this.onShowPeopleClick}>
|
||||
{this.state.showPeople ? _t("Hide") : _t("Show")}
|
||||
{ this.state.showPeople ? _t("Hide") : _t("Show") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
|
@ -236,14 +239,17 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent<
|
|||
>
|
||||
<form onSubmit={this.onSubmit}>
|
||||
<div className="mx_Dialog_content">
|
||||
{emailAddresses}
|
||||
{peopleIntro}
|
||||
{people}
|
||||
{ emailAddresses }
|
||||
{ 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>
|
||||
|
|
|
@ -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")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
>
|
||||
|
|
|
@ -44,10 +44,10 @@ export default class ConfirmWipeDeviceDialog extends React.Component<IProps> {
|
|||
>
|
||||
<div className='mx_ConfirmWipeDeviceDialog_content'>
|
||||
<p>
|
||||
{_t(
|
||||
{ _t(
|
||||
"Clearing all data from this session is permanent. Encrypted messages will be lost " +
|
||||
"unless their keys have been backed up.",
|
||||
)}
|
||||
) }
|
||||
</p>
|
||||
</div>
|
||||
<DialogButtons
|
||||
|
|
|
@ -144,11 +144,11 @@ export default class CreateCommunityPrototypeDialog extends React.PureComponent<
|
|||
if (this.state.localpart) {
|
||||
communityId = (
|
||||
<span className="mx_CreateCommunityPrototypeDialog_communityId">
|
||||
{_t("Community ID: +<localpart />:%(domain)s", {
|
||||
{ _t("Community ID: +<localpart />:%(domain)s", {
|
||||
domain: MatrixClientPeg.getHomeserverName(),
|
||||
}, {
|
||||
localpart: () => <u>{this.state.localpart}</u>,
|
||||
})}
|
||||
localpart: () => <u>{ this.state.localpart }</u>,
|
||||
}) }
|
||||
<InfoTooltip
|
||||
tooltip={_t(
|
||||
"Use this when referencing your community to others. The community ID " +
|
||||
|
@ -161,14 +161,14 @@ export default class CreateCommunityPrototypeDialog extends React.PureComponent<
|
|||
|
||||
let helpText = (
|
||||
<span className="mx_CreateCommunityPrototypeDialog_subtext">
|
||||
{_t("You can change this later if needed.")}
|
||||
{ _t("You can change this later if needed.") }
|
||||
</span>
|
||||
);
|
||||
if (this.state.error) {
|
||||
const classes = "mx_CreateCommunityPrototypeDialog_subtext mx_CreateCommunityPrototypeDialog_subtext_error";
|
||||
helpText = (
|
||||
<span className={classes}>
|
||||
{this.state.error}
|
||||
{ this.state.error }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
@ -193,31 +193,33 @@ export default class CreateCommunityPrototypeDialog extends React.PureComponent<
|
|||
placeholder={_t("Enter name")}
|
||||
label={_t("Enter name")}
|
||||
/>
|
||||
{helpText}
|
||||
{ helpText }
|
||||
<span className="mx_CreateCommunityPrototypeDialog_subtext">
|
||||
{/*nbsp is to reserve the height of this element when there's nothing*/}
|
||||
{communityId}
|
||||
{ /*nbsp is to reserve the height of this element when there's nothing*/ }
|
||||
{ communityId }
|
||||
</span>
|
||||
<AccessibleButton kind="primary" onClick={this.onSubmit} disabled={this.state.busy}>
|
||||
{_t("Create")}
|
||||
{ _t("Create") }
|
||||
</AccessibleButton>
|
||||
</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
|
||||
onClick={this.onChangeAvatar}
|
||||
className="mx_CreateCommunityPrototypeDialog_avatarContainer"
|
||||
>
|
||||
{preview}
|
||||
{ preview }
|
||||
</AccessibleButton>
|
||||
<div className="mx_CreateCommunityPrototypeDialog_tip">
|
||||
<b>{_t("Add image (optional)")}</b>
|
||||
<b>{ _t("Add image (optional)") }</b>
|
||||
<span>
|
||||
{_t("An image will help people identify your community.")}
|
||||
{ _t("An image will help people identify your community.") }
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -102,7 +102,7 @@ export default class CreateGroupDialog extends React.Component<IProps, IState> {
|
|||
});
|
||||
};
|
||||
|
||||
_onCancel = () => {
|
||||
private onCancel = () => {
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
|
@ -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}
|
||||
|
@ -167,7 +172,7 @@ export default class CreateGroupDialog extends React.Component<IProps, IState> {
|
|||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<input type="submit" value={_t('Create')} className="mx_Dialog_primary" />
|
||||
<button onClick={this._onCancel}>
|
||||
<button onClick={this.onCancel}>
|
||||
{ _t("Cancel") }
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -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 SpaceStore from "../../../stores/SpaceStore";
|
||||
import JoinRuleDropdown from "../elements/JoinRuleDropdown";
|
||||
|
||||
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) {
|
||||
opts.parentSpace = this.props.parentSpace;
|
||||
opts.parentSpace = this.props.parentSpace;
|
||||
if (this.props.parentSpace && this.state.joinRule === JoinRule.Restricted) {
|
||||
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,52 @@ 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>,
|
||||
},
|
||||
) }
|
||||
|
||||
{ _t("You can change this at any time from room settings.") }
|
||||
</p>;
|
||||
} else if (this.state.joinRule === JoinRule.Public && this.props.parentSpace) {
|
||||
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>,
|
||||
},
|
||||
) }
|
||||
|
||||
{ _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.") }
|
||||
|
||||
{ _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.",
|
||||
) }
|
||||
|
||||
{ _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) {
|
||||
|
@ -250,7 +291,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
|||
}
|
||||
e2eeSection = <React.Fragment>
|
||||
<LabelledToggleSwitch
|
||||
label={ _t("Enable end-to-end encryption")}
|
||||
label={_t("Enable end-to-end encryption")}
|
||||
onChange={this.onEncryptedChange}
|
||||
value={this.state.isEncrypted}
|
||||
className='mx_CreateRoomDialog_e2eSwitch' // for end-to-end tests
|
||||
|
@ -273,15 +314,16 @@ 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');
|
||||
}
|
||||
|
||||
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 +340,16 @@ 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}
|
||||
|
||||
<JoinRuleDropdown
|
||||
label={_t("Room visibility")}
|
||||
labelInvite={_t("Private room (invite only)")}
|
||||
labelPublic={_t("Public room")}
|
||||
labelRestricted={this.supportsRestricted ? _t("Visible to space members") : undefined}
|
||||
value={this.state.joinRule}
|
||||
onChange={this.onJoinRuleChange}
|
||||
/>
|
||||
|
||||
{ publicPrivateLabel }
|
||||
{ e2eeSection }
|
||||
{ aliasField }
|
||||
|
@ -318,7 +365,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
|||
onChange={this.onNoFederateChange}
|
||||
value={this.state.noFederate}
|
||||
/>
|
||||
<p>{federateLabel}</p>
|
||||
<p>{ federateLabel }</p>
|
||||
</details>
|
||||
</div>
|
||||
</form>
|
||||
|
|
340
src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx
Normal file
340
src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx
Normal file
|
@ -0,0 +1,340 @@
|
|||
/*
|
||||
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, { useEffect, useRef, useState } from "react";
|
||||
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { createSpace, SpaceCreateForm } from "../spaces/SpaceCreateMenu";
|
||||
import JoinRuleDropdown from "../elements/JoinRuleDropdown";
|
||||
import Field from "../elements/Field";
|
||||
import RoomAliasField from "../elements/RoomAliasField";
|
||||
import { GroupMember } from "../right_panel/UserInfo";
|
||||
import { parseMembersResponse, parseRoomsResponse } from "../../../stores/GroupStore";
|
||||
import { calculateRoomVia, makeRoomPermalink } from "../../../utils/permalinks/Permalinks";
|
||||
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import SpaceStore from "../../../stores/SpaceStore";
|
||||
import Modal from "../../../Modal";
|
||||
import InfoDialog from "./InfoDialog";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { UserTab } from "./UserSettingsDialog";
|
||||
import TagOrderActions from "../../../actions/TagOrderActions";
|
||||
|
||||
interface IProps {
|
||||
matrixClient: MatrixClient;
|
||||
groupId: string;
|
||||
onFinished(spaceId?: string): void;
|
||||
}
|
||||
|
||||
export const CreateEventField = "io.element.migrated_from_community";
|
||||
|
||||
interface IGroupRoom {
|
||||
displayname: string;
|
||||
name?: string;
|
||||
roomId: string;
|
||||
canonicalAlias?: string;
|
||||
avatarUrl?: string;
|
||||
topic?: string;
|
||||
numJoinedMembers?: number;
|
||||
worldReadable?: boolean;
|
||||
guestCanJoin?: boolean;
|
||||
isPublic?: boolean;
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
export interface IGroupSummary {
|
||||
profile: {
|
||||
avatar_url?: string;
|
||||
is_openly_joinable?: boolean;
|
||||
is_public?: boolean;
|
||||
long_description: string;
|
||||
name: string;
|
||||
short_description: string;
|
||||
};
|
||||
rooms_section: {
|
||||
rooms: unknown[];
|
||||
categories: Record<string, unknown>;
|
||||
total_room_count_estimate: number;
|
||||
};
|
||||
user: {
|
||||
is_privileged: boolean;
|
||||
is_public: boolean;
|
||||
is_publicised: boolean;
|
||||
membership: string;
|
||||
};
|
||||
users_section: {
|
||||
users: unknown[];
|
||||
roles: Record<string, unknown>;
|
||||
total_user_count_estimate: number;
|
||||
};
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, groupId, onFinished }) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const [avatar, setAvatar] = useState<File>(null); // undefined means to remove avatar
|
||||
const [name, setName] = useState("");
|
||||
const spaceNameField = useRef<Field>();
|
||||
const [alias, setAlias] = useState("#" + groupId.substring(1, groupId.indexOf(":")) + ":" + cli.getDomain());
|
||||
const spaceAliasField = useRef<RoomAliasField>();
|
||||
const [topic, setTopic] = useState("");
|
||||
const [joinRule, setJoinRule] = useState<JoinRule>(JoinRule.Public);
|
||||
|
||||
const groupSummary = useAsyncMemo<IGroupSummary>(() => cli.getGroupSummary(groupId), [groupId]);
|
||||
useEffect(() => {
|
||||
if (groupSummary) {
|
||||
setName(groupSummary.profile.name || "");
|
||||
setTopic(groupSummary.profile.short_description || "");
|
||||
setJoinRule(groupSummary.profile.is_openly_joinable ? JoinRule.Public : JoinRule.Invite);
|
||||
setLoading(false);
|
||||
}
|
||||
}, [groupSummary]);
|
||||
|
||||
if (loading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const onCreateSpaceClick = async (e) => {
|
||||
e.preventDefault();
|
||||
if (busy) return;
|
||||
|
||||
setError(null);
|
||||
setBusy(true);
|
||||
|
||||
// require & validate the space name field
|
||||
if (!await spaceNameField.current.validate({ allowEmpty: false })) {
|
||||
setBusy(false);
|
||||
spaceNameField.current.focus();
|
||||
spaceNameField.current.validate({ allowEmpty: false, focused: true });
|
||||
return;
|
||||
}
|
||||
// validate the space name alias field but do not require it
|
||||
if (joinRule === JoinRule.Public && !await spaceAliasField.current.validate({ allowEmpty: true })) {
|
||||
setBusy(false);
|
||||
spaceAliasField.current.focus();
|
||||
spaceAliasField.current.validate({ allowEmpty: true, focused: true });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const [rooms, members, invitedMembers] = await Promise.all([
|
||||
cli.getGroupRooms(groupId).then(parseRoomsResponse) as Promise<IGroupRoom[]>,
|
||||
cli.getGroupUsers(groupId).then(parseMembersResponse) as Promise<GroupMember[]>,
|
||||
cli.getGroupInvitedUsers(groupId).then(parseMembersResponse) as Promise<GroupMember[]>,
|
||||
]);
|
||||
|
||||
const viaMap = new Map<string, string[]>();
|
||||
for (const { roomId, canonicalAlias } of rooms) {
|
||||
const room = cli.getRoom(roomId);
|
||||
if (room) {
|
||||
viaMap.set(roomId, calculateRoomVia(room));
|
||||
} else if (canonicalAlias) {
|
||||
try {
|
||||
const { servers } = await cli.getRoomIdForAlias(canonicalAlias);
|
||||
viaMap.set(roomId, servers);
|
||||
} catch (e) {
|
||||
console.warn("Failed to resolve alias during community migration", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!viaMap.get(roomId)?.length) {
|
||||
// XXX: lets guess the via, this might end up being incorrect.
|
||||
const str = canonicalAlias || roomId;
|
||||
viaMap.set(roomId, [str.substring(1, str.indexOf(":"))]);
|
||||
}
|
||||
}
|
||||
|
||||
const spaceAvatar = avatar !== undefined ? avatar : groupSummary.profile.avatar_url;
|
||||
const roomId = await createSpace(name, joinRule === JoinRule.Public, alias, topic, spaceAvatar, {
|
||||
creation_content: {
|
||||
[CreateEventField]: groupId,
|
||||
},
|
||||
initial_state: rooms.map(({ roomId }) => ({
|
||||
type: EventType.SpaceChild,
|
||||
state_key: roomId,
|
||||
content: {
|
||||
via: viaMap.get(roomId) || [],
|
||||
},
|
||||
})),
|
||||
invite: [...members, ...invitedMembers].map(m => m.userId).filter(m => m !== cli.getUserId()),
|
||||
}, {
|
||||
andView: false,
|
||||
});
|
||||
|
||||
// eagerly remove it from the community panel
|
||||
dis.dispatch(TagOrderActions.removeTag(cli, groupId));
|
||||
|
||||
// don't bother awaiting this, as we don't hugely care if it fails
|
||||
cli.setGroupProfile(groupId, {
|
||||
...groupSummary.profile,
|
||||
long_description: `<a href="${makeRoomPermalink(roomId)}"><h1>` +
|
||||
_t("This community has been upgraded into a Space") + `</h1></a><br />`
|
||||
+ groupSummary.profile.long_description,
|
||||
} as IGroupSummary["profile"]).catch(e => {
|
||||
console.warn("Failed to update community profile during migration", e);
|
||||
});
|
||||
|
||||
onFinished(roomId);
|
||||
|
||||
const onSpaceClick = () => {
|
||||
dis.dispatch({
|
||||
action: "view_room",
|
||||
room_id: roomId,
|
||||
});
|
||||
};
|
||||
|
||||
const onPreferencesClick = () => {
|
||||
dis.dispatch({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Preferences,
|
||||
});
|
||||
};
|
||||
|
||||
let spacesDisabledCopy;
|
||||
if (!SpaceStore.spacesEnabled) {
|
||||
spacesDisabledCopy = _t("To view Spaces, hide communities in <a>Preferences</a>", {}, {
|
||||
a: sub => <AccessibleButton onClick={onPreferencesClick} kind="link">{ sub }</AccessibleButton>,
|
||||
});
|
||||
}
|
||||
|
||||
Modal.createDialog(InfoDialog, {
|
||||
title: _t("Space created"),
|
||||
description: <>
|
||||
<div className="mx_CreateSpaceFromCommunityDialog_SuccessInfoDialog_checkmark" />
|
||||
<p>
|
||||
{ _t("<SpaceName/> has been made and everyone who was a part of the community has " +
|
||||
"been invited to it.", {}, {
|
||||
SpaceName: () => <AccessibleButton onClick={onSpaceClick} kind="link">
|
||||
{ name }
|
||||
</AccessibleButton>,
|
||||
}) }
|
||||
|
||||
{ spacesDisabledCopy }
|
||||
</p>
|
||||
<p>
|
||||
{ _t("To create a Space from another community, just pick the community in Preferences.") }
|
||||
</p>
|
||||
</>,
|
||||
button: _t("Preferences"),
|
||||
onFinished: (openPreferences: boolean) => {
|
||||
if (openPreferences) {
|
||||
onPreferencesClick();
|
||||
}
|
||||
},
|
||||
}, "mx_CreateSpaceFromCommunityDialog_SuccessInfoDialog");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setError(e);
|
||||
}
|
||||
|
||||
setBusy(false);
|
||||
};
|
||||
|
||||
let footer;
|
||||
if (error) {
|
||||
footer = <>
|
||||
<img src={require("../../../../res/img/element-icons/warning-badge.svg")} height="24" width="24" alt="" />
|
||||
|
||||
<span className="mx_CreateSpaceFromCommunityDialog_error">
|
||||
<div className="mx_CreateSpaceFromCommunityDialog_errorHeading">{ _t("Failed to migrate community") }</div>
|
||||
<div className="mx_CreateSpaceFromCommunityDialog_errorCaption">{ _t("Try again") }</div>
|
||||
</span>
|
||||
|
||||
<AccessibleButton className="mx_CreateSpaceFromCommunityDialog_retryButton" onClick={onCreateSpaceClick}>
|
||||
{ _t("Retry") }
|
||||
</AccessibleButton>
|
||||
</>;
|
||||
} else {
|
||||
footer = <>
|
||||
<AccessibleButton kind="primary_outline" disabled={busy} onClick={() => onFinished()}>
|
||||
{ _t("Cancel") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton kind="primary" disabled={busy} onClick={onCreateSpaceClick}>
|
||||
{ busy ? _t("Creating...") : _t("Create Space") }
|
||||
</AccessibleButton>
|
||||
</>;
|
||||
}
|
||||
|
||||
return <BaseDialog
|
||||
title={_t("Create Space from community")}
|
||||
className="mx_CreateSpaceFromCommunityDialog"
|
||||
onFinished={onFinished}
|
||||
fixedWidth={false}
|
||||
>
|
||||
<div className="mx_CreateSpaceFromCommunityDialog_content">
|
||||
<p>
|
||||
{ _t("A link to the Space will be put in your community description.") }
|
||||
|
||||
{ _t("All rooms will be added and all community members will be invited.") }
|
||||
</p>
|
||||
<p className="mx_CreateSpaceFromCommunityDialog_flairNotice">
|
||||
{ _t("Flair won't be available in Spaces for the foreseeable future.") }
|
||||
</p>
|
||||
|
||||
<SpaceCreateForm
|
||||
busy={busy}
|
||||
onSubmit={onCreateSpaceClick}
|
||||
avatarUrl={groupSummary.profile.avatar_url
|
||||
? mediaFromMxc(groupSummary.profile.avatar_url).getThumbnailOfSourceHttp(80, 80, "crop")
|
||||
: undefined
|
||||
}
|
||||
setAvatar={setAvatar}
|
||||
name={name}
|
||||
setName={setName}
|
||||
nameFieldRef={spaceNameField}
|
||||
topic={topic}
|
||||
setTopic={setTopic}
|
||||
alias={alias}
|
||||
setAlias={setAlias}
|
||||
showAliasField={joinRule === JoinRule.Public}
|
||||
aliasFieldRef={spaceAliasField}
|
||||
>
|
||||
<p>{ _t("This description will be shown to people when they view your space") }</p>
|
||||
<JoinRuleDropdown
|
||||
label={_t("Space visibility")}
|
||||
labelInvite={_t("Private space (invite only)")}
|
||||
labelPublic={_t("Public space")}
|
||||
value={joinRule}
|
||||
onChange={setJoinRule}
|
||||
/>
|
||||
<p>{ joinRule === JoinRule.Public
|
||||
? _t("Open space for anyone, best for communities")
|
||||
: _t("Invite only, best for yourself or teams")
|
||||
}</p>
|
||||
{ joinRule !== JoinRule.Public &&
|
||||
<div className="mx_CreateSpaceFromCommunityDialog_nonPublicSpacer" />
|
||||
}
|
||||
</SpaceCreateForm>
|
||||
</div>
|
||||
|
||||
<div className="mx_CreateSpaceFromCommunityDialog_footer">
|
||||
{ footer }
|
||||
</div>
|
||||
</BaseDialog>;
|
||||
};
|
||||
|
||||
export default CreateSpaceFromCommunityDialog;
|
||||
|
187
src/components/views/dialogs/CreateSubspaceDialog.tsx
Normal file
187
src/components/views/dialogs/CreateSubspaceDialog.tsx
Normal file
|
@ -0,0 +1,187 @@
|
|||
/*
|
||||
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, { useRef, useState } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { BetaPill } from "../beta/BetaCard";
|
||||
import Field from "../elements/Field";
|
||||
import RoomAliasField from "../elements/RoomAliasField";
|
||||
import SpaceStore from "../../../stores/SpaceStore";
|
||||
import { createSpace, SpaceCreateForm } from "../spaces/SpaceCreateMenu";
|
||||
import { SubspaceSelector } from "./AddExistingToSpaceDialog";
|
||||
import JoinRuleDropdown from "../elements/JoinRuleDropdown";
|
||||
|
||||
interface IProps {
|
||||
space: Room;
|
||||
onAddExistingSpaceClick(): void;
|
||||
onFinished(added?: boolean): void;
|
||||
}
|
||||
|
||||
const CreateSubspaceDialog: React.FC<IProps> = ({ space, onAddExistingSpaceClick, onFinished }) => {
|
||||
const [parentSpace, setParentSpace] = useState(space);
|
||||
|
||||
const [busy, setBusy] = useState<boolean>(false);
|
||||
const [name, setName] = useState("");
|
||||
const spaceNameField = useRef<Field>();
|
||||
const [alias, setAlias] = useState("");
|
||||
const spaceAliasField = useRef<RoomAliasField>();
|
||||
const [avatar, setAvatar] = useState<File>(null);
|
||||
const [topic, setTopic] = useState<string>("");
|
||||
|
||||
const supportsRestricted = !!SpaceStore.instance.restrictedJoinRuleSupport?.preferred;
|
||||
|
||||
const spaceJoinRule = space.getJoinRule();
|
||||
let defaultJoinRule = JoinRule.Invite;
|
||||
if (spaceJoinRule === JoinRule.Public) {
|
||||
defaultJoinRule = JoinRule.Public;
|
||||
} else if (supportsRestricted) {
|
||||
defaultJoinRule = JoinRule.Restricted;
|
||||
}
|
||||
const [joinRule, setJoinRule] = useState<JoinRule>(defaultJoinRule);
|
||||
|
||||
const onCreateSubspaceClick = async (e) => {
|
||||
e.preventDefault();
|
||||
if (busy) return;
|
||||
|
||||
setBusy(true);
|
||||
// require & validate the space name field
|
||||
if (!await spaceNameField.current.validate({ allowEmpty: false })) {
|
||||
spaceNameField.current.focus();
|
||||
spaceNameField.current.validate({ allowEmpty: false, focused: true });
|
||||
setBusy(false);
|
||||
return;
|
||||
}
|
||||
// validate the space name alias field but do not require it
|
||||
if (joinRule === JoinRule.Public && !await spaceAliasField.current.validate({ allowEmpty: true })) {
|
||||
spaceAliasField.current.focus();
|
||||
spaceAliasField.current.validate({ allowEmpty: true, focused: true });
|
||||
setBusy(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await createSpace(name, joinRule === JoinRule.Public, alias, topic, avatar, {}, { parentSpace });
|
||||
|
||||
onFinished(true);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
let joinRuleMicrocopy: JSX.Element;
|
||||
if (joinRule === JoinRule.Restricted) {
|
||||
joinRuleMicrocopy = <p>
|
||||
{ _t(
|
||||
"Anyone in <SpaceName/> will be able to find and join.", {}, {
|
||||
SpaceName: () => <b>{ parentSpace.name }</b>,
|
||||
},
|
||||
) }
|
||||
</p>;
|
||||
} else if (joinRule === JoinRule.Public) {
|
||||
joinRuleMicrocopy = <p>
|
||||
{ _t(
|
||||
"Anyone will be able to find and join this space, not just members of <SpaceName/>.", {}, {
|
||||
SpaceName: () => <b>{ parentSpace.name }</b>,
|
||||
},
|
||||
) }
|
||||
</p>;
|
||||
} else if (joinRule === JoinRule.Invite) {
|
||||
joinRuleMicrocopy = <p>
|
||||
{ _t("Only people invited will be able to find and join this space.") }
|
||||
</p>;
|
||||
}
|
||||
|
||||
return <BaseDialog
|
||||
title={(
|
||||
<SubspaceSelector
|
||||
title={_t("Create a space")}
|
||||
space={space}
|
||||
value={parentSpace}
|
||||
onChange={setParentSpace}
|
||||
/>
|
||||
)}
|
||||
className="mx_CreateSubspaceDialog"
|
||||
contentId="mx_CreateSubspaceDialog"
|
||||
onFinished={onFinished}
|
||||
fixedWidth={false}
|
||||
>
|
||||
<MatrixClientContext.Provider value={space.client}>
|
||||
<div className="mx_CreateSubspaceDialog_content">
|
||||
<div className="mx_CreateSubspaceDialog_betaNotice">
|
||||
<BetaPill />
|
||||
{ _t("Add a space to a space you manage.") }
|
||||
</div>
|
||||
|
||||
<SpaceCreateForm
|
||||
busy={busy}
|
||||
onSubmit={onCreateSubspaceClick}
|
||||
setAvatar={setAvatar}
|
||||
name={name}
|
||||
setName={setName}
|
||||
nameFieldRef={spaceNameField}
|
||||
topic={topic}
|
||||
setTopic={setTopic}
|
||||
alias={alias}
|
||||
setAlias={setAlias}
|
||||
showAliasField={joinRule === JoinRule.Public}
|
||||
aliasFieldRef={spaceAliasField}
|
||||
>
|
||||
<JoinRuleDropdown
|
||||
label={_t("Space visibility")}
|
||||
labelInvite={_t("Private space (invite only)")}
|
||||
labelPublic={_t("Public space")}
|
||||
labelRestricted={supportsRestricted ? _t("Visible to space members") : undefined}
|
||||
width={478}
|
||||
value={joinRule}
|
||||
onChange={setJoinRule}
|
||||
/>
|
||||
{ joinRuleMicrocopy }
|
||||
</SpaceCreateForm>
|
||||
</div>
|
||||
|
||||
<div className="mx_CreateSubspaceDialog_footer">
|
||||
<div className="mx_CreateSubspaceDialog_footer_prompt">
|
||||
<div>{ _t("Want to add an existing space instead?") }</div>
|
||||
<AccessibleButton
|
||||
kind="link"
|
||||
onClick={() => {
|
||||
onAddExistingSpaceClick();
|
||||
onFinished();
|
||||
}}
|
||||
>
|
||||
{ _t("Add existing space") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
|
||||
<AccessibleButton kind="primary_outline" disabled={busy} onClick={() => onFinished(false)}>
|
||||
{ _t("Cancel") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton kind="primary" disabled={busy} onClick={onCreateSubspaceClick}>
|
||||
{ busy ? _t("Adding...") : _t("Add") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</MatrixClientContext.Provider>
|
||||
</BaseDialog>;
|
||||
};
|
||||
|
||||
export default CreateSubspaceDialog;
|
||||
|
|
@ -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>
|
||||
|
|
|
@ -172,11 +172,11 @@ export default class DeactivateAccountDialog extends React.Component<IProps, ISt
|
|||
</div>;
|
||||
}
|
||||
|
||||
let auth = <div>{_t("Loading...")}</div>;
|
||||
let auth = <div>{ _t("Loading...") }</div>;
|
||||
if (this.state.authData && this.state.authEnabled) {
|
||||
auth = (
|
||||
<div>
|
||||
{this.state.bodyText}
|
||||
{ this.state.bodyText }
|
||||
<InteractiveAuth
|
||||
matrixClient={MatrixClientPeg.get()}
|
||||
authData={this.state.authData}
|
||||
|
@ -230,18 +230,18 @@ export default class DeactivateAccountDialog extends React.Component<IProps, ISt
|
|||
checked={this.state.shouldErase}
|
||||
onChange={this.onEraseFieldChange}
|
||||
>
|
||||
{_t(
|
||||
{ _t(
|
||||
"Please forget all messages I have sent when my account is deactivated " +
|
||||
"(<b>Warning:</b> this will cause future users to see an incomplete view " +
|
||||
"of conversations)",
|
||||
{},
|
||||
{ b: (sub) => <b>{ sub }</b> },
|
||||
)}
|
||||
) }
|
||||
</StyledCheckbox>
|
||||
</p>
|
||||
|
||||
{error}
|
||||
{auth}
|
||||
{ error }
|
||||
{ auth }
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -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}
|
||||
|
@ -337,7 +356,7 @@ class FilteredList extends React.PureComponent<IFilteredListProps, IFilteredList
|
|||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
|
||||
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line
|
||||
if (this.props.children === nextProps.children && this.props.query === nextProps.query) return;
|
||||
this.setState({
|
||||
filteredChildren: FilteredList.filterChildren(nextProps.children, nextProps.query),
|
||||
|
@ -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={{
|
||||
eventType: this.state.event.getType(),
|
||||
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
|
||||
stateKey: this.state.event.getStateKey(),
|
||||
}} />;
|
||||
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">
|
||||
|
@ -494,7 +525,7 @@ class RoomStateExplorer extends React.PureComponent<IExplorerProps, IRoomStateEx
|
|||
}
|
||||
|
||||
return <button className={classes} key={eventType} onClick={onClickFn}>
|
||||
{eventType}
|
||||
{ eventType }
|
||||
</button>;
|
||||
})
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -726,17 +761,17 @@ const VerificationRequestExplorer: React.FC<{
|
|||
return (<div className="mx_DevTools_VerificationRequest">
|
||||
<dl>
|
||||
<dt>Transaction</dt>
|
||||
<dd>{txnId}</dd>
|
||||
<dd>{ txnId }</dd>
|
||||
<dt>Phase</dt>
|
||||
<dd>{PHASE_MAP[request.phase] || request.phase}</dd>
|
||||
<dd>{ PHASE_MAP[request.phase] || request.phase }</dd>
|
||||
<dt>Timeout</dt>
|
||||
<dd>{Math.floor(timeout / 1000)}</dd>
|
||||
<dd>{ Math.floor(timeout / 1000) }</dd>
|
||||
<dt>Methods</dt>
|
||||
<dd>{request.methods && request.methods.join(", ")}</dd>
|
||||
<dd>{ request.methods && request.methods.join(", ") }</dd>
|
||||
<dt>requestingUserId</dt>
|
||||
<dd>{request.requestingUserId}</dd>
|
||||
<dd>{ request.requestingUserId }</dd>
|
||||
<dt>observeOnly</dt>
|
||||
<dd>{JSON.stringify(request.observeOnly)}</dd>
|
||||
<dd>{ JSON.stringify(request.observeOnly) }</dd>
|
||||
</dl>
|
||||
</div>);
|
||||
};
|
||||
|
@ -771,12 +806,12 @@ class VerificationExplorer extends React.PureComponent<IExplorerProps> {
|
|||
|
||||
return (<div>
|
||||
<div className="mx_Dialog_content">
|
||||
{Array.from(inRoomRequests.entries()).reverse().map(([txnId, request]) =>
|
||||
{ Array.from(inRoomRequests.entries()).reverse().map(([txnId, request]) =>
|
||||
<VerificationRequestExplorer txnId={txnId} request={request} key={txnId} />,
|
||||
)}
|
||||
) }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.props.onBack}>{_t("Back")}</button>
|
||||
<button onClick={this.props.onBack}>{ _t("Back") }</button>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
@ -844,9 +879,9 @@ class WidgetExplorer extends React.Component<IExplorerProps, IWidgetExplorerStat
|
|||
const stateEv = allState.find(ev => ev.getId() === editWidget.eventId);
|
||||
if (!stateEv) { // "should never happen"
|
||||
return <div>
|
||||
{_t("There was an error finding this widget.")}
|
||||
{ _t("There was an error finding this widget.") }
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onBack}>{_t("Back")}</button>
|
||||
<button onClick={this.onBack}>{ _t("Back") }</button>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
@ -865,17 +900,17 @@ class WidgetExplorer extends React.Component<IExplorerProps, IWidgetExplorerStat
|
|||
return (<div>
|
||||
<div className="mx_Dialog_content">
|
||||
<FilteredList query={this.state.query} onChange={this.onQueryChange}>
|
||||
{widgets.map(w => {
|
||||
{ widgets.map(w => {
|
||||
return <button
|
||||
className='mx_DevTools_RoomStateExplorer_button'
|
||||
key={w.url + w.eventId}
|
||||
onClick={() => this.onEditWidget(w)}
|
||||
>{w.url}</button>;
|
||||
})}
|
||||
>{ w.url }</button>;
|
||||
}) }
|
||||
</FilteredList>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onBack}>{_t("Back")}</button>
|
||||
<button onClick={this.onBack}>{ _t("Back") }</button>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
@ -1007,7 +1042,7 @@ class SettingsExplorer extends React.PureComponent<IExplorerProps, ISettingsExpl
|
|||
private renderCanEditLevel(roomId: string, level: SettingLevel): React.ReactNode {
|
||||
const canEdit = SettingsStore.canSetValue(this.state.editSetting, roomId, level);
|
||||
const className = canEdit ? 'mx_DevTools_SettingsExplorer_mutable' : 'mx_DevTools_SettingsExplorer_immutable';
|
||||
return <td className={className}><code>{canEdit.toString()}</code></td>;
|
||||
return <td className={className}><code>{ canEdit.toString() }</code></td>;
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -1021,46 +1056,53 @@ 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>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{_t("Setting ID")}</th>
|
||||
<th>{_t("Value")}</th>
|
||||
<th>{_t("Value in this room")}</th>
|
||||
<th>{ _t("Setting ID") }</th>
|
||||
<th>{ _t("Value") }</th>
|
||||
<th>{ _t("Value in this room") }</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{allSettings.map(i => (
|
||||
{ allSettings.map(i => (
|
||||
<tr key={i}>
|
||||
<td>
|
||||
<a href="" onClick={(e) => this.onViewClick(e, i)}>
|
||||
<code>{i}</code>
|
||||
<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'
|
||||
>
|
||||
✏
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<code>{this.renderSettingValue(SettingsStore.getValue(i))}</code>
|
||||
<code>{ this.renderSettingValue(SettingsStore.getValue(i)) }</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>
|
||||
{this.renderSettingValue(SettingsStore.getValue(i, room.roomId))}
|
||||
{ this.renderSettingValue(SettingsStore.getValue(i, room.roomId)) }
|
||||
</code>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
)) }
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onBack}>{_t("Back")}</button>
|
||||
<button onClick={this.onBack}>{ _t("Back") }</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1068,62 +1110,70 @@ class SettingsExplorer extends React.PureComponent<IExplorerProps, ISettingsExpl
|
|||
return (
|
||||
<div>
|
||||
<div className="mx_Dialog_content mx_DevTools_SettingsExplorer">
|
||||
<h3>{_t("Setting:")} <code>{this.state.editSetting}</code></h3>
|
||||
<h3>{ _t("Setting:") } <code>{ this.state.editSetting }</code></h3>
|
||||
|
||||
<div className='mx_DevTools_SettingsExplorer_warning'>
|
||||
<b>{_t("Caution:")}</b> {_t(
|
||||
<b>{ _t("Caution:") }</b> { _t(
|
||||
"This UI does NOT check the types of the values. Use at your own risk.",
|
||||
)}
|
||||
) }
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{_t("Setting definition:")}
|
||||
<pre><code>{JSON.stringify(SETTINGS[this.state.editSetting], null, 4)}</code></pre>
|
||||
{ _t("Setting definition:") }
|
||||
<pre><code>{ JSON.stringify(SETTINGS[this.state.editSetting], null, 4) }</code></pre>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{_t("Level")}</th>
|
||||
<th>{_t("Settable at global")}</th>
|
||||
<th>{_t("Settable at room")}</th>
|
||||
<th>{ _t("Level") }</th>
|
||||
<th>{ _t("Settable at global") }</th>
|
||||
<th>{ _t("Settable at room") }</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{LEVEL_ORDER.map(lvl => (
|
||||
{ LEVEL_ORDER.map(lvl => (
|
||||
<tr key={lvl}>
|
||||
<td><code>{lvl}</code></td>
|
||||
{this.renderCanEditLevel(null, lvl)}
|
||||
{this.renderCanEditLevel(room.roomId, lvl)}
|
||||
<td><code>{ lvl }</code></td>
|
||||
{ this.renderCanEditLevel(null, lvl) }
|
||||
{ this.renderCanEditLevel(room.roomId, lvl) }
|
||||
</tr>
|
||||
))}
|
||||
)) }
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onSaveClick}>{_t("Save setting values")}</button>
|
||||
<button onClick={this.onBack}>{_t("Back")}</button>
|
||||
<button onClick={this.onSaveClick}>{ _t("Save setting values") }</button>
|
||||
<button onClick={this.onBack}>{ _t("Back") }</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1131,39 +1181,39 @@ class SettingsExplorer extends React.PureComponent<IExplorerProps, ISettingsExpl
|
|||
return (
|
||||
<div>
|
||||
<div className="mx_Dialog_content mx_DevTools_SettingsExplorer">
|
||||
<h3>{_t("Setting:")} <code>{this.state.viewSetting}</code></h3>
|
||||
<h3>{ _t("Setting:") } <code>{ this.state.viewSetting }</code></h3>
|
||||
|
||||
<div>
|
||||
{_t("Setting definition:")}
|
||||
<pre><code>{JSON.stringify(SETTINGS[this.state.viewSetting], null, 4)}</code></pre>
|
||||
{ _t("Setting definition:") }
|
||||
<pre><code>{ JSON.stringify(SETTINGS[this.state.viewSetting], null, 4) }</code></pre>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{_t("Value:")}
|
||||
<code>{this.renderSettingValue(
|
||||
{ _t("Value:") }
|
||||
<code>{ this.renderSettingValue(
|
||||
SettingsStore.getValue(this.state.viewSetting),
|
||||
)}</code>
|
||||
) }</code>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{_t("Value in this room:")}
|
||||
<code>{this.renderSettingValue(
|
||||
{ _t("Value in this room:") }
|
||||
<code>{ this.renderSettingValue(
|
||||
SettingsStore.getValue(this.state.viewSetting, room.roomId),
|
||||
)}</code>
|
||||
) }</code>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{_t("Values at explicit levels:")}
|
||||
<pre><code>{this.renderExplicitSettingValues(
|
||||
{ _t("Values at explicit levels:") }
|
||||
<pre><code>{ this.renderExplicitSettingValues(
|
||||
this.state.viewSetting, null,
|
||||
)}</code></pre>
|
||||
) }</code></pre>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{_t("Values at explicit levels in this room:")}
|
||||
<pre><code>{this.renderExplicitSettingValues(
|
||||
{ _t("Values at explicit levels in this room:") }
|
||||
<pre><code>{ this.renderExplicitSettingValues(
|
||||
this.state.viewSetting, room.roomId,
|
||||
)}</code></pre>
|
||||
) }</code></pre>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@ -1171,7 +1221,7 @@ class SettingsExplorer extends React.PureComponent<IExplorerProps, ISettingsExpl
|
|||
<button onClick={(e) => this.onEditClick(e, this.state.viewSetting)}>{
|
||||
_t("Edit Values")
|
||||
}</button>
|
||||
<button onClick={this.onBack}>{_t("Back")}</button>
|
||||
<button onClick={this.onBack}>{ _t("Back") }</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1232,12 +1282,12 @@ export default class DevtoolsDialog extends React.PureComponent<IProps, IState>
|
|||
|
||||
if (this.state.mode) {
|
||||
body = <MatrixClientContext.Consumer>
|
||||
{(cli) => <React.Fragment>
|
||||
{ (cli) => <React.Fragment>
|
||||
<div className="mx_DevTools_label_left">{ this.state.mode.getLabel() }</div>
|
||||
<div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div>
|
||||
<div className="mx_DevTools_label_bottom" />
|
||||
<this.state.mode onBack={this.onBack} room={cli.getRoom(this.props.roomId)} />
|
||||
</React.Fragment>}
|
||||
</React.Fragment> }
|
||||
</MatrixClientContext.Consumer>;
|
||||
} else {
|
||||
const classes = "mx_DevTools_RoomStateExplorer_button";
|
||||
|
|
|
@ -144,23 +144,25 @@ 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
|
||||
onClick={this.onChangeAvatar}
|
||||
className="mx_EditCommunityPrototypeDialog_avatarContainer"
|
||||
>{preview}</AccessibleButton>
|
||||
>{ preview }</AccessibleButton>
|
||||
<div className="mx_EditCommunityPrototypeDialog_tip">
|
||||
<b>{_t("Add image (optional)")}</b>
|
||||
<b>{ _t("Add image (optional)") }</b>
|
||||
<span>
|
||||
{_t("An image will help people identify your community.")}
|
||||
{ _t("An image will help people identify your community.") }
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<AccessibleButton kind="primary" onClick={this.onSubmit} disabled={this.state.busy}>
|
||||
{_t("Save")}
|
||||
{ _t("Save") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -58,10 +58,10 @@ export default (props) => {
|
|||
countlyFeedbackSection = <React.Fragment>
|
||||
<hr />
|
||||
<div className="mx_FeedbackDialog_section mx_FeedbackDialog_rateApp">
|
||||
<h3>{_t("Rate %(brand)s", { brand })}</h3>
|
||||
<h3>{ _t("Rate %(brand)s", { brand }) }</h3>
|
||||
|
||||
<p>{_t("Tell us below how you feel about %(brand)s so far.", { brand })}</p>
|
||||
<p>{_t("Please go into as much detail as you like, so we can track down the problem.")}</p>
|
||||
<p>{ _t("Tell us below how you feel about %(brand)s so far.", { brand }) }</p>
|
||||
<p>{ _t("Please go into as much detail as you like, so we can track down the problem.") }</p>
|
||||
|
||||
<StyledRadioGroup
|
||||
name="feedbackRating"
|
||||
|
@ -95,7 +95,7 @@ export default (props) => {
|
|||
let subheading;
|
||||
if (hasFeedback) {
|
||||
subheading = (
|
||||
<h2>{_t("There are two ways you can provide feedback and help us improve %(brand)s.", { brand })}</h2>
|
||||
<h2>{ _t("There are two ways you can provide feedback and help us improve %(brand)s.", { brand }) }</h2>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -106,7 +106,7 @@ export default (props) => {
|
|||
_t("PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> " +
|
||||
"to help us track down the problem.", {}, {
|
||||
debugLogsLink: sub => (
|
||||
<AccessibleButton kind="link" onClick={onDebugLogsLinkClick}>{sub}</AccessibleButton>
|
||||
<AccessibleButton kind="link" onClick={onDebugLogsLinkClick}>{ sub }</AccessibleButton>
|
||||
),
|
||||
})
|
||||
}</p>
|
||||
|
@ -121,7 +121,7 @@ export default (props) => {
|
|||
{ subheading }
|
||||
|
||||
<div className="mx_FeedbackDialog_section mx_FeedbackDialog_reportBug">
|
||||
<h3>{_t("Report a bug")}</h3>
|
||||
<h3>{ _t("Report a bug") }</h3>
|
||||
<p>{
|
||||
_t("Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. " +
|
||||
"No match? <newIssueLink>Start a new one</newIssueLink>.", {}, {
|
||||
|
@ -133,7 +133,7 @@ export default (props) => {
|
|||
},
|
||||
})
|
||||
}</p>
|
||||
{bugReports}
|
||||
{ bugReports }
|
||||
</div>
|
||||
{ countlyFeedbackSection }
|
||||
</React.Fragment>}
|
||||
|
|
|
@ -43,6 +43,7 @@ import QueryMatcher from "../../../autocomplete/QueryMatcher";
|
|||
import TruncatedList from "../elements/TruncatedList";
|
||||
import EntityTile from "../rooms/EntityTile";
|
||||
import BaseAvatar from "../avatars/BaseAvatar";
|
||||
import SpaceStore from "../../../stores/SpaceStore";
|
||||
|
||||
const AVATAR_SIZE = 30;
|
||||
|
||||
|
@ -105,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;
|
||||
|
@ -180,7 +181,7 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
|
|||
const [query, setQuery] = useState("");
|
||||
const lcQuery = query.toLowerCase();
|
||||
|
||||
const spacesEnabled = useFeatureEnabled("feature_spaces");
|
||||
const spacesEnabled = SpaceStore.spacesEnabled;
|
||||
const flairEnabled = useFeatureEnabled(UIFeature.Flair);
|
||||
const previewLayout = useSettingValue<Layout>("layout");
|
||||
|
||||
|
@ -203,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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
101
src/components/views/dialogs/GenericFeatureFeedbackDialog.tsx
Normal file
101
src/components/views/dialogs/GenericFeatureFeedbackDialog.tsx
Normal file
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
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, { useState } from "react";
|
||||
|
||||
import QuestionDialog from './QuestionDialog';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Field from "../elements/Field";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import { submitFeedback } from "../../../rageshake/submit-rageshake";
|
||||
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||
import Modal from "../../../Modal";
|
||||
import InfoDialog from "./InfoDialog";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
title: string;
|
||||
subheading: string;
|
||||
rageshakeLabel: string;
|
||||
rageshakeData?: Record<string, string>;
|
||||
}
|
||||
|
||||
const GenericFeatureFeedbackDialog: React.FC<IProps> = ({
|
||||
title,
|
||||
subheading,
|
||||
children,
|
||||
rageshakeLabel,
|
||||
rageshakeData = {},
|
||||
onFinished,
|
||||
}) => {
|
||||
const [comment, setComment] = useState("");
|
||||
const [canContact, setCanContact] = useState(false);
|
||||
|
||||
const sendFeedback = async (ok: boolean) => {
|
||||
if (!ok) return onFinished(false);
|
||||
|
||||
submitFeedback(SdkConfig.get().bug_report_endpoint_url, rageshakeLabel, comment, canContact, rageshakeData);
|
||||
onFinished(true);
|
||||
|
||||
Modal.createTrackedDialog("Feedback Sent", rageshakeLabel, InfoDialog, {
|
||||
title,
|
||||
description: _t("Thank you for your feedback, we really appreciate it."),
|
||||
button: _t("Done"),
|
||||
hasCloseButton: false,
|
||||
fixedWidth: false,
|
||||
});
|
||||
};
|
||||
|
||||
return (<QuestionDialog
|
||||
className="mx_GenericFeatureFeedbackDialog"
|
||||
hasCancelButton={true}
|
||||
title={title}
|
||||
description={<React.Fragment>
|
||||
<div className="mx_GenericFeatureFeedbackDialog_subheading">
|
||||
{ subheading }
|
||||
|
||||
{ _t("Your platform and username will be noted to help us use your feedback as much as we can.") }
|
||||
|
||||
{ children }
|
||||
</div>
|
||||
|
||||
<Field
|
||||
id="feedbackComment"
|
||||
label={_t("Feedback")}
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
value={comment}
|
||||
element="textarea"
|
||||
onChange={(ev) => {
|
||||
setComment(ev.target.value);
|
||||
}}
|
||||
autoFocus={true}
|
||||
/>
|
||||
|
||||
<StyledCheckbox
|
||||
checked={canContact}
|
||||
onChange={e => setCanContact((e.target as HTMLInputElement).checked)}
|
||||
>
|
||||
{ _t("You may contact me if you have any follow up questions") }
|
||||
</StyledCheckbox>
|
||||
</React.Fragment>}
|
||||
button={_t("Send feedback")}
|
||||
buttonDisabled={!comment}
|
||||
onFinished={sendFeedback}
|
||||
/>);
|
||||
};
|
||||
|
||||
export default GenericFeatureFeedbackDialog;
|
|
@ -177,32 +177,32 @@ export default class HostSignupDialog extends React.PureComponent<IProps, IState
|
|||
const textComponent = (
|
||||
<>
|
||||
<p>
|
||||
{_t("Continuing temporarily allows the %(hostSignupBrand)s setup process to access your " +
|
||||
{ _t("Continuing temporarily allows the %(hostSignupBrand)s setup process to access your " +
|
||||
"account to fetch verified email addresses. This data is not stored.", {
|
||||
hostSignupBrand: this.config.brand,
|
||||
})}
|
||||
}) }
|
||||
</p>
|
||||
<p>
|
||||
{_t("Learn more in our <privacyPolicyLink />, <termsOfServiceLink /> and <cookiePolicyLink />.",
|
||||
{ _t("Learn more in our <privacyPolicyLink />, <termsOfServiceLink /> and <cookiePolicyLink />.",
|
||||
{},
|
||||
{
|
||||
cookiePolicyLink: () => (
|
||||
<a href={this.config.cookiePolicyUrl} target="_blank" rel="noreferrer noopener">
|
||||
{_t("Cookie Policy")}
|
||||
{ _t("Cookie Policy") }
|
||||
</a>
|
||||
),
|
||||
privacyPolicyLink: () => (
|
||||
<a href={this.config.privacyPolicyUrl} target="_blank" rel="noreferrer noopener">
|
||||
{_t("Privacy Policy")}
|
||||
{ _t("Privacy Policy") }
|
||||
</a>
|
||||
),
|
||||
termsOfServiceLink: () => (
|
||||
<a href={this.config.termsOfServiceUrl} target="_blank" rel="noreferrer noopener">
|
||||
{_t("Terms of Service")}
|
||||
{ _t("Terms of Service") }
|
||||
</a>
|
||||
),
|
||||
},
|
||||
)}
|
||||
) }
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
|
@ -241,12 +241,12 @@ export default class HostSignupDialog extends React.PureComponent<IProps, IState
|
|||
},
|
||||
)}
|
||||
>
|
||||
{this.state.minimized &&
|
||||
{ this.state.minimized &&
|
||||
<div className="mx_Dialog_header mx_Dialog_headerWithButton">
|
||||
<div className="mx_Dialog_title">
|
||||
{_t("%(hostSignupBrand)s Setup", {
|
||||
{ _t("%(hostSignupBrand)s Setup", {
|
||||
hostSignupBrand: this.config.brand,
|
||||
})}
|
||||
}) }
|
||||
</div>
|
||||
<AccessibleButton
|
||||
className="mx_HostSignup_maximize_button"
|
||||
|
@ -256,7 +256,7 @@ export default class HostSignupDialog extends React.PureComponent<IProps, IState
|
|||
/>
|
||||
</div>
|
||||
}
|
||||
{!this.state.minimized &&
|
||||
{ !this.state.minimized &&
|
||||
<div className="mx_Dialog_header mx_Dialog_headerWithCancel">
|
||||
<AccessibleButton
|
||||
onClick={this.minimizeDialog}
|
||||
|
@ -272,12 +272,12 @@ export default class HostSignupDialog extends React.PureComponent<IProps, IState
|
|||
/>
|
||||
</div>
|
||||
}
|
||||
{this.state.error &&
|
||||
{ this.state.error &&
|
||||
<div>
|
||||
{this.state.error}
|
||||
{ this.state.error }
|
||||
</div>
|
||||
}
|
||||
{!this.state.error &&
|
||||
{ !this.state.error &&
|
||||
<iframe
|
||||
src={this.config.url}
|
||||
ref={this.iframeRef}
|
||||
|
|
|
@ -133,55 +133,60 @@ 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>
|
||||
<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>
|
||||
<h2>{ this.props.verifier.userId }</h2>
|
||||
</div>;
|
||||
} else {
|
||||
profile = <Spinner />;
|
||||
}
|
||||
|
||||
const userDetailText = [
|
||||
<p key="p1">{_t(
|
||||
<p key="p1">{ _t(
|
||||
"Verify this user to mark them as trusted. " +
|
||||
"Trusting users gives you extra peace of mind when using " +
|
||||
"end-to-end encrypted messages.",
|
||||
)}</p>,
|
||||
<p key="p2">{_t(
|
||||
) }</p>,
|
||||
<p key="p2">{ _t(
|
||||
// NB. Below wording adjusted to singular 'session' until we have
|
||||
// cross-signing
|
||||
"Verifying this user will mark their session as trusted, and " +
|
||||
"also mark your session as trusted to them.",
|
||||
)}</p>,
|
||||
) }</p>,
|
||||
];
|
||||
|
||||
const selfDetailText = [
|
||||
<p key="p1">{_t(
|
||||
<p key="p1">{ _t(
|
||||
"Verify this device to mark it as trusted. " +
|
||||
"Trusting this device gives you and other users extra peace of mind when using " +
|
||||
"end-to-end encrypted messages.",
|
||||
)}</p>,
|
||||
<p key="p2">{_t(
|
||||
) }</p>,
|
||||
<p key="p2">{ _t(
|
||||
"Verifying this device will mark it as trusted, and users who have verified with " +
|
||||
"you will trust this device.",
|
||||
)}</p>,
|
||||
) }</p>,
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
{profile}
|
||||
{isSelf ? selfDetailText : userDetailText}
|
||||
{ profile }
|
||||
{ isSelf ? selfDetailText : userDetailText }
|
||||
<DialogButtons
|
||||
primaryButton={_t('Continue')}
|
||||
hasCancel={true}
|
||||
|
@ -209,7 +214,7 @@ export default class IncomingSasDialog extends React.Component {
|
|||
return (
|
||||
<div>
|
||||
<Spinner />
|
||||
<p>{_t("Waiting for partner to confirm...")}</p>
|
||||
<p>{ _t("Waiting for partner to confirm...") }</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -251,7 +256,7 @@ export default class IncomingSasDialog extends React.Component {
|
|||
onFinished={this._onFinished}
|
||||
fixedWidth={false}
|
||||
>
|
||||
{body}
|
||||
{ body }
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -49,7 +49,7 @@ export default class IntegrationsDisabledDialog extends React.Component {
|
|||
title={_t("Integrations are disabled")}
|
||||
>
|
||||
<div className='mx_IntegrationsDisabledDialog_content'>
|
||||
<p>{_t("Enable 'Manage Integrations' in Settings to do this.")}</p>
|
||||
<p>{ _t("Enable 'Manage Integrations' in Settings to do this.") }</p>
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Settings")}
|
||||
|
|
|
@ -45,11 +45,11 @@ export default class IntegrationsImpossibleDialog extends React.Component {
|
|||
>
|
||||
<div className='mx_IntegrationsImpossibleDialog_content'>
|
||||
<p>
|
||||
{_t(
|
||||
"Your %(brand)s doesn't allow you to use an Integration Manager to do this. " +
|
||||
{ _t(
|
||||
"Your %(brand)s doesn't allow you to use an integration manager to do this. " +
|
||||
"Please contact an admin.",
|
||||
{ brand },
|
||||
)}
|
||||
) }
|
||||
</p>
|
||||
</div>
|
||||
<DialogButtons
|
||||
|
|
|
@ -163,7 +163,7 @@ export default class InteractiveAuthDialog extends React.Component {
|
|||
} else {
|
||||
content = (
|
||||
<div id='mx_Dialog_content'>
|
||||
{body}
|
||||
{ body }
|
||||
<InteractiveAuth
|
||||
ref={this._collectInteractiveAuth}
|
||||
matrixClient={this.props.matrixClient}
|
||||
|
|
|
@ -32,7 +32,6 @@ import Modal from "../../../Modal";
|
|||
import { humanizeTime } from "../../../utils/humanize";
|
||||
import createRoom, {
|
||||
canEncryptToAllUsers,
|
||||
ensureDMExists,
|
||||
findDMForUser,
|
||||
privateShouldBeEncrypted,
|
||||
} from "../../../createRoom";
|
||||
|
@ -56,7 +55,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
|||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import { getAddressType } from "../../../UserAddress";
|
||||
import BaseAvatar from '../avatars/BaseAvatar';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
|
||||
import { compare } from '../../../utils/strings';
|
||||
import { IInvite3PID } from "matrix-js-sdk/src/@types/requests";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
|
@ -64,9 +63,15 @@ import { copyPlaintext, selectText } from "../../../utils/strings";
|
|||
import * as ContextMenu from "../../structures/ContextMenu";
|
||||
import { toRightOf } from "../../structures/ContextMenu";
|
||||
import GenericTextContextMenu from "../context_menus/GenericTextContextMenu";
|
||||
import { TransferCallPayload } from '../../../dispatcher/payloads/TransferCallPayload';
|
||||
import Field from '../elements/Field';
|
||||
import TabbedView, { Tab, TabLocation } from '../../structures/TabbedView';
|
||||
import Dialpad from '../voip/DialPad';
|
||||
import QuestionDialog from "./QuestionDialog";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DialPadBackspaceButton from "../elements/DialPadBackspaceButton";
|
||||
import SpaceStore from "../../../stores/SpaceStore";
|
||||
|
||||
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
||||
/* eslint-disable camelcase */
|
||||
|
@ -79,11 +84,19 @@ interface IRecentUser {
|
|||
|
||||
export const KIND_DM = "dm";
|
||||
export const KIND_INVITE = "invite";
|
||||
// NB. This dialog needs the 'mx_InviteDialog_transferWrapper' wrapper class to have the correct
|
||||
// padding on the bottom (because all modals have 24px padding on all sides), so this needs to
|
||||
// be passed when creating the modal
|
||||
export const KIND_CALL_TRANSFER = "call_transfer";
|
||||
|
||||
const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first
|
||||
const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is clicked
|
||||
|
||||
enum TabId {
|
||||
UserDirectory = 'users',
|
||||
DialPad = 'dialpad',
|
||||
}
|
||||
|
||||
// This is the interface that is expected by various components in the Invite Dialog and RoomInvite.
|
||||
// It is a bit awkward because it also matches the RoomMember class from the js-sdk with some extra support
|
||||
// for 3PIDs/email addresses.
|
||||
|
@ -109,11 +122,11 @@ export abstract class Member {
|
|||
|
||||
class DirectoryMember extends Member {
|
||||
private readonly _userId: string;
|
||||
private readonly displayName: string;
|
||||
private readonly avatarUrl: string;
|
||||
private readonly displayName?: string;
|
||||
private readonly avatarUrl?: string;
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
constructor(userDirResult: { user_id: string, display_name: string, avatar_url: string }) {
|
||||
constructor(userDirResult: { user_id: string, display_name?: string, avatar_url?: string }) {
|
||||
super();
|
||||
this._userId = userDirResult.user_id;
|
||||
this.displayName = userDirResult.display_name;
|
||||
|
@ -183,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()
|
||||
|
@ -201,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>
|
||||
);
|
||||
|
@ -211,8 +229,8 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
|||
return (
|
||||
<span className='mx_InviteDialog_userTile'>
|
||||
<span className='mx_InviteDialog_userTile_pill'>
|
||||
{avatar}
|
||||
<span className='mx_InviteDialog_userTile_name'>{this.props.member.name}</span>
|
||||
{ avatar }
|
||||
<span className='mx_InviteDialog_userTile_name'>{ this.props.member.name }</span>
|
||||
</span>
|
||||
{ closeButton }
|
||||
</span>
|
||||
|
@ -254,20 +272,20 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
|||
// Push any text we missed (first bit/middle of text)
|
||||
if (ii > i) {
|
||||
// Push any text we aren't highlighting (middle of text match, or beginning of text)
|
||||
result.push(<span key={i + 'begin'}>{str.substring(i, ii)}</span>);
|
||||
result.push(<span key={i + 'begin'}>{ str.substring(i, ii) }</span>);
|
||||
}
|
||||
|
||||
i = ii; // copy over ii only if we have a match (to preserve i for end-of-text matching)
|
||||
|
||||
// Highlight the word the user entered
|
||||
const substr = str.substring(i, filterStr.length + i);
|
||||
result.push(<span className='mx_InviteDialog_roomTile_highlight' key={i + 'bold'}>{substr}</span>);
|
||||
result.push(<span className='mx_InviteDialog_roomTile_highlight' key={i + 'bold'}>{ substr }</span>);
|
||||
i += substr.length;
|
||||
}
|
||||
|
||||
// Push any text we missed (end of text)
|
||||
if (i < str.length) {
|
||||
result.push(<span key={i + 'end'}>{str.substring(i)}</span>);
|
||||
result.push(<span key={i + 'end'}>{ str.substring(i) }</span>);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
@ -277,14 +295,16 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
|||
let timestamp = null;
|
||||
if (this.props.lastActiveTs) {
|
||||
const humanTs = humanizeTime(this.props.lastActiveTs);
|
||||
timestamp = <span className='mx_InviteDialog_roomTile_time'>{humanTs}</span>;
|
||||
timestamp = <span className='mx_InviteDialog_roomTile_time'>{ humanTs }</span>;
|
||||
}
|
||||
|
||||
const avatarSize = 36;
|
||||
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)
|
||||
|
@ -304,8 +324,8 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
|||
// the browser from reloading the image source when the avatar remounts).
|
||||
const stackedAvatar = (
|
||||
<span className='mx_InviteDialog_roomTile_avatarStack'>
|
||||
{avatar}
|
||||
{checkmark}
|
||||
{ avatar }
|
||||
{ checkmark }
|
||||
</span>
|
||||
);
|
||||
|
||||
|
@ -315,12 +335,12 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
|||
|
||||
return (
|
||||
<div className='mx_InviteDialog_roomTile' onClick={this.onClick}>
|
||||
{stackedAvatar}
|
||||
{ stackedAvatar }
|
||||
<span className="mx_InviteDialog_roomTile_nameStack">
|
||||
<div className='mx_InviteDialog_roomTile_name'>{this.highlightName(this.props.member.name)}</div>
|
||||
<div className='mx_InviteDialog_roomTile_userId'>{caption}</div>
|
||||
<div className='mx_InviteDialog_roomTile_name'>{ this.highlightName(this.props.member.name) }</div>
|
||||
<div className='mx_InviteDialog_roomTile_userId'>{ caption }</div>
|
||||
</span>
|
||||
{timestamp}
|
||||
{ timestamp }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -356,6 +376,8 @@ interface IInviteDialogState {
|
|||
canUseIdentityServer: boolean;
|
||||
tryingIdentityServer: boolean;
|
||||
consultFirst: boolean;
|
||||
dialPadValue: string;
|
||||
currentTabId: TabId;
|
||||
|
||||
// These two flags are used for the 'Go' button to communicate what is going on.
|
||||
busy: boolean;
|
||||
|
@ -370,8 +392,9 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
};
|
||||
|
||||
private closeCopiedTooltip: () => void;
|
||||
private debounceTimer: NodeJS.Timeout = null; // actually number because we're in the browser
|
||||
private debounceTimer: number = null; // actually number because we're in the browser
|
||||
private editorRef = createRef<HTMLInputElement>();
|
||||
private numberEntryFieldRef: React.RefObject<Field> = createRef();
|
||||
private unmounted = false;
|
||||
|
||||
constructor(props) {
|
||||
|
@ -407,6 +430,8 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
canUseIdentityServer: !!MatrixClientPeg.get().getIdentityServerUrl(),
|
||||
tryingIdentityServer: false,
|
||||
consultFirst: false,
|
||||
dialPadValue: '',
|
||||
currentTabId: TabId.UserDirectory,
|
||||
|
||||
// These two flags are used for the 'Go' button to communicate what is going on.
|
||||
busy: false,
|
||||
|
@ -768,44 +793,32 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
};
|
||||
|
||||
private transferCall = async () => {
|
||||
this.convertFilter();
|
||||
const targets = this.convertFilter();
|
||||
const targetIds = targets.map(t => t.userId);
|
||||
if (targetIds.length > 1) {
|
||||
this.setState({
|
||||
errorText: _t("A call can only be transferred to a single user."),
|
||||
});
|
||||
}
|
||||
|
||||
if (this.state.consultFirst) {
|
||||
const dmRoomId = await ensureDMExists(MatrixClientPeg.get(), targetIds[0]);
|
||||
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
type: this.props.call.type,
|
||||
room_id: dmRoomId,
|
||||
transferee: this.props.call,
|
||||
});
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: dmRoomId,
|
||||
should_peek: false,
|
||||
joining: false,
|
||||
});
|
||||
this.props.onFinished();
|
||||
} else {
|
||||
this.setState({ busy: true });
|
||||
try {
|
||||
await this.props.call.transfer(targetIds[0]);
|
||||
this.setState({ busy: false });
|
||||
this.props.onFinished();
|
||||
} catch (e) {
|
||||
if (this.state.currentTabId == TabId.UserDirectory) {
|
||||
this.convertFilter();
|
||||
const targets = this.convertFilter();
|
||||
const targetIds = targets.map(t => t.userId);
|
||||
if (targetIds.length > 1) {
|
||||
this.setState({
|
||||
busy: false,
|
||||
errorText: _t("Failed to transfer call"),
|
||||
errorText: _t("A call can only be transferred to a single user."),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
dis.dispatch({
|
||||
action: Action.TransferCallToMatrixID,
|
||||
call: this.props.call,
|
||||
destination: targetIds[0],
|
||||
consultFirst: this.state.consultFirst,
|
||||
} as TransferCallPayload);
|
||||
} else {
|
||||
dis.dispatch({
|
||||
action: Action.TransferCallToPhoneNumber,
|
||||
call: this.props.call,
|
||||
destination: this.state.dialPadValue,
|
||||
consultFirst: this.state.consultFirst,
|
||||
} as TransferCallPayload);
|
||||
}
|
||||
this.props.onFinished();
|
||||
};
|
||||
|
||||
private onKeyDown = (e) => {
|
||||
|
@ -827,6 +840,10 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
}
|
||||
};
|
||||
|
||||
private onCancel = () => {
|
||||
this.props.onFinished([]);
|
||||
};
|
||||
|
||||
private updateSuggestions = async (term) => {
|
||||
MatrixClientPeg.get().searchUserDirectory({ term }).then(async r => {
|
||||
if (term !== this.state.filterText) {
|
||||
|
@ -962,11 +979,14 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
private toggleMember = (member: Member) => {
|
||||
if (!this.state.busy) {
|
||||
let filterText = this.state.filterText;
|
||||
const targets = this.state.targets.map(t => t); // cheap clone for mutation
|
||||
let targets = this.state.targets.map(t => t); // cheap clone for mutation
|
||||
const idx = targets.indexOf(member);
|
||||
if (idx >= 0) {
|
||||
targets.splice(idx, 1);
|
||||
} else {
|
||||
if (this.props.kind === KIND_CALL_TRANSFER && targets.length > 0) {
|
||||
targets = [];
|
||||
}
|
||||
targets.push(member);
|
||||
filterText = ""; // clear the filter when the user accepts a suggestion
|
||||
}
|
||||
|
@ -1140,8 +1160,8 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
if (sourceMembers.length === 0 && !hasAdditionalMembers) {
|
||||
return (
|
||||
<div className='mx_InviteDialog_section'>
|
||||
<h3>{sectionName}</h3>
|
||||
<p>{_t("No results")}</p>
|
||||
<h3>{ sectionName }</h3>
|
||||
<p>{ _t("No results") }</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1163,7 +1183,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
if (hasMore) {
|
||||
showMore = (
|
||||
<AccessibleButton onClick={showMoreFn} kind="link">
|
||||
{_t("Show more")}
|
||||
{ _t("Show more") }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
@ -1180,15 +1200,20 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
));
|
||||
return (
|
||||
<div className='mx_InviteDialog_section'>
|
||||
<h3>{sectionName}</h3>
|
||||
{sectionSubname ? <p className="mx_InviteDialog_subname">{sectionSubname}</p> : null}
|
||||
{tiles}
|
||||
{showMore}
|
||||
<h3>{ sectionName }</h3>
|
||||
{ sectionSubname ? <p className="mx_InviteDialog_subname">{ sectionSubname }</p> : null }
|
||||
{ tiles }
|
||||
{ showMore }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderEditor() {
|
||||
const hasPlaceholder = (
|
||||
this.props.kind == KIND_CALL_TRANSFER &&
|
||||
this.state.targets.length === 0 &&
|
||||
this.state.filterText.length === 0
|
||||
);
|
||||
const targets = this.state.targets.map(t => (
|
||||
<DMUserTile member={t} onRemove={!this.state.busy && this.removeMember} key={t.userId} />
|
||||
));
|
||||
|
@ -1201,14 +1226,15 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
ref={this.editorRef}
|
||||
onPaste={this.onPaste}
|
||||
autoFocus={true}
|
||||
disabled={this.state.busy}
|
||||
disabled={this.state.busy || (this.props.kind == KIND_CALL_TRANSFER && this.state.targets.length > 0)}
|
||||
autoComplete="off"
|
||||
placeholder={hasPlaceholder ? _t("Search") : null}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<div className='mx_InviteDialog_editor' onClick={this.onClickInputArea}>
|
||||
{targets}
|
||||
{input}
|
||||
{ targets }
|
||||
{ input }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1223,7 +1249,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
const defaultIdentityServerUrl = getDefaultIdentityServerUrl();
|
||||
if (defaultIdentityServerUrl) {
|
||||
return (
|
||||
<div className="mx_AddressPickerDialog_identityServer">{_t(
|
||||
<div className="mx_AddressPickerDialog_identityServer">{ _t(
|
||||
"Use an identity server to invite by email. " +
|
||||
"<default>Use the default (%(defaultIdentityServerName)s)</default> " +
|
||||
"or manage in <settings>Settings</settings>.",
|
||||
|
@ -1231,24 +1257,60 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl),
|
||||
},
|
||||
{
|
||||
default: sub => <a href="#" onClick={this.onUseDefaultIdentityServerClick}>{sub}</a>,
|
||||
settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{sub}</a>,
|
||||
default: sub => <a href="#" onClick={this.onUseDefaultIdentityServerClick}>{ sub }</a>,
|
||||
settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{ sub }</a>,
|
||||
},
|
||||
)}</div>
|
||||
) }</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="mx_AddressPickerDialog_identityServer">{_t(
|
||||
<div className="mx_AddressPickerDialog_identityServer">{ _t(
|
||||
"Use an identity server to invite by email. " +
|
||||
"Manage in <settings>Settings</settings>.",
|
||||
{}, {
|
||||
settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{sub}</a>,
|
||||
settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{ sub }</a>,
|
||||
},
|
||||
)}</div>
|
||||
) }</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private onDialFormSubmit = ev => {
|
||||
ev.preventDefault();
|
||||
this.transferCall();
|
||||
};
|
||||
|
||||
private onDialChange = ev => {
|
||||
this.setState({ dialPadValue: ev.currentTarget.value });
|
||||
};
|
||||
|
||||
private onDigitPress = (digit: string, ev: ButtonEvent) => {
|
||||
this.setState({ dialPadValue: this.state.dialPadValue + digit });
|
||||
|
||||
// Keep the number field focused so that keyboard entry is still available
|
||||
// However, don't focus if this wasn't the result of directly clicking on the button,
|
||||
// i.e someone using keyboard navigation.
|
||||
if (ev.type === "click") {
|
||||
this.numberEntryFieldRef.current?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
private onDeletePress = (ev: ButtonEvent) => {
|
||||
if (this.state.dialPadValue.length === 0) return;
|
||||
this.setState({ dialPadValue: this.state.dialPadValue.slice(0, -1) });
|
||||
|
||||
// Keep the number field focused so that keyboard entry is still available
|
||||
// However, don't focus if this wasn't the result of directly clicking on the button,
|
||||
// i.e someone using keyboard navigation.
|
||||
if (ev.type === "click") {
|
||||
this.numberEntryFieldRef.current?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
private onTabChange = (tabId: TabId) => {
|
||||
this.setState({ currentTabId: tabId });
|
||||
};
|
||||
|
||||
private async onLinkClick(e) {
|
||||
e.preventDefault();
|
||||
selectText(e.target);
|
||||
|
@ -1278,12 +1340,16 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
let helpText;
|
||||
let buttonText;
|
||||
let goButtonFn;
|
||||
let consultConnectSection;
|
||||
let extraSection;
|
||||
let footer;
|
||||
let keySharingWarning = <span />;
|
||||
|
||||
const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer);
|
||||
|
||||
const hasSelection = this.state.targets.length > 0
|
||||
|| (this.state.filterText && this.state.filterText.includes('@'));
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
const userId = cli.getUserId();
|
||||
if (this.props.kind === KIND_DM) {
|
||||
|
@ -1295,7 +1361,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
{},
|
||||
{ userId: () => {
|
||||
return (
|
||||
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>
|
||||
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{ userId }</a>
|
||||
);
|
||||
} },
|
||||
);
|
||||
|
@ -1305,7 +1371,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
{},
|
||||
{ userId: () => {
|
||||
return (
|
||||
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>
|
||||
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{ userId }</a>
|
||||
);
|
||||
} },
|
||||
);
|
||||
|
@ -1323,7 +1389,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
href={makeUserPermalink(userId)}
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>{userId}</a>
|
||||
>{ userId }</a>
|
||||
);
|
||||
},
|
||||
a: (sub) => {
|
||||
|
@ -1331,13 +1397,13 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
<AccessibleButton
|
||||
kind="link"
|
||||
onClick={this.onCommunityInviteClick}
|
||||
>{sub}</AccessibleButton>
|
||||
>{ sub }</AccessibleButton>
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
helpText = <React.Fragment>
|
||||
{ helpText } {inviteText}
|
||||
{ helpText } { inviteText }
|
||||
</React.Fragment>;
|
||||
}
|
||||
buttonText = _t("Go");
|
||||
|
@ -1364,7 +1430,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
</div>;
|
||||
} else if (this.props.kind === KIND_INVITE) {
|
||||
const room = MatrixClientPeg.get()?.getRoom(this.props.roomId);
|
||||
const isSpace = SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom();
|
||||
const isSpace = SpaceStore.spacesEnabled && room?.isSpaceRoom();
|
||||
title = isSpace
|
||||
? _t("Invite to %(spaceName)s", {
|
||||
spaceName: room.name || _t("Unnamed Space"),
|
||||
|
@ -1394,9 +1460,9 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
|
||||
helpText = _t(helpTextUntranslated, {}, {
|
||||
userId: () =>
|
||||
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>,
|
||||
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{ userId }</a>,
|
||||
a: (sub) =>
|
||||
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">{sub}</a>,
|
||||
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">{ sub }</a>,
|
||||
});
|
||||
|
||||
buttonText = _t("Invite");
|
||||
|
@ -1414,30 +1480,135 @@ 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} />
|
||||
{" " + _t("Invited people will be able to read old messages.")}
|
||||
width={14}
|
||||
height={14} />
|
||||
{ " " + _t("Invited people will be able to read old messages.") }
|
||||
</p>;
|
||||
}
|
||||
}
|
||||
} else if (this.props.kind === KIND_CALL_TRANSFER) {
|
||||
title = _t("Transfer");
|
||||
buttonText = _t("Transfer");
|
||||
goButtonFn = this.transferCall;
|
||||
footer = <div>
|
||||
|
||||
consultConnectSection = <div className="mx_InviteDialog_transferConsultConnect">
|
||||
<label>
|
||||
<input type="checkbox" checked={this.state.consultFirst} onChange={this.onConsultFirstChange} />
|
||||
{_t("Consult first")}
|
||||
{ _t("Consult first") }
|
||||
</label>
|
||||
<AccessibleButton
|
||||
kind="secondary"
|
||||
onClick={this.onCancel}
|
||||
className='mx_InviteDialog_transferConsultConnect_pushRight'
|
||||
>
|
||||
{ _t("Cancel") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
onClick={this.transferCall}
|
||||
className='mx_InviteDialog_transferButton'
|
||||
disabled={!hasSelection && this.state.dialPadValue === ''}
|
||||
>
|
||||
{ _t("Transfer") }
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
} else {
|
||||
console.error("Unknown kind of InviteDialog: " + this.props.kind);
|
||||
}
|
||||
|
||||
const hasSelection = this.state.targets.length > 0
|
||||
|| (this.state.filterText && this.state.filterText.includes('@'));
|
||||
const goButton = this.props.kind == KIND_CALL_TRANSFER ? null : <AccessibleButton
|
||||
kind="primary"
|
||||
onClick={goButtonFn}
|
||||
className='mx_InviteDialog_goButton'
|
||||
disabled={this.state.busy || !hasSelection}
|
||||
>
|
||||
{ buttonText }
|
||||
</AccessibleButton>;
|
||||
|
||||
const usersSection = <React.Fragment>
|
||||
<p className='mx_InviteDialog_helpText'>{ helpText }</p>
|
||||
<div className='mx_InviteDialog_addressBar'>
|
||||
{ this.renderEditor() }
|
||||
<div className='mx_InviteDialog_buttonAndSpinner'>
|
||||
{ goButton }
|
||||
{ spinner }
|
||||
</div>
|
||||
</div>
|
||||
{ keySharingWarning }
|
||||
{ this.renderIdentityServerWarning() }
|
||||
<div className='error'>{ this.state.errorText }</div>
|
||||
<div className='mx_InviteDialog_userSections'>
|
||||
{ this.renderSection('recents') }
|
||||
{ this.renderSection('suggestions') }
|
||||
{ extraSection }
|
||||
</div>
|
||||
{ footer }
|
||||
</React.Fragment>;
|
||||
|
||||
let dialogContent;
|
||||
if (this.props.kind === KIND_CALL_TRANSFER) {
|
||||
const tabs = [];
|
||||
tabs.push(new Tab(
|
||||
TabId.UserDirectory, _td("User Directory"), 'mx_InviteDialog_userDirectoryIcon', usersSection,
|
||||
));
|
||||
|
||||
const backspaceButton = (
|
||||
<DialPadBackspaceButton onBackspacePress={this.onDeletePress} />
|
||||
);
|
||||
|
||||
// Only show the backspace button if the field has content
|
||||
let dialPadField;
|
||||
if (this.state.dialPadValue.length !== 0) {
|
||||
dialPadField = <Field
|
||||
ref={this.numberEntryFieldRef}
|
||||
className="mx_InviteDialog_dialPadField"
|
||||
id="dialpad_number"
|
||||
value={this.state.dialPadValue}
|
||||
autoFocus={true}
|
||||
onChange={this.onDialChange}
|
||||
postfixComponent={backspaceButton}
|
||||
/>;
|
||||
} else {
|
||||
dialPadField = <Field
|
||||
ref={this.numberEntryFieldRef}
|
||||
className="mx_InviteDialog_dialPadField"
|
||||
id="dialpad_number"
|
||||
value={this.state.dialPadValue}
|
||||
autoFocus={true}
|
||||
onChange={this.onDialChange}
|
||||
/>;
|
||||
}
|
||||
|
||||
const dialPadSection = <div className="mx_InviteDialog_dialPad">
|
||||
<form onSubmit={this.onDialFormSubmit}>
|
||||
{ dialPadField }
|
||||
</form>
|
||||
<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}
|
||||
/>
|
||||
{ consultConnectSection }
|
||||
</React.Fragment>;
|
||||
} else {
|
||||
dialogContent = <React.Fragment>
|
||||
{ usersSection }
|
||||
{ consultConnectSection }
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className={classNames("mx_InviteDialog", {
|
||||
className={classNames({
|
||||
mx_InviteDialog_transfer: this.props.kind === KIND_CALL_TRANSFER,
|
||||
mx_InviteDialog_other: this.props.kind !== KIND_CALL_TRANSFER,
|
||||
mx_InviteDialog_hasFooter: !!footer,
|
||||
})}
|
||||
hasCancel={true}
|
||||
|
@ -1445,30 +1616,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
title={title}
|
||||
>
|
||||
<div className='mx_InviteDialog_content'>
|
||||
<p className='mx_InviteDialog_helpText'>{helpText}</p>
|
||||
<div className='mx_InviteDialog_addressBar'>
|
||||
{this.renderEditor()}
|
||||
<div className='mx_InviteDialog_buttonAndSpinner'>
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
onClick={goButtonFn}
|
||||
className='mx_InviteDialog_goButton'
|
||||
disabled={this.state.busy || !hasSelection}
|
||||
>
|
||||
{buttonText}
|
||||
</AccessibleButton>
|
||||
{spinner}
|
||||
</div>
|
||||
</div>
|
||||
{keySharingWarning}
|
||||
{this.renderIdentityServerWarning()}
|
||||
<div className='error'>{this.state.errorText}</div>
|
||||
<div className='mx_InviteDialog_userSections'>
|
||||
{this.renderSection('recents')}
|
||||
{this.renderSection('suggestions')}
|
||||
{extraSection}
|
||||
</div>
|
||||
{footer}
|
||||
{ dialogContent }
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
|
|
|
@ -20,10 +20,10 @@ import { _t } from '../../../languageHandler';
|
|||
import SdkConfig from '../../../SdkConfig';
|
||||
|
||||
export default function KeySignatureUploadFailedDialog({
|
||||
failures,
|
||||
source,
|
||||
continuation,
|
||||
onFinished,
|
||||
failures,
|
||||
source,
|
||||
continuation,
|
||||
onFinished,
|
||||
}) {
|
||||
const RETRIES = 2;
|
||||
const BaseDialog = sdk.getComponent('dialogs.BaseDialog');
|
||||
|
@ -69,10 +69,10 @@ export default function KeySignatureUploadFailedDialog({
|
|||
const brand = SdkConfig.get().brand;
|
||||
|
||||
body = (<div>
|
||||
<p>{_t("%(brand)s encountered an error during upload of:", { brand })}</p>
|
||||
<p>{reason}</p>
|
||||
{retrying && <Spinner />}
|
||||
<pre>{JSON.stringify(failures, null, 2)}</pre>
|
||||
<p>{ _t("%(brand)s encountered an error during upload of:", { brand }) }</p>
|
||||
<p>{ reason }</p>
|
||||
{ retrying && <Spinner /> }
|
||||
<pre>{ JSON.stringify(failures, null, 2) }</pre>
|
||||
<DialogButtons
|
||||
primaryButton='Retry'
|
||||
hasCancel={true}
|
||||
|
@ -83,11 +83,11 @@ export default function KeySignatureUploadFailedDialog({
|
|||
</div>);
|
||||
} else {
|
||||
body = (<div>
|
||||
{success ?
|
||||
<span>{_t("Upload completed")}</span> :
|
||||
{ success ?
|
||||
<span>{ _t("Upload completed") }</span> :
|
||||
cancelled ?
|
||||
<span>{_t("Cancelled signature upload")}</span> :
|
||||
<span>{_t("Unable to upload")}</span>}
|
||||
<span>{ _t("Cancelled signature upload") }</span> :
|
||||
<span>{ _t("Unable to upload") }</span> }
|
||||
<DialogButtons
|
||||
primaryButton={_t("OK")}
|
||||
hasCancel={false}
|
||||
|
@ -104,7 +104,7 @@ export default function KeySignatureUploadFailedDialog({
|
|||
fixedWidth={false}
|
||||
onFinished={() => {}}
|
||||
>
|
||||
{body}
|
||||
{ body }
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ export default (props) => {
|
|||
return (<QuestionDialog
|
||||
hasCancelButton={false}
|
||||
title={_t("Incompatible local cache")}
|
||||
description={<div><p>{description1}</p><p>{description2}</p></div>}
|
||||
description={<div><p>{ description1 }</p><p>{ description2 }</p></div>}
|
||||
button={_t("Clear cache and resync")}
|
||||
onFinished={props.onFinished}
|
||||
/>);
|
||||
|
|
|
@ -33,7 +33,7 @@ export default (props) => {
|
|||
return (<QuestionDialog
|
||||
hasCancelButton={false}
|
||||
title={_t("Updating %(brand)s", { brand })}
|
||||
description={<div>{description}</div>}
|
||||
description={<div>{ description }</div>}
|
||||
button={_t("OK")}
|
||||
onFinished={props.onFinished}
|
||||
/>);
|
||||
|
|
197
src/components/views/dialogs/LeaveSpaceDialog.tsx
Normal file
197
src/components/views/dialogs/LeaveSpaceDialog.tsx
Normal file
|
@ -0,0 +1,197 @@
|
|||
/*
|
||||
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, { useEffect, useMemo, useState } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
import BaseDialog from "../dialogs/BaseDialog";
|
||||
import SpaceStore from "../../../stores/SpaceStore";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import { Entry } from "./AddExistingToSpaceDialog";
|
||||
import SearchBox from "../../structures/SearchBox";
|
||||
import QueryMatcher from "../../../autocomplete/QueryMatcher";
|
||||
import StyledRadioGroup from "../elements/StyledRadioGroup";
|
||||
|
||||
enum RoomsToLeave {
|
||||
All = "All",
|
||||
Specific = "Specific",
|
||||
None = "None",
|
||||
}
|
||||
|
||||
const SpaceChildPicker = ({ filterPlaceholder, rooms, selected, onChange }) => {
|
||||
const [query, setQuery] = useState("");
|
||||
const lcQuery = query.toLowerCase().trim();
|
||||
|
||||
const filteredRooms = useMemo(() => {
|
||||
if (!lcQuery) {
|
||||
return rooms;
|
||||
}
|
||||
|
||||
const matcher = new QueryMatcher<Room>(rooms, {
|
||||
keys: ["name"],
|
||||
funcs: [r => [r.getCanonicalAlias(), ...r.getAltAliases()].filter(Boolean)],
|
||||
shouldMatchWordsOnly: false,
|
||||
});
|
||||
|
||||
return matcher.match(lcQuery);
|
||||
}, [rooms, lcQuery]);
|
||||
|
||||
return <div className="mx_LeaveSpaceDialog_section">
|
||||
<SearchBox
|
||||
className="mx_textinput_icon mx_textinput_search"
|
||||
placeholder={filterPlaceholder}
|
||||
onSearch={setQuery}
|
||||
autoComplete={true}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<AutoHideScrollbar className="mx_LeaveSpaceDialog_content">
|
||||
{ filteredRooms.map(room => {
|
||||
return <Entry
|
||||
key={room.roomId}
|
||||
room={room}
|
||||
checked={selected.has(room)}
|
||||
onChange={(checked) => {
|
||||
onChange(checked, room);
|
||||
}}
|
||||
/>;
|
||||
}) }
|
||||
{ filteredRooms.length < 1 ? <span className="mx_LeaveSpaceDialog_noResults">
|
||||
{ _t("No results") }
|
||||
</span> : undefined }
|
||||
</AutoHideScrollbar>
|
||||
</div>;
|
||||
};
|
||||
|
||||
const LeaveRoomsPicker = ({ space, spaceChildren, roomsToLeave, setRoomsToLeave }) => {
|
||||
const selected = useMemo(() => new Set(roomsToLeave), [roomsToLeave]);
|
||||
const [state, setState] = useState<string>(RoomsToLeave.All);
|
||||
|
||||
useEffect(() => {
|
||||
if (state === RoomsToLeave.All) {
|
||||
setRoomsToLeave(spaceChildren);
|
||||
} else {
|
||||
setRoomsToLeave([]);
|
||||
}
|
||||
}, [setRoomsToLeave, state, spaceChildren]);
|
||||
|
||||
return <div className="mx_LeaveSpaceDialog_section">
|
||||
<StyledRadioGroup
|
||||
name="roomsToLeave"
|
||||
value={state}
|
||||
onChange={setState}
|
||||
definitions={[
|
||||
{
|
||||
value: RoomsToLeave.All,
|
||||
label: _t("Leave all rooms and spaces"),
|
||||
}, {
|
||||
value: RoomsToLeave.None,
|
||||
label: _t("Don't leave any"),
|
||||
}, {
|
||||
value: RoomsToLeave.Specific,
|
||||
label: _t("Leave specific rooms and spaces"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{ state === RoomsToLeave.Specific && (
|
||||
<SpaceChildPicker
|
||||
filterPlaceholder={_t("Search %(spaceName)s", { spaceName: space.name })}
|
||||
rooms={spaceChildren}
|
||||
selected={selected}
|
||||
onChange={(selected: boolean, room: Room) => {
|
||||
if (selected) {
|
||||
setRoomsToLeave([room, ...roomsToLeave]);
|
||||
} else {
|
||||
setRoomsToLeave(roomsToLeave.filter(r => r !== room));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) }
|
||||
</div>;
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
space: Room;
|
||||
onFinished(leave: boolean, rooms?: Room[]): void;
|
||||
}
|
||||
|
||||
const isOnlyAdmin = (room: Room): boolean => {
|
||||
return !room.getJoinedMembers().some(member => {
|
||||
return member.userId !== room.client.credentials.userId && member.powerLevelNorm === 100;
|
||||
});
|
||||
};
|
||||
|
||||
const LeaveSpaceDialog: React.FC<IProps> = ({ space, onFinished }) => {
|
||||
const spaceChildren = useMemo(() => SpaceStore.instance.getChildren(space.roomId), [space.roomId]);
|
||||
const [roomsToLeave, setRoomsToLeave] = useState<Room[]>([]);
|
||||
|
||||
let rejoinWarning;
|
||||
if (space.getJoinRule() !== JoinRule.Public) {
|
||||
rejoinWarning = _t("You won't be able to rejoin unless you are re-invited.");
|
||||
}
|
||||
|
||||
let onlyAdminWarning;
|
||||
if (isOnlyAdmin(space)) {
|
||||
onlyAdminWarning = _t("You're the only admin of this space. " +
|
||||
"Leaving it will mean no one has control over it.");
|
||||
} else {
|
||||
const numChildrenOnlyAdminIn = roomsToLeave.filter(isOnlyAdmin).length;
|
||||
if (numChildrenOnlyAdminIn > 0) {
|
||||
onlyAdminWarning = _t("You're the only admin of some of the rooms or spaces you wish to leave. " +
|
||||
"Leaving them will leave them without any admins.");
|
||||
}
|
||||
}
|
||||
|
||||
return <BaseDialog
|
||||
title={_t("Leave %(spaceName)s", { spaceName: space.name })}
|
||||
className="mx_LeaveSpaceDialog"
|
||||
contentId="mx_LeaveSpaceDialog"
|
||||
onFinished={() => onFinished(false)}
|
||||
fixedWidth={false}
|
||||
>
|
||||
<div className="mx_Dialog_content" id="mx_LeaveSpaceDialog">
|
||||
<p>
|
||||
{ _t("Are you sure you want to leave <spaceName/>?", {}, {
|
||||
spaceName: () => <b>{ space.name }</b>,
|
||||
}) }
|
||||
|
||||
{ rejoinWarning }
|
||||
</p>
|
||||
|
||||
{ spaceChildren.length > 0 && <LeaveRoomsPicker
|
||||
space={space}
|
||||
spaceChildren={spaceChildren}
|
||||
roomsToLeave={roomsToLeave}
|
||||
setRoomsToLeave={setRoomsToLeave}
|
||||
/> }
|
||||
|
||||
{ onlyAdminWarning && <div className="mx_LeaveSpaceDialog_section_warning">
|
||||
{ onlyAdminWarning }
|
||||
</div> }
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Leave space")}
|
||||
onPrimaryButtonClick={() => onFinished(true, roomsToLeave)}
|
||||
hasCancel={true}
|
||||
onCancel={onFinished}
|
||||
/>
|
||||
</BaseDialog>;
|
||||
};
|
||||
|
||||
export default LeaveSpaceDialog;
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
|
||||
import Modal from '../../../Modal';
|
||||
import * as sdk from '../../../index';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
|
@ -24,19 +25,25 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
|||
import RestoreKeyBackupDialog from './security/RestoreKeyBackupDialog';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
shouldLoadBackupStatus: boolean;
|
||||
loading: boolean;
|
||||
backupInfo: IKeyBackupInfo;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.LogoutDialog")
|
||||
export default class LogoutDialog extends React.Component {
|
||||
defaultProps = {
|
||||
export default class LogoutDialog extends React.Component<IProps, IState> {
|
||||
static defaultProps = {
|
||||
onFinished: function() {},
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._onSettingsLinkClick = this._onSettingsLinkClick.bind(this);
|
||||
this._onExportE2eKeysClicked = this._onExportE2eKeysClicked.bind(this);
|
||||
this._onFinished = this._onFinished.bind(this);
|
||||
this._onSetRecoveryMethodClick = this._onSetRecoveryMethodClick.bind(this);
|
||||
this._onLogoutConfirm = this._onLogoutConfirm.bind(this);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
const shouldLoadBackupStatus = cli.isCryptoEnabled() && !cli.getKeyBackupEnabled();
|
||||
|
@ -49,11 +56,11 @@ export default class LogoutDialog extends React.Component {
|
|||
};
|
||||
|
||||
if (shouldLoadBackupStatus) {
|
||||
this._loadBackupStatus();
|
||||
this.loadBackupStatus();
|
||||
}
|
||||
}
|
||||
|
||||
async _loadBackupStatus() {
|
||||
private async loadBackupStatus() {
|
||||
try {
|
||||
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||
this.setState({
|
||||
|
@ -69,29 +76,29 @@ export default class LogoutDialog extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_onSettingsLinkClick() {
|
||||
private onSettingsLinkClick = (): void => {
|
||||
// close dialog
|
||||
this.props.onFinished();
|
||||
}
|
||||
this.props.onFinished(true);
|
||||
};
|
||||
|
||||
_onExportE2eKeysClicked() {
|
||||
private onExportE2eKeysClicked = (): void => {
|
||||
Modal.createTrackedDialogAsync('Export E2E Keys', '',
|
||||
import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
|
||||
{
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
_onFinished(confirmed) {
|
||||
private onFinished = (confirmed: boolean): void => {
|
||||
if (confirmed) {
|
||||
dis.dispatch({ action: 'logout' });
|
||||
}
|
||||
// close dialog
|
||||
this.props.onFinished();
|
||||
}
|
||||
this.props.onFinished(confirmed);
|
||||
};
|
||||
|
||||
_onSetRecoveryMethodClick() {
|
||||
private onSetRecoveryMethodClick = (): void => {
|
||||
if (this.state.backupInfo) {
|
||||
// A key backup exists for this account, but the creating device is not
|
||||
// verified, so restore the backup which will give us the keys from it and
|
||||
|
@ -108,26 +115,26 @@ export default class LogoutDialog extends React.Component {
|
|||
}
|
||||
|
||||
// close dialog
|
||||
this.props.onFinished();
|
||||
}
|
||||
this.props.onFinished(true);
|
||||
};
|
||||
|
||||
_onLogoutConfirm() {
|
||||
private onLogoutConfirm = (): void => {
|
||||
dis.dispatch({ action: 'logout' });
|
||||
|
||||
// close dialog
|
||||
this.props.onFinished();
|
||||
}
|
||||
this.props.onFinished(true);
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.shouldLoadBackupStatus) {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
const description = <div>
|
||||
<p>{_t(
|
||||
<p>{ _t(
|
||||
"Encrypted messages are secured with end-to-end encryption. " +
|
||||
"Only you and the recipient(s) have the keys to read these messages.",
|
||||
)}</p>
|
||||
<p>{_t("Back up your keys before signing out to avoid losing them.")}</p>
|
||||
) }</p>
|
||||
<p>{ _t("Back up your keys before signing out to avoid losing them.") }</p>
|
||||
</div>;
|
||||
|
||||
let dialogContent;
|
||||
|
@ -152,17 +159,17 @@ export default class LogoutDialog extends React.Component {
|
|||
</div>
|
||||
<DialogButtons primaryButton={setupButtonCaption}
|
||||
hasCancel={false}
|
||||
onPrimaryButtonClick={this._onSetRecoveryMethodClick}
|
||||
onPrimaryButtonClick={this.onSetRecoveryMethodClick}
|
||||
focus={true}
|
||||
>
|
||||
<button onClick={this._onLogoutConfirm}>
|
||||
{_t("I don't want my encrypted messages")}
|
||||
<button onClick={this.onLogoutConfirm}>
|
||||
{ _t("I don't want my encrypted messages") }
|
||||
</button>
|
||||
</DialogButtons>
|
||||
<details>
|
||||
<summary>{_t("Advanced")}</summary>
|
||||
<p><button onClick={this._onExportE2eKeysClicked}>
|
||||
{_t("Manually export keys")}
|
||||
<summary>{ _t("Advanced") }</summary>
|
||||
<p><button onClick={this.onExportE2eKeysClicked}>
|
||||
{ _t("Manually export keys") }
|
||||
</button></p>
|
||||
</details>
|
||||
</div>;
|
||||
|
@ -174,9 +181,9 @@ export default class LogoutDialog extends React.Component {
|
|||
title={_t("You'll lose access to your encrypted messages")}
|
||||
contentId='mx_Dialog_content'
|
||||
hasCancel={true}
|
||||
onFinished={this._onFinished}
|
||||
onFinished={this.onFinished}
|
||||
>
|
||||
{dialogContent}
|
||||
{ dialogContent }
|
||||
</BaseDialog>);
|
||||
} else {
|
||||
const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
|
||||
|
@ -187,7 +194,7 @@ export default class LogoutDialog extends React.Component {
|
|||
"Are you sure you want to sign out?",
|
||||
)}
|
||||
button={_t("Sign out")}
|
||||
onFinished={this._onFinished}
|
||||
onFinished={this.onFinished}
|
||||
/>);
|
||||
}
|
||||
}
|
192
src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx
Normal file
192
src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx
Normal 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;
|
||||
|
|
@ -134,18 +134,18 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
|
|||
const { error } = this.state;
|
||||
if (error.errcode === "M_UNRECOGNIZED") {
|
||||
content = (<p className="mx_MessageEditHistoryDialog_error">
|
||||
{_t("Your homeserver doesn't seem to support this feature.")}
|
||||
{ _t("Your homeserver doesn't seem to support this feature.") }
|
||||
</p>);
|
||||
} else if (error.errcode) {
|
||||
// some kind of error from the homeserver
|
||||
content = (<p className="mx_MessageEditHistoryDialog_error">
|
||||
{_t("Something went wrong!")}
|
||||
{ _t("Something went wrong!") }
|
||||
</p>);
|
||||
} else {
|
||||
content = (<p className="mx_MessageEditHistoryDialog_error">
|
||||
{_t("Cannot reach homeserver")}
|
||||
{ _t("Cannot reach homeserver") }
|
||||
<br />
|
||||
{_t("Ensure you have a stable internet connection, or get in touch with the server admin")}
|
||||
{ _t("Ensure you have a stable internet connection, or get in touch with the server admin") }
|
||||
</p>);
|
||||
}
|
||||
} else if (this.state.isLoading) {
|
||||
|
@ -155,11 +155,11 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
|
|||
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
||||
content = (<ScrollPanel
|
||||
className="mx_MessageEditHistoryDialog_scrollPanel"
|
||||
onFillRequest={ this.loadMoreEdits }
|
||||
onFillRequest={this.loadMoreEdits}
|
||||
stickyBottom={false}
|
||||
startAtBottom={false}
|
||||
>
|
||||
<ul className="mx_MessageEditHistoryDialog_edits">{this._renderEdits()}</ul>
|
||||
<ul className="mx_MessageEditHistoryDialog_edits">{ this._renderEdits() }</ul>
|
||||
</ScrollPanel>);
|
||||
}
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
@ -170,7 +170,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
|
|||
onFinished={this.props.onFinished}
|
||||
title={_t("Message edits")}
|
||||
>
|
||||
{content}
|
||||
{ content }
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -191,9 +191,9 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
|
|||
width="16"
|
||||
alt=""
|
||||
/>
|
||||
{_t("Data on this screen is shared with %(widgetDomain)s", {
|
||||
{ _t("Data on this screen is shared with %(widgetDomain)s", {
|
||||
widgetDomain: parsed.hostname,
|
||||
})}
|
||||
}) }
|
||||
</div>
|
||||
<div>
|
||||
<iframe
|
||||
|
|
|
@ -67,10 +67,10 @@ const RegistrationEmailPromptDialog: React.FC<IProps> = ({ onFinished }) => {
|
|||
fixedWidth={false}
|
||||
>
|
||||
<div className="mx_Dialog_content" id="mx_RegistrationEmailPromptDialog">
|
||||
<p>{_t("Just a heads up, if you don't add an email and forget your password, you could " +
|
||||
<p>{ _t("Just a heads up, if you don't add an email and forget your password, you could " +
|
||||
"<b>permanently lose access to your account</b>.", {}, {
|
||||
b: sub => <b>{sub}</b>,
|
||||
})}</p>
|
||||
b: sub => <b>{ sub }</b>,
|
||||
}) }</p>
|
||||
<form onSubmit={onSubmit}>
|
||||
<Field
|
||||
ref={fieldRef}
|
||||
|
|
|
@ -40,7 +40,7 @@ interface IState {
|
|||
busy: boolean;
|
||||
err?: string;
|
||||
// If we know it, the nature of the abuse, as specified by MSC3215.
|
||||
nature?: EXTENDED_NATURE;
|
||||
nature?: ExtendedNature;
|
||||
}
|
||||
|
||||
const MODERATED_BY_STATE_EVENT_TYPE = [
|
||||
|
@ -55,22 +55,22 @@ const MODERATED_BY_STATE_EVENT_TYPE = [
|
|||
const ABUSE_EVENT_TYPE = "org.matrix.msc3215.abuse.report";
|
||||
|
||||
// Standard abuse natures.
|
||||
enum NATURE {
|
||||
DISAGREEMENT = "org.matrix.msc3215.abuse.nature.disagreement",
|
||||
TOXIC = "org.matrix.msc3215.abuse.nature.toxic",
|
||||
ILLEGAL = "org.matrix.msc3215.abuse.nature.illegal",
|
||||
SPAM = "org.matrix.msc3215.abuse.nature.spam",
|
||||
OTHER = "org.matrix.msc3215.abuse.nature.other",
|
||||
enum Nature {
|
||||
Disagreement = "org.matrix.msc3215.abuse.nature.disagreement",
|
||||
Toxic = "org.matrix.msc3215.abuse.nature.toxic",
|
||||
Illegal = "org.matrix.msc3215.abuse.nature.illegal",
|
||||
Spam = "org.matrix.msc3215.abuse.nature.spam",
|
||||
Other = "org.matrix.msc3215.abuse.nature.other",
|
||||
}
|
||||
|
||||
enum NON_STANDARD_NATURE {
|
||||
enum NonStandardValue {
|
||||
// Non-standard abuse nature.
|
||||
// It should never leave the client - we use it to fallback to
|
||||
// server-wide abuse reporting.
|
||||
ADMIN = "non-standard.abuse.nature.admin"
|
||||
Admin = "non-standard.abuse.nature.admin"
|
||||
}
|
||||
|
||||
type EXTENDED_NATURE = NATURE | NON_STANDARD_NATURE;
|
||||
type ExtendedNature = Nature | NonStandardValue;
|
||||
|
||||
type Moderation = {
|
||||
// The id of the moderation room.
|
||||
|
@ -170,7 +170,7 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
|
|||
|
||||
// The user has clicked on a nature.
|
||||
private onNatureChosen = (e: React.FormEvent<HTMLInputElement>): void => {
|
||||
this.setState({ nature: e.currentTarget.value as EXTENDED_NATURE });
|
||||
this.setState({ nature: e.currentTarget.value as ExtendedNature });
|
||||
};
|
||||
|
||||
// The user has clicked "cancel".
|
||||
|
@ -187,7 +187,7 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
|
|||
// We need a nature.
|
||||
// If the nature is `NATURE.OTHER` or `NON_STANDARD_NATURE.ADMIN`, we also need a `reason`.
|
||||
if (!this.state.nature ||
|
||||
((this.state.nature == NATURE.OTHER || this.state.nature == NON_STANDARD_NATURE.ADMIN)
|
||||
((this.state.nature == Nature.Other || this.state.nature == NonStandardValue.Admin)
|
||||
&& !reason)
|
||||
) {
|
||||
this.setState({
|
||||
|
@ -214,8 +214,8 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
|
|||
try {
|
||||
const client = MatrixClientPeg.get();
|
||||
const ev = this.props.mxEvent;
|
||||
if (this.moderation && this.state.nature != NON_STANDARD_NATURE.ADMIN) {
|
||||
const nature: NATURE = this.state.nature;
|
||||
if (this.moderation && this.state.nature != NonStandardValue.Admin) {
|
||||
const nature: Nature = this.state.nature;
|
||||
|
||||
// Report to moderators through to the dedicated bot,
|
||||
// as configured in the room's state events.
|
||||
|
@ -245,7 +245,7 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
|
|||
let error = null;
|
||||
if (this.state.err) {
|
||||
error = <div className="error">
|
||||
{this.state.err}
|
||||
{ this.state.err }
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
@ -274,27 +274,27 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
|
|||
const homeServerName = SdkConfig.get()["validated_server_config"].hsName;
|
||||
let subtitle;
|
||||
switch (this.state.nature) {
|
||||
case NATURE.DISAGREEMENT:
|
||||
case Nature.Disagreement:
|
||||
subtitle = _t("What this user is writing is wrong.\n" +
|
||||
"This will be reported to the room moderators.");
|
||||
break;
|
||||
case NATURE.TOXIC:
|
||||
case Nature.Toxic:
|
||||
subtitle = _t("This user is displaying toxic behaviour, " +
|
||||
"for instance by insulting other users or sharing " +
|
||||
" adult-only content in a family-friendly room " +
|
||||
" or otherwise violating the rules of this room.\n" +
|
||||
"This will be reported to the room moderators.");
|
||||
break;
|
||||
case NATURE.ILLEGAL:
|
||||
case Nature.Illegal:
|
||||
subtitle = _t("This user is displaying illegal behaviour, " +
|
||||
"for instance by doxing people or threatening violence.\n" +
|
||||
"This will be reported to the room moderators who may escalate this to legal authorities.");
|
||||
break;
|
||||
case NATURE.SPAM:
|
||||
case Nature.Spam:
|
||||
subtitle = _t("This user is spamming the room with ads, links to ads or to propaganda.\n" +
|
||||
"This will be reported to the room moderators.");
|
||||
break;
|
||||
case NON_STANDARD_NATURE.ADMIN:
|
||||
case NonStandardValue.Admin:
|
||||
if (client.isRoomEncrypted(this.props.mxEvent.getRoomId())) {
|
||||
subtitle = _t("This room is dedicated to illegal or toxic content " +
|
||||
"or the moderators fail to moderate illegal or toxic content.\n" +
|
||||
|
@ -308,7 +308,7 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
|
|||
{ homeserver: homeServerName });
|
||||
}
|
||||
break;
|
||||
case NATURE.OTHER:
|
||||
case Nature.Other:
|
||||
subtitle = _t("Any other reason. Please describe the problem.\n" +
|
||||
"This will be reported to the room moderators.");
|
||||
break;
|
||||
|
@ -326,55 +326,55 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
|
|||
>
|
||||
<div>
|
||||
<StyledRadioButton
|
||||
name = "nature"
|
||||
value = { NATURE.DISAGREEMENT }
|
||||
checked = { this.state.nature == NATURE.DISAGREEMENT }
|
||||
onChange = { this.onNatureChosen }
|
||||
name="nature"
|
||||
value={Nature.Disagreement}
|
||||
checked={this.state.nature == Nature.Disagreement}
|
||||
onChange={this.onNatureChosen}
|
||||
>
|
||||
{_t('Disagree')}
|
||||
{ _t('Disagree') }
|
||||
</StyledRadioButton>
|
||||
<StyledRadioButton
|
||||
name = "nature"
|
||||
value = { NATURE.TOXIC }
|
||||
checked = { this.state.nature == NATURE.TOXIC }
|
||||
onChange = { this.onNatureChosen }
|
||||
name="nature"
|
||||
value={Nature.Toxic}
|
||||
checked={this.state.nature == Nature.Toxic}
|
||||
onChange={this.onNatureChosen}
|
||||
>
|
||||
{_t('Toxic Behaviour')}
|
||||
{ _t('Toxic Behaviour') }
|
||||
</StyledRadioButton>
|
||||
<StyledRadioButton
|
||||
name = "nature"
|
||||
value = { NATURE.ILLEGAL }
|
||||
checked = { this.state.nature == NATURE.ILLEGAL }
|
||||
onChange = { this.onNatureChosen }
|
||||
name="nature"
|
||||
value={Nature.Illegal}
|
||||
checked={this.state.nature == Nature.Illegal}
|
||||
onChange={this.onNatureChosen}
|
||||
>
|
||||
{_t('Illegal Content')}
|
||||
{ _t('Illegal Content') }
|
||||
</StyledRadioButton>
|
||||
<StyledRadioButton
|
||||
name = "nature"
|
||||
value = { NATURE.SPAM }
|
||||
checked = { this.state.nature == NATURE.SPAM }
|
||||
onChange = { this.onNatureChosen }
|
||||
name="nature"
|
||||
value={Nature.Spam}
|
||||
checked={this.state.nature == Nature.Spam}
|
||||
onChange={this.onNatureChosen}
|
||||
>
|
||||
{_t('Spam or propaganda')}
|
||||
{ _t('Spam or propaganda') }
|
||||
</StyledRadioButton>
|
||||
<StyledRadioButton
|
||||
name = "nature"
|
||||
value = { NON_STANDARD_NATURE.ADMIN }
|
||||
checked = { this.state.nature == NON_STANDARD_NATURE.ADMIN }
|
||||
onChange = { this.onNatureChosen }
|
||||
name="nature"
|
||||
value={NonStandardValue.Admin}
|
||||
checked={this.state.nature == NonStandardValue.Admin}
|
||||
onChange={this.onNatureChosen}
|
||||
>
|
||||
{_t('Report the entire room')}
|
||||
{ _t('Report the entire room') }
|
||||
</StyledRadioButton>
|
||||
<StyledRadioButton
|
||||
name = "nature"
|
||||
value = { NATURE.OTHER }
|
||||
checked = { this.state.nature == NATURE.OTHER }
|
||||
onChange = { this.onNatureChosen }
|
||||
name="nature"
|
||||
value={Nature.Other}
|
||||
checked={this.state.nature == Nature.Other}
|
||||
onChange={this.onNatureChosen}
|
||||
>
|
||||
{_t('Other')}
|
||||
{ _t('Other') }
|
||||
</StyledRadioButton>
|
||||
<p>
|
||||
{subtitle}
|
||||
{ subtitle }
|
||||
</p>
|
||||
<Field
|
||||
className="mx_ReportEventDialog_reason"
|
||||
|
@ -385,8 +385,8 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
|
|||
value={this.state.reason}
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
{progress}
|
||||
{error}
|
||||
{ progress }
|
||||
{ error }
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Send report")}
|
||||
|
@ -416,7 +416,7 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
|
|||
"or images.")
|
||||
}
|
||||
</p>
|
||||
{adminMessage}
|
||||
{ adminMessage }
|
||||
<Field
|
||||
className="mx_ReportEventDialog_reason"
|
||||
element="textarea"
|
||||
|
@ -426,8 +426,8 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
|
|||
value={this.state.reason}
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
{progress}
|
||||
{error}
|
||||
{ progress }
|
||||
{ error }
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Send report")}
|
||||
|
|
|
@ -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,48 +68,43 @@ 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'
|
||||
hasCancel={true}
|
||||
>
|
||||
<p>
|
||||
{_t(
|
||||
{ _t(
|
||||
"Upgrading this room requires closing down the current " +
|
||||
"instance of the room and creating a new room in its place. " +
|
||||
"To give room members the best possible experience, we will:",
|
||||
)}
|
||||
) }
|
||||
</p>
|
||||
<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("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>
|
||||
</ol>
|
||||
{buttons}
|
||||
{ buttons }
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
|
@ -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,86 +14,95 @@ 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>
|
||||
{_t(
|
||||
{ _t(
|
||||
"This usually only affects how the room is processed on the server. If you're " +
|
||||
"having problems with your %(brand)s, please report a bug.", { brand },
|
||||
)}
|
||||
) }
|
||||
</p>
|
||||
);
|
||||
if (SdkConfig.get().bug_report_endpoint_url) {
|
||||
bugReports = (
|
||||
<p>
|
||||
{_t(
|
||||
{ _t(
|
||||
"This usually only affects how the room is processed on the server. If you're " +
|
||||
"having problems with your %(brand)s, please <a>report a bug</a>.",
|
||||
{
|
||||
|
@ -101,10 +110,10 @@ 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>;
|
||||
},
|
||||
},
|
||||
)}
|
||||
) }
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
@ -119,29 +128,37 @@ 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>
|
||||
{bugReports}
|
||||
<p>
|
||||
{_t(
|
||||
{ _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>,
|
||||
newVersion: () => <code>{this.props.targetVersion}</code>,
|
||||
oldVersion: () => <code>{ this.currentVersion }</code>,
|
||||
newVersion: () => <code>{ this.props.targetVersion }</code>,
|
||||
},
|
||||
)}
|
||||
) }
|
||||
</p>
|
||||
{inviteToggle}
|
||||
{ inviteToggle }
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Upgrade")}
|
||||
onPrimaryButtonClick={this._onContinue}
|
||||
onPrimaryButtonClick={this.onContinue}
|
||||
cancelButton={_t("Cancel")}
|
||||
onCancel={this._onCancel}
|
||||
onCancel={this.onCancel}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
|
@ -54,7 +54,7 @@ export default class ServerOfflineDialog extends React.PureComponent<IProps> {
|
|||
const header = (
|
||||
<div className="mx_ServerOfflineDialog_content_context_timeline_header">
|
||||
<RoomAvatar width={24} height={24} room={c.room} />
|
||||
<span>{c.room.name}</span>
|
||||
<span>{ c.room.name }</span>
|
||||
</div>
|
||||
);
|
||||
const entries = c.transactions
|
||||
|
@ -63,26 +63,26 @@ export default class ServerOfflineDialog extends React.PureComponent<IProps> {
|
|||
let button = <Spinner w={19} h={19} />;
|
||||
if (t.status === TransactionStatus.Error) {
|
||||
button = (
|
||||
<AccessibleButton kind="link" onClick={() => t.run()}>{_t("Resend")}</AccessibleButton>
|
||||
<AccessibleButton kind="link" onClick={() => t.run()}>{ _t("Resend") }</AccessibleButton>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="mx_ServerOfflineDialog_content_context_txn" key={`txn-${j}`}>
|
||||
<span className="mx_ServerOfflineDialog_content_context_txn_desc">
|
||||
{t.auditName}
|
||||
{ t.auditName }
|
||||
</span>
|
||||
{button}
|
||||
{ button }
|
||||
</div>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div className="mx_ServerOfflineDialog_content_context" key={`context-${i}`}>
|
||||
<div className="mx_ServerOfflineDialog_content_context_timestamp">
|
||||
{formatTime(c.firstFailedTime, SettingsStore.getValue("showTwelveHourTimestamps"))}
|
||||
{ formatTime(c.firstFailedTime, SettingsStore.getValue("showTwelveHourTimestamps")) }
|
||||
</div>
|
||||
<div className="mx_ServerOfflineDialog_content_context_timeline">
|
||||
{header}
|
||||
{entries}
|
||||
{ header }
|
||||
{ entries }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -92,7 +92,7 @@ export default class ServerOfflineDialog extends React.PureComponent<IProps> {
|
|||
public render() {
|
||||
let timeline = this.renderTimeline().filter(c => !!c); // remove nulls for next check
|
||||
if (timeline.length === 0) {
|
||||
timeline = [<div key={1}>{_t("You're all caught up.")}</div>];
|
||||
timeline = [<div key={1}>{ _t("You're all caught up.") }</div>];
|
||||
}
|
||||
|
||||
const serverName = MatrixClientPeg.getHomeserverName();
|
||||
|
@ -103,23 +103,23 @@ export default class ServerOfflineDialog extends React.PureComponent<IProps> {
|
|||
hasCancel={true}
|
||||
>
|
||||
<div className="mx_ServerOfflineDialog_content">
|
||||
<p>{_t(
|
||||
<p>{ _t(
|
||||
"Your server isn't responding to some of your requests. " +
|
||||
"Below are some of the most likely reasons.",
|
||||
)}</p>
|
||||
) }</p>
|
||||
<ul>
|
||||
<li>{_t("The server (%(serverName)s) took too long to respond.", { serverName })}</li>
|
||||
<li>{_t("Your firewall or anti-virus is blocking the request.")}</li>
|
||||
<li>{_t("A browser extension is preventing the request.")}</li>
|
||||
<li>{_t("The server is offline.")}</li>
|
||||
<li>{_t("The server has denied your request.")}</li>
|
||||
<li>{_t("Your area is experiencing difficulties connecting to the internet.")}</li>
|
||||
<li>{_t("A connection error occurred while trying to contact the server.")}</li>
|
||||
<li>{_t("The server is not configured to indicate what the problem is (CORS).")}</li>
|
||||
<li>{ _t("The server (%(serverName)s) took too long to respond.", { serverName }) }</li>
|
||||
<li>{ _t("Your firewall or anti-virus is blocking the request.") }</li>
|
||||
<li>{ _t("A browser extension is preventing the request.") }</li>
|
||||
<li>{ _t("The server is offline.") }</li>
|
||||
<li>{ _t("The server has denied your request.") }</li>
|
||||
<li>{ _t("Your area is experiencing difficulties connecting to the internet.") }</li>
|
||||
<li>{ _t("A connection error occurred while trying to contact the server.") }</li>
|
||||
<li>{ _t("The server is not configured to indicate what the problem is (CORS).") }</li>
|
||||
</ul>
|
||||
<hr />
|
||||
<h2>{_t("Recent changes that have not yet been received")}</h2>
|
||||
{timeline}
|
||||
<h2>{ _t("Recent changes that have not yet been received") }</h2>
|
||||
{ timeline }
|
||||
</div>
|
||||
</BaseDialog>;
|
||||
}
|
||||
|
|
|
@ -172,7 +172,7 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
|
|||
if (this.defaultServer.hsNameIsDifferent) {
|
||||
defaultServerName = (
|
||||
<TextWithTooltip class="mx_Login_underlinedServerName" tooltip={this.defaultServer.hsUrl}>
|
||||
{this.defaultServer.hsName}
|
||||
{ this.defaultServer.hsName }
|
||||
</TextWithTooltip>
|
||||
);
|
||||
}
|
||||
|
@ -187,7 +187,7 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
|
|||
>
|
||||
<form className="mx_Dialog_content" id="mx_ServerPickerDialog" onSubmit={this.onSubmit}>
|
||||
<p>
|
||||
{_t("We call the places where you can host your account ‘homeservers’.")} {text}
|
||||
{ _t("We call the places where you can host your account ‘homeservers’.") } { text }
|
||||
</p>
|
||||
|
||||
<StyledRadioButton
|
||||
|
@ -196,7 +196,7 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
|
|||
checked={this.state.defaultChosen}
|
||||
onChange={this.onDefaultChosen}
|
||||
>
|
||||
{defaultServerName}
|
||||
{ defaultServerName }
|
||||
</StyledRadioButton>
|
||||
|
||||
<StyledRadioButton
|
||||
|
@ -205,13 +205,14 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
|
|||
className="mx_ServerPickerDialog_otherHomeserverRadio"
|
||||
checked={!this.state.defaultChosen}
|
||||
onChange={this.onOtherChosen}
|
||||
childrenInLabel={false}
|
||||
>
|
||||
<Field
|
||||
type="text"
|
||||
className="mx_ServerPickerDialog_otherHomeserver"
|
||||
label={_t("Other homeserver")}
|
||||
onChange={this.onHomeserverChange}
|
||||
onClick={this.onOtherChosen}
|
||||
onFocus={this.onOtherChosen}
|
||||
ref={this.fieldRef}
|
||||
onValidate={this.onHomeserverValidate}
|
||||
value={this.state.otherHomeserver}
|
||||
|
@ -221,16 +222,16 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
|
|||
/>
|
||||
</StyledRadioButton>
|
||||
<p>
|
||||
{_t("Use your preferred Matrix homeserver if you have one, or host your own.")}
|
||||
{ _t("Use your preferred Matrix homeserver if you have one, or host your own.") }
|
||||
</p>
|
||||
|
||||
<AccessibleButton className="mx_ServerPickerDialog_continue" kind="primary" onClick={this.onSubmit}>
|
||||
{_t("Continue")}
|
||||
{ _t("Continue") }
|
||||
</AccessibleButton>
|
||||
|
||||
<h4>{_t("Learn more")}</h4>
|
||||
<h4>{ _t("Learn more") }</h4>
|
||||
<a href="https://matrix.org/faq/#what-is-a-homeserver%3F" target="_blank" rel="noreferrer noopener">
|
||||
{_t("About homeservers")}
|
||||
{ _t("About homeservers") }
|
||||
</a>
|
||||
</form>
|
||||
</BaseDialog>;
|
||||
|
|
|
@ -33,12 +33,12 @@ export default class SeshatResetDialog extends React.PureComponent<IDialogProps>
|
|||
title={_t("Reset event store?")}>
|
||||
<div>
|
||||
<p>
|
||||
{_t("You most likely do not want to reset your event index store")}
|
||||
{ _t("You most likely do not want to reset your event index store") }
|
||||
<br />
|
||||
{_t("If you do, please note that none of your messages will be deleted, " +
|
||||
{ _t("If you do, please note that none of your messages will be deleted, " +
|
||||
"but the search experience might be degraded for a few moments " +
|
||||
"whilst the index is recreated",
|
||||
)}
|
||||
) }
|
||||
</p>
|
||||
</div>
|
||||
<DialogButtons
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -35,7 +35,7 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import GenericTextContextMenu from "../context_menus/GenericTextContextMenu.js";
|
||||
import GenericTextContextMenu from "../context_menus/GenericTextContextMenu";
|
||||
|
||||
const socials = [
|
||||
{
|
||||
|
|
|
@ -35,16 +35,16 @@ export default ({ onFinished }) => {
|
|||
const rows = [
|
||||
<tr key={"_category_" + category} className="mx_SlashCommandHelpDialog_headerRow">
|
||||
<td colSpan={3}>
|
||||
<h2>{_t(category)}</h2>
|
||||
<h2>{ _t(category) }</h2>
|
||||
</td>
|
||||
</tr>,
|
||||
];
|
||||
|
||||
categories[category].forEach(cmd => {
|
||||
rows.push(<tr key={cmd.command}>
|
||||
<td><strong>{cmd.getCommand()}</strong></td>
|
||||
<td>{cmd.args}</td>
|
||||
<td>{cmd.description}</td>
|
||||
<td><strong>{ cmd.getCommand() }</strong></td>
|
||||
<td>{ cmd.args }</td>
|
||||
<td>{ cmd.description }</td>
|
||||
</tr>);
|
||||
});
|
||||
|
||||
|
@ -56,7 +56,7 @@ export default ({ onFinished }) => {
|
|||
title={_t("Command Help")}
|
||||
description={<table>
|
||||
<tbody>
|
||||
{body}
|
||||
{ body }
|
||||
</tbody>
|
||||
</table>}
|
||||
hasCloseButton={true}
|
||||
|
|
|
@ -48,27 +48,29 @@ export default class StorageEvictedDialog extends React.Component {
|
|||
"To help us prevent this in future, please <a>send us logs</a>.",
|
||||
{},
|
||||
{
|
||||
a: text => <a href="#" onClick={this._sendBugReport}>{text}</a>,
|
||||
a: text => <a href="#" onClick={this._sendBugReport}>{ text }</a>,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<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}
|
||||
>
|
||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
<p>{_t(
|
||||
<p>{ _t(
|
||||
"Some session data, including encrypted message keys, is " +
|
||||
"missing. Sign out and sign in to fix this, restoring keys " +
|
||||
"from backup.",
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
) }</p>
|
||||
<p>{ _t(
|
||||
"Your browser likely removed this data when running low on " +
|
||||
"disk space.",
|
||||
)} {logRequest}</p>
|
||||
) } { logRequest }</p>
|
||||
</div>
|
||||
<DialogButtons primaryButton={_t("Sign out")}
|
||||
onPrimaryButtonClick={this._onSignOutClick}
|
||||
|
|
|
@ -134,7 +134,7 @@ export default class TabbedIntegrationManagerDialog extends React.Component {
|
|||
key={`tab_${i}`}
|
||||
disabled={this.state.busy}
|
||||
>
|
||||
{m.name}
|
||||
{ m.name }
|
||||
</AccessibleButton>
|
||||
);
|
||||
});
|
||||
|
@ -163,10 +163,10 @@ export default class TabbedIntegrationManagerDialog extends React.Component {
|
|||
return (
|
||||
<div className='mx_TabbedIntegrationManagerDialog_container'>
|
||||
<div className='mx_TabbedIntegrationManagerDialog_tabs'>
|
||||
{this._renderTabs()}
|
||||
{ this._renderTabs() }
|
||||
</div>
|
||||
<div className='mx_TabbedIntegrationManagerDialog_currentManager'>
|
||||
{this._renderTab()}
|
||||
{ this._renderTab() }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -90,9 +90,9 @@ export default class TermsDialog extends React.PureComponent<ITermsDialogProps,
|
|||
private nameForServiceType(serviceType: SERVICE_TYPES, host: string): JSX.Element {
|
||||
switch (serviceType) {
|
||||
case SERVICE_TYPES.IS:
|
||||
return <div>{_t("Identity Server")}<br />({host})</div>;
|
||||
return <div>{ _t("Identity server") }<br />({ host })</div>;
|
||||
case SERVICE_TYPES.IM:
|
||||
return <div>{_t("Integration Manager")}<br />({host})</div>;
|
||||
return <div>{ _t("Integration manager") }<br />({ host })</div>;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -100,13 +100,13 @@ export default class TermsDialog extends React.PureComponent<ITermsDialogProps,
|
|||
switch (serviceType) {
|
||||
case SERVICE_TYPES.IS:
|
||||
return <div>
|
||||
{_t("Find others by phone or email")}
|
||||
{ _t("Find others by phone or email") }
|
||||
<br />
|
||||
{_t("Be found by phone or email")}
|
||||
{ _t("Be found by phone or email") }
|
||||
</div>;
|
||||
case SERVICE_TYPES.IM:
|
||||
return <div>
|
||||
{_t("Use bots, bridges, widgets and sticker packs")}
|
||||
{ _t("Use bots, bridges, widgets and sticker packs") }
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
@ -136,10 +136,10 @@ export default class TermsDialog extends React.PureComponent<ITermsDialogProps,
|
|||
}
|
||||
|
||||
rows.push(<tr key={termDoc[termsLang].url}>
|
||||
<td className="mx_TermsDialog_service">{serviceName}</td>
|
||||
<td className="mx_TermsDialog_summary">{summary}</td>
|
||||
<td className="mx_TermsDialog_service">{ serviceName }</td>
|
||||
<td className="mx_TermsDialog_summary">{ summary }</td>
|
||||
<td>
|
||||
{termDoc[termsLang].name}
|
||||
{ termDoc[termsLang].name }
|
||||
<a rel="noreferrer noopener" target="_blank" href={termDoc[termsLang].url}>
|
||||
<span className="mx_TermsDialog_link" />
|
||||
</a>
|
||||
|
@ -186,16 +186,16 @@ export default class TermsDialog extends React.PureComponent<ITermsDialogProps,
|
|||
hasCancel={false}
|
||||
>
|
||||
<div id='mx_Dialog_content'>
|
||||
<p>{_t("To continue you need to accept the terms of this service.")}</p>
|
||||
<p>{ _t("To continue you need to accept the terms of this service.") }</p>
|
||||
|
||||
<table className="mx_TermsDialog_termsTable"><tbody>
|
||||
<tr className="mx_TermsDialog_termsTableHeader">
|
||||
<th>{_t("Service")}</th>
|
||||
<th>{_t("Summary")}</th>
|
||||
<th>{_t("Document")}</th>
|
||||
<th>{_t("Accept")}</th>
|
||||
<th>{ _t("Service") }</th>
|
||||
<th>{ _t("Summary") }</th>
|
||||
<th>{ _t("Document") }</th>
|
||||
<th>{ _t("Accept") }</th>
|
||||
</tr>
|
||||
{rows}
|
||||
{ rows }
|
||||
</tbody></table>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -48,13 +48,13 @@ const UntrustedDeviceDialog: React.FC<IProps> = ({ device, user, onFinished }) =
|
|||
className="mx_UntrustedDeviceDialog"
|
||||
title={<>
|
||||
<E2EIcon status="warning" size={24} hideTooltip={true} />
|
||||
{ _t("Not Trusted")}
|
||||
{ _t("Not Trusted") }
|
||||
</>}
|
||||
>
|
||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
<p>{newSessionText}</p>
|
||||
<p>{device.getDisplayName()} ({device.deviceId})</p>
|
||||
<p>{askToVerifyText}</p>
|
||||
<p>{ newSessionText }</p>
|
||||
<p>{ device.getDisplayName() } ({ device.deviceId })</p>
|
||||
<p>{ askToVerifyText }</p>
|
||||
</div>
|
||||
<div className='mx_Dialog_buttons'>
|
||||
<AccessibleButton element="button" kind="secondary" onClick={() => onFinished("legacy")}>
|
||||
|
|
|
@ -86,7 +86,7 @@ export default class UploadConfirmDialog extends React.Component<IProps> {
|
|||
preview = <div className="mx_UploadConfirmDialog_previewOuter">
|
||||
<div className="mx_UploadConfirmDialog_previewInner">
|
||||
<div><img className="mx_UploadConfirmDialog_imagePreview" src={this.objectUrl} /></div>
|
||||
<div>{this.props.file.name} ({filesize(this.props.file.size)})</div>
|
||||
<div>{ this.props.file.name } ({ filesize(this.props.file.size) })</div>
|
||||
</div>
|
||||
</div>;
|
||||
} else {
|
||||
|
@ -95,7 +95,7 @@ export default class UploadConfirmDialog extends React.Component<IProps> {
|
|||
<img className="mx_UploadConfirmDialog_fileIcon"
|
||||
src={require("../../../../res/img/feather-customised/files.svg")}
|
||||
/>
|
||||
{this.props.file.name} ({filesize(this.props.file.size)})
|
||||
{ this.props.file.name } ({ filesize(this.props.file.size) })
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
@ -103,7 +103,7 @@ export default class UploadConfirmDialog extends React.Component<IProps> {
|
|||
let uploadAllButton;
|
||||
if (this.props.currentIndex + 1 < this.props.totalFiles) {
|
||||
uploadAllButton = <button onClick={this.onUploadAllClick}>
|
||||
{_t("Upload all")}
|
||||
{ _t("Upload all") }
|
||||
</button>;
|
||||
}
|
||||
|
||||
|
@ -115,7 +115,7 @@ export default class UploadConfirmDialog extends React.Component<IProps> {
|
|||
contentId='mx_Dialog_content'
|
||||
>
|
||||
<div id='mx_Dialog_content'>
|
||||
{preview}
|
||||
{ preview }
|
||||
</div>
|
||||
|
||||
<DialogButtons primaryButton={_t('Upload')}
|
||||
|
@ -123,7 +123,7 @@ export default class UploadConfirmDialog extends React.Component<IProps> {
|
|||
onPrimaryButtonClick={this.onUploadClick}
|
||||
focus={true}
|
||||
>
|
||||
{uploadAllButton}
|
||||
{ uploadAllButton }
|
||||
</DialogButtons>
|
||||
</BaseDialog>
|
||||
);
|
||||
|
|
|
@ -60,7 +60,7 @@ export default class UploadFailureDialog extends React.Component {
|
|||
limit: filesize(this.props.contentMessages.getUploadLimit()),
|
||||
sizeOfThisFile: filesize(this.props.badFiles[0].size),
|
||||
}, {
|
||||
b: sub => <b>{sub}</b>,
|
||||
b: sub => <b>{ sub }</b>,
|
||||
},
|
||||
);
|
||||
buttons = <DialogButtons primaryButton={_t('OK')}
|
||||
|
@ -75,7 +75,7 @@ export default class UploadFailureDialog extends React.Component {
|
|||
{
|
||||
limit: filesize(this.props.contentMessages.getUploadLimit()),
|
||||
}, {
|
||||
b: sub => <b>{sub}</b>,
|
||||
b: sub => <b>{ sub }</b>,
|
||||
},
|
||||
);
|
||||
buttons = <DialogButtons primaryButton={_t('OK')}
|
||||
|
@ -90,7 +90,7 @@ export default class UploadFailureDialog extends React.Component {
|
|||
{
|
||||
limit: filesize(this.props.contentMessages.getUploadLimit()),
|
||||
}, {
|
||||
b: sub => <b>{sub}</b>,
|
||||
b: sub => <b>{ sub }</b>,
|
||||
},
|
||||
);
|
||||
const howManyOthers = this.props.totalFiles - this.props.badFiles.length;
|
||||
|
@ -111,11 +111,11 @@ export default class UploadFailureDialog extends React.Component {
|
|||
contentId='mx_Dialog_content'
|
||||
>
|
||||
<div id='mx_Dialog_content'>
|
||||
{message}
|
||||
{preview}
|
||||
{ message }
|
||||
{ preview }
|
||||
</div>
|
||||
|
||||
{buttons}
|
||||
{ buttons }
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -81,7 +81,7 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
|
|||
this.setState({ mjolnirEnabled: newValue });
|
||||
};
|
||||
|
||||
_getTabs() {
|
||||
private getTabs() {
|
||||
const tabs = [];
|
||||
|
||||
tabs.push(new Tab(
|
||||
|
@ -114,7 +114,7 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
|
|||
UserTab.Preferences,
|
||||
_td("Preferences"),
|
||||
"mx_UserSettingsDialog_preferencesIcon",
|
||||
<PreferencesUserSettingsTab />,
|
||||
<PreferencesUserSettingsTab closeSettingsFn={this.props.onFinished} />,
|
||||
));
|
||||
|
||||
if (SettingsStore.getValue(UIFeature.Voip)) {
|
||||
|
@ -170,7 +170,7 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
|
|||
title={_t("Settings")}
|
||||
>
|
||||
<div className='mx_SettingsDialog_content'>
|
||||
<TabbedView tabs={this._getTabs()} initialTabId={this.props.initialTabId} />
|
||||
<TabbedView tabs={this.getTabs()} initialTabId={this.props.initialTabId} />
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
|
|
|
@ -21,7 +21,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
|||
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import EncryptionPanel from "../right_panel/EncryptionPanel";
|
||||
import { User } from 'matrix-js-sdk';
|
||||
import { User } from 'matrix-js-sdk/src/models/user';
|
||||
|
||||
interface IProps {
|
||||
verificationRequest: VerificationRequest;
|
||||
|
|
|
@ -105,7 +105,7 @@ export default class WidgetCapabilitiesPromptDialog extends React.PureComponent<
|
|||
const checkboxRows = Object.entries(this.state.booleanStates).map(([cap, isChecked], i) => {
|
||||
const text = CapabilityText.for(cap, this.props.widgetKind);
|
||||
const byline = text.byline
|
||||
? <span className="mx_WidgetCapabilitiesPromptDialog_byline">{text.byline}</span>
|
||||
? <span className="mx_WidgetCapabilitiesPromptDialog_byline">{ text.byline }</span>
|
||||
: null;
|
||||
|
||||
return (
|
||||
|
@ -113,8 +113,8 @@ export default class WidgetCapabilitiesPromptDialog extends React.PureComponent<
|
|||
<StyledCheckbox
|
||||
checked={isChecked}
|
||||
onChange={() => this.onToggle(cap)}
|
||||
>{text.primary}</StyledCheckbox>
|
||||
{byline}
|
||||
>{ text.primary }</StyledCheckbox>
|
||||
{ byline }
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -127,8 +127,8 @@ export default class WidgetCapabilitiesPromptDialog extends React.PureComponent<
|
|||
>
|
||||
<form onSubmit={this.onSubmit}>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="text-muted">{_t("This widget would like to:")}</div>
|
||||
{checkboxRows}
|
||||
<div className="text-muted">{ _t("This widget would like to:") }</div>
|
||||
{ checkboxRows }
|
||||
<DialogButtons
|
||||
primaryButton={_t("Approve")}
|
||||
cancelButton={_t("Decline All")}
|
||||
|
|
|
@ -78,11 +78,11 @@ export default class WidgetOpenIDPermissionsDialog extends React.Component {
|
|||
>
|
||||
<div className='mx_WidgetOpenIDPermissionsDialog_content'>
|
||||
<p>
|
||||
{_t("The widget will verify your user ID, but won't be able to perform actions for you:")}
|
||||
{ _t("The widget will verify your user ID, but won't be able to perform actions for you:") }
|
||||
</p>
|
||||
<p className="text-muted">
|
||||
{/* cheap trim to just get the path */}
|
||||
{this.props.widget.templateUrl.split("?")[0].split("#")[0]}
|
||||
{ /* cheap trim to just get the path */ }
|
||||
{ this.props.widget.templateUrl.split("?")[0].split("#")[0] }
|
||||
</p>
|
||||
</div>
|
||||
<DialogButtons
|
||||
|
|
|
@ -285,11 +285,12 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
|
|||
|
||||
const resetButton = (
|
||||
<div className="mx_AccessSecretStorageDialog_reset">
|
||||
{_t("Forgotten or lost all recovery methods? <a>Reset all</a>", null, {
|
||||
{ _t("Forgotten or lost all recovery methods? <a>Reset all</a>", null, {
|
||||
a: (sub) => <a
|
||||
href="" onClick={this.onResetAllClick}
|
||||
className="mx_AccessSecretStorageDialog_reset_link">{sub}</a>,
|
||||
})}
|
||||
href=""
|
||||
onClick={this.onResetAllClick}
|
||||
className="mx_AccessSecretStorageDialog_reset_link">{ sub }</a>,
|
||||
}) }
|
||||
</div>
|
||||
);
|
||||
|
||||
|
@ -300,9 +301,9 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
|
|||
title = _t("Reset everything");
|
||||
titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_resetBadge'];
|
||||
content = <div>
|
||||
<p>{_t("Only do this if you have no other device to complete verification with.")}</p>
|
||||
<p>{_t("If you reset everything, you will restart with no trusted sessions, no trusted users, and "
|
||||
+ "might not be able to see past messages.")}</p>
|
||||
<p>{ _t("Only do this if you have no other device to complete verification with.") }</p>
|
||||
<p>{ _t("If you reset everything, you will restart with no trusted sessions, no trusted users, and "
|
||||
+ "might not be able to see past messages.") }</p>
|
||||
<DialogButtons
|
||||
primaryButton={_t('Reset')}
|
||||
onPrimaryButtonClick={this.onConfirmResetAllClick}
|
||||
|
@ -320,27 +321,27 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
|
|||
let keyStatus;
|
||||
if (this.state.keyMatches === false) {
|
||||
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
|
||||
{"\uD83D\uDC4E "}{_t(
|
||||
{ "\uD83D\uDC4E " }{ _t(
|
||||
"Unable to access secret storage. " +
|
||||
"Please verify that you entered the correct Security Phrase.",
|
||||
)}
|
||||
) }
|
||||
</div>;
|
||||
} else {
|
||||
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus" />;
|
||||
}
|
||||
|
||||
content = <div>
|
||||
<p>{_t(
|
||||
<p>{ _t(
|
||||
"Enter your Security Phrase or <button>Use your Security Key</button> to continue.", {},
|
||||
{
|
||||
button: s => <AccessibleButton className="mx_linkButton"
|
||||
element="span"
|
||||
onClick={this.onUseRecoveryKeyClick}
|
||||
>
|
||||
{s}
|
||||
{ s }
|
||||
</AccessibleButton>,
|
||||
},
|
||||
)}</p>
|
||||
) }</p>
|
||||
|
||||
<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this.onPassPhraseNext}>
|
||||
<input
|
||||
|
@ -353,7 +354,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
|
|||
autoComplete="new-password"
|
||||
placeholder={_t("Security Phrase")}
|
||||
/>
|
||||
{keyStatus}
|
||||
{ keyStatus }
|
||||
<DialogButtons
|
||||
primaryButton={_t('Continue')}
|
||||
onPrimaryButtonClick={this.onPassPhraseNext}
|
||||
|
@ -375,11 +376,11 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
|
|||
'mx_AccessSecretStorageDialog_recoveryKeyFeedback_invalid': this.state.recoveryKeyCorrect === false,
|
||||
});
|
||||
const recoveryKeyFeedback = <div className={feedbackClasses}>
|
||||
{this.getKeyValidationText()}
|
||||
{ this.getKeyValidationText() }
|
||||
</div>;
|
||||
|
||||
content = <div>
|
||||
<p>{_t("Use your Security Key to continue.")}</p>
|
||||
<p>{ _t("Use your Security Key to continue.") }</p>
|
||||
|
||||
<form
|
||||
className="mx_AccessSecretStorageDialog_primaryContainer"
|
||||
|
@ -399,7 +400,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
|
|||
/>
|
||||
</div>
|
||||
<span className="mx_AccessSecretStorageDialog_recoveryKeyEntry_entryControlSeparatorText">
|
||||
{_t("or")}
|
||||
{ _t("or") }
|
||||
</span>
|
||||
<div>
|
||||
<input type="file"
|
||||
|
@ -408,11 +409,11 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
|
|||
onChange={this.onRecoveryKeyFileChange}
|
||||
/>
|
||||
<AccessibleButton kind="primary" onClick={this.onRecoveryKeyFileUploadClick}>
|
||||
{_t("Upload")}
|
||||
{ _t("Upload") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
{recoveryKeyFeedback}
|
||||
{ recoveryKeyFeedback }
|
||||
<DialogButtons
|
||||
primaryButton={_t('Continue')}
|
||||
onPrimaryButtonClick={this.onRecoveryKeyNext}
|
||||
|
@ -435,7 +436,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
|
|||
titleClass={titleClass}
|
||||
>
|
||||
<div>
|
||||
{content}
|
||||
{ content }
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
|
|
|
@ -44,12 +44,12 @@ export default class ConfirmDestroyCrossSigningDialog extends React.Component<IP
|
|||
>
|
||||
<div className='mx_ConfirmDestroyCrossSigningDialog_content'>
|
||||
<p>
|
||||
{_t(
|
||||
{ _t(
|
||||
"Deleting cross-signing keys is permanent. " +
|
||||
"Anyone you have verified with will see security alerts. " +
|
||||
"You almost certainly don't want to do this, unless " +
|
||||
"you've lost every device you can cross-sign from.",
|
||||
)}
|
||||
) }
|
||||
</p>
|
||||
</div>
|
||||
<DialogButtons
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue