Add ability to properly edit messages in Threads. (#6877)
* Fix infinite rerender loop when editing message * Refactor "edit_event" to Action.EditEvent * Make up-arrow edit working in Threads * Properly handle timeline events edit state * Properly traverse messages to be edited * Add MatrixClientContextHOC * Refactor RoomContext to use AppRenderingContext * Typescriptify test Co-authored-by: Germain <germains@element.io>pull/21833/head
parent
5dede230f1
commit
1331e960fa
|
@ -48,6 +48,8 @@ import Spinner from "../views/elements/Spinner";
|
|||
import TileErrorBoundary from '../views/messages/TileErrorBoundary';
|
||||
import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
|
||||
import EditorStateTransfer from "../../utils/EditorStateTransfer";
|
||||
import { logger } from 'matrix-js-sdk/src/logger';
|
||||
import { Action } from '../../dispatcher/actions';
|
||||
|
||||
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||
const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
|
||||
|
@ -287,6 +289,15 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
ghostReadMarkers,
|
||||
});
|
||||
}
|
||||
|
||||
const pendingEditItem = this.pendingEditItem;
|
||||
if (!this.props.editState && this.props.room && pendingEditItem) {
|
||||
defaultDispatcher.dispatch({
|
||||
action: Action.EditEvent,
|
||||
event: this.props.room.findEventById(pendingEditItem),
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private calculateRoomMembersCount = (): void => {
|
||||
|
@ -550,10 +561,14 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
return { nextEvent, nextTile };
|
||||
}
|
||||
|
||||
private get roomHasPendingEdit(): string {
|
||||
return this.props.room && localStorage.getItem(`mx_edit_room_${this.props.room.roomId}`);
|
||||
private get pendingEditItem(): string | undefined {
|
||||
try {
|
||||
return localStorage.getItem(`mx_edit_room_${this.props.room.roomId}_${this.context.timelineRenderingType}`);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private getEventTiles(): ReactNode[] {
|
||||
this.eventNodes = {};
|
||||
|
||||
|
@ -663,13 +678,6 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
if (!this.props.editState && this.roomHasPendingEdit) {
|
||||
defaultDispatcher.dispatch({
|
||||
action: "edit_event",
|
||||
event: this.props.room.findEventById(this.roomHasPendingEdit),
|
||||
});
|
||||
}
|
||||
|
||||
if (grouper) {
|
||||
ret.push(...grouper.getTiles());
|
||||
}
|
||||
|
|
|
@ -48,8 +48,8 @@ import { Layout } from "../../settings/Layout";
|
|||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import RightPanelStore from "../../stores/RightPanelStore";
|
||||
import { haveTileForEvent } from "../views/rooms/EventTile";
|
||||
import RoomContext from "../../contexts/RoomContext";
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||
import MatrixClientContext, { withMatrixClientHOC, MatrixClientProps } from "../../contexts/MatrixClientContext";
|
||||
import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils';
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import { IMatrixClientCreds } from "../../MatrixClientPeg";
|
||||
|
@ -91,6 +91,7 @@ import TopUnreadMessagesBar from "../views/rooms/TopUnreadMessagesBar";
|
|||
import SpaceStore from "../../stores/SpaceStore";
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline';
|
||||
|
||||
const DEBUG = false;
|
||||
let debuglog = function(msg: string) {};
|
||||
|
@ -102,7 +103,7 @@ if (DEBUG) {
|
|||
debuglog = logger.log.bind(console);
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
interface IRoomProps extends MatrixClientProps {
|
||||
threepidInvite: IThreepidInvite;
|
||||
oobData?: IOOBData;
|
||||
|
||||
|
@ -113,7 +114,7 @@ interface IProps {
|
|||
onRegistered?(credentials: IMatrixClientCreds): void;
|
||||
}
|
||||
|
||||
export interface IState {
|
||||
export interface IRoomState {
|
||||
room?: Room;
|
||||
roomId?: string;
|
||||
roomAlias?: string;
|
||||
|
@ -187,10 +188,12 @@ export interface IState {
|
|||
// if it did we don't want the room to be marked as read as soon as it is loaded.
|
||||
wasContextSwitch?: boolean;
|
||||
editState?: EditorStateTransfer;
|
||||
timelineRenderingType: TimelineRenderingType;
|
||||
liveTimeline?: EventTimeline;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.RoomView")
|
||||
export default class RoomView extends React.Component<IProps, IState> {
|
||||
export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
private readonly dispatcherRef: string;
|
||||
private readonly roomStoreToken: EventSubscription;
|
||||
private readonly rightPanelStoreToken: EventSubscription;
|
||||
|
@ -247,6 +250,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
showDisplaynameChanges: true,
|
||||
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
|
||||
dragCounter: 0,
|
||||
timelineRenderingType: TimelineRenderingType.Room,
|
||||
liveTimeline: undefined,
|
||||
};
|
||||
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
|
@ -336,7 +341,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
|
||||
const roomId = RoomViewStore.getRoomId();
|
||||
|
||||
const newState: Pick<IState, any> = {
|
||||
const newState: Pick<IRoomState, any> = {
|
||||
roomId,
|
||||
roomAlias: RoomViewStore.getRoomAlias(),
|
||||
roomLoading: RoomViewStore.isRoomLoading(),
|
||||
|
@ -808,7 +813,9 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
this.onSearchClick();
|
||||
break;
|
||||
|
||||
case "edit_event": {
|
||||
case Action.EditEvent: {
|
||||
// Quit early if we're trying to edit events in wrong rendering context
|
||||
if (payload.timelineRenderingType !== this.state.timelineRenderingType) return;
|
||||
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
|
||||
this.setState({ editState }, () => {
|
||||
if (payload.event) {
|
||||
|
@ -932,6 +939,10 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
this.updateE2EStatus(room);
|
||||
this.updatePermissions(room);
|
||||
this.checkWidgets(room);
|
||||
|
||||
this.setState({
|
||||
liveTimeline: room.getLiveTimeline(),
|
||||
});
|
||||
};
|
||||
|
||||
private async calculateRecommendedVersion(room: Room) {
|
||||
|
@ -2086,3 +2097,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
const RoomViewWithMatrixClient = withMatrixClientHOC(RoomView);
|
||||
export default RoomViewWithMatrixClient;
|
||||
|
|
|
@ -34,6 +34,8 @@ import { SetRightPanelPhasePayload } from '../../dispatcher/payloads/SetRightPan
|
|||
import { Action } from '../../dispatcher/actions';
|
||||
import { MatrixClientPeg } from '../../MatrixClientPeg';
|
||||
import { E2EStatus } from '../../utils/ShieldUtils';
|
||||
import EditorStateTransfer from '../../utils/EditorStateTransfer';
|
||||
import RoomContext, { TimelineRenderingType } from '../../contexts/RoomContext';
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -47,10 +49,14 @@ interface IProps {
|
|||
interface IState {
|
||||
replyToEvent?: MatrixEvent;
|
||||
thread?: Thread;
|
||||
editState?: EditorStateTransfer;
|
||||
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.ThreadView")
|
||||
export default class ThreadView extends React.Component<IProps, IState> {
|
||||
static contextType = RoomContext;
|
||||
|
||||
private dispatcherRef: string;
|
||||
private timelinePanelRef: React.RefObject<TimelinePanel> = React.createRef();
|
||||
|
||||
|
@ -90,6 +96,23 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
this.setupThread(payload.event);
|
||||
}
|
||||
}
|
||||
switch (payload.action) {
|
||||
case Action.EditEvent: {
|
||||
// Quit early if it's not a thread context
|
||||
if (payload.timelineRenderingType !== TimelineRenderingType.Thread) return;
|
||||
// Quit early if that's not a thread event
|
||||
if (payload.event && !payload.event.getThread()) return;
|
||||
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
|
||||
this.setState({ editState }, () => {
|
||||
if (payload.event) {
|
||||
this.timelinePanelRef.current?.scrollToEventIfNeeded(payload.event.getId());
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private setupThread = (mxEv: MatrixEvent) => {
|
||||
|
@ -124,44 +147,53 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<BaseCard
|
||||
className="mx_ThreadView"
|
||||
onClose={this.props.onClose}
|
||||
previousPhase={RightPanelPhases.RoomSummary}
|
||||
withoutScrollContainer={true}
|
||||
>
|
||||
{ this.state.thread && (
|
||||
<TimelinePanel
|
||||
ref={this.timelinePanelRef}
|
||||
showReadReceipts={false} // No RR support in thread's MVP
|
||||
manageReadReceipts={false} // No RR support in thread's MVP
|
||||
manageReadMarkers={false} // No RM support in thread's MVP
|
||||
sendReadReceiptOnLoad={false} // No RR support in thread's MVP
|
||||
timelineSet={this.state?.thread?.timelineSet}
|
||||
showUrlPreview={true}
|
||||
tileShape={TileShape.Thread}
|
||||
empty={<div>empty</div>}
|
||||
alwaysShowTimestamps={true}
|
||||
layout={Layout.Group}
|
||||
hideThreadedMessages={false}
|
||||
hidden={false}
|
||||
showReactions={true}
|
||||
className="mx_RoomView_messagePanel mx_GroupLayout"
|
||||
<RoomContext.Provider value={{
|
||||
...this.context,
|
||||
timelineRenderingType: TimelineRenderingType.Thread,
|
||||
liveTimeline: this.state?.thread?.timelineSet?.getLiveTimeline(),
|
||||
}}>
|
||||
|
||||
<BaseCard
|
||||
className="mx_ThreadView"
|
||||
onClose={this.props.onClose}
|
||||
previousPhase={RightPanelPhases.RoomSummary}
|
||||
withoutScrollContainer={true}
|
||||
>
|
||||
{ this.state.thread && (
|
||||
<TimelinePanel
|
||||
ref={this.timelinePanelRef}
|
||||
showReadReceipts={false} // No RR support in thread's MVP
|
||||
manageReadReceipts={false} // No RR support in thread's MVP
|
||||
manageReadMarkers={false} // No RM support in thread's MVP
|
||||
sendReadReceiptOnLoad={false} // No RR support in thread's MVP
|
||||
timelineSet={this.state?.thread?.timelineSet}
|
||||
showUrlPreview={true}
|
||||
tileShape={TileShape.Thread}
|
||||
empty={<div>empty</div>}
|
||||
alwaysShowTimestamps={true}
|
||||
layout={Layout.Group}
|
||||
hideThreadedMessages={false}
|
||||
hidden={false}
|
||||
showReactions={true}
|
||||
className="mx_RoomView_messagePanel mx_GroupLayout"
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
membersLoaded={true}
|
||||
editState={this.state.editState}
|
||||
/>
|
||||
) }
|
||||
|
||||
{ this.state?.thread?.timelineSet && (<MessageComposer
|
||||
room={this.props.room}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
replyInThread={true}
|
||||
replyToEvent={this.state?.thread?.replyToEvent}
|
||||
showReplyPreview={false}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
membersLoaded={true}
|
||||
/>
|
||||
) }
|
||||
<MessageComposer
|
||||
room={this.props.room}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
replyInThread={true}
|
||||
replyToEvent={this.state?.thread?.replyToEvent}
|
||||
showReplyPreview={false}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
e2eStatus={this.props.e2eStatus}
|
||||
compact={true}
|
||||
/>
|
||||
</BaseCard>
|
||||
e2eStatus={this.props.e2eStatus}
|
||||
compact={true}
|
||||
/>) }
|
||||
</BaseCard>
|
||||
</RoomContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ import { Action } from '../../../dispatcher/actions';
|
|||
import { RightPanelPhases } from '../../../stores/RightPanelStorePhases';
|
||||
import { aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu } from '../../structures/ContextMenu';
|
||||
import { isContentActionable, canEditContent } from '../../../utils/EventUtils';
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||
import Toolbar from "../../../accessibility/Toolbar";
|
||||
import { RovingAccessibleTooltipButton, useRovingTabIndex } from "../../../accessibility/RovingTabIndex";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
@ -128,11 +128,6 @@ const ReactButton: React.FC<IReactButtonProps> = ({ mxEvent, reactions, onFocusC
|
|||
</React.Fragment>;
|
||||
};
|
||||
|
||||
export enum ActionBarRenderingContext {
|
||||
Room,
|
||||
Thread
|
||||
}
|
||||
|
||||
interface IMessageActionBarProps {
|
||||
mxEvent: MatrixEvent;
|
||||
reactions?: Relations;
|
||||
|
@ -142,7 +137,6 @@ interface IMessageActionBarProps {
|
|||
permalinkCreator?: RoomPermalinkCreator;
|
||||
onFocusChange?: (menuDisplayed: boolean) => void;
|
||||
toggleThreadExpanded: () => void;
|
||||
renderingContext?: ActionBarRenderingContext;
|
||||
isQuoteExpanded?: boolean;
|
||||
}
|
||||
|
||||
|
@ -150,10 +144,6 @@ interface IMessageActionBarProps {
|
|||
export default class MessageActionBar extends React.PureComponent<IMessageActionBarProps> {
|
||||
public static contextType = RoomContext;
|
||||
|
||||
public static defaultProps = {
|
||||
renderingContext: ActionBarRenderingContext.Room,
|
||||
};
|
||||
|
||||
public componentDidMount(): void {
|
||||
if (this.props.mxEvent.status && this.props.mxEvent.status !== EventStatus.SENT) {
|
||||
this.props.mxEvent.on("Event.status", this.onSent);
|
||||
|
@ -217,8 +207,9 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
|
|||
|
||||
private onEditClick = (ev: React.MouseEvent): void => {
|
||||
dis.dispatch({
|
||||
action: 'edit_event',
|
||||
action: Action.EditEvent,
|
||||
event: this.props.mxEvent,
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -298,7 +289,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.props.renderingContext === ActionBarRenderingContext.Room) {
|
||||
if (this.context.canReply && this.context.timelineRenderingType === TimelineRenderingType.Room) {
|
||||
toolbarOpts.splice(0, 0, <>
|
||||
<RovingAccessibleTooltipButton
|
||||
className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton"
|
||||
|
|
|
@ -28,7 +28,6 @@ import { parseEvent } from '../../../editor/deserialize';
|
|||
import { CommandPartCreator, Part, PartCreator, Type } from '../../../editor/parts';
|
||||
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
||||
import BasicMessageComposer, { REGEX_EMOTICON } from "./BasicMessageComposer";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { Command, CommandCategories, getCommand } from '../../../SlashCommands';
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
|
@ -46,6 +45,8 @@ import { createRedactEventDialog } from '../dialogs/ConfirmRedactDialog';
|
|||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { withMatrixClientHOC, MatrixClientProps } from '../../../contexts/MatrixClientContext';
|
||||
import RoomContext from '../../../contexts/RoomContext';
|
||||
|
||||
function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
|
||||
const html = mxEvent.getContent().formatted_body;
|
||||
|
@ -108,25 +109,24 @@ function createEditContent(model: EditorModel, editedEvent: MatrixEvent): IConte
|
|||
}, contentBody);
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
interface IEditMessageComposerProps extends MatrixClientProps {
|
||||
editState: EditorStateTransfer;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
saveDisabled: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.EditMessageComposer")
|
||||
export default class EditMessageComposer extends React.Component<IProps, IState> {
|
||||
static contextType = MatrixClientContext;
|
||||
context!: React.ContextType<typeof MatrixClientContext>;
|
||||
class EditMessageComposer extends React.Component<IEditMessageComposerProps, IState> {
|
||||
static contextType = RoomContext;
|
||||
context!: React.ContextType<typeof RoomContext>;
|
||||
|
||||
private readonly editorRef = createRef<BasicMessageComposer>();
|
||||
private readonly dispatcherRef: string;
|
||||
private model: EditorModel = null;
|
||||
|
||||
constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
|
||||
constructor(props: IEditMessageComposerProps, context: React.ContextType<typeof RoomContext>) {
|
||||
super(props);
|
||||
this.context = context; // otherwise React will only set it prior to render due to type def above
|
||||
|
||||
|
@ -141,7 +141,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
}
|
||||
|
||||
private getRoom(): Room {
|
||||
return this.context.getRoom(this.props.editState.getEvent().getRoomId());
|
||||
return this.props.mxClient.getRoom(this.props.editState.getEvent().getRoomId());
|
||||
}
|
||||
|
||||
private onKeyDown = (event: KeyboardEvent): void => {
|
||||
|
@ -162,10 +162,17 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
if (this.editorRef.current?.isModified() || !this.editorRef.current?.isCaretAtStart()) {
|
||||
return;
|
||||
}
|
||||
const previousEvent = findEditableEvent(this.getRoom(), false,
|
||||
this.props.editState.getEvent().getId());
|
||||
const previousEvent = findEditableEvent({
|
||||
events: this.events,
|
||||
isForward: false,
|
||||
fromEventId: this.props.editState.getEvent().getId(),
|
||||
});
|
||||
if (previousEvent) {
|
||||
dis.dispatch({ action: 'edit_event', event: previousEvent });
|
||||
dis.dispatch({
|
||||
action: Action.EditEvent,
|
||||
event: previousEvent,
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
event.preventDefault();
|
||||
}
|
||||
break;
|
||||
|
@ -174,12 +181,24 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
if (this.editorRef.current?.isModified() || !this.editorRef.current?.isCaretAtEnd()) {
|
||||
return;
|
||||
}
|
||||
const nextEvent = findEditableEvent(this.getRoom(), true, this.props.editState.getEvent().getId());
|
||||
const nextEvent = findEditableEvent({
|
||||
events: this.events,
|
||||
isForward: true,
|
||||
fromEventId: this.props.editState.getEvent().getId(),
|
||||
});
|
||||
if (nextEvent) {
|
||||
dis.dispatch({ action: 'edit_event', event: nextEvent });
|
||||
dis.dispatch({
|
||||
action: Action.EditEvent,
|
||||
event: nextEvent,
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
} else {
|
||||
this.clearStoredEditorState();
|
||||
dis.dispatch({ action: 'edit_event', event: null });
|
||||
dis.dispatch({
|
||||
action: Action.EditEvent,
|
||||
event: null,
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
}
|
||||
event.preventDefault();
|
||||
|
@ -189,16 +208,27 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
};
|
||||
|
||||
private get editorRoomKey(): string {
|
||||
return `mx_edit_room_${this.getRoom().roomId}`;
|
||||
return `mx_edit_room_${this.getRoom().roomId}_${this.context.timelineRenderingType}`;
|
||||
}
|
||||
|
||||
private get editorStateKey(): string {
|
||||
return `mx_edit_state_${this.props.editState.getEvent().getId()}`;
|
||||
}
|
||||
|
||||
private get events(): MatrixEvent[] {
|
||||
const liveTimelineEvents = this.context.liveTimeline.getEvents();
|
||||
const pendingEvents = this.getRoom().getPendingEvents();
|
||||
const isInThread = Boolean(this.props.editState.getEvent().getThread());
|
||||
return liveTimelineEvents.concat(isInThread ? [] : pendingEvents);
|
||||
}
|
||||
|
||||
private cancelEdit = (): void => {
|
||||
this.clearStoredEditorState();
|
||||
dis.dispatch({ action: "edit_event", event: null });
|
||||
dis.dispatch({
|
||||
action: Action.EditEvent,
|
||||
event: null,
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
};
|
||||
|
||||
|
@ -381,7 +411,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
}
|
||||
if (shouldSend) {
|
||||
this.cancelPreviousPendingEdit();
|
||||
const prom = this.context.sendMessage(roomId, editContent);
|
||||
const prom = this.props.mxClient.sendMessage(roomId, editContent);
|
||||
this.clearStoredEditorState();
|
||||
dis.dispatch({ action: "message_sent" });
|
||||
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, true, false, editContent);
|
||||
|
@ -389,7 +419,11 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
}
|
||||
|
||||
// close the event editing and focus composer
|
||||
dis.dispatch({ action: "edit_event", event: null });
|
||||
dis.dispatch({
|
||||
action: Action.EditEvent,
|
||||
event: null,
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
};
|
||||
|
||||
|
@ -400,7 +434,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
previousEdit.status === EventStatus.QUEUED ||
|
||||
previousEdit.status === EventStatus.NOT_SENT
|
||||
)) {
|
||||
this.context.cancelPendingEvent(previousEdit);
|
||||
this.props.mxClient.cancelPendingEvent(previousEdit);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -428,7 +462,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
private createEditorModel(): boolean {
|
||||
const { editState } = this.props;
|
||||
const room = this.getRoom();
|
||||
const partCreator = new CommandPartCreator(room, this.context);
|
||||
const partCreator = new CommandPartCreator(room, this.props.mxClient);
|
||||
|
||||
let parts;
|
||||
let isRestored = false;
|
||||
|
@ -493,3 +527,6 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
|||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
const EditMessageComposerWithMatrixClient = withMatrixClientHOC(EditMessageComposer);
|
||||
export default EditMessageComposerWithMatrixClient;
|
||||
|
|
|
@ -53,7 +53,7 @@ import SenderProfile from '../messages/SenderProfile';
|
|||
import MessageTimestamp from '../messages/MessageTimestamp';
|
||||
import TooltipButton from '../elements/TooltipButton';
|
||||
import ReadReceiptMarker from "./ReadReceiptMarker";
|
||||
import MessageActionBar, { ActionBarRenderingContext } from "../messages/MessageActionBar";
|
||||
import MessageActionBar from "../messages/MessageActionBar";
|
||||
import ReactionsRow from '../messages/ReactionsRow';
|
||||
import { getEventDisplayInfo } from '../../../utils/EventUtils';
|
||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||
|
@ -1063,9 +1063,6 @@ 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}
|
||||
|
@ -1073,7 +1070,6 @@ 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;
|
||||
|
@ -1178,6 +1174,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
showUrlPreview={this.props.showUrlPreview}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
tileShape={this.props.tileShape}
|
||||
editState={this.props.editState}
|
||||
/>
|
||||
</div>,
|
||||
]);
|
||||
|
@ -1211,6 +1208,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
showUrlPreview={this.props.showUrlPreview}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
tileShape={this.props.tileShape}
|
||||
editState={this.props.editState}
|
||||
/>
|
||||
{ actionBar }
|
||||
</div>,
|
||||
|
@ -1231,6 +1229,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
showUrlPreview={this.props.showUrlPreview}
|
||||
tileShape={this.props.tileShape}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
editState={this.props.editState}
|
||||
/>
|
||||
</div>,
|
||||
<a
|
||||
|
|
|
@ -45,7 +45,7 @@ import { RecordingState } from "../../../audio/VoiceRecording";
|
|||
import Tooltip, { Alignment } from "../elements/Tooltip";
|
||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||
import { E2EStatus } from '../../../utils/ShieldUtils';
|
||||
import SendMessageComposer from "./SendMessageComposer";
|
||||
import SendMessageComposer, { SendMessageComposer as SendMessageComposerClass } from "./SendMessageComposer";
|
||||
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import EditorModel from "../../../editor/model";
|
||||
|
@ -219,8 +219,8 @@ interface IState {
|
|||
@replaceableComponent("views.rooms.MessageComposer")
|
||||
export default class MessageComposer extends React.Component<IProps, IState> {
|
||||
private dispatcherRef: string;
|
||||
private messageComposerInput: SendMessageComposer;
|
||||
private voiceRecordingButton: VoiceRecordComposerTile;
|
||||
private messageComposerInput = createRef<SendMessageComposerClass>();
|
||||
private voiceRecordingButton = createRef<VoiceRecordComposerTile>();
|
||||
private ref: React.RefObject<HTMLDivElement> = createRef();
|
||||
private instanceId: number;
|
||||
|
||||
|
@ -378,14 +378,14 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
private sendMessage = async () => {
|
||||
if (this.state.haveRecording && this.voiceRecordingButton) {
|
||||
if (this.state.haveRecording && this.voiceRecordingButton.current) {
|
||||
// There shouldn't be any text message to send when a voice recording is active, so
|
||||
// just send out the voice recording.
|
||||
await this.voiceRecordingButton.send();
|
||||
await this.voiceRecordingButton.current?.send();
|
||||
return;
|
||||
}
|
||||
|
||||
this.messageComposerInput.sendMessage();
|
||||
this.messageComposerInput.current?.sendMessage();
|
||||
};
|
||||
|
||||
private onChange = (model: EditorModel) => {
|
||||
|
@ -460,7 +460,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
buttons.push(
|
||||
<AccessibleTooltipButton
|
||||
className="mx_MessageComposer_button mx_MessageComposer_voiceMessage"
|
||||
onClick={() => this.voiceRecordingButton?.onRecordStartEndClick()}
|
||||
onClick={() => this.voiceRecordingButton.current?.onRecordStartEndClick()}
|
||||
title={_t("Send voice message")}
|
||||
/>,
|
||||
);
|
||||
|
@ -521,7 +521,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
if (!this.state.tombstone && this.state.canSendMessages) {
|
||||
controls.push(
|
||||
<SendMessageComposer
|
||||
ref={(c) => this.messageComposerInput = c}
|
||||
ref={this.messageComposerInput}
|
||||
key="controls_input"
|
||||
room={this.props.room}
|
||||
placeholder={this.renderPlaceholderText()}
|
||||
|
@ -535,7 +535,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
|
||||
controls.push(<VoiceRecordComposerTile
|
||||
key="controls_voice_record"
|
||||
ref={c => this.voiceRecordingButton = c}
|
||||
ref={this.voiceRecordingButton}
|
||||
room={this.props.room} />);
|
||||
} else if (this.state.tombstone) {
|
||||
const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];
|
||||
|
|
|
@ -19,6 +19,7 @@ import EMOJI_REGEX from 'emojibase-regex';
|
|||
import { IContent, MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { DebouncedFunc, throttle } from 'lodash';
|
||||
import { EventType, RelationType } from "matrix-js-sdk/src/@types/event";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import EditorModel from '../../../editor/model';
|
||||
|
@ -40,7 +41,7 @@ import { Command, CommandCategories, getCommand } from '../../../SlashCommands';
|
|||
import Modal from '../../../Modal';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import ContentMessages from '../../../ContentMessages';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { withMatrixClientHOC, MatrixClientProps } from "../../../contexts/MatrixClientContext";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { containsEmoji } from "../../../effects/utils";
|
||||
import { CHAT_EFFECTS } from '../../../effects';
|
||||
|
@ -55,8 +56,7 @@ import ErrorDialog from "../dialogs/ErrorDialog";
|
|||
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import { decorateStartSendingTime, sendRoundTripMetric } from "../../../sendTimePerformanceMetrics";
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import RoomContext from '../../../contexts/RoomContext';
|
||||
|
||||
function addReplyToMessageContent(
|
||||
content: IContent,
|
||||
|
@ -130,7 +130,7 @@ export function isQuickReaction(model: EditorModel): boolean {
|
|||
return false;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
interface ISendMessageComposerProps extends MatrixClientProps {
|
||||
room: Room;
|
||||
placeholder?: string;
|
||||
permalinkCreator: RoomPermalinkCreator;
|
||||
|
@ -141,10 +141,8 @@ interface IProps {
|
|||
}
|
||||
|
||||
@replaceableComponent("views.rooms.SendMessageComposer")
|
||||
export default class SendMessageComposer extends React.Component<IProps> {
|
||||
static contextType = MatrixClientContext;
|
||||
context!: React.ContextType<typeof MatrixClientContext>;
|
||||
|
||||
export class SendMessageComposer extends React.Component<ISendMessageComposerProps> {
|
||||
static contextType = RoomContext;
|
||||
private readonly prepareToEncrypt?: DebouncedFunc<() => void>;
|
||||
private readonly editorRef = createRef<BasicMessageComposer>();
|
||||
private model: EditorModel = null;
|
||||
|
@ -152,26 +150,25 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
private dispatcherRef: string;
|
||||
private sendHistoryManager: SendHistoryManager;
|
||||
|
||||
constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
|
||||
constructor(props: ISendMessageComposerProps, context: React.ContextType<typeof RoomContext>) {
|
||||
super(props);
|
||||
this.context = context; // otherwise React will only set it prior to render due to type def above
|
||||
if (this.context.isCryptoEnabled() && this.context.isRoomEncrypted(this.props.room.roomId)) {
|
||||
if (this.props.mxClient.isCryptoEnabled() && this.props.mxClient.isRoomEncrypted(this.props.room.roomId)) {
|
||||
this.prepareToEncrypt = throttle(() => {
|
||||
this.context.prepareToEncrypt(this.props.room);
|
||||
this.props.mxClient.prepareToEncrypt(this.props.room);
|
||||
}, 60000, { leading: true, trailing: false });
|
||||
}
|
||||
|
||||
window.addEventListener("beforeunload", this.saveStoredEditorState);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: IProps): void {
|
||||
public componentDidUpdate(prevProps: ISendMessageComposerProps): 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 partCreator = new CommandPartCreator(this.props.room, this.props.mxClient);
|
||||
const parts = this.restoreStoredEditorState(partCreator) || [];
|
||||
this.model.reset(parts);
|
||||
this.editorRef.current?.focus();
|
||||
|
@ -202,13 +199,20 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
case MessageComposerAction.EditPrevMessage:
|
||||
// selection must be collapsed and caret at start
|
||||
if (this.editorRef.current?.isSelectionCollapsed() && this.editorRef.current?.isCaretAtStart()) {
|
||||
const editEvent = findEditableEvent(this.props.room, false);
|
||||
const events =
|
||||
this.context.liveTimeline.getEvents()
|
||||
.concat(this.props.replyInThread ? [] : this.props.room.getPendingEvents());
|
||||
const editEvent = findEditableEvent({
|
||||
events,
|
||||
isForward: false,
|
||||
});
|
||||
if (editEvent) {
|
||||
// We're selecting history, so prevent the key event from doing anything else
|
||||
event.preventDefault();
|
||||
dis.dispatch({
|
||||
action: 'edit_event',
|
||||
action: Action.EditEvent,
|
||||
event: editEvent,
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -275,7 +279,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
}
|
||||
|
||||
private sendQuickReaction(): void {
|
||||
const timeline = this.props.room.getLiveTimeline();
|
||||
const timeline = this.context.liveTimeline();
|
||||
const events = timeline.getEvents();
|
||||
const reaction = this.model.parts[1].text;
|
||||
for (let i = events.length - 1; i >= 0; i--) {
|
||||
|
@ -448,7 +452,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
decorateStartSendingTime(content);
|
||||
}
|
||||
|
||||
const prom = this.context.sendMessage(roomId, content);
|
||||
const prom = this.props.mxClient.sendMessage(roomId, content);
|
||||
if (replyToEvent) {
|
||||
// Clear reply_to_event as we put the message into the queue
|
||||
// if the send fails, retry will handle resending.
|
||||
|
@ -465,7 +469,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
});
|
||||
if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) {
|
||||
prom.then(resp => {
|
||||
sendRoundTripMetric(this.context, roomId, resp.event_id);
|
||||
sendRoundTripMetric(this.props.mxClient, roomId, resp.event_id);
|
||||
});
|
||||
}
|
||||
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, !!replyToEvent, content);
|
||||
|
@ -490,7 +494,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
|
||||
// TODO: [REACT-WARNING] Move this to constructor
|
||||
UNSAFE_componentWillMount() { // eslint-disable-line
|
||||
const partCreator = new CommandPartCreator(this.props.room, this.context);
|
||||
const partCreator = new CommandPartCreator(this.props.room, this.props.mxClient);
|
||||
const parts = this.restoreStoredEditorState(partCreator) || [];
|
||||
this.model = new EditorModel(parts, partCreator);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
|
@ -577,7 +581,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
// it puts the filename in as text/plain which we want to ignore.
|
||||
if (clipboardData.files.length && !clipboardData.types.includes("text/rtf")) {
|
||||
ContentMessages.sharedInstance().sendContentListToRoom(
|
||||
Array.from(clipboardData.files), this.props.room.roomId, this.context,
|
||||
Array.from(clipboardData.files), this.props.room.roomId, this.props.mxClient,
|
||||
);
|
||||
return true; // to skip internal onPaste handler
|
||||
}
|
||||
|
@ -608,3 +612,6 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
const SendMessageComposerWithMatrixClient = withMatrixClientHOC(SendMessageComposer);
|
||||
export default SendMessageComposerWithMatrixClient;
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
/*
|
||||
Copyright 2019 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.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { createContext } from "react";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
|
||||
const MatrixClientContext = createContext<MatrixClient>(undefined);
|
||||
MatrixClientContext.displayName = "MatrixClientContext";
|
||||
export default MatrixClientContext;
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
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.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ComponentClass, createContext, forwardRef, useContext } from "react";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
|
||||
const MatrixClientContext = createContext<MatrixClient>(undefined);
|
||||
MatrixClientContext.displayName = "MatrixClientContext";
|
||||
export default MatrixClientContext;
|
||||
|
||||
export interface MatrixClientProps {
|
||||
mxClient: MatrixClient;
|
||||
}
|
||||
|
||||
const matrixHOC = <ComposedComponentProps extends {}>(
|
||||
ComposedComponent: ComponentClass<ComposedComponentProps>,
|
||||
) => {
|
||||
type ComposedComponentInstance = InstanceType<typeof ComposedComponent>;
|
||||
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
|
||||
const TypedComponent = ComposedComponent;
|
||||
|
||||
return forwardRef<ComposedComponentInstance, Omit<ComposedComponentProps, 'mxClient'>>(
|
||||
(props, ref) => {
|
||||
const client = useContext(MatrixClientContext);
|
||||
|
||||
// @ts-ignore
|
||||
return <TypedComponent ref={ref} {...props} mxClient={client} />;
|
||||
},
|
||||
);
|
||||
};
|
||||
export const withMatrixClientHOC = matrixHOC;
|
|
@ -16,10 +16,15 @@ limitations under the License.
|
|||
|
||||
import { createContext } from "react";
|
||||
|
||||
import { IState } from "../components/structures/RoomView";
|
||||
import { IRoomState } from "../components/structures/RoomView";
|
||||
import { Layout } from "../settings/Layout";
|
||||
|
||||
const RoomContext = createContext<IState>({
|
||||
export enum TimelineRenderingType {
|
||||
Room,
|
||||
Thread
|
||||
}
|
||||
|
||||
const RoomContext = createContext<IRoomState>({
|
||||
roomLoading: true,
|
||||
peekLoading: false,
|
||||
shouldPeek: true,
|
||||
|
@ -53,6 +58,8 @@ const RoomContext = createContext<IState>({
|
|||
showDisplaynameChanges: true,
|
||||
matrixClientIsReady: false,
|
||||
dragCounter: 0,
|
||||
timelineRenderingType: TimelineRenderingType.Room,
|
||||
liveTimeline: undefined,
|
||||
});
|
||||
RoomContext.displayName = "RoomContext";
|
||||
export default RoomContext;
|
||||
|
|
|
@ -128,7 +128,7 @@ export enum Action {
|
|||
* Start a call transfer to a phone number
|
||||
* payload: TransferCallPayload
|
||||
*/
|
||||
TransferCallToPhoneNumber = "transfer_call_to_phone_number",
|
||||
TransferCallToPhoneNumber = "transfer_call_to_phone_number",
|
||||
|
||||
/**
|
||||
* Fired when CallHandler has checked for PSTN protocol support
|
||||
|
@ -205,4 +205,9 @@ export enum Action {
|
|||
* Should be used with SettingUpdatedPayload.
|
||||
*/
|
||||
SettingUpdated = "setting_updated",
|
||||
|
||||
/**
|
||||
* Fires when a user starts to edit event (e.g. up arrow in compositor)
|
||||
*/
|
||||
EditEvent = "edit_event",
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import { IState } from "./components/structures/RoomView";
|
||||
import { IRoomState } from "./components/structures/RoomView";
|
||||
|
||||
interface IDiff {
|
||||
isMemberEvent: boolean;
|
||||
|
@ -54,7 +54,7 @@ function memberEventDiff(ev: MatrixEvent): IDiff {
|
|||
* @param ctx An optional RoomContext to pull cached settings values from to avoid
|
||||
* hitting the settings store
|
||||
*/
|
||||
export default function shouldHideEvent(ev: MatrixEvent, ctx?: IState): boolean {
|
||||
export default function shouldHideEvent(ev: MatrixEvent, ctx?: IRoomState): boolean {
|
||||
// Accessing the settings store directly can be expensive if done frequently,
|
||||
// so we should prefer using cached values if a RoomContext is available
|
||||
const isEnabled = ctx ?
|
||||
|
|
|
@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { MatrixEvent, EventStatus } from 'matrix-js-sdk/src/models/event';
|
||||
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
|
@ -73,9 +72,15 @@ export function canEditOwnEvent(mxEvent: MatrixEvent): boolean {
|
|||
}
|
||||
|
||||
const MAX_JUMP_DISTANCE = 100;
|
||||
export function findEditableEvent(room: Room, isForward: boolean, fromEventId: string = undefined): MatrixEvent {
|
||||
const liveTimeline = room.getLiveTimeline();
|
||||
const events = liveTimeline.getEvents().concat(room.getPendingEvents());
|
||||
export function findEditableEvent({
|
||||
events,
|
||||
isForward,
|
||||
fromEventId,
|
||||
}: {
|
||||
events: MatrixEvent[];
|
||||
isForward: boolean;
|
||||
fromEventId?: string;
|
||||
}): MatrixEvent {
|
||||
const maxIdx = events.length - 1;
|
||||
const inc = isForward ? 1 : -1;
|
||||
const beginIdx = isForward ? 0 : maxIdx;
|
||||
|
|
|
@ -24,8 +24,10 @@ import { sleep } from "matrix-js-sdk/src/utils";
|
|||
import SendMessageComposer, {
|
||||
createMessageContent,
|
||||
isQuickReaction,
|
||||
SendMessageComposer as SendMessageComposerClass,
|
||||
} from "../../../../src/components/views/rooms/SendMessageComposer";
|
||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||
import RoomContext, { TimelineRenderingType } from "../../../../src/contexts/RoomContext";
|
||||
import EditorModel from "../../../../src/editor/model";
|
||||
import { createPartCreator, createRenderer } from "../../../editor/mock";
|
||||
import { createTestClient, mkEvent, mkStubRoom } from "../../../test-utils";
|
||||
|
@ -33,18 +35,58 @@ import BasicMessageComposer from "../../../../src/components/views/rooms/BasicMe
|
|||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import SpecPermalinkConstructor from "../../../../src/utils/permalinks/SpecPermalinkConstructor";
|
||||
import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
|
||||
import DocumentOffset from '../../../../src/editor/offset';
|
||||
import { Layout } from '../../../../src/settings/Layout';
|
||||
|
||||
jest.mock("../../../../src/stores/RoomViewStore");
|
||||
|
||||
configure({ adapter: new Adapter() });
|
||||
|
||||
describe('<SendMessageComposer/>', () => {
|
||||
const roomContext = {
|
||||
roomLoading: true,
|
||||
peekLoading: false,
|
||||
shouldPeek: true,
|
||||
membersLoaded: false,
|
||||
numUnreadMessages: 0,
|
||||
draggingFile: false,
|
||||
searching: false,
|
||||
guestsCanJoin: false,
|
||||
canPeek: false,
|
||||
showApps: false,
|
||||
isPeeking: false,
|
||||
showRightPanel: true,
|
||||
joining: false,
|
||||
atEndOfLiveTimeline: true,
|
||||
atEndOfLiveTimelineInit: false,
|
||||
showTopUnreadMessagesBar: false,
|
||||
statusBarVisible: false,
|
||||
canReact: false,
|
||||
canReply: false,
|
||||
layout: Layout.Group,
|
||||
lowBandwidth: false,
|
||||
alwaysShowTimestamps: false,
|
||||
showTwelveHourTimestamps: false,
|
||||
readMarkerInViewThresholdMs: 3000,
|
||||
readMarkerOutOfViewThresholdMs: 30000,
|
||||
showHiddenEventsInTimeline: false,
|
||||
showReadReceipts: true,
|
||||
showRedactions: true,
|
||||
showJoinLeaves: true,
|
||||
showAvatarChanges: true,
|
||||
showDisplaynameChanges: true,
|
||||
matrixClientIsReady: false,
|
||||
dragCounter: 0,
|
||||
timelineRenderingType: TimelineRenderingType.Room,
|
||||
liveTimeline: undefined,
|
||||
};
|
||||
describe("createMessageContent", () => {
|
||||
const permalinkCreator = jest.fn();
|
||||
const permalinkCreator = jest.fn() as any;
|
||||
|
||||
it("sends plaintext messages correctly", () => {
|
||||
const model = new EditorModel([], createPartCreator(), createRenderer());
|
||||
model.update("hello world", "insertText", { offset: 11, atNodeEnd: true });
|
||||
const documentOffset = new DocumentOffset(11, true);
|
||||
model.update("hello world", "insertText", documentOffset);
|
||||
|
||||
const content = createMessageContent(model, null, false, permalinkCreator);
|
||||
|
||||
|
@ -56,7 +98,8 @@ describe('<SendMessageComposer/>', () => {
|
|||
|
||||
it("sends markdown messages correctly", () => {
|
||||
const model = new EditorModel([], createPartCreator(), createRenderer());
|
||||
model.update("hello *world*", "insertText", { offset: 13, atNodeEnd: true });
|
||||
const documentOffset = new DocumentOffset(13, true);
|
||||
model.update("hello *world*", "insertText", documentOffset);
|
||||
|
||||
const content = createMessageContent(model, null, false, permalinkCreator);
|
||||
|
||||
|
@ -70,7 +113,8 @@ describe('<SendMessageComposer/>', () => {
|
|||
|
||||
it("strips /me from messages and marks them as m.emote accordingly", () => {
|
||||
const model = new EditorModel([], createPartCreator(), createRenderer());
|
||||
model.update("/me blinks __quickly__", "insertText", { offset: 22, atNodeEnd: true });
|
||||
const documentOffset = new DocumentOffset(22, true);
|
||||
model.update("/me blinks __quickly__", "insertText", documentOffset);
|
||||
|
||||
const content = createMessageContent(model, null, false, permalinkCreator);
|
||||
|
||||
|
@ -84,7 +128,9 @@ describe('<SendMessageComposer/>', () => {
|
|||
|
||||
it("allows sending double-slash escaped slash commands correctly", () => {
|
||||
const model = new EditorModel([], createPartCreator(), createRenderer());
|
||||
model.update("//dev/null is my favourite place", "insertText", { offset: 32, atNodeEnd: true });
|
||||
const documentOffset = new DocumentOffset(32, true);
|
||||
|
||||
model.update("//dev/null is my favourite place", "insertText", documentOffset);
|
||||
|
||||
const content = createMessageContent(model, null, false, permalinkCreator);
|
||||
|
||||
|
@ -97,9 +143,11 @@ describe('<SendMessageComposer/>', () => {
|
|||
|
||||
describe("functions correctly mounted", () => {
|
||||
const mockClient = MatrixClientPeg.matrixClient = createTestClient();
|
||||
const mockRoom = mkStubRoom();
|
||||
const mockRoom = mkStubRoom('myfakeroom') as any;
|
||||
const mockEvent = mkEvent({
|
||||
type: "m.room.message",
|
||||
room: 'myfakeroom',
|
||||
user: 'myfakeuser',
|
||||
content: "Replying to this",
|
||||
event: true,
|
||||
});
|
||||
|
@ -116,11 +164,13 @@ describe('<SendMessageComposer/>', () => {
|
|||
|
||||
it("renders text and placeholder correctly", () => {
|
||||
const wrapper = mount(<MatrixClientContext.Provider value={mockClient}>
|
||||
<SendMessageComposer
|
||||
room={mockRoom}
|
||||
placeholder="placeholder string"
|
||||
permalinkCreator={new SpecPermalinkConstructor()}
|
||||
/>
|
||||
<RoomContext.Provider value={roomContext}>
|
||||
<SendMessageComposer
|
||||
room={mockRoom as any}
|
||||
placeholder="placeholder string"
|
||||
permalinkCreator={new SpecPermalinkConstructor() as any}
|
||||
/>
|
||||
</RoomContext.Provider>
|
||||
</MatrixClientContext.Provider>);
|
||||
|
||||
expect(wrapper.find('[aria-label="placeholder string"]')).toHaveLength(1);
|
||||
|
@ -135,12 +185,15 @@ describe('<SendMessageComposer/>', () => {
|
|||
|
||||
it("correctly persists state to and from localStorage", () => {
|
||||
const wrapper = mount(<MatrixClientContext.Provider value={mockClient}>
|
||||
<SendMessageComposer
|
||||
room={mockRoom}
|
||||
placeholder=""
|
||||
permalinkCreator={new SpecPermalinkConstructor()}
|
||||
replyToEvent={mockEvent}
|
||||
/>
|
||||
<RoomContext.Provider value={roomContext}>
|
||||
|
||||
<SendMessageComposer
|
||||
room={mockRoom as any}
|
||||
placeholder=""
|
||||
permalinkCreator={new SpecPermalinkConstructor() as any}
|
||||
replyToEvent={mockEvent}
|
||||
/>
|
||||
</RoomContext.Provider>
|
||||
</MatrixClientContext.Provider>);
|
||||
|
||||
act(() => {
|
||||
|
@ -148,7 +201,7 @@ describe('<SendMessageComposer/>', () => {
|
|||
wrapper.update();
|
||||
});
|
||||
|
||||
const key = wrapper.find(SendMessageComposer).instance().editorStateKey;
|
||||
const key = wrapper.find(SendMessageComposerClass).instance().editorStateKey;
|
||||
|
||||
expect(wrapper.text()).toBe("Test Text");
|
||||
expect(localStorage.getItem(key)).toBeNull();
|
||||
|
@ -177,11 +230,14 @@ describe('<SendMessageComposer/>', () => {
|
|||
|
||||
it("persists state correctly without replyToEvent onbeforeunload", () => {
|
||||
const wrapper = mount(<MatrixClientContext.Provider value={mockClient}>
|
||||
<SendMessageComposer
|
||||
room={mockRoom}
|
||||
placeholder=""
|
||||
permalinkCreator={new SpecPermalinkConstructor()}
|
||||
/>
|
||||
<RoomContext.Provider value={roomContext}>
|
||||
|
||||
<SendMessageComposer
|
||||
room={mockRoom as any}
|
||||
placeholder=""
|
||||
permalinkCreator={new SpecPermalinkConstructor() as any}
|
||||
/>
|
||||
</RoomContext.Provider>
|
||||
</MatrixClientContext.Provider>);
|
||||
|
||||
act(() => {
|
||||
|
@ -189,7 +245,7 @@ describe('<SendMessageComposer/>', () => {
|
|||
wrapper.update();
|
||||
});
|
||||
|
||||
const key = wrapper.find(SendMessageComposer).instance().editorStateKey;
|
||||
const key = wrapper.find(SendMessageComposerClass).instance().editorStateKey;
|
||||
|
||||
expect(wrapper.text()).toBe("Hello World");
|
||||
expect(localStorage.getItem(key)).toBeNull();
|
||||
|
@ -203,12 +259,15 @@ describe('<SendMessageComposer/>', () => {
|
|||
|
||||
it("persists to session history upon sending", async () => {
|
||||
const wrapper = mount(<MatrixClientContext.Provider value={mockClient}>
|
||||
<SendMessageComposer
|
||||
room={mockRoom}
|
||||
placeholder="placeholder"
|
||||
permalinkCreator={new SpecPermalinkConstructor()}
|
||||
replyToEvent={mockEvent}
|
||||
/>
|
||||
<RoomContext.Provider value={roomContext}>
|
||||
|
||||
<SendMessageComposer
|
||||
room={mockRoom as any}
|
||||
placeholder="placeholder"
|
||||
permalinkCreator={new SpecPermalinkConstructor() as any}
|
||||
replyToEvent={mockEvent}
|
||||
/>
|
||||
</RoomContext.Provider>
|
||||
</MatrixClientContext.Provider>);
|
||||
|
||||
act(() => {
|
||||
|
@ -230,12 +289,38 @@ describe('<SendMessageComposer/>', () => {
|
|||
replyEventId: mockEvent.getId(),
|
||||
});
|
||||
});
|
||||
|
||||
it('correctly sets the editorStateKey for threads', () => {
|
||||
const mockThread ={
|
||||
getThread: () => {
|
||||
return {
|
||||
id: 'myFakeThreadId',
|
||||
};
|
||||
},
|
||||
} as any;
|
||||
const wrapper = mount(<MatrixClientContext.Provider value={mockClient}>
|
||||
<RoomContext.Provider value={roomContext}>
|
||||
|
||||
<SendMessageComposer
|
||||
room={mockRoom as any}
|
||||
placeholder=""
|
||||
permalinkCreator={new SpecPermalinkConstructor() as any}
|
||||
replyToEvent={mockThread}
|
||||
/>
|
||||
</RoomContext.Provider>
|
||||
</MatrixClientContext.Provider>);
|
||||
|
||||
const instance = wrapper.find(SendMessageComposerClass).instance();
|
||||
const key = instance.editorStateKey;
|
||||
|
||||
expect(key).toEqual('mx_cider_state_myfakeroom_myFakeThreadId');
|
||||
});
|
||||
});
|
||||
|
||||
describe("isQuickReaction", () => {
|
||||
it("correctly detects quick reaction", () => {
|
||||
const model = new EditorModel([], createPartCreator(), createRenderer());
|
||||
model.update("+😊", "insertText", { offset: 3, atNodeEnd: true });
|
||||
model.update("+😊", "insertText", new DocumentOffset(3, true));
|
||||
|
||||
const isReaction = isQuickReaction(model);
|
||||
|
||||
|
@ -244,7 +329,7 @@ describe('<SendMessageComposer/>', () => {
|
|||
|
||||
it("correctly detects quick reaction with space", () => {
|
||||
const model = new EditorModel([], createPartCreator(), createRenderer());
|
||||
model.update("+ 😊", "insertText", { offset: 4, atNodeEnd: true });
|
||||
model.update("+ 😊", "insertText", new DocumentOffset(4, true));
|
||||
|
||||
const isReaction = isQuickReaction(model);
|
||||
|
||||
|
@ -256,10 +341,10 @@ describe('<SendMessageComposer/>', () => {
|
|||
const model2 = new EditorModel([], createPartCreator(), createRenderer());
|
||||
const model3 = new EditorModel([], createPartCreator(), createRenderer());
|
||||
const model4 = new EditorModel([], createPartCreator(), createRenderer());
|
||||
model.update("+😊hello", "insertText", { offset: 8, atNodeEnd: true });
|
||||
model2.update(" +😊", "insertText", { offset: 4, atNodeEnd: true });
|
||||
model3.update("+ 😊😊", "insertText", { offset: 6, atNodeEnd: true });
|
||||
model4.update("+smiley", "insertText", { offset: 7, atNodeEnd: true });
|
||||
model.update("+😊hello", "insertText", new DocumentOffset( 8, true));
|
||||
model2.update(" +😊", "insertText", new DocumentOffset( 4, true));
|
||||
model3.update("+ 😊😊", "insertText", new DocumentOffset( 6, true));
|
||||
model4.update("+smiley", "insertText", new DocumentOffset( 7, true));
|
||||
|
||||
expect(isQuickReaction(model)).toBeFalsy();
|
||||
expect(isQuickReaction(model2)).toBeFalsy();
|
Loading…
Reference in New Issue