Merge branch 'develop' into gsouquet/threads-action-bar-19127

This commit is contained in:
Germain Souquet 2021-09-28 09:34:43 +01:00
commit e52a33e93c
45 changed files with 668 additions and 468 deletions

View file

@ -15,7 +15,6 @@ limitations under the License.
*/
import React from 'react';
import classNames from 'classnames';
import { lexicographicCompare } from 'matrix-js-sdk/src/utils';
import { Room } from 'matrix-js-sdk/src/models/room';
@ -35,16 +34,6 @@ interface IProps {
room: Room;
userId: string;
showApps: boolean; // Render apps
// maxHeight attribute for the aux panel and the video
// therein
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;
resizeNotifier: ResizeNotifier;
}
@ -92,13 +81,6 @@ export default class AuxPanel extends React.Component<IProps, IState> {
return objectHasDiff(this.props, nextProps) || objectHasDiff(this.state, nextState);
}
componentDidUpdate(prevProps, prevState) {
// most changes are likely to cause a resize
if (this.props.onResize) {
this.props.onResize();
}
}
private rateLimitedUpdate = throttle(() => {
this.setState({ counters: this.computeCounters() });
}, 500, { leading: true, trailing: true });
@ -138,7 +120,6 @@ export default class AuxPanel extends React.Component<IProps, IState> {
const callView = (
<CallViewForRoom
roomId={this.props.room.roomId}
maxVideoHeight={this.props.maxHeight}
resizeNotifier={this.props.resizeNotifier}
/>
);
@ -148,7 +129,6 @@ export default class AuxPanel extends React.Component<IProps, IState> {
appsDrawer = <AppsDrawer
room={this.props.room}
userId={this.props.userId}
maxHeight={this.props.maxHeight}
showApps={this.props.showApps}
resizeNotifier={this.props.resizeNotifier}
/>;
@ -204,21 +184,12 @@ export default class AuxPanel extends React.Component<IProps, IState> {
}
}
const classes = classNames({
"mx_RoomView_auxPanel": true,
"mx_RoomView_auxPanel_fullHeight": this.props.fullHeight,
});
const style: React.CSSProperties = {};
if (!this.props.fullHeight) {
style.maxHeight = this.props.maxHeight;
}
return (
<AutoHideScrollbar className={classes} style={style}>
<AutoHideScrollbar className="mx_RoomView_auxPanel">
{ stateViews }
{ this.props.children }
{ appsDrawer }
{ callView }
{ this.props.children }
</AutoHideScrollbar>
);
}

View file

@ -58,6 +58,7 @@ import ReactionsRow from '../messages/ReactionsRow';
import { getEventDisplayInfo } from '../../../utils/EventUtils';
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
import SettingsStore from "../../../settings/SettingsStore";
import MKeyVerificationConclusion from "../messages/MKeyVerificationConclusion";
const eventTileTypes = {
[EventType.RoomMessage]: 'messages.MessageEvent',
@ -144,8 +145,7 @@ export function getHandlerTile(ev) {
// XXX: This is extremely a hack. Possibly these components should have an interface for
// declining to render?
if (type === "m.key.verification.cancel" || type === "m.key.verification.done") {
const MKeyVerificationConclusion = sdk.getComponent("messages.MKeyVerificationConclusion");
if (!MKeyVerificationConclusion.prototype._shouldRender.call(null, ev, ev.request)) {
if (!MKeyVerificationConclusion.shouldRender(ev, ev.request)) {
return;
}
}
@ -323,7 +323,7 @@ interface IState {
reactions: Relations;
hover: boolean;
isQuoteExpanded?: boolean;
thread?: Thread;
}
@ -331,7 +331,8 @@ interface IState {
export default class EventTile extends React.Component<IProps, IState> {
private suppressReadReceiptAnimation: boolean;
private isListeningForReceipts: boolean;
private tile = React.createRef();
// TODO: Types
private tile = React.createRef<unknown>();
private replyThread = React.createRef<ReplyThread>();
public readonly ref = createRef<HTMLElement>();
@ -889,8 +890,8 @@ export default class EventTile extends React.Component<IProps, IState> {
actionBarFocused: focused,
});
};
getTile = () => this.tile.current;
// TODO: Types
getTile: () => any | null = () => this.tile.current;
getReplyThread = () => this.replyThread.current;
@ -915,6 +916,11 @@ export default class EventTile extends React.Component<IProps, IState> {
});
};
private setQuoteExpanded = (expanded: boolean) => {
this.setState({
isQuoteExpanded: expanded,
});
};
render() {
const msgtype = this.props.mxEvent.getContent().msgtype;
const eventType = this.props.mxEvent.getType() as EventType;
@ -924,6 +930,7 @@ export default class EventTile extends React.Component<IProps, IState> {
isInfoMessage,
isLeftAlignedBubbleMessage,
} = getEventDisplayInfo(this.props.mxEvent);
const { isQuoteExpanded } = this.state;
// This shouldn't happen: the caller should check we support this type
// before trying to instantiate us
@ -936,6 +943,7 @@ export default class EventTile extends React.Component<IProps, IState> {
</div>
</div>;
}
const EventTileType = sdk.getComponent(tileHandler);
const isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1);
@ -1057,6 +1065,7 @@ export default class EventTile extends React.Component<IProps, IState> {
getReplyThread={this.getReplyThread}
onFocusChange={this.onActionBarFocusChange}
isInThreadTimeline={isInThreadTimeline}
toggleThreadExpanded={() => this.setQuoteExpanded(!isQuoteExpanded)}
/> : undefined;
const showTimestamp = this.props.mxEvent.getTs()
@ -1229,20 +1238,18 @@ export default class EventTile extends React.Component<IProps, IState> {
}
default: {
let thread;
// When the "showHiddenEventsInTimeline" lab is enabled,
// avoid showing replies for hidden events (events without tiles)
if (haveTileForEvent(this.props.mxEvent)) {
thread = ReplyThread.makeThread(
this.props.mxEvent,
this.props.onHeightChanged,
this.props.permalinkCreator,
this.replyThread,
this.props.layout,
this.props.alwaysShowTimestamps || this.state.hover,
);
}
const thread = haveTileForEvent(this.props.mxEvent) &&
ReplyThread.hasThreadReply(this.props.mxEvent) ? (
<ReplyThread
parentEv={this.props.mxEvent}
onHeightChanged={this.props.onHeightChanged}
ref={this.replyThread}
permalinkCreator={this.props.permalinkCreator}
layout={this.props.layout}
alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.hover}
isQuoteExpanded={isQuoteExpanded}
setQuoteExpanded={this.setQuoteExpanded}
/>) : null;
const isOwnEvent = this.props.mxEvent?.sender?.userId === MatrixClientPeg.get().getUserId();
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers

View file

@ -16,7 +16,7 @@ limitations under the License.
import React, { useContext, useEffect } from "react";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { IPreviewUrlResponse } from "matrix-js-sdk/src/client";
import { IPreviewUrlResponse, MatrixClient } from "matrix-js-sdk/src/client";
import { useStateToggle } from "../../../hooks/useStateToggle";
import LinkPreviewWidget from "./LinkPreviewWidget";
@ -40,13 +40,7 @@ const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick, onH
const ts = mxEvent.getTs();
const previews = useAsyncMemo<[string, IPreviewUrlResponse][]>(async () => {
return Promise.all<[string, IPreviewUrlResponse] | void>(links.map(async link => {
try {
return [link, await cli.getUrlPreview(link, ts)];
} catch (error) {
console.error("Failed to get URL preview: " + error);
}
})).then(a => a.filter(Boolean)) as Promise<[string, IPreviewUrlResponse][]>;
return fetchPreviews(cli, links, ts);
}, [links, ts], []);
useEffect(() => {
@ -89,4 +83,18 @@ const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick, onH
</div>;
};
const fetchPreviews = (cli: MatrixClient, links: string[], ts: number):
Promise<[string, IPreviewUrlResponse][]> => {
return Promise.all<[string, IPreviewUrlResponse] | void>(links.map(async link => {
try {
const preview = await cli.getUrlPreview(link, ts);
if (preview && Object.keys(preview).length > 0) {
return [link, preview];
}
} catch (error) {
console.error("Failed to get URL preview: " + error);
}
})).then(a => a.filter(Boolean)) as Promise<[string, IPreviewUrlResponse][]>;
};
export default LinkPreviewGroup;

View file

@ -35,6 +35,7 @@ interface IProps {
highlights?: string[];
highlightLink?: string;
onHeightChanged?(): void;
toggleExpandedQuote?: () => void;
}
@replaceableComponent("views.rooms.ReplyTile")
@ -82,12 +83,17 @@ export default class ReplyTile extends React.PureComponent<IProps> {
// This allows the permalink to be opened in a new tab/window or copied as
// matrix.to, but also for it to enable routing within Riot when clicked.
e.preventDefault();
dis.dispatch({
action: 'view_room',
event_id: this.props.mxEvent.getId(),
highlighted: true,
room_id: this.props.mxEvent.getRoomId(),
});
// Expand thread on shift key
if (this.props.toggleExpandedQuote && e.shiftKey) {
this.props.toggleExpandedQuote();
} else {
dis.dispatch({
action: 'view_room',
event_id: this.props.mxEvent.getId(),
highlighted: true,
room_id: this.props.mxEvent.getRoomId(),
});
}
}
};

View file

@ -164,6 +164,20 @@ export default class SendMessageComposer extends React.Component<IProps> {
window.addEventListener("beforeunload", this.saveStoredEditorState);
}
public componentDidUpdate(prevProps: IProps): void {
const replyToEventChanged = this.props.replyInThread && (this.props.replyToEvent !== prevProps.replyToEvent);
if (replyToEventChanged) {
this.model.reset([]);
}
if (this.props.replyInThread && this.props.replyToEvent && (!prevProps.replyToEvent || replyToEventChanged)) {
const partCreator = new CommandPartCreator(this.props.room, this.context);
const parts = this.restoreStoredEditorState(partCreator) || [];
this.model.reset(parts);
this.editorRef.current?.focus();
}
}
private onKeyDown = (event: KeyboardEvent): void => {
// ignore any keypress while doing IME compositions
if (this.editorRef.current?.isComposing(event)) {
@ -484,7 +498,12 @@ export default class SendMessageComposer extends React.Component<IProps> {
}
private get editorStateKey() {
return `mx_cider_state_${this.props.room.roomId}`;
let key = `mx_cider_state_${this.props.room.roomId}`;
const thread = this.props.replyToEvent?.getThread();
if (thread) {
key += `_${thread.id}`;
}
return key;
}
private clearStoredEditorState(): void {
@ -492,6 +511,10 @@ export default class SendMessageComposer extends React.Component<IProps> {
}
private restoreStoredEditorState(partCreator: PartCreator): Part[] {
if (this.props.replyInThread && !this.props.replyToEvent) {
return null;
}
const json = localStorage.getItem(this.editorStateKey);
if (json) {
try {