Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into travis/blurhash

 Conflicts:
	src/ContentMessages.tsx
	src/components/structures/UploadBar.tsx
	src/components/views/messages/MImageBody.js
	src/components/views/messages/MStickerBody.js
	src/components/views/messages/MVideoBody.tsx
This commit is contained in:
Michael Telatynski 2021-07-01 20:48:34 +01:00
commit 1f337b28ac
793 changed files with 13366 additions and 10246 deletions

View file

@ -0,0 +1,124 @@
/*
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 "../../../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";
import DurationClock from "./DurationClock";
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;
}
@replaceableComponent("views.audio_messages.AudioPlayer")
export default class AudioPlayer extends React.PureComponent<IProps, IState> {
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
// interaction is typically on press).
if (ev.key === Key.SPACE) {
ev.stopPropagation();
this.playPauseRef.current?.toggleState();
} else if (ev.key === Key.ARROW_LEFT) {
ev.stopPropagation();
this.seekRef.current?.left();
} else if (ev.key === Key.ARROW_RIGHT) {
ev.stopPropagation();
this.seekRef.current?.right();
}
};
protected renderFileSize(): string {
const bytes = this.props.playback.sizeBytes;
if (!bytes) return null;
// Not translated here - we're just presenting the data which should already
// be translated if needed.
return `(${formatBytes(bytes)})`;
}
public render(): ReactNode {
// tabIndex=0 to ensure that the whole component becomes a tab stop, where we handle keyboard
// events for accessibility
return <div className='mx_MediaBody mx_AudioPlayer_container' tabIndex={0} onKeyDown={this.onKeyDown}>
<div className='mx_AudioPlayer_primaryContainer'>
<PlayPauseButton
playback={this.props.playback}
playbackPhase={this.state.playbackPhase}
tabIndex={-1} // prevent tabbing into the button
ref={this.playPauseRef}
/>
<div className='mx_AudioPlayer_mediaInfo'>
<span className='mx_AudioPlayer_mediaName'>
{this.props.mediaName || _t("Unnamed audio")}
</span>
<div className='mx_AudioPlayer_byline'>
<DurationClock playback={this.props.playback} />
&nbsp; {/* easiest way to introduce a gap between the components */}
{ this.renderFileSize() }
</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>;
}
}

View file

@ -15,9 +15,9 @@ limitations under the License.
*/
import React from "react";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
export interface IProps {
seconds: number;
}
@ -28,7 +28,7 @@ interface IState {
* Simply converts seconds into minutes and seconds. Note that hours will not be
* displayed, making it possible to see "82:29".
*/
@replaceableComponent("views.voice_messages.Clock")
@replaceableComponent("views.audio_messages.Clock")
export default class Clock extends React.Component<IProps, IState> {
public constructor(props) {
super(props);

View file

@ -0,0 +1,55 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Clock from "./Clock";
import { Playback } from "../../../voice/Playback";
interface IProps {
playback: Playback;
}
interface IState {
durationSeconds: number;
}
/**
* A clock which shows a clip's maximum duration.
*/
@replaceableComponent("views.audio_messages.DurationClock")
export default class DurationClock extends React.PureComponent<IProps, IState> {
public constructor(props) {
super(props);
this.state = {
// we track the duration on state because we won't really know what the clip duration
// is until the first time update, and as a PureComponent we are trying to dedupe state
// updates as much as possible. This is just the easiest way to avoid a forceUpdate() or
// member property to track "did we get a duration".
durationSeconds: this.props.playback.clockInfo.durationSeconds,
};
this.props.playback.clockInfo.liveData.onUpdate(this.onTimeUpdate);
}
private onTimeUpdate = (time: number[]) => {
this.setState({ durationSeconds: time[1] });
};
public render() {
return <Clock seconds={this.state.durationSeconds} />;
}
}

View file

@ -15,9 +15,10 @@ limitations under the License.
*/
import React from "react";
import {IRecordingUpdate, VoiceRecording} from "../../../voice/VoiceRecording";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { IRecordingUpdate, VoiceRecording } from "../../../voice/VoiceRecording";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Clock from "./Clock";
import { MarkedExecution } from "../../../utils/MarkedExecution";
interface IProps {
recorder: VoiceRecording;
@ -30,18 +31,33 @@ interface IState {
/**
* A clock for a live recording.
*/
@replaceableComponent("views.voice_messages.LiveRecordingClock")
@replaceableComponent("views.audio_messages.LiveRecordingClock")
export default class LiveRecordingClock extends React.PureComponent<IProps, IState> {
public constructor(props) {
super(props);
private seconds = 0;
private scheduledUpdate = new MarkedExecution(
() => this.updateClock(),
() => requestAnimationFrame(() => this.scheduledUpdate.trigger()),
);
this.state = {seconds: 0};
this.props.recorder.liveData.onUpdate(this.onRecordingUpdate);
constructor(props) {
super(props);
this.state = {
seconds: 0,
};
}
private onRecordingUpdate = (update: IRecordingUpdate) => {
this.setState({seconds: update.timeSeconds});
};
componentDidMount() {
this.props.recorder.liveData.onUpdate((update: IRecordingUpdate) => {
this.seconds = update.timeSeconds;
this.scheduledUpdate.mark();
});
}
private updateClock() {
this.setState({
seconds: this.seconds,
});
}
public render() {
return <Clock seconds={this.state.seconds} />;

View file

@ -0,0 +1,74 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import { IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording } from "../../../voice/VoiceRecording";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { arrayFastResample } from "../../../utils/arrays";
import { percentageOf } from "../../../utils/numbers";
import Waveform from "./Waveform";
import { MarkedExecution } from "../../../utils/MarkedExecution";
interface IProps {
recorder: VoiceRecording;
}
interface IState {
waveform: number[];
}
/**
* A waveform which shows the waveform of a live recording
*/
@replaceableComponent("views.audio_messages.LiveRecordingWaveform")
export default class LiveRecordingWaveform extends React.PureComponent<IProps, IState> {
public static defaultProps = {
progress: 1,
};
private waveform: number[] = [];
private scheduledUpdate = new MarkedExecution(
() => this.updateWaveform(),
() => requestAnimationFrame(() => this.scheduledUpdate.trigger()),
);
constructor(props) {
super(props);
this.state = {
waveform: [],
};
}
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));
this.scheduledUpdate.mark();
});
}
private updateWaveform() {
this.setState({ waveform: this.waveform });
}
public render() {
return <Waveform relHeights={this.state.waveform} />;
}
}

View file

@ -14,14 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {ReactNode} from "react";
import {replaceableComponent} from "../../../utils/replaceableComponent";
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 { _t } from "../../../languageHandler";
import { Playback, PlaybackState } from "../../../voice/Playback";
import classNames from "classnames";
interface IProps {
// omitted props are handled by render function
interface IProps extends Omit<React.ComponentProps<typeof AccessibleTooltipButton>, "title" | "onClick" | "disabled"> {
// Playback instance to manipulate. Cannot change during the component lifecycle.
playback: Playback;
@ -33,19 +34,25 @@ interface IProps {
* Displays a play/pause button (activating the play/pause function of the recorder)
* to be displayed in reference to a recording.
*/
@replaceableComponent("views.voice_messages.PlayPauseButton")
@replaceableComponent("views.audio_messages.PlayPauseButton")
export default class PlayPauseButton extends React.PureComponent<IProps> {
public constructor(props) {
super(props);
}
private onClick = async () => {
await this.props.playback.toggle();
private onClick = () => {
// noinspection JSIgnoredPromiseFromCall
this.toggleState();
};
public async toggleState() {
await this.props.playback.toggle();
}
public render(): ReactNode {
const isPlaying = this.props.playback.isPlaying;
const isDisabled = this.props.playbackPhase === PlaybackState.Decoding;
const { playback, playbackPhase, ...restProps } = this.props;
const isPlaying = playback.isPlaying;
const isDisabled = playbackPhase === PlaybackState.Decoding;
const classes = classNames('mx_PlayPauseButton', {
'mx_PlayPauseButton_play': !isPlaying,
'mx_PlayPauseButton_pause': isPlaying,
@ -56,6 +63,7 @@ export default class PlayPauseButton extends React.PureComponent<IProps> {
title={isPlaying ? _t("Pause") : _t("Play")}
onClick={this.onClick}
disabled={isDisabled}
{...restProps}
/>;
}
}

View file

@ -15,13 +15,18 @@ limitations under the License.
*/
import React from "react";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Clock from "./Clock";
import {Playback, PlaybackState} from "../../../voice/Playback";
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
import { Playback, PlaybackState } from "../../../voice/Playback";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
interface IProps {
playback: Playback;
// The default number of seconds to show when the playback has completed or
// has not started. Not used during playback, even when paused. Defaults to
// clip duration length.
defaultDisplaySeconds?: number;
}
interface IState {
@ -33,7 +38,7 @@ interface IState {
/**
* A clock for a playback of a recording.
*/
@replaceableComponent("views.voice_messages.PlaybackClock")
@replaceableComponent("views.audio_messages.PlaybackClock")
export default class PlaybackClock extends React.PureComponent<IProps, IState> {
public constructor(props) {
super(props);
@ -54,17 +59,21 @@ export default class PlaybackClock extends React.PureComponent<IProps, IState> {
private onPlaybackUpdate = (ev: PlaybackState) => {
// Convert Decoding -> Stopped because we don't care about the distinction here
if (ev === PlaybackState.Decoding) ev = PlaybackState.Stopped;
this.setState({playbackPhase: ev});
this.setState({ playbackPhase: ev });
};
private onTimeUpdate = (time: number[]) => {
this.setState({seconds: time[0], durationSeconds: time[1]});
this.setState({ seconds: time[0], durationSeconds: time[1] });
};
public render() {
let seconds = this.state.seconds;
if (this.state.playbackPhase === PlaybackState.Stopped) {
seconds = this.state.durationSeconds;
if (Number.isFinite(this.props.defaultDisplaySeconds)) {
seconds = this.props.defaultDisplaySeconds;
} else {
seconds = this.state.durationSeconds;
}
}
return <Clock seconds={seconds} />;
}

View file

@ -15,11 +15,11 @@ limitations under the License.
*/
import React from "react";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {arraySeed, arrayTrimFill} from "../../../utils/arrays";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { arraySeed, arrayTrimFill } from "../../../utils/arrays";
import Waveform from "./Waveform";
import {Playback, PLAYBACK_WAVEFORM_SAMPLES} from "../../../voice/Playback";
import {percentageOf} from "../../../utils/numbers";
import { Playback, PLAYBACK_WAVEFORM_SAMPLES } from "../../../voice/Playback";
import { percentageOf } from "../../../utils/numbers";
interface IProps {
playback: Playback;
@ -33,7 +33,7 @@ interface IState {
/**
* A waveform which shows the waveform of a previously recorded recording
*/
@replaceableComponent("views.voice_messages.PlaybackWaveform")
@replaceableComponent("views.audio_messages.PlaybackWaveform")
export default class PlaybackWaveform extends React.PureComponent<IProps, IState> {
public constructor(props) {
super(props);
@ -53,13 +53,13 @@ export default class PlaybackWaveform extends React.PureComponent<IProps, IState
}
private onWaveformUpdate = (waveform: number[]) => {
this.setState({heights: this.toHeights(waveform)});
this.setState({ heights: this.toHeights(waveform) });
};
private onTimeUpdate = (time: number[]) => {
// Track percentages to a general precision to avoid over-waking the component.
const progress = Number(percentageOf(time[0], 0, time[1]).toFixed(3));
this.setState({progress});
this.setState({ progress });
};
public render() {

View file

@ -14,12 +14,13 @@ 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 { 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
@ -31,6 +32,7 @@ interface IState {
playbackPhase: PlaybackState;
}
@replaceableComponent("views.audio_messages.RecordingPlayback")
export default class RecordingPlayback extends React.PureComponent<IProps, IState> {
constructor(props: IProps) {
super(props);
@ -49,14 +51,14 @@ export default class RecordingPlayback extends React.PureComponent<IProps, IStat
}
private onPlaybackUpdate = (ev: PlaybackState) => {
this.setState({playbackPhase: ev});
this.setState({ playbackPhase: ev });
};
public render(): ReactNode {
return <div className='mx_VoiceMessagePrimaryContainer'>
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>
</div>;
}
}

View file

@ -0,0 +1,112 @@
/*
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 "../../../voice/Playback";
import React, { ChangeEvent, CSSProperties, ReactNode } from "react";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { MarkedExecution } from "../../../utils/MarkedExecution";
import { percentageOf } from "../../../utils/numbers";
interface IProps {
// Playback instance to render. Cannot change during component lifecycle: create
// an all-new component instead.
playback: Playback;
// Tab index for the underlying component. Useful if the seek bar is in a managed state.
// Defaults to zero.
tabIndex?: number;
playbackPhase: PlaybackState;
}
interface IState {
percentage: number;
}
interface ISeekCSS extends CSSProperties {
'--fillTo': number;
}
const ARROW_SKIP_SECONDS = 5; // arbitrary
@replaceableComponent("views.audio_messages.SeekBar")
export default class SeekBar extends React.PureComponent<IProps, IState> {
// We use an animation frame request to avoid overly spamming prop updates, even if we aren't
// really using anything demanding on the CSS front.
private animationFrameFn = new MarkedExecution(
() => this.doUpdate(),
() => requestAnimationFrame(() => this.animationFrameFn.trigger()));
public static defaultProps = {
tabIndex: 0,
};
constructor(props: IProps) {
super(props);
this.state = {
percentage: 0,
};
// We don't need to de-register: the class handles this for us internally
this.props.playback.clockInfo.liveData.onUpdate(() => this.animationFrameFn.mark());
}
private doUpdate() {
this.setState({
percentage: percentageOf(
this.props.playback.clockInfo.timeSeconds,
0,
this.props.playback.clockInfo.durationSeconds),
});
}
public left() {
// noinspection JSIgnoredPromiseFromCall
this.props.playback.skipTo(this.props.playback.clockInfo.timeSeconds - ARROW_SKIP_SECONDS);
}
public right() {
// noinspection JSIgnoredPromiseFromCall
this.props.playback.skipTo(this.props.playback.clockInfo.timeSeconds + ARROW_SKIP_SECONDS);
}
private onChange = (ev: ChangeEvent<HTMLInputElement>) => {
// Thankfully, onChange is only called when the user changes the value, not when we
// change the value on the component. We can use this as a reliable "skip to X" function.
//
// noinspection JSIgnoredPromiseFromCall
this.props.playback.skipTo(Number(ev.target.value) * this.props.playback.clockInfo.durationSeconds);
};
public render(): ReactNode {
// We use a range input to avoid having to re-invent accessibility handling on
// a custom set of divs.
return <input
type="range"
className='mx_SeekBar'
tabIndex={this.props.tabIndex}
onChange={this.onChange}
min={0}
max={1}
value={this.state.percentage}
step={0.001}
style={{ '--fillTo': this.state.percentage } as ISeekCSS}
disabled={this.props.playbackPhase === PlaybackState.Decoding}
/>;
}
}

View file

@ -15,8 +15,13 @@ limitations under the License.
*/
import React from "react";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import classNames from "classnames";
import { CSSProperties } from "react";
interface WaveformCSSProperties extends CSSProperties {
'--barHeight': number;
}
interface IProps {
relHeights: number[]; // relative heights (0-1)
@ -34,16 +39,12 @@ interface IState {
* For CSS purposes, a mx_Waveform_bar_100pct class is added when the bar should be
* "filled", as a demonstration of the progress property.
*/
@replaceableComponent("views.voice_messages.Waveform")
@replaceableComponent("views.audio_messages.Waveform")
export default class Waveform extends React.PureComponent<IProps, IState> {
public static defaultProps = {
progress: 1,
};
public constructor(props) {
super(props);
}
public render() {
return <div className='mx_Waveform'>
{this.props.relHeights.map((h, i) => {
@ -53,7 +54,9 @@ export default class Waveform extends React.PureComponent<IProps, IState> {
'mx_Waveform_bar': true,
'mx_Waveform_bar_100pct': isCompleteBar,
});
return <span key={i} style={{height: (h * 100) + '%'}} className={classes} />;
return <span key={i} style={{
"--barHeight": h,
} as WaveformCSSProperties} className={classes} />;
})}
</div>;
}

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import React from 'react';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.auth.AuthBody")
export default class AuthBody extends React.PureComponent {

View file

@ -18,7 +18,7 @@ limitations under the License.
import { _t } from '../../../languageHandler';
import React from 'react';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.auth.AuthFooter")
export default class AuthFooter extends React.Component {

View file

@ -18,7 +18,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.auth.AuthHeader")
export default class AuthHeader extends React.Component {

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import React from 'react';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.auth.AuthHeaderLogo")
export default class AuthHeaderLogo extends React.PureComponent {

View file

@ -18,7 +18,7 @@ limitations under the License.
import React from 'react';
import * as sdk from '../../../index';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.auth.AuthPage")
export default class AuthPage extends React.PureComponent {

View file

@ -14,11 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {createRef} from 'react';
import React, { createRef } from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import CountlyAnalytics from "../../../CountlyAnalytics";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
const DIV_ID = 'mx_recaptcha';

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import React from 'react';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.auth.CompleteSecurityBody")
export default class CompleteSecurityBody extends React.PureComponent {

View file

@ -19,10 +19,10 @@ import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import {COUNTRIES, getEmojiFlag} from '../../../phonenumber';
import { COUNTRIES, getEmojiFlag } from '../../../phonenumber';
import SdkConfig from "../../../SdkConfig";
import { _t } from "../../../languageHandler";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
const COUNTRIES_BY_ISO2 = {};
for (const c of COUNTRIES) {

View file

@ -24,7 +24,7 @@ import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from "../elements/AccessibleButton";
import Spinner from "../elements/Spinner";
import CountlyAnalytics from "../../../CountlyAnalytics";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { LocalisedPolicy, Policies } from '../../../Terms';
/* This file contains a collection of components which are used by the
@ -354,7 +354,6 @@ export class TermsAuthEntry extends React.Component<ITermsAuthEntryProps, ITerms
CountlyAnalytics.instance.track("onboarding_terms_begin");
}
componentDidMount() {
this.props.onPhaseChange(DEFAULT_PHASE);
}
@ -371,7 +370,7 @@ export class TermsAuthEntry extends React.Component<ITermsAuthEntryProps, ITerms
newToggles[policy.id] = checked;
}
this.setState({"toggledPolicies": newToggles});
this.setState({ "toggledPolicies": newToggles });
}
private trySubmit = () => {
@ -382,10 +381,10 @@ export class TermsAuthEntry extends React.Component<ITermsAuthEntryProps, ITerms
}
if (allChecked) {
this.props.submitAuthDict({type: AuthType.Terms});
this.props.submitAuthDict({ type: AuthType.Terms });
CountlyAnalytics.instance.track("onboarding_terms_complete");
} else {
this.setState({errorText: _t("Please review and accept all of the homeserver's policies")});
this.setState({ errorText: _t("Please review and accept all of the homeserver's policies") });
}
};
@ -518,11 +517,11 @@ export class MsisdnAuthEntry extends React.Component<IMsisdnAuthEntryProps, IMsi
componentDidMount() {
this.props.onPhaseChange(DEFAULT_PHASE);
this.setState({requestingToken: true});
this.setState({ requestingToken: true });
this.requestMsisdnToken().catch((e) => {
this.props.fail(e);
}).finally(() => {
this.setState({requestingToken: false});
this.setState({ requestingToken: false });
});
}
@ -710,7 +709,7 @@ export class SSOAuthEntry extends React.Component<ISSOAuthEntryProps, ISSOAuthEn
// context.
this.popupWindow = window.open(this.ssoUrl, "_blank");
this.setState({phase: SSOAuthEntry.PHASE_POSTAUTH});
this.setState({ phase: SSOAuthEntry.PHASE_POSTAUTH });
this.props.onPhaseChange(SSOAuthEntry.PHASE_POSTAUTH);
};

View file

@ -15,12 +15,12 @@ limitations under the License.
*/
import SdkConfig from "../../../SdkConfig";
import {getCurrentLanguage} from "../../../languageHandler";
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 { SettingLevel } from "../../../settings/SettingLevel";
function onChange(newLang) {
if (getCurrentLanguage() !== newLang) {
@ -29,7 +29,7 @@ function onChange(newLang) {
}
}
export default function LanguageSelector({disabled}) {
export default function LanguageSelector({ disabled }) {
if (SdkConfig.get()['disable_login_language_selector']) return <div />;
const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown');

View file

@ -14,15 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {PureComponent, RefCallback, RefObject} from "react";
import React, { PureComponent, RefCallback, RefObject } from "react";
import classNames from "classnames";
import zxcvbn from "zxcvbn";
import SdkConfig from "../../../SdkConfig";
import withValidation, {IFieldState, IValidationResult} from "../elements/Validation";
import {_t, _td} from "../../../languageHandler";
import Field, {IInputProps} from "../elements/Field";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import withValidation, { IFieldState, IValidationResult } from "../elements/Validation";
import { _t, _td } from "../../../languageHandler";
import Field, { IInputProps } from "../elements/Field";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps extends Omit<IInputProps, "onValidate"> {
autoFocus?: boolean;

View file

@ -19,14 +19,14 @@ import classNames from 'classnames';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import { ValidatedServerConfig } from "../../../utils/AutoDiscoveryUtils";
import AccessibleButton from "../elements/AccessibleButton";
import CountlyAnalytics from "../../../CountlyAnalytics";
import withValidation from "../elements/Validation";
import * as Email from "../../../email";
import Field from "../elements/Field";
import CountryDropdown from "./CountryDropdown";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
// For validating phone numbers without country codes
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
@ -166,7 +166,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
};
private onPasswordChanged = ev => {
this.setState({password: ev.target.value});
this.setState({ password: ev.target.value });
};
private async verifyFieldsBeforeSubmit() {
@ -322,7 +322,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
const result = await this.validatePasswordRules(fieldState);
this.markFieldValid(LoginField.Password, result.valid);
return result;
}
};
private renderLoginField(loginType: IState["loginType"], autoFocus: boolean) {
const classes = {

View file

@ -25,12 +25,12 @@ import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import { SAFE_LOCALPART_REGEX } from '../../../Registration';
import withValidation from '../elements/Validation';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import { ValidatedServerConfig } from "../../../utils/AutoDiscoveryUtils";
import PassphraseField from "./PassphraseField";
import CountlyAnalytics from "../../../CountlyAnalytics";
import Field from '../elements/Field';
import RegistrationEmailPromptDialog from '../dialogs/RegistrationEmailPromptDialog';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
enum RegistrationField {
Email = "field_email",

View file

@ -20,11 +20,11 @@ import classNames from "classnames";
import * as sdk from '../../../index';
import SdkConfig from '../../../SdkConfig';
import AuthPage from "./AuthPage";
import {_td} from "../../../languageHandler";
import { _td } from "../../../languageHandler";
import SettingsStore from "../../../settings/SettingsStore";
import {UIFeature} from "../../../settings/UIFeature";
import { UIFeature } from "../../../settings/UIFeature";
import CountlyAnalytics from "../../../CountlyAnalytics";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
// translatable strings for Welcome pages
_td("Sign in with SSO");

View file

@ -17,16 +17,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useCallback, useContext, useEffect, useState} from 'react';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import classNames from 'classnames';
import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
import * as AvatarLogic from '../../../Avatar';
import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from '../elements/AccessibleButton';
import RoomContext from "../../../contexts/RoomContext";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {useEventEmitter} from "../../../hooks/useEventEmitter";
import {toPx} from "../../../utils/units";
import {ResizeMethod} from "../../../Avatar";
import { useEventEmitter } from "../../../hooks/useEventEmitter";
import { toPx } from "../../../utils/units";
import { _t } from '../../../languageHandler';
interface IProps {
@ -63,7 +64,7 @@ const calculateUrls = (url, urls, lowBandwidth) => {
return Array.from(new Set(_urls));
};
const useImageUrl = ({url, urls}): [string, () => void] => {
const useImageUrl = ({ url, urls }): [string, () => void] => {
// Since this is a hot code path and the settings store can be slow, we
// use the cached lowBandwidth value from the room context if it exists
const roomContext = useContext(RoomContext);
@ -114,7 +115,7 @@ const BaseAvatar = (props: IProps) => {
...otherProps
} = props;
const [imageUrl, onError] = useImageUrl({url, urls});
const [imageUrl, onError] = useImageUrl({ url, urls });
if (!imageUrl && defaultToInitialLetter) {
const initialLetter = AvatarLogic.getInitialLetter(name);

View file

@ -24,12 +24,12 @@ import RoomAvatar from "./RoomAvatar";
import NotificationBadge from '../rooms/NotificationBadge';
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import { NotificationState } from "../../../stores/notifications/NotificationState";
import {isPresenceEnabled} from "../../../utils/presence";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {_t} from "../../../languageHandler";
import { isPresenceEnabled } from "../../../utils/presence";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { _t } from "../../../languageHandler";
import TextWithTooltip from "../elements/TextWithTooltip";
import DMRoomMap from "../../../utils/DMRoomMap";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
room: Room;
@ -121,7 +121,7 @@ export default class DecoratedRoomAvatar extends React.PureComponent<IProps, ISt
if (ev.getType() === 'm.room.join_rules' || ev.getType() === 'm.room.member') {
const newIcon = this.calculateIcon();
if (newIcon !== this.state.icon) {
this.setState({icon: newIcon});
this.setState({ icon: newIcon });
}
}
};
@ -130,7 +130,7 @@ export default class DecoratedRoomAvatar extends React.PureComponent<IProps, ISt
if (this.isUnmounted) return;
const newIcon = this.getPresenceIcon();
if (newIcon !== this.state.icon) this.setState({icon: newIcon});
if (newIcon !== this.state.icon) this.setState({ icon: newIcon });
};
private getPresenceIcon(): Icon {

View file

@ -15,10 +15,11 @@ limitations under the License.
*/
import React from 'react';
import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
import BaseAvatar from './BaseAvatar';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
import {ResizeMethod} from "../../../Avatar";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
export interface IProps {
groupId?: string;
@ -51,7 +52,7 @@ export default class GroupAvatar extends React.Component<IProps> {
// extract the props we use from props so we can pass any others through
// should consider adding this as a global rule in js-sdk?
/* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
const {groupId, groupAvatarUrl, groupName, ...otherProps} = this.props;
const { groupId, groupAvatarUrl, groupName, ...otherProps } = this.props;
return (
<BaseAvatar

View file

@ -16,14 +16,14 @@ limitations under the License.
*/
import React from 'react';
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
import dis from "../../../dispatcher/dispatcher";
import {Action} from "../../../dispatcher/actions";
import { Action } from "../../../dispatcher/actions";
import BaseAvatar from "./BaseAvatar";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
import {ResizeMethod} from "../../../Avatar";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url"> {
member: RoomMember;
@ -89,7 +89,7 @@ export default class MemberAvatar extends React.Component<IProps, IState> {
}
render() {
let {member, fallbackUserId, onClick, viewUserOnClick, ...otherProps} = this.props;
let { member, fallbackUserId, onClick, viewUserOnClick, ...otherProps } = this.props;
const userId = member ? member.userId : fallbackUserId;
if (viewUserOnClick) {

View file

@ -14,16 +14,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {createRef} from 'react';
import React, { createRef } from 'react';
import PropTypes from 'prop-types';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {_t} from "../../../languageHandler";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from "../../../languageHandler";
import MemberAvatar from '../avatars/MemberAvatar';
import classNames from 'classnames';
import StatusMessageContextMenu from "../context_menus/StatusMessageContextMenu";
import SettingsStore from "../../../settings/SettingsStore";
import {ContextMenu, ContextMenuButton} from "../../structures/ContextMenu";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.avatars.MemberStatusMessageAvatar")
export default class MemberStatusMessageAvatar extends React.Component {

View file

@ -13,17 +13,17 @@ 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 React, { ComponentProps } from 'react';
import { Room } from 'matrix-js-sdk/src/models/room';
import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
import BaseAvatar from './BaseAvatar';
import ImageView from '../elements/ImageView';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import * as Avatar from '../../../Avatar';
import {ResizeMethod} from "../../../Avatar";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick"> {
// Room may be left unset here, but if it is,
@ -128,7 +128,7 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
};
public render() {
const {room, oobData, viewAvatarOnClick, onClick, ...otherProps} = this.props;
const { room, oobData, viewAvatarOnClick, onClick, ...otherProps } = this.props;
const roomName = room ? room.name : oobData.name;

View file

@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {ComponentProps} from 'react';
import React, { ComponentProps } from 'react';
import classNames from 'classnames';
import {IApp} from "../../../stores/WidgetStore";
import BaseAvatar, {BaseAvatarType} from "./BaseAvatar";
import {mediaFromMxc} from "../../../customisations/Media";
import { IApp } from "../../../stores/WidgetStore";
import BaseAvatar, { BaseAvatarType } from "./BaseAvatar";
import { mediaFromMxc } from "../../../customisations/Media";
interface IProps extends Omit<ComponentProps<BaseAvatarType>, "name" | "url" | "urls"> {
app: IApp;
@ -49,7 +49,7 @@ const WidgetAvatar: React.FC<IProps> = ({ app, className, width = 20, height = 2
width={width}
height={height}
/>
)
);
};
export default WidgetAvatar;

View file

@ -17,14 +17,15 @@ limitations under the License.
import React from "react";
import classNames from "classnames";
import {_t} from "../../../languageHandler";
import { _t } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
import SettingsStore from "../../../settings/SettingsStore";
import {SettingLevel} from "../../../settings/SettingLevel";
import { SettingLevel } from "../../../settings/SettingLevel";
import TextWithTooltip from "../elements/TextWithTooltip";
import Modal from "../../../Modal";
import BetaFeedbackDialog from "../dialogs/BetaFeedbackDialog";
import SdkConfig from "../../../SdkConfig";
import SettingsFlag from "../elements/SettingsFlag";
interface IProps {
title?: string;
@ -66,7 +67,7 @@ const BetaCard = ({ title: titleOverride, featureId }: IProps) => {
const info = SettingsStore.getBetaInfo(featureId);
if (!info) return null; // Beta is invalid/disabled
const { title, caption, disclaimer, image, feedbackLabel, feedbackSubheading } = info;
const { title, caption, disclaimer, image, feedbackLabel, feedbackSubheading, extraSettings } = info;
const value = SettingsStore.getValue(featureId);
let feedbackButton;
@ -82,26 +83,33 @@ const BetaCard = ({ title: titleOverride, featureId }: IProps) => {
}
return <div className="mx_BetaCard">
<div>
<h3 className="mx_BetaCard_title">
{ titleOverride || _t(title) }
<BetaPill />
</h3>
<span className="mx_BetaCard_caption">{ _t(caption) }</span>
<div className="mx_BetaCard_columns">
<div>
{ feedbackButton }
<AccessibleButton
onClick={() => SettingsStore.setValue(featureId, null, SettingLevel.DEVICE, !value)}
kind={feedbackButton ? "primary_outline" : "primary"}
>
{ value ? _t("Leave the beta") : _t("Join the beta") }
</AccessibleButton>
<h3 className="mx_BetaCard_title">
{ titleOverride || _t(title) }
<BetaPill />
</h3>
<span className="mx_BetaCard_caption">{ _t(caption) }</span>
<div className="mx_BetaCard_buttons">
{ feedbackButton }
<AccessibleButton
onClick={() => SettingsStore.setValue(featureId, null, SettingLevel.DEVICE, !value)}
kind={feedbackButton ? "primary_outline" : "primary"}
>
{ value ? _t("Leave the beta") : _t("Join the beta") }
</AccessibleButton>
</div>
{ disclaimer && <div className="mx_BetaCard_disclaimer">
{ disclaimer(value) }
</div> }
</div>
{ disclaimer && <div className="mx_BetaCard_disclaimer">
{ disclaimer(value) }
</div> }
<img src={image} alt="" />
</div>
<img src={image} alt="" />
{ extraSettings && <div className="mx_BetaCard_relatedSettings">
{ extraSettings.map(key => (
<SettingsFlag key={key} name={key} level={SettingLevel.DEVICE} />
)) }
</div> }
</div>;
};

View file

@ -22,7 +22,7 @@ import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import CallHandler from '../../../CallHandler';
import InviteDialog, { KIND_CALL_TRANSFER } from '../dialogs/InviteDialog';
import Modal from '../../../Modal';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps extends IContextMenuProps {
call: MatrixCall;
@ -42,21 +42,21 @@ export default class CallContextMenu extends React.Component<IProps> {
onHoldClick = () => {
this.props.call.setRemoteOnHold(true);
this.props.onFinished();
}
};
onUnholdClick = () => {
CallHandler.sharedInstance().setActiveCallRoomId(this.props.call.roomId);
this.props.onFinished();
}
};
onTransferClick = () => {
Modal.createTrackedDialog(
'Transfer Call', '', InviteDialog, {kind: KIND_CALL_TRANSFER, call: this.props.call},
'Transfer Call', '', InviteDialog, { kind: KIND_CALL_TRANSFER, call: this.props.call },
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
);
this.props.onFinished();
}
};
render() {
const holdUnholdCaption = this.props.call.isRemoteOnHold() ? _t("Resume") : _t("Hold");

View file

@ -18,8 +18,9 @@ import React from 'react';
import { _t } from '../../../languageHandler';
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 {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps extends IContextMenuProps {
call: MatrixCall;
@ -36,13 +37,17 @@ export default class DialpadContextMenu extends React.Component<IProps, IState>
this.state = {
value: '',
}
};
}
onDigitPress = (digit) => {
this.props.call.sendDtmfDigit(digit);
this.setState({value: this.state.value + digit});
}
this.setState({ value: this.state.value + digit });
};
onChange = (ev) => {
this.setState({ value: ev.target.value });
};
render() {
return <ContextMenu {...this.props}>
@ -50,7 +55,10 @@ export default class DialpadContextMenu extends React.Component<IProps, IState>
<div>
<span className="mx_DialPadContextMenu_title">{_t("Dial pad")}</span>
</div>
<div className="mx_DialPadContextMenu_dialled">{this.state.value}</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">

View file

@ -16,14 +16,13 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
/*
* This component can be used to display generic HTML content in a contextual
* menu.
*/
@replaceableComponent("views.context_menus.GenericElementContextMenu")
export default class GenericElementContextMenu extends React.Component {
static propTypes = {

View file

@ -16,7 +16,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.context_menus.GenericTextContextMenu")
export default class GenericTextContextMenu extends React.Component {

View file

@ -20,10 +20,10 @@ import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import {Group} from 'matrix-js-sdk/src/models/group';
import { Group } from 'matrix-js-sdk/src/models/group';
import GroupStore from "../../../stores/GroupStore";
import {MenuItem} from "../../structures/ContextMenu";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { MenuItem } from "../../structures/ContextMenu";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.context_menus.GroupInviteTileContextMenu")
export default class GroupInviteTileContextMenu extends React.Component {

View file

@ -90,14 +90,14 @@ export const IconizedContextMenuCheckbox: React.FC<ICheckboxProps> = ({
</MenuItemCheckbox>;
};
export const IconizedContextMenuOption: React.FC<IOptionProps> = ({label, iconClassName, ...props}) => {
export const IconizedContextMenuOption: React.FC<IOptionProps> = ({ label, iconClassName, ...props }) => {
return <MenuItem {...props} label={label}>
{ iconClassName && <span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} /> }
<span className="mx_IconizedContextMenu_label">{label}</span>
</MenuItem>;
};
export const IconizedContextMenuOptionList: React.FC<IOptionListProps> = ({first, red, className, children}) => {
export const IconizedContextMenuOptionList: React.FC<IOptionListProps> = ({ first, red, className, children }) => {
const classes = classNames("mx_IconizedContextMenu_optionList", className, {
mx_IconizedContextMenu_optionList_notFirst: !first,
mx_IconizedContextMenu_optionList_red: red,
@ -108,7 +108,7 @@ export const IconizedContextMenuOptionList: React.FC<IOptionListProps> = ({first
</div>;
};
const IconizedContextMenu: React.FC<IProps> = ({className, children, compact, ...props}) => {
const IconizedContextMenu: React.FC<IProps> = ({ className, children, compact, ...props }) => {
const classes = classNames("mx_IconizedContextMenu", className, {
mx_IconizedContextMenu_compact: compact,
});

View file

@ -28,11 +28,12 @@ import Resend from '../../../Resend';
import SettingsStore from '../../../settings/SettingsStore';
import { isUrlPermitted } from '../../../HtmlUtils';
import { isContentActionable } from '../../../utils/EventUtils';
import { MenuItem } from "../../structures/ContextMenu";
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from './IconizedContextMenu';
import { EventType } from "matrix-js-sdk/src/@types/event";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard";
import ForwardDialog from "../dialogs/ForwardDialog";
import { Action } from "../../../dispatcher/actions";
export function canCancel(eventStatus) {
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
@ -89,7 +90,7 @@ export default class MessageContextMenu extends React.Component {
// HACK: Intentionally say we can't pin if the user doesn't want to use the functionality
if (!SettingsStore.getValue("feature_pinning")) canPin = false;
this.setState({canRedact, canPin});
this.setState({ canRedact, canPin });
};
_isPinned() {
@ -148,7 +149,7 @@ export default class MessageContextMenu extends React.Component {
// display error message stating you couldn't delete this.
Modal.createTrackedDialog('You cannot delete this message', '', ErrorDialog, {
title: _t('Error'),
description: _t('You cannot delete this message. (%(code)s)', {code}),
description: _t('You cannot delete this message. (%(code)s)', { code }),
});
}
}
@ -178,7 +179,7 @@ export default class MessageContextMenu extends React.Component {
pinnedIds.push(eventId);
cli.setRoomAccountData(room.roomId, ReadPinsEventId, {
event_ids: [
...room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids,
...(room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids || []),
eventId,
],
});
@ -200,13 +201,13 @@ export default class MessageContextMenu extends React.Component {
onQuoteClick = () => {
dis.dispatch({
action: 'quote',
action: Action.ComposerInsert,
event: this.props.mxEvent,
});
this.closeMenu();
};
onPermalinkClick = (e: Event) => {
onPermalinkClick = (e) => {
e.preventDefault();
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
Modal.createTrackedDialog('share room message dialog', '', ShareDialog, {
@ -256,55 +257,68 @@ export default class MessageContextMenu extends React.Component {
let externalURLButton;
let quoteButton;
let collapseReplyThread;
let redactItemList;
// status is SENT before remote-echo, null after
const isSent = !eventStatus || eventStatus === EventStatus.SENT;
if (!mxEvent.isRedacted()) {
if (unsentReactionsCount !== 0) {
resendReactionsButton = (
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onResendReactionsClick}>
{ _t('Resend %(unsentCount)s reaction(s)', {unsentCount: unsentReactionsCount}) }
</MenuItem>
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconResend"
label={ _t('Resend %(unsentCount)s reaction(s)', { unsentCount: unsentReactionsCount }) }
onClick={this.onResendReactionsClick}
/>
);
}
}
if (isSent && this.state.canRedact) {
redactButton = (
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onRedactClick}>
{ _t('Remove') }
</MenuItem>
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconRedact"
label={_t("Remove")}
onClick={this.onRedactClick}
/>
);
}
if (isContentActionable(mxEvent)) {
forwardButton = (
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onForwardClick}>
{ _t('Forward Message') }
</MenuItem>
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconForward"
label={_t("Forward")}
onClick={this.onForwardClick}
/>
);
if (this.state.canPin) {
pinButton = (
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onPinClick}>
{ this._isPinned() ? _t('Unpin Message') : _t('Pin Message') }
</MenuItem>
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconPin"
label={ this._isPinned() ? _t('Unpin') : _t('Pin') }
onClick={this.onPinClick}
/>
);
}
}
const viewSourceButton = (
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onViewSourceClick}>
{ _t('View Source') }
</MenuItem>
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconSource"
label={_t("View source")}
onClick={this.onViewSourceClick}
/>
);
if (this.props.eventTileOps) {
if (this.props.eventTileOps.isWidgetHidden()) {
unhidePreviewButton = (
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onUnhidePreviewClick}>
{ _t('Unhide Preview') }
</MenuItem>
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconUnhidePreview"
label={_t("Show preview")}
onClick={this.onUnhidePreviewClick}
/>
);
}
}
@ -315,77 +329,97 @@ export default class MessageContextMenu extends React.Component {
}
// XXX: if we use room ID, we should also include a server where the event can be found (other than in the domain of the event ID)
const permalinkButton = (
<MenuItem
element="a"
className="mx_MessageContextMenu_field"
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconPermalink"
onClick={this.onPermalinkClick}
label= {_t('Share')}
element="a"
href={permalink}
target="_blank"
rel="noreferrer noopener"
>
{ mxEvent.isRedacted() || mxEvent.getType() !== 'm.room.message'
? _t('Share Permalink') : _t('Share Message') }
</MenuItem>
/>
);
if (this.props.eventTileOps) { // this event is rendered using TextualBody
quoteButton = (
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onQuoteClick}>
{ _t('Quote') }
</MenuItem>
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconQuote"
label={_t("Quote")}
onClick={this.onQuoteClick}
/>
);
}
// Bridges can provide a 'external_url' to link back to the source.
if (
typeof(mxEvent.event.content.external_url) === "string" &&
if (typeof (mxEvent.event.content.external_url) === "string" &&
isUrlPermitted(mxEvent.event.content.external_url)
) {
externalURLButton = (
<MenuItem
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconLink"
onClick={this.closeMenu}
label={ _t('Source URL') }
element="a"
className="mx_MessageContextMenu_field"
target="_blank"
rel="noreferrer noopener"
onClick={this.closeMenu}
href={mxEvent.event.content.external_url}
>
{ _t('Source URL') }
</MenuItem>
/>
);
}
if (this.props.collapseReplyThread) {
collapseReplyThread = (
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onCollapseReplyThreadClick}>
{ _t('Collapse Reply Thread') }
</MenuItem>
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconCollapse"
label={_t("Collapse reply thread")}
onClick={this.onCollapseReplyThreadClick}
/>
);
}
let reportEventButton;
if (mxEvent.getSender() !== me) {
reportEventButton = (
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onReportEventClick}>
{ _t('Report Content') }
</MenuItem>
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconReport"
label={_t("Report")}
onClick={this.onReportEventClick}
/>
);
}
const commonItemsList = (
<IconizedContextMenuOptionList>
{ quoteButton }
{ forwardButton }
{ pinButton }
{ permalinkButton }
{ reportEventButton }
{ externalURLButton }
{ unhidePreviewButton }
{ viewSourceButton }
{ resendReactionsButton }
{ collapseReplyThread }
</IconizedContextMenuOptionList>
);
if (redactButton) {
redactItemList = (
<IconizedContextMenuOptionList red>
{ redactButton }
</IconizedContextMenuOptionList>
);
}
return (
<div className="mx_MessageContextMenu">
{ resendReactionsButton }
{ redactButton }
{ forwardButton }
{ pinButton }
{ viewSourceButton }
{ unhidePreviewButton }
{ permalinkButton }
{ quoteButton }
{ externalURLButton }
{ collapseReplyThread }
{ reportEventButton }
</div>
<IconizedContextMenu
{...this.props}
className="mx_MessageContextMenu"
compact={true}
>
{ commonItemsList }
{ redactItemList }
</IconizedContextMenu>
);
}
}

View file

@ -17,10 +17,10 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import AccessibleButton from '../elements/AccessibleButton';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.context_menus.StatusMessageContextMenu")
export default class StatusMessageContextMenu extends React.Component {

View file

@ -20,48 +20,73 @@ import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher/dispatcher';
import TagOrderActions from '../../../actions/TagOrderActions';
import {MenuItem} from "../../structures/ContextMenu";
import { MenuItem } from "../../structures/ContextMenu";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import GroupFilterOrderStore from "../../../stores/GroupFilterOrderStore";
@replaceableComponent("views.context_menus.TagTileContextMenu")
export default class TagTileContextMenu extends React.Component {
static propTypes = {
tag: PropTypes.string.isRequired,
index: PropTypes.number.isRequired,
/* callback called when the menu is dismissed */
onFinished: PropTypes.func.isRequired,
};
static contextType = MatrixClientContext;
constructor() {
super();
this._onViewCommunityClick = this._onViewCommunityClick.bind(this);
this._onRemoveClick = this._onRemoveClick.bind(this);
}
_onViewCommunityClick() {
_onViewCommunityClick = () => {
dis.dispatch({
action: 'view_group',
group_id: this.props.tag,
});
this.props.onFinished();
}
};
_onRemoveClick() {
_onRemoveClick = () => {
dis.dispatch(TagOrderActions.removeTag(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();
};
_onMoveDown = () => {
dis.dispatch(TagOrderActions.moveTag(this.context, this.props.tag, this.props.index + 1));
this.props.onFinished();
};
render() {
let moveUp;
let moveDown;
if (this.props.index > 0) {
moveUp = (
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_moveUp" onClick={this._onMoveUp}>
{ _t("Move up") }
</MenuItem>
);
}
if (this.props.index < (GroupFilterOrderStore.getOrderedTags() || []).length - 1) {
moveDown = (
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_moveDown" onClick={this._onMoveDown}>
{ _t("Move down") }
</MenuItem>
);
}
return <div>
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_viewCommunity" onClick={this._onViewCommunityClick}>
{ _t('View Community') }
</MenuItem>
{ (moveUp || moveDown) ? <hr className="mx_TagTileContextMenu_separator" role="separator" /> : null }
{ moveUp }
{ moveDown }
<hr className="mx_TagTileContextMenu_separator" role="separator" />
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_hideCommunity" onClick={this._onRemoveClick}>
{ _t('Hide') }
{ _t("Unpin") }
</MenuItem>
</div>;
}

View file

@ -14,22 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useContext} from "react";
import {MatrixCapabilities} from "matrix-widget-api";
import React, { useContext } from "react";
import { MatrixCapabilities } from "matrix-widget-api";
import IconizedContextMenu, {IconizedContextMenuOption, IconizedContextMenuOptionList} from "./IconizedContextMenu";
import {ChevronFace} from "../../structures/ContextMenu";
import {_t} from "../../../languageHandler";
import {IApp} from "../../../stores/WidgetStore";
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu";
import { ChevronFace } from "../../structures/ContextMenu";
import { _t } from "../../../languageHandler";
import { IApp } from "../../../stores/WidgetStore";
import WidgetUtils from "../../../utils/WidgetUtils";
import {WidgetMessagingStore} from "../../../stores/widgets/WidgetMessagingStore";
import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
import RoomContext from "../../../contexts/RoomContext";
import dis from "../../../dispatcher/dispatcher";
import SettingsStore from "../../../settings/SettingsStore";
import Modal from "../../../Modal";
import QuestionDialog from "../dialogs/QuestionDialog";
import ErrorDialog from "../dialogs/ErrorDialog";
import {WidgetType} from "../../../widgets/WidgetType";
import { WidgetType } from "../../../widgets/WidgetType";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
import { getConfigLivestreamUrl, startJitsiAudioLivestream } from "../../../Livestream";
@ -54,7 +54,7 @@ const WidgetContextMenu: React.FC<IProps> = ({
...props
}) => {
const cli = useContext(MatrixClientContext);
const {room, roomId} = useContext(RoomContext);
const { room, roomId } = useContext(RoomContext);
const widgetMessaging = WidgetMessagingStore.instance.getMessagingForId(app.id);
const canModify = userWidget || WidgetUtils.canUserModifyWidgets(roomId);

View file

@ -14,31 +14,34 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {ReactNode, useContext, useMemo, useState} from "react";
import React, { ReactNode, useContext, useMemo, useState } from "react";
import classNames from "classnames";
import {Room} from "matrix-js-sdk/src/models/room";
import {MatrixClient} from "matrix-js-sdk/src/client";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixClient } from "matrix-js-sdk/src/client";
import {_t} from '../../../languageHandler';
import {IDialogProps} from "./IDialogProps";
import { _t } from '../../../languageHandler';
import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog";
import Dropdown from "../elements/Dropdown";
import SearchBox from "../../structures/SearchBox";
import SpaceStore from "../../../stores/SpaceStore";
import RoomAvatar from "../avatars/RoomAvatar";
import {getDisplayAliasForRoom} from "../../../Rooms";
import { getDisplayAliasForRoom } from "../../../Rooms";
import AccessibleButton from "../elements/AccessibleButton";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import {sleep} from "../../../utils/promise";
import { sleep } from "../../../utils/promise";
import DMRoomMap from "../../../utils/DMRoomMap";
import {calculateRoomVia} from "../../../utils/permalinks/Permalinks";
import { calculateRoomVia } from "../../../utils/permalinks/Permalinks";
import StyledCheckbox from "../elements/StyledCheckbox";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
import { sortRooms } from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
import ProgressBar from "../elements/ProgressBar";
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
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;
@ -204,6 +207,17 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
setSelectedToAdd(new Set(selectedToAdd));
} : null;
const [truncateAt, setTruncateAt] = useState(20);
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)} />
);
}
return <div className="mx_AddExistingToSpace">
<SearchBox
className="mx_textinput_icon mx_textinput_search"
@ -216,16 +230,21 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
{ rooms.length > 0 ? (
<div className="mx_AddExistingToSpace_section">
<h3>{ _t("Rooms") }</h3>
{ rooms.map(room => {
return <Entry
key={room.roomId}
room={room}
checked={selectedToAdd.has(room)}
onChange={onChange ? (checked) => {
onChange(checked, room);
} : null}
/>;
}) }
<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>
) : undefined }

View file

@ -17,23 +17,23 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {createRef} from 'react';
import React, { createRef } from 'react';
import PropTypes from 'prop-types';
import { _t, _td } from '../../../languageHandler';
import * as sdk from '../../../index';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import dis from '../../../dispatcher/dispatcher';
import { addressTypes, getAddressType } from '../../../UserAddress.js';
import { addressTypes, getAddressType } from '../../../UserAddress';
import GroupStore from '../../../stores/GroupStore';
import * as Email from '../../../email';
import IdentityAuthClient from '../../../IdentityAuthClient';
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from '../../../utils/IdentityServerUtils';
import { abbreviateUrl } from '../../../utils/UrlUtils';
import {sleep} from "../../../utils/promise";
import {Key} from "../../../Keyboard";
import {Action} from "../../../dispatcher/actions";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { sleep } from "../../../utils/promise";
import { Key } from "../../../Keyboard";
import { Action } from "../../../dispatcher/actions";
import { replaceableComponent } from "../../../utils/replaceableComponent";
const TRUNCATE_QUERY_LIST = 40;
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
@ -457,7 +457,7 @@ export default class AddressPickerDialog extends React.Component {
const addrType = getAddressType(query);
if (this.state.validAddressTypes.includes(addrType)) {
if (addrType === 'email' && !Email.looksValid(query)) {
this.setState({searchError: _t("That doesn't look like a valid email address")});
this.setState({ searchError: _t("That doesn't look like a valid email address") });
return;
}
suggestedList.unshift({
@ -573,13 +573,13 @@ export default class AddressPickerDialog extends React.Component {
_getFilteredSuggestions() {
// map addressType => set of addresses to avoid O(n*m) operation
const selectedAddresses = {};
this.state.selectedList.forEach(({address, addressType}) => {
this.state.selectedList.forEach(({ address, addressType }) => {
if (!selectedAddresses[addressType]) selectedAddresses[addressType] = new Set();
selectedAddresses[addressType].add(address);
});
// Filter out any addresses in the above already selected addresses (matching both type and address)
return this.state.suggestedList.filter(({address, addressType}) => {
return this.state.suggestedList.filter(({ address, addressType }) => {
return !(selectedAddresses[addressType] && selectedAddresses[addressType].has(address));
});
}

View file

@ -15,39 +15,41 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
import {SettingLevel} from "../../../settings/SettingLevel";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { SettingLevel } from "../../../settings/SettingLevel";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
unknownProfileUsers: Array<{
userId: string;
errorText: string;
}>;
onInviteAnyways: () => void;
onGiveUp: () => void;
onFinished: (success: boolean) => void;
}
@replaceableComponent("views.dialogs.AskInviteAnywayDialog")
export default class AskInviteAnywayDialog extends React.Component {
static propTypes = {
unknownProfileUsers: PropTypes.array.isRequired, // [ {userId, errorText}... ]
onInviteAnyways: PropTypes.func.isRequired,
onGiveUp: PropTypes.func.isRequired,
onFinished: PropTypes.func.isRequired,
};
_onInviteClicked = () => {
export default class AskInviteAnywayDialog extends React.Component<IProps> {
private onInviteClicked = (): void => {
this.props.onInviteAnyways();
this.props.onFinished(true);
};
_onInviteNeverWarnClicked = () => {
private onInviteNeverWarnClicked = (): void => {
SettingsStore.setValue("promptBeforeInviteUnknownUsers", null, SettingLevel.ACCOUNT, false);
this.props.onInviteAnyways();
this.props.onFinished(true);
};
_onGiveUpClicked = () => {
private onGiveUpClicked = (): void => {
this.props.onGiveUp();
this.props.onFinished(false);
};
render() {
public render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const errorList = this.props.unknownProfileUsers
@ -55,11 +57,12 @@ export default class AskInviteAnywayDialog extends React.Component {
return (
<BaseDialog className='mx_RetryInvitesDialog'
onFinished={this._onGiveUpClicked}
onFinished={this.onGiveUpClicked}
title={_t('The following users may not exist')}
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>
<ul>
{ errorList }
@ -67,13 +70,13 @@ export default class AskInviteAnywayDialog extends React.Component {
</div>
<div className="mx_Dialog_buttons">
<button onClick={this._onGiveUpClicked}>
<button onClick={this.onGiveUpClicked}>
{ _t('Close') }
</button>
<button onClick={this._onInviteNeverWarnClicked}>
<button onClick={this.onInviteNeverWarnClicked}>
{ _t('Invite anyway and never warn me again') }
</button>
<button onClick={this._onInviteClicked} autoFocus={true}>
<button onClick={this.onInviteClicked} autoFocus={true}>
{ _t('Invite anyway') }
</button>
</div>

View file

@ -23,10 +23,10 @@ import classNames from 'classnames';
import { Key } from '../../../Keyboard';
import AccessibleButton from '../elements/AccessibleButton';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from "../../../languageHandler";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
/*
* Basic container for modal dialogs.

View file

@ -14,28 +14,28 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useState} from "react";
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 { IDialogProps } from "./IDialogProps";
import SettingsStore from "../../../settings/SettingsStore";
import {submitFeedback} from "../../../rageshake/submit-rageshake";
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 {USER_LABS_TAB} from "./UserSettingsDialog";
import { Action } from "../../../dispatcher/actions";
import { UserTab } from "./UserSettingsDialog";
interface IProps extends IDialogProps {
featureId: string;
}
const BetaFeedbackDialog: React.FC<IProps> = ({featureId, onFinished}) => {
const BetaFeedbackDialog: React.FC<IProps> = ({ featureId, onFinished }) => {
const info = SettingsStore.getBetaInfo(featureId);
const [comment, setComment] = useState("");
@ -44,7 +44,12 @@ const BetaFeedbackDialog: React.FC<IProps> = ({featureId, onFinished}) => {
const sendFeedback = async (ok: boolean) => {
if (!ok) return onFinished(false);
submitFeedback(SdkConfig.get().bug_report_endpoint_url, info.feedbackLabel, comment, canContact);
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, {
@ -70,7 +75,7 @@ const BetaFeedbackDialog: React.FC<IProps> = ({featureId, onFinished}) => {
onFinished(false);
defaultDispatcher.dispatch({
action: Action.ViewUserSettings,
initialTabId: USER_LABS_TAB,
initialTabId: UserTab.Labs,
});
}}>
{ _t("To leave the beta, visit your settings.") }

View file

@ -18,17 +18,35 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
import sendBugReport, {downloadBugReport} from '../../../rageshake/submit-rageshake';
import sendBugReport, { downloadBugReport } from '../../../rageshake/submit-rageshake';
import AccessibleButton from "../elements/AccessibleButton";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
onFinished: (success: boolean) => void;
initialText?: string;
label?: string;
}
interface IState {
sendLogs: boolean;
busy: boolean;
err: string;
issueUrl: string;
text: string;
progress: string;
downloadBusy: boolean;
downloadProgress: string;
}
@replaceableComponent("views.dialogs.BugReportDialog")
export default class BugReportDialog extends React.Component {
export default class BugReportDialog extends React.Component<IProps, IState> {
private unmounted: boolean;
constructor(props) {
super(props);
this.state = {
@ -41,25 +59,18 @@ export default class BugReportDialog extends React.Component {
downloadBusy: false,
downloadProgress: null,
};
this._unmounted = false;
this._onSubmit = this._onSubmit.bind(this);
this._onCancel = this._onCancel.bind(this);
this._onTextChange = this._onTextChange.bind(this);
this._onIssueUrlChange = this._onIssueUrlChange.bind(this);
this._onSendLogsChange = this._onSendLogsChange.bind(this);
this._sendProgressCallback = this._sendProgressCallback.bind(this);
this._downloadProgressCallback = this._downloadProgressCallback.bind(this);
this.unmounted = false;
}
componentWillUnmount() {
this._unmounted = true;
public componentWillUnmount() {
this.unmounted = true;
}
_onCancel(ev) {
private onCancel = (): void => {
this.props.onFinished(false);
}
};
_onSubmit(ev) {
private onSubmit = (): void => {
if ((!this.state.text || !this.state.text.trim()) && (!this.state.issueUrl || !this.state.issueUrl.trim())) {
this.setState({
err: _t("Please tell us what went wrong or, better, create a GitHub issue that describes the problem."),
@ -72,15 +83,15 @@ export default class BugReportDialog extends React.Component {
(this.state.issueUrl.length > 0 ? this.state.issueUrl : 'No issue link given');
this.setState({ busy: true, progress: null, err: null });
this._sendProgressCallback(_t("Preparing to send logs"));
this.sendProgressCallback(_t("Preparing to send logs"));
sendBugReport(SdkConfig.get().bug_report_endpoint_url, {
userText,
sendLogs: true,
progressCallback: this._sendProgressCallback,
progressCallback: this.sendProgressCallback,
label: this.props.label,
}).then(() => {
if (!this._unmounted) {
if (!this.unmounted) {
this.props.onFinished(false);
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
// N.B. first param is passed to piwik and so doesn't want i18n
@ -91,7 +102,7 @@ export default class BugReportDialog extends React.Component {
});
}
}, (err) => {
if (!this._unmounted) {
if (!this.unmounted) {
this.setState({
busy: false,
progress: null,
@ -99,16 +110,16 @@ export default class BugReportDialog extends React.Component {
});
}
});
}
};
_onDownload = async (ev) => {
private onDownload = async (): Promise<void> => {
this.setState({ downloadBusy: true });
this._downloadProgressCallback(_t("Preparing to download logs"));
this.downloadProgressCallback(_t("Preparing to download logs"));
try {
await downloadBugReport({
sendLogs: true,
progressCallback: this._downloadProgressCallback,
progressCallback: this.downloadProgressCallback,
label: this.props.label,
});
@ -117,7 +128,7 @@ export default class BugReportDialog extends React.Component {
downloadProgress: null,
});
} catch (err) {
if (!this._unmounted) {
if (!this.unmounted) {
this.setState({
downloadBusy: false,
downloadProgress: _t("Failed to send logs: ") + `${err.message}`,
@ -126,33 +137,29 @@ export default class BugReportDialog extends React.Component {
}
};
_onTextChange(ev) {
this.setState({ text: ev.target.value });
}
private onTextChange = (ev: React.FormEvent<HTMLTextAreaElement>): void => {
this.setState({ text: ev.currentTarget.value });
};
_onIssueUrlChange(ev) {
this.setState({ issueUrl: ev.target.value });
}
private onIssueUrlChange = (ev: React.FormEvent<HTMLInputElement>): void => {
this.setState({ issueUrl: ev.currentTarget.value });
};
_onSendLogsChange(ev) {
this.setState({ sendLogs: ev.target.checked });
}
_sendProgressCallback(progress) {
if (this._unmounted) {
private sendProgressCallback = (progress: string): void => {
if (this.unmounted) {
return;
}
this.setState({progress: progress});
}
this.setState({ progress });
};
_downloadProgressCallback(downloadProgress) {
if (this._unmounted) {
private downloadProgressCallback = (downloadProgress: string): void => {
if (this.unmounted) {
return;
}
this.setState({ downloadProgress });
}
};
render() {
public render() {
const Loader = sdk.getComponent("elements.Spinner");
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
@ -183,7 +190,7 @@ export default class BugReportDialog extends React.Component {
}
return (
<BaseDialog className="mx_BugReportDialog" onFinished={this._onCancel}
<BaseDialog className="mx_BugReportDialog" onFinished={this.onCancel}
title={_t('Submit debug logs')}
contentId='mx_Dialog_content'
>
@ -213,7 +220,7 @@ export default class BugReportDialog extends React.Component {
</b></p>
<div className="mx_BugReportDialog_download">
<AccessibleButton onClick={this._onDownload} kind="link" disabled={this.state.downloadBusy}>
<AccessibleButton onClick={this.onDownload} kind="link" disabled={this.state.downloadBusy}>
{ _t("Download logs") }
</AccessibleButton>
{this.state.downloadProgress && <span>{this.state.downloadProgress} ...</span>}
@ -223,7 +230,7 @@ export default class BugReportDialog extends React.Component {
type="text"
className="mx_BugReportDialog_field_input"
label={_t("GitHub issue")}
onChange={this._onIssueUrlChange}
onChange={this.onIssueUrlChange}
value={this.state.issueUrl}
placeholder="https://github.com/vector-im/element-web/issues/..."
/>
@ -232,7 +239,7 @@ export default class BugReportDialog extends React.Component {
element="textarea"
label={_t("Notes")}
rows={5}
onChange={this._onTextChange}
onChange={this.onTextChange}
value={this.state.text}
placeholder={_t(
"If there is additional context that would help in " +
@ -245,17 +252,12 @@ export default class BugReportDialog extends React.Component {
{error}
</div>
<DialogButtons primaryButton={_t("Send logs")}
onPrimaryButtonClick={this._onSubmit}
onPrimaryButtonClick={this.onSubmit}
focus={true}
onCancel={this._onCancel}
onCancel={this.onCancel}
disabled={this.state.busy}
/>
</BaseDialog>
);
}
}
BugReportDialog.propTypes = {
onFinished: PropTypes.func.isRequired,
initialText: PropTypes.string,
};

View file

@ -16,21 +16,26 @@ Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
*/
import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import request from 'browser-request';
import { _t } from '../../../languageHandler';
interface IProps {
newVersion: string;
version: string;
onFinished: (success: boolean) => void;
}
const REPOS = ['vector-im/element-web', 'matrix-org/matrix-react-sdk', 'matrix-org/matrix-js-sdk'];
export default class ChangelogDialog extends React.Component {
export default class ChangelogDialog extends React.Component<IProps> {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {
public componentDidMount() {
const version = this.props.newVersion.split('-');
const version2 = this.props.version.split('-');
if (version == null || version2 == null) return;
@ -44,12 +49,12 @@ export default class ChangelogDialog extends React.Component {
this.setState({ [REPOS[i]]: response.statusText });
return;
}
this.setState({[REPOS[i]]: JSON.parse(body).commits});
this.setState({ [REPOS[i]]: JSON.parse(body).commits });
});
}
}
_elementsForCommit(commit) {
private elementsForCommit(commit): JSX.Element {
return (
<li key={commit.sha} className="mx_ChangelogDialog_li">
<a href={commit.html_url} target="_blank" rel="noreferrer noopener">
@ -59,7 +64,7 @@ export default class ChangelogDialog extends React.Component {
);
}
render() {
public render() {
const Spinner = sdk.getComponent('views.elements.Spinner');
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
@ -72,7 +77,7 @@ export default class ChangelogDialog extends React.Component {
msg: this.state[repo],
});
} else {
content = this.state[repo].map(this._elementsForCommit);
content = this.state[repo].map(this.elementsForCommit);
}
return (
<div key={repo}>
@ -88,7 +93,6 @@ export default class ChangelogDialog extends React.Component {
</div>
);
return (
<QuestionDialog
title={_t("Changelog")}
@ -99,9 +103,3 @@ export default class ChangelogDialog extends React.Component {
);
}
}
ChangelogDialog.propTypes = {
version: PropTypes.string.isRequired,
newVersion: PropTypes.string.isRequired,
onFinished: PropTypes.func.isRequired,
};

View file

@ -26,12 +26,12 @@ import SdkConfig from "../../../SdkConfig";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import InviteDialog from "./InviteDialog";
import BaseAvatar from "../avatars/BaseAvatar";
import {inviteMultipleToRoom, showAnyInviteErrors} from "../../../RoomInvite";
import { inviteMultipleToRoom, showAnyInviteErrors } from "../../../RoomInvite";
import StyledCheckbox from "../elements/StyledCheckbox";
import Modal from "../../../Modal";
import ErrorDialog from "./ErrorDialog";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
interface IProps extends IDialogProps {
roomId: string;
@ -86,7 +86,7 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent<
ev.preventDefault();
ev.stopPropagation();
this.setState({busy: true});
this.setState({ busy: true });
try {
const targets = [...this.state.emailTargets, ...this.state.userTargets];
const result = await inviteMultipleToRoom(this.props.roomId, targets);
@ -95,10 +95,10 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent<
if (success) {
this.props.onFinished(true);
} else {
this.setState({busy: false});
this.setState({ busy: false });
}
} catch (e) {
this.setState({busy: false});
this.setState({ busy: false });
console.error(e);
Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, {
title: _t("Failed to invite"),
@ -114,7 +114,7 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent<
} else {
targets[index] = ev.target.value;
}
this.setState({emailTargets: targets});
this.setState({ emailTargets: targets });
};
private onAddressBlur = (index: number) => {
@ -122,12 +122,12 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent<
if (index >= targets.length) return; // not important
if (targets[index].trim() === "") {
targets.splice(index, 1);
this.setState({emailTargets: targets});
this.setState({ emailTargets: targets });
}
};
private onShowPeopleClick = () => {
this.setState({showPeople: !this.state.showPeople});
this.setState({ showPeople: !this.state.showPeople });
};
private setPersonToggle = (person: IPerson, selected: boolean) => {
@ -137,7 +137,7 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent<
} else if (!selected && targets.includes(person.userId)) {
targets.splice(targets.indexOf(person.userId), 1);
}
this.setState({userTargets: targets});
this.setState({ userTargets: targets });
};
private renderPerson(person: IPerson, key: any) {
@ -165,7 +165,7 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent<
}
private onShowMorePeople = () => {
this.setState({numPeople: this.state.numPeople + 5}); // arbitrary increase
this.setState({ numPeople: this.state.numPeople + 5 }); // arbitrary increase
};
public render() {
@ -214,7 +214,7 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent<
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")}
</AccessibleButton>
@ -225,14 +225,14 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent<
let buttonText = _t("Skip");
const targetCount = this.state.userTargets.length + this.state.emailTargets.length;
if (targetCount > 0) {
buttonText = _t("Send %(count)s invites", {count: targetCount});
buttonText = _t("Send %(count)s invites", { count: targetCount });
}
return (
<BaseDialog
className="mx_CommunityPrototypeInviteDialog"
onFinished={this.props.onFinished}
title={_t("Invite people to join %(communityName)s", {communityName: this.props.communityName})}
title={_t("Invite people to join %(communityName)s", { communityName: this.props.communityName })}
>
<form onSubmit={this.onSubmit}>
<div className="mx_Dialog_content">

View file

@ -17,7 +17,17 @@ limitations under the License.
import React from 'react';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
redact: () => Promise<void>;
onFinished: (success: boolean) => void;
}
interface IState {
isRedacting: boolean;
redactionErrorCode: string | number;
}
/*
* A dialog for confirming a redaction.
@ -32,7 +42,7 @@ import {replaceableComponent} from "../../../utils/replaceableComponent";
* To avoid this, we keep the dialog open as long as /redact is in progress.
*/
@replaceableComponent("views.dialogs.ConfirmAndWaitRedactDialog")
export default class ConfirmAndWaitRedactDialog extends React.PureComponent {
export default class ConfirmAndWaitRedactDialog extends React.PureComponent<IProps, IState> {
constructor(props) {
super(props);
this.state = {
@ -41,16 +51,16 @@ export default class ConfirmAndWaitRedactDialog extends React.PureComponent {
};
}
onParentFinished = async (proceed) => {
public onParentFinished = async (proceed: boolean): Promise<void> => {
if (proceed) {
this.setState({isRedacting: true});
this.setState({ isRedacting: true });
try {
await this.props.redact();
this.props.onFinished(true);
} catch (error) {
const code = error.errcode || error.statusCode;
if (typeof code !== "undefined") {
this.setState({redactionErrorCode: code});
this.setState({ redactionErrorCode: code });
} else {
this.props.onFinished(true);
}
@ -60,7 +70,7 @@ export default class ConfirmAndWaitRedactDialog extends React.PureComponent {
}
};
render() {
public render() {
if (this.state.isRedacting) {
if (this.state.redactionErrorCode) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@ -69,7 +79,7 @@ export default class ConfirmAndWaitRedactDialog extends React.PureComponent {
<ErrorDialog
onFinished={this.props.onFinished}
title={_t('Error')}
description={_t('You cannot delete this message. (%(code)s)', {code})}
description={_t('You cannot delete this message. (%(code)s)', { code })}
/>
);
} else {

View file

@ -17,13 +17,17 @@ limitations under the License.
import React from 'react';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
onFinished: (success: boolean) => void;
}
/*
* A dialog for confirming a redaction.
*/
@replaceableComponent("views.dialogs.ConfirmRedactDialog")
export default class ConfirmRedactDialog extends React.Component {
export default class ConfirmRedactDialog extends React.Component<IProps> {
render() {
const TextInputDialog = sdk.getComponent('views.dialogs.TextInputDialog');
return (

View file

@ -15,13 +15,31 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk/src/client';
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import { GroupMemberType } from '../../../groups';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
interface IProps {
// matrix-js-sdk (room) member object. Supply either this or 'groupMember'
member: RoomMember;
// group member object. Supply either this or 'member'
groupMember: GroupMemberType;
// needed if a group member is specified
matrixClient?: MatrixClient,
action: string; // eg. 'Ban'
title: string; // eg. 'Ban this user?'
// Whether to display a text field for a reason
// If true, the second argument to onFinished will
// be the string entered.
askReason?: boolean;
danger?: boolean;
onFinished: (success: boolean, reason?: string) => void;
}
/*
* A dialog for confirming an operation on another user.
@ -32,53 +50,23 @@ import {mediaFromMxc} from "../../../customisations/Media";
* Also tweaks the style for 'dangerous' actions (albeit only with colour)
*/
@replaceableComponent("views.dialogs.ConfirmUserActionDialog")
export default class ConfirmUserActionDialog extends React.Component {
static propTypes = {
// matrix-js-sdk (room) member object. Supply either this or 'groupMember'
member: PropTypes.object,
// group member object. Supply either this or 'member'
groupMember: GroupMemberType,
// needed if a group member is specified
matrixClient: PropTypes.instanceOf(MatrixClient),
action: PropTypes.string.isRequired, // eg. 'Ban'
title: PropTypes.string.isRequired, // eg. 'Ban this user?'
// Whether to display a text field for a reason
// If true, the second argument to onFinished will
// be the string entered.
askReason: PropTypes.bool,
danger: PropTypes.bool,
onFinished: PropTypes.func.isRequired,
};
export default class ConfirmUserActionDialog extends React.Component<IProps> {
private reasonField: React.RefObject<HTMLInputElement> = React.createRef();
static defaultProps = {
danger: false,
askReason: false,
};
constructor(props) {
super(props);
this._reasonField = null;
}
onOk = () => {
let reason;
if (this._reasonField) {
reason = this._reasonField.value;
}
this.props.onFinished(true, reason);
public onOk = (): void => {
this.props.onFinished(true, this.reasonField.current?.value);
};
onCancel = () => {
public onCancel = (): void => {
this.props.onFinished(false);
};
_collectReasonField = e => {
this._reasonField = e;
};
render() {
public render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
@ -92,7 +80,7 @@ export default class ConfirmUserActionDialog extends React.Component {
<div>
<form onSubmit={this.onOk}>
<input className="mx_ConfirmUserActionDialog_reasonField"
ref={this._collectReasonField}
ref={this.reasonField}
placeholder={_t("Reason")}
autoFocus={true}
/>

View file

@ -15,22 +15,21 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler";
import { _t } from "../../../languageHandler";
import * as sdk from "../../../index";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
onFinished: (success: boolean) => void;
}
@replaceableComponent("views.dialogs.ConfirmWipeDeviceDialog")
export default class ConfirmWipeDeviceDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
};
_onConfirm = () => {
export default class ConfirmWipeDeviceDialog extends React.Component<IProps> {
private onConfirm = (): void => {
this.props.onFinished(true);
};
_onDecline = () => {
private onDecline = (): void => {
this.props.onFinished(false);
};
@ -55,10 +54,10 @@ export default class ConfirmWipeDeviceDialog extends React.Component {
</div>
<DialogButtons
primaryButton={_t("Clear all data")}
onPrimaryButtonClick={this._onConfirm}
onPrimaryButtonClick={this.onConfirm}
primaryButtonClass="danger"
cancelButton={_t("Cancel")}
onCancel={this._onDecline}
onCancel={this.onDecline}
/>
</BaseDialog>
);

View file

@ -23,9 +23,9 @@ import AccessibleButton from "../elements/AccessibleButton";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import InfoTooltip from "../elements/InfoTooltip";
import dis from "../../../dispatcher/dispatcher";
import {showCommunityRoomInviteDialog} from "../../../RoomInvite";
import { showCommunityRoomInviteDialog } from "../../../RoomInvite";
import GroupStore from "../../../stores/GroupStore";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps extends IDialogProps {
}
@ -58,7 +58,7 @@ export default class CreateCommunityPrototypeDialog extends React.PureComponent<
private onNameChange = (ev: ChangeEvent<HTMLInputElement>) => {
const localpart = (ev.target.value || "").toLowerCase().replace(/[^a-z0-9.\-_]/g, '-');
this.setState({name: ev.target.value, localpart});
this.setState({ name: ev.target.value, localpart });
};
private onSubmit = async (ev) => {
@ -69,7 +69,7 @@ export default class CreateCommunityPrototypeDialog extends React.PureComponent<
// We'll create the community now to see if it's taken, leaving it active in
// the background for the user to look at while they invite people.
this.setState({busy: true});
this.setState({ busy: true });
try {
let avatarUrl = ''; // must be a string for synapse to accept it
if (this.state.avatarFile) {
@ -85,7 +85,7 @@ export default class CreateCommunityPrototypeDialog extends React.PureComponent<
});
// Ensure the tag gets selected now that we've created it
dis.dispatch({action: 'deselect_tags'}, true);
dis.dispatch({ action: 'deselect_tags' }, true);
dis.dispatch({
action: 'select_tag',
tag: result.group_id,
@ -123,13 +123,13 @@ export default class CreateCommunityPrototypeDialog extends React.PureComponent<
private onAvatarChanged = (e: ChangeEvent<HTMLInputElement>) => {
if (!e.target.files || !e.target.files.length) {
this.setState({avatarFile: null});
this.setState({ avatarFile: null });
} else {
this.setState({busy: true});
this.setState({ busy: true });
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = (ev: ProgressEvent<FileReader>) => {
this.setState({avatarFile: file, busy: false, avatarPreview: ev.target.result as string});
this.setState({ avatarFile: file, busy: false, avatarPreview: ev.target.result as string });
};
reader.readAsDataURL(file);
}
@ -175,7 +175,7 @@ export default class CreateCommunityPrototypeDialog extends React.PureComponent<
let preview = <img src={this.state.avatarPreview} className="mx_CreateCommunityPrototypeDialog_avatar" />;
if (!this.state.avatarPreview) {
preview = <div className="mx_CreateCommunityPrototypeDialog_placeholderAvatar" />
preview = <div className="mx_CreateCommunityPrototypeDialog_placeholderAvatar" />;
}
return (
@ -204,7 +204,7 @@ export default class CreateCommunityPrototypeDialog extends React.PureComponent<
</div>
<div className="mx_CreateCommunityPrototypeDialog_colAvatar">
<input
type="file" style={{display: "none"}}
type="file" style={{ display: "none" }}
ref={this.avatarUploadRef} accept="image/*"
onChange={this.onAvatarChanged}
/>

View file

@ -15,44 +15,51 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
onFinished: (success: boolean) => void;
}
interface IState {
groupName: string;
groupId: string;
groupIdError: string;
creating: boolean;
createError: Error;
}
@replaceableComponent("views.dialogs.CreateGroupDialog")
export default class CreateGroupDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
};
state = {
export default class CreateGroupDialog extends React.Component<IProps, IState> {
public state = {
groupName: '',
groupId: '',
groupError: null,
groupIdError: '',
creating: false,
createError: null,
};
_onGroupNameChange = e => {
private onGroupNameChange = (e: React.FormEvent<HTMLInputElement>): void => {
this.setState({
groupName: e.target.value,
groupName: e.currentTarget.value,
});
};
_onGroupIdChange = e => {
private onGroupIdChange = (e: React.FormEvent<HTMLInputElement>): void => {
this.setState({
groupId: e.target.value,
groupId: e.currentTarget.value,
});
};
_onGroupIdBlur = e => {
this._checkGroupId();
private onGroupIdBlur = (): void => {
this.checkGroupId();
};
_checkGroupId(e) {
private checkGroupId() {
let error = null;
if (!this.state.groupId) {
error = _t("Community IDs cannot be empty.");
@ -67,16 +74,16 @@ export default class CreateGroupDialog extends React.Component {
return error;
}
_onFormSubmit = e => {
private onFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (this._checkGroupId()) return;
if (this.checkGroupId()) return;
const profile = {};
const profile: any = {};
if (this.state.groupName !== '') {
profile.name = this.state.groupName;
}
this.setState({creating: true});
this.setState({ creating: true });
MatrixClientPeg.get().createGroup({
localpart: this.state.groupId,
profile: profile,
@ -88,9 +95,9 @@ export default class CreateGroupDialog extends React.Component {
});
this.props.onFinished(true);
}).catch((e) => {
this.setState({createError: e});
this.setState({ createError: e });
}).finally(() => {
this.setState({creating: false});
this.setState({ creating: false });
});
};
@ -121,7 +128,7 @@ export default class CreateGroupDialog extends React.Component {
<BaseDialog className="mx_CreateGroupDialog" onFinished={this.props.onFinished}
title={_t('Create Community')}
>
<form onSubmit={this._onFormSubmit}>
<form onSubmit={this.onFormSubmit}>
<div className="mx_Dialog_content">
<div className="mx_CreateGroupDialog_inputRow">
<div className="mx_CreateGroupDialog_label">
@ -129,9 +136,9 @@ export default class CreateGroupDialog extends React.Component {
</div>
<div>
<input id="groupname" className="mx_CreateGroupDialog_input"
autoFocus={true} size="64"
autoFocus={true} size={64}
placeholder={_t('Example')}
onChange={this._onGroupNameChange}
onChange={this.onGroupNameChange}
value={this.state.groupName}
/>
</div>
@ -144,10 +151,10 @@ export default class CreateGroupDialog extends React.Component {
<span className="mx_CreateGroupDialog_prefix">+</span>
<input id="groupid"
className="mx_CreateGroupDialog_input mx_CreateGroupDialog_input_hasPrefixAndSuffix"
size="32"
size={32}
placeholder={_t('example')}
onChange={this._onGroupIdChange}
onBlur={this._onGroupIdBlur}
onChange={this.onGroupIdChange}
onBlur={this.onGroupIdBlur}
value={this.state.groupId}
/>
<span className="mx_CreateGroupDialog_suffix">

View file

@ -137,9 +137,9 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
if (activeElement) {
activeElement.blur();
}
await this.nameField.current.validate({allowEmpty: false});
await this.nameField.current.validate({ allowEmpty: false });
if (this.aliasField.current) {
await this.aliasField.current.validate({allowEmpty: false});
await this.aliasField.current.validate({ allowEmpty: false });
}
// Validation and state updates are async, so we need to wait for them to complete
// first. Queue a `setState` callback and wait for it to resolve.
@ -194,7 +194,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
private onNameValidate = async (fieldState: IFieldState) => {
const result = await CreateRoomDialog.validateRoomName(fieldState);
this.setState({nameIsValid: result.valid});
this.setState({ nameIsValid: result.valid });
return result;
};
@ -276,7 +276,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
let title = this.state.isPublic ? _t('Create a public room') : _t('Create a private room');
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
const name = CommunityPrototypeStore.instance.getSelectedCommunityName();
title = _t("Create a room in %(communityName)s", {communityName: name});
title = _t("Create a room in %(communityName)s", { communityName: name });
}
return (
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished}
@ -313,7 +313,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
<LabelledToggleSwitch
label={_t(
"Block anyone not part of %(serverName)s from ever joining this room.",
{serverName: MatrixClientPeg.getHomeserverName()},
{ serverName: MatrixClientPeg.getHomeserverName() },
)}
onChange={this.onNoFederateChange}
value={this.state.noFederate}

View file

@ -22,7 +22,11 @@ import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal';
export default (props) => {
interface IProps {
onFinished: (success: boolean) => void;
}
export default (props: IProps) => {
const brand = SdkConfig.get().brand;
const _onLogoutClicked = () => {
@ -39,8 +43,8 @@ export default (props) => {
focus: false,
onFinished: (doLogout) => {
if (doLogout) {
dis.dispatch({action: 'logout'});
props.onFinished();
dis.dispatch({ action: 'logout' });
props.onFinished(true);
}
},
});

View file

@ -16,20 +16,36 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import Analytics from '../../../Analytics';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import * as Lifecycle from '../../../Lifecycle';
import { _t } from '../../../languageHandler';
import InteractiveAuth, {ERROR_USER_CANCELLED} from "../../structures/InteractiveAuth";
import {DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry} from "../auth/InteractiveAuthEntryComponents";
import InteractiveAuth, { ERROR_USER_CANCELLED } from "../../structures/InteractiveAuth";
import { DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry } from "../auth/InteractiveAuthEntryComponents";
import StyledCheckbox from "../elements/StyledCheckbox";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
onFinished: (success: boolean) => void;
}
interface IState {
shouldErase: boolean;
errStr: string;
authData: any; // for UIA
authEnabled: boolean; // see usages for information
// A few strings that are passed to InteractiveAuth for design or are displayed
// next to the InteractiveAuth component.
bodyText: string;
continueText: string;
continueKind: string;
}
@replaceableComponent("views.dialogs.DeactivateAccountDialog")
export default class DeactivateAccountDialog extends React.Component {
export default class DeactivateAccountDialog extends React.Component<IProps, IState> {
constructor(props) {
super(props);
@ -46,10 +62,10 @@ export default class DeactivateAccountDialog extends React.Component {
continueKind: null,
};
this._initAuth(/* shouldErase= */false);
this.initAuth(/* shouldErase= */false);
}
_onStagePhaseChange = (stage, phase) => {
private onStagePhaseChange = (stage: string, phase: string): void => {
const dialogAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: {
body: _t("Confirm your account deactivation by using Single Sign On to prove your identity."),
@ -84,22 +100,22 @@ export default class DeactivateAccountDialog extends React.Component {
if (phaseAesthetics && phaseAesthetics.continueText) continueText = phaseAesthetics.continueText;
if (phaseAesthetics && phaseAesthetics.continueKind) continueKind = phaseAesthetics.continueKind;
}
this.setState({bodyText, continueText, continueKind});
this.setState({ bodyText, continueText, continueKind });
};
_onUIAuthFinished = (success, result, extra) => {
private onUIAuthFinished = (success: boolean, result: Error) => {
if (success) return; // great! makeRequest() will be called too.
if (result === ERROR_USER_CANCELLED) {
this._onCancel();
this.onCancel();
return;
}
console.error("Error during UI Auth:", {result, extra});
this.setState({errStr: _t("There was a problem communicating with the server. Please try again.")});
console.error("Error during UI Auth:", { result });
this.setState({ errStr: _t("There was a problem communicating with the server. Please try again.") });
};
_onUIAuthComplete = (auth) => {
private onUIAuthComplete = (auth: any): void => {
MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase).then(r => {
// Deactivation worked - logout & close this dialog
Analytics.trackEvent('Account', 'Deactivate Account');
@ -107,13 +123,13 @@ export default class DeactivateAccountDialog extends React.Component {
this.props.onFinished(true);
}).catch(e => {
console.error(e);
this.setState({errStr: _t("There was a problem communicating with the server. Please try again.")});
this.setState({ errStr: _t("There was a problem communicating with the server. Please try again.") });
});
};
_onEraseFieldChange = (ev) => {
private onEraseFieldChange = (ev: React.FormEvent<HTMLInputElement>): void => {
this.setState({
shouldErase: ev.target.checked,
shouldErase: ev.currentTarget.checked,
// Disable the auth form because we're going to have to reinitialize the auth
// information. We do this because we can't modify the parameters in the UIA
@ -123,32 +139,32 @@ export default class DeactivateAccountDialog extends React.Component {
});
// As mentioned above, set up for auth again to get updated UIA session info
this._initAuth(/* shouldErase= */ev.target.checked);
this.initAuth(/* shouldErase= */ev.currentTarget.checked);
};
_onCancel() {
private onCancel(): void {
this.props.onFinished(false);
}
_initAuth(shouldErase) {
private initAuth(shouldErase: boolean): void {
MatrixClientPeg.get().deactivateAccount(null, shouldErase).then(r => {
// If we got here, oops. The server didn't require any auth.
// Our application lifecycle will catch the error and do the logout bits.
// We'll try to log something in an vain attempt to record what happened (storage
// is also obliterated on logout).
console.warn("User's account got deactivated without confirmation: Server had no auth");
this.setState({errStr: _t("Server did not require any authentication")});
this.setState({ errStr: _t("Server did not require any authentication") });
}).catch(e => {
if (e && e.httpStatus === 401 && e.data) {
// Valid UIA response
this.setState({authData: e.data, authEnabled: true});
this.setState({ authData: e.data, authEnabled: true });
} else {
this.setState({errStr: _t("Server did not return valid authentication information.")});
this.setState({ errStr: _t("Server did not return valid authentication information.") });
}
});
}
render() {
public render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
let error = null;
@ -166,9 +182,9 @@ export default class DeactivateAccountDialog extends React.Component {
<InteractiveAuth
matrixClient={MatrixClientPeg.get()}
authData={this.state.authData}
makeRequest={this._onUIAuthComplete}
onAuthFinished={this._onUIAuthFinished}
onStagePhaseChange={this._onStagePhaseChange}
makeRequest={this.onUIAuthComplete}
onAuthFinished={this.onUIAuthFinished}
onStagePhaseChange={this.onStagePhaseChange}
continueText={this.state.continueText}
continueKind={this.state.continueKind}
/>
@ -214,7 +230,7 @@ export default class DeactivateAccountDialog extends React.Component {
<p>
<StyledCheckbox
checked={this.state.shouldErase}
onChange={this._onEraseFieldChange}
onChange={this.onEraseFieldChange}
>
{_t(
"Please forget all messages I have sent when my account is deactivated " +
@ -235,7 +251,3 @@ export default class DeactivateAccountDialog extends React.Component {
);
}
}
DeactivateAccountDialog.propTypes = {
onFinished: PropTypes.func.isRequired,
};

View file

@ -62,13 +62,13 @@ abstract class GenericEditor<
} else {
this.props.onBack();
}
}
};
protected onChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
// @ts-ignore: Unsure how to convince TS this is okay when the state
// type can be extended.
this.setState({[e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value});
}
this.setState({ [e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value });
};
protected abstract send();
@ -119,7 +119,7 @@ export class SendCustomEvent extends GenericEditor<ISendCustomEventProps, ISendC
constructor(props) {
super(props);
const {eventType, stateKey, evContent} = Object.assign({
const { eventType, stateKey, evContent } = Object.assign({
eventType: '',
stateKey: '',
evContent: '{\n\n}',
@ -158,7 +158,7 @@ export class SendCustomEvent extends GenericEditor<ISendCustomEventProps, ISendC
message = _t('Failed to send custom event.') + ' (' + e.toString() + ')';
}
this.setState({ message });
}
};
render() {
if (this.state.message) {
@ -187,7 +187,7 @@ export class SendCustomEvent extends GenericEditor<ISendCustomEventProps, ISendC
<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"}}>
{ showTglFlip && <div style={{ float: "right" }}>
<input id="isStateEvent" className="mx_DevTools_tgl mx_DevTools_tgl-flip"
type="checkbox"
checked={this.state.isStateEvent}
@ -228,7 +228,7 @@ class SendAccountData extends GenericEditor<ISendAccountDataProps, ISendAccountD
constructor(props) {
super(props);
const {eventType, evContent} = Object.assign({
const { eventType, evContent } = Object.assign({
eventType: '',
evContent: '{\n\n}',
}, this.props.inputs);
@ -264,7 +264,7 @@ class SendAccountData extends GenericEditor<ISendAccountDataProps, ISendAccountD
message = _t('Failed to send custom event.') + ' (' + e.toString() + ')';
}
this.setState({ message });
}
};
render() {
if (this.state.message) {
@ -287,7 +287,7 @@ class SendAccountData extends GenericEditor<ISendAccountDataProps, ISendAccountD
<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"}}>
{ !this.state.message && <div style={{ float: "right" }}>
<input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip"
type="checkbox"
checked={this.state.isRoomAccountData}
@ -442,19 +442,19 @@ class RoomStateExplorer extends React.PureComponent<IExplorerProps, IRoomStateEx
} else {
this.props.onBack();
}
}
};
private editEv = () => {
this.setState({ editing: true });
}
};
private onQueryEventType = (filterEventType: string) => {
this.setState({ queryEventType: filterEventType });
}
};
private onQueryStateKey = (filterStateKey: string) => {
this.setState({ queryStateKey: filterStateKey });
}
};
render() {
if (this.state.event) {
@ -525,11 +525,11 @@ class RoomStateExplorer extends React.PureComponent<IExplorerProps, IRoomStateEx
}
interface IAccountDataExplorerState {
[inputId: string]: boolean | string | any;
isRoomAccountData: boolean;
event?: MatrixEvent;
editing: boolean;
queryEventType: string;
[inputId: string]: boolean | string;
}
class AccountDataExplorer extends React.PureComponent<IExplorerProps, IAccountDataExplorerState> {
@ -570,19 +570,19 @@ class AccountDataExplorer extends React.PureComponent<IExplorerProps, IAccountDa
} else {
this.props.onBack();
}
}
};
private onChange = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({[e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value});
}
this.setState({ [e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value });
};
private editEv = () => {
this.setState({ editing: true });
}
};
private onQueryEventType = (queryEventType: string) => {
this.setState({ queryEventType });
}
};
render() {
if (this.state.event) {
@ -630,7 +630,7 @@ class AccountDataExplorer extends React.PureComponent<IExplorerProps, IAccountDa
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.onBack}>{ _t('Back') }</button>
<div style={{float: "right"}}>
<div style={{ float: "right" }}>
<input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip"
type="checkbox"
checked={this.state.isRoomAccountData}
@ -676,7 +676,7 @@ class ServersInRoomList extends React.PureComponent<IExplorerProps, IServersInRo
private onQuery = (query: string) => {
this.setState({ query });
}
};
render() {
return <div>
@ -704,7 +704,7 @@ const PHASE_MAP = {
const VerificationRequestExplorer: React.FC<{
txnId: string;
request: VerificationRequest;
}> = ({txnId, request}) => {
}> = ({ txnId, request }) => {
const [, updateState] = useState();
const [timeout, setRequestTimeout] = useState(request.timeout);
@ -739,7 +739,7 @@ const VerificationRequestExplorer: React.FC<{
<dd>{JSON.stringify(request.observeOnly)}</dd>
</dl>
</div>);
}
};
class VerificationExplorer extends React.PureComponent<IExplorerProps> {
static getLabel() {
@ -751,7 +751,7 @@ class VerificationExplorer extends React.PureComponent<IExplorerProps> {
private onNewRequest = () => {
this.forceUpdate();
}
};
componentDidMount() {
const cli = this.context;
@ -766,7 +766,7 @@ class VerificationExplorer extends React.PureComponent<IExplorerProps> {
render() {
const cli = this.context;
const room = this.props.room;
const inRoomChannel = cli.crypto._inRoomVerificationRequests;
const inRoomChannel = cli.crypto.inRoomVerificationRequests;
const inRoomRequests = (inRoomChannel._requestsByRoomId || new Map()).get(room.roomId) || new Map();
return (<div>
@ -806,17 +806,17 @@ class WidgetExplorer extends React.Component<IExplorerProps, IWidgetExplorerStat
};
private onQueryChange = (query: string) => {
this.setState({query});
this.setState({ query });
};
private onEditWidget = (widget: IApp) => {
this.setState({editWidget: widget});
this.setState({ editWidget: widget });
};
private onBack = () => {
const widgets = WidgetStore.instance.getApps(this.props.room.roomId);
if (this.state.editWidget && widgets.includes(this.state.editWidget)) {
this.setState({editWidget: null});
this.setState({ editWidget: null });
} else {
this.props.onBack();
}
@ -908,22 +908,22 @@ class SettingsExplorer extends React.PureComponent<IExplorerProps, ISettingsExpl
}
private onQueryChange = (ev: ChangeEvent<HTMLInputElement>) => {
this.setState({query: ev.target.value});
this.setState({ query: ev.target.value });
};
private onExplValuesEdit = (ev: ChangeEvent<HTMLTextAreaElement>) => {
this.setState({explicitValues: ev.target.value});
this.setState({ explicitValues: ev.target.value });
};
private onExplRoomValuesEdit = (ev: ChangeEvent<HTMLTextAreaElement>) => {
this.setState({explicitRoomValues: ev.target.value});
this.setState({ explicitRoomValues: ev.target.value });
};
private onBack = () => {
if (this.state.editSetting) {
this.setState({editSetting: null});
this.setState({ editSetting: null });
} else if (this.state.viewSetting) {
this.setState({viewSetting: null});
this.setState({ viewSetting: null });
} else {
this.props.onBack();
}
@ -931,7 +931,7 @@ class SettingsExplorer extends React.PureComponent<IExplorerProps, ISettingsExpl
private onViewClick = (ev: MouseEvent, settingId: string) => {
ev.preventDefault();
this.setState({viewSetting: settingId});
this.setState({ viewSetting: settingId });
};
private onEditClick = (ev: MouseEvent, settingId: string) => {
@ -1221,11 +1221,11 @@ export default class DevtoolsDialog extends React.PureComponent<IProps, IState>
private onBack = () => {
this.setState({ mode: null });
}
};
private onCancel = () => {
this.props.onFinished(false);
}
};
render() {
let body;

View file

@ -23,8 +23,8 @@ import AccessibleButton from "../elements/AccessibleButton";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
import FlairStore from "../../../stores/FlairStore";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
interface IProps extends IDialogProps {
communityId: string;
@ -60,7 +60,7 @@ export default class EditCommunityPrototypeDialog extends React.PureComponent<IP
}
private onNameChange = (ev: ChangeEvent<HTMLInputElement>) => {
this.setState({name: ev.target.value});
this.setState({ name: ev.target.value });
};
private onSubmit = async (ev) => {
@ -71,7 +71,7 @@ export default class EditCommunityPrototypeDialog extends React.PureComponent<IP
// We'll create the community now to see if it's taken, leaving it active in
// the background for the user to look at while they invite people.
this.setState({busy: true});
this.setState({ busy: true });
try {
let avatarUrl = this.state.currentAvatarUrl || ""; // must be a string for synapse to accept it
if (this.state.avatarFile) {
@ -99,13 +99,13 @@ export default class EditCommunityPrototypeDialog extends React.PureComponent<IP
private onAvatarChanged = (e: ChangeEvent<HTMLInputElement>) => {
if (!e.target.files || !e.target.files.length) {
this.setState({avatarFile: null});
this.setState({ avatarFile: null });
} else {
this.setState({busy: true});
this.setState({ busy: true });
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = (ev: ProgressEvent<FileReader>) => {
this.setState({avatarFile: file, busy: false, avatarPreview: ev.target.result as string});
this.setState({ avatarFile: file, busy: false, avatarPreview: ev.target.result as string });
};
reader.readAsDataURL(file);
}
@ -122,7 +122,7 @@ export default class EditCommunityPrototypeDialog extends React.PureComponent<IP
const url = mediaFromMxc(this.state.currentAvatarUrl).srcHttp;
preview = <img src={url} className="mx_EditCommunityPrototypeDialog_avatar" />;
} else {
preview = <div className="mx_EditCommunityPrototypeDialog_placeholderAvatar" />
preview = <div className="mx_EditCommunityPrototypeDialog_placeholderAvatar" />;
}
}
@ -144,7 +144,7 @@ export default class EditCommunityPrototypeDialog extends React.PureComponent<IP
</div>
<div className="mx_EditCommunityPrototypeDialog_rowAvatar">
<input
type="file" style={{display: "none"}}
type="file" style={{ display: "none" }}
ref={this.avatarUploadRef} accept="image/*"
onChange={this.onAvatarChanged}
/>

View file

@ -26,37 +26,37 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
onFinished: (success: boolean) => void;
title?: string;
description?: React.ReactNode;
button?: string;
focus?: boolean;
headerImage?: string;
}
interface IState {
onFinished: (success: boolean) => void;
}
@replaceableComponent("views.dialogs.ErrorDialog")
export default class ErrorDialog extends React.Component {
static propTypes = {
title: PropTypes.string,
description: PropTypes.oneOfType([
PropTypes.element,
PropTypes.string,
]),
button: PropTypes.string,
focus: PropTypes.bool,
onFinished: PropTypes.func.isRequired,
headerImage: PropTypes.string,
};
static defaultProps = {
export default class ErrorDialog extends React.Component<IProps, IState> {
public static defaultProps = {
focus: true,
title: null,
description: null,
button: null,
};
onClick = () => {
private onClick = () => {
this.props.onFinished(true);
};
render() {
public render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<BaseDialog

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useState} from 'react';
import React, { useState } from 'react';
import QuestionDialog from './QuestionDialog';
import { _t } from '../../../languageHandler';
import Field from "../elements/Field";
@ -30,7 +30,6 @@ const existingIssuesUrl = "https://github.com/vector-im/element-web/issues" +
"?q=is%3Aopen+is%3Aissue+sort%3Areactions-%2B1-desc";
const newIssueUrl = "https://github.com/vector-im/element-web/issues/new";
export default (props) => {
const [rating, setRating] = useState("");
const [comment, setComment] = useState("");

View file

@ -14,31 +14,35 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useMemo, useState, useEffect} from "react";
import React, { useMemo, useState, useEffect } from "react";
import classnames from "classnames";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import {Room} from "matrix-js-sdk/src/models/room";
import {MatrixClient} from "matrix-js-sdk/src/client";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import {_t} from "../../../languageHandler";
import { _t } from "../../../languageHandler";
import dis from "../../../dispatcher/dispatcher";
import {useSettingValue, useFeatureEnabled} from "../../../hooks/useSettings";
import {UIFeature} from "../../../settings/UIFeature";
import {Layout} from "../../../settings/Layout";
import {IDialogProps} from "./IDialogProps";
import { useSettingValue, useFeatureEnabled } from "../../../hooks/useSettings";
import { UIFeature } from "../../../settings/UIFeature";
import { Layout } from "../../../settings/Layout";
import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog";
import {avatarUrlForUser} from "../../../Avatar";
import { avatarUrlForUser } from "../../../Avatar";
import EventTile from "../rooms/EventTile";
import SearchBox from "../../structures/SearchBox";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import {Alignment} from '../elements/Tooltip';
import { Alignment } from '../elements/Tooltip';
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState";
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
import NotificationBadge from "../rooms/NotificationBadge";
import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import { sortRooms } from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
import QueryMatcher from "../../../autocomplete/QueryMatcher";
import TruncatedList from "../elements/TruncatedList";
import EntityTile from "../rooms/EntityTile";
import BaseAvatar from "../avatars/BaseAvatar";
const AVATAR_SIZE = 30;
@ -162,6 +166,7 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
});
mockEvent.sender = {
name: profileInfo.displayname || userId,
rawDisplayName: profileInfo.displayname,
userId,
getAvatarUrl: (..._) => {
return avatarUrlForUser(
@ -170,7 +175,7 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
);
},
getMxcAvatarUrl: () => profileInfo.avatar_url,
};
} as RoomMember;
const [query, setQuery] = useState("");
const lcQuery = query.toLowerCase();
@ -194,6 +199,17 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
}).match(lcQuery);
}
const [truncateAt, setTruncateAt] = useState(20);
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)} />
);
}
return <BaseDialog
title={_t("Forward message")}
className="mx_ForwardDialog"
@ -226,15 +242,20 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
<AutoHideScrollbar className="mx_ForwardList_content">
{ rooms.length > 0 ? (
<div className="mx_ForwardList_results">
{ rooms.map(room =>
<Entry
key={room.roomId}
room={room}
event={event}
matrixClient={cli}
onFinished={onFinished}
/>,
) }
<TruncatedList
truncateAt={truncateAt}
createOverflowElement={overflowTile}
getChildren={(start, end) => rooms.slice(start, end).map(room =>
<Entry
key={room.roomId}
room={room}
event={event}
matrixClient={cli}
onFinished={onFinished}
/>,
)}
getChildCount={() => rooms.length}
/>
</div>
) : <span className="mx_ForwardList_noResults">
{ _t("No results") }

View file

@ -31,7 +31,7 @@ import {
IPostmessageResponseData,
PostmessageAction,
} from "./HostSignupDialogTypes";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
const HOST_SIGNUP_KEY = "host_signup";
@ -86,7 +86,7 @@ export default class HostSignupDialog extends React.PureComponent<IProps, IState
case PostmessageAction.CloseDialog:
return this.closeDialog();
}
}
};
private maximizeDialog = () => {
this.setState({
@ -96,7 +96,7 @@ export default class HostSignupDialog extends React.PureComponent<IProps, IState
this.sendMessage({
action: PostmessageAction.Maximize,
});
}
};
private minimizeDialog = () => {
this.setState({
@ -106,7 +106,7 @@ export default class HostSignupDialog extends React.PureComponent<IProps, IState
this.sendMessage({
action: PostmessageAction.Minimize,
});
}
};
private closeDialog = async () => {
window.removeEventListener("message", this.messageHandler);
@ -114,7 +114,7 @@ export default class HostSignupDialog extends React.PureComponent<IProps, IState
PersistedElement.destroyElement("host_signup");
// Finally clear the flag in
return HostSignupStore.instance.setHostSignupActive(false);
}
};
private onCloseClick = async () => {
if (this.state.completed) {
@ -137,16 +137,16 @@ export default class HostSignupDialog extends React.PureComponent<IProps, IState
},
);
}
}
};
private sendMessage = (message: IPostmessageResponseData) => {
this.iframeRef.current.contentWindow.postMessage(message, this.config.url);
}
};
private async sendAccountDetails() {
const openIdToken = await MatrixClientPeg.get().getOpenIdToken();
if (!openIdToken || !openIdToken.access_token) {
console.warn("Failed to connect to homeserver for OpenID token.")
console.warn("Failed to connect to homeserver for OpenID token.");
this.setState({
completed: true,
error: _t("Failed to connect to your homeserver. Please close this dialog and try again."),
@ -171,7 +171,7 @@ export default class HostSignupDialog extends React.PureComponent<IProps, IState
return this.sendAccountDetails();
}
return this.closeDialog();
}
};
private onAccountDetailsRequest = () => {
const textComponent = (
@ -215,7 +215,7 @@ export default class HostSignupDialog extends React.PureComponent<IProps, IState
onFinished: this.onAccountDetailsDialogFinished,
},
);
}
};
public componentDidMount() {
window.addEventListener("message", this.messageHandler);

View file

@ -16,11 +16,11 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
const PHASE_START = 0;
const PHASE_SHOW_SAS = 1;
@ -86,9 +86,9 @@ export default class IncomingSasDialog extends React.Component {
}
_onContinueClick = () => {
this.setState({phase: PHASE_WAIT_FOR_PARTNER_TO_CONFIRM});
this.setState({ phase: PHASE_WAIT_FOR_PARTNER_TO_CONFIRM });
this.props.verifier.verify().then(() => {
this.setState({phase: PHASE_VERIFIED});
this.setState({ phase: PHASE_VERIFIED });
}).catch((e) => {
console.log("Verification failed", e);
});

View file

@ -16,11 +16,11 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler";
import { _t } from "../../../languageHandler";
import * as sdk from "../../../index";
import dis from '../../../dispatcher/dispatcher';
import {Action} from "../../../dispatcher/actions";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { Action } from "../../../dispatcher/actions";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.dialogs.IntegrationsDisabledDialog")
export default class IntegrationsDisabledDialog extends React.Component {

View file

@ -16,10 +16,10 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler";
import { _t } from "../../../languageHandler";
import SdkConfig from "../../../SdkConfig";
import * as sdk from "../../../index";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.dialogs.IntegrationsImpossibleDialog")
export default class IntegrationsImpossibleDialog extends React.Component {

View file

@ -23,9 +23,9 @@ import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import AccessibleButton from '../elements/AccessibleButton';
import {ERROR_USER_CANCELLED} from "../../structures/InteractiveAuth";
import {SSOAuthEntry} from "../auth/InteractiveAuthEntryComponents";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { ERROR_USER_CANCELLED } from "../../structures/InteractiveAuth";
import { SSOAuthEntry } from "../auth/InteractiveAuthEntryComponents";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.dialogs.InteractiveAuthDialog")
export default class InteractiveAuthDialog extends React.Component {
@ -117,7 +117,7 @@ export default class InteractiveAuthDialog extends React.Component {
_onUpdateStagePhase = (newStage, newPhase) => {
// We copy the stage and stage phase params into state for title selection in render()
this.setState({uiaStage: newStage, uiaStagePhase: newPhase});
this.setState({ uiaStage: newStage, uiaStagePhase: newPhase });
};
_onDismissClick = () => {

View file

@ -17,37 +17,45 @@ limitations under the License.
import React, { createRef } from 'react';
import classNames from 'classnames';
import {_t, _td} from "../../../languageHandler";
import { _t, _td } from "../../../languageHandler";
import * as sdk from "../../../index";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {makeRoomPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { makeRoomPermalink, makeUserPermalink } from "../../../utils/permalinks/Permalinks";
import DMRoomMap from "../../../utils/DMRoomMap";
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import SdkConfig from "../../../SdkConfig";
import * as Email from "../../../email";
import {getDefaultIdentityServerUrl, useDefaultIdentityServer} from "../../../utils/IdentityServerUtils";
import {abbreviateUrl} from "../../../utils/UrlUtils";
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from "../../../utils/IdentityServerUtils";
import { abbreviateUrl } from "../../../utils/UrlUtils";
import dis from "../../../dispatcher/dispatcher";
import IdentityAuthClient from "../../../IdentityAuthClient";
import Modal from "../../../Modal";
import {humanizeTime} from "../../../utils/humanize";
import { humanizeTime } from "../../../utils/humanize";
import createRoom, {
canEncryptToAllUsers, ensureDMExists, findDMForUser, privateShouldBeEncrypted,
canEncryptToAllUsers,
ensureDMExists,
findDMForUser,
privateShouldBeEncrypted,
} from "../../../createRoom";
import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite";
import {Key} from "../../../Keyboard";
import {Action} from "../../../dispatcher/actions";
import {DefaultTagID} from "../../../stores/room-list/models";
import {
IInviteResult,
inviteMultipleToRoom,
showAnyInviteErrors,
showCommunityInviteDialog,
} from "../../../RoomInvite";
import { Key } from "../../../Keyboard";
import { Action } from "../../../dispatcher/actions";
import { DefaultTagID } from "../../../stores/room-list/models";
import RoomListStore from "../../../stores/room-list/RoomListStore";
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
import SettingsStore from "../../../settings/SettingsStore";
import {UIFeature} from "../../../settings/UIFeature";
import { UIFeature } from "../../../settings/UIFeature";
import CountlyAnalytics from "../../../CountlyAnalytics";
import {Room} from "matrix-js-sdk/src/models/room";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
import {getAddressType} from "../../../UserAddress";
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 { compare } from '../../../utils/strings';
@ -74,10 +82,10 @@ 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
// This is the interface that is expected by various components in this file. It is a bit
// awkward because it also matches the RoomMember class from the js-sdk with some extra support
// 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.
abstract class Member {
export abstract class Member {
/**
* The display name of this Member. For users this should be their profile's display
* name or user ID if none set. For 3PIDs this should be the 3PID address (email).
@ -102,7 +110,8 @@ class DirectoryMember extends Member {
private readonly displayName: string;
private readonly avatarUrl: string;
constructor(userDirResult: {user_id: string, display_name: string, avatar_url: string}) {
// eslint-disable-next-line camelcase
constructor(userDirResult: { user_id: string, display_name: string, avatar_url: string }) {
super();
this._userId = userDirResult.user_id;
this.displayName = userDirResult.display_name;
@ -153,8 +162,8 @@ class ThreepidMember extends Member {
}
interface IDMUserTileProps {
member: RoomMember;
onRemove(member: RoomMember): void;
member: Member;
onRemove(member: Member): void;
}
class DMUserTile extends React.PureComponent<IDMUserTileProps> {
@ -168,7 +177,7 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
render() {
const avatarSize = 20;
const avatar = this.props.member.isEmail
const avatar = (this.props.member as ThreepidMember).isEmail
? <img
className='mx_InviteDialog_userTile_avatar mx_InviteDialog_userTile_threepidAvatar'
src={require("../../../../res/img/icon-email-pill-avatar.svg")}
@ -210,9 +219,9 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
}
interface IDMRoomTileProps {
member: RoomMember;
member: Member;
lastActiveTs: number;
onToggle(member: RoomMember): void;
onToggle(member: Member): void;
highlightWord: string;
isSelected: boolean;
}
@ -270,7 +279,7 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
}
const avatarSize = 36;
const avatar = this.props.member.isEmail
const avatar = (this.props.member as ThreepidMember).isEmail
? <img
src={require("../../../../res/img/icon-email-pill-avatar.svg")}
width={avatarSize} height={avatarSize} />
@ -298,7 +307,7 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
</span>
);
const caption = this.props.member.isEmail
const caption = (this.props.member as ThreepidMember).isEmail
? _t("Invite by email")
: this.highlightName(this.props.member.userId);
@ -334,7 +343,7 @@ interface IInviteDialogProps {
}
interface IInviteDialogState {
targets: RoomMember[]; // array of Member objects (see interface above)
targets: Member[]; // array of Member objects (see interface above)
filterText: string;
recents: { user: Member, userId: string }[];
numRecentsShown: number;
@ -417,8 +426,8 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
}
private onConsultFirstChange = (ev) => {
this.setState({consultFirst: ev.target.checked});
}
this.setState({ consultFirst: ev.target.checked });
};
public static buildRecents(excludedTargetIds: Set<string>): IRecentUser[] {
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
@ -473,7 +482,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
continue;
}
recents.push({userId, user: member, lastActive: lastEventTs});
recents.push({ userId, user: member, lastActive: lastEventTs });
}
if (!recents) console.warn("[Invite:Recents] No recents to suggest!");
@ -581,7 +590,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
const scoreBoost = Math.max(1, inverseTime / (15 * 60 * 1000)); // 15min segments to keep scores sane
let record = memberScores[userId];
if (!record) record = memberScores[userId] = {score: 0};
if (!record) record = memberScores[userId] = { score: 0 };
record.member = member;
record.score += scoreBoost;
}
@ -598,22 +607,13 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
return b.score - a.score;
});
return members.map(m => ({userId: m.member.userId, user: m.member}));
return members.map(m => ({ userId: m.member.userId, user: m.member }));
}
private shouldAbortAfterInviteError(result): boolean {
const failedUsers = Object.keys(result.states).filter(a => result.states[a] === 'error');
if (failedUsers.length > 0) {
console.log("Failed to invite users: ", result);
this.setState({
busy: false,
errorText: _t("Failed to invite the following users to chat: %(csvUsers)s", {
csvUsers: failedUsers.join(", "),
}),
});
return true; // abort
}
return false;
private shouldAbortAfterInviteError(result: IInviteResult, room: Room): boolean {
this.setState({ busy: false });
const userMap = new Map<string, Member>(this.state.targets.map(member => [member.userId, member]));
return !showAnyInviteErrors(result.states, room, result.inviter, userMap);
}
private convertFilter(): Member[] {
@ -623,18 +623,18 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
let newMember: Member;
if (this.state.filterText.startsWith('@')) {
// Assume mxid
newMember = new DirectoryMember({user_id: this.state.filterText, display_name: null, avatar_url: null});
newMember = new DirectoryMember({ user_id: this.state.filterText, display_name: null, avatar_url: null });
} else if (SettingsStore.getValue(UIFeature.IdentityServer)) {
// Assume email
newMember = new ThreepidMember(this.state.filterText);
}
const newTargets = [...(this.state.targets || []), newMember];
this.setState({targets: newTargets, filterText: ''});
this.setState({ targets: newTargets, filterText: '' });
return newTargets;
}
private startDm = async () => {
this.setState({busy: true});
this.setState({ busy: true });
const client = MatrixClientPeg.get();
const targets = this.convertFilter();
const targetIds = targets.map(t => t.userId);
@ -657,7 +657,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
return;
}
const createRoomOptions = {inlineErrors: true} as any; // XXX: Type out `createRoomOptions`
const createRoomOptions = { inlineErrors: true } as any; // XXX: Type out `createRoomOptions`
if (privateShouldBeEncrypted()) {
// Check whether all users have uploaded device keys before.
@ -696,7 +696,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
return roomOptions;
},
{ invite: [], invite_3pid: [] },
)
);
}
await createRoom(createRoomOptions);
@ -712,7 +712,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
private inviteUsers = async () => {
const startTime = CountlyAnalytics.getTimestamp();
this.setState({busy: true});
this.setState({ busy: true });
this.convertFilter();
const targets = this.convertFilter();
const targetIds = targets.map(t => t.userId);
@ -729,9 +729,9 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
}
try {
const result = await inviteMultipleToRoom(this.props.roomId, targetIds)
const result = await inviteMultipleToRoom(this.props.roomId, targetIds);
CountlyAnalytics.instance.trackSendInvite(startTime, this.props.roomId, targetIds.length);
if (!this.shouldAbortAfterInviteError(result)) { // handles setting error message too
if (!this.shouldAbortAfterInviteError(result, room)) { // handles setting error message too
this.props.onFinished();
}
@ -792,10 +792,10 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
});
this.props.onFinished();
} else {
this.setState({busy: true});
this.setState({ busy: true });
try {
await this.props.call.transfer(targetIds[0]);
this.setState({busy: false});
this.setState({ busy: false });
this.props.onFinished();
} catch (e) {
this.setState({
@ -826,7 +826,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
};
private updateSuggestions = async (term) => {
MatrixClientPeg.get().searchUserDirectory({term}).then(async r => {
MatrixClientPeg.get().searchUserDirectory({ term }).then(async r => {
if (term !== this.state.filterText) {
// Discard the results - we were probably too slow on the server-side to make
// these results useful. This is a race we want to avoid because we could overwrite
@ -874,14 +874,14 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
}).catch(e => {
console.error("Error searching user directory:");
console.error(e);
this.setState({serverResultsMixin: []}); // clear results because it's moderately fatal
this.setState({ serverResultsMixin: [] }); // clear results because it's moderately fatal
});
// Whenever we search the directory, also try to search the identity server. It's
// all debounced the same anyways.
if (!this.state.canUseIdentityServer) {
// The user doesn't have an identity server set - warn them of that.
this.setState({tryingIdentityServer: true});
this.setState({ tryingIdentityServer: true });
return;
}
if (term.indexOf('@') > 0 && Email.looksValid(term) && SettingsStore.getValue(UIFeature.IdentityServer)) {
@ -889,7 +889,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
// to a real account.
this.setState({
// per above: the userId is a lie here - it's just a regular identifier
threepidResultsMixin: [{user: new ThreepidMember(term), userId: term}],
threepidResultsMixin: [{ user: new ThreepidMember(term), userId: term }],
});
try {
const authClient = new IdentityAuthClient();
@ -929,14 +929,14 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
} catch (e) {
console.error("Error searching identity server:");
console.error(e);
this.setState({threepidResultsMixin: []}); // clear results because it's moderately fatal
this.setState({ threepidResultsMixin: [] }); // clear results because it's moderately fatal
}
}
};
private updateFilter = (e) => {
const term = e.target.value;
this.setState({filterText: term});
this.setState({ filterText: term });
// Debounce server lookups to reduce spam. We don't clear the existing server
// results because they might still be vaguely accurate, likewise for races which
@ -950,11 +950,11 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
};
private showMoreRecents = () => {
this.setState({numRecentsShown: this.state.numRecentsShown + INCREMENT_ROOMS_SHOWN});
this.setState({ numRecentsShown: this.state.numRecentsShown + INCREMENT_ROOMS_SHOWN });
};
private showMoreSuggestions = () => {
this.setState({numSuggestionsShown: this.state.numSuggestionsShown + INCREMENT_ROOMS_SHOWN});
this.setState({ numSuggestionsShown: this.state.numSuggestionsShown + INCREMENT_ROOMS_SHOWN });
};
private toggleMember = (member: Member) => {
@ -968,7 +968,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
targets.push(member);
filterText = ""; // clear the filter when the user accepts a suggestion
}
this.setState({targets, filterText});
this.setState({ targets, filterText });
if (this.editorRef && this.editorRef.current) {
this.editorRef.current.focus();
@ -981,7 +981,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
const idx = targets.indexOf(member);
if (idx >= 0) {
targets.splice(idx, 1);
this.setState({targets});
this.setState({ targets });
}
if (this.editorRef && this.editorRef.current) {
@ -1051,13 +1051,13 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
title: _t('Failed to find the following users'),
description: _t(
"The following users might not exist or are invalid, and cannot be invited: %(csvNames)s",
{csvNames: failed.join(", ")},
{ csvNames: failed.join(", ") },
),
button: _t('OK'),
});
}
this.setState({targets: [...this.state.targets, ...toAdd]});
this.setState({ targets: [...this.state.targets, ...toAdd] });
};
private onClickInputArea = (e) => {
@ -1076,7 +1076,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
// Update the IS in account data. Actually using it may trigger terms.
// eslint-disable-next-line react-hooks/rules-of-hooks
useDefaultIdentityServer();
this.setState({canUseIdentityServer: true, tryingIdentityServer: false});
this.setState({ canUseIdentityServer: true, tryingIdentityServer: false });
};
private onManageSettingsClick = (e) => {
@ -1100,7 +1100,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
if (kind === 'suggestions' && CommunityPrototypeStore.instance.getSelectedCommunityId()) {
const communityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
sectionSubname = _t("May include members not in %(communityName)s", {communityName});
sectionSubname = _t("May include members not in %(communityName)s", { communityName });
}
if (this.props.kind === KIND_INVITE) {
@ -1297,21 +1297,21 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
helpText = _t(
"Start a conversation with someone using their name, email address or username (like <userId/>).",
{},
{userId: () => {
{ userId: () => {
return (
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>
);
}},
} },
);
} else {
helpText = _t(
"Start a conversation with someone using their name or username (like <userId/>).",
{},
{userId: () => {
{ userId: () => {
return (
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>
);
}},
} },
);
}
@ -1320,7 +1320,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
const inviteText = _t(
"This won't invite them to %(communityName)s. " +
"To invite someone to %(communityName)s, click <a>here</a>",
{communityName}, {
{ communityName }, {
userId: () => {
return (
<a
@ -1365,7 +1365,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
<div />
</AccessibleTooltipButton>
</div>
</div>
</div>;
} else if (this.props.kind === KIND_INVITE) {
const room = MatrixClientPeg.get()?.getRoom(this.props.roomId);
const isSpace = SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom();

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useState, useCallback, useRef} from 'react';
import React, { useState, useCallback, useRef } from 'react';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';

View file

@ -22,7 +22,7 @@ import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import RestoreKeyBackupDialog from './security/RestoreKeyBackupDialog';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.dialogs.LogoutDialog")
export default class LogoutDialog extends React.Component {
@ -85,7 +85,7 @@ export default class LogoutDialog extends React.Component {
_onFinished(confirmed) {
if (confirmed) {
dis.dispatch({action: 'logout'});
dis.dispatch({ action: 'logout' });
}
// close dialog
this.props.onFinished();
@ -112,7 +112,7 @@ export default class LogoutDialog extends React.Component {
}
_onLogoutConfirm() {
dis.dispatch({action: 'logout'});
dis.dispatch({ action: 'logout' });
// close dialog
this.props.onFinished();

View file

@ -20,11 +20,11 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import * as FormattingUtils from '../../../utils/FormattingUtils';
import { _t } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.dialogs.ManualDeviceKeyVerificationDialog")
export default class ManualDeviceKeyVerificationDialog extends React.Component {

View file

@ -16,12 +16,12 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { _t } from '../../../languageHandler';
import * as sdk from "../../../index";
import {wantsDateSeparator} from '../../../DateUtils';
import { wantsDateSeparator } from '../../../DateUtils';
import SettingsStore from '../../../settings/SettingsStore';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.dialogs.MessageEditHistoryDialog")
export default class MessageEditHistoryDialog extends React.PureComponent {
@ -46,7 +46,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
// bail out on backwards as we only paginate in one direction
return false;
}
const opts = {from: this.state.nextBatch};
const opts = { from: this.state.nextBatch };
const roomId = this.props.mxEvent.getRoomId();
const eventId = this.props.mxEvent.getId();
const client = MatrixClientPeg.get();
@ -62,7 +62,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
if (error.errcode) {
console.error("fetching /relations failed with error", error);
}
this.setState({error}, () => reject(error));
this.setState({ error }, () => reject(error));
return promise;
}
@ -131,7 +131,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
render() {
let content;
if (this.state.error) {
const {error} = this.state;
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.")}

View file

@ -33,13 +33,13 @@ import {
WidgetApiFromWidgetAction,
WidgetKind,
} from "matrix-widget-api";
import {StopGapWidgetDriver} from "../../../stores/widgets/StopGapWidgetDriver";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {OwnProfileStore} from "../../../stores/OwnProfileStore";
import { StopGapWidgetDriver } from "../../../stores/widgets/StopGapWidgetDriver";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { OwnProfileStore } from "../../../stores/OwnProfileStore";
import { arrayFastClone } from "../../../utils/arrays";
import { ElementWidget } from "../../../stores/widgets/StopGapWidget";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {ELEMENT_CLIENT_ID} from "../../../identifiers";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { ELEMENT_CLIENT_ID } from "../../../identifiers";
import SettingsStore from "../../../settings/SettingsStore";
interface IProps {
@ -63,7 +63,8 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
private appFrame: React.RefObject<HTMLIFrameElement> = React.createRef();
state: IState = {
disabledButtonIds: [],
disabledButtonIds: (this.props.widgetDefinition.buttons || []).filter(b => b.disabled)
.map(b => b.id),
};
constructor(props) {
@ -80,7 +81,7 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
public componentDidMount() {
const driver = new StopGapWidgetDriver( [], this.widget, WidgetKind.Modal);
const messaging = new ClientWidgetApi(this.widget, this.appFrame.current, driver);
this.setState({messaging});
this.setState({ messaging });
}
public componentWillUnmount() {
@ -101,14 +102,14 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
private onWidgetClose = (ev: CustomEvent<IModalWidgetCloseRequest>) => {
this.props.onFinished(true, ev.detail.data);
}
};
private onButtonEnableToggle = (ev: CustomEvent<ISetModalButtonEnabledActionRequest>) => {
ev.preventDefault();
const isClose = ev.detail.data.button === BuiltInModalButtonID.Close;
if (isClose || !this.possibleButtons.includes(ev.detail.data.button)) {
return this.state.messaging.transport.reply(ev.detail, {
error: {message: "Invalid button"},
error: { message: "Invalid button" },
} as IWidgetApiErrorResponseData);
}
@ -121,7 +122,7 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
tempSet.add(ev.detail.data.button);
buttonIds = Array.from(tempSet);
}
this.setState({disabledButtonIds: buttonIds});
this.setState({ disabledButtonIds: buttonIds });
this.state.messaging.transport.reply(ev.detail, {} as IWidgetApiAcknowledgeResponseData);
};
@ -159,7 +160,7 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
break;
case ModalButtonKind.Secondary:
kind = "primary_outline";
break
break;
case ModalButtonKind.Danger:
kind = "danger";
break;

View file

@ -1,121 +0,0 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import { replaceableComponent } from '../../../utils/replaceableComponent';
import VerificationRequestDialog from './VerificationRequestDialog';
import BaseDialog from './BaseDialog';
import DialogButtons from '../elements/DialogButtons';
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import * as sdk from '../../../index';
@replaceableComponent("views.dialogs.NewSessionReviewDialog")
export default class NewSessionReviewDialog extends React.PureComponent {
static propTypes = {
userId: PropTypes.string.isRequired,
device: PropTypes.object.isRequired,
onFinished: PropTypes.func.isRequired,
}
onCancelClick = () => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog("Verification failed", "insecure", ErrorDialog, {
headerImage: require("../../../../res/img/e2e/warning.svg"),
title: _t("Your account is not secure"),
description: <div>
{_t("One of the following may be compromised:")}
<ul>
<li>{_t("Your password")}</li>
<li>{_t("Your homeserver")}</li>
<li>{_t("This session, or the other session")}</li>
<li>{_t("The internet connection either session is using")}</li>
</ul>
<div>
{_t("We recommend you change your password and Security Key in Settings immediately")}
</div>
</div>,
onFinished: () => this.props.onFinished(false),
});
}
onContinueClick = () => {
const { userId, device } = this.props;
const cli = MatrixClientPeg.get();
const requestPromise = cli.requestVerification(
userId,
[device.deviceId],
);
this.props.onFinished(true);
Modal.createTrackedDialog('New Session Verification', 'Starting dialog', VerificationRequestDialog, {
verificationRequestPromise: requestPromise,
member: cli.getUser(userId),
onFinished: async () => {
const request = await requestPromise;
request.cancel();
},
});
}
render() {
const { device } = this.props;
const icon = <span className="mx_NewSessionReviewDialog_headerIcon mx_E2EIcon_warning"></span>;
const titleText = _t("New session");
const title = <h2 className="mx_NewSessionReviewDialog_header">
{icon}
{titleText}
</h2>;
return (
<BaseDialog
title={title}
onFinished={this.props.onFinished}
>
<div className="mx_NewSessionReviewDialog_body">
<p>{_t(
"Use this session to verify your new one, " +
"granting it access to encrypted messages:",
)}</p>
<div className="mx_NewSessionReviewDialog_deviceInfo">
<div>
<span className="mx_NewSessionReviewDialog_deviceName">
{device.getDisplayName()}
</span> <span className="mx_NewSessionReviewDialog_deviceID">
({device.deviceId})
</span>
</div>
</div>
<p>{_t(
"If you didnt sign in to this session, " +
"your account may be compromised.",
)}</p>
<DialogButtons
cancelButton={_t("This wasn't me")}
cancelButtonClass="danger"
primaryButton={_t("Continue")}
onCancel={this.onCancelClick}
onPrimaryButtonClick={this.onContinueClick}
/>
</div>
</BaseDialog>
);
}
}

View file

@ -18,7 +18,7 @@ import * as React from "react";
import { _t } from '../../../languageHandler';
import { IDialogProps } from "./IDialogProps";
import {useRef, useState} from "react";
import { useRef, useState } from "react";
import Field from "../elements/Field";
import CountlyAnalytics from "../../../CountlyAnalytics";
import withValidation from "../elements/Validation";
@ -40,7 +40,7 @@ const validation = withValidation({
],
});
const RegistrationEmailPromptDialog: React.FC<IProps> = ({onFinished}) => {
const RegistrationEmailPromptDialog: React.FC<IProps> = ({ onFinished }) => {
const [email, setEmail] = useState("");
const fieldRef = useRef<Field>();

View file

@ -1,149 +0,0 @@
/*
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
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, {PureComponent} from 'react';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import PropTypes from "prop-types";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import SdkConfig from '../../../SdkConfig';
import Markdown from '../../../Markdown';
import {replaceableComponent} from "../../../utils/replaceableComponent";
/*
* A dialog for reporting an event.
*/
@replaceableComponent("views.dialogs.ReportEventDialog")
export default class ReportEventDialog extends PureComponent {
static propTypes = {
mxEvent: PropTypes.instanceOf(MatrixEvent).isRequired,
onFinished: PropTypes.func.isRequired,
};
constructor(props) {
super(props);
this.state = {
reason: "",
busy: false,
err: null,
};
}
_onReasonChange = ({target: {value: reason}}) => {
this.setState({ reason });
};
_onCancel = () => {
this.props.onFinished(false);
};
_onSubmit = async () => {
if (!this.state.reason || !this.state.reason.trim()) {
this.setState({
err: _t("Please fill why you're reporting."),
});
return;
}
this.setState({
busy: true,
err: null,
});
try {
const ev = this.props.mxEvent;
await MatrixClientPeg.get().reportEvent(ev.getRoomId(), ev.getId(), -100, this.state.reason.trim());
this.props.onFinished(true);
} catch (e) {
this.setState({
busy: false,
err: e.message,
});
}
};
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const Loader = sdk.getComponent('elements.Spinner');
const Field = sdk.getComponent('elements.Field');
let error = null;
if (this.state.err) {
error = <div className="error">
{this.state.err}
</div>;
}
let progress = null;
if (this.state.busy) {
progress = (
<div className="progress">
<Loader />
</div>
);
}
const adminMessageMD =
SdkConfig.get().reportEvent &&
SdkConfig.get().reportEvent.adminMessageMD;
let adminMessage;
if (adminMessageMD) {
const html = new Markdown(adminMessageMD).toHTML({ externalLinks: true });
adminMessage = <p dangerouslySetInnerHTML={{ __html: html }} />;
}
return (
<BaseDialog
className="mx_BugReportDialog"
onFinished={this.props.onFinished}
title={_t('Report Content to Your Homeserver Administrator')}
contentId='mx_ReportEventDialog'
>
<div className="mx_ReportEventDialog" id="mx_ReportEventDialog">
<p>
{
_t("Reporting this message will send its unique 'event ID' to the administrator of " +
"your homeserver. If messages in this room are encrypted, your homeserver " +
"administrator will not be able to read the message text or view any files or images.")
}
</p>
{adminMessage}
<Field
className="mx_ReportEventDialog_reason"
element="textarea"
label={_t("Reason")}
rows={5}
onChange={this._onReasonChange}
value={this.state.reason}
disabled={this.state.busy}
/>
{progress}
{error}
</div>
<DialogButtons
primaryButton={_t("Send report")}
onPrimaryButtonClick={this._onSubmit}
focus={true}
onCancel={this._onCancel}
disabled={this.state.busy}
/>
</BaseDialog>
);
}
}

View file

@ -0,0 +1,444 @@
/*
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import { ensureDMExists } from "../../../createRoom";
import { IDialogProps } from "./IDialogProps";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import SdkConfig from '../../../SdkConfig';
import Markdown from '../../../Markdown';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import SettingsStore from "../../../settings/SettingsStore";
import StyledRadioButton from "../elements/StyledRadioButton";
interface IProps extends IDialogProps {
mxEvent: MatrixEvent;
}
interface IState {
// A free-form text describing the abuse.
reason: string;
busy: boolean;
err?: string;
// If we know it, the nature of the abuse, as specified by MSC3215.
nature?: EXTENDED_NATURE;
}
const MODERATED_BY_STATE_EVENT_TYPE = [
"org.matrix.msc3215.room.moderation.moderated_by",
/**
* Unprefixed state event. Not ready for prime time.
*
* "m.room.moderation.moderated_by"
*/
];
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 NON_STANDARD_NATURE {
// 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"
}
type EXTENDED_NATURE = NATURE | NON_STANDARD_NATURE;
type Moderation = {
// The id of the moderation room.
moderationRoomId: string;
// The id of the bot in charge of forwarding abuse reports to the moderation room.
moderationBotUserId: string;
};
/*
* A dialog for reporting an event.
*
* The actual content of the dialog will depend on two things:
*
* 1. Is `feature_report_to_moderators` enabled?
* 2. Does the room support moderation as per MSC3215, i.e. is there
* a well-formed state event `m.room.moderation.moderated_by`
* /`org.matrix.msc3215.room.moderation.moderated_by`?
*/
@replaceableComponent("views.dialogs.ReportEventDialog")
export default class ReportEventDialog extends React.Component<IProps, IState> {
// If the room supports moderation, the moderation information.
private moderation?: Moderation;
constructor(props: IProps) {
super(props);
let moderatedByRoomId = null;
let moderatedByUserId = null;
if (SettingsStore.getValue("feature_report_to_moderators")) {
// The client supports reporting to moderators.
// Does the room support it, too?
// Extract state events to determine whether we should display
const client = MatrixClientPeg.get();
const room = client.getRoom(props.mxEvent.getRoomId());
for (const stateEventType of MODERATED_BY_STATE_EVENT_TYPE) {
const stateEvent = room.currentState.getStateEvents(stateEventType, stateEventType);
if (!stateEvent) {
continue;
}
if (Array.isArray(stateEvent)) {
// Internal error.
throw new TypeError(`getStateEvents(${stateEventType}, ${stateEventType}) ` +
"should return at most one state event");
}
const event = stateEvent.event;
if (!("content" in event) || typeof event["content"] != "object") {
// The room is improperly configured.
// Display this debug message for the sake of moderators.
console.debug("Moderation error", "state event", stateEventType,
"should have an object field `content`, got", event);
continue;
}
const content = event["content"];
if (!("room_id" in content) || typeof content["room_id"] != "string") {
// The room is improperly configured.
// Display this debug message for the sake of moderators.
console.debug("Moderation error", "state event", stateEventType,
"should have a string field `content.room_id`, got", event);
continue;
}
if (!("user_id" in content) || typeof content["user_id"] != "string") {
// The room is improperly configured.
// Display this debug message for the sake of moderators.
console.debug("Moderation error", "state event", stateEventType,
"should have a string field `content.user_id`, got", event);
continue;
}
moderatedByRoomId = content["room_id"];
moderatedByUserId = content["user_id"];
}
if (moderatedByRoomId && moderatedByUserId) {
// The room supports moderation.
this.moderation = {
moderationRoomId: moderatedByRoomId,
moderationBotUserId: moderatedByUserId,
};
}
}
this.state = {
// A free-form text describing the abuse.
reason: "",
busy: false,
err: null,
// If specified, the nature of the abuse, as specified by MSC3215.
nature: null,
};
}
// The user has written down a freeform description of the abuse.
private onReasonChange = ({ target: { value: reason } }): void => {
this.setState({ reason });
};
// The user has clicked on a nature.
private onNatureChosen = (e: React.FormEvent<HTMLInputElement>): void => {
this.setState({ nature: e.currentTarget.value as EXTENDED_NATURE });
};
// The user has clicked "cancel".
private onCancel = (): void => {
this.props.onFinished(false);
};
// The user has clicked "submit".
private onSubmit = async () => {
let reason = this.state.reason || "";
reason = reason.trim();
if (this.moderation) {
// This room supports moderation.
// 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)
&& !reason)
) {
this.setState({
err: _t("Please fill why you're reporting."),
});
return;
}
} else {
// This room does not support moderation.
// We need a `reason`.
if (!reason) {
this.setState({
err: _t("Please fill why you're reporting."),
});
return;
}
}
this.setState({
busy: true,
err: null,
});
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;
// Report to moderators through to the dedicated bot,
// as configured in the room's state events.
const dmRoomId = await ensureDMExists(client, this.moderation.moderationBotUserId);
await client.sendEvent(dmRoomId, ABUSE_EVENT_TYPE, {
event_id: ev.getId(),
room_id: ev.getRoomId(),
moderated_by_id: this.moderation.moderationRoomId,
nature,
reporter: client.getUserId(),
comment: this.state.reason.trim(),
});
} else {
// Report to homeserver admin through the dedicated Matrix API.
await client.reportEvent(ev.getRoomId(), ev.getId(), -100, this.state.reason.trim());
}
this.props.onFinished(true);
} catch (e) {
this.setState({
busy: false,
err: e.message,
});
}
};
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const Loader = sdk.getComponent('elements.Spinner');
const Field = sdk.getComponent('elements.Field');
let error = null;
if (this.state.err) {
error = <div className="error">
{this.state.err}
</div>;
}
let progress = null;
if (this.state.busy) {
progress = (
<div className="progress">
<Loader />
</div>
);
}
const adminMessageMD =
SdkConfig.get().reportEvent &&
SdkConfig.get().reportEvent.adminMessageMD;
let adminMessage;
if (adminMessageMD) {
const html = new Markdown(adminMessageMD).toHTML({ externalLinks: true });
adminMessage = <p dangerouslySetInnerHTML={{ __html: html }} />;
}
if (this.moderation) {
// Display report-to-moderator dialog.
// We let the user pick a nature.
const client = MatrixClientPeg.get();
const homeServerName = SdkConfig.get()["validated_server_config"].hsName;
let subtitle;
switch (this.state.nature) {
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:
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:
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:
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:
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" +
"This will be reported to the administrators of %(homeserver)s. " +
"The administrators will NOT be able to read the encrypted content of this room.",
{ homeserver: homeServerName });
} else {
subtitle = _t("This room is dedicated to illegal or toxic content " +
"or the moderators fail to moderate illegal or toxic content.\n" +
" This will be reported to the administrators of %(homeserver)s.",
{ homeserver: homeServerName });
}
break;
case NATURE.OTHER:
subtitle = _t("Any other reason. Please describe the problem.\n" +
"This will be reported to the room moderators.");
break;
default:
subtitle = _t("Please pick a nature and describe what makes this message abusive.");
break;
}
return (
<BaseDialog
className="mx_ReportEventDialog"
onFinished={this.props.onFinished}
title={_t('Report Content')}
contentId='mx_ReportEventDialog'
>
<div>
<StyledRadioButton
name = "nature"
value = { NATURE.DISAGREEMENT }
checked = { this.state.nature == NATURE.DISAGREEMENT }
onChange = { this.onNatureChosen }
>
{_t('Disagree')}
</StyledRadioButton>
<StyledRadioButton
name = "nature"
value = { NATURE.TOXIC }
checked = { this.state.nature == NATURE.TOXIC }
onChange = { this.onNatureChosen }
>
{_t('Toxic Behaviour')}
</StyledRadioButton>
<StyledRadioButton
name = "nature"
value = { NATURE.ILLEGAL }
checked = { this.state.nature == NATURE.ILLEGAL }
onChange = { this.onNatureChosen }
>
{_t('Illegal Content')}
</StyledRadioButton>
<StyledRadioButton
name = "nature"
value = { NATURE.SPAM }
checked = { this.state.nature == NATURE.SPAM }
onChange = { this.onNatureChosen }
>
{_t('Spam or propaganda')}
</StyledRadioButton>
<StyledRadioButton
name = "nature"
value = { NON_STANDARD_NATURE.ADMIN }
checked = { this.state.nature == NON_STANDARD_NATURE.ADMIN }
onChange = { this.onNatureChosen }
>
{_t('Report the entire room')}
</StyledRadioButton>
<StyledRadioButton
name = "nature"
value = { NATURE.OTHER }
checked = { this.state.nature == NATURE.OTHER }
onChange = { this.onNatureChosen }
>
{_t('Other')}
</StyledRadioButton>
<p>
{subtitle}
</p>
<Field
className="mx_ReportEventDialog_reason"
element="textarea"
label={_t("Reason")}
rows={5}
onChange={this.onReasonChange}
value={this.state.reason}
disabled={this.state.busy}
/>
{progress}
{error}
</div>
<DialogButtons
primaryButton={_t("Send report")}
onPrimaryButtonClick={this.onSubmit}
focus={true}
onCancel={this.onCancel}
disabled={this.state.busy}
/>
</BaseDialog>
);
}
// Report to homeserver admin.
// Currently, the API does not support natures.
return (
<BaseDialog
className="mx_ReportEventDialog"
onFinished={this.props.onFinished}
title={_t('Report Content to Your Homeserver Administrator')}
contentId='mx_ReportEventDialog'
>
<div className="mx_ReportEventDialog" id="mx_ReportEventDialog">
<p>
{
_t("Reporting this message will send its unique 'event ID' to the administrator of " +
"your homeserver. If messages in this room are encrypted, your homeserver " +
"administrator will not be able to read the message text or view any files " +
"or images.")
}
</p>
{adminMessage}
<Field
className="mx_ReportEventDialog_reason"
element="textarea"
label={_t("Reason")}
rows={5}
onChange={this.onReasonChange}
value={this.state.reason}
disabled={this.state.busy}
/>
{progress}
{error}
</div>
<DialogButtons
primaryButton={_t("Send report")}
onPrimaryButtonClick={this.onSubmit}
focus={true}
onCancel={this.onCancel}
disabled={this.state.busy}
/>
</BaseDialog>
);
}
}

View file

@ -16,8 +16,8 @@ limitations under the License.
*/
import React from 'react';
import TabbedView, {Tab} from "../../structures/TabbedView";
import {_t, _td} from "../../../languageHandler";
import TabbedView, { Tab } from "../../structures/TabbedView";
import { _t, _td } from "../../../languageHandler";
import AdvancedRoomSettingsTab from "../settings/tabs/room/AdvancedRoomSettingsTab";
import RolesRoomSettingsTab from "../settings/tabs/room/RolesRoomSettingsTab";
import GeneralRoomSettingsTab from "../settings/tabs/room/GeneralRoomSettingsTab";
@ -25,11 +25,11 @@ import SecurityRoomSettingsTab from "../settings/tabs/room/SecurityRoomSettingsT
import NotificationSettingsTab from "../settings/tabs/room/NotificationSettingsTab";
import BridgeSettingsTab from "../settings/tabs/room/BridgeSettingsTab";
import * as sdk from "../../../index";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import dis from "../../../dispatcher/dispatcher";
import SettingsStore from "../../../settings/SettingsStore";
import {UIFeature} from "../../../settings/UIFeature";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { UIFeature } from "../../../settings/UIFeature";
import { replaceableComponent } from "../../../utils/replaceableComponent";
export const ROOM_GENERAL_TAB = "ROOM_GENERAL_TAB";
export const ROOM_SECURITY_TAB = "ROOM_SECURITY_TAB";
@ -108,7 +108,10 @@ export default class RoomSettingsDialog extends React.Component<IProps> {
ROOM_ADVANCED_TAB,
_td("Advanced"),
"mx_RoomSettingsDialog_warningIcon",
<AdvancedRoomSettingsTab roomId={this.props.roomId} closeSettingsFn={this.props.onFinished} />,
<AdvancedRoomSettingsTab
roomId={this.props.roomId}
closeSettingsFn={() => this.props.onFinished(true)}
/>,
));
}
@ -124,7 +127,7 @@ export default class RoomSettingsDialog extends React.Component<IProps> {
className='mx_RoomSettingsDialog'
hasCancel={true}
onFinished={this.props.onFinished}
title={_t("Room Settings - %(roomName)s", {roomName})}
title={_t("Room Settings - %(roomName)s", { roomName })}
>
<div className='mx_SettingsDialog_content'>
<TabbedView

View file

@ -17,10 +17,10 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.dialogs.RoomUpgradeDialog")
export default class RoomUpgradeDialog extends React.Component {
@ -36,7 +36,7 @@ export default class RoomUpgradeDialog extends React.Component {
async componentDidMount() {
const recommended = await this.props.room.getRecommendedVersion();
this._targetVersion = recommended.version;
this.setState({busy: false});
this.setState({ busy: false });
}
_onCancelClick = () => {
@ -44,7 +44,7 @@ export default class RoomUpgradeDialog extends React.Component {
};
_onUpgradeClick = () => {
this.setState({busy: true});
this.setState({ busy: true });
MatrixClientPeg.get().upgradeRoom(this.props.room.roomId, this._targetVersion).then(() => {
this.props.onFinished(true);
}).catch((err) => {
@ -54,7 +54,7 @@ export default class RoomUpgradeDialog extends React.Component {
description: ((err && err.message) ? err.message : _t("The room upgrade could not be completed")),
});
}).finally(() => {
this.setState({busy: false});
this.setState({ busy: false });
});
};
@ -70,7 +70,7 @@ export default class RoomUpgradeDialog extends React.Component {
buttons = <DialogButtons
primaryButton={_t(
'Upgrade this room to version %(version)s',
{version: this._targetVersion},
{ version: this._targetVersion },
)}
primaryButtonClass="danger"
hasCancel={true}

View file

@ -16,13 +16,13 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler";
import { _t } from "../../../languageHandler";
import SdkConfig from "../../../SdkConfig";
import * as sdk from "../../../index";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import Modal from "../../../Modal";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.dialogs.RoomUpgradeWarningDialog")
export default class RoomUpgradeWarningDialog extends React.Component {
@ -46,15 +46,15 @@ export default class RoomUpgradeWarningDialog extends React.Component {
}
_onContinue = () => {
this.props.onFinished({continue: true, invite: this.state.isPrivate && this.state.inviteUsersToNewRoom});
this.props.onFinished({ continue: true, invite: this.state.isPrivate && this.state.inviteUsersToNewRoom });
};
_onCancel = () => {
this.props.onFinished({continue: false, invite: false});
this.props.onFinished({ continue: false, invite: false });
};
_onInviteUsersToggle = (newVal) => {
this.setState({inviteUsersToNewRoom: newVal});
this.setState({ inviteUsersToNewRoom: newVal });
};
_openBugReportDialog = (e) => {
@ -86,7 +86,7 @@ export default class RoomUpgradeWarningDialog extends React.Component {
<p>
{_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},
"having problems with your %(brand)s, please report a bug.", { brand },
)}
</p>
);

View file

@ -28,7 +28,7 @@ import AccessibleButton from "../elements/AccessibleButton";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { IDialogProps } from "./IDialogProps";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps extends IDialogProps {
}
@ -85,7 +85,7 @@ export default class ServerOfflineDialog extends React.PureComponent<IProps> {
{entries}
</div>
</div>
)
);
});
}
@ -108,7 +108,7 @@ export default class ServerOfflineDialog extends React.PureComponent<IProps> {
"Below are some of the most likely reasons.",
)}</p>
<ul>
<li>{_t("The server (%(serverName)s) took too long to respond.", {serverName})}</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>

View file

@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {createRef} from "react";
import {AutoDiscovery} from "matrix-js-sdk/src/autodiscovery";
import React, { createRef } from "react";
import { AutoDiscovery } from "matrix-js-sdk/src/autodiscovery";
import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../../utils/AutoDiscoveryUtils";
import BaseDialog from './BaseDialog';
import { _t } from '../../../languageHandler';
import AccessibleButton from "../elements/AccessibleButton";
@ -25,8 +25,8 @@ import SdkConfig from "../../../SdkConfig";
import Field from "../elements/Field";
import StyledRadioButton from "../elements/StyledRadioButton";
import TextWithTooltip from "../elements/TextWithTooltip";
import withValidation, {IFieldState} from "../elements/Validation";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import withValidation, { IFieldState } from "../elements/Validation";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
title?: string;

View file

@ -15,13 +15,13 @@ limitations under the License.
*/
import React from 'react';
import {_t} from "../../../languageHandler";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { _t } from "../../../languageHandler";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
import {IDialogProps} from "./IDialogProps";
import { IDialogProps } from "./IDialogProps";
@replaceableComponent("views.dialogs.SeshatResetDialog")
export default class SeshatResetDialog extends React.PureComponent<IDialogProps> {

View file

@ -22,7 +22,7 @@ import * as sdk from '../../../index';
import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.dialogs.SessionRestoreErrorDialog")
export default class SessionRestoreErrorDialog extends React.Component {

View file

@ -22,8 +22,7 @@ import * as Email from '../../../email';
import AddThreepid from '../../../AddThreepid';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
/*
* Prompt the user to set an email address.
@ -71,14 +70,14 @@ export default class SetEmailDialog extends React.Component {
onFinished: this.onEmailDialogFinished,
});
}, (err) => {
this.setState({emailBusy: false});
this.setState({ emailBusy: false });
console.error("Unable to add email address " + emailAddress + " " + err);
Modal.createTrackedDialog('Unable to add email address', '', ErrorDialog, {
title: _t("Unable to add email address"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
});
this.setState({emailBusy: true});
this.setState({ emailBusy: true });
};
onCancelled = () => {
@ -89,7 +88,7 @@ export default class SetEmailDialog extends React.Component {
if (ok) {
this.verifyEmailAddress();
} else {
this.setState({emailBusy: false});
this.setState({ emailBusy: false });
}
};
@ -97,7 +96,7 @@ export default class SetEmailDialog extends React.Component {
this._addThreepid.checkEmailLinkClicked().then(() => {
this.props.onFinished(true);
}, (err) => {
this.setState({emailBusy: false});
this.setState({ emailBusy: false });
if (err.errcode == 'M_THREEPID_AUTH_FAILED') {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const message = _t("Unable to verify email address.") + " " +

View file

@ -17,24 +17,24 @@ limitations under the License.
import * as React from 'react';
import * as PropTypes from 'prop-types';
import {Room} from "matrix-js-sdk/src/models/room";
import {User} from "matrix-js-sdk/src/models/user";
import {Group} from "matrix-js-sdk/src/models/group";
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room";
import { User } from "matrix-js-sdk/src/models/user";
import { Group } from "matrix-js-sdk/src/models/group";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import QRCode from "../elements/QRCode";
import {RoomPermalinkCreator, makeGroupPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks";
import { RoomPermalinkCreator, makeGroupPermalink, makeUserPermalink } from "../../../utils/permalinks/Permalinks";
import * as ContextMenu from "../../structures/ContextMenu";
import {toRightOf} from "../../structures/ContextMenu";
import {copyPlaintext, selectText} from "../../../utils/strings";
import { toRightOf } from "../../structures/ContextMenu";
import { copyPlaintext, selectText } from "../../../utils/strings";
import StyledCheckbox from '../elements/StyledCheckbox';
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
import { IDialogProps } from "./IDialogProps";
import SettingsStore from "../../../settings/SettingsStore";
import {UIFeature} from "../../../settings/UIFeature";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { UIFeature } from "../../../settings/UIFeature";
import { replaceableComponent } from "../../../utils/replaceableComponent";
const socials = [
{
@ -120,7 +120,7 @@ export default class ShareDialog extends React.PureComponent<IProps, IState> {
const successful = await copyPlaintext(this.getUrl());
const buttonRect = target.getBoundingClientRect();
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
const { close } = ContextMenu.createMenu(GenericTextContextMenu, {
...toRightOf(buttonRect, 2),
message: successful ? _t('Copied!') : _t('Failed to copy'),
});

View file

@ -15,11 +15,11 @@ limitations under the License.
*/
import React from 'react';
import {_t} from "../../../languageHandler";
import {CommandCategories, Commands} from "../../../SlashCommands";
import { _t } from "../../../languageHandler";
import { CommandCategories, Commands } from "../../../SlashCommands";
import * as sdk from "../../../index";
export default ({onFinished}) => {
export default ({ onFinished }) => {
const InfoDialog = sdk.getComponent('dialogs.InfoDialog');
const categories = {};

View file

@ -14,24 +14,27 @@ 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 {MatrixClient} from "matrix-js-sdk/src/client";
import {EventType} from "matrix-js-sdk/src/@types/event";
import React, { useMemo } from 'react';
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixClient } from "matrix-js-sdk/src/client";
import {_t} from '../../../languageHandler';
import {IDialogProps} from "./IDialogProps";
import { _t, _td } from '../../../languageHandler';
import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog";
import DevtoolsDialog from "./DevtoolsDialog";
import SpaceBasicSettings from '../spaces/SpaceBasicSettings';
import {getTopic} from "../elements/RoomTopic";
import {avatarUrlForRoom} from "../../../Avatar";
import ToggleSwitch from "../elements/ToggleSwitch";
import AccessibleButton from "../elements/AccessibleButton";
import Modal from "../../../Modal";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import {useDispatcher} from "../../../hooks/useDispatcher";
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
import { useDispatcher } from "../../../hooks/useDispatcher";
import TabbedView, { Tab } from "../../structures/TabbedView";
import SpaceSettingsGeneralTab from '../spaces/SpaceSettingsGeneralTab';
import SpaceSettingsVisibilityTab from "../spaces/SpaceSettingsVisibilityTab";
import SettingsStore from "../../../settings/SettingsStore";
import { UIFeature } from "../../../settings/UIFeature";
import AdvancedRoomSettingsTab from "../settings/tabs/room/AdvancedRoomSettingsTab";
export enum SpaceSettingsTab {
General = "SPACE_GENERAL_TAB",
Visibility = "SPACE_VISIBILITY_TAB",
Advanced = "SPACE_ADVANCED_TAB",
}
interface IProps extends IDialogProps {
matrixClient: MatrixClient;
@ -39,69 +42,36 @@ interface IProps extends IDialogProps {
}
const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFinished }) => {
useDispatcher(defaultDispatcher, ({action, ...params}) => {
useDispatcher(defaultDispatcher, ({ action, ...params }) => {
if (action === "after_leave_room" && params.room_id === space.roomId) {
onFinished(false);
}
});
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const userId = cli.getUserId();
const [newAvatar, setNewAvatar] = useState<File>(null); // undefined means to remove avatar
const canSetAvatar = space.currentState.maySendStateEvent(EventType.RoomAvatar, userId);
const avatarChanged = newAvatar !== null;
const [name, setName] = useState<string>(space.name);
const canSetName = space.currentState.maySendStateEvent(EventType.RoomName, userId);
const nameChanged = name !== space.name;
const currentTopic = getTopic(space);
const [topic, setTopic] = useState<string>(currentTopic);
const canSetTopic = space.currentState.maySendStateEvent(EventType.RoomTopic, userId);
const topicChanged = topic !== currentTopic;
const currentJoinRule = space.getJoinRule();
const [joinRule, setJoinRule] = useState(currentJoinRule);
const canSetJoinRule = space.currentState.maySendStateEvent(EventType.RoomJoinRules, userId);
const joinRuleChanged = joinRule !== currentJoinRule;
const onSave = async () => {
setBusy(true);
const promises = [];
if (avatarChanged) {
if (newAvatar) {
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {
url: await cli.uploadContent(newAvatar),
}, ""));
} else {
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {}, ""));
}
}
if (nameChanged) {
promises.push(cli.setRoomName(space.roomId, name));
}
if (topicChanged) {
promises.push(cli.setRoomTopic(space.roomId, topic));
}
if (joinRuleChanged) {
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomJoinRules, { join_rule: joinRule }, ""));
}
const results = await Promise.allSettled(promises);
setBusy(false);
const failures = results.filter(r => r.status === "rejected");
if (failures.length > 0) {
console.error("Failed to save space settings: ", failures);
setError(_t("Failed to save space settings."));
}
};
const tabs = useMemo(() => {
return [
new Tab(
SpaceSettingsTab.General,
_td("General"),
"mx_SpaceSettingsDialog_generalIcon",
<SpaceSettingsGeneralTab matrixClient={cli} space={space} onFinished={onFinished} />,
),
new Tab(
SpaceSettingsTab.Visibility,
_td("Visibility"),
"mx_SpaceSettingsDialog_visibilityIcon",
<SpaceSettingsVisibilityTab matrixClient={cli} space={space} />,
),
SettingsStore.getValue(UIFeature.AdvancedSettings)
? new Tab(
SpaceSettingsTab.Advanced,
_td("Advanced"),
"mx_RoomSettingsDialog_warningIcon",
<AdvancedRoomSettingsTab roomId={space.roomId} closeSettingsFn={onFinished} />,
)
: null,
].filter(Boolean);
}, [cli, space, onFinished]);
return <BaseDialog
title={_t("Space settings")}
@ -110,61 +80,14 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
onFinished={onFinished}
fixedWidth={false}
>
<div className="mx_SpaceSettingsDialog_content" id="mx_SpaceSettingsDialog">
<div>{ _t("Edit settings relating to your space.") }</div>
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
<SpaceFeedbackPrompt onClick={() => onFinished(false)} />
<SpaceBasicSettings
avatarUrl={avatarUrlForRoom(space, 80, 80, "crop")}
avatarDisabled={busy || !canSetAvatar}
setAvatar={setNewAvatar}
name={name}
nameDisabled={busy || !canSetName}
setName={setName}
topic={topic}
topicDisabled={busy || !canSetTopic}
setTopic={setTopic}
/>
<div>
{ _t("Make this space private") }
<ToggleSwitch
checked={joinRule !== "public"}
onChange={checked => setJoinRule(checked ? "invite" : "public")}
disabled={!canSetJoinRule}
aria-label={_t("Make this space private")}
/>
</div>
<AccessibleButton
kind="danger"
onClick={() => {
defaultDispatcher.dispatch({
action: "leave_room",
room_id: space.roomId,
});
}}
>
{ _t("Leave Space") }
</AccessibleButton>
<div className="mx_SpaceSettingsDialog_buttons">
<AccessibleButton onClick={() => Modal.createDialog(DevtoolsDialog, {roomId: space.roomId})}>
{ _t("View dev tools") }
</AccessibleButton>
<AccessibleButton onClick={onFinished} disabled={busy} kind="link">
{ _t("Cancel") }
</AccessibleButton>
<AccessibleButton onClick={onSave} disabled={busy} kind="primary">
{ busy ? _t("Saving...") : _t("Save Changes") }
</AccessibleButton>
</div>
<div
className="mx_SpaceSettingsDialog_content"
id="mx_SpaceSettingsDialog"
title={_t("Settings - %(spaceName)s", { spaceName: space.name })}
>
<TabbedView tabs={tabs} />
</div>
</BaseDialog>;
};
export default SpaceSettingsDialog;

View file

@ -20,7 +20,7 @@ import * as sdk from '../../../index';
import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.dialogs.StorageEvictedDialog")
export default class StorageEvictedDialog extends React.Component {

View file

@ -16,13 +16,13 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import {Room} from "matrix-js-sdk/src/models/room";
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
import { Room } from "matrix-js-sdk/src/models/room";
import * as sdk from '../../../index';
import {dialogTermsInteractionCallback, TermsNotSignedError} from "../../../Terms";
import { dialogTermsInteractionCallback, TermsNotSignedError } from "../../../Terms";
import classNames from 'classnames';
import * as ScalarMessaging from "../../../ScalarMessaging";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.dialogs.TabbedIntegrationManagerDialog")
export default class TabbedIntegrationManagerDialog extends React.Component {
@ -63,11 +63,11 @@ export default class TabbedIntegrationManagerDialog extends React.Component {
};
}
componentDidMount(): void {
componentDidMount() {
this.openManager(0, true);
}
openManager = async (i: number, force = false) => {
openManager = async (i, force = false) => {
if (i === this.state.currentIndex && !force) return;
const manager = this.state.managers[i];

View file

@ -16,23 +16,22 @@ limitations under the License.
import url from 'url';
import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t, pickBestLanguage } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {SERVICE_TYPES} from "matrix-js-sdk/src/service-types";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types";
class TermsCheckbox extends React.PureComponent {
static propTypes = {
onChange: PropTypes.func.isRequired,
url: PropTypes.string.isRequired,
checked: PropTypes.bool.isRequired,
}
interface ITermsCheckboxProps {
onChange: (url: string, checked: boolean) => void;
url: string;
checked: boolean;
}
onChange = (ev) => {
this.props.onChange(this.props.url, ev.target.checked);
}
class TermsCheckbox extends React.PureComponent<ITermsCheckboxProps> {
private onChange = (ev: React.FormEvent<HTMLInputElement>): void => {
this.props.onChange(this.props.url, ev.currentTarget.checked);
};
render() {
return <input type="checkbox"
@ -42,30 +41,34 @@ class TermsCheckbox extends React.PureComponent {
}
}
interface ITermsDialogProps {
/**
* Array of [Service, policies] pairs, where policies is the response from the
* /terms endpoint for that service
*/
policiesAndServicePairs: any[],
/**
* urls that the user has already agreed to
*/
agreedUrls?: string[],
/**
* Called with:
* * success {bool} True if the user accepted any douments, false if cancelled
* * agreedUrls {string[]} List of agreed URLs
*/
onFinished: (success: boolean, agreedUrls?: string[]) => void,
}
interface IState {
agreedUrls: any;
}
@replaceableComponent("views.dialogs.TermsDialog")
export default class TermsDialog extends React.PureComponent {
static propTypes = {
/**
* Array of [Service, policies] pairs, where policies is the response from the
* /terms endpoint for that service
*/
policiesAndServicePairs: PropTypes.array.isRequired,
/**
* urls that the user has already agreed to
*/
agreedUrls: PropTypes.arrayOf(PropTypes.string),
/**
* Called with:
* * success {bool} True if the user accepted any douments, false if cancelled
* * agreedUrls {string[]} List of agreed URLs
*/
onFinished: PropTypes.func.isRequired,
}
export default class TermsDialog extends React.PureComponent<ITermsDialogProps, IState> {
constructor(props) {
super();
super(props);
this.state = {
// url -> boolean
agreedUrls: {},
@ -75,15 +78,15 @@ export default class TermsDialog extends React.PureComponent {
}
}
_onCancelClick = () => {
private onCancelClick = (): void => {
this.props.onFinished(false);
}
};
_onNextClick = () => {
private onNextClick = (): void => {
this.props.onFinished(true, Object.keys(this.state.agreedUrls).filter((url) => this.state.agreedUrls[url]));
}
};
_nameForServiceType(serviceType, host) {
private nameForServiceType(serviceType: SERVICE_TYPES, host: string): JSX.Element {
switch (serviceType) {
case SERVICE_TYPES.IS:
return <div>{_t("Identity Server")}<br />({host})</div>;
@ -92,7 +95,7 @@ export default class TermsDialog extends React.PureComponent {
}
}
_summaryForServiceType(serviceType) {
private summaryForServiceType(serviceType: SERVICE_TYPES): JSX.Element {
switch (serviceType) {
case SERVICE_TYPES.IS:
return <div>
@ -107,13 +110,13 @@ export default class TermsDialog extends React.PureComponent {
}
}
_onTermsCheckboxChange = (url, checked) => {
private onTermsCheckboxChange = (url: string, checked: boolean) => {
this.setState({
agreedUrls: Object.assign({}, this.state.agreedUrls, { [url]: checked }),
});
}
};
render() {
public render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
@ -128,8 +131,8 @@ export default class TermsDialog extends React.PureComponent {
let serviceName;
let summary;
if (i === 0) {
serviceName = this._nameForServiceType(policiesAndService.service.serviceType, parsedBaseUrl.host);
summary = this._summaryForServiceType(
serviceName = this.nameForServiceType(policiesAndService.service.serviceType, parsedBaseUrl.host);
summary = this.summaryForServiceType(
policiesAndService.service.serviceType,
);
}
@ -137,12 +140,15 @@ export default class TermsDialog extends React.PureComponent {
rows.push(<tr key={termDoc[termsLang].url}>
<td className="mx_TermsDialog_service">{serviceName}</td>
<td className="mx_TermsDialog_summary">{summary}</td>
<td>{termDoc[termsLang].name} <a rel="noreferrer noopener" target="_blank" href={termDoc[termsLang].url}>
<span className="mx_TermsDialog_link" />
</a></td>
<td>
{termDoc[termsLang].name}
<a rel="noreferrer noopener" target="_blank" href={termDoc[termsLang].url}>
<span className="mx_TermsDialog_link" />
</a>
</td>
<td><TermsCheckbox
url={termDoc[termsLang].url}
onChange={this._onTermsCheckboxChange}
onChange={this.onTermsCheckboxChange}
checked={Boolean(this.state.agreedUrls[termDoc[termsLang].url])}
/></td>
</tr>);
@ -176,7 +182,7 @@ export default class TermsDialog extends React.PureComponent {
return (
<BaseDialog
fixedWidth={false}
onFinished={this._onCancelClick}
onFinished={this.onCancelClick}
title={_t("Terms of Service")}
contentId='mx_Dialog_content'
hasCancel={false}
@ -197,8 +203,8 @@ export default class TermsDialog extends React.PureComponent {
<DialogButtons primaryButton={_t('Next')}
hasCancel={true}
onCancel={this._onCancelClick}
onPrimaryButtonClick={this._onNextClick}
onCancel={this.onCancelClick}
onPrimaryButtonClick={this.onNextClick}
primaryDisabled={!enableSubmit}
/>
</BaseDialog>

View file

@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {createRef} from 'react';
import React, { createRef } from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import Field from "../elements/Field";
import { _t, _td } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.dialogs.TextInputDialog")
export default class TextInputDialog extends React.Component {

View file

@ -30,7 +30,7 @@ interface IProps extends IDialogProps {
device: IDevice;
}
const UntrustedDeviceDialog: React.FC<IProps> = ({device, user, onFinished}) => {
const UntrustedDeviceDialog: React.FC<IProps> = ({ device, user, onFinished }) => {
let askToVerifyText;
let newSessionText;
@ -39,7 +39,7 @@ const UntrustedDeviceDialog: React.FC<IProps> = ({device, user, onFinished}) =>
askToVerifyText = _t("Verify your other session using one of the options below.");
} else {
newSessionText = _t("%(name)s (%(userId)s) signed in to a new session without verifying it:",
{name: user.displayName, userId: user.userId});
{ name: user.displayName, userId: user.userId });
askToVerifyText = _t("Ask this user to verify their session, or manually verify it below.");
}

View file

@ -36,7 +36,7 @@ export default class UploadConfirmDialog extends React.Component<IProps> {
static defaultProps = {
totalFiles: 1,
}
};
constructor(props) {
super(props);
@ -56,15 +56,15 @@ export default class UploadConfirmDialog extends React.Component<IProps> {
private onCancelClick = () => {
this.props.onFinished(false);
}
};
private onUploadClick = () => {
this.props.onFinished(true);
}
};
private onUploadAllClick = () => {
this.props.onFinished(true, true);
}
};
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');

View file

@ -21,7 +21,7 @@ import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import ContentMessages from '../../../ContentMessages';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
/*
* Tells the user about files we know cannot be uploaded before we even try uploading

View file

@ -16,11 +16,10 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import TabbedView, {Tab} from "../../structures/TabbedView";
import {_t, _td} from "../../../languageHandler";
import TabbedView, { Tab } from "../../structures/TabbedView";
import { _t, _td } from "../../../languageHandler";
import GeneralUserSettingsTab from "../settings/tabs/user/GeneralUserSettingsTab";
import SettingsStore from "../../../settings/SettingsStore";
import SettingsStore, { CallbackFn } from "../../../settings/SettingsStore";
import LabsUserSettingsTab from "../settings/tabs/user/LabsUserSettingsTab";
import AppearanceUserSettingsTab from "../settings/tabs/user/AppearanceUserSettingsTab";
import SecurityUserSettingsTab from "../settings/tabs/user/SecurityUserSettingsTab";
@ -32,79 +31,87 @@ import FlairUserSettingsTab from "../settings/tabs/user/FlairUserSettingsTab";
import * as sdk from "../../../index";
import SdkConfig from "../../../SdkConfig";
import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab";
import {UIFeature} from "../../../settings/UIFeature";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { UIFeature } from "../../../settings/UIFeature";
import { replaceableComponent } from "../../../utils/replaceableComponent";
export const USER_GENERAL_TAB = "USER_GENERAL_TAB";
export const USER_APPEARANCE_TAB = "USER_APPEARANCE_TAB";
export const USER_FLAIR_TAB = "USER_FLAIR_TAB";
export const USER_NOTIFICATIONS_TAB = "USER_NOTIFICATIONS_TAB";
export const USER_PREFERENCES_TAB = "USER_PREFERENCES_TAB";
export const USER_VOICE_TAB = "USER_VOICE_TAB";
export const USER_SECURITY_TAB = "USER_SECURITY_TAB";
export const USER_LABS_TAB = "USER_LABS_TAB";
export const USER_MJOLNIR_TAB = "USER_MJOLNIR_TAB";
export const USER_HELP_TAB = "USER_HELP_TAB";
export enum UserTab {
General = "USER_GENERAL_TAB",
Appearance = "USER_APPEARANCE_TAB",
Flair = "USER_FLAIR_TAB",
Notifications = "USER_NOTIFICATIONS_TAB",
Preferences = "USER_PREFERENCES_TAB",
Voice = "USER_VOICE_TAB",
Security = "USER_SECURITY_TAB",
Labs = "USER_LABS_TAB",
Mjolnir = "USER_MJOLNIR_TAB",
Help = "USER_HELP_TAB",
}
interface IProps {
onFinished: (success: boolean) => void;
initialTabId?: string;
}
interface IState {
mjolnirEnabled: boolean;
}
@replaceableComponent("views.dialogs.UserSettingsDialog")
export default class UserSettingsDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
initialTabId: PropTypes.string,
};
export default class UserSettingsDialog extends React.Component<IProps, IState> {
private mjolnirWatcher: string;
constructor() {
super();
constructor(props) {
super(props);
this.state = {
mjolnirEnabled: SettingsStore.getValue("feature_mjolnir"),
};
}
componentDidMount(): void {
this._mjolnirWatcher = SettingsStore.watchSetting("feature_mjolnir", null, this._mjolnirChanged.bind(this));
public componentDidMount(): void {
this.mjolnirWatcher = SettingsStore.watchSetting("feature_mjolnir", null, this.mjolnirChanged);
}
componentWillUnmount(): void {
SettingsStore.unwatchSetting(this._mjolnirWatcher);
public componentWillUnmount(): void {
SettingsStore.unwatchSetting(this.mjolnirWatcher);
}
_mjolnirChanged(settingName, roomId, atLevel, newValue) {
private mjolnirChanged: CallbackFn = (settingName, roomId, atLevel, newValue) => {
// We can cheat because we know what levels a feature is tracked at, and how it is tracked
this.setState({mjolnirEnabled: newValue});
}
this.setState({ mjolnirEnabled: newValue });
};
_getTabs() {
const tabs = [];
tabs.push(new Tab(
USER_GENERAL_TAB,
UserTab.General,
_td("General"),
"mx_UserSettingsDialog_settingsIcon",
<GeneralUserSettingsTab closeSettingsFn={this.props.onFinished} />,
));
tabs.push(new Tab(
USER_APPEARANCE_TAB,
UserTab.Appearance,
_td("Appearance"),
"mx_UserSettingsDialog_appearanceIcon",
<AppearanceUserSettingsTab />,
));
if (SettingsStore.getValue(UIFeature.Flair)) {
tabs.push(new Tab(
USER_FLAIR_TAB,
UserTab.Flair,
_td("Flair"),
"mx_UserSettingsDialog_flairIcon",
<FlairUserSettingsTab />,
));
}
tabs.push(new Tab(
USER_NOTIFICATIONS_TAB,
UserTab.Notifications,
_td("Notifications"),
"mx_UserSettingsDialog_bellIcon",
<NotificationUserSettingsTab />,
));
tabs.push(new Tab(
USER_PREFERENCES_TAB,
UserTab.Preferences,
_td("Preferences"),
"mx_UserSettingsDialog_preferencesIcon",
<PreferencesUserSettingsTab />,
@ -112,7 +119,7 @@ export default class UserSettingsDialog extends React.Component {
if (SettingsStore.getValue(UIFeature.Voip)) {
tabs.push(new Tab(
USER_VOICE_TAB,
UserTab.Voice,
_td("Voice & Video"),
"mx_UserSettingsDialog_voiceIcon",
<VoiceUserSettingsTab />,
@ -120,7 +127,7 @@ export default class UserSettingsDialog extends React.Component {
}
tabs.push(new Tab(
USER_SECURITY_TAB,
UserTab.Security,
_td("Security & Privacy"),
"mx_UserSettingsDialog_securityIcon",
<SecurityUserSettingsTab closeSettingsFn={this.props.onFinished} />,
@ -130,7 +137,7 @@ export default class UserSettingsDialog extends React.Component {
|| SettingsStore.getFeatureSettingNames().some(k => SettingsStore.getBetaInfo(k))
) {
tabs.push(new Tab(
USER_LABS_TAB,
UserTab.Labs,
_td("Labs"),
"mx_UserSettingsDialog_labsIcon",
<LabsUserSettingsTab />,
@ -138,17 +145,17 @@ export default class UserSettingsDialog extends React.Component {
}
if (this.state.mjolnirEnabled) {
tabs.push(new Tab(
USER_MJOLNIR_TAB,
UserTab.Mjolnir,
_td("Ignored users"),
"mx_UserSettingsDialog_mjolnirIcon",
<MjolnirUserSettingsTab />,
));
}
tabs.push(new Tab(
USER_HELP_TAB,
UserTab.Help,
_td("Help & About"),
"mx_UserSettingsDialog_helpIcon",
<HelpUserSettingsTab closeSettingsFn={this.props.onFinished} />,
<HelpUserSettingsTab closeSettingsFn={() => this.props.onFinished(true)} />,
));
return tabs;

View file

@ -1,5 +1,5 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2020-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,36 +15,40 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent";
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';
interface IProps {
verificationRequest: VerificationRequest;
verificationRequestPromise: Promise<VerificationRequest>;
onFinished: () => void;
member: User;
}
interface IState {
verificationRequest: VerificationRequest;
}
@replaceableComponent("views.dialogs.VerificationRequestDialog")
export default class VerificationRequestDialog extends React.Component {
static propTypes = {
verificationRequest: PropTypes.object,
verificationRequestPromise: PropTypes.object,
onFinished: PropTypes.func.isRequired,
member: PropTypes.string,
};
constructor(...args) {
super(...args);
this.state = {};
if (this.props.verificationRequest) {
this.state.verificationRequest = this.props.verificationRequest;
} else if (this.props.verificationRequestPromise) {
export default class VerificationRequestDialog extends React.Component<IProps, IState> {
constructor(props) {
super(props);
this.state = {
verificationRequest: this.props.verificationRequest,
};
if (this.props.verificationRequestPromise) {
this.props.verificationRequestPromise.then(r => {
this.setState({verificationRequest: r});
this.setState({ verificationRequest: r });
});
}
}
render() {
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel");
const request = this.state.verificationRequest;
const otherUserId = request && request.otherUserId;
const member = this.props.member ||
@ -65,6 +69,7 @@ export default class VerificationRequestDialog extends React.Component {
verificationRequestPromise={this.props.verificationRequestPromise}
onClose={this.props.onFinished}
member={member}
isRoomEncrypted={false}
/>
</BaseDialog>;
}

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