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

This commit is contained in:
Šimon Brandner 2022-07-23 14:13:49 +02:00 committed by GitHub
parent dfa844a035
commit 787ace9dc5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 236 additions and 56 deletions

View file

@ -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';
@ -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;
}
}
};

View file

@ -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';
@ -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) {};
@ -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(),
@ -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(),

View file

@ -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';
@ -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;
@ -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 = (
@ -621,6 +692,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
<IconizedContextMenuOptionList>
{ reactButton }
{ replyButton }
{ replyInThreadButton }
{ editButton }
</IconizedContextMenuOptionList>
);

View file

@ -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";
@ -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 {
@ -191,7 +191,8 @@ 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,
@ -199,7 +200,8 @@ const ReplyInThreadButton = ({ mxEvent }: IReplyInThreadButton) => {
push: context.isCard,
});
} else {
showThread({
defaultDispatcher.dispatch<ShowThreadPayload>({
action: Action.ShowThread,
rootEvent: mxEvent,
push: context.isCard,
});

View file

@ -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";
@ -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;
@ -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);

View file

@ -21,7 +21,6 @@ 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";
@ -29,6 +28,9 @@ import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewSto
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;
@ -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,
});