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

Commit

Permalink
Add Reply in thread button to the right-click message context-menu (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
SimonBrandner authored Jul 23, 2022
1 parent dfa844a commit 787ace9
Show file tree
Hide file tree
Showing 13 changed files with 236 additions and 56 deletions.
30 changes: 30 additions & 0 deletions cypress/e2e/5-threads/threads.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,4 +212,34 @@ describe("Threads", () => {
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content")
.should("contain", "I'm very good thanks :)");
});

it("right panel behaves correctly", () => {
// Create room
let roomId: string;
cy.createRoom({}).then(_roomId => {
roomId = _roomId;
cy.visit("/#/room/" + roomId);
});
// Send message
cy.get(".mx_RoomView_body .mx_BasicMessageComposer_input").type("Hello Mr. Bot{enter}");

// Create thread
cy.get(".mx_RoomView_body .mx_EventTile").contains(".mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot")
.realHover().find(".mx_MessageActionBar_threadButton").click();
cy.get(".mx_ThreadView_timelinePanelWrapper").should("have.length", 1);

// Send message to thread
cy.get(".mx_BaseCard .mx_BasicMessageComposer_input").type("Hello Mr. User{enter}");
cy.get(".mx_BaseCard .mx_EventTile").should("contain", "Hello Mr. User");

// Close thread
cy.get(".mx_BaseCard_close").click();

// Open existing thread
cy.get(".mx_RoomView_body .mx_EventTile").contains(".mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot")
.realHover().find(".mx_MessageActionBar_threadButton").click();
cy.get(".mx_ThreadView_timelinePanelWrapper").should("have.length", 1);
cy.get(".mx_BaseCard .mx_EventTile").should("contain", "Hello Mr. Bot");
cy.get(".mx_BaseCard .mx_EventTile").should("contain", "Hello Mr. User");
});
});
4 changes: 4 additions & 0 deletions res/css/views/context_menus/_MessageContextMenu.pcss
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 @@ -130,6 +130,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";
import { UseCaseSelection } from '../views/elements/UseCaseSelection';
import { ValidatedServerConfig } from '../../utils/ValidatedServerConfig';
import { isLocalRoom } from '../../utils/localRoom/isLocalRoom';
Expand Down Expand Up @@ -803,6 +807,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";
import useFavouriteMessages from '../../../hooks/useFavouriteMessages';

interface IOptionsButtonProps {
Expand Down Expand Up @@ -191,15 +191,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";
import { isLocalRoom } from '../../../utils/localRoom/isLocalRoom';

export type GetRelationsForEvent = (eventId: string, relationType: string, eventType: string) => Relations;
Expand Down Expand Up @@ -1357,7 +1357,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: {
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

0 comments on commit 787ace9

Please sign in to comment.