Add Reply in thread
button to the right-click message context-menu (#9004)
This commit is contained in:
parent
dfa844a035
commit
787ace9dc5
13 changed files with 236 additions and 56 deletions
|
@ -212,4 +212,34 @@ describe("Threads", () => {
|
||||||
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content")
|
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content")
|
||||||
.should("contain", "I'm very good thanks :)");
|
.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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -107,6 +107,10 @@ limitations under the License.
|
||||||
mask-image: url('$(res)/img/element-icons/room/message-bar/reply.svg');
|
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 {
|
.mx_MessageContextMenu_iconReact::before {
|
||||||
mask-image: url('$(res)/img/element-icons/room/message-bar/emoji.svg');
|
mask-image: url('$(res)/img/element-icons/room/message-bar/emoji.svg');
|
||||||
}
|
}
|
||||||
|
|
|
@ -130,6 +130,10 @@ import { SnakedObject } from "../../utils/SnakedObject";
|
||||||
import { leaveRoomBehaviour } from "../../utils/leave-behaviour";
|
import { leaveRoomBehaviour } from "../../utils/leave-behaviour";
|
||||||
import VideoChannelStore from "../../stores/VideoChannelStore";
|
import VideoChannelStore from "../../stores/VideoChannelStore";
|
||||||
import { IRoomStateEventsActionPayload } from "../../actions/MatrixActionCreators";
|
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 { UseCaseSelection } from '../views/elements/UseCaseSelection';
|
||||||
import { ValidatedServerConfig } from '../../utils/ValidatedServerConfig';
|
import { ValidatedServerConfig } from '../../utils/ValidatedServerConfig';
|
||||||
import { isLocalRoom } from '../../utils/localRoom/isLocalRoom';
|
import { isLocalRoom } from '../../utils/localRoom/isLocalRoom';
|
||||||
|
@ -803,6 +807,41 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
hideAnalyticsToast();
|
hideAnalyticsToast();
|
||||||
SettingsStore.setValue("pseudonymousAnalyticsOptIn", null, SettingLevel.ACCOUNT, false);
|
SettingsStore.setValue("pseudonymousAnalyticsOptIn", null, SettingLevel.ACCOUNT, false);
|
||||||
break;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -97,7 +97,6 @@ import RoomStatusBar from "./RoomStatusBar";
|
||||||
import MessageComposer from '../views/rooms/MessageComposer';
|
import MessageComposer from '../views/rooms/MessageComposer';
|
||||||
import JumpToBottomButton from "../views/rooms/JumpToBottomButton";
|
import JumpToBottomButton from "../views/rooms/JumpToBottomButton";
|
||||||
import TopUnreadMessagesBar from "../views/rooms/TopUnreadMessagesBar";
|
import TopUnreadMessagesBar from "../views/rooms/TopUnreadMessagesBar";
|
||||||
import { showThread } from '../../dispatcher/dispatch-actions/threads';
|
|
||||||
import { fetchInitialEvent } from "../../utils/EventUtils";
|
import { fetchInitialEvent } from "../../utils/EventUtils";
|
||||||
import { ComposerInsertPayload, ComposerType } from "../../dispatcher/payloads/ComposerInsertPayload";
|
import { ComposerInsertPayload, ComposerType } from "../../dispatcher/payloads/ComposerInsertPayload";
|
||||||
import AppsDrawer from '../views/rooms/AppsDrawer';
|
import AppsDrawer from '../views/rooms/AppsDrawer';
|
||||||
|
@ -111,6 +110,7 @@ import FileDropTarget from './FileDropTarget';
|
||||||
import Measured from '../views/elements/Measured';
|
import Measured from '../views/elements/Measured';
|
||||||
import { FocusComposerPayload } from '../../dispatcher/payloads/FocusComposerPayload';
|
import { FocusComposerPayload } from '../../dispatcher/payloads/FocusComposerPayload';
|
||||||
import { haveRendererForEvent } from "../../events/EventTileFactory";
|
import { haveRendererForEvent } from "../../events/EventTileFactory";
|
||||||
|
import { ShowThreadPayload } from "../../dispatcher/payloads/ShowThreadPayload";
|
||||||
|
|
||||||
const DEBUG = false;
|
const DEBUG = false;
|
||||||
let debuglog = function(msg: string) {};
|
let debuglog = function(msg: string) {};
|
||||||
|
@ -452,7 +452,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
|
|
||||||
const thread = initialEvent?.getThread();
|
const thread = initialEvent?.getThread();
|
||||||
if (thread && !initialEvent?.isThreadRoot) {
|
if (thread && !initialEvent?.isThreadRoot) {
|
||||||
showThread({
|
dis.dispatch<ShowThreadPayload>({
|
||||||
|
action: Action.ShowThread,
|
||||||
rootEvent: thread.rootEvent,
|
rootEvent: thread.rootEvent,
|
||||||
initialEvent,
|
initialEvent,
|
||||||
highlighted: RoomViewStore.instance.isInitialEventHighlighted(),
|
highlighted: RoomViewStore.instance.isInitialEventHighlighted(),
|
||||||
|
@ -464,7 +465,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
newState.initialEventScrollIntoView = RoomViewStore.instance.initialEventScrollIntoView();
|
newState.initialEventScrollIntoView = RoomViewStore.instance.initialEventScrollIntoView();
|
||||||
|
|
||||||
if (thread && initialEvent?.isThreadRoot) {
|
if (thread && initialEvent?.isThreadRoot) {
|
||||||
showThread({
|
dis.dispatch<ShowThreadPayload>({
|
||||||
|
action: Action.ShowThread,
|
||||||
rootEvent: thread.rootEvent,
|
rootEvent: thread.rootEvent,
|
||||||
initialEvent,
|
initialEvent,
|
||||||
highlighted: RoomViewStore.instance.isInitialEventHighlighted(),
|
highlighted: RoomViewStore.instance.isInitialEventHighlighted(),
|
||||||
|
|
|
@ -16,12 +16,13 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
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 { EventStatus, MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||||
import { EventType, RelationType } from "matrix-js-sdk/src/@types/event";
|
import { EventType, RelationType } from "matrix-js-sdk/src/@types/event";
|
||||||
import { Relations } from 'matrix-js-sdk/src/models/relations';
|
import { Relations } from 'matrix-js-sdk/src/models/relations';
|
||||||
import { RoomMemberEvent } from "matrix-js-sdk/src/models/room-member";
|
import { RoomMemberEvent } from "matrix-js-sdk/src/models/room-member";
|
||||||
import { M_POLL_START } from "matrix-events-sdk";
|
import { M_POLL_START } from "matrix-events-sdk";
|
||||||
|
import { Thread } from "matrix-js-sdk/src/models/thread";
|
||||||
|
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
|
@ -58,6 +59,59 @@ import { OpenReportEventDialogPayload } from "../../../dispatcher/payloads/OpenR
|
||||||
import { createMapSiteLinkFromEvent } from '../../../utils/location';
|
import { createMapSiteLinkFromEvent } from '../../../utils/location';
|
||||||
import { getForwardableEvent } from '../../../events/forward/getForwardableEvent';
|
import { getForwardableEvent } from '../../../events/forward/getForwardableEvent';
|
||||||
import { getShareableLocationEvent } from '../../../events/location/getShareableLocationEvent';
|
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 {
|
interface IProps extends IPosition {
|
||||||
chevronFace: ChevronFace;
|
chevronFace: ChevronFace;
|
||||||
|
@ -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;
|
let reactButton;
|
||||||
if (rightClick && contentActionable && canReact) {
|
if (rightClick && contentActionable && canReact) {
|
||||||
reactButton = (
|
reactButton = (
|
||||||
|
@ -621,6 +692,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
||||||
<IconizedContextMenuOptionList>
|
<IconizedContextMenuOptionList>
|
||||||
{ reactButton }
|
{ reactButton }
|
||||||
{ replyButton }
|
{ replyButton }
|
||||||
|
{ replyInThreadButton }
|
||||||
{ editButton }
|
{ editButton }
|
||||||
</IconizedContextMenuOptionList>
|
</IconizedContextMenuOptionList>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 type { Relations } from 'matrix-js-sdk/src/models/relations';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis, { defaultDispatcher } from '../../../dispatcher/dispatcher';
|
||||||
import ContextMenu, { aboveLeftOf, ContextMenuTooltipButton, useContextMenu } from '../../structures/ContextMenu';
|
import ContextMenu, { aboveLeftOf, ContextMenuTooltipButton, useContextMenu } from '../../structures/ContextMenu';
|
||||||
import { isContentActionable, canEditContent, editEvent, canCancel } from '../../../utils/EventUtils';
|
import { isContentActionable, canEditContent, editEvent, canCancel } from '../../../utils/EventUtils';
|
||||||
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||||
|
@ -41,13 +41,13 @@ import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
||||||
import ReplyChain from '../elements/ReplyChain';
|
import ReplyChain from '../elements/ReplyChain';
|
||||||
import ReactionPicker from "../emojipicker/ReactionPicker";
|
import ReactionPicker from "../emojipicker/ReactionPicker";
|
||||||
import { CardContext } from '../right_panel/context';
|
import { CardContext } from '../right_panel/context';
|
||||||
import { showThread } from "../../../dispatcher/dispatch-actions/threads";
|
|
||||||
import { shouldDisplayReply } from '../../../utils/Reply';
|
import { shouldDisplayReply } from '../../../utils/Reply';
|
||||||
import { Key } from "../../../Keyboard";
|
import { Key } from "../../../Keyboard";
|
||||||
import { ALTERNATE_KEY_NAME } from "../../../accessibility/KeyboardShortcuts";
|
import { ALTERNATE_KEY_NAME } from "../../../accessibility/KeyboardShortcuts";
|
||||||
import { UserTab } from '../dialogs/UserTab';
|
import { UserTab } from '../dialogs/UserTab';
|
||||||
import { Action } from '../../../dispatcher/actions';
|
import { Action } from '../../../dispatcher/actions';
|
||||||
import SdkConfig from "../../../SdkConfig";
|
import SdkConfig from "../../../SdkConfig";
|
||||||
|
import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload";
|
||||||
import useFavouriteMessages from '../../../hooks/useFavouriteMessages';
|
import useFavouriteMessages from '../../../hooks/useFavouriteMessages';
|
||||||
|
|
||||||
interface IOptionsButtonProps {
|
interface IOptionsButtonProps {
|
||||||
|
@ -191,7 +191,8 @@ const ReplyInThreadButton = ({ mxEvent }: IReplyInThreadButton) => {
|
||||||
initialTabId: UserTab.Labs,
|
initialTabId: UserTab.Labs,
|
||||||
});
|
});
|
||||||
} else if (mxEvent.getThread() && !mxEvent.isThreadRoot) {
|
} else if (mxEvent.getThread() && !mxEvent.isThreadRoot) {
|
||||||
showThread({
|
defaultDispatcher.dispatch<ShowThreadPayload>({
|
||||||
|
action: Action.ShowThread,
|
||||||
rootEvent: mxEvent.getThread().rootEvent,
|
rootEvent: mxEvent.getThread().rootEvent,
|
||||||
initialEvent: mxEvent,
|
initialEvent: mxEvent,
|
||||||
scroll_into_view: true,
|
scroll_into_view: true,
|
||||||
|
@ -199,7 +200,8 @@ const ReplyInThreadButton = ({ mxEvent }: IReplyInThreadButton) => {
|
||||||
push: context.isCard,
|
push: context.isCard,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
showThread({
|
defaultDispatcher.dispatch<ShowThreadPayload>({
|
||||||
|
action: Action.ShowThread,
|
||||||
rootEvent: mxEvent,
|
rootEvent: mxEvent,
|
||||||
push: context.isCard,
|
push: context.isCard,
|
||||||
});
|
});
|
||||||
|
|
|
@ -58,7 +58,6 @@ import MessageActionBar from "../messages/MessageActionBar";
|
||||||
import ReactionsRow from '../messages/ReactionsRow';
|
import ReactionsRow from '../messages/ReactionsRow';
|
||||||
import { getEventDisplayInfo } from '../../../utils/EventRenderingUtils';
|
import { getEventDisplayInfo } from '../../../utils/EventRenderingUtils';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { showThread } from '../../../dispatcher/dispatch-actions/threads';
|
|
||||||
import { MessagePreviewStore } from '../../../stores/room-list/MessagePreviewStore';
|
import { MessagePreviewStore } from '../../../stores/room-list/MessagePreviewStore';
|
||||||
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||||
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
|
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
|
||||||
|
@ -80,6 +79,7 @@ import { haveRendererForEvent, isMessageEvent, renderTile } from "../../../event
|
||||||
import ThreadSummary, { ThreadMessagePreview } from "./ThreadSummary";
|
import ThreadSummary, { ThreadMessagePreview } from "./ThreadSummary";
|
||||||
import { ReadReceiptGroup } from './ReadReceiptGroup';
|
import { ReadReceiptGroup } from './ReadReceiptGroup';
|
||||||
import { useTooltip } from "../../../utils/useTooltip";
|
import { useTooltip } from "../../../utils/useTooltip";
|
||||||
|
import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload";
|
||||||
import { isLocalRoom } from '../../../utils/localRoom/isLocalRoom';
|
import { isLocalRoom } from '../../../utils/localRoom/isLocalRoom';
|
||||||
|
|
||||||
export type GetRelationsForEvent = (eventId: string, relationType: string, eventType: string) => Relations;
|
export type GetRelationsForEvent = (eventId: string, relationType: string, eventType: string) => Relations;
|
||||||
|
@ -1357,7 +1357,11 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
||||||
"onMouseEnter": () => this.setState({ hover: true }),
|
"onMouseEnter": () => this.setState({ hover: true }),
|
||||||
"onMouseLeave": () => this.setState({ hover: false }),
|
"onMouseLeave": () => this.setState({ hover: false }),
|
||||||
"onClick": (ev: MouseEvent) => {
|
"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 target = ev.currentTarget as HTMLElement;
|
||||||
const index = Array.from(target.parentElement.children).indexOf(target);
|
const index = Array.from(target.parentElement.children).indexOf(target);
|
||||||
PosthogTrackers.trackInteraction("WebThreadsPanelThreadItem", ev, index);
|
PosthogTrackers.trackInteraction("WebThreadsPanelThreadItem", ev, index);
|
||||||
|
|
|
@ -21,7 +21,6 @@ import { IContent, MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/model
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { CardContext } from "../right_panel/context";
|
import { CardContext } from "../right_panel/context";
|
||||||
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
||||||
import { showThread } from "../../../dispatcher/dispatch-actions/threads";
|
|
||||||
import PosthogTrackers from "../../../PosthogTrackers";
|
import PosthogTrackers from "../../../PosthogTrackers";
|
||||||
import { useTypedEventEmitter, useTypedEventEmitterState } from "../../../hooks/useEventEmitter";
|
import { useTypedEventEmitter, useTypedEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||||
import RoomContext from "../../../contexts/RoomContext";
|
import RoomContext from "../../../contexts/RoomContext";
|
||||||
|
@ -29,6 +28,9 @@ import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewSto
|
||||||
import MemberAvatar from "../avatars/MemberAvatar";
|
import MemberAvatar from "../avatars/MemberAvatar";
|
||||||
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
|
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
import { Action } from "../../../dispatcher/actions";
|
||||||
|
import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload";
|
||||||
|
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
|
@ -50,7 +52,8 @@ const ThreadSummary = ({ mxEvent, thread }: IProps) => {
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
className="mx_ThreadSummary"
|
className="mx_ThreadSummary"
|
||||||
onClick={(ev: ButtonEvent) => {
|
onClick={(ev: ButtonEvent) => {
|
||||||
showThread({
|
defaultDispatcher.dispatch<ShowThreadPayload>({
|
||||||
|
action: Action.ShowThread,
|
||||||
rootEvent: mxEvent,
|
rootEvent: mxEvent,
|
||||||
push: cardContext.isCard,
|
push: cardContext.isCard,
|
||||||
});
|
});
|
||||||
|
|
|
@ -326,4 +326,9 @@ export enum Action {
|
||||||
* Fires with the PlatformSetPayload.
|
* Fires with the PlatformSetPayload.
|
||||||
*/
|
*/
|
||||||
PlatformSet = "platform_set",
|
PlatformSet = "platform_set",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired when we want to view a thread, either a new one or an existing one
|
||||||
|
*/
|
||||||
|
ShowThread = "show_thread",
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,46 +14,8 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|
||||||
|
|
||||||
import RightPanelStore from "../../stores/right-panel/RightPanelStore";
|
import RightPanelStore from "../../stores/right-panel/RightPanelStore";
|
||||||
import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases";
|
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 = () => {
|
export const showThreadPanel = () => {
|
||||||
RightPanelStore.instance.setCard({ phase: RightPanelPhases.ThreadPanel });
|
RightPanelStore.instance.setCard({ phase: RightPanelPhases.ThreadPanel });
|
||||||
|
|
30
src/dispatcher/payloads/ShowThreadPayload.ts
Normal file
30
src/dispatcher/payloads/ShowThreadPayload.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 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 { 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;
|
||||||
|
}
|
|
@ -40,6 +40,7 @@ import { makeBeaconEvent, makeBeaconInfoEvent, makeLocationEvent, stubClient } f
|
||||||
import dispatcher from '../../../../src/dispatcher/dispatcher';
|
import dispatcher from '../../../../src/dispatcher/dispatcher';
|
||||||
import SettingsStore from '../../../../src/settings/SettingsStore';
|
import SettingsStore from '../../../../src/settings/SettingsStore';
|
||||||
import { ReadPinsEventId } from '../../../../src/components/views/right_panel/types';
|
import { ReadPinsEventId } from '../../../../src/components/views/right_panel/types';
|
||||||
|
import { Action } from "../../../../src/dispatcher/actions";
|
||||||
|
|
||||||
jest.mock("../../../../src/utils/strings", () => ({
|
jest.mock("../../../../src/utils/strings", () => ({
|
||||||
copyPlaintext: jest.fn(),
|
copyPlaintext: jest.fn(),
|
||||||
|
@ -50,6 +51,7 @@ jest.mock("../../../../src/utils/EventUtils", () => ({
|
||||||
...jest.requireActual("../../../../src/utils/EventUtils"),
|
...jest.requireActual("../../../../src/utils/EventUtils"),
|
||||||
canEditContent: jest.fn(),
|
canEditContent: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
jest.mock('../../../../src/dispatcher/dispatcher');
|
||||||
|
|
||||||
const roomId = 'roomid';
|
const roomId = 'roomid';
|
||||||
|
|
||||||
|
@ -461,6 +463,29 @@ describe('MessageContextMenu', () => {
|
||||||
const reactButton = menu.find('div[aria-label="View in room"]');
|
const reactButton = menu.find('div[aria-label="View in room"]');
|
||||||
expect(reactButton).toHaveLength(0);
|
expect(reactButton).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('creates a new thread on reply in thread click', () => {
|
||||||
|
const eventContent = MessageEvent.from("hello");
|
||||||
|
const mxEvent = new MatrixEvent(eventContent.serialize());
|
||||||
|
|
||||||
|
Thread.hasServerSideSupport = true;
|
||||||
|
const context = {
|
||||||
|
canSendMessages: true,
|
||||||
|
};
|
||||||
|
jest.spyOn(SettingsStore, 'getValue').mockReturnValue(true);
|
||||||
|
|
||||||
|
const menu = createRightClickMenu(mxEvent, context);
|
||||||
|
|
||||||
|
const replyInThreadButton = menu.find('div[aria-label="Reply in thread"]');
|
||||||
|
expect(replyInThreadButton).toHaveLength(1);
|
||||||
|
replyInThreadButton.simulate("click");
|
||||||
|
|
||||||
|
expect(dispatcher.dispatch).toHaveBeenCalledWith({
|
||||||
|
action: Action.ShowThread,
|
||||||
|
rootEvent: mxEvent,
|
||||||
|
push: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -471,6 +496,10 @@ function createRightClickMenuWithContent(
|
||||||
return createMenuWithContent(eventContent, { rightClick: true }, context);
|
return createMenuWithContent(eventContent, { rightClick: true }, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createRightClickMenu(mxEvent: MatrixEvent, context?: Partial<IRoomState>): ReactWrapper {
|
||||||
|
return createMenu(mxEvent, { rightClick: true }, context);
|
||||||
|
}
|
||||||
|
|
||||||
function createMenuWithContent(
|
function createMenuWithContent(
|
||||||
eventContent: ExtensibleEvent,
|
eventContent: ExtensibleEvent,
|
||||||
props?: Partial<React.ComponentProps<typeof MessageContextMenu>>,
|
props?: Partial<React.ComponentProps<typeof MessageContextMenu>>,
|
||||||
|
|
|
@ -41,12 +41,8 @@ import dispatcher from '../../../../src/dispatcher/dispatcher';
|
||||||
import SettingsStore from '../../../../src/settings/SettingsStore';
|
import SettingsStore from '../../../../src/settings/SettingsStore';
|
||||||
import { Action } from '../../../../src/dispatcher/actions';
|
import { Action } from '../../../../src/dispatcher/actions';
|
||||||
import { UserTab } from '../../../../src/components/views/dialogs/UserTab';
|
import { UserTab } from '../../../../src/components/views/dialogs/UserTab';
|
||||||
import { showThread } from '../../../../src/dispatcher/dispatch-actions/threads';
|
|
||||||
|
|
||||||
jest.mock('../../../../src/dispatcher/dispatcher');
|
jest.mock('../../../../src/dispatcher/dispatcher');
|
||||||
jest.mock('../../../../src/dispatcher/dispatch-actions/threads', () => ({
|
|
||||||
showThread: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('<MessageActionBar />', () => {
|
describe('<MessageActionBar />', () => {
|
||||||
const userId = '@alice:server.org';
|
const userId = '@alice:server.org';
|
||||||
|
@ -447,7 +443,8 @@ describe('<MessageActionBar />', () => {
|
||||||
fireEvent.click(getByLabelText('Reply in thread'));
|
fireEvent.click(getByLabelText('Reply in thread'));
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(showThread).toHaveBeenCalledWith({
|
expect(dispatcher.dispatch).toHaveBeenCalledWith({
|
||||||
|
action: Action.ShowThread,
|
||||||
rootEvent: alicesMessageEvent,
|
rootEvent: alicesMessageEvent,
|
||||||
push: false,
|
push: false,
|
||||||
});
|
});
|
||||||
|
@ -475,7 +472,8 @@ describe('<MessageActionBar />', () => {
|
||||||
fireEvent.click(getByLabelText('Reply in thread'));
|
fireEvent.click(getByLabelText('Reply in thread'));
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(showThread).toHaveBeenCalledWith({
|
expect(dispatcher.dispatch).toHaveBeenCalledWith({
|
||||||
|
action: Action.ShowThread,
|
||||||
rootEvent: alicesMessageEvent,
|
rootEvent: alicesMessageEvent,
|
||||||
initialEvent: threadReplyEvent,
|
initialEvent: threadReplyEvent,
|
||||||
highlighted: true,
|
highlighted: true,
|
||||||
|
|
Loading…
Reference in a new issue