Merge branch 'develop' into gsouquet/threads-action-bar-19127
This commit is contained in:
commit
e52a33e93c
45 changed files with 668 additions and 468 deletions
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue