Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Add Reply in thread button to the right-click message context-menu #9004

Merged
merged 10 commits into from
Jul 23, 2022
39 changes: 39 additions & 0 deletions src/components/structures/MatrixChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ import { SnakedObject } from "../../utils/SnakedObject";
import { leaveRoomBehaviour } from "../../utils/leave-behaviour";
import VideoChannelStore from "../../stores/VideoChannelStore";
import { IRoomStateEventsActionPayload } from "../../actions/MatrixActionCreators";
import { ShowThreadPayload } from "../../dispatcher/payloads/ShowThreadPayload";
import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases";
import RightPanelStore from "../../stores/right-panel/RightPanelStore";
import { TimelineRenderingType } from "../../contexts/RoomContext";

// legacy export
export { default as Views } from "../../Views";
Expand Down Expand Up @@ -798,6 +802,41 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
hideAnalyticsToast();
SettingsStore.setValue("pseudonymousAnalyticsOptIn", null, SettingLevel.ACCOUNT, false);
break;
case Action.ShowThread: {
const {
rootEvent,
initialEvent,
highlighted,
scrollIntoView,
push,
} = payload as ShowThreadPayload;

const threadViewCard = {
phase: RightPanelPhases.ThreadView,
state: {
threadHeadEvent: rootEvent,
initialEvent: initialEvent,
isInitialEventHighlighted: highlighted,
initialEventScrollIntoView: scrollIntoView,
},
};
if (push ?? false) {
RightPanelStore.instance.pushCard(threadViewCard);
} else {
RightPanelStore.instance.setCards([
{ phase: RightPanelPhases.ThreadPanel },
threadViewCard,
]);
}

// Focus the composer
dis.dispatch({
action: Action.FocusSendMessageComposer,
context: TimelineRenderingType.Thread,
});

break;
}
}
};

Expand Down
8 changes: 5 additions & 3 deletions src/components/structures/RoomView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ import RoomStatusBar from "./RoomStatusBar";
import MessageComposer from '../views/rooms/MessageComposer';
import JumpToBottomButton from "../views/rooms/JumpToBottomButton";
import TopUnreadMessagesBar from "../views/rooms/TopUnreadMessagesBar";
import { showThread } from '../../dispatcher/dispatch-actions/threads';
import { fetchInitialEvent } from "../../utils/EventUtils";
import { ComposerInsertPayload, ComposerType } from "../../dispatcher/payloads/ComposerInsertPayload";
import AppsDrawer from '../views/rooms/AppsDrawer';
Expand All @@ -111,6 +110,7 @@ import FileDropTarget from './FileDropTarget';
import Measured from '../views/elements/Measured';
import { FocusComposerPayload } from '../../dispatcher/payloads/FocusComposerPayload';
import { haveRendererForEvent } from "../../events/EventTileFactory";
import { ShowThreadPayload } from "../../dispatcher/payloads/ShowThreadPayload";

const DEBUG = false;
let debuglog = function(msg: string) {};
Expand Down Expand Up @@ -452,7 +452,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {

const thread = initialEvent?.getThread();
if (thread && !initialEvent?.isThreadRoot) {
showThread({
dis.dispatch<ShowThreadPayload>({
action: Action.ShowThread,
rootEvent: thread.rootEvent,
initialEvent,
highlighted: RoomViewStore.instance.isInitialEventHighlighted(),
Expand All @@ -464,7 +465,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
newState.initialEventScrollIntoView = RoomViewStore.instance.initialEventScrollIntoView();

if (thread && initialEvent?.isThreadRoot) {
showThread({
dis.dispatch<ShowThreadPayload>({
action: Action.ShowThread,
rootEvent: thread.rootEvent,
initialEvent,
highlighted: RoomViewStore.instance.isInitialEventHighlighted(),
Expand Down
91 changes: 59 additions & 32 deletions src/components/views/context_menus/MessageContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import React, { createRef } from 'react';
import React, { createRef, useContext } from 'react';
import { EventStatus, MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { EventType, RelationType } from "matrix-js-sdk/src/@types/event";
import { Relations } from 'matrix-js-sdk/src/models/relations';
Expand Down Expand Up @@ -59,8 +59,59 @@ import { OpenReportEventDialogPayload } from "../../../dispatcher/payloads/OpenR
import { createMapSiteLinkFromEvent } from '../../../utils/location';
import { getForwardableEvent } from '../../../events/forward/getForwardableEvent';
import { getShareableLocationEvent } from '../../../events/location/getShareableLocationEvent';
import { showThread } from "../../../dispatcher/dispatch-actions/threads";
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload";
import { CardContext } from "../right_panel/context";
import { UserTab } from "../dialogs/UserTab";

interface IReplyInThreadButton {
mxEvent: MatrixEvent;
closeMenu: () => void;
}

const ReplyInThreadButton = ({ mxEvent, closeMenu }: IReplyInThreadButton) => {
const context = useContext(CardContext);
const relationType = mxEvent?.getRelation()?.rel_type;

// Can't create a thread from an event with an existing relation
if (Boolean(relationType) && relationType !== RelationType.Thread) return;

const onClick = (): void => {
if (!localStorage.getItem("mx_seen_feature_thread")) {
localStorage.setItem("mx_seen_feature_thread", "true");
}

if (!SettingsStore.getValue("feature_thread")) {
dis.dispatch({
action: Action.ViewUserSettings,
initialTabId: UserTab.Labs,
});
} else if (mxEvent.getThread() && !mxEvent.isThreadRoot) {
dis.dispatch<ShowThreadPayload>({
action: Action.ShowThread,
rootEvent: mxEvent.getThread().rootEvent,
initialEvent: mxEvent,
scroll_into_view: true,
highlighted: true,
push: context.isCard,
});
} else {
dis.dispatch<ShowThreadPayload>({
action: Action.ShowThread,
rootEvent: mxEvent,
push: context.isCard,
});
}
closeMenu();
};

return (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconReplyInThread"
label={_t("Reply in thread")}
onClick={onClick}
/>
);
};

interface IProps extends IPosition {
chevronFace: ChevronFace;
Expand Down Expand Up @@ -284,30 +335,6 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
this.closeMenu();
};

private onReplyInThreadClick = (): void => {
const { mxEvent } = this.props;

if (!localStorage.getItem("mx_seen_feature_thread")) {
localStorage.setItem("mx_seen_feature_thread", "true");
}

if (mxEvent.getThread() && !mxEvent.isThreadRoot) {
showThread({
rootEvent: mxEvent.getThread().rootEvent,
initialEvent: mxEvent,
scroll_into_view: true,
highlighted: true,
push: RightPanelStore.instance.isOpen,
});
} else {
showThread({
rootEvent: mxEvent,
push: RightPanelStore.instance.isOpen,
});
}
this.closeMenu();
};

private onReactClick = (): void => {
this.setState({ reactionPickerDisplayed: true });
};
Expand Down Expand Up @@ -613,15 +640,15 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
if (
rightClick &&
contentActionable &&
canSendMessages && SettingsStore.getValue("feature_thread") &&
canSendMessages &&
SettingsStore.getValue("feature_thread") &&
Thread.hasServerSideSupport &&
timelineRenderingType !== TimelineRenderingType.Thread
) {
replyInThreadButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconReplyInThread"
label={_t("Reply in thread")}
onClick={this.onReplyInThreadClick}
<ReplyInThreadButton
mxEvent={mxEvent}
closeMenu={this.closeMenu}
/>
);
}
Expand Down
10 changes: 6 additions & 4 deletions src/components/views/messages/MessageActionBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { M_BEACON_INFO } from 'matrix-js-sdk/src/@types/beacon';

import type { Relations } from 'matrix-js-sdk/src/models/relations';
import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher/dispatcher';
import dis, { defaultDispatcher } from '../../../dispatcher/dispatcher';
import ContextMenu, { aboveLeftOf, ContextMenuTooltipButton, useContextMenu } from '../../structures/ContextMenu';
import { isContentActionable, canEditContent, editEvent, canCancel } from '../../../utils/EventUtils';
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
Expand All @@ -41,13 +41,13 @@ import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
import ReplyChain from '../elements/ReplyChain';
import ReactionPicker from "../emojipicker/ReactionPicker";
import { CardContext } from '../right_panel/context';
import { showThread } from "../../../dispatcher/dispatch-actions/threads";
import { shouldDisplayReply } from '../../../utils/Reply';
import { Key } from "../../../Keyboard";
import { ALTERNATE_KEY_NAME } from "../../../accessibility/KeyboardShortcuts";
import { UserTab } from '../dialogs/UserTab';
import { Action } from '../../../dispatcher/actions';
import SdkConfig from "../../../SdkConfig";
import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload";

interface IOptionsButtonProps {
mxEvent: MatrixEvent;
Expand Down Expand Up @@ -190,15 +190,17 @@ const ReplyInThreadButton = ({ mxEvent }: IReplyInThreadButton) => {
initialTabId: UserTab.Labs,
});
} else if (mxEvent.getThread() && !mxEvent.isThreadRoot) {
showThread({
defaultDispatcher.dispatch<ShowThreadPayload>({
action: Action.ShowThread,
rootEvent: mxEvent.getThread().rootEvent,
initialEvent: mxEvent,
scroll_into_view: true,
highlighted: true,
push: context.isCard,
});
} else {
showThread({
defaultDispatcher.dispatch<ShowThreadPayload>({
action: Action.ShowThread,
rootEvent: mxEvent,
push: context.isCard,
});
Expand Down
8 changes: 6 additions & 2 deletions src/components/views/rooms/EventTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ import MessageActionBar from "../messages/MessageActionBar";
import ReactionsRow from '../messages/ReactionsRow';
import { getEventDisplayInfo } from '../../../utils/EventRenderingUtils';
import SettingsStore from "../../../settings/SettingsStore";
import { showThread } from '../../../dispatcher/dispatch-actions/threads';
import { MessagePreviewStore } from '../../../stores/room-list/MessagePreviewStore';
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
Expand All @@ -80,6 +79,7 @@ import { haveRendererForEvent, isMessageEvent, renderTile } from "../../../event
import ThreadSummary, { ThreadMessagePreview } from "./ThreadSummary";
import { ReadReceiptGroup } from './ReadReceiptGroup';
import { useTooltip } from "../../../utils/useTooltip";
import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload";

export type GetRelationsForEvent = (eventId: string, relationType: string, eventType: string) => Relations;

Expand Down Expand Up @@ -1353,7 +1353,11 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
"onMouseEnter": () => this.setState({ hover: true }),
"onMouseLeave": () => this.setState({ hover: false }),
"onClick": (ev: MouseEvent) => {
showThread({ rootEvent: this.props.mxEvent, push: true });
dis.dispatch<ShowThreadPayload>({
action: Action.ShowThread,
rootEvent: this.props.mxEvent,
push: true,
});
const target = ev.currentTarget as HTMLElement;
const index = Array.from(target.parentElement.children).indexOf(target);
PosthogTrackers.trackInteraction("WebThreadsPanelThreadItem", ev, index);
Expand Down
7 changes: 5 additions & 2 deletions src/components/views/rooms/ThreadSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,16 @@ import { IContent, MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/model
import { _t } from "../../../languageHandler";
import { CardContext } from "../right_panel/context";
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
import { showThread } from "../../../dispatcher/dispatch-actions/threads";
import PosthogTrackers from "../../../PosthogTrackers";
import { useTypedEventEmitter, useTypedEventEmitterState } from "../../../hooks/useEventEmitter";
import RoomContext from "../../../contexts/RoomContext";
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
import MemberAvatar from "../avatars/MemberAvatar";
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { Action } from "../../../dispatcher/actions";
import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload";
import defaultDispatcher from "../../../dispatcher/dispatcher";

interface IProps {
mxEvent: MatrixEvent;
Expand All @@ -50,7 +52,8 @@ const ThreadSummary = ({ mxEvent, thread }: IProps) => {
<AccessibleButton
className="mx_ThreadSummary"
onClick={(ev: ButtonEvent) => {
showThread({
defaultDispatcher.dispatch<ShowThreadPayload>({
action: Action.ShowThread,
rootEvent: mxEvent,
push: cardContext.isCard,
});
Expand Down
5 changes: 5 additions & 0 deletions src/dispatcher/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,4 +326,9 @@ export enum Action {
* Fires with the PlatformSetPayload.
*/
PlatformSet = "platform_set",

/**
* Fired when we want to view a thread, either a new one or an existing one
*/
ShowThread = "show_thread",
}
38 changes: 0 additions & 38 deletions src/dispatcher/dispatch-actions/threads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,46 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import { MatrixEvent } from "matrix-js-sdk/src/models/event";

import RightPanelStore from "../../stores/right-panel/RightPanelStore";
import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases";
import dis from "../dispatcher";
import { Action } from "../actions";
import { TimelineRenderingType } from "../../contexts/RoomContext";

export const showThread = (props: {
kerryarchibald marked this conversation as resolved.
Show resolved Hide resolved
rootEvent: MatrixEvent;
initialEvent?: MatrixEvent;
highlighted?: boolean;
scroll_into_view?: boolean;
push?: boolean;
}) => {
const push = props.push ?? false;
const threadViewCard = {
phase: RightPanelPhases.ThreadView,
state: {
threadHeadEvent: props.rootEvent,
initialEvent: props.initialEvent,
isInitialEventHighlighted: props.highlighted,
initialEventScrollIntoView: props.scroll_into_view,
},
};
if (push) {
RightPanelStore.instance.pushCard(threadViewCard);
} else {
RightPanelStore.instance.setCards([
{ phase: RightPanelPhases.ThreadPanel },
threadViewCard,
]);
}

// Focus the composer
dis.dispatch({
action: Action.FocusSendMessageComposer,
context: TimelineRenderingType.Thread,
});
};

export const showThreadPanel = () => {
RightPanelStore.instance.setCard({ phase: RightPanelPhases.ThreadPanel });
Expand Down
Loading