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>
This commit is contained in:
parent
5dede230f1
commit
1331e960fa
15 changed files with 403 additions and 189 deletions
|
@ -48,6 +48,8 @@ import Spinner from "../views/elements/Spinner";
|
||||||
import TileErrorBoundary from '../views/messages/TileErrorBoundary';
|
import TileErrorBoundary from '../views/messages/TileErrorBoundary';
|
||||||
import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
|
import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
|
||||||
import EditorStateTransfer from "../../utils/EditorStateTransfer";
|
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 CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||||
const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
|
const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
|
||||||
|
@ -287,6 +289,15 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
ghostReadMarkers,
|
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 => {
|
private calculateRoomMembersCount = (): void => {
|
||||||
|
@ -550,10 +561,14 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
return { nextEvent, nextTile };
|
return { nextEvent, nextTile };
|
||||||
}
|
}
|
||||||
|
|
||||||
private get roomHasPendingEdit(): string {
|
private get pendingEditItem(): string | undefined {
|
||||||
return this.props.room && localStorage.getItem(`mx_edit_room_${this.props.room.roomId}`);
|
try {
|
||||||
|
return localStorage.getItem(`mx_edit_room_${this.props.room.roomId}_${this.context.timelineRenderingType}`);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getEventTiles(): ReactNode[] {
|
private getEventTiles(): ReactNode[] {
|
||||||
this.eventNodes = {};
|
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) {
|
if (grouper) {
|
||||||
ret.push(...grouper.getTiles());
|
ret.push(...grouper.getTiles());
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,8 +48,8 @@ import { Layout } from "../../settings/Layout";
|
||||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||||
import RightPanelStore from "../../stores/RightPanelStore";
|
import RightPanelStore from "../../stores/RightPanelStore";
|
||||||
import { haveTileForEvent } from "../views/rooms/EventTile";
|
import { haveTileForEvent } from "../views/rooms/EventTile";
|
||||||
import RoomContext from "../../contexts/RoomContext";
|
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
import MatrixClientContext, { withMatrixClientHOC, MatrixClientProps } from "../../contexts/MatrixClientContext";
|
||||||
import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils';
|
import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils';
|
||||||
import { Action } from "../../dispatcher/actions";
|
import { Action } from "../../dispatcher/actions";
|
||||||
import { IMatrixClientCreds } from "../../MatrixClientPeg";
|
import { IMatrixClientCreds } from "../../MatrixClientPeg";
|
||||||
|
@ -91,6 +91,7 @@ import TopUnreadMessagesBar from "../views/rooms/TopUnreadMessagesBar";
|
||||||
import SpaceStore from "../../stores/SpaceStore";
|
import SpaceStore from "../../stores/SpaceStore";
|
||||||
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline';
|
||||||
|
|
||||||
const DEBUG = false;
|
const DEBUG = false;
|
||||||
let debuglog = function(msg: string) {};
|
let debuglog = function(msg: string) {};
|
||||||
|
@ -102,7 +103,7 @@ if (DEBUG) {
|
||||||
debuglog = logger.log.bind(console);
|
debuglog = logger.log.bind(console);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IProps {
|
interface IRoomProps extends MatrixClientProps {
|
||||||
threepidInvite: IThreepidInvite;
|
threepidInvite: IThreepidInvite;
|
||||||
oobData?: IOOBData;
|
oobData?: IOOBData;
|
||||||
|
|
||||||
|
@ -113,7 +114,7 @@ interface IProps {
|
||||||
onRegistered?(credentials: IMatrixClientCreds): void;
|
onRegistered?(credentials: IMatrixClientCreds): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IState {
|
export interface IRoomState {
|
||||||
room?: Room;
|
room?: Room;
|
||||||
roomId?: string;
|
roomId?: string;
|
||||||
roomAlias?: 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.
|
// if it did we don't want the room to be marked as read as soon as it is loaded.
|
||||||
wasContextSwitch?: boolean;
|
wasContextSwitch?: boolean;
|
||||||
editState?: EditorStateTransfer;
|
editState?: EditorStateTransfer;
|
||||||
|
timelineRenderingType: TimelineRenderingType;
|
||||||
|
liveTimeline?: EventTimeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("structures.RoomView")
|
@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 dispatcherRef: string;
|
||||||
private readonly roomStoreToken: EventSubscription;
|
private readonly roomStoreToken: EventSubscription;
|
||||||
private readonly rightPanelStoreToken: EventSubscription;
|
private readonly rightPanelStoreToken: EventSubscription;
|
||||||
|
@ -247,6 +250,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
showDisplaynameChanges: true,
|
showDisplaynameChanges: true,
|
||||||
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
|
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
|
||||||
dragCounter: 0,
|
dragCounter: 0,
|
||||||
|
timelineRenderingType: TimelineRenderingType.Room,
|
||||||
|
liveTimeline: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
|
@ -336,7 +341,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
const roomId = RoomViewStore.getRoomId();
|
const roomId = RoomViewStore.getRoomId();
|
||||||
|
|
||||||
const newState: Pick<IState, any> = {
|
const newState: Pick<IRoomState, any> = {
|
||||||
roomId,
|
roomId,
|
||||||
roomAlias: RoomViewStore.getRoomAlias(),
|
roomAlias: RoomViewStore.getRoomAlias(),
|
||||||
roomLoading: RoomViewStore.isRoomLoading(),
|
roomLoading: RoomViewStore.isRoomLoading(),
|
||||||
|
@ -808,7 +813,9 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
this.onSearchClick();
|
this.onSearchClick();
|
||||||
break;
|
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;
|
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
|
||||||
this.setState({ editState }, () => {
|
this.setState({ editState }, () => {
|
||||||
if (payload.event) {
|
if (payload.event) {
|
||||||
|
@ -932,6 +939,10 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
this.updateE2EStatus(room);
|
this.updateE2EStatus(room);
|
||||||
this.updatePermissions(room);
|
this.updatePermissions(room);
|
||||||
this.checkWidgets(room);
|
this.checkWidgets(room);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
liveTimeline: room.getLiveTimeline(),
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private async calculateRecommendedVersion(room: Room) {
|
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 { Action } from '../../dispatcher/actions';
|
||||||
import { MatrixClientPeg } from '../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../MatrixClientPeg';
|
||||||
import { E2EStatus } from '../../utils/ShieldUtils';
|
import { E2EStatus } from '../../utils/ShieldUtils';
|
||||||
|
import EditorStateTransfer from '../../utils/EditorStateTransfer';
|
||||||
|
import RoomContext, { TimelineRenderingType } from '../../contexts/RoomContext';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -47,10 +49,14 @@ interface IProps {
|
||||||
interface IState {
|
interface IState {
|
||||||
replyToEvent?: MatrixEvent;
|
replyToEvent?: MatrixEvent;
|
||||||
thread?: Thread;
|
thread?: Thread;
|
||||||
|
editState?: EditorStateTransfer;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("structures.ThreadView")
|
@replaceableComponent("structures.ThreadView")
|
||||||
export default class ThreadView extends React.Component<IProps, IState> {
|
export default class ThreadView extends React.Component<IProps, IState> {
|
||||||
|
static contextType = RoomContext;
|
||||||
|
|
||||||
private dispatcherRef: string;
|
private dispatcherRef: string;
|
||||||
private timelinePanelRef: React.RefObject<TimelinePanel> = React.createRef();
|
private timelinePanelRef: React.RefObject<TimelinePanel> = React.createRef();
|
||||||
|
|
||||||
|
@ -90,6 +96,23 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||||
this.setupThread(payload.event);
|
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) => {
|
private setupThread = (mxEv: MatrixEvent) => {
|
||||||
|
@ -124,6 +147,12 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
|
<RoomContext.Provider value={{
|
||||||
|
...this.context,
|
||||||
|
timelineRenderingType: TimelineRenderingType.Thread,
|
||||||
|
liveTimeline: this.state?.thread?.timelineSet?.getLiveTimeline(),
|
||||||
|
}}>
|
||||||
|
|
||||||
<BaseCard
|
<BaseCard
|
||||||
className="mx_ThreadView"
|
className="mx_ThreadView"
|
||||||
onClose={this.props.onClose}
|
onClose={this.props.onClose}
|
||||||
|
@ -149,9 +178,11 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||||
className="mx_RoomView_messagePanel mx_GroupLayout"
|
className="mx_RoomView_messagePanel mx_GroupLayout"
|
||||||
permalinkCreator={this.props.permalinkCreator}
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
membersLoaded={true}
|
membersLoaded={true}
|
||||||
|
editState={this.state.editState}
|
||||||
/>
|
/>
|
||||||
) }
|
) }
|
||||||
<MessageComposer
|
|
||||||
|
{ this.state?.thread?.timelineSet && (<MessageComposer
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
replyInThread={true}
|
replyInThread={true}
|
||||||
|
@ -160,8 +191,9 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||||
permalinkCreator={this.props.permalinkCreator}
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
e2eStatus={this.props.e2eStatus}
|
e2eStatus={this.props.e2eStatus}
|
||||||
compact={true}
|
compact={true}
|
||||||
/>
|
/>) }
|
||||||
</BaseCard>
|
</BaseCard>
|
||||||
|
</RoomContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ import { Action } from '../../../dispatcher/actions';
|
||||||
import { RightPanelPhases } from '../../../stores/RightPanelStorePhases';
|
import { RightPanelPhases } from '../../../stores/RightPanelStorePhases';
|
||||||
import { aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu } from '../../structures/ContextMenu';
|
import { aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu } from '../../structures/ContextMenu';
|
||||||
import { isContentActionable, canEditContent } from '../../../utils/EventUtils';
|
import { isContentActionable, canEditContent } from '../../../utils/EventUtils';
|
||||||
import RoomContext from "../../../contexts/RoomContext";
|
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||||
import Toolbar from "../../../accessibility/Toolbar";
|
import Toolbar from "../../../accessibility/Toolbar";
|
||||||
import { RovingAccessibleTooltipButton, useRovingTabIndex } from "../../../accessibility/RovingTabIndex";
|
import { RovingAccessibleTooltipButton, useRovingTabIndex } from "../../../accessibility/RovingTabIndex";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
@ -128,11 +128,6 @@ const ReactButton: React.FC<IReactButtonProps> = ({ mxEvent, reactions, onFocusC
|
||||||
</React.Fragment>;
|
</React.Fragment>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum ActionBarRenderingContext {
|
|
||||||
Room,
|
|
||||||
Thread
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IMessageActionBarProps {
|
interface IMessageActionBarProps {
|
||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
reactions?: Relations;
|
reactions?: Relations;
|
||||||
|
@ -142,7 +137,6 @@ interface IMessageActionBarProps {
|
||||||
permalinkCreator?: RoomPermalinkCreator;
|
permalinkCreator?: RoomPermalinkCreator;
|
||||||
onFocusChange?: (menuDisplayed: boolean) => void;
|
onFocusChange?: (menuDisplayed: boolean) => void;
|
||||||
toggleThreadExpanded: () => void;
|
toggleThreadExpanded: () => void;
|
||||||
renderingContext?: ActionBarRenderingContext;
|
|
||||||
isQuoteExpanded?: boolean;
|
isQuoteExpanded?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,10 +144,6 @@ interface IMessageActionBarProps {
|
||||||
export default class MessageActionBar extends React.PureComponent<IMessageActionBarProps> {
|
export default class MessageActionBar extends React.PureComponent<IMessageActionBarProps> {
|
||||||
public static contextType = RoomContext;
|
public static contextType = RoomContext;
|
||||||
|
|
||||||
public static defaultProps = {
|
|
||||||
renderingContext: ActionBarRenderingContext.Room,
|
|
||||||
};
|
|
||||||
|
|
||||||
public componentDidMount(): void {
|
public componentDidMount(): void {
|
||||||
if (this.props.mxEvent.status && this.props.mxEvent.status !== EventStatus.SENT) {
|
if (this.props.mxEvent.status && this.props.mxEvent.status !== EventStatus.SENT) {
|
||||||
this.props.mxEvent.on("Event.status", this.onSent);
|
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 => {
|
private onEditClick = (ev: React.MouseEvent): void => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'edit_event',
|
action: Action.EditEvent,
|
||||||
event: this.props.mxEvent,
|
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.
|
// 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
|
// 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()`.
|
// 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, <>
|
toolbarOpts.splice(0, 0, <>
|
||||||
<RovingAccessibleTooltipButton
|
<RovingAccessibleTooltipButton
|
||||||
className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton"
|
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 { CommandPartCreator, Part, PartCreator, Type } from '../../../editor/parts';
|
||||||
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
||||||
import BasicMessageComposer, { REGEX_EMOTICON } from "./BasicMessageComposer";
|
import BasicMessageComposer, { REGEX_EMOTICON } from "./BasicMessageComposer";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
|
||||||
import { Command, CommandCategories, getCommand } from '../../../SlashCommands';
|
import { Command, CommandCategories, getCommand } from '../../../SlashCommands';
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||||
|
@ -46,6 +45,8 @@ import { createRedactEventDialog } from '../dialogs/ConfirmRedactDialog';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { withMatrixClientHOC, MatrixClientProps } from '../../../contexts/MatrixClientContext';
|
||||||
|
import RoomContext from '../../../contexts/RoomContext';
|
||||||
|
|
||||||
function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
|
function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
|
||||||
const html = mxEvent.getContent().formatted_body;
|
const html = mxEvent.getContent().formatted_body;
|
||||||
|
@ -108,25 +109,24 @@ function createEditContent(model: EditorModel, editedEvent: MatrixEvent): IConte
|
||||||
}, contentBody);
|
}, contentBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IProps {
|
interface IEditMessageComposerProps extends MatrixClientProps {
|
||||||
editState: EditorStateTransfer;
|
editState: EditorStateTransfer;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
saveDisabled: boolean;
|
saveDisabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.EditMessageComposer")
|
@replaceableComponent("views.rooms.EditMessageComposer")
|
||||||
export default class EditMessageComposer extends React.Component<IProps, IState> {
|
class EditMessageComposer extends React.Component<IEditMessageComposerProps, IState> {
|
||||||
static contextType = MatrixClientContext;
|
static contextType = RoomContext;
|
||||||
context!: React.ContextType<typeof MatrixClientContext>;
|
context!: React.ContextType<typeof RoomContext>;
|
||||||
|
|
||||||
private readonly editorRef = createRef<BasicMessageComposer>();
|
private readonly editorRef = createRef<BasicMessageComposer>();
|
||||||
private readonly dispatcherRef: string;
|
private readonly dispatcherRef: string;
|
||||||
private model: EditorModel = null;
|
private model: EditorModel = null;
|
||||||
|
|
||||||
constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
|
constructor(props: IEditMessageComposerProps, context: React.ContextType<typeof RoomContext>) {
|
||||||
super(props);
|
super(props);
|
||||||
this.context = context; // otherwise React will only set it prior to render due to type def above
|
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 {
|
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 => {
|
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()) {
|
if (this.editorRef.current?.isModified() || !this.editorRef.current?.isCaretAtStart()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const previousEvent = findEditableEvent(this.getRoom(), false,
|
const previousEvent = findEditableEvent({
|
||||||
this.props.editState.getEvent().getId());
|
events: this.events,
|
||||||
|
isForward: false,
|
||||||
|
fromEventId: this.props.editState.getEvent().getId(),
|
||||||
|
});
|
||||||
if (previousEvent) {
|
if (previousEvent) {
|
||||||
dis.dispatch({ action: 'edit_event', event: previousEvent });
|
dis.dispatch({
|
||||||
|
action: Action.EditEvent,
|
||||||
|
event: previousEvent,
|
||||||
|
timelineRenderingType: this.context.timelineRenderingType,
|
||||||
|
});
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -174,12 +181,24 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
||||||
if (this.editorRef.current?.isModified() || !this.editorRef.current?.isCaretAtEnd()) {
|
if (this.editorRef.current?.isModified() || !this.editorRef.current?.isCaretAtEnd()) {
|
||||||
return;
|
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) {
|
if (nextEvent) {
|
||||||
dis.dispatch({ action: 'edit_event', event: nextEvent });
|
dis.dispatch({
|
||||||
|
action: Action.EditEvent,
|
||||||
|
event: nextEvent,
|
||||||
|
timelineRenderingType: this.context.timelineRenderingType,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.clearStoredEditorState();
|
this.clearStoredEditorState();
|
||||||
dis.dispatch({ action: 'edit_event', event: null });
|
dis.dispatch({
|
||||||
|
action: Action.EditEvent,
|
||||||
|
event: null,
|
||||||
|
timelineRenderingType: this.context.timelineRenderingType,
|
||||||
|
});
|
||||||
dis.fire(Action.FocusSendMessageComposer);
|
dis.fire(Action.FocusSendMessageComposer);
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
@ -189,16 +208,27 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
||||||
};
|
};
|
||||||
|
|
||||||
private get editorRoomKey(): string {
|
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 {
|
private get editorStateKey(): string {
|
||||||
return `mx_edit_state_${this.props.editState.getEvent().getId()}`;
|
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 => {
|
private cancelEdit = (): void => {
|
||||||
this.clearStoredEditorState();
|
this.clearStoredEditorState();
|
||||||
dis.dispatch({ action: "edit_event", event: null });
|
dis.dispatch({
|
||||||
|
action: Action.EditEvent,
|
||||||
|
event: null,
|
||||||
|
timelineRenderingType: this.context.timelineRenderingType,
|
||||||
|
});
|
||||||
dis.fire(Action.FocusSendMessageComposer);
|
dis.fire(Action.FocusSendMessageComposer);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -381,7 +411,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
||||||
}
|
}
|
||||||
if (shouldSend) {
|
if (shouldSend) {
|
||||||
this.cancelPreviousPendingEdit();
|
this.cancelPreviousPendingEdit();
|
||||||
const prom = this.context.sendMessage(roomId, editContent);
|
const prom = this.props.mxClient.sendMessage(roomId, editContent);
|
||||||
this.clearStoredEditorState();
|
this.clearStoredEditorState();
|
||||||
dis.dispatch({ action: "message_sent" });
|
dis.dispatch({ action: "message_sent" });
|
||||||
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, true, false, editContent);
|
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
|
// 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);
|
dis.fire(Action.FocusSendMessageComposer);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -400,7 +434,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
||||||
previousEdit.status === EventStatus.QUEUED ||
|
previousEdit.status === EventStatus.QUEUED ||
|
||||||
previousEdit.status === EventStatus.NOT_SENT
|
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 {
|
private createEditorModel(): boolean {
|
||||||
const { editState } = this.props;
|
const { editState } = this.props;
|
||||||
const room = this.getRoom();
|
const room = this.getRoom();
|
||||||
const partCreator = new CommandPartCreator(room, this.context);
|
const partCreator = new CommandPartCreator(room, this.props.mxClient);
|
||||||
|
|
||||||
let parts;
|
let parts;
|
||||||
let isRestored = false;
|
let isRestored = false;
|
||||||
|
@ -493,3 +527,6 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const EditMessageComposerWithMatrixClient = withMatrixClientHOC(EditMessageComposer);
|
||||||
|
export default EditMessageComposerWithMatrixClient;
|
||||||
|
|
|
@ -53,7 +53,7 @@ import SenderProfile from '../messages/SenderProfile';
|
||||||
import MessageTimestamp from '../messages/MessageTimestamp';
|
import MessageTimestamp from '../messages/MessageTimestamp';
|
||||||
import TooltipButton from '../elements/TooltipButton';
|
import TooltipButton from '../elements/TooltipButton';
|
||||||
import ReadReceiptMarker from "./ReadReceiptMarker";
|
import ReadReceiptMarker from "./ReadReceiptMarker";
|
||||||
import MessageActionBar, { ActionBarRenderingContext } from "../messages/MessageActionBar";
|
import MessageActionBar from "../messages/MessageActionBar";
|
||||||
import ReactionsRow from '../messages/ReactionsRow';
|
import ReactionsRow from '../messages/ReactionsRow';
|
||||||
import { getEventDisplayInfo } from '../../../utils/EventUtils';
|
import { getEventDisplayInfo } from '../../../utils/EventUtils';
|
||||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
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 showMessageActionBar = !isEditing && !this.props.forExport;
|
||||||
const renderingContext = this.props.tileShape === TileShape.Thread
|
|
||||||
? ActionBarRenderingContext.Thread
|
|
||||||
: ActionBarRenderingContext.Room;
|
|
||||||
const actionBar = showMessageActionBar ? <MessageActionBar
|
const actionBar = showMessageActionBar ? <MessageActionBar
|
||||||
mxEvent={this.props.mxEvent}
|
mxEvent={this.props.mxEvent}
|
||||||
reactions={this.state.reactions}
|
reactions={this.state.reactions}
|
||||||
|
@ -1073,7 +1070,6 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
getTile={this.getTile}
|
getTile={this.getTile}
|
||||||
getReplyThread={this.getReplyThread}
|
getReplyThread={this.getReplyThread}
|
||||||
onFocusChange={this.onActionBarFocusChange}
|
onFocusChange={this.onActionBarFocusChange}
|
||||||
renderingContext={renderingContext}
|
|
||||||
isQuoteExpanded={isQuoteExpanded}
|
isQuoteExpanded={isQuoteExpanded}
|
||||||
toggleThreadExpanded={() => this.setQuoteExpanded(!isQuoteExpanded)}
|
toggleThreadExpanded={() => this.setQuoteExpanded(!isQuoteExpanded)}
|
||||||
/> : undefined;
|
/> : undefined;
|
||||||
|
@ -1178,6 +1174,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
showUrlPreview={this.props.showUrlPreview}
|
showUrlPreview={this.props.showUrlPreview}
|
||||||
onHeightChanged={this.props.onHeightChanged}
|
onHeightChanged={this.props.onHeightChanged}
|
||||||
tileShape={this.props.tileShape}
|
tileShape={this.props.tileShape}
|
||||||
|
editState={this.props.editState}
|
||||||
/>
|
/>
|
||||||
</div>,
|
</div>,
|
||||||
]);
|
]);
|
||||||
|
@ -1211,6 +1208,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
showUrlPreview={this.props.showUrlPreview}
|
showUrlPreview={this.props.showUrlPreview}
|
||||||
onHeightChanged={this.props.onHeightChanged}
|
onHeightChanged={this.props.onHeightChanged}
|
||||||
tileShape={this.props.tileShape}
|
tileShape={this.props.tileShape}
|
||||||
|
editState={this.props.editState}
|
||||||
/>
|
/>
|
||||||
{ actionBar }
|
{ actionBar }
|
||||||
</div>,
|
</div>,
|
||||||
|
@ -1231,6 +1229,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
showUrlPreview={this.props.showUrlPreview}
|
showUrlPreview={this.props.showUrlPreview}
|
||||||
tileShape={this.props.tileShape}
|
tileShape={this.props.tileShape}
|
||||||
onHeightChanged={this.props.onHeightChanged}
|
onHeightChanged={this.props.onHeightChanged}
|
||||||
|
editState={this.props.editState}
|
||||||
/>
|
/>
|
||||||
</div>,
|
</div>,
|
||||||
<a
|
<a
|
||||||
|
|
|
@ -45,7 +45,7 @@ import { RecordingState } from "../../../audio/VoiceRecording";
|
||||||
import Tooltip, { Alignment } from "../elements/Tooltip";
|
import Tooltip, { Alignment } from "../elements/Tooltip";
|
||||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||||
import { E2EStatus } from '../../../utils/ShieldUtils';
|
import { E2EStatus } from '../../../utils/ShieldUtils';
|
||||||
import SendMessageComposer from "./SendMessageComposer";
|
import SendMessageComposer, { SendMessageComposer as SendMessageComposerClass } from "./SendMessageComposer";
|
||||||
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import EditorModel from "../../../editor/model";
|
import EditorModel from "../../../editor/model";
|
||||||
|
@ -219,8 +219,8 @@ interface IState {
|
||||||
@replaceableComponent("views.rooms.MessageComposer")
|
@replaceableComponent("views.rooms.MessageComposer")
|
||||||
export default class MessageComposer extends React.Component<IProps, IState> {
|
export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
private dispatcherRef: string;
|
private dispatcherRef: string;
|
||||||
private messageComposerInput: SendMessageComposer;
|
private messageComposerInput = createRef<SendMessageComposerClass>();
|
||||||
private voiceRecordingButton: VoiceRecordComposerTile;
|
private voiceRecordingButton = createRef<VoiceRecordComposerTile>();
|
||||||
private ref: React.RefObject<HTMLDivElement> = createRef();
|
private ref: React.RefObject<HTMLDivElement> = createRef();
|
||||||
private instanceId: number;
|
private instanceId: number;
|
||||||
|
|
||||||
|
@ -378,14 +378,14 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendMessage = async () => {
|
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
|
// There shouldn't be any text message to send when a voice recording is active, so
|
||||||
// just send out the voice recording.
|
// just send out the voice recording.
|
||||||
await this.voiceRecordingButton.send();
|
await this.voiceRecordingButton.current?.send();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.messageComposerInput.sendMessage();
|
this.messageComposerInput.current?.sendMessage();
|
||||||
};
|
};
|
||||||
|
|
||||||
private onChange = (model: EditorModel) => {
|
private onChange = (model: EditorModel) => {
|
||||||
|
@ -460,7 +460,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
buttons.push(
|
buttons.push(
|
||||||
<AccessibleTooltipButton
|
<AccessibleTooltipButton
|
||||||
className="mx_MessageComposer_button mx_MessageComposer_voiceMessage"
|
className="mx_MessageComposer_button mx_MessageComposer_voiceMessage"
|
||||||
onClick={() => this.voiceRecordingButton?.onRecordStartEndClick()}
|
onClick={() => this.voiceRecordingButton.current?.onRecordStartEndClick()}
|
||||||
title={_t("Send voice message")}
|
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) {
|
if (!this.state.tombstone && this.state.canSendMessages) {
|
||||||
controls.push(
|
controls.push(
|
||||||
<SendMessageComposer
|
<SendMessageComposer
|
||||||
ref={(c) => this.messageComposerInput = c}
|
ref={this.messageComposerInput}
|
||||||
key="controls_input"
|
key="controls_input"
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
placeholder={this.renderPlaceholderText()}
|
placeholder={this.renderPlaceholderText()}
|
||||||
|
@ -535,7 +535,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
controls.push(<VoiceRecordComposerTile
|
controls.push(<VoiceRecordComposerTile
|
||||||
key="controls_voice_record"
|
key="controls_voice_record"
|
||||||
ref={c => this.voiceRecordingButton = c}
|
ref={this.voiceRecordingButton}
|
||||||
room={this.props.room} />);
|
room={this.props.room} />);
|
||||||
} else if (this.state.tombstone) {
|
} else if (this.state.tombstone) {
|
||||||
const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];
|
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 { IContent, MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||||
import { DebouncedFunc, throttle } from 'lodash';
|
import { DebouncedFunc, throttle } from 'lodash';
|
||||||
import { EventType, RelationType } from "matrix-js-sdk/src/@types/event";
|
import { EventType, RelationType } from "matrix-js-sdk/src/@types/event";
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import EditorModel from '../../../editor/model';
|
import EditorModel from '../../../editor/model';
|
||||||
|
@ -40,7 +41,7 @@ import { Command, CommandCategories, getCommand } from '../../../SlashCommands';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import { _t, _td } from '../../../languageHandler';
|
import { _t, _td } from '../../../languageHandler';
|
||||||
import ContentMessages from '../../../ContentMessages';
|
import ContentMessages from '../../../ContentMessages';
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import { withMatrixClientHOC, MatrixClientProps } from "../../../contexts/MatrixClientContext";
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import { containsEmoji } from "../../../effects/utils";
|
import { containsEmoji } from "../../../effects/utils";
|
||||||
import { CHAT_EFFECTS } from '../../../effects';
|
import { CHAT_EFFECTS } from '../../../effects';
|
||||||
|
@ -55,8 +56,7 @@ import ErrorDialog from "../dialogs/ErrorDialog";
|
||||||
import QuestionDialog from "../dialogs/QuestionDialog";
|
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||||
import { decorateStartSendingTime, sendRoundTripMetric } from "../../../sendTimePerformanceMetrics";
|
import { decorateStartSendingTime, sendRoundTripMetric } from "../../../sendTimePerformanceMetrics";
|
||||||
|
import RoomContext from '../../../contexts/RoomContext';
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
|
||||||
|
|
||||||
function addReplyToMessageContent(
|
function addReplyToMessageContent(
|
||||||
content: IContent,
|
content: IContent,
|
||||||
|
@ -130,7 +130,7 @@ export function isQuickReaction(model: EditorModel): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IProps {
|
interface ISendMessageComposerProps extends MatrixClientProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
permalinkCreator: RoomPermalinkCreator;
|
permalinkCreator: RoomPermalinkCreator;
|
||||||
|
@ -141,10 +141,8 @@ interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.SendMessageComposer")
|
@replaceableComponent("views.rooms.SendMessageComposer")
|
||||||
export default class SendMessageComposer extends React.Component<IProps> {
|
export class SendMessageComposer extends React.Component<ISendMessageComposerProps> {
|
||||||
static contextType = MatrixClientContext;
|
static contextType = RoomContext;
|
||||||
context!: React.ContextType<typeof MatrixClientContext>;
|
|
||||||
|
|
||||||
private readonly prepareToEncrypt?: DebouncedFunc<() => void>;
|
private readonly prepareToEncrypt?: DebouncedFunc<() => void>;
|
||||||
private readonly editorRef = createRef<BasicMessageComposer>();
|
private readonly editorRef = createRef<BasicMessageComposer>();
|
||||||
private model: EditorModel = null;
|
private model: EditorModel = null;
|
||||||
|
@ -152,26 +150,25 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
||||||
private dispatcherRef: string;
|
private dispatcherRef: string;
|
||||||
private sendHistoryManager: SendHistoryManager;
|
private sendHistoryManager: SendHistoryManager;
|
||||||
|
|
||||||
constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
|
constructor(props: ISendMessageComposerProps, context: React.ContextType<typeof RoomContext>) {
|
||||||
super(props);
|
super(props);
|
||||||
this.context = context; // otherwise React will only set it prior to render due to type def above
|
if (this.props.mxClient.isCryptoEnabled() && this.props.mxClient.isRoomEncrypted(this.props.room.roomId)) {
|
||||||
if (this.context.isCryptoEnabled() && this.context.isRoomEncrypted(this.props.room.roomId)) {
|
|
||||||
this.prepareToEncrypt = throttle(() => {
|
this.prepareToEncrypt = throttle(() => {
|
||||||
this.context.prepareToEncrypt(this.props.room);
|
this.props.mxClient.prepareToEncrypt(this.props.room);
|
||||||
}, 60000, { leading: true, trailing: false });
|
}, 60000, { leading: true, trailing: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("beforeunload", this.saveStoredEditorState);
|
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);
|
const replyToEventChanged = this.props.replyInThread && (this.props.replyToEvent !== prevProps.replyToEvent);
|
||||||
if (replyToEventChanged) {
|
if (replyToEventChanged) {
|
||||||
this.model.reset([]);
|
this.model.reset([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.replyInThread && this.props.replyToEvent && (!prevProps.replyToEvent || replyToEventChanged)) {
|
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) || [];
|
const parts = this.restoreStoredEditorState(partCreator) || [];
|
||||||
this.model.reset(parts);
|
this.model.reset(parts);
|
||||||
this.editorRef.current?.focus();
|
this.editorRef.current?.focus();
|
||||||
|
@ -202,13 +199,20 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
||||||
case MessageComposerAction.EditPrevMessage:
|
case MessageComposerAction.EditPrevMessage:
|
||||||
// selection must be collapsed and caret at start
|
// selection must be collapsed and caret at start
|
||||||
if (this.editorRef.current?.isSelectionCollapsed() && this.editorRef.current?.isCaretAtStart()) {
|
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) {
|
if (editEvent) {
|
||||||
// We're selecting history, so prevent the key event from doing anything else
|
// We're selecting history, so prevent the key event from doing anything else
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'edit_event',
|
action: Action.EditEvent,
|
||||||
event: editEvent,
|
event: editEvent,
|
||||||
|
timelineRenderingType: this.context.timelineRenderingType,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -275,7 +279,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendQuickReaction(): void {
|
private sendQuickReaction(): void {
|
||||||
const timeline = this.props.room.getLiveTimeline();
|
const timeline = this.context.liveTimeline();
|
||||||
const events = timeline.getEvents();
|
const events = timeline.getEvents();
|
||||||
const reaction = this.model.parts[1].text;
|
const reaction = this.model.parts[1].text;
|
||||||
for (let i = events.length - 1; i >= 0; i--) {
|
for (let i = events.length - 1; i >= 0; i--) {
|
||||||
|
@ -448,7 +452,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
||||||
decorateStartSendingTime(content);
|
decorateStartSendingTime(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
const prom = this.context.sendMessage(roomId, content);
|
const prom = this.props.mxClient.sendMessage(roomId, content);
|
||||||
if (replyToEvent) {
|
if (replyToEvent) {
|
||||||
// Clear reply_to_event as we put the message into the queue
|
// Clear reply_to_event as we put the message into the queue
|
||||||
// if the send fails, retry will handle resending.
|
// 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")) {
|
if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) {
|
||||||
prom.then(resp => {
|
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);
|
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
|
// TODO: [REACT-WARNING] Move this to constructor
|
||||||
UNSAFE_componentWillMount() { // eslint-disable-line
|
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) || [];
|
const parts = this.restoreStoredEditorState(partCreator) || [];
|
||||||
this.model = new EditorModel(parts, partCreator);
|
this.model = new EditorModel(parts, partCreator);
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
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.
|
// it puts the filename in as text/plain which we want to ignore.
|
||||||
if (clipboardData.files.length && !clipboardData.types.includes("text/rtf")) {
|
if (clipboardData.files.length && !clipboardData.types.includes("text/rtf")) {
|
||||||
ContentMessages.sharedInstance().sendContentListToRoom(
|
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
|
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;
|
|
46
src/contexts/MatrixClientContext.tsx
Normal file
46
src/contexts/MatrixClientContext.tsx
Normal file
|
@ -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 { createContext } from "react";
|
||||||
|
|
||||||
import { IState } from "../components/structures/RoomView";
|
import { IRoomState } from "../components/structures/RoomView";
|
||||||
import { Layout } from "../settings/Layout";
|
import { Layout } from "../settings/Layout";
|
||||||
|
|
||||||
const RoomContext = createContext<IState>({
|
export enum TimelineRenderingType {
|
||||||
|
Room,
|
||||||
|
Thread
|
||||||
|
}
|
||||||
|
|
||||||
|
const RoomContext = createContext<IRoomState>({
|
||||||
roomLoading: true,
|
roomLoading: true,
|
||||||
peekLoading: false,
|
peekLoading: false,
|
||||||
shouldPeek: true,
|
shouldPeek: true,
|
||||||
|
@ -53,6 +58,8 @@ const RoomContext = createContext<IState>({
|
||||||
showDisplaynameChanges: true,
|
showDisplaynameChanges: true,
|
||||||
matrixClientIsReady: false,
|
matrixClientIsReady: false,
|
||||||
dragCounter: 0,
|
dragCounter: 0,
|
||||||
|
timelineRenderingType: TimelineRenderingType.Room,
|
||||||
|
liveTimeline: undefined,
|
||||||
});
|
});
|
||||||
RoomContext.displayName = "RoomContext";
|
RoomContext.displayName = "RoomContext";
|
||||||
export default RoomContext;
|
export default RoomContext;
|
||||||
|
|
|
@ -205,4 +205,9 @@ export enum Action {
|
||||||
* Should be used with SettingUpdatedPayload.
|
* Should be used with SettingUpdatedPayload.
|
||||||
*/
|
*/
|
||||||
SettingUpdated = "setting_updated",
|
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 { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
import { IState } from "./components/structures/RoomView";
|
import { IRoomState } from "./components/structures/RoomView";
|
||||||
|
|
||||||
interface IDiff {
|
interface IDiff {
|
||||||
isMemberEvent: boolean;
|
isMemberEvent: boolean;
|
||||||
|
@ -54,7 +54,7 @@ function memberEventDiff(ev: MatrixEvent): IDiff {
|
||||||
* @param ctx An optional RoomContext to pull cached settings values from to avoid
|
* @param ctx An optional RoomContext to pull cached settings values from to avoid
|
||||||
* hitting the settings store
|
* 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,
|
// Accessing the settings store directly can be expensive if done frequently,
|
||||||
// so we should prefer using cached values if a RoomContext is available
|
// so we should prefer using cached values if a RoomContext is available
|
||||||
const isEnabled = ctx ?
|
const isEnabled = ctx ?
|
||||||
|
|
|
@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
|
||||||
import { MatrixEvent, EventStatus } from 'matrix-js-sdk/src/models/event';
|
import { MatrixEvent, EventStatus } from 'matrix-js-sdk/src/models/event';
|
||||||
|
|
||||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||||
|
@ -73,9 +72,15 @@ export function canEditOwnEvent(mxEvent: MatrixEvent): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_JUMP_DISTANCE = 100;
|
const MAX_JUMP_DISTANCE = 100;
|
||||||
export function findEditableEvent(room: Room, isForward: boolean, fromEventId: string = undefined): MatrixEvent {
|
export function findEditableEvent({
|
||||||
const liveTimeline = room.getLiveTimeline();
|
events,
|
||||||
const events = liveTimeline.getEvents().concat(room.getPendingEvents());
|
isForward,
|
||||||
|
fromEventId,
|
||||||
|
}: {
|
||||||
|
events: MatrixEvent[];
|
||||||
|
isForward: boolean;
|
||||||
|
fromEventId?: string;
|
||||||
|
}): MatrixEvent {
|
||||||
const maxIdx = events.length - 1;
|
const maxIdx = events.length - 1;
|
||||||
const inc = isForward ? 1 : -1;
|
const inc = isForward ? 1 : -1;
|
||||||
const beginIdx = isForward ? 0 : maxIdx;
|
const beginIdx = isForward ? 0 : maxIdx;
|
||||||
|
|
|
@ -24,8 +24,10 @@ import { sleep } from "matrix-js-sdk/src/utils";
|
||||||
import SendMessageComposer, {
|
import SendMessageComposer, {
|
||||||
createMessageContent,
|
createMessageContent,
|
||||||
isQuickReaction,
|
isQuickReaction,
|
||||||
|
SendMessageComposer as SendMessageComposerClass,
|
||||||
} from "../../../../src/components/views/rooms/SendMessageComposer";
|
} from "../../../../src/components/views/rooms/SendMessageComposer";
|
||||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||||
|
import RoomContext, { TimelineRenderingType } from "../../../../src/contexts/RoomContext";
|
||||||
import EditorModel from "../../../../src/editor/model";
|
import EditorModel from "../../../../src/editor/model";
|
||||||
import { createPartCreator, createRenderer } from "../../../editor/mock";
|
import { createPartCreator, createRenderer } from "../../../editor/mock";
|
||||||
import { createTestClient, mkEvent, mkStubRoom } from "../../../test-utils";
|
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 { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||||
import SpecPermalinkConstructor from "../../../../src/utils/permalinks/SpecPermalinkConstructor";
|
import SpecPermalinkConstructor from "../../../../src/utils/permalinks/SpecPermalinkConstructor";
|
||||||
import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
|
import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
|
||||||
|
import DocumentOffset from '../../../../src/editor/offset';
|
||||||
|
import { Layout } from '../../../../src/settings/Layout';
|
||||||
|
|
||||||
jest.mock("../../../../src/stores/RoomViewStore");
|
jest.mock("../../../../src/stores/RoomViewStore");
|
||||||
|
|
||||||
configure({ adapter: new Adapter() });
|
configure({ adapter: new Adapter() });
|
||||||
|
|
||||||
describe('<SendMessageComposer/>', () => {
|
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", () => {
|
describe("createMessageContent", () => {
|
||||||
const permalinkCreator = jest.fn();
|
const permalinkCreator = jest.fn() as any;
|
||||||
|
|
||||||
it("sends plaintext messages correctly", () => {
|
it("sends plaintext messages correctly", () => {
|
||||||
const model = new EditorModel([], createPartCreator(), createRenderer());
|
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);
|
const content = createMessageContent(model, null, false, permalinkCreator);
|
||||||
|
|
||||||
|
@ -56,7 +98,8 @@ describe('<SendMessageComposer/>', () => {
|
||||||
|
|
||||||
it("sends markdown messages correctly", () => {
|
it("sends markdown messages correctly", () => {
|
||||||
const model = new EditorModel([], createPartCreator(), createRenderer());
|
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);
|
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", () => {
|
it("strips /me from messages and marks them as m.emote accordingly", () => {
|
||||||
const model = new EditorModel([], createPartCreator(), createRenderer());
|
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);
|
const content = createMessageContent(model, null, false, permalinkCreator);
|
||||||
|
|
||||||
|
@ -84,7 +128,9 @@ describe('<SendMessageComposer/>', () => {
|
||||||
|
|
||||||
it("allows sending double-slash escaped slash commands correctly", () => {
|
it("allows sending double-slash escaped slash commands correctly", () => {
|
||||||
const model = new EditorModel([], createPartCreator(), createRenderer());
|
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);
|
const content = createMessageContent(model, null, false, permalinkCreator);
|
||||||
|
|
||||||
|
@ -97,9 +143,11 @@ describe('<SendMessageComposer/>', () => {
|
||||||
|
|
||||||
describe("functions correctly mounted", () => {
|
describe("functions correctly mounted", () => {
|
||||||
const mockClient = MatrixClientPeg.matrixClient = createTestClient();
|
const mockClient = MatrixClientPeg.matrixClient = createTestClient();
|
||||||
const mockRoom = mkStubRoom();
|
const mockRoom = mkStubRoom('myfakeroom') as any;
|
||||||
const mockEvent = mkEvent({
|
const mockEvent = mkEvent({
|
||||||
type: "m.room.message",
|
type: "m.room.message",
|
||||||
|
room: 'myfakeroom',
|
||||||
|
user: 'myfakeuser',
|
||||||
content: "Replying to this",
|
content: "Replying to this",
|
||||||
event: true,
|
event: true,
|
||||||
});
|
});
|
||||||
|
@ -116,11 +164,13 @@ describe('<SendMessageComposer/>', () => {
|
||||||
|
|
||||||
it("renders text and placeholder correctly", () => {
|
it("renders text and placeholder correctly", () => {
|
||||||
const wrapper = mount(<MatrixClientContext.Provider value={mockClient}>
|
const wrapper = mount(<MatrixClientContext.Provider value={mockClient}>
|
||||||
|
<RoomContext.Provider value={roomContext}>
|
||||||
<SendMessageComposer
|
<SendMessageComposer
|
||||||
room={mockRoom}
|
room={mockRoom as any}
|
||||||
placeholder="placeholder string"
|
placeholder="placeholder string"
|
||||||
permalinkCreator={new SpecPermalinkConstructor()}
|
permalinkCreator={new SpecPermalinkConstructor() as any}
|
||||||
/>
|
/>
|
||||||
|
</RoomContext.Provider>
|
||||||
</MatrixClientContext.Provider>);
|
</MatrixClientContext.Provider>);
|
||||||
|
|
||||||
expect(wrapper.find('[aria-label="placeholder string"]')).toHaveLength(1);
|
expect(wrapper.find('[aria-label="placeholder string"]')).toHaveLength(1);
|
||||||
|
@ -135,12 +185,15 @@ describe('<SendMessageComposer/>', () => {
|
||||||
|
|
||||||
it("correctly persists state to and from localStorage", () => {
|
it("correctly persists state to and from localStorage", () => {
|
||||||
const wrapper = mount(<MatrixClientContext.Provider value={mockClient}>
|
const wrapper = mount(<MatrixClientContext.Provider value={mockClient}>
|
||||||
|
<RoomContext.Provider value={roomContext}>
|
||||||
|
|
||||||
<SendMessageComposer
|
<SendMessageComposer
|
||||||
room={mockRoom}
|
room={mockRoom as any}
|
||||||
placeholder=""
|
placeholder=""
|
||||||
permalinkCreator={new SpecPermalinkConstructor()}
|
permalinkCreator={new SpecPermalinkConstructor() as any}
|
||||||
replyToEvent={mockEvent}
|
replyToEvent={mockEvent}
|
||||||
/>
|
/>
|
||||||
|
</RoomContext.Provider>
|
||||||
</MatrixClientContext.Provider>);
|
</MatrixClientContext.Provider>);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
|
@ -148,7 +201,7 @@ describe('<SendMessageComposer/>', () => {
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
});
|
});
|
||||||
|
|
||||||
const key = wrapper.find(SendMessageComposer).instance().editorStateKey;
|
const key = wrapper.find(SendMessageComposerClass).instance().editorStateKey;
|
||||||
|
|
||||||
expect(wrapper.text()).toBe("Test Text");
|
expect(wrapper.text()).toBe("Test Text");
|
||||||
expect(localStorage.getItem(key)).toBeNull();
|
expect(localStorage.getItem(key)).toBeNull();
|
||||||
|
@ -177,11 +230,14 @@ describe('<SendMessageComposer/>', () => {
|
||||||
|
|
||||||
it("persists state correctly without replyToEvent onbeforeunload", () => {
|
it("persists state correctly without replyToEvent onbeforeunload", () => {
|
||||||
const wrapper = mount(<MatrixClientContext.Provider value={mockClient}>
|
const wrapper = mount(<MatrixClientContext.Provider value={mockClient}>
|
||||||
|
<RoomContext.Provider value={roomContext}>
|
||||||
|
|
||||||
<SendMessageComposer
|
<SendMessageComposer
|
||||||
room={mockRoom}
|
room={mockRoom as any}
|
||||||
placeholder=""
|
placeholder=""
|
||||||
permalinkCreator={new SpecPermalinkConstructor()}
|
permalinkCreator={new SpecPermalinkConstructor() as any}
|
||||||
/>
|
/>
|
||||||
|
</RoomContext.Provider>
|
||||||
</MatrixClientContext.Provider>);
|
</MatrixClientContext.Provider>);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
|
@ -189,7 +245,7 @@ describe('<SendMessageComposer/>', () => {
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
});
|
});
|
||||||
|
|
||||||
const key = wrapper.find(SendMessageComposer).instance().editorStateKey;
|
const key = wrapper.find(SendMessageComposerClass).instance().editorStateKey;
|
||||||
|
|
||||||
expect(wrapper.text()).toBe("Hello World");
|
expect(wrapper.text()).toBe("Hello World");
|
||||||
expect(localStorage.getItem(key)).toBeNull();
|
expect(localStorage.getItem(key)).toBeNull();
|
||||||
|
@ -203,12 +259,15 @@ describe('<SendMessageComposer/>', () => {
|
||||||
|
|
||||||
it("persists to session history upon sending", async () => {
|
it("persists to session history upon sending", async () => {
|
||||||
const wrapper = mount(<MatrixClientContext.Provider value={mockClient}>
|
const wrapper = mount(<MatrixClientContext.Provider value={mockClient}>
|
||||||
|
<RoomContext.Provider value={roomContext}>
|
||||||
|
|
||||||
<SendMessageComposer
|
<SendMessageComposer
|
||||||
room={mockRoom}
|
room={mockRoom as any}
|
||||||
placeholder="placeholder"
|
placeholder="placeholder"
|
||||||
permalinkCreator={new SpecPermalinkConstructor()}
|
permalinkCreator={new SpecPermalinkConstructor() as any}
|
||||||
replyToEvent={mockEvent}
|
replyToEvent={mockEvent}
|
||||||
/>
|
/>
|
||||||
|
</RoomContext.Provider>
|
||||||
</MatrixClientContext.Provider>);
|
</MatrixClientContext.Provider>);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
|
@ -230,12 +289,38 @@ describe('<SendMessageComposer/>', () => {
|
||||||
replyEventId: mockEvent.getId(),
|
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", () => {
|
describe("isQuickReaction", () => {
|
||||||
it("correctly detects quick reaction", () => {
|
it("correctly detects quick reaction", () => {
|
||||||
const model = new EditorModel([], createPartCreator(), createRenderer());
|
const model = new EditorModel([], createPartCreator(), createRenderer());
|
||||||
model.update("+😊", "insertText", { offset: 3, atNodeEnd: true });
|
model.update("+😊", "insertText", new DocumentOffset(3, true));
|
||||||
|
|
||||||
const isReaction = isQuickReaction(model);
|
const isReaction = isQuickReaction(model);
|
||||||
|
|
||||||
|
@ -244,7 +329,7 @@ describe('<SendMessageComposer/>', () => {
|
||||||
|
|
||||||
it("correctly detects quick reaction with space", () => {
|
it("correctly detects quick reaction with space", () => {
|
||||||
const model = new EditorModel([], createPartCreator(), createRenderer());
|
const model = new EditorModel([], createPartCreator(), createRenderer());
|
||||||
model.update("+ 😊", "insertText", { offset: 4, atNodeEnd: true });
|
model.update("+ 😊", "insertText", new DocumentOffset(4, true));
|
||||||
|
|
||||||
const isReaction = isQuickReaction(model);
|
const isReaction = isQuickReaction(model);
|
||||||
|
|
||||||
|
@ -256,10 +341,10 @@ describe('<SendMessageComposer/>', () => {
|
||||||
const model2 = new EditorModel([], createPartCreator(), createRenderer());
|
const model2 = new EditorModel([], createPartCreator(), createRenderer());
|
||||||
const model3 = new EditorModel([], createPartCreator(), createRenderer());
|
const model3 = new EditorModel([], createPartCreator(), createRenderer());
|
||||||
const model4 = new EditorModel([], createPartCreator(), createRenderer());
|
const model4 = new EditorModel([], createPartCreator(), createRenderer());
|
||||||
model.update("+😊hello", "insertText", { offset: 8, atNodeEnd: true });
|
model.update("+😊hello", "insertText", new DocumentOffset( 8, true));
|
||||||
model2.update(" +😊", "insertText", { offset: 4, atNodeEnd: true });
|
model2.update(" +😊", "insertText", new DocumentOffset( 4, true));
|
||||||
model3.update("+ 😊😊", "insertText", { offset: 6, atNodeEnd: true });
|
model3.update("+ 😊😊", "insertText", new DocumentOffset( 6, true));
|
||||||
model4.update("+smiley", "insertText", { offset: 7, atNodeEnd: true });
|
model4.update("+smiley", "insertText", new DocumentOffset( 7, true));
|
||||||
|
|
||||||
expect(isQuickReaction(model)).toBeFalsy();
|
expect(isQuickReaction(model)).toBeFalsy();
|
||||||
expect(isQuickReaction(model2)).toBeFalsy();
|
expect(isQuickReaction(model2)).toBeFalsy();
|
Loading…
Add table
Add a link
Reference in a new issue