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
4 changes: 4 additions & 0 deletions res/css/views/context_menus/_MessageContextMenu.scss
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ limitations under the License.
mask-image: url('$(res)/img/element-icons/room/message-bar/reply.svg');
}

.mx_MessageContextMenu_iconReplyInThread::before {
mask-image: url('$(res)/img/element-icons/message/thread.svg');
}

.mx_MessageContextMenu_iconReact::before {
mask-image: url('$(res)/img/element-icons/room/message-bar/emoji.svg');
}
Expand Down
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
74 changes: 73 additions & 1 deletion src/components/views/context_menus/MessageContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ 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';
import { RoomMemberEvent } from "matrix-js-sdk/src/models/room-member";
import { M_POLL_START } from "matrix-events-sdk";
import { Thread } from "matrix-js-sdk/src/models/thread";

import { MatrixClientPeg } from '../../../MatrixClientPeg';
import dis from '../../../dispatcher/dispatcher';
Expand Down Expand Up @@ -58,6 +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 { 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 @@ -582,6 +636,23 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
);
}

let replyInThreadButton: JSX.Element;
if (
rightClick &&
contentActionable &&
canSendMessages &&
SettingsStore.getValue("feature_thread") &&
Thread.hasServerSideSupport &&
timelineRenderingType !== TimelineRenderingType.Thread
) {
replyInThreadButton = (
<ReplyInThreadButton
mxEvent={mxEvent}
closeMenu={this.closeMenu}
/>
);
}

let reactButton;
if (rightClick && contentActionable && canReact) {
reactButton = (
Expand Down Expand Up @@ -621,6 +692,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
<IconizedContextMenuOptionList>
{ reactButton }
{ replyButton }
{ replyInThreadButton }
{ editButton }
</IconizedContextMenuOptionList>
);
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
30 changes: 30 additions & 0 deletions src/dispatcher/payloads/ShowThreadPayload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
SimonBrandner marked this conversation as resolved.
Show resolved Hide resolved

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 { MatrixEvent } from "matrix-js-sdk/src/models/event";

import { ActionPayload } from "../payloads";
import { Action } from "../actions";

export interface ShowThreadPayload extends ActionPayload {
action: Action.ShowThread;

rootEvent: MatrixEvent;
initialEvent?: MatrixEvent;
highlighted?: boolean;
scrollIntoView?: boolean;
push?: boolean;
}
Loading