Allow quote-reply in thread view element-web (#6959)
parent
d39002338d
commit
694ec946e2
|
@ -779,7 +779,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
});
|
||||
break;
|
||||
case 'reply_to_event':
|
||||
if (this.state.searchResults && payload.event.getRoomId() === this.state.roomId && !this.unmounted) {
|
||||
if (this.state.searchResults
|
||||
&& payload.event.getRoomId() === this.state.roomId
|
||||
&& !this.unmounted
|
||||
&& payload.context === TimelineRenderingType.Room) {
|
||||
this.onCancelSearchClick();
|
||||
}
|
||||
break;
|
||||
|
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
|
||||
import { EventTimelineSet } from 'matrix-js-sdk/src/models/event-timeline-set';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
|
@ -24,7 +23,6 @@ import BaseCard from "../views/right_panel/BaseCard";
|
|||
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
|
||||
|
||||
import ResizeNotifier from '../../utils/ResizeNotifier';
|
||||
import EventTile, { TileShape } from '../views/rooms/EventTile';
|
||||
import MatrixClientContext from '../../contexts/MatrixClientContext';
|
||||
import { _t } from '../../languageHandler';
|
||||
import { ContextMenuButton } from '../../accessibility/context_menu/ContextMenuButton';
|
||||
|
@ -34,6 +32,7 @@ import TimelinePanel from './TimelinePanel';
|
|||
import { Layout } from '../../settings/Layout';
|
||||
import { useEventEmitter } from '../../hooks/useEventEmitter';
|
||||
import AccessibleButton from '../views/elements/AccessibleButton';
|
||||
import { TileShape } from '../views/rooms/EventTile';
|
||||
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
|
@ -41,18 +40,6 @@ interface IProps {
|
|||
resizeNotifier: ResizeNotifier;
|
||||
}
|
||||
|
||||
export const ThreadPanelItem: React.FC<{ event: MatrixEvent }> = ({ event }) => {
|
||||
return <EventTile
|
||||
key={event.getId()}
|
||||
mxEvent={event}
|
||||
enableFlair={false}
|
||||
showReadReceipts={false}
|
||||
as="div"
|
||||
tileShape={TileShape.Thread}
|
||||
alwaysShowTimestamps={true}
|
||||
/>;
|
||||
};
|
||||
|
||||
export enum ThreadFilterType {
|
||||
"My",
|
||||
"All"
|
||||
|
@ -230,7 +217,7 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose }) => {
|
|||
showReactions={true}
|
||||
className="mx_RoomView_messagePanel mx_GroupLayout"
|
||||
membersLoaded={true}
|
||||
tileShape={TileShape.ThreadPanel}
|
||||
tileShape={TileShape.Thread}
|
||||
/>
|
||||
</BaseCard>
|
||||
</RoomContext.Provider>
|
||||
|
|
|
@ -50,6 +50,7 @@ interface IProps {
|
|||
interface IState {
|
||||
thread?: Thread;
|
||||
editState?: EditorStateTransfer;
|
||||
replyToEvent?: MatrixEvent;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.ThreadView")
|
||||
|
@ -114,6 +115,13 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
});
|
||||
break;
|
||||
}
|
||||
case 'reply_to_event':
|
||||
if (payload.context === TimelineRenderingType.Thread) {
|
||||
this.setState({
|
||||
replyToEvent: payload.event,
|
||||
});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -199,7 +207,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
rel_type: RelationType.Thread,
|
||||
event_id: this.state.thread.id,
|
||||
}}
|
||||
showReplyPreview={false}
|
||||
replyToEvent={this.state.replyToEvent}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
e2eStatus={this.props.e2eStatus}
|
||||
compact={true}
|
||||
|
|
|
@ -63,7 +63,7 @@ const OptionsButton: React.FC<IOptionsButtonProps> =
|
|||
let contextMenu;
|
||||
if (menuDisplayed) {
|
||||
const tile = getTile && getTile();
|
||||
const replyThread = getReplyChain && getReplyChain();
|
||||
const replyChain = getReplyChain && getReplyChain();
|
||||
|
||||
const buttonRect = button.current.getBoundingClientRect();
|
||||
contextMenu = <MessageContextMenu
|
||||
|
@ -71,7 +71,7 @@ const OptionsButton: React.FC<IOptionsButtonProps> =
|
|||
mxEvent={mxEvent}
|
||||
permalinkCreator={permalinkCreator}
|
||||
eventTileOps={tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined}
|
||||
collapseReplyChain={replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined}
|
||||
collapseReplyChain={replyChain && replyChain.canCollapse() ? replyChain.collapse : undefined}
|
||||
onFinished={closeMenu}
|
||||
/>;
|
||||
}
|
||||
|
@ -191,6 +191,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
|
|||
dis.dispatch({
|
||||
action: 'reply_to_event',
|
||||
event: this.props.mxEvent,
|
||||
context: this.context.timelineRenderingType,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -289,7 +290,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
|
|||
// Like the resend button, the react and reply buttons need to appear before the edit.
|
||||
// The only catch is we do the reply button first so that we can make sure the react
|
||||
// button is the very first button without having to do length checks for `splice()`.
|
||||
if (this.context.canReply && this.context.timelineRenderingType !== TimelineRenderingType.Thread) {
|
||||
if (this.context.canReply) {
|
||||
toolbarOpts.splice(0, 0, <>
|
||||
<RovingAccessibleTooltipButton
|
||||
className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton"
|
||||
|
@ -297,7 +298,8 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
|
|||
onClick={this.onReplyClick}
|
||||
key="reply"
|
||||
/>
|
||||
{ SettingsStore.getValue("feature_thread") && (
|
||||
{ (SettingsStore.getValue("feature_thread")
|
||||
&& this.context.timelineRenderingType !== TimelineRenderingType.Thread) && (
|
||||
<RovingAccessibleTooltipButton
|
||||
className="mx_MessageActionBar_maskButton mx_MessageActionBar_threadButton"
|
||||
title={_t("Thread")}
|
||||
|
|
|
@ -339,7 +339,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
private isListeningForReceipts: boolean;
|
||||
// TODO: Types
|
||||
private tile = React.createRef<unknown>();
|
||||
private replyThread = React.createRef<ReplyChain>();
|
||||
private replyChain = React.createRef<ReplyChain>();
|
||||
|
||||
public readonly ref = createRef<HTMLElement>();
|
||||
|
||||
|
@ -933,7 +933,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
// TODO: Types
|
||||
getTile: () => any | null = () => this.tile.current;
|
||||
|
||||
getReplyChain = () => this.replyThread.current;
|
||||
getReplyChain = () => this.replyChain.current;
|
||||
|
||||
getReactions = () => {
|
||||
if (
|
||||
|
@ -1214,12 +1214,26 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
]);
|
||||
}
|
||||
case TileShape.Thread: {
|
||||
const thread = haveTileForEvent(this.props.mxEvent) &&
|
||||
ReplyChain.hasReply(this.props.mxEvent) ? (
|
||||
<ReplyChain
|
||||
parentEv={this.props.mxEvent}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
ref={this.replyChain}
|
||||
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 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,
|
||||
"data-has-reply": !!thread,
|
||||
}, [
|
||||
<div className="mx_EventTile_roomName" key="mx_EventTile_roomName">
|
||||
<RoomAvatar room={room} width={28} height={28} />
|
||||
|
@ -1235,6 +1249,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
</a>
|
||||
</div>,
|
||||
<div className="mx_EventTile_line" key="mx_EventTile_line">
|
||||
{ thread }
|
||||
<EventTileType ref={this.tile}
|
||||
mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
|
@ -1287,7 +1302,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
<ReplyChain
|
||||
parentEv={this.props.mxEvent}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
ref={this.replyThread}
|
||||
ref={this.replyChain}
|
||||
forExport={this.props.forExport}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
layout={this.props.layout}
|
||||
|
|
|
@ -55,6 +55,7 @@ import UIStore, { UI_EVENTS } from '../../../stores/UIStore';
|
|||
import Modal from "../../../Modal";
|
||||
import InfoDialog from "../dialogs/InfoDialog";
|
||||
import { RelationType } from 'matrix-js-sdk/src/@types/event';
|
||||
import RoomContext from '../../../contexts/RoomContext';
|
||||
|
||||
let instanceCount = 0;
|
||||
const NARROW_MODE_BREAKPOINT = 500;
|
||||
|
@ -227,7 +228,6 @@ interface IProps {
|
|||
permalinkCreator: RoomPermalinkCreator;
|
||||
replyToEvent?: MatrixEvent;
|
||||
relation?: IEventRelation;
|
||||
showReplyPreview?: boolean;
|
||||
e2eStatus?: E2EStatus;
|
||||
compact?: boolean;
|
||||
}
|
||||
|
@ -252,8 +252,9 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
private ref: React.RefObject<HTMLDivElement> = createRef();
|
||||
private instanceId: number;
|
||||
|
||||
public static contextType = RoomContext;
|
||||
|
||||
static defaultProps = {
|
||||
showReplyPreview: true,
|
||||
compact: false,
|
||||
};
|
||||
|
||||
|
@ -294,7 +295,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
if (payload.action === 'reply_to_event') {
|
||||
if (payload.action === 'reply_to_event' && payload.context === this.context.timelineRenderingType) {
|
||||
// add a timeout for the reply preview to be rendered, so
|
||||
// that the ScrollPanel listening to the resizeNotifier can
|
||||
// correctly measure it's new height and scroll down to keep
|
||||
|
@ -633,9 +634,9 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
<div className={classes} ref={this.ref}>
|
||||
{ recordingTooltip }
|
||||
<div className="mx_MessageComposer_wrapper">
|
||||
{ this.props.showReplyPreview && (
|
||||
<ReplyPreview permalinkCreator={this.props.permalinkCreator} />
|
||||
) }
|
||||
<ReplyPreview
|
||||
replyToEvent={this.props.replyToEvent}
|
||||
permalinkCreator={this.props.permalinkCreator} />
|
||||
<div className="mx_MessageComposer_row">
|
||||
{ controls }
|
||||
{ this.renderButtons(menuPosition) }
|
||||
|
|
|
@ -17,63 +17,31 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import ReplyTile from './ReplyTile';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { EventSubscription } from 'fbemitter';
|
||||
import RoomContext, { TimelineRenderingType } from '../../../contexts/RoomContext';
|
||||
|
||||
function cancelQuoting() {
|
||||
function cancelQuoting(context: TimelineRenderingType) {
|
||||
dis.dispatch({
|
||||
action: 'reply_to_event',
|
||||
event: null,
|
||||
context,
|
||||
});
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
permalinkCreator: RoomPermalinkCreator;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
event: MatrixEvent;
|
||||
replyToEvent: MatrixEvent;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.ReplyPreview")
|
||||
export default class ReplyPreview extends React.Component<IProps, IState> {
|
||||
private unmounted = false;
|
||||
private readonly roomStoreToken: EventSubscription;
|
||||
export default class ReplyPreview extends React.Component<IProps> {
|
||||
public static contextType = RoomContext;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
event: RoomViewStore.getQuotingEvent(),
|
||||
};
|
||||
|
||||
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
|
||||
// Remove RoomStore listener
|
||||
if (this.roomStoreToken) {
|
||||
this.roomStoreToken.remove();
|
||||
}
|
||||
}
|
||||
|
||||
private onRoomViewStoreUpdate = (): void => {
|
||||
if (this.unmounted) return;
|
||||
|
||||
const event = RoomViewStore.getQuotingEvent();
|
||||
if (this.state.event !== event) {
|
||||
this.setState({ event });
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
if (!this.state.event) return null;
|
||||
public render(): JSX.Element {
|
||||
if (!this.props.replyToEvent) return null;
|
||||
|
||||
return <div className="mx_ReplyPreview">
|
||||
<div className="mx_ReplyPreview_section">
|
||||
|
@ -86,13 +54,13 @@ export default class ReplyPreview extends React.Component<IProps, IState> {
|
|||
src={require("../../../../res/img/cancel.svg")}
|
||||
width="18"
|
||||
height="18"
|
||||
onClick={cancelQuoting}
|
||||
onClick={() => cancelQuoting(this.context.timelineRenderingType)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_ReplyPreview_clear" />
|
||||
<div className="mx_ReplyPreview_tile">
|
||||
<ReplyTile
|
||||
mxEvent={this.state.event}
|
||||
mxEvent={this.props.replyToEvent}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -238,6 +238,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
dis.dispatch({
|
||||
action: 'reply_to_event',
|
||||
event: null,
|
||||
context: this.context.timelineRenderingType,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
|
@ -269,6 +270,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
dis.dispatch({
|
||||
action: 'reply_to_event',
|
||||
event: replyEventId ? this.props.room.findEventById(replyEventId) : null,
|
||||
context: this.context.timelineRenderingType,
|
||||
});
|
||||
if (parts) {
|
||||
this.model.reset(parts);
|
||||
|
@ -479,6 +481,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
dis.dispatch({
|
||||
action: 'reply_to_event',
|
||||
event: null,
|
||||
context: this.context.timelineRenderingType,
|
||||
});
|
||||
}
|
||||
dis.dispatch({ action: "message_sent" });
|
||||
|
@ -552,6 +555,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
dis.dispatch({
|
||||
action: 'reply_to_event',
|
||||
event: this.props.room.findEventById(replyEventId),
|
||||
context: this.context.timelineRenderingType,
|
||||
});
|
||||
}
|
||||
return parts;
|
||||
|
@ -583,7 +587,9 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
switch (payload.action) {
|
||||
case 'reply_to_event':
|
||||
case Action.FocusSendMessageComposer:
|
||||
this.editorRef.current?.focus();
|
||||
if (payload.context === this.context.timelineRenderingType) {
|
||||
this.editorRef.current?.focus();
|
||||
}
|
||||
break;
|
||||
case "send_composer_insert":
|
||||
if (payload.userId) {
|
||||
|
|
|
@ -20,11 +20,11 @@ import { IRoomState } from "../components/structures/RoomView";
|
|||
import { Layout } from "../settings/Layout";
|
||||
|
||||
export enum TimelineRenderingType {
|
||||
Room,
|
||||
Thread,
|
||||
ThreadsList,
|
||||
File,
|
||||
Notification,
|
||||
Room = "Room",
|
||||
Thread = "Thread",
|
||||
ThreadsList = "ThreadsList",
|
||||
File = "File",
|
||||
Notification = "Notification",
|
||||
}
|
||||
|
||||
const RoomContext = createContext<IRoomState>({
|
||||
|
|
|
@ -32,6 +32,7 @@ import { retry } from "../utils/promise";
|
|||
import CountlyAnalytics from "../CountlyAnalytics";
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { TimelineRenderingType } from "../contexts/RoomContext";
|
||||
|
||||
const NUM_JOIN_RETRY = 5;
|
||||
|
||||
|
@ -153,16 +154,19 @@ class RoomViewStore extends Store<ActionPayload> {
|
|||
case 'reply_to_event':
|
||||
// If currently viewed room does not match the room in which we wish to reply then change rooms
|
||||
// this can happen when performing a search across all rooms
|
||||
if (payload.event && payload.event.getRoomId() !== this.state.roomId) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: payload.event.getRoomId(),
|
||||
replyingToEvent: payload.event,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
replyingToEvent: payload.event,
|
||||
});
|
||||
if (payload.context === TimelineRenderingType.Room) {
|
||||
if (payload.event
|
||||
&& payload.event.getRoomId() !== this.state.roomId) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: payload.event.getRoomId(),
|
||||
replyingToEvent: payload.event,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
replyingToEvent: payload.event,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'open_room_settings': {
|
||||
|
|
|
@ -216,6 +216,7 @@ describe('<SendMessageComposer/>', () => {
|
|||
expect(spyDispatcher).toHaveBeenCalledWith({
|
||||
action: "reply_to_event",
|
||||
event: mockEvent,
|
||||
context: TimelineRenderingType.Room,
|
||||
});
|
||||
|
||||
// now try with localStorage wiped out
|
||||
|
@ -277,6 +278,7 @@ describe('<SendMessageComposer/>', () => {
|
|||
expect(spyDispatcher).toHaveBeenCalledWith({
|
||||
action: "reply_to_event",
|
||||
event: null,
|
||||
context: TimelineRenderingType.Room,
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toBe("");
|
||||
|
|
Loading…
Reference in New Issue