Merge branch 'develop' into export-conversations

This commit is contained in:
Jaiwanth 2021-09-29 19:32:05 +05:30
commit 208b914cb0
62 changed files with 936 additions and 678 deletions

View file

@ -317,7 +317,7 @@ const PersistentVResizer: React.FC<IPersistentResizerProps> = ({
});
return <Resizable
size={{ height: Math.min(height, maxHeight), width: null }}
size={{ height: Math.min(height, maxHeight), width: undefined }}
minHeight={minHeight}
maxHeight={maxHeight}
onResizeStart={() => {

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

@ -42,6 +42,7 @@ import ErrorDialog from "../dialogs/ErrorDialog";
import QuestionDialog from "../dialogs/QuestionDialog";
import { ActionPayload } from "../../../dispatcher/payloads";
import AccessibleButton from '../elements/AccessibleButton';
import { createRedactEventDialog } from '../dialogs/ConfirmRedactDialog';
import SettingsStore from "../../../settings/SettingsStore";
import { logger } from "matrix-js-sdk/src/logger";
@ -331,6 +332,14 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
let shouldSend = true;
if (newContent?.body === '') {
this.cancelPreviousPendingEdit();
createRedactEventDialog({
mxEvent: editedEvent,
});
return;
}
// If content is modified then send an updated event into the room
if (this.isContentModified(newContent)) {
const roomId = editedEvent.getRoomId();

View file

@ -53,11 +53,12 @@ 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 MessageActionBar, { ActionBarRenderingContext } from "../messages/MessageActionBar";
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;
}
}
@ -192,6 +192,7 @@ export enum TileShape {
Notif = "notif",
FileGrid = "file_grid",
Pinned = "pinned",
Thread = "thread",
}
interface IProps {
@ -324,7 +325,7 @@ interface IState {
reactions: Relations;
hover: boolean;
isQuoteExpanded?: boolean;
thread?: Thread;
}
@ -332,7 +333,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>();
@ -894,8 +896,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;
@ -920,6 +922,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;
@ -929,6 +936,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
@ -941,6 +949,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);
@ -1054,6 +1063,9 @@ export default class EventTile extends React.Component<IProps, IState> {
}
const showMessageActionBar = !isEditing && !this.props.forExport;
const renderingContext = this.props.tileShape === TileShape.Thread
? ActionBarRenderingContext.Thread
: ActionBarRenderingContext.Room;
const actionBar = showMessageActionBar ? <MessageActionBar
mxEvent={this.props.mxEvent}
reactions={this.state.reactions}
@ -1061,6 +1073,9 @@ export default class EventTile extends React.Component<IProps, IState> {
getTile={this.getTile}
getReplyThread={this.getReplyThread}
onFocusChange={this.onActionBarFocusChange}
renderingContext={renderingContext}
isQuoteExpanded={isQuoteExpanded}
toggleThreadExpanded={() => this.setQuoteExpanded(!isQuoteExpanded)}
/> : undefined;
const showTimestamp = this.props.mxEvent.getTs()
@ -1167,6 +1182,40 @@ export default class EventTile extends React.Component<IProps, IState> {
</div>,
]);
}
case TileShape.Thread: {
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
return React.createElement(this.props.as || "li", {
"className": classes,
"aria-live": ariaLive,
"aria-atomic": true,
"data-scroll-tokens": scrollToken,
}, [
<div className="mx_EventTile_roomName" key="mx_EventTile_roomName">
<RoomAvatar room={room} width={28} height={28} />
<a href={permalink} onClick={this.onPermalinkClicked}>
{ room ? room.name : '' }
</a>
</div>,
<div className="mx_EventTile_senderDetails" key="mx_EventTile_senderDetails">
{ avatar }
<a href={permalink} onClick={this.onPermalinkClicked}>
{ sender }
{ timestamp }
</a>
</div>,
<div className="mx_EventTile_line" key="mx_EventTile_line">
<EventTileType ref={this.tile}
mxEvent={this.props.mxEvent}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}
onHeightChanged={this.props.onHeightChanged}
tileShape={this.props.tileShape}
/>
{ actionBar }
</div>,
]);
}
case TileShape.FileGrid: {
return React.createElement(this.props.as || "li", {
"className": classes,
@ -1199,21 +1248,19 @@ 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.forExport,
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}
forExport={this.props.forExport}
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 {