Add message right click context menu v2 (#5672)
* migrate the message context menu to IconizedContextMenu Signed-off-by: Michael Weimann <mail@michael-weimann.eu> * migrate the message context menu to IconizedContextMenu Signed-off-by: Michael Weimann <mail@michael-weimann.eu> * Added right-click menu Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * add message context menu group keys Signed-off-by: Michael Weimann <mail@michael-weimann.eu> * add message context menu icons Signed-off-by: Michael Weimann <mail@michael-weimann.eu> * add _MessageContextMenu.scss license header Signed-off-by: Michael Weimann <mail@michael-weimann.eu> * use null vars for context menu lists * Add allowOverridingNativeContextMenus() Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Use allowOverridingNativeContextMenus() Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Fix types Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Fix types Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Remove mistaken line Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Fix styling Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * InputHTMLAttributes -> AllHTMLAttributes Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Convert to TS Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Add some types Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Make onClick optional Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Add rightClick prop Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Add copy button Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * What about upgrading deps after the eslint migration, Simon? Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Add edit button Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * fix Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Add reply button Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Add react button Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Cleanup render() Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Fix comments Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Add save button Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Don't show context menu if editing Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Add special handling for click a timestamp Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Fix double empty line Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Don't show context menu for images Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Cleanup Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Fix order Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Keep action bar shown when right-clicking Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Highlight event tile when right-clicking Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Delint Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Pointless change so that I can re-run the CI Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Remove dowload button Because we don't use this menu when clicking on images Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Be more clear for non-bools Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Use triggerOnMouse down prop Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Remove a comment Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Remove unused var Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Remove unnecessary import Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Add some missing types Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Add missing type Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Remove unused import Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Add a missing type Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Fix types Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Fix types/naming Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Add missing current Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Remove unused var Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Fix editing and replying Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * i18n Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Fix import Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Support right-click context menu for threads Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Make button order match `MessageActionBar` Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Fix missing permalink button Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Remove useless part of if statement Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Some small refactoring for consistency Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Some more refactoring Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Fix `editEvent()` call Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Make editing polls work Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Fix collapse reply chain button Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Fix timelineRenderingType Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Fix reply button Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Hide right-click context menu behind a labs flag Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Add missing return type Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Make `contextMene` optional Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Move `renderContextMenu()` Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Simplify `renderContextMenu()` Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Improve `aboveLeftOf` typing Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Use `InputHTMLAttributes` Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Disable message right-click context menu in browser (for now) Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> * Give permalink button more props Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> Co-authored-by: Michael Weimann <mail@michael-weimann.eu>
This commit is contained in:
parent
77b0addbc7
commit
d162e021e1
9 changed files with 408 additions and 123 deletions
|
@ -90,6 +90,22 @@ limitations under the License.
|
||||||
mask-image: url('$(res)/img/element-icons/room/pin.svg');
|
mask-image: url('$(res)/img/element-icons/room/pin.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_MessageContextMenu_iconCopy::before {
|
||||||
|
mask-image: url($copy-button-url);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageContextMenu_iconEdit::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/message-bar/edit.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageContextMenu_iconReply::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/message-bar/reply.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageContextMenu_iconReact::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/message-bar/emoji.svg');
|
||||||
|
}
|
||||||
|
|
||||||
.mx_MessageContextMenu_iconViewInRoom::before {
|
.mx_MessageContextMenu_iconViewInRoom::before {
|
||||||
mask-image: url('$(res)/img/element-icons/view-in-room.svg');
|
mask-image: url('$(res)/img/element-icons/view-in-room.svg');
|
||||||
}
|
}
|
||||||
|
|
|
@ -145,6 +145,13 @@ export default abstract class BasePlatform {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if platform allows overriding native context menus
|
||||||
|
*/
|
||||||
|
public allowOverridingNativeContextMenus(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the platform supports displaying
|
* Returns true if the platform supports displaying
|
||||||
* notifications, otherwise false.
|
* notifications, otherwise false.
|
||||||
|
|
|
@ -429,7 +429,7 @@ export type AboveLeftOf = IPosition & {
|
||||||
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect,
|
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect,
|
||||||
// and either above or below: wherever there is more space (maybe this should be aboveOrBelowLeftOf?)
|
// and either above or below: wherever there is more space (maybe this should be aboveOrBelowLeftOf?)
|
||||||
export const aboveLeftOf = (
|
export const aboveLeftOf = (
|
||||||
elementRect: DOMRect,
|
elementRect: Pick<DOMRect, "right" | "top" | "bottom">,
|
||||||
chevronFace = ChevronFace.None,
|
chevronFace = ChevronFace.None,
|
||||||
vPadding = 0,
|
vPadding = 0,
|
||||||
): AboveLeftOf => {
|
): AboveLeftOf => {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
|
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
Copyright 2021 - 2022 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,7 +16,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { ReactElement } from 'react';
|
import React, { createRef } 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';
|
||||||
|
@ -30,20 +31,25 @@ import Modal from '../../../Modal';
|
||||||
import Resend from '../../../Resend';
|
import Resend from '../../../Resend';
|
||||||
import SettingsStore from '../../../settings/SettingsStore';
|
import SettingsStore from '../../../settings/SettingsStore';
|
||||||
import { isUrlPermitted } from '../../../HtmlUtils';
|
import { isUrlPermitted } from '../../../HtmlUtils';
|
||||||
import { isContentActionable } from '../../../utils/EventUtils';
|
import { canEditContent, editEvent, isContentActionable } from '../../../utils/EventUtils';
|
||||||
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from './IconizedContextMenu';
|
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from './IconizedContextMenu';
|
||||||
import { ReadPinsEventId } from "../right_panel/types";
|
import { ReadPinsEventId } from "../right_panel/types";
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
|
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
||||||
|
import { ButtonEvent } from '../elements/AccessibleButton';
|
||||||
|
import { copyPlaintext } from '../../../utils/strings';
|
||||||
|
import ContextMenu, { toRightOf } from '../../structures/ContextMenu';
|
||||||
|
import ReactionPicker from '../emojipicker/ReactionPicker';
|
||||||
import ViewSource from '../../structures/ViewSource';
|
import ViewSource from '../../structures/ViewSource';
|
||||||
import { createRedactEventDialog } from '../dialogs/ConfirmRedactDialog';
|
import { createRedactEventDialog } from '../dialogs/ConfirmRedactDialog';
|
||||||
import ShareDialog from '../dialogs/ShareDialog';
|
import ShareDialog from '../dialogs/ShareDialog';
|
||||||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
import { IPosition, ChevronFace } from '../../structures/ContextMenu';
|
||||||
import { ChevronFace, IPosition } from '../../structures/ContextMenu';
|
|
||||||
import RoomContext, { TimelineRenderingType } from '../../../contexts/RoomContext';
|
import RoomContext, { TimelineRenderingType } from '../../../contexts/RoomContext';
|
||||||
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||||
import EndPollDialog from '../dialogs/EndPollDialog';
|
import EndPollDialog from '../dialogs/EndPollDialog';
|
||||||
import { isPollEnded } from '../messages/MPollBody';
|
import { isPollEnded } from '../messages/MPollBody';
|
||||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||||
|
import { GetRelationsForEvent } from "../rooms/EventTile";
|
||||||
import { OpenForwardDialogPayload } from "../../../dispatcher/payloads/OpenForwardDialogPayload";
|
import { OpenForwardDialogPayload } from "../../../dispatcher/payloads/OpenForwardDialogPayload";
|
||||||
import { OpenReportEventDialogPayload } from "../../../dispatcher/payloads/OpenReportEventDialogPayload";
|
import { OpenReportEventDialogPayload } from "../../../dispatcher/payloads/OpenReportEventDialogPayload";
|
||||||
import { createMapSiteLink } from '../../../utils/location';
|
import { createMapSiteLink } from '../../../utils/location';
|
||||||
|
@ -65,42 +71,54 @@ interface IProps extends IPosition {
|
||||||
chevronFace: ChevronFace;
|
chevronFace: ChevronFace;
|
||||||
/* the MatrixEvent associated with the context menu */
|
/* the MatrixEvent associated with the context menu */
|
||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
/* an optional EventTileOps implementation that can be used to unhide preview widgets */
|
// An optional EventTileOps implementation that can be used to unhide preview widgets
|
||||||
eventTileOps?: IEventTileOps;
|
eventTileOps?: IEventTileOps;
|
||||||
|
// Callback called when the menu is dismissed
|
||||||
permalinkCreator?: RoomPermalinkCreator;
|
permalinkCreator?: RoomPermalinkCreator;
|
||||||
/* an optional function to be called when the user clicks collapse thread, if not provided hide button */
|
/* an optional function to be called when the user clicks collapse thread, if not provided hide button */
|
||||||
collapseReplyChain?(): void;
|
collapseReplyChain?(): void;
|
||||||
/* callback called when the menu is dismissed */
|
/* callback called when the menu is dismissed */
|
||||||
onFinished(): void;
|
onFinished(): void;
|
||||||
/* if the menu is inside a dialog, we sometimes need to close that dialog after click (forwarding) */
|
// If the menu is inside a dialog, we sometimes need to close that dialog after click (forwarding)
|
||||||
onCloseDialog?(): void;
|
onCloseDialog?(): void;
|
||||||
getRelationsForEvent?: (
|
// True if the menu is being used as a right click menu
|
||||||
eventId: string,
|
rightClick?: boolean;
|
||||||
relationType: string,
|
// The Relations model from the JS SDK for reactions to `mxEvent`
|
||||||
eventType: string
|
reactions?: Relations;
|
||||||
) => Relations;
|
// A permalink to the event
|
||||||
|
showPermalink?: boolean;
|
||||||
|
|
||||||
|
getRelationsForEvent?: GetRelationsForEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
canRedact: boolean;
|
canRedact: boolean;
|
||||||
canPin: boolean;
|
canPin: boolean;
|
||||||
|
reactionPickerDisplayed: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class MessageContextMenu extends React.Component<IProps, IState> {
|
export default class MessageContextMenu extends React.Component<IProps, IState> {
|
||||||
static contextType = RoomContext;
|
static contextType = RoomContext;
|
||||||
public context!: React.ContextType<typeof RoomContext>;
|
public context!: React.ContextType<typeof RoomContext>;
|
||||||
|
|
||||||
state = {
|
private reactButtonRef = createRef<any>(); // XXX Ref to a functional component
|
||||||
canRedact: false,
|
|
||||||
canPin: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
canRedact: false,
|
||||||
|
canPin: false,
|
||||||
|
reactionPickerDisplayed: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
MatrixClientPeg.get().on(RoomMemberEvent.PowerLevel, this.checkPermissions);
|
MatrixClientPeg.get().on(RoomMemberEvent.PowerLevel, this.checkPermissions);
|
||||||
this.checkPermissions();
|
this.checkPermissions();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount(): void {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
if (cli) {
|
if (cli) {
|
||||||
cli.removeListener(RoomMemberEvent.PowerLevel, this.checkPermissions);
|
cli.removeListener(RoomMemberEvent.PowerLevel, this.checkPermissions);
|
||||||
|
@ -233,11 +251,45 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
||||||
this.closeMenu();
|
this.closeMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onCopyPermalinkClick = (e: ButtonEvent): void => {
|
||||||
|
e.preventDefault(); // So that we don't open the permalink
|
||||||
|
copyPlaintext(this.getPermalink());
|
||||||
|
this.closeMenu();
|
||||||
|
};
|
||||||
|
|
||||||
private onCollapseReplyChainClick = (): void => {
|
private onCollapseReplyChainClick = (): void => {
|
||||||
this.props.collapseReplyChain();
|
this.props.collapseReplyChain();
|
||||||
this.closeMenu();
|
this.closeMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onCopyClick = (): void => {
|
||||||
|
copyPlaintext(this.getSelectedText());
|
||||||
|
this.closeMenu();
|
||||||
|
};
|
||||||
|
|
||||||
|
private onEditClick = (): void => {
|
||||||
|
editEvent(this.props.mxEvent, this.context.timelineRenderingType, this.props.getRelationsForEvent);
|
||||||
|
this.closeMenu();
|
||||||
|
};
|
||||||
|
|
||||||
|
private onReplyClick = (): void => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'reply_to_event',
|
||||||
|
event: this.props.mxEvent,
|
||||||
|
context: this.context.timelineRenderingType,
|
||||||
|
});
|
||||||
|
this.closeMenu();
|
||||||
|
};
|
||||||
|
|
||||||
|
private onReactClick = (): void => {
|
||||||
|
this.setState({ reactionPickerDisplayed: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
private onCloseReactionPicker = (): void => {
|
||||||
|
this.setState({ reactionPickerDisplayed: false });
|
||||||
|
this.closeMenu();
|
||||||
|
};
|
||||||
|
|
||||||
private onEndPollClick = (): void => {
|
private onEndPollClick = (): void => {
|
||||||
const matrixClient = MatrixClientPeg.get();
|
const matrixClient = MatrixClientPeg.get();
|
||||||
Modal.createTrackedDialog('End Poll', '', EndPollDialog, {
|
Modal.createTrackedDialog('End Poll', '', EndPollDialog, {
|
||||||
|
@ -258,11 +310,20 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getSelectedText(): string {
|
||||||
|
return window.getSelection().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPermalink(): string {
|
||||||
|
if (!this.props.permalinkCreator) return;
|
||||||
|
return this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
|
||||||
|
}
|
||||||
|
|
||||||
private getUnsentReactions(): MatrixEvent[] {
|
private getUnsentReactions(): MatrixEvent[] {
|
||||||
return this.getReactions(e => e.status === EventStatus.NOT_SENT);
|
return this.getReactions(e => e.status === EventStatus.NOT_SENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
private viewInRoom = () => {
|
private viewInRoom = (): void => {
|
||||||
dis.dispatch<ViewRoomPayload>({
|
dis.dispatch<ViewRoomPayload>({
|
||||||
action: Action.ViewRoom,
|
action: Action.ViewRoom,
|
||||||
event_id: this.props.mxEvent.getId(),
|
event_id: this.props.mxEvent.getId(),
|
||||||
|
@ -273,12 +334,22 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
||||||
this.closeMenu();
|
this.closeMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const me = cli.getUserId();
|
const me = cli.getUserId();
|
||||||
const mxEvent = this.props.mxEvent;
|
const { mxEvent, rightClick, showPermalink, eventTileOps, reactions, collapseReplyChain } = this.props;
|
||||||
const eventStatus = mxEvent.status;
|
const eventStatus = mxEvent.status;
|
||||||
const unsentReactionsCount = this.getUnsentReactions().length;
|
const unsentReactionsCount = this.getUnsentReactions().length;
|
||||||
|
const contentActionable = isContentActionable(mxEvent);
|
||||||
|
const permalink = this.getPermalink();
|
||||||
|
// status is SENT before remote-echo, null after
|
||||||
|
const isSent = !eventStatus || eventStatus === EventStatus.SENT;
|
||||||
|
const { timelineRenderingType, canReact, canSendMessages } = this.context;
|
||||||
|
const isThread = (
|
||||||
|
timelineRenderingType === TimelineRenderingType.Thread ||
|
||||||
|
timelineRenderingType === TimelineRenderingType.ThreadsList
|
||||||
|
);
|
||||||
|
const isThreadRootEvent = isThread && mxEvent?.getThread()?.rootEvent === mxEvent;
|
||||||
|
|
||||||
let openInMapSiteButton: JSX.Element;
|
let openInMapSiteButton: JSX.Element;
|
||||||
let endPollButton: JSX.Element;
|
let endPollButton: JSX.Element;
|
||||||
|
@ -289,21 +360,27 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
||||||
let unhidePreviewButton: JSX.Element;
|
let unhidePreviewButton: JSX.Element;
|
||||||
let externalURLButton: JSX.Element;
|
let externalURLButton: JSX.Element;
|
||||||
let quoteButton: JSX.Element;
|
let quoteButton: JSX.Element;
|
||||||
let collapseReplyChain: JSX.Element;
|
|
||||||
let redactItemList: JSX.Element;
|
let redactItemList: JSX.Element;
|
||||||
|
let reportEventButton: JSX.Element;
|
||||||
|
let copyButton: JSX.Element;
|
||||||
|
let editButton: JSX.Element;
|
||||||
|
let replyButton: JSX.Element;
|
||||||
|
let reactButton: JSX.Element;
|
||||||
|
let reactionPicker: JSX.Element;
|
||||||
|
let quickItemsList: JSX.Element;
|
||||||
|
let nativeItemsList: JSX.Element;
|
||||||
|
let permalinkButton: JSX.Element;
|
||||||
|
let collapseReplyChainButton: JSX.Element;
|
||||||
|
let viewInRoomButton: JSX.Element;
|
||||||
|
|
||||||
// status is SENT before remote-echo, null after
|
if (!mxEvent.isRedacted() && unsentReactionsCount !== 0) {
|
||||||
const isSent = !eventStatus || eventStatus === EventStatus.SENT;
|
resendReactionsButton = (
|
||||||
if (!mxEvent.isRedacted()) {
|
<IconizedContextMenuOption
|
||||||
if (unsentReactionsCount !== 0) {
|
iconClassName="mx_MessageContextMenu_iconResend"
|
||||||
resendReactionsButton = (
|
label={_t('Resend %(unsentCount)s reaction(s)', { unsentCount: unsentReactionsCount })}
|
||||||
<IconizedContextMenuOption
|
onClick={this.onResendReactionsClick}
|
||||||
iconClassName="mx_MessageContextMenu_iconResend"
|
/>
|
||||||
label={_t('Resend %(unsentCount)s reaction(s)', { unsentCount: unsentReactionsCount })}
|
);
|
||||||
onClick={this.onResendReactionsClick}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSent && this.state.canRedact) {
|
if (isSent && this.state.canRedact) {
|
||||||
|
@ -335,26 +412,24 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isContentActionable(mxEvent)) {
|
if (contentActionable && canForward(mxEvent)) {
|
||||||
if (canForward(mxEvent)) {
|
forwardButton = (
|
||||||
forwardButton = (
|
<IconizedContextMenuOption
|
||||||
<IconizedContextMenuOption
|
iconClassName="mx_MessageContextMenu_iconForward"
|
||||||
iconClassName="mx_MessageContextMenu_iconForward"
|
label={_t("Forward")}
|
||||||
label={_t("Forward")}
|
onClick={this.onForwardClick}
|
||||||
onClick={this.onForwardClick}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.canPin) {
|
if (contentActionable && this.state.canPin) {
|
||||||
pinButton = (
|
pinButton = (
|
||||||
<IconizedContextMenuOption
|
<IconizedContextMenuOption
|
||||||
iconClassName="mx_MessageContextMenu_iconPin"
|
iconClassName="mx_MessageContextMenu_iconPin"
|
||||||
label={this.isPinned() ? _t('Unpin') : _t('Pin')}
|
label={this.isPinned() ? _t('Unpin') : _t('Pin')}
|
||||||
onClick={this.onPinClick}
|
onClick={this.onPinClick}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let viewSourceButton: JSX.Element;
|
let viewSourceButton: JSX.Element;
|
||||||
|
@ -368,39 +443,38 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.eventTileOps) {
|
if (eventTileOps?.isWidgetHidden()) {
|
||||||
if (this.props.eventTileOps.isWidgetHidden()) {
|
unhidePreviewButton = (
|
||||||
unhidePreviewButton = (
|
<IconizedContextMenuOption
|
||||||
<IconizedContextMenuOption
|
iconClassName="mx_MessageContextMenu_iconUnhidePreview"
|
||||||
iconClassName="mx_MessageContextMenu_iconUnhidePreview"
|
label={_t("Show preview")}
|
||||||
label={_t("Show preview")}
|
onClick={this.onUnhidePreviewClick}
|
||||||
onClick={this.onUnhidePreviewClick}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let permalink: string | null = null;
|
if (permalink) {
|
||||||
let permalinkButton: ReactElement | null = null;
|
permalinkButton = (
|
||||||
if (this.props.permalinkCreator) {
|
<IconizedContextMenuOption
|
||||||
permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
|
iconClassName={showPermalink
|
||||||
}
|
? "mx_MessageContextMenu_iconCopy"
|
||||||
permalinkButton = (
|
: "mx_MessageContextMenu_iconPermalink"
|
||||||
<IconizedContextMenuOption
|
|
||||||
iconClassName="mx_MessageContextMenu_iconPermalink"
|
|
||||||
onClick={this.onPermalinkClick}
|
|
||||||
label={_t('Share')}
|
|
||||||
element="a"
|
|
||||||
{
|
|
||||||
// XXX: Typescript signature for AccessibleButton doesn't work properly for non-inputs like `a`
|
|
||||||
...{
|
|
||||||
href: permalink,
|
|
||||||
target: "_blank",
|
|
||||||
rel: "noreferrer noopener",
|
|
||||||
}
|
}
|
||||||
}
|
onClick={showPermalink ? this.onCopyPermalinkClick : this.onPermalinkClick}
|
||||||
/>
|
label={showPermalink ? _t('Copy link') : _t('Share')}
|
||||||
);
|
element="a"
|
||||||
|
{
|
||||||
|
// XXX: Typescript signature for AccessibleButton doesn't work properly for non-inputs like `a`
|
||||||
|
...{
|
||||||
|
|
||||||
|
href: permalink,
|
||||||
|
target: "_blank",
|
||||||
|
rel: "noreferrer noopener",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.canEndPoll(mxEvent)) {
|
if (this.canEndPoll(mxEvent)) {
|
||||||
endPollButton = (
|
endPollButton = (
|
||||||
|
@ -412,7 +486,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.eventTileOps) { // this event is rendered using TextualBody
|
if (eventTileOps) { // this event is rendered using TextualBody
|
||||||
quoteButton = (
|
quoteButton = (
|
||||||
<IconizedContextMenuOption
|
<IconizedContextMenuOption
|
||||||
iconClassName="mx_MessageContextMenu_iconQuote"
|
iconClassName="mx_MessageContextMenu_iconQuote"
|
||||||
|
@ -423,7 +497,8 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bridges can provide a 'external_url' to link back to the source.
|
// Bridges can provide a 'external_url' to link back to the source.
|
||||||
if (typeof (mxEvent.getContent().external_url) === "string" &&
|
if (
|
||||||
|
typeof (mxEvent.getContent().external_url) === "string" &&
|
||||||
isUrlPermitted(mxEvent.getContent().external_url)
|
isUrlPermitted(mxEvent.getContent().external_url)
|
||||||
) {
|
) {
|
||||||
externalURLButton = (
|
externalURLButton = (
|
||||||
|
@ -444,8 +519,8 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.collapseReplyChain) {
|
if (collapseReplyChain) {
|
||||||
collapseReplyChain = (
|
collapseReplyChainButton = (
|
||||||
<IconizedContextMenuOption
|
<IconizedContextMenuOption
|
||||||
iconClassName="mx_MessageContextMenu_iconCollapse"
|
iconClassName="mx_MessageContextMenu_iconCollapse"
|
||||||
label={_t("Collapse reply thread")}
|
label={_t("Collapse reply thread")}
|
||||||
|
@ -454,7 +529,6 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let reportEventButton: JSX.Element;
|
|
||||||
if (mxEvent.getSender() !== me) {
|
if (mxEvent.getSender() !== me) {
|
||||||
reportEventButton = (
|
reportEventButton = (
|
||||||
<IconizedContextMenuOption
|
<IconizedContextMenuOption
|
||||||
|
@ -465,20 +539,79 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { timelineRenderingType } = this.context;
|
if (rightClick && this.getSelectedText()) {
|
||||||
const isThread = (
|
copyButton = (
|
||||||
timelineRenderingType === TimelineRenderingType.Thread ||
|
<IconizedContextMenuOption
|
||||||
timelineRenderingType === TimelineRenderingType.ThreadsList
|
iconClassName="mx_MessageContextMenu_iconCopy"
|
||||||
);
|
label={_t("Copy")}
|
||||||
const isThreadRootEvent = isThread && this.props.mxEvent.isThreadRoot;
|
triggerOnMouseDown={true} // We use onMouseDown so that the selection isn't cleared when we click
|
||||||
|
onClick={this.onCopyClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const commonItemsList = (
|
if (rightClick && canEditContent(mxEvent)) {
|
||||||
<IconizedContextMenuOptionList>
|
editButton = (
|
||||||
{ isThreadRootEvent && <IconizedContextMenuOption
|
<IconizedContextMenuOption
|
||||||
|
iconClassName="mx_MessageContextMenu_iconEdit"
|
||||||
|
label={_t("Edit")}
|
||||||
|
onClick={this.onEditClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rightClick && contentActionable && canSendMessages) {
|
||||||
|
replyButton = (
|
||||||
|
<IconizedContextMenuOption
|
||||||
|
iconClassName="mx_MessageContextMenu_iconReply"
|
||||||
|
label={_t("Reply")}
|
||||||
|
onClick={this.onReplyClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rightClick && contentActionable && canReact) {
|
||||||
|
reactButton = (
|
||||||
|
<IconizedContextMenuOption
|
||||||
|
iconClassName="mx_MessageContextMenu_iconReact"
|
||||||
|
label={_t("React")}
|
||||||
|
onClick={this.onReactClick}
|
||||||
|
inputRef={this.reactButtonRef}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isThreadRootEvent) {
|
||||||
|
viewInRoomButton = (
|
||||||
|
<IconizedContextMenuOption
|
||||||
iconClassName="mx_MessageContextMenu_iconViewInRoom"
|
iconClassName="mx_MessageContextMenu_iconViewInRoom"
|
||||||
label={_t("View in room")}
|
label={_t("View in room")}
|
||||||
onClick={this.viewInRoom}
|
onClick={this.viewInRoom}
|
||||||
/> }
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (copyButton) {
|
||||||
|
nativeItemsList = (
|
||||||
|
<IconizedContextMenuOptionList>
|
||||||
|
{ copyButton }
|
||||||
|
</IconizedContextMenuOptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editButton || replyButton || reactButton) {
|
||||||
|
quickItemsList = (
|
||||||
|
<IconizedContextMenuOptionList>
|
||||||
|
{ reactButton }
|
||||||
|
{ replyButton }
|
||||||
|
{ editButton }
|
||||||
|
</IconizedContextMenuOptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const commonItemsList = (
|
||||||
|
<IconizedContextMenuOptionList>
|
||||||
|
{ viewInRoomButton }
|
||||||
{ openInMapSiteButton }
|
{ openInMapSiteButton }
|
||||||
{ endPollButton }
|
{ endPollButton }
|
||||||
{ quoteButton }
|
{ quoteButton }
|
||||||
|
@ -490,7 +623,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
||||||
{ unhidePreviewButton }
|
{ unhidePreviewButton }
|
||||||
{ viewSourceButton }
|
{ viewSourceButton }
|
||||||
{ resendReactionsButton }
|
{ resendReactionsButton }
|
||||||
{ collapseReplyChain }
|
{ collapseReplyChainButton }
|
||||||
</IconizedContextMenuOptionList>
|
</IconizedContextMenuOptionList>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -501,15 +634,38 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
||||||
</IconizedContextMenuOptionList>
|
</IconizedContextMenuOptionList>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.state.reactionPickerDisplayed) {
|
||||||
|
const buttonRect = (this.reactButtonRef.current as HTMLElement)?.getBoundingClientRect();
|
||||||
|
reactionPicker = (
|
||||||
|
<ContextMenu
|
||||||
|
{...toRightOf(buttonRect)}
|
||||||
|
onFinished={this.closeMenu}
|
||||||
|
managed={false}
|
||||||
|
>
|
||||||
|
<ReactionPicker
|
||||||
|
mxEvent={mxEvent}
|
||||||
|
onFinished={this.onCloseReactionPicker}
|
||||||
|
reactions={reactions}
|
||||||
|
/>
|
||||||
|
</ContextMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconizedContextMenu
|
<React.Fragment>
|
||||||
{...this.props}
|
<IconizedContextMenu
|
||||||
className="mx_MessageContextMenu"
|
{...this.props}
|
||||||
compact={true}
|
className="mx_MessageContextMenu"
|
||||||
>
|
compact={true}
|
||||||
{ commonItemsList }
|
>
|
||||||
{ redactItemList }
|
{ nativeItemsList }
|
||||||
</IconizedContextMenu>
|
{ quickItemsList }
|
||||||
|
{ commonItemsList }
|
||||||
|
{ redactItemList }
|
||||||
|
</IconizedContextMenu>
|
||||||
|
{ reactionPicker }
|
||||||
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,6 +53,7 @@ interface IProps extends React.InputHTMLAttributes<Element> {
|
||||||
tabIndex?: number;
|
tabIndex?: number;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
triggerOnMouseDown?: boolean;
|
||||||
onClick(e?: ButtonEvent): void | Promise<void>;
|
onClick(e?: ButtonEvent): void | Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,13 +79,18 @@ export default function AccessibleButton({
|
||||||
className,
|
className,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
onKeyUp,
|
onKeyUp,
|
||||||
|
triggerOnMouseDown,
|
||||||
...restProps
|
...restProps
|
||||||
}: IProps) {
|
}: IProps) {
|
||||||
const newProps: IAccessibleButtonProps = restProps;
|
const newProps: IAccessibleButtonProps = restProps;
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
newProps["aria-disabled"] = true;
|
newProps["aria-disabled"] = true;
|
||||||
} else {
|
} else {
|
||||||
newProps.onClick = onClick;
|
if (triggerOnMouseDown) {
|
||||||
|
newProps.onMouseDown = onClick;
|
||||||
|
} else {
|
||||||
|
newProps.onClick = onClick;
|
||||||
|
}
|
||||||
// We need to consume enter onKeyDown and space onKeyUp
|
// We need to consume enter onKeyDown and space onKeyUp
|
||||||
// otherwise we are risking also activating other keyboard focusable elements
|
// otherwise we are risking also activating other keyboard focusable elements
|
||||||
// that might receive focus as a result of the AccessibleButtonClick action
|
// that might receive focus as a result of the AccessibleButtonClick action
|
||||||
|
|
|
@ -23,7 +23,7 @@ import Tooltip, { Alignment } from './Tooltip';
|
||||||
interface IProps extends React.ComponentProps<typeof AccessibleButton> {
|
interface IProps extends React.ComponentProps<typeof AccessibleButton> {
|
||||||
title: string;
|
title: string;
|
||||||
tooltip?: React.ReactNode;
|
tooltip?: React.ReactNode;
|
||||||
label?: React.ReactNode;
|
label?: string;
|
||||||
tooltipClassName?: string;
|
tooltipClassName?: string;
|
||||||
forceHide?: boolean;
|
forceHide?: boolean;
|
||||||
yOffset?: number;
|
yOffset?: number;
|
||||||
|
|
|
@ -38,6 +38,8 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import { E2EState } from "./E2EIcon";
|
import { E2EState } from "./E2EIcon";
|
||||||
import { toRem } from "../../../utils/units";
|
import { toRem } from "../../../utils/units";
|
||||||
import RoomAvatar from "../avatars/RoomAvatar";
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
|
import MessageContextMenu, { IEventTileOps } from "../context_menus/MessageContextMenu";
|
||||||
|
import { aboveLeftOf } from '../../structures/ContextMenu';
|
||||||
import { objectHasDiff } from "../../../utils/objects";
|
import { objectHasDiff } from "../../../utils/objects";
|
||||||
import Tooltip from "../elements/Tooltip";
|
import Tooltip from "../elements/Tooltip";
|
||||||
import EditorStateTransfer from "../../../utils/EditorStateTransfer";
|
import EditorStateTransfer from "../../../utils/EditorStateTransfer";
|
||||||
|
@ -47,6 +49,7 @@ import NotificationBadge from "./NotificationBadge";
|
||||||
import CallEventGrouper from "../../structures/CallEventGrouper";
|
import CallEventGrouper from "../../structures/CallEventGrouper";
|
||||||
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||||
import { Action } from '../../../dispatcher/actions';
|
import { Action } from '../../../dispatcher/actions';
|
||||||
|
import PlatformPeg from '../../../PlatformPeg';
|
||||||
import MemberAvatar from '../avatars/MemberAvatar';
|
import MemberAvatar from '../avatars/MemberAvatar';
|
||||||
import SenderProfile from '../messages/SenderProfile';
|
import SenderProfile from '../messages/SenderProfile';
|
||||||
import MessageTimestamp from '../messages/MessageTimestamp';
|
import MessageTimestamp from '../messages/MessageTimestamp';
|
||||||
|
@ -96,6 +99,10 @@ export interface IReadReceiptProps {
|
||||||
ts: number;
|
ts: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IEventTileType extends React.Component {
|
||||||
|
getEventTileOps?(): IEventTileOps;
|
||||||
|
}
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
// the MatrixEvent to show
|
// the MatrixEvent to show
|
||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
|
@ -220,6 +227,13 @@ interface IState {
|
||||||
reactions: Relations;
|
reactions: Relations;
|
||||||
|
|
||||||
hover: boolean;
|
hover: boolean;
|
||||||
|
|
||||||
|
// Position of the context menu
|
||||||
|
contextMenu?: {
|
||||||
|
position: Pick<DOMRect, "right" | "top" | "bottom">;
|
||||||
|
showPermalink?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
isQuoteExpanded?: boolean;
|
isQuoteExpanded?: boolean;
|
||||||
|
|
||||||
thread: Thread;
|
thread: Thread;
|
||||||
|
@ -230,8 +244,7 @@ interface IState {
|
||||||
export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
||||||
private suppressReadReceiptAnimation: boolean;
|
private suppressReadReceiptAnimation: boolean;
|
||||||
private isListeningForReceipts: boolean;
|
private isListeningForReceipts: boolean;
|
||||||
// TODO: Types
|
private tile = React.createRef<IEventTileType>();
|
||||||
private tile = React.createRef<unknown>();
|
|
||||||
private replyChain = React.createRef<ReplyChain>();
|
private replyChain = React.createRef<ReplyChain>();
|
||||||
private threadState: ThreadNotificationState;
|
private threadState: ThreadNotificationState;
|
||||||
|
|
||||||
|
@ -264,6 +277,8 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
||||||
previouslyRequestedKeys: false,
|
previouslyRequestedKeys: false,
|
||||||
// The Relations model from the JS SDK for reactions to `mxEvent`
|
// The Relations model from the JS SDK for reactions to `mxEvent`
|
||||||
reactions: this.getReactions(),
|
reactions: this.getReactions(),
|
||||||
|
// Context menu position
|
||||||
|
contextMenu: null,
|
||||||
|
|
||||||
hover: false,
|
hover: false,
|
||||||
|
|
||||||
|
@ -898,10 +913,10 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
||||||
private onActionBarFocusChange = (actionBarFocused: boolean) => {
|
private onActionBarFocusChange = (actionBarFocused: boolean) => {
|
||||||
this.setState({ actionBarFocused });
|
this.setState({ actionBarFocused });
|
||||||
};
|
};
|
||||||
// TODO: Types
|
|
||||||
private getTile: () => any | null = () => this.tile.current;
|
|
||||||
|
|
||||||
private getReplyChain = () => this.replyChain.current;
|
private getTile: () => IEventTileType = () => this.tile.current;
|
||||||
|
|
||||||
|
private getReplyChain = (): ReplyChain => this.replyChain.current;
|
||||||
|
|
||||||
private getReactions = () => {
|
private getReactions = () => {
|
||||||
if (
|
if (
|
||||||
|
@ -923,6 +938,44 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onContextMenu = (ev: React.MouseEvent): void => {
|
||||||
|
this.showContextMenu(ev);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onTimestampContextMenu = (ev: React.MouseEvent): void => {
|
||||||
|
this.showContextMenu(ev, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
private showContextMenu(ev: React.MouseEvent, showPermalink?: boolean): void {
|
||||||
|
if (!SettingsStore.getValue("feature_message_right_click_context_menu")) return;
|
||||||
|
// There is no way to copy non-PNG images into clipboard, so we can't
|
||||||
|
// have our own handling for copying images, so we leave it to the
|
||||||
|
// Electron layer (webcontents-handler.ts)
|
||||||
|
if (ev.target instanceof HTMLImageElement) return;
|
||||||
|
if (!PlatformPeg.get().allowOverridingNativeContextMenus()) return;
|
||||||
|
if (this.props.editState) return;
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
this.setState({
|
||||||
|
contextMenu: {
|
||||||
|
position: {
|
||||||
|
right: ev.clientX,
|
||||||
|
top: ev.clientY,
|
||||||
|
bottom: ev.clientY,
|
||||||
|
},
|
||||||
|
showPermalink: showPermalink,
|
||||||
|
},
|
||||||
|
actionBarFocused: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private onCloseMenu = (): void => {
|
||||||
|
this.setState({
|
||||||
|
contextMenu: null,
|
||||||
|
actionBarFocused: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
private setQuoteExpanded = (expanded: boolean) => {
|
private setQuoteExpanded = (expanded: boolean) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
isQuoteExpanded: expanded,
|
isQuoteExpanded: expanded,
|
||||||
|
@ -941,6 +994,29 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderContextMenu(): React.ReactFragment {
|
||||||
|
if (!this.state.contextMenu) return null;
|
||||||
|
|
||||||
|
const tile = this.getTile();
|
||||||
|
const replyChain = this.getReplyChain();
|
||||||
|
const eventTileOps = tile?.getEventTileOps ? tile.getEventTileOps() : undefined;
|
||||||
|
const collapseReplyChain = replyChain?.canCollapse() ? replyChain.collapse : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MessageContextMenu
|
||||||
|
{...aboveLeftOf(this.state.contextMenu.position)}
|
||||||
|
mxEvent={this.props.mxEvent}
|
||||||
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
|
eventTileOps={eventTileOps}
|
||||||
|
collapseReplyChain={collapseReplyChain}
|
||||||
|
onFinished={this.onCloseMenu}
|
||||||
|
rightClick={true}
|
||||||
|
reactions={this.state.reactions}
|
||||||
|
showPermalink={this.state.contextMenu.showPermalink}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const msgtype = this.props.mxEvent.getContent().msgtype;
|
const msgtype = this.props.mxEvent.getContent().msgtype;
|
||||||
const eventType = this.props.mxEvent.getType() as EventType;
|
const eventType = this.props.mxEvent.getType() as EventType;
|
||||||
|
@ -1004,8 +1080,10 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
||||||
mx_EventTile_12hr: this.props.isTwelveHour,
|
mx_EventTile_12hr: this.props.isTwelveHour,
|
||||||
// Note: we keep the `sending` state class for tests, not for our styles
|
// Note: we keep the `sending` state class for tests, not for our styles
|
||||||
mx_EventTile_sending: !isEditing && isSending,
|
mx_EventTile_sending: !isEditing && isSending,
|
||||||
mx_EventTile_highlight: this.shouldHighlight(),
|
mx_EventTile_highlight: (this.context.timelineRenderingType === TimelineRenderingType.Notification
|
||||||
mx_EventTile_selected: this.props.isSelectedEvent,
|
? false
|
||||||
|
: this.shouldHighlight()),
|
||||||
|
mx_EventTile_selected: this.props.isSelectedEvent || this.state.contextMenu,
|
||||||
mx_EventTile_continuation: isContinuation || eventType === EventType.CallInvite,
|
mx_EventTile_continuation: isContinuation || eventType === EventType.CallInvite,
|
||||||
mx_EventTile_last: this.props.last,
|
mx_EventTile_last: this.props.last,
|
||||||
mx_EventTile_lastInSection: this.props.lastInSection,
|
mx_EventTile_lastInSection: this.props.lastInSection,
|
||||||
|
@ -1126,7 +1204,8 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
||||||
&& (this.props.alwaysShowTimestamps
|
&& (this.props.alwaysShowTimestamps
|
||||||
|| this.props.last
|
|| this.props.last
|
||||||
|| this.state.hover
|
|| this.state.hover
|
||||||
|| this.state.actionBarFocused);
|
|| this.state.actionBarFocused
|
||||||
|
|| Boolean(this.state.contextMenu));
|
||||||
|
|
||||||
// Thread panel shows the timestamp of the last reply in that thread
|
// Thread panel shows the timestamp of the last reply in that thread
|
||||||
const ts = this.context.timelineRenderingType !== TimelineRenderingType.ThreadsList
|
const ts = this.context.timelineRenderingType !== TimelineRenderingType.ThreadsList
|
||||||
|
@ -1197,6 +1276,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
||||||
href={permalink}
|
href={permalink}
|
||||||
onClick={this.onPermalinkClicked}
|
onClick={this.onPermalinkClicked}
|
||||||
aria-label={formatTime(new Date(this.props.mxEvent.getTs()), this.props.isTwelveHour)}
|
aria-label={formatTime(new Date(this.props.mxEvent.getTs()), this.props.isTwelveHour)}
|
||||||
|
onContextMenu={this.onTimestampContextMenu}
|
||||||
>
|
>
|
||||||
{ timestamp }
|
{ timestamp }
|
||||||
</a>;
|
</a>;
|
||||||
|
@ -1252,12 +1332,17 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
||||||
</div>,
|
</div>,
|
||||||
<div className="mx_EventTile_senderDetails" key="mx_EventTile_senderDetails">
|
<div className="mx_EventTile_senderDetails" key="mx_EventTile_senderDetails">
|
||||||
{ avatar }
|
{ avatar }
|
||||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
<a
|
||||||
|
href={permalink}
|
||||||
|
onClick={this.onPermalinkClicked}
|
||||||
|
onContextMenu={this.onTimestampContextMenu}
|
||||||
|
>
|
||||||
{ sender }
|
{ sender }
|
||||||
{ timestamp }
|
{ timestamp }
|
||||||
</a>
|
</a>
|
||||||
</div>,
|
</div>,
|
||||||
<div className={lineClasses} key="mx_EventTile_line">
|
<div className={lineClasses} key="mx_EventTile_line" onContextMenu={this.onContextMenu}>
|
||||||
|
{ this.renderContextMenu() }
|
||||||
{ renderTile(TimelineRenderingType.Notification, {
|
{ renderTile(TimelineRenderingType.Notification, {
|
||||||
...this.props,
|
...this.props,
|
||||||
|
|
||||||
|
@ -1298,7 +1383,8 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
||||||
{ avatar }
|
{ avatar }
|
||||||
{ sender }
|
{ sender }
|
||||||
</div>,
|
</div>,
|
||||||
<div className={lineClasses} key="mx_EventTile_line">
|
<div className={lineClasses} key="mx_EventTile_line" onContextMenu={this.onContextMenu}>
|
||||||
|
{ this.renderContextMenu() }
|
||||||
{ replyChain }
|
{ replyChain }
|
||||||
{ renderTile(TimelineRenderingType.Thread, {
|
{ renderTile(TimelineRenderingType.Thread, {
|
||||||
...this.props,
|
...this.props,
|
||||||
|
@ -1385,7 +1471,8 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
||||||
"aria-atomic": true,
|
"aria-atomic": true,
|
||||||
"data-scroll-tokens": scrollToken,
|
"data-scroll-tokens": scrollToken,
|
||||||
}, [
|
}, [
|
||||||
<div className={lineClasses} key="mx_EventTile_line">
|
<div className={lineClasses} key="mx_EventTile_line" onContextMenu={this.onContextMenu}>
|
||||||
|
{ this.renderContextMenu() }
|
||||||
{ renderTile(TimelineRenderingType.File, {
|
{ renderTile(TimelineRenderingType.File, {
|
||||||
...this.props,
|
...this.props,
|
||||||
|
|
||||||
|
@ -1406,7 +1493,10 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
||||||
href={permalink}
|
href={permalink}
|
||||||
onClick={this.onPermalinkClicked}
|
onClick={this.onPermalinkClicked}
|
||||||
>
|
>
|
||||||
<div className="mx_EventTile_senderDetails">
|
<div
|
||||||
|
className="mx_EventTile_senderDetails"
|
||||||
|
onContextMenu={this.onTimestampContextMenu}
|
||||||
|
>
|
||||||
{ sender }
|
{ sender }
|
||||||
{ timestamp }
|
{ timestamp }
|
||||||
</div>
|
</div>
|
||||||
|
@ -1434,7 +1524,8 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
||||||
{ sender }
|
{ sender }
|
||||||
{ ircPadlock }
|
{ ircPadlock }
|
||||||
{ avatar }
|
{ avatar }
|
||||||
<div className={lineClasses} key="mx_EventTile_line">
|
<div className={lineClasses} key="mx_EventTile_line" onContextMenu={this.onContextMenu}>
|
||||||
|
{ this.renderContextMenu() }
|
||||||
{ groupTimestamp }
|
{ groupTimestamp }
|
||||||
{ groupPadlock }
|
{ groupPadlock }
|
||||||
{ replyChain }
|
{ replyChain }
|
||||||
|
|
|
@ -897,6 +897,7 @@
|
||||||
"Right panel stays open (defaults to room member list)": "Right panel stays open (defaults to room member list)",
|
"Right panel stays open (defaults to room member list)": "Right panel stays open (defaults to room member list)",
|
||||||
"Jump to date (adds /jumptodate and jump to date headers)": "Jump to date (adds /jumptodate and jump to date headers)",
|
"Jump to date (adds /jumptodate and jump to date headers)": "Jump to date (adds /jumptodate and jump to date headers)",
|
||||||
"Don't send read receipts": "Don't send read receipts",
|
"Don't send read receipts": "Don't send read receipts",
|
||||||
|
"Right-click message context menu": "Right-click message context menu",
|
||||||
"Location sharing - pin drop (under active development)": "Location sharing - pin drop (under active development)",
|
"Location sharing - pin drop (under active development)": "Location sharing - pin drop (under active development)",
|
||||||
"Live location sharing - share current location (active development, and temporarily, locations persist in room history)": "Live location sharing - share current location (active development, and temporarily, locations persist in room history)",
|
"Live location sharing - share current location (active development, and temporarily, locations persist in room history)": "Live location sharing - share current location (active development, and temporarily, locations persist in room history)",
|
||||||
"Font size": "Font size",
|
"Font size": "Font size",
|
||||||
|
@ -2882,6 +2883,7 @@
|
||||||
"Forward": "Forward",
|
"Forward": "Forward",
|
||||||
"View source": "View source",
|
"View source": "View source",
|
||||||
"Show preview": "Show preview",
|
"Show preview": "Show preview",
|
||||||
|
"Copy link": "Copy link",
|
||||||
"Source URL": "Source URL",
|
"Source URL": "Source URL",
|
||||||
"Collapse reply thread": "Collapse reply thread",
|
"Collapse reply thread": "Collapse reply thread",
|
||||||
"Report": "Report",
|
"Report": "Report",
|
||||||
|
|
|
@ -414,6 +414,13 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
displayName: _td("Don't send read receipts"),
|
displayName: _td("Don't send read receipts"),
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
"feature_message_right_click_context_menu": {
|
||||||
|
isFeature: true,
|
||||||
|
supportedLevels: LEVELS_FEATURE,
|
||||||
|
labsGroup: LabGroup.Rooms,
|
||||||
|
displayName: _td("Right-click message context menu"),
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
"feature_location_share_pin_drop": {
|
"feature_location_share_pin_drop": {
|
||||||
isFeature: true,
|
isFeature: true,
|
||||||
labsGroup: LabGroup.Messaging,
|
labsGroup: LabGroup.Messaging,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue