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

This commit is contained in:
Jaiwanth 2021-06-29 11:31:30 +05:30
commit 637e3afa48
24 changed files with 461 additions and 312 deletions

View file

@ -82,6 +82,7 @@ import SpaceRoomView from "./SpaceRoomView";
import { IOpts } from "../../createRoom";
import { replaceableComponent } from "../../utils/replaceableComponent";
import UIStore from "../../stores/UIStore";
import EditorStateTransfer from "../../utils/EditorStateTransfer";
const DEBUG = false;
let debuglog = function(msg: string) {};
@ -192,6 +193,7 @@ export interface IState {
// whether or not a spaces context switch brought us here,
// if it did we don't want the room to be marked as read as soon as it is loaded.
wasContextSwitch?: boolean;
editState?: EditorStateTransfer;
}
@replaceableComponent("structures.RoomView")
@ -815,6 +817,36 @@ export default class RoomView extends React.Component<IProps, IState> {
case 'focus_search':
this.onSearchClick();
break;
case "edit_event": {
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
this.setState({ editState }, () => {
if (payload.event) {
this.messagePanel?.scrollToEventIfNeeded(payload.event.getId());
}
});
break;
}
case Action.ComposerInsert: {
// re-dispatch to the correct composer
if (this.state.editState) {
dis.dispatch({
...payload,
action: "edit_composer_insert",
});
} else {
dis.dispatch({
...payload,
action: "send_composer_insert",
});
}
break;
}
case "scroll_to_bottom":
this.messagePanel?.jumpToLiveTimeline();
break;
}
};

View file

@ -34,12 +34,10 @@ import * as sdk from "../../index";
import { Key } from '../../Keyboard';
import Timer from '../../utils/Timer';
import shouldHideEvent from '../../shouldHideEvent';
import EditorStateTransfer from '../../utils/EditorStateTransfer';
import { haveTileForEvent } from "../views/rooms/EventTile";
import { UIFeature } from "../../settings/UIFeature";
import { replaceableComponent } from "../../utils/replaceableComponent";
import { arrayFastClone } from "../../utils/arrays";
import { Action } from "../../dispatcher/actions";
const PAGINATE_SIZE = 20;
const INITIAL_SIZE = 20;
@ -72,6 +70,8 @@ class TimelinePanel extends React.Component {
manageReadReceipts: PropTypes.bool,
sendReadReceiptOnLoad: PropTypes.bool,
manageReadMarkers: PropTypes.bool,
// with this enabled it'll listen and react to Action.ComposerInsert and `edit_event`
manageComposerDispatches: PropTypes.bool,
// true to give the component a 'display: none' style.
hidden: PropTypes.bool,
@ -444,38 +444,6 @@ class TimelinePanel extends React.Component {
case "ignore_state_changed":
this.forceUpdate();
break;
case "edit_event": {
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
this.setState({editState}, () => {
if (payload.event && this._messagePanel.current) {
this._messagePanel.current.scrollToEventIfNeeded(
payload.event.getId(),
);
}
});
break;
}
case Action.ComposerInsert: {
// re-dispatch to the correct composer
if (this.state.editState) {
dis.dispatch({
...payload,
action: "edit_composer_insert",
});
} else {
dis.dispatch({
...payload,
action: "send_composer_insert",
});
}
break;
}
case "scroll_to_bottom":
this.jumpToLiveTimeline();
break;
}
};
@ -866,6 +834,12 @@ class TimelinePanel extends React.Component {
}
};
scrollToEventIfNeeded = (eventId) => {
if (this._messagePanel.current) {
this._messagePanel.current.scrollToEventIfNeeded(eventId);
}
}
/* scroll to show the read-up-to marker. We put it 1/3 of the way down
* the container.
*/
@ -1473,7 +1447,7 @@ class TimelinePanel extends React.Component {
tileShape={this.props.tileShape}
resizeNotifier={this.props.resizeNotifier}
getRelationsForEvent={this.getRelationsForEvent}
editState={this.state.editState}
editState={this.props.editState}
showReactions={this.props.showReactions}
layout={this.props.layout}
enableFlair={SettingsStore.getValue(UIFeature.Flair)}

View file

@ -140,7 +140,7 @@ export default class SenderProfile extends React.Component<IProps, IState> {
}
return (
<div className="mx_SenderProfile mx_SenderProfile_hover" dir="auto" onClick={this.props.onClick}>
<div className="mx_SenderProfile" dir="auto" onClick={this.props.onClick}>
<span className={`mx_SenderProfile_displayName ${colorClass}`}>
{ displayName }
</span>

View file

@ -851,7 +851,7 @@ export default class EventTile extends React.Component<IProps, IState> {
let tileHandler = getHandlerTile(this.props.mxEvent);
// Info messages are basically information about commands processed on a room
const isBubbleMessage = eventType.startsWith("m.key.verification") ||
let isBubbleMessage = eventType.startsWith("m.key.verification") ||
(eventType === EventType.RoomMessage && msgtype && msgtype.startsWith("m.key.verification")) ||
(eventType === EventType.RoomCreate) ||
(eventType === EventType.RoomEncryption) ||
@ -867,6 +867,7 @@ export default class EventTile extends React.Component<IProps, IState> {
// duplicate of the thing they are replacing).
if (SettingsStore.getValue("showHiddenEventsInTimeline") && !haveTileForEvent(this.props.mxEvent)) {
tileHandler = "messages.ViewSourceEvent";
isBubbleMessage = false;
// Reuse info message avatar and sender profile styling
isInfoMessage = true;
}

View file

@ -15,19 +15,26 @@ limitations under the License.
*/
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import {_t} from "../../../languageHandler";
import { _t } from "../../../languageHandler";
import React, {ReactNode} from "react";
import {RecordingState, VoiceRecording} from "../../../voice/VoiceRecording";
import {
IRecordingUpdate,
RECORDING_PLAYBACK_SAMPLES,
RecordingState,
VoiceRecording,
} from "../../../voice/VoiceRecording";
import {Room} from "matrix-js-sdk/src/models/room";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import classNames from "classnames";
import LiveRecordingWaveform from "../voice_messages/LiveRecordingWaveform";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { arrayFastResample, arraySeed } from "../../../utils/arrays";
import { percentageOf } from "../../../utils/numbers";
import LiveRecordingClock from "../voice_messages/LiveRecordingClock";
import {VoiceRecordingStore} from "../../../stores/VoiceRecordingStore";
import { VoiceRecordingStore } from "../../../stores/VoiceRecordingStore";
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
import RecordingPlayback from "../voice_messages/RecordingPlayback";
import {MsgType} from "matrix-js-sdk/src/@types/event";
import { MsgType } from "matrix-js-sdk/src/@types/event";
import Modal from "../../../Modal";
import ErrorDialog from "../dialogs/ErrorDialog";
import MediaDeviceHandler from "../../../MediaDeviceHandler";
@ -39,6 +46,8 @@ interface IProps {
interface IState {
recorder?: VoiceRecording;
recordingPhase?: RecordingState;
relHeights: number[];
seconds: number;
}
/**
@ -46,18 +55,58 @@ 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) {
@ -65,12 +114,13 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
}
await this.state.recorder.stop();
const mxc = await this.state.recorder.upload();
const upload = await this.state.recorder.upload(this.props.room.roomId);
MatrixClientPeg.get().sendMessage(this.props.room.roomId, {
"body": "Voice message",
//"msgtype": "org.matrix.msc2516.voice",
"msgtype": MsgType.Audio,
"url": mxc,
"url": upload.mxc,
"file": upload.encrypted,
"info": {
duration: Math.round(this.state.recorder.durationSeconds * 1000),
mimetype: this.state.recorder.contentType,
@ -81,7 +131,8 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
// https://github.com/matrix-org/matrix-doc/pull/3245
"org.matrix.msc1767.text": "Voice message",
"org.matrix.msc1767.file": {
url: mxc,
url: upload.mxc,
file: upload.encrypted,
name: "Voice message.ogg",
mimetype: this.state.recorder.contentType,
size: this.state.recorder.contentLength,

View file

@ -50,7 +50,7 @@ const GenericToast: React.FC<XOR<IPropsExtended, IProps>> = ({
{detailContent}
</div>
<div className="mx_Toast_buttons" aria-live="off">
{onReject && rejectLabel && <AccessibleButton kind="danger" onClick={onReject}>
{onReject && rejectLabel && <AccessibleButton kind="danger_outline" onClick={onReject}>
{ rejectLabel }
</AccessibleButton> }
<AccessibleButton onClick={onAccept} kind="primary">

View file

@ -15,9 +15,9 @@ limitations under the License.
*/
import React from "react";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
export interface IProps {
seconds: number;
}

View file

@ -1,12 +1,9 @@
/*
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.
@ -15,12 +12,16 @@ limitations under the License.
*/
import React from "react";
import {IRecordingUpdate, VoiceRecording} from "../../../voice/VoiceRecording";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import Clock from "./Clock";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { MarkedExecution } from "../../../utils/MarkedExecution";
import {
IRecordingUpdate,
VoiceRecording,
} from "../../../voice/VoiceRecording";
interface IProps {
recorder: VoiceRecording;
recorder?: VoiceRecording;
}
interface IState {
@ -32,16 +33,31 @@ interface IState {
*/
@replaceableComponent("views.voice_messages.LiveRecordingClock")
export default class LiveRecordingClock extends React.PureComponent<IProps, IState> {
public constructor(props) {
super(props);
private seconds = 0;
private scheduledUpdate = new MarkedExecution(
() => this.updateClock(),
() => requestAnimationFrame(() => this.scheduledUpdate.trigger()),
);
this.state = {seconds: 0};
this.props.recorder.liveData.onUpdate(this.onRecordingUpdate);
constructor(props) {
super(props);
this.state = {
seconds: 0,
};
}
private onRecordingUpdate = (update: IRecordingUpdate) => {
this.setState({seconds: update.timeSeconds});
};
componentDidMount() {
this.props.recorder.liveData.onUpdate((update: IRecordingUpdate) => {
this.seconds = update.timeSeconds;
this.scheduledUpdate.mark();
});
}
private updateClock() {
this.setState({
seconds: this.seconds,
});
}
public render() {
return <Clock seconds={this.state.seconds} />;

View file

@ -1,12 +1,9 @@
/*
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.
@ -15,18 +12,20 @@ limitations under the License.
*/
import React from "react";
import {IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording} from "../../../voice/VoiceRecording";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {arrayFastResample, arraySeed} from "../../../utils/arrays";
import {percentageOf} from "../../../utils/numbers";
import Waveform from "./Waveform";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { MarkedExecution } from "../../../utils/MarkedExecution";
import {
IRecordingUpdate,
VoiceRecording,
} from "../../../voice/VoiceRecording";
interface IProps {
recorder: VoiceRecording;
recorder?: VoiceRecording;
}
interface IState {
heights: number[];
waveform: number[];
}
/**
@ -34,27 +33,37 @@ interface IState {
*/
@replaceableComponent("views.voice_messages.LiveRecordingWaveform")
export default class LiveRecordingWaveform extends React.PureComponent<IProps, IState> {
public constructor(props) {
super(props);
this.state = {heights: arraySeed(0, RECORDING_PLAYBACK_SAMPLES)};
this.props.recorder.liveData.onUpdate(this.onRecordingUpdate);
}
private onRecordingUpdate = (update: IRecordingUpdate) => {
// 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(update.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.
heights: bars.map(b => percentageOf(b, 0, 0.50)),
});
public static defaultProps = {
progress: 1,
};
private waveform: number[] = [];
private scheduledUpdate = new MarkedExecution(
() => this.updateWaveform(),
() => requestAnimationFrame(() => this.scheduledUpdate.trigger()),
);
constructor(props) {
super(props);
this.state = {
waveform: [],
};
}
componentDidMount() {
this.props.recorder.liveData.onUpdate((update: IRecordingUpdate) => {
this.waveform = update.waveform;
this.scheduledUpdate.mark();
});
}
private updateWaveform() {
this.setState({
waveform: this.waveform,
})
}
public render() {
return <Waveform relHeights={this.state.heights} />;
return <Waveform relHeights={this.state.waveform} />;
}
}

View file

@ -15,10 +15,10 @@ limitations under the License.
*/
import React from "react";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import classNames from "classnames";
interface IProps {
export interface IProps {
relHeights: number[]; // relative heights (0-1)
progress: number; // percent complete, 0-1, default 100%
}
@ -34,16 +34,19 @@ 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")
export default class Waveform extends React.PureComponent<IProps, IState> {
public static defaultProps = {
progress: 1,
};
public constructor(props) {
super(props);
}
public render() {
return <div className='mx_Waveform'>
{this.props.relHeights.map((h, i) => {
@ -53,7 +56,9 @@ export default class Waveform extends React.PureComponent<IProps, IState> {
'mx_Waveform_bar': true,
'mx_Waveform_bar_100pct': isCompleteBar,
});
return <span key={i} style={{height: (h * 100) + '%'}} className={classes} />;
return <span key={i} style={{
"--barHeight": h,
} as WaveformCSSProperties} className={classes} />;
})}
</div>;
}