Merge branch 'develop' into text-for-event-perf
This commit is contained in:
commit
b147bcd207
238 changed files with 3504 additions and 2904 deletions
124
src/components/views/audio_messages/AudioPlayer.tsx
Normal file
124
src/components/views/audio_messages/AudioPlayer.tsx
Normal 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} />
|
||||
{/* 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>;
|
||||
}
|
||||
}
|
|
@ -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);
|
55
src/components/views/audio_messages/DurationClock.tsx
Normal file
55
src/components/views/audio_messages/DurationClock.tsx
Normal 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} />;
|
||||
}
|
||||
}
|
|
@ -1,9 +1,12 @@
|
|||
/*
|
||||
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.
|
||||
|
@ -12,16 +15,13 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import Clock from "./Clock";
|
||||
import { IRecordingUpdate, VoiceRecording } from "../../../voice/VoiceRecording";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Clock from "./Clock";
|
||||
import { MarkedExecution } from "../../../utils/MarkedExecution";
|
||||
import {
|
||||
IRecordingUpdate,
|
||||
VoiceRecording,
|
||||
} from "../../../voice/VoiceRecording";
|
||||
|
||||
interface IProps {
|
||||
recorder?: VoiceRecording;
|
||||
recorder: VoiceRecording;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -31,7 +31,7 @@ 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> {
|
||||
private seconds = 0;
|
||||
private scheduledUpdate = new MarkedExecution(
|
|
@ -1,9 +1,12 @@
|
|||
/*
|
||||
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.
|
||||
|
@ -12,16 +15,15 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import Waveform from "./Waveform";
|
||||
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";
|
||||
import {
|
||||
IRecordingUpdate,
|
||||
VoiceRecording,
|
||||
} from "../../../voice/VoiceRecording";
|
||||
|
||||
interface IProps {
|
||||
recorder?: VoiceRecording;
|
||||
recorder: VoiceRecording;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -31,7 +33,7 @@ interface IState {
|
|||
/**
|
||||
* A waveform which shows the waveform of a live recording
|
||||
*/
|
||||
@replaceableComponent("views.voice_messages.LiveRecordingWaveform")
|
||||
@replaceableComponent("views.audio_messages.LiveRecordingWaveform")
|
||||
export default class LiveRecordingWaveform extends React.PureComponent<IProps, IState> {
|
||||
public static defaultProps = {
|
||||
progress: 1,
|
||||
|
@ -52,15 +54,18 @@ export default class LiveRecordingWaveform extends React.PureComponent<IProps, I
|
|||
|
||||
componentDidMount() {
|
||||
this.props.recorder.liveData.onUpdate((update: IRecordingUpdate) => {
|
||||
this.waveform = update.waveform;
|
||||
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,
|
||||
});
|
||||
this.setState({ waveform: this.waveform });
|
||||
}
|
||||
|
||||
public render() {
|
|
@ -21,7 +21,8 @@ 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}
|
||||
/>;
|
||||
}
|
||||
}
|
|
@ -22,6 +22,11 @@ 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);
|
||||
|
@ -64,7 +69,11 @@ export default class PlaybackClock extends React.PureComponent<IProps, IState> {
|
|||
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} />;
|
||||
}
|
|
@ -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);
|
|
@ -20,6 +20,7 @@ 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);
|
||||
|
@ -53,7 +55,7 @@ export default class RecordingPlayback extends React.PureComponent<IProps, IStat
|
|||
};
|
||||
|
||||
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} />
|
112
src/components/views/audio_messages/SeekBar.tsx
Normal file
112
src/components/views/audio_messages/SeekBar.tsx
Normal 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}
|
||||
/>;
|
||||
}
|
||||
}
|
|
@ -17,8 +17,13 @@ limitations under the License.
|
|||
import React from "react";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import classNames from "classnames";
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
export interface IProps {
|
||||
interface WaveformCSSProperties extends CSSProperties {
|
||||
'--barHeight': number;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
relHeights: number[]; // relative heights (0-1)
|
||||
progress: number; // percent complete, 0-1, default 100%
|
||||
}
|
||||
|
@ -34,14 +39,7 @@ 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.
|
||||
*/
|
||||
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
export interface WaveformCSSProperties extends CSSProperties {
|
||||
'--barHeight': number;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.voice_messages.Waveform")
|
||||
@replaceableComponent("views.audio_messages.Waveform")
|
||||
export default class Waveform extends React.PureComponent<IProps, IState> {
|
||||
public static defaultProps = {
|
||||
progress: 1,
|
|
@ -18,7 +18,6 @@ import React, { ChangeEvent, createRef, FormEvent, MouseEvent } from 'react';
|
|||
import classNames from 'classnames';
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
|
@ -26,6 +25,8 @@ import Spinner from "../elements/Spinner";
|
|||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { LocalisedPolicy, Policies } from '../../../Terms';
|
||||
import Field from '../elements/Field';
|
||||
import CaptchaForm from "./CaptchaForm";
|
||||
|
||||
/* This file contains a collection of components which are used by the
|
||||
* InteractiveAuth to prompt the user to enter the information needed
|
||||
|
@ -164,8 +165,7 @@ export class PasswordAuthEntry extends React.Component<IAuthEntryProps, IPasswor
|
|||
|
||||
let submitButtonOrSpinner;
|
||||
if (this.props.busy) {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
submitButtonOrSpinner = <Loader />;
|
||||
submitButtonOrSpinner = <Spinner />;
|
||||
} else {
|
||||
submitButtonOrSpinner = (
|
||||
<input type="submit"
|
||||
|
@ -185,8 +185,6 @@ export class PasswordAuthEntry extends React.Component<IAuthEntryProps, IPasswor
|
|||
);
|
||||
}
|
||||
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>{ _t("Confirm your identity by entering your account password below.") }</p>
|
||||
|
@ -236,13 +234,11 @@ export class RecaptchaAuthEntry extends React.Component<IRecaptchaAuthEntryProps
|
|||
|
||||
render() {
|
||||
if (this.props.busy) {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
return <Loader />;
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
let errorText = this.props.errorText;
|
||||
|
||||
const CaptchaForm = sdk.getComponent("views.auth.CaptchaForm");
|
||||
let sitePublicKey;
|
||||
if (!this.props.stageParams || !this.props.stageParams.public_key) {
|
||||
errorText = _t(
|
||||
|
@ -390,8 +386,7 @@ export class TermsAuthEntry extends React.Component<ITermsAuthEntryProps, ITerms
|
|||
|
||||
render() {
|
||||
if (this.props.busy) {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
return <Loader />;
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const checkboxes = [];
|
||||
|
@ -590,8 +585,7 @@ export class MsisdnAuthEntry extends React.Component<IMsisdnAuthEntryProps, IMsi
|
|||
|
||||
render() {
|
||||
if (this.state.requestingToken) {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
return <Loader />;
|
||||
return <Spinner />;
|
||||
} else {
|
||||
const enableSubmit = Boolean(this.state.token);
|
||||
const submitClasses = classNames({
|
||||
|
|
|
@ -52,8 +52,8 @@ interface IProps {
|
|||
|
||||
interface IState {
|
||||
fieldValid: Partial<Record<LoginField, boolean>>;
|
||||
loginType: LoginField.Email | LoginField.MatrixId | LoginField.Phone,
|
||||
password: "",
|
||||
loginType: LoginField.Email | LoginField.MatrixId | LoginField.Phone;
|
||||
password: "";
|
||||
}
|
||||
|
||||
enum LoginField {
|
||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import * as Email from '../../../email';
|
||||
import { looksValid as phoneNumberLooksValid } from '../../../phonenumber';
|
||||
import Modal from '../../../Modal';
|
||||
|
@ -31,6 +30,7 @@ import CountlyAnalytics from "../../../CountlyAnalytics";
|
|||
import Field from '../elements/Field';
|
||||
import RegistrationEmailPromptDialog from '../dialogs/RegistrationEmailPromptDialog';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import CountryDropdown from "./CountryDropdown";
|
||||
|
||||
enum RegistrationField {
|
||||
Email = "field_email",
|
||||
|
@ -471,7 +471,6 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
|
|||
if (!this.showPhoneNumber()) {
|
||||
return null;
|
||||
}
|
||||
const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown');
|
||||
const phoneLabel = this.authStepIsRequired('m.login.msisdn') ?
|
||||
_t("Phone") :
|
||||
_t("Phone (optional)");
|
||||
|
|
|
@ -30,13 +30,14 @@ import { _t } from "../../../languageHandler";
|
|||
import TextWithTooltip from "../elements/TextWithTooltip";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { IOOBData } from "../../../stores/ThreepidInviteStore";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
avatarSize: number;
|
||||
displayBadge?: boolean;
|
||||
forceCount?: boolean;
|
||||
oobData?: object;
|
||||
oobData?: IOOBData;
|
||||
viewAvatarOnClick?: boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -24,14 +24,14 @@ import Modal from '../../../Modal';
|
|||
import * as Avatar from '../../../Avatar';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import { IOOBData } from '../../../stores/ThreepidInviteStore';
|
||||
|
||||
interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick"> {
|
||||
// Room may be left unset here, but if it is,
|
||||
// oobData.avatarUrl should be set (else there
|
||||
// would be nowhere to get the avatar from)
|
||||
room?: Room;
|
||||
// TODO: type when js-sdk has types
|
||||
oobData?: any;
|
||||
oobData?: IOOBData;
|
||||
width?: number;
|
||||
height?: number;
|
||||
resizeMethod?: ResizeMethod;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2015, 2016, 2018, 2019, 2021 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -16,12 +16,11 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { EventStatus } from 'matrix-js-sdk/src/models/event';
|
||||
import { EventStatus, MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { EventType, RelationType } from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Modal from '../../../Modal';
|
||||
import Resend from '../../../Resend';
|
||||
|
@ -29,53 +28,65 @@ import SettingsStore from '../../../settings/SettingsStore';
|
|||
import { isUrlPermitted } from '../../../HtmlUtils';
|
||||
import { isContentActionable } from '../../../utils/EventUtils';
|
||||
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";
|
||||
import ReportEventDialog from '../dialogs/ReportEventDialog';
|
||||
import ViewSource from '../../structures/ViewSource';
|
||||
import ConfirmRedactDialog from '../dialogs/ConfirmRedactDialog';
|
||||
import ErrorDialog from '../dialogs/ErrorDialog';
|
||||
import ShareDialog from '../dialogs/ShareDialog';
|
||||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||
|
||||
export function canCancel(eventStatus) {
|
||||
export function canCancel(eventStatus: EventStatus): boolean {
|
||||
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
|
||||
}
|
||||
|
||||
interface IEventTileOps {
|
||||
isWidgetHidden(): boolean;
|
||||
unhideWidget(): void;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
/* the MatrixEvent associated with the context menu */
|
||||
mxEvent: MatrixEvent;
|
||||
/* an optional EventTileOps implementation that can be used to unhide preview widgets */
|
||||
eventTileOps?: IEventTileOps;
|
||||
permalinkCreator?: RoomPermalinkCreator;
|
||||
/* an optional function to be called when the user clicks collapse thread, if not provided hide button */
|
||||
collapseReplyThread?(): void;
|
||||
/* callback called when the menu is dismissed */
|
||||
onFinished(): void;
|
||||
/* if the menu is inside a dialog, we sometimes need to close that dialog after click (forwarding) */
|
||||
onCloseDialog?(): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
canRedact: boolean;
|
||||
canPin: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.context_menus.MessageContextMenu")
|
||||
export default class MessageContextMenu extends React.Component {
|
||||
static propTypes = {
|
||||
/* the MatrixEvent associated with the context menu */
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
|
||||
/* an optional EventTileOps implementation that can be used to unhide preview widgets */
|
||||
eventTileOps: PropTypes.object,
|
||||
|
||||
/* an optional function to be called when the user clicks collapse thread, if not provided hide button */
|
||||
collapseReplyThread: PropTypes.func,
|
||||
|
||||
/* callback called when the menu is dismissed */
|
||||
onFinished: PropTypes.func,
|
||||
|
||||
/* if the menu is inside a dialog, we sometimes need to close that dialog after click (forwarding) */
|
||||
onCloseDialog: PropTypes.func,
|
||||
};
|
||||
|
||||
export default class MessageContextMenu extends React.Component<IProps, IState> {
|
||||
state = {
|
||||
canRedact: false,
|
||||
canPin: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
MatrixClientPeg.get().on('RoomMember.powerLevel', this._checkPermissions);
|
||||
this._checkPermissions();
|
||||
MatrixClientPeg.get().on('RoomMember.powerLevel', this.checkPermissions);
|
||||
this.checkPermissions();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli) {
|
||||
cli.removeListener('RoomMember.powerLevel', this._checkPermissions);
|
||||
cli.removeListener('RoomMember.powerLevel', this.checkPermissions);
|
||||
}
|
||||
}
|
||||
|
||||
_checkPermissions = () => {
|
||||
private checkPermissions = (): void => {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(this.props.mxEvent.getRoomId());
|
||||
|
||||
|
@ -93,7 +104,7 @@ export default class MessageContextMenu extends React.Component {
|
|||
this.setState({ canRedact, canPin });
|
||||
};
|
||||
|
||||
_isPinned() {
|
||||
private isPinned(): boolean {
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||
const pinnedEvent = room.currentState.getStateEvents(EventType.RoomPinnedEvents, '');
|
||||
if (!pinnedEvent) return false;
|
||||
|
@ -101,38 +112,35 @@ export default class MessageContextMenu extends React.Component {
|
|||
return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId());
|
||||
}
|
||||
|
||||
onResendReactionsClick = () => {
|
||||
for (const reaction of this._getUnsentReactions()) {
|
||||
private onResendReactionsClick = (): void => {
|
||||
for (const reaction of this.getUnsentReactions()) {
|
||||
Resend.resend(reaction);
|
||||
}
|
||||
this.closeMenu();
|
||||
};
|
||||
|
||||
onReportEventClick = () => {
|
||||
const ReportEventDialog = sdk.getComponent("dialogs.ReportEventDialog");
|
||||
private onReportEventClick = (): void => {
|
||||
Modal.createTrackedDialog('Report Event', '', ReportEventDialog, {
|
||||
mxEvent: this.props.mxEvent,
|
||||
}, 'mx_Dialog_reportEvent');
|
||||
this.closeMenu();
|
||||
};
|
||||
|
||||
onViewSourceClick = () => {
|
||||
const ViewSource = sdk.getComponent('structures.ViewSource');
|
||||
private onViewSourceClick = (): void => {
|
||||
Modal.createTrackedDialog('View Event Source', '', ViewSource, {
|
||||
mxEvent: this.props.mxEvent,
|
||||
}, 'mx_Dialog_viewsource');
|
||||
this.closeMenu();
|
||||
};
|
||||
|
||||
onRedactClick = () => {
|
||||
const ConfirmRedactDialog = sdk.getComponent("dialogs.ConfirmRedactDialog");
|
||||
private onRedactClick = (): void => {
|
||||
Modal.createTrackedDialog('Confirm Redact Dialog', '', ConfirmRedactDialog, {
|
||||
onFinished: async (proceed, reason) => {
|
||||
onFinished: async (proceed: boolean, reason?: string) => {
|
||||
if (!proceed) return;
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
try {
|
||||
if (this.props.onCloseDialog) this.props.onCloseDialog();
|
||||
this.props.onCloseDialog?.();
|
||||
await cli.redactEvent(
|
||||
this.props.mxEvent.getRoomId(),
|
||||
this.props.mxEvent.getId(),
|
||||
|
@ -145,7 +153,6 @@ export default class MessageContextMenu extends React.Component {
|
|||
// (e.g. no errcode or statusCode) as in that case the redactions end up in the
|
||||
// detached queue and we show the room status bar to allow retry
|
||||
if (typeof code !== "undefined") {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
// display error message stating you couldn't delete this.
|
||||
Modal.createTrackedDialog('You cannot delete this message', '', ErrorDialog, {
|
||||
title: _t('Error'),
|
||||
|
@ -158,7 +165,7 @@ export default class MessageContextMenu extends React.Component {
|
|||
this.closeMenu();
|
||||
};
|
||||
|
||||
onForwardClick = () => {
|
||||
private onForwardClick = (): void => {
|
||||
Modal.createTrackedDialog('Forward Message', '', ForwardDialog, {
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
event: this.props.mxEvent,
|
||||
|
@ -167,12 +174,12 @@ export default class MessageContextMenu extends React.Component {
|
|||
this.closeMenu();
|
||||
};
|
||||
|
||||
onPinClick = () => {
|
||||
private onPinClick = (): void => {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(this.props.mxEvent.getRoomId());
|
||||
const eventId = this.props.mxEvent.getId();
|
||||
|
||||
const pinnedIds = room?.currentState?.getStateEvents(EventType.RoomPinnedEvents, "")?.pinned || [];
|
||||
const pinnedIds = room?.currentState?.getStateEvents(EventType.RoomPinnedEvents, "")?.getContent().pinned || [];
|
||||
if (pinnedIds.includes(eventId)) {
|
||||
pinnedIds.splice(pinnedIds.indexOf(eventId), 1);
|
||||
} else {
|
||||
|
@ -188,18 +195,16 @@ export default class MessageContextMenu extends React.Component {
|
|||
this.closeMenu();
|
||||
};
|
||||
|
||||
closeMenu = () => {
|
||||
if (this.props.onFinished) this.props.onFinished();
|
||||
private closeMenu = (): void => {
|
||||
this.props.onFinished();
|
||||
};
|
||||
|
||||
onUnhidePreviewClick = () => {
|
||||
if (this.props.eventTileOps) {
|
||||
this.props.eventTileOps.unhideWidget();
|
||||
}
|
||||
private onUnhidePreviewClick = (): void => {
|
||||
this.props.eventTileOps?.unhideWidget();
|
||||
this.closeMenu();
|
||||
};
|
||||
|
||||
onQuoteClick = () => {
|
||||
private onQuoteClick = (): void => {
|
||||
dis.dispatch({
|
||||
action: Action.ComposerInsert,
|
||||
event: this.props.mxEvent,
|
||||
|
@ -207,9 +212,8 @@ export default class MessageContextMenu extends React.Component {
|
|||
this.closeMenu();
|
||||
};
|
||||
|
||||
onPermalinkClick = (e) => {
|
||||
private onPermalinkClick = (e: React.MouseEvent): void => {
|
||||
e.preventDefault();
|
||||
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
|
||||
Modal.createTrackedDialog('share room message dialog', '', ShareDialog, {
|
||||
target: this.props.mxEvent,
|
||||
permalinkCreator: this.props.permalinkCreator,
|
||||
|
@ -217,30 +221,27 @@ export default class MessageContextMenu extends React.Component {
|
|||
this.closeMenu();
|
||||
};
|
||||
|
||||
onCollapseReplyThreadClick = () => {
|
||||
private onCollapseReplyThreadClick = (): void => {
|
||||
this.props.collapseReplyThread();
|
||||
this.closeMenu();
|
||||
};
|
||||
|
||||
_getReactions(filter) {
|
||||
private getReactions(filter: (e: MatrixEvent) => boolean): MatrixEvent[] {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(this.props.mxEvent.getRoomId());
|
||||
const eventId = this.props.mxEvent.getId();
|
||||
return room.getPendingEvents().filter(e => {
|
||||
const relation = e.getRelation();
|
||||
return relation &&
|
||||
relation.rel_type === "m.annotation" &&
|
||||
relation.event_id === eventId &&
|
||||
filter(e);
|
||||
return relation?.rel_type === RelationType.Annotation && relation.event_id === eventId && filter(e);
|
||||
});
|
||||
}
|
||||
|
||||
_getPendingReactions() {
|
||||
return this._getReactions(e => canCancel(e.status));
|
||||
private getPendingReactions(): MatrixEvent[] {
|
||||
return this.getReactions(e => canCancel(e.status));
|
||||
}
|
||||
|
||||
_getUnsentReactions() {
|
||||
return this._getReactions(e => e.status === EventStatus.NOT_SENT);
|
||||
private getUnsentReactions(): MatrixEvent[] {
|
||||
return this.getReactions(e => e.status === EventStatus.NOT_SENT);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -248,16 +249,17 @@ export default class MessageContextMenu extends React.Component {
|
|||
const me = cli.getUserId();
|
||||
const mxEvent = this.props.mxEvent;
|
||||
const eventStatus = mxEvent.status;
|
||||
const unsentReactionsCount = this._getUnsentReactions().length;
|
||||
let resendReactionsButton;
|
||||
let redactButton;
|
||||
let forwardButton;
|
||||
let pinButton;
|
||||
let unhidePreviewButton;
|
||||
let externalURLButton;
|
||||
let quoteButton;
|
||||
let collapseReplyThread;
|
||||
let redactItemList;
|
||||
const unsentReactionsCount = this.getUnsentReactions().length;
|
||||
|
||||
let resendReactionsButton: JSX.Element;
|
||||
let redactButton: JSX.Element;
|
||||
let forwardButton: JSX.Element;
|
||||
let pinButton: JSX.Element;
|
||||
let unhidePreviewButton: JSX.Element;
|
||||
let externalURLButton: JSX.Element;
|
||||
let quoteButton: JSX.Element;
|
||||
let collapseReplyThread: JSX.Element;
|
||||
let redactItemList: JSX.Element;
|
||||
|
||||
// status is SENT before remote-echo, null after
|
||||
const isSent = !eventStatus || eventStatus === EventStatus.SENT;
|
||||
|
@ -296,7 +298,7 @@ export default class MessageContextMenu extends React.Component {
|
|||
pinButton = (
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_MessageContextMenu_iconPin"
|
||||
label={ this._isPinned() ? _t('Unpin') : _t('Pin') }
|
||||
label={ this.isPinned() ? _t('Unpin') : _t('Pin') }
|
||||
onClick={this.onPinClick}
|
||||
/>
|
||||
);
|
||||
|
@ -327,16 +329,20 @@ export default class MessageContextMenu extends React.Component {
|
|||
if (this.props.permalinkCreator) {
|
||||
permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
|
||||
}
|
||||
// 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 = (
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_MessageContextMenu_iconPermalink"
|
||||
onClick={this.onPermalinkClick}
|
||||
label= {_t('Share')}
|
||||
element="a"
|
||||
href={permalink}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
{
|
||||
// XXX: Typescript signature for AccessibleButton doesn't work properly for non-inputs like `a`
|
||||
...{
|
||||
href: permalink,
|
||||
target: "_blank",
|
||||
rel: "noreferrer noopener",
|
||||
}
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -351,8 +357,8 @@ export default class MessageContextMenu extends React.Component {
|
|||
}
|
||||
|
||||
// Bridges can provide a 'external_url' to link back to the source.
|
||||
if (typeof (mxEvent.event.content.external_url) === "string" &&
|
||||
isUrlPermitted(mxEvent.event.content.external_url)
|
||||
if (typeof (mxEvent.getContent().external_url) === "string" &&
|
||||
isUrlPermitted(mxEvent.getContent().external_url)
|
||||
) {
|
||||
externalURLButton = (
|
||||
<IconizedContextMenuOption
|
||||
|
@ -360,9 +366,14 @@ export default class MessageContextMenu extends React.Component {
|
|||
onClick={this.closeMenu}
|
||||
label={ _t('Source URL') }
|
||||
element="a"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
href={mxEvent.event.content.external_url}
|
||||
{
|
||||
// XXX: Typescript signature for AccessibleButton doesn't work properly for non-inputs like `a`
|
||||
...{
|
||||
target: "_blank",
|
||||
rel: "noreferrer noopener",
|
||||
href: mxEvent.getContent().external_url,
|
||||
}
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -377,7 +388,7 @@ export default class MessageContextMenu extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
let reportEventButton;
|
||||
let reportEventButton: JSX.Element;
|
||||
if (mxEvent.getSender() !== me) {
|
||||
reportEventButton = (
|
||||
<IconizedContextMenuOption
|
|
@ -18,6 +18,7 @@ import React, { ReactNode, useContext, useMemo, useState } from "react";
|
|||
import classNames from "classnames";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
|
@ -29,7 +30,6 @@ import RoomAvatar from "../avatars/RoomAvatar";
|
|||
import { getDisplayAliasForRoom } from "../../../Rooms";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import { sleep } from "../../../utils/promise";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import { calculateRoomVia } from "../../../utils/permalinks/Permalinks";
|
||||
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||
|
|
|
@ -19,6 +19,7 @@ limitations under the License.
|
|||
|
||||
import React, { createRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
|
@ -30,7 +31,6 @@ 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";
|
||||
|
|
|
@ -15,11 +15,11 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
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 BaseDialog from "./BaseDialog";
|
||||
|
||||
interface IProps {
|
||||
unknownProfileUsers: Array<{
|
||||
|
@ -50,8 +50,6 @@ export default class AskInviteAnywayDialog extends React.Component<IProps> {
|
|||
};
|
||||
|
||||
public render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
const errorList = this.props.unknownProfileUsers
|
||||
.map(address => <li key={address.userId}>{address.userId}: {address.errorText}</li>);
|
||||
|
||||
|
|
|
@ -18,13 +18,17 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
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 AccessibleButton from "../elements/AccessibleButton";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import QuestionDialog from "./QuestionDialog";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import Field from '../elements/Field';
|
||||
import Spinner from "../elements/Spinner";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
|
@ -93,7 +97,6 @@ export default class BugReportDialog extends React.Component<IProps, IState> {
|
|||
}).then(() => {
|
||||
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
|
||||
Modal.createTrackedDialog('Bug report sent', '', QuestionDialog, {
|
||||
title: _t('Logs sent'),
|
||||
|
@ -160,11 +163,6 @@ export default class BugReportDialog extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
public render() {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
|
||||
let error = null;
|
||||
if (this.state.err) {
|
||||
error = <div className="error">
|
||||
|
@ -176,7 +174,7 @@ export default class BugReportDialog extends React.Component<IProps, IState> {
|
|||
if (this.state.busy) {
|
||||
progress = (
|
||||
<div className="progress">
|
||||
<Loader />
|
||||
<Spinner />
|
||||
{this.state.progress} ...
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -16,9 +16,10 @@ Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import * as sdk from '../../../index';
|
||||
import request from 'browser-request';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import QuestionDialog from "./QuestionDialog";
|
||||
import Spinner from "../elements/Spinner";
|
||||
|
||||
interface IProps {
|
||||
newVersion: string;
|
||||
|
@ -65,9 +66,6 @@ export default class ChangelogDialog extends React.Component<IProps> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const Spinner = sdk.getComponent('views.elements.Spinner');
|
||||
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||
|
||||
const logs = REPOS.map(repo => {
|
||||
let content;
|
||||
if (this.state[repo] == null) {
|
||||
|
|
|
@ -15,9 +15,12 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import ConfirmRedactDialog from './ConfirmRedactDialog';
|
||||
import ErrorDialog from './ErrorDialog';
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import Spinner from "../elements/Spinner";
|
||||
|
||||
interface IProps {
|
||||
redact: () => Promise<void>;
|
||||
|
@ -73,7 +76,6 @@ export default class ConfirmAndWaitRedactDialog extends React.PureComponent<IPro
|
|||
public render() {
|
||||
if (this.state.isRedacting) {
|
||||
if (this.state.redactionErrorCode) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const code = this.state.redactionErrorCode;
|
||||
return (
|
||||
<ErrorDialog
|
||||
|
@ -83,8 +85,6 @@ export default class ConfirmAndWaitRedactDialog extends React.PureComponent<IPro
|
|||
/>
|
||||
);
|
||||
} else {
|
||||
const BaseDialog = sdk.getComponent("dialogs.BaseDialog");
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
return (
|
||||
<BaseDialog
|
||||
onFinished={this.props.onFinished}
|
||||
|
@ -95,7 +95,6 @@ export default class ConfirmAndWaitRedactDialog extends React.PureComponent<IPro
|
|||
);
|
||||
}
|
||||
} else {
|
||||
const ConfirmRedactDialog = sdk.getComponent("dialogs.ConfirmRedactDialog");
|
||||
return <ConfirmRedactDialog onFinished={this.onParentFinished} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,9 +15,9 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import TextInputDialog from "./TextInputDialog";
|
||||
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
|
@ -29,7 +29,6 @@ interface IProps {
|
|||
@replaceableComponent("views.dialogs.ConfirmRedactDialog")
|
||||
export default class ConfirmRedactDialog extends React.Component<IProps> {
|
||||
render() {
|
||||
const TextInputDialog = sdk.getComponent('views.dialogs.TextInputDialog');
|
||||
return (
|
||||
<TextInputDialog onFinished={this.props.onFinished}
|
||||
title={_t("Confirm Removal")}
|
||||
|
|
|
@ -17,11 +17,14 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
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 MemberAvatar from '../avatars/MemberAvatar';
|
||||
import BaseAvatar from '../avatars/BaseAvatar';
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
|
||||
interface IProps {
|
||||
// matrix-js-sdk (room) member object. Supply either this or 'groupMember'
|
||||
|
@ -29,7 +32,7 @@ interface IProps {
|
|||
// group member object. Supply either this or 'member'
|
||||
groupMember: GroupMemberType;
|
||||
// needed if a group member is specified
|
||||
matrixClient?: MatrixClient,
|
||||
matrixClient?: MatrixClient;
|
||||
action: string; // eg. 'Ban'
|
||||
title: string; // eg. 'Ban this user?'
|
||||
|
||||
|
@ -67,11 +70,6 @@ export default class ConfirmUserActionDialog extends React.Component<IProps> {
|
|||
};
|
||||
|
||||
public render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
|
||||
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
|
||||
|
||||
const confirmButtonClass = this.props.danger ? 'danger' : '';
|
||||
|
||||
let reasonBox;
|
||||
|
|
|
@ -16,8 +16,9 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import { _t } from "../../../languageHandler";
|
||||
import * as sdk from "../../../index";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
|
@ -34,9 +35,6 @@ export default class ConfirmWipeDeviceDialog extends React.Component<IProps> {
|
|||
};
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className='mx_ConfirmWipeDeviceDialog'
|
||||
|
|
|
@ -15,11 +15,12 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import * as sdk from '../../../index';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import Spinner from "../elements/Spinner";
|
||||
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
|
@ -106,9 +107,6 @@ export default class CreateGroupDialog extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
|
||||
if (this.state.creating) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
|
|
@ -16,21 +16,22 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import * as sdk from '../../../index';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import Modal from '../../../Modal';
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
import QuestionDialog from "./QuestionDialog";
|
||||
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
}
|
||||
|
||||
export default (props: IProps) => {
|
||||
const CryptoStoreTooNewDialog: React.FC<IProps> = (props: IProps) => {
|
||||
const brand = SdkConfig.get().brand;
|
||||
|
||||
const _onLogoutClicked = () => {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('Logout e2e db too new', '', QuestionDialog, {
|
||||
title: _t("Sign out"),
|
||||
description: _t(
|
||||
|
@ -58,8 +59,6 @@ export default (props: IProps) => {
|
|||
{ brand },
|
||||
);
|
||||
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return (<BaseDialog className="mx_CryptoStoreTooNewDialog"
|
||||
contentId='mx_Dialog_content'
|
||||
title={_t("Incompatible Database")}
|
||||
|
@ -79,3 +78,5 @@ export default (props: IProps) => {
|
|||
</DialogButtons>
|
||||
</BaseDialog>);
|
||||
};
|
||||
|
||||
export default CryptoStoreTooNewDialog;
|
||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import Analytics from '../../../Analytics';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import * as Lifecycle from '../../../Lifecycle';
|
||||
|
@ -26,6 +25,7 @@ import InteractiveAuth, { ERROR_USER_CANCELLED } from "../../structures/Interact
|
|||
import { DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry } from "../auth/InteractiveAuthEntryComponents";
|
||||
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
|
@ -165,8 +165,6 @@ export default class DeactivateAccountDialog extends React.Component<IProps, ISt
|
|||
}
|
||||
|
||||
public render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
let error = null;
|
||||
if (this.state.errStr) {
|
||||
error = <div className="error">
|
||||
|
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, { useState, useEffect, ChangeEvent, MouseEvent } from 'react';
|
||||
import * as sdk from '../../../index';
|
||||
import SyntaxHighlight from '../elements/SyntaxHighlight';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Field from "../elements/Field";
|
||||
|
@ -42,6 +41,8 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
|||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { SettingLevel } from '../../../settings/SettingLevel';
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import TruncatedList from "../elements/TruncatedList";
|
||||
|
||||
interface IGenericEditorProps {
|
||||
onBack: () => void;
|
||||
|
@ -369,7 +370,6 @@ class FilteredList extends React.PureComponent<IFilteredListProps, IFilteredList
|
|||
};
|
||||
|
||||
render() {
|
||||
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
||||
return <div>
|
||||
<Field label={_t('Filter results')} autoFocus={true} size={64}
|
||||
type="text" autoComplete="off" value={this.props.query} onChange={this.onQuery}
|
||||
|
@ -1261,7 +1261,6 @@ export default class DevtoolsDialog extends React.PureComponent<IProps, IState>
|
|||
</React.Fragment>;
|
||||
}
|
||||
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished} title={_t('Developer Tools')}>
|
||||
{ body }
|
||||
|
|
|
@ -26,9 +26,9 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
|
@ -57,7 +57,6 @@ export default class ErrorDialog extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
public render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_ErrorDialog"
|
||||
|
|
|
@ -18,7 +18,6 @@ import React, { createRef } from 'react';
|
|||
import classNames from 'classnames';
|
||||
|
||||
import { _t, _td } from "../../../languageHandler";
|
||||
import * as sdk from "../../../index";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { makeRoomPermalink, makeUserPermalink } from "../../../utils/permalinks/Permalinks";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
|
@ -65,14 +64,17 @@ import { copyPlaintext, selectText } from "../../../utils/strings";
|
|||
import * as ContextMenu from "../../structures/ContextMenu";
|
||||
import { toRightOf } from "../../structures/ContextMenu";
|
||||
import GenericTextContextMenu from "../context_menus/GenericTextContextMenu";
|
||||
import QuestionDialog from "./QuestionDialog";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
|
||||
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
interface IRecentUser {
|
||||
userId: string,
|
||||
user: RoomMember,
|
||||
lastActive: number,
|
||||
userId: string;
|
||||
user: RoomMember;
|
||||
lastActive: number;
|
||||
}
|
||||
|
||||
export const KIND_DM = "dm";
|
||||
|
@ -330,16 +332,16 @@ interface IInviteDialogProps {
|
|||
|
||||
// The kind of invite being performed. Assumed to be KIND_DM if
|
||||
// not provided.
|
||||
kind: string,
|
||||
kind: string;
|
||||
|
||||
// The room ID this dialog is for. Only required for KIND_INVITE.
|
||||
roomId: string,
|
||||
roomId: string;
|
||||
|
||||
// The call to transfer. Only required for KIND_CALL_TRANSFER.
|
||||
call: MatrixCall,
|
||||
call: MatrixCall;
|
||||
|
||||
// Initial value to populate the filter with
|
||||
initialText: string,
|
||||
initialText: string;
|
||||
}
|
||||
|
||||
interface IInviteDialogState {
|
||||
|
@ -356,8 +358,8 @@ interface IInviteDialogState {
|
|||
consultFirst: boolean;
|
||||
|
||||
// These two flags are used for the 'Go' button to communicate what is going on.
|
||||
busy: boolean,
|
||||
errorText: string,
|
||||
busy: boolean;
|
||||
errorText: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.InviteDialog")
|
||||
|
@ -1046,7 +1048,6 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
if (this.unmounted) return;
|
||||
|
||||
if (failed.length > 0) {
|
||||
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||
Modal.createTrackedDialog('Invite Paste Fail', '', QuestionDialog, {
|
||||
title: _t('Failed to find the following users'),
|
||||
description: _t(
|
||||
|
@ -1158,7 +1159,6 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
const toRender = sourceMembers.slice(0, showNum);
|
||||
const hasMore = toRender.length < sourceMembers.length;
|
||||
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
let showMore = null;
|
||||
if (hasMore) {
|
||||
showMore = (
|
||||
|
@ -1269,10 +1269,6 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
};
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
|
||||
let spinner = null;
|
||||
if (this.state.busy) {
|
||||
spinner = <Spinner w={20} h={20} />;
|
||||
|
|
|
@ -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 didn’t 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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -15,7 +15,6 @@ 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";
|
||||
|
@ -26,6 +25,10 @@ import Markdown from '../../../Markdown';
|
|||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import StyledRadioButton from "../elements/StyledRadioButton";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
import Field from "../elements/Field";
|
||||
import Spinner from "../elements/Spinner";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
mxEvent: MatrixEvent;
|
||||
|
@ -239,11 +242,6 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
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">
|
||||
|
@ -255,7 +253,7 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
|
|||
if (this.state.busy) {
|
||||
progress = (
|
||||
<div className="progress">
|
||||
<Loader />
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -24,12 +24,12 @@ import GeneralRoomSettingsTab from "../settings/tabs/room/GeneralRoomSettingsTab
|
|||
import SecurityRoomSettingsTab from "../settings/tabs/room/SecurityRoomSettingsTab";
|
||||
import NotificationSettingsTab from "../settings/tabs/room/NotificationSettingsTab";
|
||||
import BridgeSettingsTab from "../settings/tabs/room/BridgeSettingsTab";
|
||||
import * as sdk from "../../../index";
|
||||
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 BaseDialog from "./BaseDialog";
|
||||
|
||||
export const ROOM_GENERAL_TAB = "ROOM_GENERAL_TAB";
|
||||
export const ROOM_SECURITY_TAB = "ROOM_SECURITY_TAB";
|
||||
|
@ -119,8 +119,6 @@ export default class RoomSettingsDialog extends React.Component<IProps> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
const roomName = MatrixClientPeg.get().getRoom(this.props.roomId).name;
|
||||
return (
|
||||
<BaseDialog
|
||||
|
|
|
@ -22,7 +22,6 @@ 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";
|
||||
|
@ -35,6 +34,8 @@ import { IDialogProps } from "./IDialogProps";
|
|||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import GenericTextContextMenu from "../context_menus/GenericTextContextMenu.js";
|
||||
|
||||
const socials = [
|
||||
{
|
||||
|
@ -119,7 +120,6 @@ 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, {
|
||||
...toRightOf(buttonRect, 2),
|
||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||
|
@ -230,7 +230,6 @@ export default class ShareDialog extends React.PureComponent<IProps, IState> {
|
|||
</>;
|
||||
}
|
||||
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return <BaseDialog
|
||||
title={title}
|
||||
className='mx_ShareDialog'
|
||||
|
|
|
@ -16,11 +16,12 @@ limitations under the License.
|
|||
|
||||
import url from 'url';
|
||||
import React from 'react';
|
||||
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 DialogButtons from "../elements/DialogButtons";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
|
||||
interface ITermsCheckboxProps {
|
||||
onChange: (url: string, checked: boolean) => void;
|
||||
|
@ -46,19 +47,19 @@ interface ITermsDialogProps {
|
|||
* Array of [Service, policies] pairs, where policies is the response from the
|
||||
* /terms endpoint for that service
|
||||
*/
|
||||
policiesAndServicePairs: any[],
|
||||
policiesAndServicePairs: any[];
|
||||
|
||||
/**
|
||||
* urls that the user has already agreed to
|
||||
*/
|
||||
agreedUrls?: string[],
|
||||
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,
|
||||
onFinished: (success: boolean, agreedUrls?: string[]) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -117,9 +118,6 @@ export default class TermsDialog extends React.PureComponent<ITermsDialogProps,
|
|||
};
|
||||
|
||||
public render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
||||
const rows = [];
|
||||
for (const policiesAndService of this.props.policiesAndServicePairs) {
|
||||
const parsedBaseUrl = url.parse(policiesAndService.service.baseUrl);
|
||||
|
|
|
@ -16,11 +16,12 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import filesize from "filesize";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { getBlobSafeMimeType } from '../../../utils/blobs';
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
|
||||
interface IProps {
|
||||
file: File;
|
||||
|
@ -67,9 +68,6 @@ export default class UploadConfirmDialog extends React.Component<IProps> {
|
|||
};
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
||||
let title;
|
||||
if (this.props.totalFiles > 1 && this.props.currentIndex !== undefined) {
|
||||
title = _t(
|
||||
|
|
|
@ -28,11 +28,11 @@ import PreferencesUserSettingsTab from "../settings/tabs/user/PreferencesUserSet
|
|||
import VoiceUserSettingsTab from "../settings/tabs/user/VoiceUserSettingsTab";
|
||||
import HelpUserSettingsTab from "../settings/tabs/user/HelpUserSettingsTab";
|
||||
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 BaseDialog from "./BaseDialog";
|
||||
|
||||
export enum UserTab {
|
||||
General = "USER_GENERAL_TAB",
|
||||
|
@ -162,8 +162,6 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
|
|||
}
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className='mx_UserSettingsDialog'
|
||||
|
|
|
@ -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,27 +15,33 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
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 });
|
||||
});
|
||||
|
@ -43,8 +49,6 @@ export default class VerificationRequestDialog extends React.Component {
|
|||
}
|
||||
|
||||
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>;
|
||||
}
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
import { debounce } from "lodash";
|
||||
import classNames from 'classnames';
|
||||
import React, { ChangeEvent, FormEvent } from 'react';
|
||||
import { ISecretStorageKeyInfo } from "matrix-js-sdk/src";
|
||||
import { ISecretStorageKeyInfo } from "matrix-js-sdk/src/crypto/api";
|
||||
|
||||
import * as sdk from '../../../../index';
|
||||
import { MatrixClientPeg } from '../../../../MatrixClientPeg';
|
||||
|
|
|
@ -16,8 +16,9 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import * as sdk from "../../../../index";
|
||||
import { replaceableComponent } from "../../../../utils/replaceableComponent";
|
||||
import BaseDialog from "../BaseDialog";
|
||||
import DialogButtons from "../../elements/DialogButtons";
|
||||
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
|
@ -34,9 +35,6 @@ export default class ConfirmDestroyCrossSigningDialog extends React.Component<IP
|
|||
};
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className='mx_ConfirmDestroyCrossSigningDialog'
|
||||
|
|
|
@ -41,7 +41,8 @@ import QuestionDialog from "../dialogs/QuestionDialog";
|
|||
import UIStore from "../../../stores/UIStore";
|
||||
import { compare } from "../../../utils/strings";
|
||||
|
||||
export const ALL_ROOMS = Symbol("ALL_ROOMS");
|
||||
// XXX: We would ideally use a symbol here but we can't since we save this value to localStorage
|
||||
export const ALL_ROOMS = "ALL_ROOMS";
|
||||
|
||||
const SETTING_NAME = "room_directory_servers";
|
||||
|
||||
|
@ -94,8 +95,7 @@ export interface IInstance {
|
|||
fields: object;
|
||||
network_id: string;
|
||||
// XXX: this is undocumented but we rely on it.
|
||||
// we inject a fake entry with a symbolic instance_id.
|
||||
instance_id: string | symbol;
|
||||
instance_id: string;
|
||||
}
|
||||
|
||||
export interface IProtocol {
|
||||
|
@ -112,8 +112,8 @@ export type Protocols = Record<string, IProtocol>;
|
|||
interface IProps {
|
||||
protocols: Protocols;
|
||||
selectedServerName: string;
|
||||
selectedInstanceId: string | symbol;
|
||||
onOptionChange(server: string, instanceId?: string | symbol): void;
|
||||
selectedInstanceId: string;
|
||||
onOptionChange(server: string, instanceId?: string): void;
|
||||
}
|
||||
|
||||
// This dropdown sources homeservers from three places:
|
||||
|
@ -171,7 +171,7 @@ const NetworkDropdown = ({ onOptionChange, protocols = {}, selectedServerName, s
|
|||
|
||||
const protocolsList = server === hsName ? Object.values(protocols) : [];
|
||||
if (protocolsList.length > 0) {
|
||||
// add a fake protocol with the ALL_ROOMS symbol
|
||||
// add a fake protocol with ALL_ROOMS
|
||||
protocolsList.push({
|
||||
instances: [{
|
||||
fields: [],
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { ReactHTML } from 'react';
|
||||
|
||||
import { Key } from '../../../Keyboard';
|
||||
import classnames from 'classnames';
|
||||
|
@ -29,7 +29,7 @@ export type ButtonEvent = React.MouseEvent<Element> | React.KeyboardEvent<Elemen
|
|||
*/
|
||||
interface IProps extends React.InputHTMLAttributes<Element> {
|
||||
inputRef?: React.Ref<Element>;
|
||||
element?: string;
|
||||
element?: keyof ReactHTML;
|
||||
// The kind of button, similar to how Bootstrap works.
|
||||
// See available classes for AccessibleButton for options.
|
||||
kind?: string;
|
||||
|
@ -122,7 +122,7 @@ export default function AccessibleButton({
|
|||
}
|
||||
|
||||
AccessibleButton.defaultProps = {
|
||||
element: 'div',
|
||||
element: 'div' as keyof ReactHTML,
|
||||
role: 'button',
|
||||
tabIndex: 0,
|
||||
};
|
||||
|
|
|
@ -29,7 +29,7 @@ interface IProps {
|
|||
// The minimum number of events needed to trigger summarisation
|
||||
threshold?: number;
|
||||
// Whether or not to begin with state.expanded=true
|
||||
startExpanded?: boolean,
|
||||
startExpanded?: boolean;
|
||||
// The list of room members for which to show avatars next to the summary
|
||||
summaryMembers?: RoomMember[];
|
||||
// The text to show as the summary of this event list
|
||||
|
|
|
@ -260,6 +260,7 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
|
|||
});
|
||||
|
||||
// Handle displaying feedback on validity
|
||||
// FIXME: Using an import will result in test failures
|
||||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
||||
let fieldTooltip;
|
||||
if (tooltipContent || this.state.feedback) {
|
||||
|
|
|
@ -24,7 +24,7 @@ import FocusLock from "react-focus-lock";
|
|||
import MemberAvatar from "../avatars/MemberAvatar";
|
||||
import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton";
|
||||
import MessageContextMenu from "../context_menus/MessageContextMenu";
|
||||
import { aboveLeftOf, ContextMenu } from '../../structures/ContextMenu';
|
||||
import { aboveLeftOf } from '../../structures/ContextMenu';
|
||||
import MessageTimestamp from "../messages/MessageTimestamp";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { formatFullDate } from "../../../DateUtils";
|
||||
|
@ -44,31 +44,31 @@ const ZOOM_COEFFICIENT = 0.0025;
|
|||
const ZOOM_DISTANCE = 10;
|
||||
|
||||
interface IProps {
|
||||
src: string, // the source of the image being displayed
|
||||
name?: string, // the main title ('name') for the image
|
||||
link?: string, // the link (if any) applied to the name of the image
|
||||
width?: number, // width of the image src in pixels
|
||||
height?: number, // height of the image src in pixels
|
||||
fileSize?: number, // size of the image src in bytes
|
||||
onFinished(): void, // callback when the lightbox is dismissed
|
||||
src: string; // the source of the image being displayed
|
||||
name?: string; // the main title ('name') for the image
|
||||
link?: string; // the link (if any) applied to the name of the image
|
||||
width?: number; // width of the image src in pixels
|
||||
height?: number; // height of the image src in pixels
|
||||
fileSize?: number; // size of the image src in bytes
|
||||
onFinished(): void; // callback when the lightbox is dismissed
|
||||
|
||||
// the event (if any) that the Image is displaying. Used for event-specific stuff like
|
||||
// redactions, senders, timestamps etc. Other descriptors are taken from the explicit
|
||||
// properties above, which let us use lightboxes to display images which aren't associated
|
||||
// with events.
|
||||
mxEvent: MatrixEvent,
|
||||
permalinkCreator: RoomPermalinkCreator,
|
||||
mxEvent: MatrixEvent;
|
||||
permalinkCreator: RoomPermalinkCreator;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
zoom: number,
|
||||
minZoom: number,
|
||||
maxZoom: number,
|
||||
rotation: number,
|
||||
translationX: number,
|
||||
translationY: number,
|
||||
moving: boolean,
|
||||
contextMenuDisplayed: boolean,
|
||||
zoom: number;
|
||||
minZoom: number;
|
||||
maxZoom: number;
|
||||
rotation: number;
|
||||
translationX: number;
|
||||
translationY: number;
|
||||
moving: boolean;
|
||||
contextMenuDisplayed: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.elements.ImageView")
|
||||
|
@ -122,7 +122,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
const image = this.image.current;
|
||||
const imageWrapper = this.imageWrapper.current;
|
||||
|
||||
const rotation = inputRotation || this.state.rotation;
|
||||
const rotation = inputRotation ?? this.state.rotation;
|
||||
|
||||
const imageIsNotFlipped = rotation % 180 === 0;
|
||||
|
||||
|
@ -304,17 +304,13 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
let contextMenu = null;
|
||||
if (this.state.contextMenuDisplayed) {
|
||||
contextMenu = (
|
||||
<ContextMenu
|
||||
<MessageContextMenu
|
||||
{...aboveLeftOf(this.contextMenuButton.current.getBoundingClientRect())}
|
||||
mxEvent={this.props.mxEvent}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
onFinished={this.onCloseContextMenu}
|
||||
>
|
||||
<MessageContextMenu
|
||||
mxEvent={this.props.mxEvent}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
onFinished={this.onCloseContextMenu}
|
||||
onCloseDialog={this.props.onFinished}
|
||||
/>
|
||||
</ContextMenu>
|
||||
onCloseDialog={this.props.onFinished}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -334,7 +334,7 @@ export default class ReplyThread extends React.Component {
|
|||
events,
|
||||
});
|
||||
|
||||
dis.fire(Action.FocusComposer);
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -27,6 +27,7 @@ interface IProps {
|
|||
value: string;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
onChange?(value: string): void;
|
||||
}
|
||||
|
||||
|
@ -68,6 +69,7 @@ export default class RoomAliasField extends React.PureComponent<IProps, IState>
|
|||
onChange={this.onChange}
|
||||
value={this.props.value.substring(1, this.props.value.length - this.props.domain.length - 1)}
|
||||
maxLength={maxlength}
|
||||
disabled={this.props.disabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -17,11 +17,11 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
|
||||
import Dropdown from "../../views/elements/Dropdown";
|
||||
import * as sdk from '../../../index';
|
||||
import PlatformPeg from "../../../PlatformPeg";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Spinner from "./Spinner";
|
||||
|
||||
function languageMatchesSearchQuery(query, language) {
|
||||
if (language.label.toUpperCase().includes(query.toUpperCase())) return true;
|
||||
|
@ -30,14 +30,14 @@ function languageMatchesSearchQuery(query, language) {
|
|||
}
|
||||
|
||||
interface SpellCheckLanguagesDropdownIProps {
|
||||
className: string,
|
||||
value: string,
|
||||
onOptionChange(language: string),
|
||||
className: string;
|
||||
value: string;
|
||||
onOptionChange(language: string);
|
||||
}
|
||||
|
||||
interface SpellCheckLanguagesDropdownIState {
|
||||
searchQuery: string,
|
||||
languages: any,
|
||||
searchQuery: string;
|
||||
languages: any;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.elements.SpellCheckLanguagesDropdown")
|
||||
|
@ -84,7 +84,6 @@ export default class SpellCheckLanguagesDropdown extends React.Component<SpellCh
|
|||
|
||||
render() {
|
||||
if (this.state.languages === null) {
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
import * as sdk from "../../../index";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
|
||||
interface IProps {
|
||||
// Whether or not this toggle is in the 'on' position.
|
||||
|
@ -43,7 +43,6 @@ export default ({ checked, disabled = false, onChange, ...props }: IProps) => {
|
|||
"mx_ToggleSwitch_enabled": !disabled,
|
||||
});
|
||||
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
return (
|
||||
<AccessibleButton {...props}
|
||||
className={classes}
|
||||
|
|
|
@ -16,11 +16,11 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import * as sdk from '../../../index';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Tooltip from './Tooltip';
|
||||
|
||||
interface IProps {
|
||||
helpText: string;
|
||||
helpText: React.ReactNode | string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -49,7 +49,6 @@ export default class TooltipButton extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
render() {
|
||||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
||||
const tip = this.state.hover ? <Tooltip
|
||||
className="mx_TooltipButton_container"
|
||||
tooltipClassName="mx_TooltipButton_helpText"
|
||||
|
|
|
@ -33,7 +33,7 @@ export const EMOJI_HEIGHT = 37;
|
|||
export const EMOJIS_PER_ROW = 8;
|
||||
|
||||
interface IProps {
|
||||
selectedEmojis: Set<string>;
|
||||
selectedEmojis?: Set<string>;
|
||||
showQuickReactions?: boolean;
|
||||
onChoose(unicode: string): boolean;
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
|||
|
||||
interface IProps {
|
||||
categories: ICategory[];
|
||||
onAnchorClick(id: CategoryKey): void
|
||||
onAnchorClick(id: CategoryKey): void;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.emojipicker.Header")
|
||||
|
|
|
@ -22,6 +22,7 @@ import EmojiPicker from "./EmojiPicker";
|
|||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { Action } from '../../../dispatcher/actions';
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
|
@ -93,6 +94,7 @@ class ReactionPicker extends React.Component<IProps, IState> {
|
|||
this.props.mxEvent.getRoomId(),
|
||||
myReactions[reaction],
|
||||
);
|
||||
dis.dispatch({ action: Action.FocusAComposer });
|
||||
// Tell the emoji picker not to bump this in the more frequently used list.
|
||||
return false;
|
||||
} else {
|
||||
|
@ -104,6 +106,7 @@ class ReactionPicker extends React.Component<IProps, IState> {
|
|||
},
|
||||
});
|
||||
dis.dispatch({ action: "message_sent" });
|
||||
dis.dispatch({ action: Action.FocusAComposer });
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,112 +0,0 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
|
||||
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 MFileBody from './MFileBody';
|
||||
|
||||
import { decryptFile } from '../../../utils/DecryptFile';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import InlineSpinner from '../elements/InlineSpinner';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromContent } from "../../../customisations/Media";
|
||||
|
||||
@replaceableComponent("views.messages.MAudioBody")
|
||||
export default class MAudioBody extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
playing: false,
|
||||
decryptedUrl: null,
|
||||
decryptedBlob: null,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
onPlayToggle() {
|
||||
this.setState({
|
||||
playing: !this.state.playing,
|
||||
});
|
||||
}
|
||||
|
||||
_getContentUrl() {
|
||||
const media = mediaFromContent(this.props.mxEvent.getContent());
|
||||
if (media.isEncrypted) {
|
||||
return this.state.decryptedUrl;
|
||||
} else {
|
||||
return media.srcHttp;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
let decryptedBlob;
|
||||
decryptFile(content.file).then(function(blob) {
|
||||
decryptedBlob = blob;
|
||||
return URL.createObjectURL(decryptedBlob);
|
||||
}).then((url) => {
|
||||
this.setState({
|
||||
decryptedUrl: url,
|
||||
decryptedBlob: decryptedBlob,
|
||||
});
|
||||
}, (err) => {
|
||||
console.warn("Unable to decrypt attachment: ", err);
|
||||
this.setState({
|
||||
error: err,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.state.decryptedUrl) {
|
||||
URL.revokeObjectURL(this.state.decryptedUrl);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
|
||||
if (this.state.error !== null) {
|
||||
return (
|
||||
<span className="mx_MAudioBody">
|
||||
<img src={require("../../../../res/img/warning.svg")} width="16" height="16" />
|
||||
{ _t("Error decrypting audio") }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
// Need to decrypt the attachment
|
||||
// The attachment is decrypted in componentDidMount.
|
||||
// For now add an img tag with a 16x16 spinner.
|
||||
// Not sure how tall the audio player is so not sure how tall it should actually be.
|
||||
return (
|
||||
<span className="mx_MAudioBody">
|
||||
<InlineSpinner />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const contentUrl = this._getContentUrl();
|
||||
|
||||
return (
|
||||
<span className="mx_MAudioBody">
|
||||
<audio src={contentUrl} controls />
|
||||
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
110
src/components/views/messages/MAudioBody.tsx
Normal file
110
src/components/views/messages/MAudioBody.tsx
Normal file
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { Playback } from "../../../voice/Playback";
|
||||
import MFileBody from "./MFileBody";
|
||||
import InlineSpinner from '../elements/InlineSpinner';
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { mediaFromContent } from "../../../customisations/Media";
|
||||
import { decryptFile } from "../../../utils/DecryptFile";
|
||||
import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent";
|
||||
import AudioPlayer from "../audio_messages/AudioPlayer";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
error?: Error;
|
||||
playback?: Playback;
|
||||
decryptedBlob?: Blob;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.MAudioBody")
|
||||
export default class MAudioBody extends React.PureComponent<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
let buffer: ArrayBuffer;
|
||||
const content: IMediaEventContent = this.props.mxEvent.getContent();
|
||||
const media = mediaFromContent(content);
|
||||
if (media.isEncrypted) {
|
||||
try {
|
||||
const blob = await decryptFile(content.file);
|
||||
buffer = await blob.arrayBuffer();
|
||||
this.setState({ decryptedBlob: blob });
|
||||
} catch (e) {
|
||||
this.setState({ error: e });
|
||||
console.warn("Unable to decrypt audio message", e);
|
||||
return; // stop processing the audio file
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
buffer = await media.downloadSource().then(r => r.blob()).then(r => r.arrayBuffer());
|
||||
} catch (e) {
|
||||
this.setState({ error: e });
|
||||
console.warn("Unable to download audio message", e);
|
||||
return; // stop processing the audio file
|
||||
}
|
||||
}
|
||||
|
||||
// We should have a buffer to work with now: let's set it up
|
||||
const playback = new Playback(buffer);
|
||||
playback.clockInfo.populatePlaceholdersFrom(this.props.mxEvent);
|
||||
this.setState({ playback });
|
||||
// Note: the RecordingPlayback component will handle preparing the Playback class for us.
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.state.playback?.destroy();
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (this.state.error) {
|
||||
// TODO: @@TR: Verify error state
|
||||
return (
|
||||
<span className="mx_MAudioBody">
|
||||
<img src={require("../../../../res/img/warning.svg")} width="16" height="16" />
|
||||
{ _t("Error processing audio message") }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.state.playback) {
|
||||
// TODO: @@TR: Verify loading/decrypting state
|
||||
return (
|
||||
<span className="mx_MAudioBody">
|
||||
<InlineSpinner />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// At this point we should have a playable state
|
||||
return (
|
||||
<span className="mx_MAudioBody">
|
||||
<AudioPlayer playback={this.state.playback} mediaName={this.props.mxEvent.getContent().body} />
|
||||
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@ limitations under the License.
|
|||
|
||||
import React, { createRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Blurhash } from "react-blurhash";
|
||||
|
||||
import MFileBody from './MFileBody';
|
||||
import Modal from '../../../Modal';
|
||||
|
@ -29,6 +30,7 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
|||
import InlineSpinner from '../elements/InlineSpinner';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromContent } from "../../../customisations/Media";
|
||||
import { BLURHASH_FIELD } from "../../../ContentMessages";
|
||||
|
||||
@replaceableComponent("views.messages.MImageBody")
|
||||
export default class MImageBody extends React.Component {
|
||||
|
@ -333,7 +335,8 @@ export default class MImageBody extends React.Component {
|
|||
infoWidth = content.info.w;
|
||||
infoHeight = content.info.h;
|
||||
} else {
|
||||
// Whilst the image loads, display nothing.
|
||||
// Whilst the image loads, display nothing. We also don't display a blurhash image
|
||||
// because we don't really know what size of image we'll end up with.
|
||||
//
|
||||
// Once loaded, use the loaded image dimensions stored in `loadedImageDimensions`.
|
||||
//
|
||||
|
@ -368,12 +371,8 @@ export default class MImageBody extends React.Component {
|
|||
let placeholder = null;
|
||||
let gifLabel = null;
|
||||
|
||||
// e2e image hasn't been decrypted yet
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
placeholder = <InlineSpinner w={32} h={32} />;
|
||||
} else if (!this.state.imgLoaded) {
|
||||
// Deliberately, getSpinner is left unimplemented here, MStickerBody overides
|
||||
placeholder = this.getPlaceholder();
|
||||
if (!this.state.imgLoaded) {
|
||||
placeholder = this.getPlaceholder(maxWidth, maxHeight);
|
||||
}
|
||||
|
||||
let showPlaceholder = Boolean(placeholder);
|
||||
|
@ -395,7 +394,7 @@ export default class MImageBody extends React.Component {
|
|||
|
||||
if (!this.state.showImage) {
|
||||
img = <HiddenImagePlaceholder style={{ maxWidth: maxWidth + "px" }} />;
|
||||
showPlaceholder = false; // because we're hiding the image, so don't show the sticker icon.
|
||||
showPlaceholder = false; // because we're hiding the image, so don't show the placeholder.
|
||||
}
|
||||
|
||||
if (this._isGif() && !SettingsStore.getValue("autoplayGifsAndVideos") && !this.state.hover) {
|
||||
|
@ -411,9 +410,7 @@ export default class MImageBody extends React.Component {
|
|||
// Constrain width here so that spinner appears central to the loaded thumbnail
|
||||
maxWidth: infoWidth + "px",
|
||||
}}>
|
||||
<div className="mx_MImageBody_thumbnail_spinner">
|
||||
{ placeholder }
|
||||
</div>
|
||||
{ placeholder }
|
||||
</div>
|
||||
}
|
||||
|
||||
|
@ -437,9 +434,12 @@ export default class MImageBody extends React.Component {
|
|||
}
|
||||
|
||||
// Overidden by MStickerBody
|
||||
getPlaceholder() {
|
||||
// MImageBody doesn't show a placeholder whilst the image loads, (but it could do)
|
||||
return null;
|
||||
getPlaceholder(width, height) {
|
||||
const blurhash = this.props.mxEvent.getContent().info[BLURHASH_FIELD];
|
||||
if (blurhash) return <Blurhash hash={blurhash} width={width} height={height} />;
|
||||
return <div className="mx_MImageBody_thumbnail_spinner">
|
||||
<InlineSpinner w={32} h={32} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
// Overidden by MStickerBody
|
||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { getNameForEventRoom, userLabelForEventRoom }
|
||||
from '../../../utils/KeyVerificationStateObserver';
|
||||
|
@ -26,9 +25,10 @@ import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
|||
import { Action } from "../../../dispatcher/actions";
|
||||
import EventTileBubble from "./EventTileBubble";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent
|
||||
mxEvent: MatrixEvent;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.MKeyVerificationRequest")
|
||||
|
@ -115,8 +115,6 @@ export default class MKeyVerificationRequest extends React.Component<IProps> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
||||
const { mxEvent } = this.props;
|
||||
const request = mxEvent.verificationRequest;
|
||||
|
||||
|
@ -154,7 +152,7 @@ export default class MKeyVerificationRequest extends React.Component<IProps> {
|
|||
<AccessibleButton kind="danger" onClick={this.onRejectClicked}>
|
||||
{_t("Decline")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton onClick={this.onAcceptClicked}>
|
||||
<AccessibleButton kind="primary" onClick={this.onAcceptClicked}>
|
||||
{_t("Accept")}
|
||||
</AccessibleButton>
|
||||
</div>);
|
||||
|
|
|
@ -18,6 +18,7 @@ import React from 'react';
|
|||
import MImageBody from './MImageBody';
|
||||
import * as sdk from '../../../index';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { BLURHASH_FIELD } from "../../../ContentMessages";
|
||||
|
||||
@replaceableComponent("views.messages.MStickerBody")
|
||||
export default class MStickerBody extends MImageBody {
|
||||
|
@ -41,9 +42,9 @@ export default class MStickerBody extends MImageBody {
|
|||
|
||||
// Placeholder to show in place of the sticker image if
|
||||
// img onLoad hasn't fired yet.
|
||||
getPlaceholder() {
|
||||
const TintableSVG = sdk.getComponent('elements.TintableSvg');
|
||||
return <TintableSVG src={require("../../../../res/img/icons-show-stickers.svg")} width="75" height="75" />;
|
||||
getPlaceholder(width, height) {
|
||||
if (this.props.mxEvent.getContent().info[BLURHASH_FIELD]) return super.getPlaceholder(width, height);
|
||||
return <img src={require("../../../../res/img/icons-show-stickers.svg")} width="75" height="75" />;
|
||||
}
|
||||
|
||||
// Tooltip to show on mouse over
|
||||
|
|
|
@ -16,6 +16,8 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { decode } from "blurhash";
|
||||
|
||||
import MFileBody from './MFileBody';
|
||||
import { decryptFile } from '../../../utils/DecryptFile';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -23,6 +25,7 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
import InlineSpinner from '../elements/InlineSpinner';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromContent } from "../../../customisations/Media";
|
||||
import { BLURHASH_FIELD } from "../../../ContentMessages";
|
||||
|
||||
interface IProps {
|
||||
/* the MatrixEvent to show */
|
||||
|
@ -32,11 +35,13 @@ interface IProps {
|
|||
}
|
||||
|
||||
interface IState {
|
||||
decryptedUrl: string|null,
|
||||
decryptedThumbnailUrl: string|null,
|
||||
decryptedBlob: Blob|null,
|
||||
error: any|null,
|
||||
fetchingData: boolean,
|
||||
decryptedUrl?: string;
|
||||
decryptedThumbnailUrl?: string;
|
||||
decryptedBlob?: Blob;
|
||||
error?: any;
|
||||
fetchingData: boolean;
|
||||
posterLoading: boolean;
|
||||
blurhashUrl: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.MVideoBody")
|
||||
|
@ -51,10 +56,12 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
|||
decryptedThumbnailUrl: null,
|
||||
decryptedBlob: null,
|
||||
error: null,
|
||||
posterLoading: false,
|
||||
blurhashUrl: null,
|
||||
};
|
||||
}
|
||||
|
||||
thumbScale(fullWidth: number, fullHeight: number, thumbWidth: number, thumbHeight: number) {
|
||||
thumbScale(fullWidth: number, fullHeight: number, thumbWidth = 480, thumbHeight = 360) {
|
||||
if (!fullWidth || !fullHeight) {
|
||||
// Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even
|
||||
// log this because it's spammy
|
||||
|
@ -92,8 +99,11 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
|||
private getThumbUrl(): string|null {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
const media = mediaFromContent(content);
|
||||
if (media.isEncrypted) {
|
||||
|
||||
if (media.isEncrypted && this.state.decryptedThumbnailUrl) {
|
||||
return this.state.decryptedThumbnailUrl;
|
||||
} else if (this.state.posterLoading) {
|
||||
return this.state.blurhashUrl;
|
||||
} else if (media.hasThumbnail) {
|
||||
return media.thumbnailHttp;
|
||||
} else {
|
||||
|
@ -101,18 +111,57 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
private loadBlurhash() {
|
||||
const info = this.props.mxEvent.getContent()?.info;
|
||||
if (!info[BLURHASH_FIELD]) return;
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
|
||||
let width = info.w;
|
||||
let height = info.h;
|
||||
const scale = this.thumbScale(info.w, info.h);
|
||||
if (scale) {
|
||||
width = Math.floor(info.w * scale);
|
||||
height = Math.floor(info.h * scale);
|
||||
}
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
const pixels = decode(info[BLURHASH_FIELD], width, height);
|
||||
const ctx = canvas.getContext("2d");
|
||||
const imgData = ctx.createImageData(width, height);
|
||||
imgData.data.set(pixels);
|
||||
ctx.putImageData(imgData, 0, 0);
|
||||
|
||||
this.setState({
|
||||
blurhashUrl: canvas.toDataURL(),
|
||||
posterLoading: true,
|
||||
});
|
||||
|
||||
const content = this.props.mxEvent.getContent();
|
||||
const media = mediaFromContent(content);
|
||||
if (media.hasThumbnail) {
|
||||
const image = new Image();
|
||||
image.onload = () => {
|
||||
this.setState({ posterLoading: false });
|
||||
};
|
||||
image.src = media.thumbnailHttp;
|
||||
}
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const autoplay = SettingsStore.getValue("autoplayGifsAndVideos") as boolean;
|
||||
const content = this.props.mxEvent.getContent();
|
||||
this.loadBlurhash();
|
||||
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
let thumbnailPromise = Promise.resolve(null);
|
||||
if (content.info && content.info.thumbnail_file) {
|
||||
thumbnailPromise = decryptFile(
|
||||
content.info.thumbnail_file,
|
||||
).then(function(blob) {
|
||||
return URL.createObjectURL(blob);
|
||||
});
|
||||
if (content?.info?.thumbnail_file) {
|
||||
thumbnailPromise = decryptFile(content.info.thumbnail_file)
|
||||
.then(blob => URL.createObjectURL(blob));
|
||||
}
|
||||
|
||||
try {
|
||||
const thumbnailUrl = await thumbnailPromise;
|
||||
if (autoplay) {
|
||||
|
@ -218,7 +267,7 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
|||
let poster = null;
|
||||
let preload = "metadata";
|
||||
if (content.info) {
|
||||
const scale = this.thumbScale(content.info.w, content.info.h, 480, 360);
|
||||
const scale = this.thumbScale(content.info.w, content.info.h);
|
||||
if (scale) {
|
||||
width = Math.floor(content.info.w * scale);
|
||||
height = Math.floor(content.info.h * scale);
|
||||
|
|
|
@ -23,7 +23,7 @@ import InlineSpinner from '../elements/InlineSpinner';
|
|||
import { _t } from "../../../languageHandler";
|
||||
import { mediaFromContent } from "../../../customisations/Media";
|
||||
import { decryptFile } from "../../../utils/DecryptFile";
|
||||
import RecordingPlayback from "../voice_messages/RecordingPlayback";
|
||||
import RecordingPlayback from "../audio_messages/RecordingPlayback";
|
||||
import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent";
|
||||
|
||||
interface IProps {
|
||||
|
|
|
@ -24,7 +24,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
onClick(): void;
|
||||
onClick?(): void;
|
||||
enableFlair: boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -16,134 +14,151 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef } from 'react';
|
||||
import React, { createRef, SyntheticEvent } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import highlight from 'highlight.js';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { MsgType } from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
import * as HtmlUtils from '../../../HtmlUtils';
|
||||
import { formatDate } from '../../../DateUtils';
|
||||
import * as sdk from '../../../index';
|
||||
import Modal from '../../../Modal';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as ContextMenu from '../../structures/ContextMenu';
|
||||
import { toRightOf } from '../../structures/ContextMenu';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import ReplyThread from "../elements/ReplyThread";
|
||||
import { pillifyLinks, unmountPills } from '../../../utils/pillify';
|
||||
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
|
||||
import { isPermalinkHost } from "../../../utils/permalinks/Permalinks";
|
||||
import { toRightOf } from "../../structures/ContextMenu";
|
||||
import { copyPlaintext } from "../../../utils/strings";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { TileShape } from '../rooms/EventTile';
|
||||
import EditorStateTransfer from "../../../utils/EditorStateTransfer";
|
||||
import GenericTextContextMenu from "../context_menus/GenericTextContextMenu";
|
||||
import Spoiler from "../elements/Spoiler";
|
||||
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||
import MessageEditHistoryDialog from "../dialogs/MessageEditHistoryDialog";
|
||||
import EditMessageComposer from '../rooms/EditMessageComposer';
|
||||
import LinkPreviewGroup from '../rooms/LinkPreviewGroup';
|
||||
|
||||
interface IProps {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: MatrixEvent;
|
||||
|
||||
/* a list of words to highlight */
|
||||
highlights?: string[];
|
||||
|
||||
/* link URL for the highlights */
|
||||
highlightLink?: string;
|
||||
|
||||
/* should show URL previews for this event */
|
||||
showUrlPreview?: boolean;
|
||||
|
||||
/* the shape of the tile, used */
|
||||
tileShape?: TileShape;
|
||||
|
||||
editState?: EditorStateTransfer;
|
||||
replacingEventId?: string;
|
||||
|
||||
/* callback for when our widget has loaded */
|
||||
onHeightChanged(): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
// the URLs (if any) to be previewed with a LinkPreviewWidget inside this TextualBody.
|
||||
links: string[];
|
||||
|
||||
// track whether the preview widget is hidden
|
||||
widgetHidden: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.TextualBody")
|
||||
export default class TextualBody extends React.Component {
|
||||
static propTypes = {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
export default class TextualBody extends React.Component<IProps, IState> {
|
||||
private readonly contentRef = createRef<HTMLSpanElement>();
|
||||
|
||||
/* a list of words to highlight */
|
||||
highlights: PropTypes.array,
|
||||
|
||||
/* link URL for the highlights */
|
||||
highlightLink: PropTypes.string,
|
||||
|
||||
/* should show URL previews for this event */
|
||||
showUrlPreview: PropTypes.bool,
|
||||
|
||||
/* callback for when our widget has loaded */
|
||||
onHeightChanged: PropTypes.func,
|
||||
|
||||
/* the shape of the tile, used */
|
||||
tileShape: PropTypes.string,
|
||||
};
|
||||
private unmounted = false;
|
||||
private pills: Element[] = [];
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._content = createRef();
|
||||
|
||||
this.state = {
|
||||
// the URLs (if any) to be previewed with a LinkPreviewWidget
|
||||
// inside this TextualBody.
|
||||
links: [],
|
||||
|
||||
// track whether the preview widget is hidden
|
||||
widgetHidden: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._unmounted = false;
|
||||
this._pills = [];
|
||||
if (!this.props.editState) {
|
||||
this._applyFormatting();
|
||||
this.applyFormatting();
|
||||
}
|
||||
}
|
||||
|
||||
_applyFormatting() {
|
||||
private applyFormatting(): void {
|
||||
const showLineNumbers = SettingsStore.getValue("showCodeLineNumbers");
|
||||
this.activateSpoilers([this._content.current]);
|
||||
this.activateSpoilers([this.contentRef.current]);
|
||||
|
||||
// pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer
|
||||
// are still sent as plaintext URLs. If these are ever pillified in the composer,
|
||||
// we should be pillify them here by doing the linkifying BEFORE the pillifying.
|
||||
pillifyLinks([this._content.current], this.props.mxEvent, this._pills);
|
||||
HtmlUtils.linkifyElement(this._content.current);
|
||||
pillifyLinks([this.contentRef.current], this.props.mxEvent, this.pills);
|
||||
HtmlUtils.linkifyElement(this.contentRef.current);
|
||||
this.calculateUrlPreview();
|
||||
|
||||
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") {
|
||||
// Handle expansion and add buttons
|
||||
const pres = ReactDOM.findDOMNode(this).getElementsByTagName("pre");
|
||||
const pres = (ReactDOM.findDOMNode(this) as Element).getElementsByTagName("pre");
|
||||
if (pres.length > 0) {
|
||||
for (let i = 0; i < pres.length; i++) {
|
||||
// If there already is a div wrapping the codeblock we want to skip this.
|
||||
// This happens after the codeblock was edited.
|
||||
if (pres[i].parentNode.className == "mx_EventTile_pre_container") continue;
|
||||
if (pres[i].parentElement.className == "mx_EventTile_pre_container") continue;
|
||||
// Add code element if it's missing since we depend on it
|
||||
if (pres[i].getElementsByTagName("code").length == 0) {
|
||||
this._addCodeElement(pres[i]);
|
||||
this.addCodeElement(pres[i]);
|
||||
}
|
||||
// Wrap a div around <pre> so that the copy button can be correctly positioned
|
||||
// when the <pre> overflows and is scrolled horizontally.
|
||||
const div = this._wrapInDiv(pres[i]);
|
||||
this._handleCodeBlockExpansion(pres[i]);
|
||||
this._addCodeExpansionButton(div, pres[i]);
|
||||
this._addCodeCopyButton(div);
|
||||
const div = this.wrapInDiv(pres[i]);
|
||||
this.handleCodeBlockExpansion(pres[i]);
|
||||
this.addCodeExpansionButton(div, pres[i]);
|
||||
this.addCodeCopyButton(div);
|
||||
if (showLineNumbers) {
|
||||
this._addLineNumbers(pres[i]);
|
||||
this.addLineNumbers(pres[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Highlight code
|
||||
const codes = ReactDOM.findDOMNode(this).getElementsByTagName("code");
|
||||
const codes = (ReactDOM.findDOMNode(this) as Element).getElementsByTagName("code");
|
||||
if (codes.length > 0) {
|
||||
// Do this asynchronously: parsing code takes time and we don't
|
||||
// need to block the DOM update on it.
|
||||
setTimeout(() => {
|
||||
if (this._unmounted) return;
|
||||
if (this.unmounted) return;
|
||||
for (let i = 0; i < codes.length; i++) {
|
||||
// If the code already has the hljs class we want to skip this.
|
||||
// This happens after the codeblock was edited.
|
||||
if (codes[i].className.includes("hljs")) continue;
|
||||
this._highlightCode(codes[i]);
|
||||
this.highlightCode(codes[i]);
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_addCodeElement(pre) {
|
||||
private addCodeElement(pre: HTMLPreElement): void {
|
||||
const code = document.createElement("code");
|
||||
code.append(...pre.childNodes);
|
||||
pre.appendChild(code);
|
||||
}
|
||||
|
||||
_addCodeExpansionButton(div, pre) {
|
||||
private addCodeExpansionButton(div: HTMLDivElement, pre: HTMLPreElement): void {
|
||||
// Calculate how many percent does the pre element take up.
|
||||
// If it's less than 30% we don't add the expansion button.
|
||||
const percentageOfViewport = pre.offsetHeight / UIStore.instance.windowHeight * 100;
|
||||
|
@ -175,7 +190,7 @@ export default class TextualBody extends React.Component {
|
|||
div.appendChild(button);
|
||||
}
|
||||
|
||||
_addCodeCopyButton(div) {
|
||||
private addCodeCopyButton(div: HTMLDivElement): void {
|
||||
const button = document.createElement("span");
|
||||
button.className = "mx_EventTile_button mx_EventTile_copyButton ";
|
||||
|
||||
|
@ -185,11 +200,10 @@ export default class TextualBody extends React.Component {
|
|||
if (expansionButtonExists.length > 0) button.className += "mx_EventTile_buttonBottom";
|
||||
|
||||
button.onclick = async () => {
|
||||
const copyCode = button.parentNode.getElementsByTagName("code")[0];
|
||||
const copyCode = button.parentElement.getElementsByTagName("code")[0];
|
||||
const successful = await copyPlaintext(copyCode.textContent);
|
||||
|
||||
const buttonRect = button.getBoundingClientRect();
|
||||
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
|
||||
const { close } = ContextMenu.createMenu(GenericTextContextMenu, {
|
||||
...toRightOf(buttonRect, 2),
|
||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||
|
@ -200,7 +214,7 @@ export default class TextualBody extends React.Component {
|
|||
div.appendChild(button);
|
||||
}
|
||||
|
||||
_wrapInDiv(pre) {
|
||||
private wrapInDiv(pre: HTMLPreElement): HTMLDivElement {
|
||||
const div = document.createElement("div");
|
||||
div.className = "mx_EventTile_pre_container";
|
||||
|
||||
|
@ -212,13 +226,13 @@ export default class TextualBody extends React.Component {
|
|||
return div;
|
||||
}
|
||||
|
||||
_handleCodeBlockExpansion(pre) {
|
||||
private handleCodeBlockExpansion(pre: HTMLPreElement): void {
|
||||
if (!SettingsStore.getValue("expandCodeByDefault")) {
|
||||
pre.className = "mx_EventTile_collapsedCodeBlock";
|
||||
}
|
||||
}
|
||||
|
||||
_addLineNumbers(pre) {
|
||||
private addLineNumbers(pre: HTMLPreElement): void {
|
||||
// Calculate number of lines in pre
|
||||
const number = pre.innerHTML.replace(/\n(<\/code>)?$/, "").split(/\n/).length;
|
||||
pre.innerHTML = '<span class="mx_EventTile_lineNumbers"></span>' + pre.innerHTML + '<span></span>';
|
||||
|
@ -229,7 +243,7 @@ export default class TextualBody extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_highlightCode(code) {
|
||||
private highlightCode(code: HTMLElement): void {
|
||||
if (SettingsStore.getValue("enableSyntaxHighlightLanguageDetection")) {
|
||||
highlight.highlightBlock(code);
|
||||
} else {
|
||||
|
@ -249,14 +263,14 @@ export default class TextualBody extends React.Component {
|
|||
const stoppedEditing = prevProps.editState && !this.props.editState;
|
||||
const messageWasEdited = prevProps.replacingEventId !== this.props.replacingEventId;
|
||||
if (messageWasEdited || stoppedEditing) {
|
||||
this._applyFormatting();
|
||||
this.applyFormatting();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
unmountPills(this._pills);
|
||||
this.unmounted = true;
|
||||
unmountPills(this.pills);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
|
@ -273,26 +287,20 @@ export default class TextualBody extends React.Component {
|
|||
nextState.widgetHidden !== this.state.widgetHidden);
|
||||
}
|
||||
|
||||
calculateUrlPreview() {
|
||||
private calculateUrlPreview(): void {
|
||||
//console.info("calculateUrlPreview: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
|
||||
|
||||
if (this.props.showUrlPreview) {
|
||||
// pass only the first child which is the event tile otherwise this recurses on edited events
|
||||
let links = this.findLinks([this._content.current]);
|
||||
let links = this.findLinks([this.contentRef.current]);
|
||||
if (links.length) {
|
||||
// de-duplicate the links after stripping hashes as they don't affect the preview
|
||||
// using a set here maintains the order
|
||||
links = Array.from(new Set(links.map(link => {
|
||||
const url = new URL(link);
|
||||
url.hash = "";
|
||||
return url.toString();
|
||||
})));
|
||||
|
||||
// de-duplicate the links using a set here maintains the order
|
||||
links = Array.from(new Set(links));
|
||||
this.setState({ links });
|
||||
|
||||
// lazy-load the hidden state of the preview widget from localstorage
|
||||
if (global.localStorage) {
|
||||
const hidden = global.localStorage.getItem("hide_preview_" + this.props.mxEvent.getId());
|
||||
if (window.localStorage) {
|
||||
const hidden = !!window.localStorage.getItem("hide_preview_" + this.props.mxEvent.getId());
|
||||
this.setState({ widgetHidden: hidden });
|
||||
}
|
||||
} else if (this.state.links.length) {
|
||||
|
@ -301,19 +309,15 @@ export default class TextualBody extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
activateSpoilers(nodes) {
|
||||
private activateSpoilers(nodes: ArrayLike<Element>): void {
|
||||
let node = nodes[0];
|
||||
while (node) {
|
||||
if (node.tagName === "SPAN" && typeof node.getAttribute("data-mx-spoiler") === "string") {
|
||||
const spoilerContainer = document.createElement('span');
|
||||
|
||||
const reason = node.getAttribute("data-mx-spoiler");
|
||||
const Spoiler = sdk.getComponent('elements.Spoiler');
|
||||
node.removeAttribute("data-mx-spoiler"); // we don't want to recurse
|
||||
const spoiler = <Spoiler
|
||||
reason={reason}
|
||||
contentHtml={node.outerHTML}
|
||||
/>;
|
||||
const spoiler = <Spoiler reason={reason} contentHtml={node.outerHTML} />;
|
||||
|
||||
ReactDOM.render(spoiler, spoilerContainer);
|
||||
node.parentNode.replaceChild(spoilerContainer, node);
|
||||
|
@ -322,15 +326,15 @@ export default class TextualBody extends React.Component {
|
|||
}
|
||||
|
||||
if (node.childNodes && node.childNodes.length) {
|
||||
this.activateSpoilers(node.childNodes);
|
||||
this.activateSpoilers(node.childNodes as NodeListOf<Element>);
|
||||
}
|
||||
|
||||
node = node.nextSibling;
|
||||
node = node.nextSibling as Element;
|
||||
}
|
||||
}
|
||||
|
||||
findLinks(nodes) {
|
||||
let links = [];
|
||||
private findLinks(nodes: ArrayLike<Element>): string[] {
|
||||
let links: string[] = [];
|
||||
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i];
|
||||
|
@ -348,7 +352,7 @@ export default class TextualBody extends React.Component {
|
|||
return links;
|
||||
}
|
||||
|
||||
isLinkPreviewable(node) {
|
||||
private isLinkPreviewable(node: Element): boolean {
|
||||
// don't try to preview relative links
|
||||
if (!node.getAttribute("href").startsWith("http://") &&
|
||||
!node.getAttribute("href").startsWith("https://")) {
|
||||
|
@ -381,7 +385,7 @@ export default class TextualBody extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
onCancelClick = event => {
|
||||
private onCancelClick = (): void => {
|
||||
this.setState({ widgetHidden: true });
|
||||
// FIXME: persist this somewhere smarter than local storage
|
||||
if (global.localStorage) {
|
||||
|
@ -390,7 +394,7 @@ export default class TextualBody extends React.Component {
|
|||
this.forceUpdate();
|
||||
};
|
||||
|
||||
onEmoteSenderClick = event => {
|
||||
private onEmoteSenderClick = (): void => {
|
||||
const mxEvent = this.props.mxEvent;
|
||||
dis.dispatch<ComposerInsertPayload>({
|
||||
action: Action.ComposerInsert,
|
||||
|
@ -398,7 +402,7 @@ export default class TextualBody extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
getEventTileOps = () => ({
|
||||
public getEventTileOps = () => ({
|
||||
isWidgetHidden: () => {
|
||||
return this.state.widgetHidden;
|
||||
},
|
||||
|
@ -411,7 +415,7 @@ export default class TextualBody extends React.Component {
|
|||
},
|
||||
});
|
||||
|
||||
onStarterLinkClick = (starterLink, ev) => {
|
||||
private onStarterLinkClick = (starterLink: string, ev: SyntheticEvent): void => {
|
||||
ev.preventDefault();
|
||||
// We need to add on our scalar token to the starter link, but we may not have one!
|
||||
// In addition, we can't fetch one on click and then go to it immediately as that
|
||||
|
@ -431,7 +435,6 @@ export default class TextualBody extends React.Component {
|
|||
const scalarClient = integrationManager.getScalarClient();
|
||||
scalarClient.connect().then(() => {
|
||||
const completeUrl = scalarClient.getStarterLink(starterLink);
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const integrationsUrl = integrationManager.uiUrl;
|
||||
Modal.createTrackedDialog('Add an integration', '', QuestionDialog, {
|
||||
title: _t("Add an Integration"),
|
||||
|
@ -458,12 +461,11 @@ export default class TextualBody extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
_openHistoryDialog = async () => {
|
||||
const MessageEditHistoryDialog = sdk.getComponent("views.dialogs.MessageEditHistoryDialog");
|
||||
private openHistoryDialog = async (): Promise<void> => {
|
||||
Modal.createDialog(MessageEditHistoryDialog, { mxEvent: this.props.mxEvent });
|
||||
};
|
||||
|
||||
_renderEditedMarker() {
|
||||
private renderEditedMarker() {
|
||||
const date = this.props.mxEvent.replacingEventDate();
|
||||
const dateString = date && formatDate(date);
|
||||
|
||||
|
@ -479,7 +481,7 @@ export default class TextualBody extends React.Component {
|
|||
return (
|
||||
<AccessibleTooltipButton
|
||||
className="mx_EventTile_edited"
|
||||
onClick={this._openHistoryDialog}
|
||||
onClick={this.openHistoryDialog}
|
||||
title={_t("Edited at %(date)s. Click to view edits.", { date: dateString })}
|
||||
tooltip={tooltip}
|
||||
>
|
||||
|
@ -490,24 +492,25 @@ export default class TextualBody extends React.Component {
|
|||
|
||||
render() {
|
||||
if (this.props.editState) {
|
||||
const EditMessageComposer = sdk.getComponent('rooms.EditMessageComposer');
|
||||
return <EditMessageComposer editState={this.props.editState} className="mx_EventTile_content" />;
|
||||
}
|
||||
const mxEvent = this.props.mxEvent;
|
||||
const content = mxEvent.getContent();
|
||||
|
||||
// only strip reply if this is the original replying event, edits thereafter do not have the fallback
|
||||
const stripReply = !mxEvent.replacingEvent() && ReplyThread.getParentEventId(mxEvent);
|
||||
const stripReply = !mxEvent.replacingEvent() && !!ReplyThread.getParentEventId(mxEvent);
|
||||
let body = HtmlUtils.bodyToHtml(content, this.props.highlights, {
|
||||
disableBigEmoji: content.msgtype === "m.emote" || !SettingsStore.getValue('TextualBody.enableBigEmoji'),
|
||||
disableBigEmoji: content.msgtype === MsgType.Emote
|
||||
|| !SettingsStore.getValue<boolean>('TextualBody.enableBigEmoji'),
|
||||
// Part of Replies fallback support
|
||||
stripReplyFallback: stripReply,
|
||||
ref: this._content,
|
||||
ref: this.contentRef,
|
||||
returnString: false,
|
||||
});
|
||||
if (this.props.replacingEventId) {
|
||||
body = <>
|
||||
{body}
|
||||
{this._renderEditedMarker()}
|
||||
{this.renderEditedMarker()}
|
||||
</>;
|
||||
}
|
||||
|
||||
|
@ -521,20 +524,16 @@ export default class TextualBody extends React.Component {
|
|||
|
||||
let widgets;
|
||||
if (this.state.links.length && !this.state.widgetHidden && this.props.showUrlPreview) {
|
||||
const LinkPreviewWidget = sdk.getComponent('rooms.LinkPreviewWidget');
|
||||
widgets = this.state.links.map((link)=>{
|
||||
return <LinkPreviewWidget
|
||||
key={link}
|
||||
link={link}
|
||||
mxEvent={this.props.mxEvent}
|
||||
onCancelClick={this.onCancelClick}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
/>;
|
||||
});
|
||||
widgets = <LinkPreviewGroup
|
||||
links={this.state.links}
|
||||
mxEvent={this.props.mxEvent}
|
||||
onCancelClick={this.onCancelClick}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
/>;
|
||||
}
|
||||
|
||||
switch (content.msgtype) {
|
||||
case "m.emote":
|
||||
case MsgType.Emote:
|
||||
return (
|
||||
<span className="mx_MEmoteBody mx_EventTile_content">
|
||||
*
|
||||
|
@ -549,7 +548,7 @@ export default class TextualBody extends React.Component {
|
|||
{ widgets }
|
||||
</span>
|
||||
);
|
||||
case "m.notice":
|
||||
case MsgType.Notice:
|
||||
return (
|
||||
<span className="mx_MNoticeBody mx_EventTile_content">
|
||||
{ body }
|
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -17,12 +16,12 @@ limitations under the License.
|
|||
|
||||
import React from "react";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import * as TextForEvent from "../../../TextForEvent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
// The event to show
|
||||
mxEvent: MatrixEvent;
|
||||
}
|
||||
|
||||
|
|
|
@ -16,13 +16,13 @@ limitations under the License.
|
|||
|
||||
import React from "react";
|
||||
|
||||
import * as sdk from "../../../index";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { User } from "matrix-js-sdk/src/models/user";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import Spinner from "../elements/Spinner";
|
||||
|
||||
export const PendingActionSpinner = ({ text }) => {
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
return <div className="mx_EncryptionInfo_spinner">
|
||||
<Spinner />
|
||||
{ text }
|
||||
|
@ -64,7 +64,6 @@ const EncryptionInfo: React.FC<IProps> = ({
|
|||
}
|
||||
content = <PendingActionSpinner text={text} />;
|
||||
} else {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
content = (
|
||||
<AccessibleButton kind="primary" className="mx_UserInfo_wideButton" onClick={onStartVerification}>
|
||||
{_t("Start Verification")}
|
||||
|
|
|
@ -39,9 +39,8 @@ interface IProps {
|
|||
member: RoomMember | User;
|
||||
onClose: () => void;
|
||||
verificationRequest: VerificationRequest;
|
||||
verificationRequestPromise: Promise<VerificationRequest>;
|
||||
verificationRequestPromise?: Promise<VerificationRequest>;
|
||||
layout: string;
|
||||
inDialog: boolean;
|
||||
isRoomEncrypted: boolean;
|
||||
}
|
||||
|
||||
|
@ -82,6 +81,7 @@ const EncryptionPanel: React.FC<IProps> = (props: IProps) => {
|
|||
const changeHandler = useCallback(() => {
|
||||
// handle transitions -> cancelled for mismatches which fire a modal instead of showing a card
|
||||
if (request && request.cancelled && MISMATCHES.includes(request.cancellationCode)) {
|
||||
// FIXME: Using an import will result in test failures
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog("Verification failed", "insecure", ErrorDialog, {
|
||||
headerImage: require("../../../../res/img/e2e/warning.svg"),
|
||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
import React from "react";
|
||||
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import * as sdk from '../../../index';
|
||||
import { verificationMethods } from 'matrix-js-sdk/src/crypto';
|
||||
import { SCAN_QR_CODE_METHOD } from "matrix-js-sdk/src/crypto/verification/QRCode";
|
||||
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
|
@ -38,6 +37,8 @@ import {
|
|||
} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import VerificationShowSas from "../verification/VerificationShowSas";
|
||||
|
||||
// XXX: Should be defined in matrix-js-sdk
|
||||
enum VerificationPhase {
|
||||
|
@ -81,7 +82,6 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
|||
const { member, request } = this.props;
|
||||
const showSAS: boolean = request.otherPartySupportsMethod(verificationMethods.SAS);
|
||||
const showQR: boolean = request.otherPartySupportsMethod(SCAN_QR_CODE_METHOD);
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const brand = SdkConfig.get().brand;
|
||||
|
||||
const noCommonMethodError: JSX.Element = !showSAS && !showQR ?
|
||||
|
@ -195,7 +195,6 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
|||
|
||||
private renderQRReciprocatePhase() {
|
||||
const { member, request } = this.props;
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
const description = request.isSelfVerification ?
|
||||
_t("Almost there! Is your other session showing the same shield?") :
|
||||
_t("Almost there! Is %(displayName)s showing the same shield?", {
|
||||
|
@ -265,7 +264,6 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
|||
});
|
||||
}
|
||||
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return (
|
||||
<div className="mx_UserInfo_container mx_VerificationPanel_verified_section">
|
||||
<h3>{_t("Verified")}</h3>
|
||||
|
@ -282,8 +280,6 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
|||
private renderCancelledPhase() {
|
||||
const { member, request } = this.props;
|
||||
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
let startAgainInstruction: string;
|
||||
if (request.isSelfVerification) {
|
||||
startAgainInstruction = _t("Start verification again from the notification.");
|
||||
|
@ -332,7 +328,6 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
|||
case verificationMethods.RECIPROCATE_QR_CODE:
|
||||
return this.renderQRReciprocatePhase();
|
||||
case verificationMethods.SAS: {
|
||||
const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas');
|
||||
const emojis = this.state.sasEvent ?
|
||||
<VerificationShowSas
|
||||
displayName={displayName}
|
||||
|
|
|
@ -21,7 +21,6 @@ import { Room } from 'matrix-js-sdk/src/models/room';
|
|||
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import AppsDrawer from './AppsDrawer';
|
||||
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
|
@ -29,35 +28,36 @@ import ResizeNotifier from "../../../utils/ResizeNotifier";
|
|||
import CallViewForRoom from '../voip/CallViewForRoom';
|
||||
import { objectHasDiff } from "../../../utils/objects";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { throttle } from 'lodash';
|
||||
|
||||
interface IProps {
|
||||
// js-sdk room object
|
||||
room: Room,
|
||||
userId: string,
|
||||
showApps: boolean, // Render apps
|
||||
room: Room;
|
||||
userId: string;
|
||||
showApps: boolean; // Render apps
|
||||
|
||||
// maxHeight attribute for the aux panel and the video
|
||||
// therein
|
||||
maxHeight: number,
|
||||
maxHeight: number;
|
||||
|
||||
// a callback which is called when the content of the aux panel changes
|
||||
// content in a way that is likely to make it change size.
|
||||
onResize: () => void,
|
||||
fullHeight: boolean,
|
||||
onResize: () => void;
|
||||
fullHeight: boolean;
|
||||
|
||||
resizeNotifier: ResizeNotifier,
|
||||
resizeNotifier: ResizeNotifier;
|
||||
}
|
||||
|
||||
interface Counter {
|
||||
title: string,
|
||||
value: number,
|
||||
link: string,
|
||||
severity: string,
|
||||
stateKey: string,
|
||||
title: string;
|
||||
value: number;
|
||||
link: string;
|
||||
severity: string;
|
||||
stateKey: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
counters: Counter[],
|
||||
counters: Counter[];
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.AuxPanel")
|
||||
|
@ -99,9 +99,9 @@ export default class AuxPanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
private rateLimitedUpdate = new RateLimitedFunc(() => {
|
||||
private rateLimitedUpdate = throttle(() => {
|
||||
this.setState({ counters: this.computeCounters() });
|
||||
}, 500);
|
||||
}, 500, { leading: true, trailing: true });
|
||||
|
||||
private computeCounters() {
|
||||
const counters = [];
|
||||
|
|
|
@ -41,7 +41,7 @@ import { Key } from "../../../Keyboard";
|
|||
import { EMOTICON_TO_EMOJI } from "../../../emoji";
|
||||
import { CommandCategories, CommandMap, parseCommandString } from "../../../SlashCommands";
|
||||
import Range from "../../../editor/range";
|
||||
import MessageComposerFormatBar from "./MessageComposerFormatBar";
|
||||
import MessageComposerFormatBar, { Formatting } from "./MessageComposerFormatBar";
|
||||
import DocumentOffset from "../../../editor/offset";
|
||||
import { IDiff } from "../../../editor/diff";
|
||||
import AutocompleteWrapperModel from "../../../editor/autocomplete";
|
||||
|
@ -55,7 +55,7 @@ const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.sourc
|
|||
|
||||
const IS_MAC = navigator.platform.indexOf("Mac") !== -1;
|
||||
|
||||
function ctrlShortcutLabel(key) {
|
||||
function ctrlShortcutLabel(key: string): string {
|
||||
return (IS_MAC ? "⌘" : "Ctrl") + "+" + key;
|
||||
}
|
||||
|
||||
|
@ -81,14 +81,6 @@ function selectionEquals(a: Partial<Selection>, b: Selection): boolean {
|
|||
a.type === b.type;
|
||||
}
|
||||
|
||||
enum Formatting {
|
||||
Bold = "bold",
|
||||
Italics = "italics",
|
||||
Strikethrough = "strikethrough",
|
||||
Code = "code",
|
||||
Quote = "quote",
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
model: EditorModel;
|
||||
room: Room;
|
||||
|
@ -111,9 +103,9 @@ interface IState {
|
|||
|
||||
@replaceableComponent("views.rooms.BasicMessageEditor")
|
||||
export default class BasicMessageEditor extends React.Component<IProps, IState> {
|
||||
private editorRef = createRef<HTMLDivElement>();
|
||||
public readonly editorRef = createRef<HTMLDivElement>();
|
||||
private autocompleteRef = createRef<Autocomplete>();
|
||||
private formatBarRef = createRef<typeof MessageComposerFormatBar>();
|
||||
private formatBarRef = createRef<MessageComposerFormatBar>();
|
||||
|
||||
private modifiedFlag = false;
|
||||
private isIMEComposing = false;
|
||||
|
@ -156,7 +148,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
}
|
||||
}
|
||||
|
||||
private replaceEmoticon = (caretPosition: DocumentPosition) => {
|
||||
private replaceEmoticon = (caretPosition: DocumentPosition): number => {
|
||||
const { model } = this.props;
|
||||
const range = model.startRange(caretPosition);
|
||||
// expand range max 8 characters backwards from caretPosition,
|
||||
|
@ -188,7 +180,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
}
|
||||
};
|
||||
|
||||
private updateEditorState = (selection: Caret, inputType?: string, diff?: IDiff) => {
|
||||
private updateEditorState = (selection: Caret, inputType?: string, diff?: IDiff): void => {
|
||||
renderModel(this.editorRef.current, this.props.model);
|
||||
if (selection) { // set the caret/selection
|
||||
try {
|
||||
|
@ -230,25 +222,25 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
}
|
||||
};
|
||||
|
||||
private showPlaceholder() {
|
||||
private showPlaceholder(): void {
|
||||
// escape single quotes
|
||||
const placeholder = this.props.placeholder.replace(/'/g, '\\\'');
|
||||
this.editorRef.current.style.setProperty("--placeholder", `'${placeholder}'`);
|
||||
this.editorRef.current.classList.add("mx_BasicMessageComposer_inputEmpty");
|
||||
}
|
||||
|
||||
private hidePlaceholder() {
|
||||
private hidePlaceholder(): void {
|
||||
this.editorRef.current.classList.remove("mx_BasicMessageComposer_inputEmpty");
|
||||
this.editorRef.current.style.removeProperty("--placeholder");
|
||||
}
|
||||
|
||||
private onCompositionStart = () => {
|
||||
private onCompositionStart = (): void => {
|
||||
this.isIMEComposing = true;
|
||||
// even if the model is empty, the composition text shouldn't be mixed with the placeholder
|
||||
this.hidePlaceholder();
|
||||
};
|
||||
|
||||
private onCompositionEnd = () => {
|
||||
private onCompositionEnd = (): void => {
|
||||
this.isIMEComposing = false;
|
||||
// some browsers (Chrome) don't fire an input event after ending a composition,
|
||||
// so trigger a model update after the composition is done by calling the input handler.
|
||||
|
@ -271,14 +263,14 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
}
|
||||
};
|
||||
|
||||
isComposing(event: React.KeyboardEvent) {
|
||||
public isComposing(event: React.KeyboardEvent): boolean {
|
||||
// checking the event.isComposing flag just in case any browser out there
|
||||
// emits events related to the composition after compositionend
|
||||
// has been fired
|
||||
return !!(this.isIMEComposing || (event.nativeEvent && event.nativeEvent.isComposing));
|
||||
}
|
||||
|
||||
private onCutCopy = (event: ClipboardEvent, type: string) => {
|
||||
private onCutCopy = (event: ClipboardEvent, type: string): void => {
|
||||
const selection = document.getSelection();
|
||||
const text = selection.toString();
|
||||
if (text) {
|
||||
|
@ -296,15 +288,15 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
}
|
||||
};
|
||||
|
||||
private onCopy = (event: ClipboardEvent) => {
|
||||
private onCopy = (event: ClipboardEvent): void => {
|
||||
this.onCutCopy(event, "copy");
|
||||
};
|
||||
|
||||
private onCut = (event: ClipboardEvent) => {
|
||||
private onCut = (event: ClipboardEvent): void => {
|
||||
this.onCutCopy(event, "cut");
|
||||
};
|
||||
|
||||
private onPaste = (event: ClipboardEvent<HTMLDivElement>) => {
|
||||
private onPaste = (event: ClipboardEvent<HTMLDivElement>): boolean => {
|
||||
event.preventDefault(); // we always handle the paste ourselves
|
||||
if (this.props.onPaste && this.props.onPaste(event, this.props.model)) {
|
||||
// to prevent double handling, allow props.onPaste to skip internal onPaste
|
||||
|
@ -328,7 +320,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
replaceRangeAndMoveCaret(range, parts);
|
||||
};
|
||||
|
||||
private onInput = (event: Partial<InputEvent>) => {
|
||||
private onInput = (event: Partial<InputEvent>): void => {
|
||||
// ignore any input while doing IME compositions
|
||||
if (this.isIMEComposing) {
|
||||
return;
|
||||
|
@ -339,7 +331,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
this.props.model.update(text, event.inputType, caret);
|
||||
};
|
||||
|
||||
private insertText(textToInsert: string, inputType = "insertText") {
|
||||
private insertText(textToInsert: string, inputType = "insertText"): void {
|
||||
const sel = document.getSelection();
|
||||
const { caret, text } = getCaretOffsetAndText(this.editorRef.current, sel);
|
||||
const newText = text.substr(0, caret.offset) + textToInsert + text.substr(caret.offset);
|
||||
|
@ -353,14 +345,14 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
// we don't need to. But if the user is navigating the caret without input
|
||||
// we need to recalculate it, to be able to know where to insert content after
|
||||
// losing focus
|
||||
private setLastCaretFromPosition(position: DocumentPosition) {
|
||||
private setLastCaretFromPosition(position: DocumentPosition): void {
|
||||
const { model } = this.props;
|
||||
this._isCaretAtEnd = position.isAtEnd(model);
|
||||
this.lastCaret = position.asOffset(model);
|
||||
this.lastSelection = cloneSelection(document.getSelection());
|
||||
}
|
||||
|
||||
private refreshLastCaretIfNeeded() {
|
||||
private refreshLastCaretIfNeeded(): DocumentOffset {
|
||||
// XXX: needed when going up and down in editing messages ... not sure why yet
|
||||
// because the editors should stop doing this when when blurred ...
|
||||
// maybe it's on focus and the _editorRef isn't available yet or something.
|
||||
|
@ -377,38 +369,38 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
return this.lastCaret;
|
||||
}
|
||||
|
||||
clearUndoHistory() {
|
||||
public clearUndoHistory(): void {
|
||||
this.historyManager.clear();
|
||||
}
|
||||
|
||||
getCaret() {
|
||||
public getCaret(): DocumentOffset {
|
||||
return this.lastCaret;
|
||||
}
|
||||
|
||||
isSelectionCollapsed() {
|
||||
public isSelectionCollapsed(): boolean {
|
||||
return !this.lastSelection || this.lastSelection.isCollapsed;
|
||||
}
|
||||
|
||||
isCaretAtStart() {
|
||||
public isCaretAtStart(): boolean {
|
||||
return this.getCaret().offset === 0;
|
||||
}
|
||||
|
||||
isCaretAtEnd() {
|
||||
public isCaretAtEnd(): boolean {
|
||||
return this._isCaretAtEnd;
|
||||
}
|
||||
|
||||
private onBlur = () => {
|
||||
private onBlur = (): void => {
|
||||
document.removeEventListener("selectionchange", this.onSelectionChange);
|
||||
};
|
||||
|
||||
private onFocus = () => {
|
||||
private onFocus = (): void => {
|
||||
document.addEventListener("selectionchange", this.onSelectionChange);
|
||||
// force to recalculate
|
||||
this.lastSelection = null;
|
||||
this.refreshLastCaretIfNeeded();
|
||||
};
|
||||
|
||||
private onSelectionChange = () => {
|
||||
private onSelectionChange = (): void => {
|
||||
const { isEmpty } = this.props.model;
|
||||
|
||||
this.refreshLastCaretIfNeeded();
|
||||
|
@ -427,7 +419,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
}
|
||||
};
|
||||
|
||||
private onKeyDown = (event: React.KeyboardEvent) => {
|
||||
private onKeyDown = (event: React.KeyboardEvent): void => {
|
||||
const model = this.props.model;
|
||||
let handled = false;
|
||||
const action = getKeyBindingsManager().getMessageComposerAction(event);
|
||||
|
@ -523,7 +515,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
}
|
||||
};
|
||||
|
||||
private async tabCompleteName() {
|
||||
private async tabCompleteName(): Promise<void> {
|
||||
try {
|
||||
await new Promise<void>(resolve => this.setState({ showVisualBell: false }, resolve));
|
||||
const { model } = this.props;
|
||||
|
@ -557,27 +549,27 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
}
|
||||
}
|
||||
|
||||
isModified() {
|
||||
public isModified(): boolean {
|
||||
return this.modifiedFlag;
|
||||
}
|
||||
|
||||
private onAutoCompleteConfirm = (completion: ICompletion) => {
|
||||
private onAutoCompleteConfirm = (completion: ICompletion): void => {
|
||||
this.modifiedFlag = true;
|
||||
this.props.model.autoComplete.onComponentConfirm(completion);
|
||||
};
|
||||
|
||||
private onAutoCompleteSelectionChange = (completion: ICompletion, completionIndex: number) => {
|
||||
private onAutoCompleteSelectionChange = (completion: ICompletion, completionIndex: number): void => {
|
||||
this.modifiedFlag = true;
|
||||
this.props.model.autoComplete.onComponentSelectionChange(completion);
|
||||
this.setState({ completionIndex });
|
||||
};
|
||||
|
||||
private configureEmoticonAutoReplace = () => {
|
||||
private configureEmoticonAutoReplace = (): void => {
|
||||
const shouldReplace = SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji');
|
||||
this.props.model.setTransformCallback(shouldReplace ? this.replaceEmoticon : null);
|
||||
};
|
||||
|
||||
private configureShouldShowPillAvatar = () => {
|
||||
private configureShouldShowPillAvatar = (): void => {
|
||||
const showPillAvatar = SettingsStore.getValue("Pill.shouldShowPillAvatar");
|
||||
this.setState({ showPillAvatar });
|
||||
};
|
||||
|
@ -611,8 +603,8 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
this.editorRef.current.focus();
|
||||
}
|
||||
|
||||
private getInitialCaretPosition() {
|
||||
let caretPosition;
|
||||
private getInitialCaretPosition(): DocumentPosition {
|
||||
let caretPosition: DocumentPosition;
|
||||
if (this.props.initialCaret) {
|
||||
// if restoring state from a previous editor,
|
||||
// restore caret position from the state
|
||||
|
@ -625,7 +617,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
return caretPosition;
|
||||
}
|
||||
|
||||
private onFormatAction = (action: Formatting) => {
|
||||
private onFormatAction = (action: Formatting): void => {
|
||||
const range = getRangeForSelection(this.editorRef.current, this.props.model, document.getSelection());
|
||||
// trim the range as we want it to exclude leading/trailing spaces
|
||||
range.trim();
|
||||
|
@ -680,9 +672,9 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
});
|
||||
|
||||
const shortcuts = {
|
||||
bold: ctrlShortcutLabel("B"),
|
||||
italics: ctrlShortcutLabel("I"),
|
||||
quote: ctrlShortcutLabel(">"),
|
||||
[Formatting.Bold]: ctrlShortcutLabel("B"),
|
||||
[Formatting.Italics]: ctrlShortcutLabel("I"),
|
||||
[Formatting.Quote]: ctrlShortcutLabel(">"),
|
||||
};
|
||||
|
||||
const { completionIndex } = this.state;
|
||||
|
@ -714,11 +706,12 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
</div>);
|
||||
}
|
||||
|
||||
focus() {
|
||||
public focus(): void {
|
||||
this.editorRef.current.focus();
|
||||
}
|
||||
|
||||
public insertMention(userId: string) {
|
||||
public insertMention(userId: string): void {
|
||||
this.modifiedFlag = true;
|
||||
const { model } = this.props;
|
||||
const { partCreator } = model;
|
||||
const member = this.props.room.getMember(userId);
|
||||
|
@ -736,7 +729,8 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
this.focus();
|
||||
}
|
||||
|
||||
public insertQuotedMessage(event: MatrixEvent) {
|
||||
public insertQuotedMessage(event: MatrixEvent): void {
|
||||
this.modifiedFlag = true;
|
||||
const { model } = this.props;
|
||||
const { partCreator } = model;
|
||||
const quoteParts = parseEvent(event, partCreator, { isQuotedMessage: true });
|
||||
|
@ -751,7 +745,8 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
this.focus();
|
||||
}
|
||||
|
||||
public insertPlaintext(text: string) {
|
||||
public insertPlaintext(text: string): void {
|
||||
this.modifiedFlag = true;
|
||||
const { model } = this.props;
|
||||
const { partCreator } = model;
|
||||
const caret = this.getCaret();
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,37 +13,42 @@ 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 React, { createRef, KeyboardEvent } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { EventStatus, IContent, MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import PropTypes from 'prop-types';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import EditorModel from '../../../editor/model';
|
||||
import { getCaretOffsetAndText } from '../../../editor/dom';
|
||||
import { htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand } from '../../../editor/serialize';
|
||||
import { findEditableEvent } from '../../../utils/EventUtils';
|
||||
import { parseEvent } from '../../../editor/deserialize';
|
||||
import { CommandPartCreator } from '../../../editor/parts';
|
||||
import { CommandPartCreator, Part, PartCreator } from '../../../editor/parts';
|
||||
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
||||
import classNames from 'classnames';
|
||||
import { EventStatus } from 'matrix-js-sdk/src/models/event';
|
||||
import BasicMessageComposer from "./BasicMessageComposer";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { CommandCategories, getCommand } from '../../../SlashCommands';
|
||||
import { Command, CommandCategories, getCommand } from '../../../SlashCommands';
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import { getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindingsManager';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import SendHistoryManager from '../../../SendHistoryManager';
|
||||
import Modal from '../../../Modal';
|
||||
import { MsgType } from 'matrix-js-sdk/src/@types/event';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
function _isReply(mxEvent) {
|
||||
function eventIsReply(mxEvent: MatrixEvent): boolean {
|
||||
const relatesTo = mxEvent.getContent()["m.relates_to"];
|
||||
const isReply = !!(relatesTo && relatesTo["m.in_reply_to"]);
|
||||
return isReply;
|
||||
return !!(relatesTo && relatesTo["m.in_reply_to"]);
|
||||
}
|
||||
|
||||
function getHtmlReplyFallback(mxEvent) {
|
||||
function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
|
||||
const html = mxEvent.getContent().formatted_body;
|
||||
if (!html) {
|
||||
return "";
|
||||
|
@ -54,7 +58,7 @@ function getHtmlReplyFallback(mxEvent) {
|
|||
return (mxReply && mxReply.outerHTML) || "";
|
||||
}
|
||||
|
||||
function getTextReplyFallback(mxEvent) {
|
||||
function getTextReplyFallback(mxEvent: MatrixEvent): string {
|
||||
const body = mxEvent.getContent().body;
|
||||
const lines = body.split("\n").map(l => l.trim());
|
||||
if (lines.length > 2 && lines[0].startsWith("> ") && lines[1].length === 0) {
|
||||
|
@ -63,12 +67,12 @@ function getTextReplyFallback(mxEvent) {
|
|||
return "";
|
||||
}
|
||||
|
||||
function createEditContent(model, editedEvent) {
|
||||
function createEditContent(model: EditorModel, editedEvent: MatrixEvent): IContent {
|
||||
const isEmote = containsEmote(model);
|
||||
if (isEmote) {
|
||||
model = stripEmoteCommand(model);
|
||||
}
|
||||
const isReply = _isReply(editedEvent);
|
||||
const isReply = eventIsReply(editedEvent);
|
||||
let plainPrefix = "";
|
||||
let htmlPrefix = "";
|
||||
|
||||
|
@ -79,11 +83,11 @@ function createEditContent(model, editedEvent) {
|
|||
|
||||
const body = textSerialize(model);
|
||||
|
||||
const newContent = {
|
||||
"msgtype": isEmote ? "m.emote" : "m.text",
|
||||
const newContent: IContent = {
|
||||
"msgtype": isEmote ? MsgType.Emote : MsgType.Text,
|
||||
"body": body,
|
||||
};
|
||||
const contentBody = {
|
||||
const contentBody: IContent = {
|
||||
msgtype: newContent.msgtype,
|
||||
body: `${plainPrefix} * ${body}`,
|
||||
};
|
||||
|
@ -105,55 +109,61 @@ function createEditContent(model, editedEvent) {
|
|||
}, contentBody);
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
editState: EditorStateTransfer;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
saveDisabled: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.EditMessageComposer")
|
||||
export default class EditMessageComposer extends React.Component {
|
||||
static propTypes = {
|
||||
// the message event being edited
|
||||
editState: PropTypes.instanceOf(EditorStateTransfer).isRequired,
|
||||
};
|
||||
|
||||
export default class EditMessageComposer extends React.Component<IProps, IState> {
|
||||
static contextType = MatrixClientContext;
|
||||
context!: React.ContextType<typeof MatrixClientContext>;
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.model = null;
|
||||
this._editorRef = null;
|
||||
private readonly editorRef = createRef<BasicMessageComposer>();
|
||||
private readonly dispatcherRef: string;
|
||||
private model: EditorModel = null;
|
||||
|
||||
constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
|
||||
super(props);
|
||||
this.context = context; // otherwise React will only set it prior to render due to type def above
|
||||
|
||||
const isRestored = this.createEditorModel();
|
||||
const ev = this.props.editState.getEvent();
|
||||
this.state = {
|
||||
saveDisabled: true,
|
||||
saveDisabled: !isRestored || !this.isContentModified(createEditContent(this.model, ev)["m.new_content"]),
|
||||
};
|
||||
this._createEditorModel();
|
||||
window.addEventListener("beforeunload", this._saveStoredEditorState);
|
||||
|
||||
window.addEventListener("beforeunload", this.saveStoredEditorState);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
}
|
||||
|
||||
_setEditorRef = ref => {
|
||||
this._editorRef = ref;
|
||||
};
|
||||
|
||||
_getRoom() {
|
||||
private getRoom(): Room {
|
||||
return this.context.getRoom(this.props.editState.getEvent().getRoomId());
|
||||
}
|
||||
|
||||
_onKeyDown = (event) => {
|
||||
private onKeyDown = (event: KeyboardEvent): void => {
|
||||
// ignore any keypress while doing IME compositions
|
||||
if (this._editorRef.isComposing(event)) {
|
||||
if (this.editorRef.current?.isComposing(event)) {
|
||||
return;
|
||||
}
|
||||
const action = getKeyBindingsManager().getMessageComposerAction(event);
|
||||
switch (action) {
|
||||
case MessageComposerAction.Send:
|
||||
this._sendEdit();
|
||||
this.sendEdit();
|
||||
event.preventDefault();
|
||||
break;
|
||||
case MessageComposerAction.CancelEditing:
|
||||
this._cancelEdit();
|
||||
this.cancelEdit();
|
||||
break;
|
||||
case MessageComposerAction.EditPrevMessage: {
|
||||
if (this._editorRef.isModified() || !this._editorRef.isCaretAtStart()) {
|
||||
if (this.editorRef.current?.isModified() || !this.editorRef.current?.isCaretAtStart()) {
|
||||
return;
|
||||
}
|
||||
const previousEvent = findEditableEvent(this._getRoom(), false,
|
||||
const previousEvent = findEditableEvent(this.getRoom(), false,
|
||||
this.props.editState.getEvent().getId());
|
||||
if (previousEvent) {
|
||||
dis.dispatch({ action: 'edit_event', event: previousEvent });
|
||||
|
@ -162,47 +172,47 @@ export default class EditMessageComposer extends React.Component {
|
|||
break;
|
||||
}
|
||||
case MessageComposerAction.EditNextMessage: {
|
||||
if (this._editorRef.isModified() || !this._editorRef.isCaretAtEnd()) {
|
||||
if (this.editorRef.current?.isModified() || !this.editorRef.current?.isCaretAtEnd()) {
|
||||
return;
|
||||
}
|
||||
const nextEvent = findEditableEvent(this._getRoom(), true, this.props.editState.getEvent().getId());
|
||||
const nextEvent = findEditableEvent(this.getRoom(), true, this.props.editState.getEvent().getId());
|
||||
if (nextEvent) {
|
||||
dis.dispatch({ action: 'edit_event', event: nextEvent });
|
||||
} else {
|
||||
this._clearStoredEditorState();
|
||||
this.clearStoredEditorState();
|
||||
dis.dispatch({ action: 'edit_event', event: null });
|
||||
dis.fire(Action.FocusComposer);
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
}
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private get editorRoomKey(): string {
|
||||
return `mx_edit_room_${this.getRoom().roomId}`;
|
||||
}
|
||||
|
||||
get _editorRoomKey() {
|
||||
return `mx_edit_room_${this._getRoom().roomId}`;
|
||||
}
|
||||
|
||||
get _editorStateKey() {
|
||||
private get editorStateKey(): string {
|
||||
return `mx_edit_state_${this.props.editState.getEvent().getId()}`;
|
||||
}
|
||||
|
||||
_cancelEdit = () => {
|
||||
this._clearStoredEditorState();
|
||||
private cancelEdit = (): void => {
|
||||
this.clearStoredEditorState();
|
||||
dis.dispatch({ action: "edit_event", event: null });
|
||||
dis.fire(Action.FocusComposer);
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
};
|
||||
|
||||
private get shouldSaveStoredEditorState(): boolean {
|
||||
return localStorage.getItem(this.editorRoomKey) !== null;
|
||||
}
|
||||
|
||||
get _shouldSaveStoredEditorState() {
|
||||
return localStorage.getItem(this._editorRoomKey) !== null;
|
||||
}
|
||||
|
||||
_restoreStoredEditorState(partCreator) {
|
||||
const json = localStorage.getItem(this._editorStateKey);
|
||||
private restoreStoredEditorState(partCreator: PartCreator): Part[] {
|
||||
const json = localStorage.getItem(this.editorStateKey);
|
||||
if (json) {
|
||||
try {
|
||||
const { parts: serializedParts } = JSON.parse(json);
|
||||
const parts = serializedParts.map(p => partCreator.deserializePart(p));
|
||||
const parts: Part[] = serializedParts.map(p => partCreator.deserializePart(p));
|
||||
return parts;
|
||||
} catch (e) {
|
||||
console.error("Error parsing editing state: ", e);
|
||||
|
@ -210,25 +220,25 @@ export default class EditMessageComposer extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_clearStoredEditorState() {
|
||||
localStorage.removeItem(this._editorRoomKey);
|
||||
localStorage.removeItem(this._editorStateKey);
|
||||
private clearStoredEditorState(): void {
|
||||
localStorage.removeItem(this.editorRoomKey);
|
||||
localStorage.removeItem(this.editorStateKey);
|
||||
}
|
||||
|
||||
_clearPreviousEdit() {
|
||||
if (localStorage.getItem(this._editorRoomKey)) {
|
||||
localStorage.removeItem(`mx_edit_state_${localStorage.getItem(this._editorRoomKey)}`);
|
||||
private clearPreviousEdit(): void {
|
||||
if (localStorage.getItem(this.editorRoomKey)) {
|
||||
localStorage.removeItem(`mx_edit_state_${localStorage.getItem(this.editorRoomKey)}`);
|
||||
}
|
||||
}
|
||||
|
||||
_saveStoredEditorState() {
|
||||
private saveStoredEditorState = (): void => {
|
||||
const item = SendHistoryManager.createItem(this.model);
|
||||
this._clearPreviousEdit();
|
||||
localStorage.setItem(this._editorRoomKey, this.props.editState.getEvent().getId());
|
||||
localStorage.setItem(this._editorStateKey, JSON.stringify(item));
|
||||
}
|
||||
this.clearPreviousEdit();
|
||||
localStorage.setItem(this.editorRoomKey, this.props.editState.getEvent().getId());
|
||||
localStorage.setItem(this.editorStateKey, JSON.stringify(item));
|
||||
};
|
||||
|
||||
_isSlashCommand() {
|
||||
private isSlashCommand(): boolean {
|
||||
const parts = this.model.parts;
|
||||
const firstPart = parts[0];
|
||||
if (firstPart) {
|
||||
|
@ -244,19 +254,18 @@ export default class EditMessageComposer extends React.Component {
|
|||
return false;
|
||||
}
|
||||
|
||||
_isContentModified(newContent) {
|
||||
private isContentModified(newContent: IContent): boolean {
|
||||
// if nothing has changed then bail
|
||||
const oldContent = this.props.editState.getEvent().getContent();
|
||||
if (!this._editorRef.isModified() ||
|
||||
(oldContent["msgtype"] === newContent["msgtype"] && oldContent["body"] === newContent["body"] &&
|
||||
if (oldContent["msgtype"] === newContent["msgtype"] && oldContent["body"] === newContent["body"] &&
|
||||
oldContent["format"] === newContent["format"] &&
|
||||
oldContent["formatted_body"] === newContent["formatted_body"])) {
|
||||
oldContent["formatted_body"] === newContent["formatted_body"]) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
_getSlashCommand() {
|
||||
private getSlashCommand(): [Command, string, string] {
|
||||
const commandText = this.model.parts.reduce((text, part) => {
|
||||
// use mxid to textify user pills in a command
|
||||
if (part.type === "user-pill") {
|
||||
|
@ -268,7 +277,7 @@ export default class EditMessageComposer extends React.Component {
|
|||
return [cmd, args, commandText];
|
||||
}
|
||||
|
||||
async _runSlashCommand(cmd, args, roomId) {
|
||||
private async runSlashCommand(cmd: Command, args: string, roomId: string): Promise<void> {
|
||||
const result = cmd.run(roomId, args);
|
||||
let messageContent;
|
||||
let error = result.error;
|
||||
|
@ -285,7 +294,6 @@ export default class EditMessageComposer extends React.Component {
|
|||
}
|
||||
if (error) {
|
||||
console.error("Command failure: %s", error);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
// assume the error is a server error when the command is async
|
||||
const isServerError = !!result.promise;
|
||||
const title = isServerError ? _td("Server error") : _td("Command error");
|
||||
|
@ -309,7 +317,7 @@ export default class EditMessageComposer extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_sendEdit = async () => {
|
||||
private sendEdit = async (): Promise<void> => {
|
||||
const startTime = CountlyAnalytics.getTimestamp();
|
||||
const editedEvent = this.props.editState.getEvent();
|
||||
const editContent = createEditContent(this.model, editedEvent);
|
||||
|
@ -318,20 +326,19 @@ export default class EditMessageComposer extends React.Component {
|
|||
let shouldSend = true;
|
||||
|
||||
// If content is modified then send an updated event into the room
|
||||
if (this._isContentModified(newContent)) {
|
||||
if (this.isContentModified(newContent)) {
|
||||
const roomId = editedEvent.getRoomId();
|
||||
if (!containsEmote(this.model) && this._isSlashCommand()) {
|
||||
const [cmd, args, commandText] = this._getSlashCommand();
|
||||
if (!containsEmote(this.model) && this.isSlashCommand()) {
|
||||
const [cmd, args, commandText] = this.getSlashCommand();
|
||||
if (cmd) {
|
||||
if (cmd.category === CommandCategories.messages) {
|
||||
editContent["m.new_content"] = await this._runSlashCommand(cmd, args, roomId);
|
||||
editContent["m.new_content"] = await this.runSlashCommand(cmd, args, roomId);
|
||||
} else {
|
||||
this._runSlashCommand(cmd, args, roomId);
|
||||
this.runSlashCommand(cmd, args, roomId);
|
||||
shouldSend = false;
|
||||
}
|
||||
} else {
|
||||
// ask the user if their unknown command should be sent as a message
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const { finished } = Modal.createTrackedDialog("Unknown command", "", QuestionDialog, {
|
||||
title: _t("Unknown Command"),
|
||||
description: <div>
|
||||
|
@ -358,9 +365,9 @@ export default class EditMessageComposer extends React.Component {
|
|||
}
|
||||
}
|
||||
if (shouldSend) {
|
||||
this._cancelPreviousPendingEdit();
|
||||
this.cancelPreviousPendingEdit();
|
||||
const prom = this.context.sendMessage(roomId, editContent);
|
||||
this._clearStoredEditorState();
|
||||
this.clearStoredEditorState();
|
||||
dis.dispatch({ action: "message_sent" });
|
||||
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, true, false, editContent);
|
||||
}
|
||||
|
@ -368,10 +375,10 @@ export default class EditMessageComposer extends React.Component {
|
|||
|
||||
// close the event editing and focus composer
|
||||
dis.dispatch({ action: "edit_event", event: null });
|
||||
dis.fire(Action.FocusComposer);
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
};
|
||||
|
||||
_cancelPreviousPendingEdit() {
|
||||
private cancelPreviousPendingEdit(): void {
|
||||
const originalEvent = this.props.editState.getEvent();
|
||||
const previousEdit = originalEvent.replacingEvent();
|
||||
if (previousEdit && (
|
||||
|
@ -389,54 +396,45 @@ export default class EditMessageComposer extends React.Component {
|
|||
const sel = document.getSelection();
|
||||
let caret;
|
||||
if (sel.focusNode) {
|
||||
caret = getCaretOffsetAndText(this._editorRef, sel).caret;
|
||||
caret = getCaretOffsetAndText(this.editorRef.current?.editorRef.current, sel).caret;
|
||||
}
|
||||
const parts = this.model.serializeParts();
|
||||
// if caret is undefined because for some reason there isn't a valid selection,
|
||||
// then when mounting the editor again with the same editor state,
|
||||
// it will set the cursor at the end.
|
||||
this.props.editState.setEditorState(caret, parts);
|
||||
window.removeEventListener("beforeunload", this._saveStoredEditorState);
|
||||
if (this._shouldSaveStoredEditorState) {
|
||||
this._saveStoredEditorState();
|
||||
window.removeEventListener("beforeunload", this.saveStoredEditorState);
|
||||
if (this.shouldSaveStoredEditorState) {
|
||||
this.saveStoredEditorState();
|
||||
}
|
||||
dis.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
||||
_createEditorModel() {
|
||||
private createEditorModel(): boolean {
|
||||
const { editState } = this.props;
|
||||
const room = this._getRoom();
|
||||
const room = this.getRoom();
|
||||
const partCreator = new CommandPartCreator(room, this.context);
|
||||
|
||||
let parts;
|
||||
let isRestored = false;
|
||||
if (editState.hasEditorState()) {
|
||||
// if restoring state from a previous editor,
|
||||
// restore serialized parts from the state
|
||||
parts = editState.getSerializedParts().map(p => partCreator.deserializePart(p));
|
||||
} else {
|
||||
//otherwise, either restore serialized parts from localStorage or parse the body of the event
|
||||
parts = this._restoreStoredEditorState(partCreator) || parseEvent(editState.getEvent(), partCreator);
|
||||
// otherwise, either restore serialized parts from localStorage or parse the body of the event
|
||||
const restoredParts = this.restoreStoredEditorState(partCreator);
|
||||
parts = restoredParts || parseEvent(editState.getEvent(), partCreator);
|
||||
isRestored = !!restoredParts;
|
||||
}
|
||||
this.model = new EditorModel(parts, partCreator);
|
||||
this._saveStoredEditorState();
|
||||
this.saveStoredEditorState();
|
||||
|
||||
return isRestored;
|
||||
}
|
||||
|
||||
_getInitialCaretPosition() {
|
||||
const { editState } = this.props;
|
||||
let caretPosition;
|
||||
if (editState.hasEditorState() && editState.getCaret()) {
|
||||
// if restoring state from a previous editor,
|
||||
// restore caret position from the state
|
||||
const caret = editState.getCaret();
|
||||
caretPosition = this.model.positionForOffset(caret.offset, caret.atNodeEnd);
|
||||
} else {
|
||||
// otherwise, set it at the end
|
||||
caretPosition = this.model.getPositionAtEnd();
|
||||
}
|
||||
return caretPosition;
|
||||
}
|
||||
|
||||
_onChange = () => {
|
||||
if (!this.state.saveDisabled || !this._editorRef || !this._editorRef.isModified()) {
|
||||
private onChange = (): void => {
|
||||
if (!this.state.saveDisabled || !this.editorRef.current?.isModified()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -445,33 +443,36 @@ export default class EditMessageComposer extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
onAction = payload => {
|
||||
if (payload.action === "edit_composer_insert" && this._editorRef) {
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
if (payload.action === "edit_composer_insert" && this.editorRef.current) {
|
||||
if (payload.userId) {
|
||||
this._editorRef.insertMention(payload.userId);
|
||||
this.editorRef.current?.insertMention(payload.userId);
|
||||
} else if (payload.event) {
|
||||
this._editorRef.insertQuotedMessage(payload.event);
|
||||
this.editorRef.current?.insertQuotedMessage(payload.event);
|
||||
} else if (payload.text) {
|
||||
this._editorRef.insertPlaintext(payload.text);
|
||||
this.editorRef.current?.insertPlaintext(payload.text);
|
||||
}
|
||||
} else if (payload.action === Action.FocusEditMessageComposer && this.editorRef.current) {
|
||||
this.editorRef.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return (<div className={classNames("mx_EditMessageComposer", this.props.className)} onKeyDown={this._onKeyDown}>
|
||||
return (<div className={classNames("mx_EditMessageComposer", this.props.className)} onKeyDown={this.onKeyDown}>
|
||||
<BasicMessageComposer
|
||||
ref={this._setEditorRef}
|
||||
ref={this.editorRef}
|
||||
model={this.model}
|
||||
room={this._getRoom()}
|
||||
room={this.getRoom()}
|
||||
initialCaret={this.props.editState.getCaret()}
|
||||
label={_t("Edit message")}
|
||||
onChange={this._onChange}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<div className="mx_EditMessageComposer_buttons">
|
||||
<AccessibleButton kind="secondary" onClick={this._cancelEdit}>{_t("Cancel")}</AccessibleButton>
|
||||
<AccessibleButton kind="primary" onClick={this._sendEdit} disabled={this.state.saveDisabled}>
|
||||
{_t("Save")}
|
||||
<AccessibleButton kind="secondary" onClick={this.cancelEdit}>
|
||||
{ _t("Cancel") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton kind="primary" onClick={this.sendEdit} disabled={this.state.saveDisabled}>
|
||||
{ _t("Save") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>);
|
|
@ -47,6 +47,13 @@ import { StaticNotificationState } from "../../../stores/notifications/StaticNot
|
|||
import NotificationBadge from "./NotificationBadge";
|
||||
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||
import { Action } from '../../../dispatcher/actions';
|
||||
import MemberAvatar from '../avatars/MemberAvatar';
|
||||
import SenderProfile from '../messages/SenderProfile';
|
||||
import MessageTimestamp from '../messages/MessageTimestamp';
|
||||
import TooltipButton from '../elements/TooltipButton';
|
||||
import ReadReceiptMarker from "./ReadReceiptMarker";
|
||||
import MessageActionBar from "../messages/MessageActionBar";
|
||||
import ReactionsRow from '../messages/ReactionsRow';
|
||||
|
||||
const eventTileTypes = {
|
||||
[EventType.RoomMessage]: 'messages.MessageEvent',
|
||||
|
@ -267,7 +274,7 @@ interface IProps {
|
|||
showReactions?: boolean;
|
||||
|
||||
// which layout to use
|
||||
layout: Layout;
|
||||
layout?: Layout;
|
||||
|
||||
// whether or not to show flair at all
|
||||
enableFlair?: boolean;
|
||||
|
@ -287,10 +294,10 @@ interface IProps {
|
|||
permalinkCreator?: RoomPermalinkCreator;
|
||||
|
||||
// Symbol of the root node
|
||||
as?: string
|
||||
as?: string;
|
||||
|
||||
// whether or not to always show timestamps
|
||||
alwaysShowTimestamps?: boolean
|
||||
alwaysShowTimestamps?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -321,6 +328,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
static defaultProps = {
|
||||
// no-op function because onHeightChanged is optional yet some sub-components assume its existence
|
||||
onHeightChanged: function() {},
|
||||
layout: Layout.Group,
|
||||
};
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
@ -665,7 +673,6 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
const ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker');
|
||||
const avatars = [];
|
||||
const receiptOffset = 15;
|
||||
let left = 0;
|
||||
|
@ -732,7 +739,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
onSenderProfileClick = event => {
|
||||
onSenderProfileClick = () => {
|
||||
const mxEvent = this.props.mxEvent;
|
||||
dis.dispatch<ComposerInsertPayload>({
|
||||
action: Action.ComposerInsert,
|
||||
|
@ -840,10 +847,6 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
render() {
|
||||
const MessageTimestamp = sdk.getComponent('messages.MessageTimestamp');
|
||||
const SenderProfile = sdk.getComponent('messages.SenderProfile');
|
||||
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
|
||||
//console.info("EventTile showUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
|
||||
|
||||
const content = this.props.mxEvent.getContent();
|
||||
|
@ -986,7 +989,6 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
const MessageActionBar = sdk.getComponent('messages.MessageActionBar');
|
||||
const actionBar = !isEditing ? <MessageActionBar
|
||||
mxEvent={this.props.mxEvent}
|
||||
reactions={this.state.reactions}
|
||||
|
@ -1026,7 +1028,6 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
{ 'requestLink': (sub) => <a onClick={this.onRequestKeysClick}>{ sub }</a> },
|
||||
);
|
||||
|
||||
const TooltipButton = sdk.getComponent('elements.TooltipButton');
|
||||
const keyRequestInfo = isEncryptionFailure && !isRedacted ?
|
||||
<div className="mx_EventTile_keyRequestInfo">
|
||||
<span className="mx_EventTile_keyRequestInfo_text">
|
||||
|
@ -1037,7 +1038,6 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
|
||||
let reactionsRow;
|
||||
if (!isRedacted) {
|
||||
const ReactionsRow = sdk.getComponent('messages.ReactionsRow');
|
||||
reactionsRow = <ReactionsRow
|
||||
mxEvent={this.props.mxEvent}
|
||||
reactions={this.state.reactions}
|
||||
|
|
76
src/components/views/rooms/LinkPreviewGroup.tsx
Normal file
76
src/components/views/rooms/LinkPreviewGroup.tsx
Normal file
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import { useStateToggle } from "../../../hooks/useStateToggle";
|
||||
import LinkPreviewWidget from "./LinkPreviewWidget";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
const INITIAL_NUM_PREVIEWS = 2;
|
||||
|
||||
interface IProps {
|
||||
links: string[]; // the URLs to be previewed
|
||||
mxEvent: MatrixEvent; // the Event associated with the preview
|
||||
onCancelClick?(): void; // called when the preview's cancel ('hide') button is clicked
|
||||
onHeightChanged?(): void; // called when the preview's contents has loaded
|
||||
}
|
||||
|
||||
const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick, onHeightChanged }) => {
|
||||
const [expanded, toggleExpanded] = useStateToggle();
|
||||
useEffect(() => {
|
||||
onHeightChanged();
|
||||
}, [onHeightChanged, expanded]);
|
||||
|
||||
const shownLinks = expanded ? links : links.slice(0, INITIAL_NUM_PREVIEWS);
|
||||
|
||||
let toggleButton;
|
||||
if (links.length > INITIAL_NUM_PREVIEWS) {
|
||||
toggleButton = <AccessibleButton onClick={toggleExpanded}>
|
||||
{ expanded
|
||||
? _t("Collapse")
|
||||
: _t("Show %(count)s other previews", { count: links.length - shownLinks.length }) }
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
return <div className="mx_LinkPreviewGroup">
|
||||
{ shownLinks.map((link, i) => (
|
||||
<LinkPreviewWidget key={link} link={link} mxEvent={mxEvent} onHeightChanged={onHeightChanged}>
|
||||
{ i === 0 ? (
|
||||
<AccessibleButton
|
||||
className="mx_LinkPreviewGroup_hide"
|
||||
onClick={onCancelClick}
|
||||
aria-label={_t("Close preview")}
|
||||
>
|
||||
<img
|
||||
className="mx_filterFlipColor"
|
||||
alt=""
|
||||
role="presentation"
|
||||
src={require("../../../../res/img/cancel.svg")}
|
||||
width="18"
|
||||
height="18"
|
||||
/>
|
||||
</AccessibleButton>
|
||||
): undefined }
|
||||
</LinkPreviewWidget>
|
||||
)) }
|
||||
{ toggleButton }
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default LinkPreviewGroup;
|
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -16,26 +15,33 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, { createRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { AllHtmlEntities } from 'html-entities';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { IPreviewUrlResponse } from 'matrix-js-sdk/src/client';
|
||||
|
||||
import { linkifyElement } from '../../../HtmlUtils';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import * as sdk from "../../../index";
|
||||
import Modal from "../../../Modal";
|
||||
import * as ImageUtils from "../../../ImageUtils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import ImageView from '../elements/ImageView';
|
||||
|
||||
interface IProps {
|
||||
link: string; // the URL being previewed
|
||||
mxEvent: MatrixEvent; // the Event associated with the preview
|
||||
onHeightChanged(): void; // called when the preview's contents has loaded
|
||||
}
|
||||
|
||||
interface IState {
|
||||
preview?: IPreviewUrlResponse;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.LinkPreviewWidget")
|
||||
export default class LinkPreviewWidget extends React.Component {
|
||||
static propTypes = {
|
||||
link: PropTypes.string.isRequired, // the URL being previewed
|
||||
mxEvent: PropTypes.object.isRequired, // the Event associated with the preview
|
||||
onCancelClick: PropTypes.func, // called when the preview's cancel ('hide') button is clicked
|
||||
onHeightChanged: PropTypes.func, // called when the preview's contents has loaded
|
||||
};
|
||||
export default class LinkPreviewWidget extends React.Component<IProps, IState> {
|
||||
private unmounted = false;
|
||||
private readonly description = createRef<HTMLDivElement>();
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -44,31 +50,25 @@ export default class LinkPreviewWidget extends React.Component {
|
|||
preview: null,
|
||||
};
|
||||
|
||||
this.unmounted = false;
|
||||
MatrixClientPeg.get().getUrlPreview(this.props.link, this.props.mxEvent.getTs()).then((res)=>{
|
||||
MatrixClientPeg.get().getUrlPreview(this.props.link, this.props.mxEvent.getTs()).then((preview) => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
this.setState(
|
||||
{ preview: res },
|
||||
this.props.onHeightChanged,
|
||||
);
|
||||
}, (error)=>{
|
||||
this.setState({ preview }, this.props.onHeightChanged);
|
||||
}, (error) => {
|
||||
console.error("Failed to get URL preview: " + error);
|
||||
});
|
||||
|
||||
this._description = createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this._description.current) {
|
||||
linkifyElement(this._description.current);
|
||||
if (this.description.current) {
|
||||
linkifyElement(this.description.current);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this._description.current) {
|
||||
linkifyElement(this._description.current);
|
||||
if (this.description.current) {
|
||||
linkifyElement(this.description.current);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -76,11 +76,10 @@ export default class LinkPreviewWidget extends React.Component {
|
|||
this.unmounted = true;
|
||||
}
|
||||
|
||||
onImageClick = ev => {
|
||||
private onImageClick = ev => {
|
||||
const p = this.state.preview;
|
||||
if (ev.button != 0 || ev.metaKey) return;
|
||||
ev.preventDefault();
|
||||
const ImageView = sdk.getComponent("elements.ImageView");
|
||||
|
||||
let src = p["og:image"];
|
||||
if (src && src.startsWith("mxc://")) {
|
||||
|
@ -136,21 +135,21 @@ export default class LinkPreviewWidget extends React.Component {
|
|||
// opaque string. This does not allow any HTML to be injected into the DOM.
|
||||
const description = AllHtmlEntities.decode(p["og:description"] || "");
|
||||
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return (
|
||||
<div className="mx_LinkPreviewWidget">
|
||||
{ img }
|
||||
<div className="mx_LinkPreviewWidget_caption">
|
||||
<div className="mx_LinkPreviewWidget_title"><a href={this.props.link} target="_blank" rel="noreferrer noopener">{ p["og:title"] }</a></div>
|
||||
<div className="mx_LinkPreviewWidget_siteName">{ p["og:site_name"] ? (" - " + p["og:site_name"]) : null }</div>
|
||||
<div className="mx_LinkPreviewWidget_description" ref={this._description}>
|
||||
<div className="mx_LinkPreviewWidget_title">
|
||||
<a href={this.props.link} target="_blank" rel="noreferrer noopener">{ p["og:title"] }</a>
|
||||
{ p["og:site_name"] && <span className="mx_LinkPreviewWidget_siteName">
|
||||
{ (" - " + p["og:site_name"]) }
|
||||
</span> }
|
||||
</div>
|
||||
<div className="mx_LinkPreviewWidget_description" ref={this.description}>
|
||||
{ description }
|
||||
</div>
|
||||
</div>
|
||||
<AccessibleButton className="mx_LinkPreviewWidget_cancel" onClick={this.props.onCancelClick} aria-label={_t("Close preview")}>
|
||||
<img className="mx_filterFlipColor" alt="" role="presentation"
|
||||
src={require("../../../../res/img/cancel.svg")} width="18" height="18" />
|
||||
</AccessibleButton>
|
||||
{ this.props.children }
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -22,7 +22,6 @@ import { _t } from '../../../languageHandler';
|
|||
import SdkConfig from '../../../SdkConfig';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { isValid3pidInvite } from "../../../RoomInvite";
|
||||
import rateLimitedFunction from "../../../ratelimitedfunc";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||
import BaseCard from "../right_panel/BaseCard";
|
||||
|
@ -43,6 +42,7 @@ import AccessibleButton from '../elements/AccessibleButton';
|
|||
import EntityTile from "./EntityTile";
|
||||
import MemberTile from "./MemberTile";
|
||||
import BaseAvatar from '../avatars/BaseAvatar';
|
||||
import { throttle } from 'lodash';
|
||||
|
||||
const INITIAL_LOAD_NUM_MEMBERS = 30;
|
||||
const INITIAL_LOAD_NUM_INVITED = 5;
|
||||
|
@ -133,7 +133,7 @@ export default class MemberList extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
// cancel any pending calls to the rate_limited_funcs
|
||||
this.updateList.cancelPendingCall();
|
||||
this.updateList.cancel();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -237,9 +237,9 @@ export default class MemberList extends React.Component<IProps, IState> {
|
|||
if (this.canInvite !== this.state.canInvite) this.setState({ canInvite: this.canInvite });
|
||||
};
|
||||
|
||||
private updateList = rateLimitedFunction(() => {
|
||||
private updateList = throttle(() => {
|
||||
this.updateListNow();
|
||||
}, 500);
|
||||
}, 500, { leading: true, trailing: true });
|
||||
|
||||
private updateListNow(): void {
|
||||
const members = this.roomMembers();
|
||||
|
|
|
@ -17,7 +17,6 @@ import React from 'react';
|
|||
import classNames from 'classnames';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import * as sdk from '../../../index';
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
|
@ -43,13 +42,15 @@ import { E2EStatus } from '../../../utils/ShieldUtils';
|
|||
import SendMessageComposer from "./SendMessageComposer";
|
||||
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import EditorModel from "../../../editor/model";
|
||||
import EmojiPicker from '../emojipicker/EmojiPicker';
|
||||
import MemberStatusMessageAvatar from "../avatars/MemberStatusMessageAvatar";
|
||||
|
||||
interface IComposerAvatarProps {
|
||||
me: object;
|
||||
}
|
||||
|
||||
function ComposerAvatar(props: IComposerAvatarProps) {
|
||||
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
|
||||
return <div className="mx_MessageComposer_avatar">
|
||||
<MemberStatusMessageAvatar member={props.me} width={24} height={24} />
|
||||
</div>;
|
||||
|
@ -75,7 +76,6 @@ const EmojiButton = ({ addEmoji }) => {
|
|||
let contextMenu;
|
||||
if (menuDisplayed) {
|
||||
const buttonRect = button.current.getBoundingClientRect();
|
||||
const EmojiPicker = sdk.getComponent('emojipicker.EmojiPicker');
|
||||
contextMenu = <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu} managed={false}>
|
||||
<EmojiPicker onChoose={addEmoji} showQuickReactions={true} />
|
||||
</ContextMenu>;
|
||||
|
@ -318,14 +318,14 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
addEmoji(emoji: string) {
|
||||
private addEmoji(emoji: string) {
|
||||
dis.dispatch<ComposerInsertPayload>({
|
||||
action: Action.ComposerInsert,
|
||||
text: emoji,
|
||||
});
|
||||
}
|
||||
|
||||
sendMessage = async () => {
|
||||
private sendMessage = async () => {
|
||||
if (this.state.haveRecording && this.voiceRecordingButton) {
|
||||
// There shouldn't be any text message to send when a voice recording is active, so
|
||||
// just send out the voice recording.
|
||||
|
@ -333,11 +333,10 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
return;
|
||||
}
|
||||
|
||||
// XXX: Private function access
|
||||
this.messageComposerInput._sendMessage();
|
||||
this.messageComposerInput.sendMessage();
|
||||
};
|
||||
|
||||
onChange = (model) => {
|
||||
private onChange = (model: EditorModel) => {
|
||||
this.setState({
|
||||
isComposerEmpty: model.isEmpty,
|
||||
});
|
||||
|
@ -366,15 +365,12 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
];
|
||||
|
||||
if (!this.state.tombstone && this.state.canSendMessages) {
|
||||
const SendMessageComposer = sdk.getComponent("rooms.SendMessageComposer");
|
||||
|
||||
controls.push(
|
||||
<SendMessageComposer
|
||||
ref={(c) => this.messageComposerInput = c}
|
||||
key="controls_input"
|
||||
room={this.props.room}
|
||||
placeholder={this.renderPlaceholderText()}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
replyToEvent={this.props.replyToEvent}
|
||||
onChange={this.onChange}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,21 +14,35 @@ 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 React, { createRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.rooms.MessageComposerFormatBar")
|
||||
export default class MessageComposerFormatBar extends React.PureComponent {
|
||||
static propTypes = {
|
||||
onAction: PropTypes.func.isRequired,
|
||||
shortcuts: PropTypes.object.isRequired,
|
||||
};
|
||||
export enum Formatting {
|
||||
Bold = "bold",
|
||||
Italics = "italics",
|
||||
Strikethrough = "strikethrough",
|
||||
Code = "code",
|
||||
Quote = "quote",
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
interface IProps {
|
||||
shortcuts: Partial<Record<Formatting, string>>;
|
||||
onAction(action: Formatting): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.MessageComposerFormatBar")
|
||||
export default class MessageComposerFormatBar extends React.PureComponent<IProps, IState> {
|
||||
private readonly formatBarRef = createRef<HTMLDivElement>();
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = { visible: false };
|
||||
}
|
||||
|
@ -37,49 +51,53 @@ export default class MessageComposerFormatBar extends React.PureComponent {
|
|||
const classes = classNames("mx_MessageComposerFormatBar", {
|
||||
"mx_MessageComposerFormatBar_shown": this.state.visible,
|
||||
});
|
||||
return (<div className={classes} ref={ref => this._formatBarRef = ref}>
|
||||
<FormatButton label={_t("Bold")} onClick={() => this.props.onAction("bold")} icon="Bold" shortcut={this.props.shortcuts.bold} visible={this.state.visible} />
|
||||
<FormatButton label={_t("Italics")} onClick={() => this.props.onAction("italics")} icon="Italic" shortcut={this.props.shortcuts.italics} visible={this.state.visible} />
|
||||
<FormatButton label={_t("Strikethrough")} onClick={() => this.props.onAction("strikethrough")} icon="Strikethrough" visible={this.state.visible} />
|
||||
<FormatButton label={_t("Code block")} onClick={() => this.props.onAction("code")} icon="Code" visible={this.state.visible} />
|
||||
<FormatButton label={_t("Quote")} onClick={() => this.props.onAction("quote")} icon="Quote" shortcut={this.props.shortcuts.quote} visible={this.state.visible} />
|
||||
return (<div className={classes} ref={this.formatBarRef}>
|
||||
<FormatButton label={_t("Bold")} onClick={() => this.props.onAction(Formatting.Bold)} icon="Bold" shortcut={this.props.shortcuts.bold} visible={this.state.visible} />
|
||||
<FormatButton label={_t("Italics")} onClick={() => this.props.onAction(Formatting.Italics)} icon="Italic" shortcut={this.props.shortcuts.italics} visible={this.state.visible} />
|
||||
<FormatButton label={_t("Strikethrough")} onClick={() => this.props.onAction(Formatting.Strikethrough)} icon="Strikethrough" visible={this.state.visible} />
|
||||
<FormatButton label={_t("Code block")} onClick={() => this.props.onAction(Formatting.Code)} icon="Code" visible={this.state.visible} />
|
||||
<FormatButton label={_t("Quote")} onClick={() => this.props.onAction(Formatting.Quote)} icon="Quote" shortcut={this.props.shortcuts.quote} visible={this.state.visible} />
|
||||
</div>);
|
||||
}
|
||||
|
||||
showAt(selectionRect) {
|
||||
public showAt(selectionRect: DOMRect): void {
|
||||
if (!this.formatBarRef.current) return;
|
||||
|
||||
this.setState({ visible: true });
|
||||
const parentRect = this._formatBarRef.parentElement.getBoundingClientRect();
|
||||
this._formatBarRef.style.left = `${selectionRect.left - parentRect.left}px`;
|
||||
const parentRect = this.formatBarRef.current.parentElement.getBoundingClientRect();
|
||||
this.formatBarRef.current.style.left = `${selectionRect.left - parentRect.left}px`;
|
||||
// 12 is half the height of the bar (e.g. to center it) and 16 is an offset that felt ok.
|
||||
this._formatBarRef.style.top = `${selectionRect.top - parentRect.top - 16 - 12}px`;
|
||||
this.formatBarRef.current.style.top = `${selectionRect.top - parentRect.top - 16 - 12}px`;
|
||||
}
|
||||
|
||||
hide() {
|
||||
public hide(): void {
|
||||
this.setState({ visible: false });
|
||||
}
|
||||
}
|
||||
|
||||
class FormatButton extends React.PureComponent {
|
||||
static propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
icon: PropTypes.string.isRequired,
|
||||
shortcut: PropTypes.string,
|
||||
visible: PropTypes.bool,
|
||||
};
|
||||
interface IFormatButtonProps {
|
||||
label: string;
|
||||
icon: string;
|
||||
shortcut?: string;
|
||||
visible?: boolean;
|
||||
onClick(): void;
|
||||
}
|
||||
|
||||
class FormatButton extends React.PureComponent<IFormatButtonProps> {
|
||||
render() {
|
||||
const className = `mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIcon${this.props.icon}`;
|
||||
let shortcut;
|
||||
if (this.props.shortcut) {
|
||||
shortcut = <div className="mx_MessageComposerFormatBar_tooltipShortcut">{this.props.shortcut}</div>;
|
||||
shortcut = <div className="mx_MessageComposerFormatBar_tooltipShortcut">
|
||||
{ this.props.shortcut }
|
||||
</div>;
|
||||
}
|
||||
const tooltip = <div>
|
||||
<div className="mx_Tooltip_title">
|
||||
{this.props.label}
|
||||
{ this.props.label }
|
||||
</div>
|
||||
<div className="mx_Tooltip_sub">
|
||||
{shortcut}
|
||||
{ shortcut }
|
||||
</div>
|
||||
</div>;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -16,11 +16,9 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
|
||||
|
@ -31,54 +29,65 @@ import RoomTopic from "../elements/RoomTopic";
|
|||
import RoomName from "../elements/RoomName";
|
||||
import { PlaceCallType } from "../../../CallHandler";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { throttle } from 'lodash';
|
||||
import { MatrixEvent, Room, RoomState } from 'matrix-js-sdk/src';
|
||||
import { E2EStatus } from '../../../utils/ShieldUtils';
|
||||
import { IOOBData } from '../../../stores/ThreepidInviteStore';
|
||||
import { SearchScope } from './SearchBar';
|
||||
|
||||
export interface ISearchInfo {
|
||||
searchTerm: string;
|
||||
searchScope: SearchScope;
|
||||
searchCount: number;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
oobData?: IOOBData;
|
||||
inRoom: boolean;
|
||||
onSettingsClick: () => void;
|
||||
onSearchClick: () => void;
|
||||
onForgetClick: () => void;
|
||||
onCallPlaced: (type: PlaceCallType) => void;
|
||||
onAppsClick: () => void;
|
||||
e2eStatus: E2EStatus;
|
||||
appsShown: boolean;
|
||||
searchInfo: ISearchInfo;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.RoomHeader")
|
||||
export default class RoomHeader extends React.Component {
|
||||
static propTypes = {
|
||||
room: PropTypes.object,
|
||||
oobData: PropTypes.object,
|
||||
inRoom: PropTypes.bool,
|
||||
onSettingsClick: PropTypes.func,
|
||||
onSearchClick: PropTypes.func,
|
||||
onLeaveClick: PropTypes.func,
|
||||
e2eStatus: PropTypes.string,
|
||||
onAppsClick: PropTypes.func,
|
||||
appsShown: PropTypes.bool,
|
||||
onCallPlaced: PropTypes.func, // (PlaceCallType) => void;
|
||||
};
|
||||
|
||||
export default class RoomHeader extends React.Component<IProps> {
|
||||
static defaultProps = {
|
||||
editing: false,
|
||||
inRoom: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
public componentDidMount() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli.on("RoomState.events", this._onRoomStateEvents);
|
||||
cli.on("RoomState.events", this.onRoomStateEvents);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
public componentWillUnmount() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli) {
|
||||
cli.removeListener("RoomState.events", this._onRoomStateEvents);
|
||||
cli.removeListener("RoomState.events", this.onRoomStateEvents);
|
||||
}
|
||||
}
|
||||
|
||||
_onRoomStateEvents = (event, state) => {
|
||||
private onRoomStateEvents = (event: MatrixEvent, state: RoomState) => {
|
||||
if (!this.props.room || event.getRoomId() !== this.props.room.roomId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// redisplay the room name, topic, etc.
|
||||
this._rateLimitedUpdate();
|
||||
this.rateLimitedUpdate();
|
||||
};
|
||||
|
||||
_rateLimitedUpdate = new RateLimitedFunc(function() {
|
||||
/* eslint-disable @babel/no-invalid-this */
|
||||
private rateLimitedUpdate = throttle(() => {
|
||||
this.forceUpdate();
|
||||
}, 500);
|
||||
}, 500, { leading: true, trailing: true });
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
let searchStatus = null;
|
||||
|
||||
// don't display the search count until the search completes and
|
|
@ -22,7 +22,7 @@ import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
|||
import SpaceStore from "../../../stores/SpaceStore";
|
||||
|
||||
interface IProps {
|
||||
onVisibilityChange?: () => void
|
||||
onVisibilityChange?: () => void;
|
||||
}
|
||||
|
||||
const RoomListNumResults: React.FC<IProps> = ({ onVisibilityChange }) => {
|
||||
|
|
|
@ -18,23 +18,22 @@ limitations under the License.
|
|||
import React from "react";
|
||||
import { SearchResult } from "matrix-js-sdk/src/models/search-result";
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import { haveTileForEvent } from "./EventTile";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||
import DateSeparator from "../messages/DateSeparator";
|
||||
import EventTile from "./EventTile";
|
||||
import EventTile, { haveTileForEvent } from "./EventTile";
|
||||
|
||||
interface IProps {
|
||||
// The details of this result
|
||||
// a matrix-js-sdk SearchResult containing the details of this result
|
||||
searchResult: SearchResult;
|
||||
// Strings to be highlighted in the results
|
||||
// a list of strings to be highlighted in the results
|
||||
searchHighlights?: string[];
|
||||
// href for the highlights in this result
|
||||
resultLink?: string;
|
||||
onHeightChanged: () => void;
|
||||
permalinkCreator: RoomPermalinkCreator;
|
||||
onHeightChanged?: () => void;
|
||||
permalinkCreator?: RoomPermalinkCreator;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.SearchResultTile")
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,42 +13,53 @@ 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 React, { ClipboardEvent, createRef, KeyboardEvent } from 'react';
|
||||
import EMOJI_REGEX from 'emojibase-regex';
|
||||
import { IContent, MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { DebouncedFunc, throttle } from 'lodash';
|
||||
import { EventType, RelationType } from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import EditorModel from '../../../editor/model';
|
||||
import {
|
||||
htmlSerializeIfNeeded,
|
||||
textSerialize,
|
||||
containsEmote,
|
||||
stripEmoteCommand,
|
||||
unescapeMessage,
|
||||
htmlSerializeIfNeeded,
|
||||
startsWith,
|
||||
stripEmoteCommand,
|
||||
stripPrefix,
|
||||
textSerialize,
|
||||
unescapeMessage,
|
||||
} from '../../../editor/serialize';
|
||||
import { CommandPartCreator } from '../../../editor/parts';
|
||||
import { CommandPartCreator, Part, PartCreator, SerializedPart } from '../../../editor/parts';
|
||||
import BasicMessageComposer from "./BasicMessageComposer";
|
||||
import ReplyThread from "../elements/ReplyThread";
|
||||
import { findEditableEvent } from '../../../utils/EventUtils';
|
||||
import SendHistoryManager from "../../../SendHistoryManager";
|
||||
import { CommandCategories, getCommand } from '../../../SlashCommands';
|
||||
import * as sdk from '../../../index';
|
||||
import { Command, CommandCategories, getCommand } from '../../../SlashCommands';
|
||||
import Modal from '../../../Modal';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import ContentMessages from '../../../ContentMessages';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { containsEmoji } from "../../../effects/utils";
|
||||
import { CHAT_EFFECTS } from '../../../effects';
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import EMOJI_REGEX from 'emojibase-regex';
|
||||
import { getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindingsManager';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
|
||||
function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
|
||||
function addReplyToMessageContent(
|
||||
content: IContent,
|
||||
repliedToEvent: MatrixEvent,
|
||||
permalinkCreator: RoomPermalinkCreator,
|
||||
): void {
|
||||
const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent);
|
||||
Object.assign(content, replyContent);
|
||||
|
||||
|
@ -65,7 +75,11 @@ function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
|
|||
}
|
||||
|
||||
// exported for tests
|
||||
export function createMessageContent(model, permalinkCreator, replyToEvent) {
|
||||
export function createMessageContent(
|
||||
model: EditorModel,
|
||||
permalinkCreator: RoomPermalinkCreator,
|
||||
replyToEvent: MatrixEvent,
|
||||
): IContent {
|
||||
const isEmote = containsEmote(model);
|
||||
if (isEmote) {
|
||||
model = stripEmoteCommand(model);
|
||||
|
@ -76,7 +90,7 @@ export function createMessageContent(model, permalinkCreator, replyToEvent) {
|
|||
model = unescapeMessage(model);
|
||||
|
||||
const body = textSerialize(model);
|
||||
const content = {
|
||||
const content: IContent = {
|
||||
msgtype: isEmote ? "m.emote" : "m.text",
|
||||
body: body,
|
||||
};
|
||||
|
@ -94,7 +108,7 @@ export function createMessageContent(model, permalinkCreator, replyToEvent) {
|
|||
}
|
||||
|
||||
// exported for tests
|
||||
export function isQuickReaction(model) {
|
||||
export function isQuickReaction(model: EditorModel): boolean {
|
||||
const parts = model.parts;
|
||||
if (parts.length == 0) return false;
|
||||
const text = textSerialize(model);
|
||||
|
@ -111,46 +125,48 @@ export function isQuickReaction(model) {
|
|||
return false;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
placeholder?: string;
|
||||
permalinkCreator: RoomPermalinkCreator;
|
||||
replyToEvent?: MatrixEvent;
|
||||
disabled?: boolean;
|
||||
onChange?(model: EditorModel): void;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.SendMessageComposer")
|
||||
export default class SendMessageComposer extends React.Component {
|
||||
static propTypes = {
|
||||
room: PropTypes.object.isRequired,
|
||||
placeholder: PropTypes.string,
|
||||
permalinkCreator: PropTypes.object.isRequired,
|
||||
replyToEvent: PropTypes.object,
|
||||
onChange: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default class SendMessageComposer extends React.Component<IProps> {
|
||||
static contextType = MatrixClientContext;
|
||||
context!: React.ContextType<typeof MatrixClientContext>;
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.model = null;
|
||||
this._editorRef = null;
|
||||
this.currentlyComposedEditorState = null;
|
||||
private readonly prepareToEncrypt?: DebouncedFunc<() => void>;
|
||||
private readonly editorRef = createRef<BasicMessageComposer>();
|
||||
private model: EditorModel = null;
|
||||
private currentlyComposedEditorState: SerializedPart[] = null;
|
||||
private dispatcherRef: string;
|
||||
private sendHistoryManager: SendHistoryManager;
|
||||
|
||||
constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
|
||||
super(props);
|
||||
this.context = context; // otherwise React will only set it prior to render due to type def above
|
||||
if (this.context.isCryptoEnabled() && this.context.isRoomEncrypted(this.props.room.roomId)) {
|
||||
this._prepareToEncrypt = new RateLimitedFunc(() => {
|
||||
this.prepareToEncrypt = throttle(() => {
|
||||
this.context.prepareToEncrypt(this.props.room);
|
||||
}, 60000);
|
||||
}, 60000, { leading: true, trailing: false });
|
||||
}
|
||||
|
||||
window.addEventListener("beforeunload", this._saveStoredEditorState);
|
||||
window.addEventListener("beforeunload", this.saveStoredEditorState);
|
||||
}
|
||||
|
||||
_setEditorRef = ref => {
|
||||
this._editorRef = ref;
|
||||
};
|
||||
|
||||
_onKeyDown = (event) => {
|
||||
private onKeyDown = (event: KeyboardEvent): void => {
|
||||
// ignore any keypress while doing IME compositions
|
||||
if (this._editorRef.isComposing(event)) {
|
||||
if (this.editorRef.current?.isComposing(event)) {
|
||||
return;
|
||||
}
|
||||
const action = getKeyBindingsManager().getMessageComposerAction(event);
|
||||
switch (action) {
|
||||
case MessageComposerAction.Send:
|
||||
this._sendMessage();
|
||||
this.sendMessage();
|
||||
event.preventDefault();
|
||||
break;
|
||||
case MessageComposerAction.SelectPrevSendHistory:
|
||||
|
@ -165,7 +181,7 @@ export default class SendMessageComposer extends React.Component {
|
|||
}
|
||||
case MessageComposerAction.EditPrevMessage:
|
||||
// selection must be collapsed and caret at start
|
||||
if (this._editorRef.isSelectionCollapsed() && this._editorRef.isCaretAtStart()) {
|
||||
if (this.editorRef.current?.isSelectionCollapsed() && this.editorRef.current?.isCaretAtStart()) {
|
||||
const editEvent = findEditableEvent(this.props.room, false);
|
||||
if (editEvent) {
|
||||
// We're selecting history, so prevent the key event from doing anything else
|
||||
|
@ -184,29 +200,29 @@ export default class SendMessageComposer extends React.Component {
|
|||
});
|
||||
break;
|
||||
default:
|
||||
if (this._prepareToEncrypt) {
|
||||
if (this.prepareToEncrypt) {
|
||||
// This needs to be last!
|
||||
this._prepareToEncrypt();
|
||||
this.prepareToEncrypt();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// we keep sent messages/commands in a separate history (separate from undo history)
|
||||
// so you can alt+up/down in them
|
||||
selectSendHistory(up) {
|
||||
private selectSendHistory(up: boolean): boolean {
|
||||
const delta = up ? -1 : 1;
|
||||
// True if we are not currently selecting history, but composing a message
|
||||
if (this.sendHistoryManager.currentIndex === this.sendHistoryManager.history.length) {
|
||||
// We can't go any further - there isn't any more history, so nop.
|
||||
if (!up) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
this.currentlyComposedEditorState = this.model.serializeParts();
|
||||
} else if (this.sendHistoryManager.currentIndex + delta === this.sendHistoryManager.history.length) {
|
||||
// True when we return to the message being composed currently
|
||||
this.model.reset(this.currentlyComposedEditorState);
|
||||
this.sendHistoryManager.currentIndex = this.sendHistoryManager.history.length;
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
const { parts, replyEventId } = this.sendHistoryManager.getItem(delta);
|
||||
dis.dispatch({
|
||||
|
@ -215,11 +231,12 @@ export default class SendMessageComposer extends React.Component {
|
|||
});
|
||||
if (parts) {
|
||||
this.model.reset(parts);
|
||||
this._editorRef.focus();
|
||||
this.editorRef.current?.focus();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
_isSlashCommand() {
|
||||
private isSlashCommand(): boolean {
|
||||
const parts = this.model.parts;
|
||||
const firstPart = parts[0];
|
||||
if (firstPart) {
|
||||
|
@ -237,17 +254,17 @@ export default class SendMessageComposer extends React.Component {
|
|||
return false;
|
||||
}
|
||||
|
||||
_sendQuickReaction() {
|
||||
private sendQuickReaction(): void {
|
||||
const timeline = this.props.room.getLiveTimeline();
|
||||
const events = timeline.getEvents();
|
||||
const reaction = this.model.parts[1].text;
|
||||
for (let i = events.length - 1; i >= 0; i--) {
|
||||
if (events[i].getType() === "m.room.message") {
|
||||
if (events[i].getType() === EventType.RoomMessage) {
|
||||
let shouldReact = true;
|
||||
const lastMessage = events[i];
|
||||
const userId = MatrixClientPeg.get().getUserId();
|
||||
const messageReactions = this.props.room.getUnfilteredTimelineSet()
|
||||
.getRelationsForEvent(lastMessage.getId(), "m.annotation", "m.reaction");
|
||||
.getRelationsForEvent(lastMessage.getId(), RelationType.Annotation, EventType.Reaction);
|
||||
|
||||
// if we have already sent this reaction, don't redact but don't re-send
|
||||
if (messageReactions) {
|
||||
|
@ -258,9 +275,9 @@ export default class SendMessageComposer extends React.Component {
|
|||
shouldReact = !myReactionKeys.includes(reaction);
|
||||
}
|
||||
if (shouldReact) {
|
||||
MatrixClientPeg.get().sendEvent(lastMessage.getRoomId(), "m.reaction", {
|
||||
MatrixClientPeg.get().sendEvent(lastMessage.getRoomId(), EventType.Reaction, {
|
||||
"m.relates_to": {
|
||||
"rel_type": "m.annotation",
|
||||
"rel_type": RelationType.Annotation,
|
||||
"event_id": lastMessage.getId(),
|
||||
"key": reaction,
|
||||
},
|
||||
|
@ -272,7 +289,7 @@ export default class SendMessageComposer extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_getSlashCommand() {
|
||||
private getSlashCommand(): [Command, string, string] {
|
||||
const commandText = this.model.parts.reduce((text, part) => {
|
||||
// use mxid to textify user pills in a command
|
||||
if (part.type === "user-pill") {
|
||||
|
@ -284,7 +301,7 @@ export default class SendMessageComposer extends React.Component {
|
|||
return [cmd, args, commandText];
|
||||
}
|
||||
|
||||
async _runSlashCommand(cmd, args) {
|
||||
private async runSlashCommand(cmd: Command, args: string): Promise<void> {
|
||||
const result = cmd.run(this.props.room.roomId, args);
|
||||
let messageContent;
|
||||
let error = result.error;
|
||||
|
@ -302,7 +319,6 @@ export default class SendMessageComposer extends React.Component {
|
|||
}
|
||||
if (error) {
|
||||
console.error("Command failure: %s", error);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
// assume the error is a server error when the command is async
|
||||
const isServerError = !!result.promise;
|
||||
const title = isServerError ? _td("Server error") : _td("Command error");
|
||||
|
@ -326,7 +342,7 @@ export default class SendMessageComposer extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
async _sendMessage() {
|
||||
public async sendMessage(): Promise<void> {
|
||||
if (this.model.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
@ -335,21 +351,20 @@ export default class SendMessageComposer extends React.Component {
|
|||
let shouldSend = true;
|
||||
let content;
|
||||
|
||||
if (!containsEmote(this.model) && this._isSlashCommand()) {
|
||||
const [cmd, args, commandText] = this._getSlashCommand();
|
||||
if (!containsEmote(this.model) && this.isSlashCommand()) {
|
||||
const [cmd, args, commandText] = this.getSlashCommand();
|
||||
if (cmd) {
|
||||
if (cmd.category === CommandCategories.messages) {
|
||||
content = await this._runSlashCommand(cmd, args);
|
||||
content = await this.runSlashCommand(cmd, args);
|
||||
if (replyToEvent) {
|
||||
addReplyToMessageContent(content, replyToEvent, this.props.permalinkCreator);
|
||||
}
|
||||
} else {
|
||||
this._runSlashCommand(cmd, args);
|
||||
this.runSlashCommand(cmd, args);
|
||||
shouldSend = false;
|
||||
}
|
||||
} else {
|
||||
// ask the user if their unknown command should be sent as a message
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const { finished } = Modal.createTrackedDialog("Unknown command", "", QuestionDialog, {
|
||||
title: _t("Unknown Command"),
|
||||
description: <div>
|
||||
|
@ -378,7 +393,7 @@ export default class SendMessageComposer extends React.Component {
|
|||
|
||||
if (isQuickReaction(this.model)) {
|
||||
shouldSend = false;
|
||||
this._sendQuickReaction();
|
||||
this.sendQuickReaction();
|
||||
}
|
||||
|
||||
if (shouldSend) {
|
||||
|
@ -411,9 +426,9 @@ export default class SendMessageComposer extends React.Component {
|
|||
this.sendHistoryManager.save(this.model, replyToEvent);
|
||||
// clear composer
|
||||
this.model.reset([]);
|
||||
this._editorRef.clearUndoHistory();
|
||||
this._editorRef.focus();
|
||||
this._clearStoredEditorState();
|
||||
this.editorRef.current?.clearUndoHistory();
|
||||
this.editorRef.current?.focus();
|
||||
this.clearStoredEditorState();
|
||||
if (SettingsStore.getValue("scrollToBottomOnMessageSent")) {
|
||||
dis.dispatch({ action: "scroll_to_bottom" });
|
||||
}
|
||||
|
@ -421,33 +436,33 @@ export default class SendMessageComposer extends React.Component {
|
|||
|
||||
componentWillUnmount() {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
window.removeEventListener("beforeunload", this._saveStoredEditorState);
|
||||
this._saveStoredEditorState();
|
||||
window.removeEventListener("beforeunload", this.saveStoredEditorState);
|
||||
this.saveStoredEditorState();
|
||||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Move this to constructor
|
||||
UNSAFE_componentWillMount() { // eslint-disable-line camelcase
|
||||
const partCreator = new CommandPartCreator(this.props.room, this.context);
|
||||
const parts = this._restoreStoredEditorState(partCreator) || [];
|
||||
const parts = this.restoreStoredEditorState(partCreator) || [];
|
||||
this.model = new EditorModel(parts, partCreator);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
this.sendHistoryManager = new SendHistoryManager(this.props.room.roomId, 'mx_cider_history_');
|
||||
}
|
||||
|
||||
get _editorStateKey() {
|
||||
private get editorStateKey() {
|
||||
return `mx_cider_state_${this.props.room.roomId}`;
|
||||
}
|
||||
|
||||
_clearStoredEditorState() {
|
||||
localStorage.removeItem(this._editorStateKey);
|
||||
private clearStoredEditorState(): void {
|
||||
localStorage.removeItem(this.editorStateKey);
|
||||
}
|
||||
|
||||
_restoreStoredEditorState(partCreator) {
|
||||
const json = localStorage.getItem(this._editorStateKey);
|
||||
private restoreStoredEditorState(partCreator: PartCreator): Part[] {
|
||||
const json = localStorage.getItem(this.editorStateKey);
|
||||
if (json) {
|
||||
try {
|
||||
const { parts: serializedParts, replyEventId } = JSON.parse(json);
|
||||
const parts = serializedParts.map(p => partCreator.deserializePart(p));
|
||||
const parts: Part[] = serializedParts.map(p => partCreator.deserializePart(p));
|
||||
if (replyEventId) {
|
||||
dis.dispatch({
|
||||
action: 'reply_to_event',
|
||||
|
@ -462,42 +477,42 @@ export default class SendMessageComposer extends React.Component {
|
|||
}
|
||||
|
||||
// should save state when editor has contents or reply is open
|
||||
_shouldSaveStoredEditorState = () => {
|
||||
return !this.model.isEmpty || this.props.replyToEvent;
|
||||
}
|
||||
private shouldSaveStoredEditorState = (): boolean => {
|
||||
return !this.model.isEmpty || !!this.props.replyToEvent;
|
||||
};
|
||||
|
||||
_saveStoredEditorState = () => {
|
||||
if (this._shouldSaveStoredEditorState()) {
|
||||
private saveStoredEditorState = (): void => {
|
||||
if (this.shouldSaveStoredEditorState()) {
|
||||
const item = SendHistoryManager.createItem(this.model, this.props.replyToEvent);
|
||||
localStorage.setItem(this._editorStateKey, JSON.stringify(item));
|
||||
localStorage.setItem(this.editorStateKey, JSON.stringify(item));
|
||||
} else {
|
||||
this._clearStoredEditorState();
|
||||
this.clearStoredEditorState();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onAction = (payload) => {
|
||||
private onAction = (payload: ActionPayload): void => {
|
||||
// don't let the user into the composer if it is disabled - all of these branches lead
|
||||
// to the cursor being in the composer
|
||||
if (this.props.disabled) return;
|
||||
|
||||
switch (payload.action) {
|
||||
case 'reply_to_event':
|
||||
case Action.FocusComposer:
|
||||
this._editorRef && this._editorRef.focus();
|
||||
case Action.FocusSendMessageComposer:
|
||||
this.editorRef.current?.focus();
|
||||
break;
|
||||
case "send_composer_insert":
|
||||
if (payload.userId) {
|
||||
this._editorRef && this._editorRef.insertMention(payload.userId);
|
||||
this.editorRef.current?.insertMention(payload.userId);
|
||||
} else if (payload.event) {
|
||||
this._editorRef && this._editorRef.insertQuotedMessage(payload.event);
|
||||
this.editorRef.current?.insertQuotedMessage(payload.event);
|
||||
} else if (payload.text) {
|
||||
this._editorRef && this._editorRef.insertPlaintext(payload.text);
|
||||
this.editorRef.current?.insertPlaintext(payload.text);
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
_onPaste = (event) => {
|
||||
private onPaste = (event: ClipboardEvent<HTMLDivElement>): boolean => {
|
||||
const { clipboardData } = event;
|
||||
// Prioritize text on the clipboard over files as Office on macOS puts a bitmap
|
||||
// in the clipboard as well as the content being copied.
|
||||
|
@ -511,23 +526,27 @@ export default class SendMessageComposer extends React.Component {
|
|||
);
|
||||
return true; // to skip internal onPaste handler
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onChange = () => {
|
||||
private onChange = (): void => {
|
||||
if (this.props.onChange) this.props.onChange(this.model);
|
||||
}
|
||||
};
|
||||
|
||||
private focusComposer = (): void => {
|
||||
this.editorRef.current?.focus();
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="mx_SendMessageComposer" onClick={this.focusComposer} onKeyDown={this._onKeyDown}>
|
||||
<div className="mx_SendMessageComposer" onClick={this.focusComposer} onKeyDown={this.onKeyDown}>
|
||||
<BasicMessageComposer
|
||||
onChange={this.onChange}
|
||||
ref={this._setEditorRef}
|
||||
ref={this.editorRef}
|
||||
model={this.model}
|
||||
room={this.props.room}
|
||||
label={this.props.placeholder}
|
||||
placeholder={this.props.placeholder}
|
||||
onPaste={this._onPaste}
|
||||
onPaste={this.onPaste}
|
||||
disabled={this.props.disabled}
|
||||
/>
|
||||
</div>
|
|
@ -27,7 +27,7 @@ export default class SimpleRoomHeader extends React.Component {
|
|||
static propTypes = {
|
||||
title: PropTypes.string,
|
||||
|
||||
// `src` to a TintableSvg. Optional.
|
||||
// `src` to an image. Optional.
|
||||
icon: PropTypes.string,
|
||||
};
|
||||
|
||||
|
|
|
@ -20,13 +20,14 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import * as sdk from "../../../index";
|
||||
import Modal from "../../../Modal";
|
||||
import { isValid3pidInvite } from "../../../RoomInvite";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import RoomName from "../elements/RoomName";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import ErrorDialog from '../dialogs/ErrorDialog';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
interface IProps {
|
||||
event: MatrixEvent;
|
||||
|
@ -104,7 +105,6 @@ export default class ThirdPartyMemberInfo extends React.Component<IProps, IState
|
|||
// Revert echo because of error
|
||||
this.setState({ invited: true });
|
||||
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Revoke 3pid invite failed', '', ErrorDialog, {
|
||||
title: _t("Failed to revoke invite"),
|
||||
description: _t(
|
||||
|
@ -119,8 +119,6 @@ export default class ThirdPartyMemberInfo extends React.Component<IProps, IState
|
|||
};
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
||||
let adminTools = null;
|
||||
if (this.state.canKick && this.state.invited) {
|
||||
adminTools = (
|
||||
|
|
|
@ -18,26 +18,22 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
|||
import { _t } from "../../../languageHandler";
|
||||
import React, { ReactNode } from "react";
|
||||
import {
|
||||
IRecordingUpdate,
|
||||
RECORDING_PLAYBACK_SAMPLES,
|
||||
RecordingState,
|
||||
VoiceRecording,
|
||||
} from "../../../voice/VoiceRecording";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import classNames from "classnames";
|
||||
import LiveRecordingWaveform from "../voice_messages/LiveRecordingWaveform";
|
||||
import LiveRecordingWaveform from "../audio_messages/LiveRecordingWaveform";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { arrayFastResample, arraySeed } from "../../../utils/arrays";
|
||||
import { percentageOf } from "../../../utils/numbers";
|
||||
import LiveRecordingClock from "../voice_messages/LiveRecordingClock";
|
||||
import LiveRecordingClock from "../audio_messages/LiveRecordingClock";
|
||||
import { VoiceRecordingStore } from "../../../stores/VoiceRecordingStore";
|
||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||
import RecordingPlayback from "../voice_messages/RecordingPlayback";
|
||||
import RecordingPlayback from "../audio_messages/RecordingPlayback";
|
||||
import { MsgType } from "matrix-js-sdk/src/@types/event";
|
||||
import Modal from "../../../Modal";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import MediaDeviceHandler from "../../../MediaDeviceHandler";
|
||||
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../MediaDeviceHandler";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -46,8 +42,6 @@ interface IProps {
|
|||
interface IState {
|
||||
recorder?: VoiceRecording;
|
||||
recordingPhase?: RecordingState;
|
||||
relHeights: number[];
|
||||
seconds: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -55,58 +49,18 @@ interface IState {
|
|||
*/
|
||||
@replaceableComponent("views.rooms.VoiceRecordComposerTile")
|
||||
export default class VoiceRecordComposerTile extends React.PureComponent<IProps, IState> {
|
||||
private waveform: number[] = [];
|
||||
private seconds = 0;
|
||||
private scheduledAnimationFrame = false;
|
||||
|
||||
public constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
recorder: null, // no recording started by default
|
||||
seconds: 0,
|
||||
relHeights: arraySeed(0, RECORDING_PLAYBACK_SAMPLES),
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps, prevState) {
|
||||
if (!prevState.recorder && this.state.recorder) {
|
||||
this.state.recorder.liveData.onUpdate(this.onRecordingUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
public async componentWillUnmount() {
|
||||
await VoiceRecordingStore.instance.disposeRecording();
|
||||
}
|
||||
|
||||
private onRecordingUpdate = (update: IRecordingUpdate): void => {
|
||||
this.waveform = update.waveform;
|
||||
this.seconds = update.timeSeconds;
|
||||
|
||||
if (this.scheduledAnimationFrame) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.scheduledAnimationFrame = true;
|
||||
// The audio recorder flushes data faster than the screen refresh rate
|
||||
// Using requestAnimationFrame makes sure that we only flush the data
|
||||
// to react once per tick to avoid unneeded work.
|
||||
requestAnimationFrame(() => {
|
||||
// The waveform and the downsample target are pretty close, so we should be fine to
|
||||
// do this, despite the docs on arrayFastResample.
|
||||
const bars = arrayFastResample(Array.from(this.waveform), RECORDING_PLAYBACK_SAMPLES);
|
||||
this.setState({
|
||||
// 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.
|
||||
relHeights: bars.map(b => percentageOf(b, 0, 0.50)),
|
||||
seconds: this.seconds,
|
||||
});
|
||||
this.scheduledAnimationFrame = false;
|
||||
});
|
||||
};
|
||||
|
||||
// called by composer
|
||||
public async send() {
|
||||
if (!this.state.recorder) {
|
||||
|
@ -181,7 +135,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
// change between this and recording, but at least we will have tried.
|
||||
try {
|
||||
const devices = await MediaDeviceHandler.getDevices();
|
||||
if (!devices?.['audioInput']?.length) {
|
||||
if (!devices?.[MediaDeviceKindEnum.AudioInput]?.length) {
|
||||
Modal.createTrackedDialog('No Microphone Error', '', ErrorDialog, {
|
||||
title: _t("No microphone found"),
|
||||
description: <>
|
||||
|
@ -228,7 +182,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
}
|
||||
|
||||
// only other UI is the recording-in-progress UI
|
||||
return <div className="mx_VoiceMessagePrimaryContainer mx_VoiceRecordComposerTile_recording">
|
||||
return <div className="mx_MediaBody mx_VoiceMessagePrimaryContainer mx_VoiceRecordComposerTile_recording">
|
||||
<LiveRecordingClock recorder={this.state.recorder} />
|
||||
<LiveRecordingWaveform recorder={this.state.recorder} />
|
||||
</div>;
|
||||
|
|
|
@ -16,15 +16,14 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import SettingsFlag from '../elements/SettingsFlag';
|
||||
|
||||
const SETTING_MANUALLY_VERIFY_ALL_SESSIONS = "e2ee.manuallyVerifyAllSessions";
|
||||
|
||||
const E2eAdvancedPanel = props => {
|
||||
const SettingsFlag = sdk.getComponent('views.elements.SettingsFlag');
|
||||
return <div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Encryption")}</span>
|
||||
|
||||
|
|
|
@ -18,7 +18,6 @@ import React from 'react';
|
|||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import * as sdk from '../../../index';
|
||||
import Modal from '../../../Modal';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
|
@ -27,6 +26,7 @@ import EventIndexPeg from "../../../indexing/EventIndexPeg";
|
|||
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import SeshatResetDialog from '../dialogs/SeshatResetDialog';
|
||||
import InlineSpinner from '../elements/InlineSpinner';
|
||||
|
||||
interface IState {
|
||||
enabling: boolean;
|
||||
|
@ -147,7 +147,6 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
|
|||
|
||||
render() {
|
||||
let eventIndexingSettings = null;
|
||||
const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
|
||||
const brand = SdkConfig.get().brand;
|
||||
|
||||
if (EventIndexPeg.get() !== null) {
|
||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
import url from 'url';
|
||||
import React from 'react';
|
||||
import { _t } from "../../../languageHandler";
|
||||
import * as sdk from '../../../index';
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import Modal from '../../../Modal';
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
|
@ -28,6 +27,10 @@ import { getDefaultIdentityServerUrl, doesIdentityServerHaveTerms } from '../../
|
|||
import { timeout } from "../../../utils/promise";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { ActionPayload } from '../../../dispatcher/payloads';
|
||||
import InlineSpinner from '../elements/InlineSpinner';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import Field from '../elements/Field';
|
||||
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||
|
||||
// We'll wait up to this long when checking for 3PID bindings on the IS.
|
||||
const REACHABILITY_TIMEOUT = 10000; // ms
|
||||
|
@ -126,7 +129,6 @@ export default class SetIdServer extends React.Component<IProps, IState> {
|
|||
|
||||
private getTooltip = () => {
|
||||
if (this.state.checking) {
|
||||
const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner');
|
||||
return <div>
|
||||
<InlineSpinner />
|
||||
{ _t("Checking server") }
|
||||
|
@ -217,7 +219,6 @@ export default class SetIdServer extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private showNoTermsWarning(fullUrl) {
|
||||
const QuestionDialog = sdk.getComponent("views.dialogs.QuestionDialog");
|
||||
const { finished } = Modal.createTrackedDialog('No Terms Warning', '', QuestionDialog, {
|
||||
title: _t("Identity server has no terms of service"),
|
||||
description: (
|
||||
|
@ -319,7 +320,6 @@ export default class SetIdServer extends React.Component<IProps, IState> {
|
|||
message = unboundMessage;
|
||||
}
|
||||
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const { finished } = Modal.createTrackedDialog('Identity Server Bound Warning', '', QuestionDialog, {
|
||||
title,
|
||||
description: message,
|
||||
|
@ -352,8 +352,6 @@ export default class SetIdServer extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
const idServerUrl = this.state.currentClientIdServer;
|
||||
let sectionTitle;
|
||||
let bodyText;
|
||||
|
@ -398,7 +396,6 @@ export default class SetIdServer extends React.Component<IProps, IState> {
|
|||
discoButtonContent = _t("Do not use an identity server");
|
||||
}
|
||||
if (this.state.disconnectBusy) {
|
||||
const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner');
|
||||
discoButtonContent = <InlineSpinner />;
|
||||
}
|
||||
discoSection = <div>
|
||||
|
|
|
@ -17,15 +17,25 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
|
||||
import { IntegrationManagerInstance } from "../../../integrations/IntegrationManagerInstance";
|
||||
import * as sdk from '../../../index';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
|
||||
}
|
||||
|
||||
interface IState {
|
||||
currentManager: IntegrationManagerInstance;
|
||||
provisioningEnabled: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.settings.SetIntegrationManager")
|
||||
export default class SetIntegrationManager extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
export default class SetIntegrationManager extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
const currentManager = IntegrationManagers.sharedInstance().getPrimaryManager();
|
||||
|
||||
|
@ -35,7 +45,7 @@ export default class SetIntegrationManager extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
onProvisioningToggled = () => {
|
||||
private onProvisioningToggled = (): void => {
|
||||
const current = this.state.provisioningEnabled;
|
||||
SettingsStore.setValue("integrationProvisioning", null, SettingLevel.ACCOUNT, !current).catch(err => {
|
||||
console.error("Error changing integration manager provisioning");
|
||||
|
@ -46,7 +56,7 @@ export default class SetIntegrationManager extends React.Component {
|
|||
this.setState({ provisioningEnabled: !current });
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): React.ReactNode {
|
||||
const ToggleSwitch = sdk.getComponent("views.elements.ToggleSwitch");
|
||||
|
||||
const currentManager = this.state.currentManager;
|
|
@ -21,17 +21,17 @@ import { _t } from "../../../languageHandler";
|
|||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface ExistingSpellCheckLanguageIProps {
|
||||
language: string,
|
||||
onRemoved(language: string),
|
||||
language: string;
|
||||
onRemoved(language: string);
|
||||
}
|
||||
|
||||
interface SpellCheckLanguagesIProps {
|
||||
languages: Array<string>,
|
||||
onLanguagesChange(languages: Array<string>),
|
||||
languages: Array<string>;
|
||||
onLanguagesChange(languages: Array<string>);
|
||||
}
|
||||
|
||||
interface SpellCheckLanguagesIState {
|
||||
newLanguage: string,
|
||||
newLanguage: string;
|
||||
}
|
||||
|
||||
export class ExistingSpellCheckLanguage extends React.Component<ExistingSpellCheckLanguageIProps> {
|
||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import { _t, _td } from "../../../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
||||
import * as sdk from "../../../../..";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import Modal from "../../../../../Modal";
|
||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||
|
@ -26,6 +25,8 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
|||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { RoomState } from "matrix-js-sdk/src/models/room-state";
|
||||
import { compare } from "../../../../../utils/strings";
|
||||
import ErrorDialog from '../../../dialogs/ErrorDialog';
|
||||
import PowerSelector from "../../../elements/PowerSelector";
|
||||
|
||||
const plEventsToLabels = {
|
||||
// These will be translated for us later.
|
||||
|
@ -76,7 +77,6 @@ interface IBannedUserProps {
|
|||
export class BannedUser extends React.Component<IBannedUserProps> {
|
||||
private onUnbanClick = (e) => {
|
||||
MatrixClientPeg.get().unban(this.props.member.roomId, this.props.member.userId).catch((err) => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to unban: " + err);
|
||||
Modal.createTrackedDialog('Failed to unban', '', ErrorDialog, {
|
||||
title: _t('Error'),
|
||||
|
@ -176,7 +176,6 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
|||
client.sendStateEvent(this.props.roomId, "m.room.power_levels", plContent).catch(e => {
|
||||
console.error(e);
|
||||
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Power level requirement change failed', '', ErrorDialog, {
|
||||
title: _t('Error changing power level requirement'),
|
||||
description: _t(
|
||||
|
@ -203,7 +202,6 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
|||
client.sendStateEvent(this.props.roomId, "m.room.power_levels", plContent).catch(e => {
|
||||
console.error(e);
|
||||
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Power level change failed', '', ErrorDialog, {
|
||||
title: _t('Error changing power level'),
|
||||
description: _t(
|
||||
|
@ -215,8 +213,6 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
|||
};
|
||||
|
||||
render() {
|
||||
const PowerSelector = sdk.getComponent('elements.PowerSelector');
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(this.props.roomId);
|
||||
const plEvent = room.currentState.getStateEvents('m.room.power_levels', '');
|
||||
|
|
|
@ -18,7 +18,6 @@ import React from 'react';
|
|||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { _t } from "../../../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
||||
import * as sdk from "../../../../..";
|
||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
||||
import Modal from "../../../../../Modal";
|
||||
import QuestionDialog from "../../../dialogs/QuestionDialog";
|
||||
|
@ -27,6 +26,7 @@ import { SettingLevel } from "../../../../../settings/SettingLevel";
|
|||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import { UIFeature } from "../../../../../settings/UIFeature";
|
||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||
import SettingsFlag from '../../../elements/SettingsFlag';
|
||||
|
||||
// Knock and private are reserved keywords which are not yet implemented.
|
||||
export enum JoinRule {
|
||||
|
@ -385,8 +385,6 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
}
|
||||
|
||||
render() {
|
||||
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(this.props.roomId);
|
||||
const isEncrypted = this.state.encrypted;
|
||||
|
|
|
@ -22,7 +22,6 @@ import AccessibleTooltipButton from '../../../elements/AccessibleTooltipButton';
|
|||
import SdkConfig from "../../../../../SdkConfig";
|
||||
import createRoom from "../../../../../createRoom";
|
||||
import Modal from "../../../../../Modal";
|
||||
import * as sdk from "../../../../..";
|
||||
import PlatformPeg from "../../../../../PlatformPeg";
|
||||
import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts";
|
||||
import UpdateCheckButton from "../../UpdateCheckButton";
|
||||
|
@ -30,6 +29,8 @@ import { replaceableComponent } from "../../../../../utils/replaceableComponent"
|
|||
import { copyPlaintext } from "../../../../../utils/strings";
|
||||
import * as ContextMenu from "../../../../structures/ContextMenu";
|
||||
import { toRightOf } from "../../../../structures/ContextMenu";
|
||||
import BugReportDialog from '../../../dialogs/BugReportDialog';
|
||||
import GenericTextContextMenu from "../../../context_menus/GenericTextContextMenu";
|
||||
|
||||
interface IProps {
|
||||
closeSettingsFn: () => void;
|
||||
|
@ -81,10 +82,6 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
|||
};
|
||||
|
||||
private onBugReport = (e) => {
|
||||
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
|
||||
if (!BugReportDialog) {
|
||||
return;
|
||||
}
|
||||
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {});
|
||||
};
|
||||
|
||||
|
@ -171,7 +168,6 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
|||
|
||||
const successful = await copyPlaintext(MatrixClientPeg.get().getAccessToken());
|
||||
const buttonRect = target.getBoundingClientRect();
|
||||
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
|
||||
const { close } = ContextMenu.createMenu(GenericTextContextMenu, {
|
||||
...toRightOf(buttonRect, 2),
|
||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||
|
|
|
@ -22,8 +22,11 @@ import { ListRule } from "../../../../../mjolnir/ListRule";
|
|||
import { BanList, RULE_SERVER, RULE_USER } from "../../../../../mjolnir/BanList";
|
||||
import Modal from "../../../../../Modal";
|
||||
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
||||
import * as sdk from "../../../../../index";
|
||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||
import ErrorDialog from "../../../dialogs/ErrorDialog";
|
||||
import QuestionDialog from "../../../dialogs/QuestionDialog";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import Field from "../../../elements/Field";
|
||||
|
||||
interface IState {
|
||||
busy: boolean;
|
||||
|
@ -68,7 +71,6 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
|
|||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to add Mjolnir rule', '', ErrorDialog, {
|
||||
title: _t('Error adding ignored user/server'),
|
||||
description: _t('Something went wrong. Please try again or view your console for hints.'),
|
||||
|
@ -90,7 +92,6 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
|
|||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to subscribe to Mjolnir list', '', ErrorDialog, {
|
||||
title: _t('Error subscribing to list'),
|
||||
description: _t('Please verify the room ID or address and try again.'),
|
||||
|
@ -108,7 +109,6 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
|
|||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to remove Mjolnir rule', '', ErrorDialog, {
|
||||
title: _t('Error removing ignored user/server'),
|
||||
description: _t('Something went wrong. Please try again or view your console for hints.'),
|
||||
|
@ -126,7 +126,6 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
|
|||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to unsubscribe from Mjolnir list', '', ErrorDialog, {
|
||||
title: _t('Error unsubscribing from list'),
|
||||
description: _t('Please try again or view your console for hints.'),
|
||||
|
@ -137,8 +136,6 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
|
|||
}
|
||||
|
||||
private viewListRules(list: BanList) {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(list.roomId);
|
||||
const name = room ? room.name : list.roomId;
|
||||
|
||||
|
@ -168,8 +165,6 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
|
|||
}
|
||||
|
||||
private renderPersonalBanListRules() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
const list = Mjolnir.sharedInstance().getPersonalList();
|
||||
const rules = list ? [...list.userRules, ...list.serverRules] : [];
|
||||
if (!list || rules.length <= 0) return <i>{_t("You have not ignored anyone.")}</i>;
|
||||
|
@ -199,8 +194,6 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
|
|||
}
|
||||
|
||||
private renderSubscribedBanLists() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
const personalList = Mjolnir.sharedInstance().getPersonalList();
|
||||
const lists = Mjolnir.sharedInstance().lists.filter(b => {
|
||||
return personalList? personalList.roomId !== b.roomId : true;
|
||||
|
@ -241,8 +234,6 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
|
|||
}
|
||||
|
||||
render() {
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const brand = SdkConfig.get().brand;
|
||||
|
||||
return (
|
||||
|
|
|
@ -20,10 +20,12 @@ import { _t } from "../../../../../languageHandler";
|
|||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import Field from "../../../elements/Field";
|
||||
import * as sdk from "../../../../..";
|
||||
import PlatformPeg from "../../../../../PlatformPeg";
|
||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||
import SettingsFlag from '../../../elements/SettingsFlag';
|
||||
import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
|
||||
interface IState {
|
||||
autoLaunch: boolean;
|
||||
|
@ -45,6 +47,10 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta
|
|||
'breadcrumbs',
|
||||
];
|
||||
|
||||
static KEYBINDINGS_SETTINGS = [
|
||||
'ctrlFForSearch',
|
||||
];
|
||||
|
||||
static COMPOSER_SETTINGS = [
|
||||
'MessageComposerInput.autoReplaceEmoji',
|
||||
'MessageComposerInput.suggestEmoji',
|
||||
|
@ -53,28 +59,32 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta
|
|||
'MessageComposerInput.showStickersButton',
|
||||
];
|
||||
|
||||
static TIMELINE_SETTINGS = [
|
||||
'showTypingNotifications',
|
||||
'autoplayGifsAndVideos',
|
||||
'urlPreviewsEnabled',
|
||||
'TextualBody.enableBigEmoji',
|
||||
'showReadReceipts',
|
||||
static TIME_SETTINGS = [
|
||||
'showTwelveHourTimestamps',
|
||||
'alwaysShowTimestamps',
|
||||
'showRedactions',
|
||||
];
|
||||
static CODE_BLOCKS_SETTINGS = [
|
||||
'enableSyntaxHighlightLanguageDetection',
|
||||
'expandCodeByDefault',
|
||||
'scrollToBottomOnMessageSent',
|
||||
'showCodeLineNumbers',
|
||||
'showJoinLeaves',
|
||||
'showAvatarChanges',
|
||||
'showDisplaynameChanges',
|
||||
'showImages',
|
||||
'showChatEffects',
|
||||
'Pill.shouldShowPillAvatar',
|
||||
'ctrlFForSearch',
|
||||
];
|
||||
|
||||
static IMAGES_AND_VIDEOS_SETTINGS = [
|
||||
'urlPreviewsEnabled',
|
||||
'autoplayGifsAndVideos',
|
||||
'showImages',
|
||||
];
|
||||
static TIMELINE_SETTINGS = [
|
||||
'showTypingNotifications',
|
||||
'showRedactions',
|
||||
'showReadReceipts',
|
||||
'showJoinLeaves',
|
||||
'showDisplaynameChanges',
|
||||
'showChatEffects',
|
||||
'showAvatarChanges',
|
||||
'Pill.shouldShowPillAvatar',
|
||||
'TextualBody.enableBigEmoji',
|
||||
'scrollToBottomOnMessageSent',
|
||||
];
|
||||
static GENERAL_SETTINGS = [
|
||||
'TagPanel.enableTagPanel',
|
||||
'promptBeforeInviteUnknownUsers',
|
||||
|
@ -174,7 +184,6 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta
|
|||
};
|
||||
|
||||
private renderGroup(settingIds: string[]): React.ReactNodeArray {
|
||||
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
|
||||
return settingIds.filter(SettingsStore.isEnabled).map(i => {
|
||||
return <SettingsFlag key={i} name={i} level={SettingLevel.ACCOUNT} />;
|
||||
});
|
||||
|
@ -222,11 +231,34 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta
|
|||
{this.renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS)}
|
||||
</div>
|
||||
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Keyboard shortcuts")}</span>
|
||||
<AccessibleButton className="mx_SettingsFlag" onClick={KeyboardShortcuts.toggleDialog}>
|
||||
{ _t("To view all keyboard shortcuts, click here.") }
|
||||
</AccessibleButton>
|
||||
{this.renderGroup(PreferencesUserSettingsTab.KEYBINDINGS_SETTINGS)}
|
||||
</div>
|
||||
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Displaying time")}</span>
|
||||
{this.renderGroup(PreferencesUserSettingsTab.TIME_SETTINGS)}
|
||||
</div>
|
||||
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Composer")}</span>
|
||||
{this.renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS)}
|
||||
</div>
|
||||
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Code blocks")}</span>
|
||||
{this.renderGroup(PreferencesUserSettingsTab.CODE_BLOCKS_SETTINGS)}
|
||||
</div>
|
||||
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Images, GIFs and videos")}</span>
|
||||
{this.renderGroup(PreferencesUserSettingsTab.IMAGES_AND_VIDEOS_SETTINGS)}
|
||||
</div>
|
||||
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Timeline")}</span>
|
||||
{this.renderGroup(PreferencesUserSettingsTab.TIMELINE_SETTINGS)}
|
||||
|
|
|
@ -17,6 +17,8 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import { _t } from "../../../../../languageHandler";
|
||||
import SdkConfig from "../../../../../SdkConfig";
|
||||
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
||||
|
@ -25,7 +27,6 @@ import AccessibleButton from "../../../elements/AccessibleButton";
|
|||
import Analytics from "../../../../../Analytics";
|
||||
import Modal from "../../../../../Modal";
|
||||
import * as sdk from "../../../../..";
|
||||
import { sleep } from "../../../../../utils/promise";
|
||||
import dis from "../../../../../dispatcher/dispatcher";
|
||||
import { privateShouldBeEncrypted } from "../../../../../createRoom";
|
||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||
|
|
|
@ -18,41 +18,58 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import { _t } from "../../../../../languageHandler";
|
||||
import SdkConfig from "../../../../../SdkConfig";
|
||||
import MediaDeviceHandler from "../../../../../MediaDeviceHandler";
|
||||
import MediaDeviceHandler, { IMediaDevices, MediaDeviceKindEnum } from "../../../../../MediaDeviceHandler";
|
||||
import Field from "../../../elements/Field";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
||||
import * as sdk from "../../../../../index";
|
||||
import Modal from "../../../../../Modal";
|
||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||
import SettingsFlag from '../../../elements/SettingsFlag';
|
||||
import ErrorDialog from '../../../dialogs/ErrorDialog';
|
||||
|
||||
const getDefaultDevice = (devices: Array<Partial<MediaDeviceInfo>>) => {
|
||||
// Note we're looking for a device with deviceId 'default' but adding a device
|
||||
// with deviceId == the empty string: this is because Chrome gives us a device
|
||||
// with deviceId 'default', so we're looking for this, not the one we are adding.
|
||||
if (!devices.some((i) => i.deviceId === 'default')) {
|
||||
devices.unshift({ deviceId: '', label: _t('Default Device') });
|
||||
return '';
|
||||
} else {
|
||||
return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
interface IState extends Record<MediaDeviceKindEnum, string> {
|
||||
mediaDevices: IMediaDevices;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.settings.tabs.user.VoiceUserSettingsTab")
|
||||
export default class VoiceUserSettingsTab extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
export default class VoiceUserSettingsTab extends React.Component<{}, IState> {
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
mediaDevices: false,
|
||||
activeAudioOutput: null,
|
||||
activeAudioInput: null,
|
||||
activeVideoInput: null,
|
||||
mediaDevices: null,
|
||||
[MediaDeviceKindEnum.AudioOutput]: null,
|
||||
[MediaDeviceKindEnum.AudioInput]: null,
|
||||
[MediaDeviceKindEnum.VideoInput]: null,
|
||||
};
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const canSeeDeviceLabels = await MediaDeviceHandler.hasAnyLabeledDevices();
|
||||
if (canSeeDeviceLabels) {
|
||||
this._refreshMediaDevices();
|
||||
this.refreshMediaDevices();
|
||||
}
|
||||
}
|
||||
|
||||
_refreshMediaDevices = async (stream) => {
|
||||
private refreshMediaDevices = async (stream?: MediaStream): Promise<void> => {
|
||||
this.setState({
|
||||
mediaDevices: await MediaDeviceHandler.getDevices(),
|
||||
activeAudioOutput: MediaDeviceHandler.getAudioOutput(),
|
||||
activeAudioInput: MediaDeviceHandler.getAudioInput(),
|
||||
activeVideoInput: MediaDeviceHandler.getVideoInput(),
|
||||
[MediaDeviceKindEnum.AudioOutput]: MediaDeviceHandler.getAudioOutput(),
|
||||
[MediaDeviceKindEnum.AudioInput]: MediaDeviceHandler.getAudioInput(),
|
||||
[MediaDeviceKindEnum.VideoInput]: MediaDeviceHandler.getVideoInput(),
|
||||
});
|
||||
if (stream) {
|
||||
// kill stream (after we've enumerated the devices, otherwise we'd get empty labels again)
|
||||
|
@ -62,7 +79,7 @@ export default class VoiceUserSettingsTab extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
_requestMediaPermissions = async () => {
|
||||
private requestMediaPermissions = async (): Promise<void> => {
|
||||
let constraints;
|
||||
let stream;
|
||||
let error;
|
||||
|
@ -86,7 +103,6 @@ export default class VoiceUserSettingsTab extends React.Component {
|
|||
if (error) {
|
||||
console.log("Failed to list userMedia devices", error);
|
||||
const brand = SdkConfig.get().brand;
|
||||
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
||||
Modal.createTrackedDialog('No media permissions', '', ErrorDialog, {
|
||||
title: _t('No media permissions'),
|
||||
description: _t(
|
||||
|
@ -95,137 +111,93 @@ export default class VoiceUserSettingsTab extends React.Component {
|
|||
),
|
||||
});
|
||||
} else {
|
||||
this._refreshMediaDevices(stream);
|
||||
this.refreshMediaDevices(stream);
|
||||
}
|
||||
};
|
||||
|
||||
_setAudioOutput = (e) => {
|
||||
MediaDeviceHandler.instance.setAudioOutput(e.target.value);
|
||||
this.setState({
|
||||
activeAudioOutput: e.target.value,
|
||||
});
|
||||
private setDevice = (deviceId: string, kind: MediaDeviceKindEnum): void => {
|
||||
MediaDeviceHandler.instance.setDevice(deviceId, kind);
|
||||
this.setState<null>({ [kind]: deviceId });
|
||||
};
|
||||
|
||||
_setAudioInput = (e) => {
|
||||
MediaDeviceHandler.instance.setAudioInput(e.target.value);
|
||||
this.setState({
|
||||
activeAudioInput: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
_setVideoInput = (e) => {
|
||||
MediaDeviceHandler.instance.setVideoInput(e.target.value);
|
||||
this.setState({
|
||||
activeVideoInput: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
_changeWebRtcMethod = (p2p) => {
|
||||
private changeWebRtcMethod = (p2p: boolean): void => {
|
||||
MatrixClientPeg.get().setForceTURN(!p2p);
|
||||
};
|
||||
|
||||
_changeFallbackICEServerAllowed = (allow) => {
|
||||
private changeFallbackICEServerAllowed = (allow: boolean): void => {
|
||||
MatrixClientPeg.get().setFallbackICEServerAllowed(allow);
|
||||
};
|
||||
|
||||
_renderDeviceOptions(devices, category) {
|
||||
private renderDeviceOptions(devices: Array<MediaDeviceInfo>, category: MediaDeviceKindEnum): Array<JSX.Element> {
|
||||
return devices.map((d) => {
|
||||
return (<option key={`${category}-${d.deviceId}`} value={d.deviceId}>{d.label}</option>);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
|
||||
private renderDropdown(kind: MediaDeviceKindEnum, label: string): JSX.Element {
|
||||
const devices = this.state.mediaDevices[kind].slice(0);
|
||||
if (devices.length === 0) return null;
|
||||
|
||||
const defaultDevice = getDefaultDevice(devices);
|
||||
return (
|
||||
<Field
|
||||
element="select"
|
||||
label={label}
|
||||
value={this.state[kind] || defaultDevice}
|
||||
onChange={(e) => this.setDevice(e.target.value, kind)}
|
||||
>
|
||||
{ this.renderDeviceOptions(devices, kind) }
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
let requestButton = null;
|
||||
let speakerDropdown = null;
|
||||
let microphoneDropdown = null;
|
||||
let webcamDropdown = null;
|
||||
if (this.state.mediaDevices === false) {
|
||||
if (!this.state.mediaDevices) {
|
||||
requestButton = (
|
||||
<div className='mx_VoiceUserSettingsTab_missingMediaPermissions'>
|
||||
<p>{_t("Missing media permissions, click the button below to request.")}</p>
|
||||
<AccessibleButton onClick={this._requestMediaPermissions} kind="primary">
|
||||
<AccessibleButton onClick={this.requestMediaPermissions} kind="primary">
|
||||
{_t("Request media permissions")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.mediaDevices) {
|
||||
speakerDropdown = <p>{ _t('No Audio Outputs detected') }</p>;
|
||||
microphoneDropdown = <p>{ _t('No Microphones detected') }</p>;
|
||||
webcamDropdown = <p>{ _t('No Webcams detected') }</p>;
|
||||
|
||||
const defaultOption = {
|
||||
deviceId: '',
|
||||
label: _t('Default Device'),
|
||||
};
|
||||
const getDefaultDevice = (devices) => {
|
||||
// Note we're looking for a device with deviceId 'default' but adding a device
|
||||
// with deviceId == the empty string: this is because Chrome gives us a device
|
||||
// with deviceId 'default', so we're looking for this, not the one we are adding.
|
||||
if (!devices.some((i) => i.deviceId === 'default')) {
|
||||
devices.unshift(defaultOption);
|
||||
return '';
|
||||
} else {
|
||||
return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
const audioOutputs = this.state.mediaDevices.audioOutput.slice(0);
|
||||
if (audioOutputs.length > 0) {
|
||||
const defaultDevice = getDefaultDevice(audioOutputs);
|
||||
speakerDropdown = (
|
||||
<Field element="select" label={_t("Audio Output")}
|
||||
value={this.state.activeAudioOutput || defaultDevice}
|
||||
onChange={this._setAudioOutput}>
|
||||
{this._renderDeviceOptions(audioOutputs, 'audioOutput')}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
const audioInputs = this.state.mediaDevices.audioInput.slice(0);
|
||||
if (audioInputs.length > 0) {
|
||||
const defaultDevice = getDefaultDevice(audioInputs);
|
||||
microphoneDropdown = (
|
||||
<Field element="select" label={_t("Microphone")}
|
||||
value={this.state.activeAudioInput || defaultDevice}
|
||||
onChange={this._setAudioInput}>
|
||||
{this._renderDeviceOptions(audioInputs, 'audioInput')}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
const videoInputs = this.state.mediaDevices.videoInput.slice(0);
|
||||
if (videoInputs.length > 0) {
|
||||
const defaultDevice = getDefaultDevice(videoInputs);
|
||||
webcamDropdown = (
|
||||
<Field element="select" label={_t("Camera")}
|
||||
value={this.state.activeVideoInput || defaultDevice}
|
||||
onChange={this._setVideoInput}>
|
||||
{this._renderDeviceOptions(videoInputs, 'videoInput')}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
speakerDropdown = (
|
||||
this.renderDropdown(MediaDeviceKindEnum.AudioOutput, _t("Audio Output")) ||
|
||||
<p>{ _t('No Audio Outputs detected') }</p>
|
||||
);
|
||||
microphoneDropdown = (
|
||||
this.renderDropdown(MediaDeviceKindEnum.AudioInput, _t("Microphone")) ||
|
||||
<p>{ _t('No Microphones detected') }</p>
|
||||
);
|
||||
webcamDropdown = (
|
||||
this.renderDropdown(MediaDeviceKindEnum.VideoInput, _t("Camera")) ||
|
||||
<p>{ _t('No Webcams detected') }</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_VoiceUserSettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("Voice & Video")}</div>
|
||||
<div className="mx_SettingsTab_section">
|
||||
{requestButton}
|
||||
{speakerDropdown}
|
||||
{microphoneDropdown}
|
||||
{webcamDropdown}
|
||||
{ requestButton }
|
||||
{ speakerDropdown }
|
||||
{ microphoneDropdown }
|
||||
{ webcamDropdown }
|
||||
<SettingsFlag name='VideoView.flipVideoHorizontally' level={SettingLevel.ACCOUNT} />
|
||||
<SettingsFlag
|
||||
name='webRtcAllowPeerToPeer'
|
||||
level={SettingLevel.DEVICE}
|
||||
onChange={this._changeWebRtcMethod}
|
||||
onChange={this.changeWebRtcMethod}
|
||||
/>
|
||||
<SettingsFlag
|
||||
name='fallbackICEServerAllowed'
|
||||
level={SettingLevel.DEVICE}
|
||||
onChange={this._changeFallbackICEServerAllowed}
|
||||
onChange={this.changeFallbackICEServerAllowed}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
|
@ -23,7 +23,7 @@ import Field from "../elements/Field";
|
|||
interface IProps {
|
||||
avatarUrl?: string;
|
||||
avatarDisabled?: boolean;
|
||||
name?: string,
|
||||
name?: string;
|
||||
nameDisabled?: boolean;
|
||||
topic?: string;
|
||||
topicDisabled?: boolean;
|
||||
|
|
|
@ -220,6 +220,7 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
|||
value={alias}
|
||||
placeholder={name ? nameToAlias(name, domain) : _t("e.g. my-space")}
|
||||
label={_t("Address")}
|
||||
disabled={busy}
|
||||
/>
|
||||
: null
|
||||
}
|
||||
|
|
|
@ -16,11 +16,11 @@ limitations under the License.
|
|||
|
||||
import React, { useState } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { copyPlaintext } from "../../../utils/strings";
|
||||
import { sleep } from "../../../utils/promise";
|
||||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||
import { showRoomInviteDialog } from "../../../RoomInvite";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
|
|
|
@ -96,7 +96,7 @@ const SpaceSettingsGeneralTab = ({ matrixClient: cli, space, onFinished }: IProp
|
|||
|
||||
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
|
||||
|
||||
<SpaceFeedbackPrompt onClick={() => onFinished(false)} />
|
||||
<SpaceFeedbackPrompt />
|
||||
|
||||
<div className="mx_SettingsTab_section">
|
||||
<SpaceBasicSettings
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue