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
|
@ -38,6 +38,8 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
|||
import { E2EState } from "./E2EIcon";
|
||||
import { toRem } from "../../../utils/units";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import MessageContextMenu, { IEventTileOps } from "../context_menus/MessageContextMenu";
|
||||
import { aboveLeftOf } from '../../structures/ContextMenu';
|
||||
import { objectHasDiff } from "../../../utils/objects";
|
||||
import Tooltip from "../elements/Tooltip";
|
||||
import EditorStateTransfer from "../../../utils/EditorStateTransfer";
|
||||
|
@ -47,6 +49,7 @@ import NotificationBadge from "./NotificationBadge";
|
|||
import CallEventGrouper from "../../structures/CallEventGrouper";
|
||||
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||
import { Action } from '../../../dispatcher/actions';
|
||||
import PlatformPeg from '../../../PlatformPeg';
|
||||
import MemberAvatar from '../avatars/MemberAvatar';
|
||||
import SenderProfile from '../messages/SenderProfile';
|
||||
import MessageTimestamp from '../messages/MessageTimestamp';
|
||||
|
@ -96,6 +99,10 @@ export interface IReadReceiptProps {
|
|||
ts: number;
|
||||
}
|
||||
|
||||
export interface IEventTileType extends React.Component {
|
||||
getEventTileOps?(): IEventTileOps;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
// the MatrixEvent to show
|
||||
mxEvent: MatrixEvent;
|
||||
|
@ -220,6 +227,13 @@ interface IState {
|
|||
reactions: Relations;
|
||||
|
||||
hover: boolean;
|
||||
|
||||
// Position of the context menu
|
||||
contextMenu?: {
|
||||
position: Pick<DOMRect, "right" | "top" | "bottom">;
|
||||
showPermalink?: boolean;
|
||||
};
|
||||
|
||||
isQuoteExpanded?: boolean;
|
||||
|
||||
thread: Thread;
|
||||
|
@ -230,8 +244,7 @@ interface IState {
|
|||
export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
||||
private suppressReadReceiptAnimation: boolean;
|
||||
private isListeningForReceipts: boolean;
|
||||
// TODO: Types
|
||||
private tile = React.createRef<unknown>();
|
||||
private tile = React.createRef<IEventTileType>();
|
||||
private replyChain = React.createRef<ReplyChain>();
|
||||
private threadState: ThreadNotificationState;
|
||||
|
||||
|
@ -264,6 +277,8 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
previouslyRequestedKeys: false,
|
||||
// The Relations model from the JS SDK for reactions to `mxEvent`
|
||||
reactions: this.getReactions(),
|
||||
// Context menu position
|
||||
contextMenu: null,
|
||||
|
||||
hover: false,
|
||||
|
||||
|
@ -898,10 +913,10 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
private onActionBarFocusChange = (actionBarFocused: boolean) => {
|
||||
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 = () => {
|
||||
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) => {
|
||||
this.setState({
|
||||
isQuoteExpanded: expanded,
|
||||
|
@ -941,6 +994,29 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
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() {
|
||||
const msgtype = this.props.mxEvent.getContent().msgtype;
|
||||
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,
|
||||
// Note: we keep the `sending` state class for tests, not for our styles
|
||||
mx_EventTile_sending: !isEditing && isSending,
|
||||
mx_EventTile_highlight: this.shouldHighlight(),
|
||||
mx_EventTile_selected: this.props.isSelectedEvent,
|
||||
mx_EventTile_highlight: (this.context.timelineRenderingType === TimelineRenderingType.Notification
|
||||
? false
|
||||
: this.shouldHighlight()),
|
||||
mx_EventTile_selected: this.props.isSelectedEvent || this.state.contextMenu,
|
||||
mx_EventTile_continuation: isContinuation || eventType === EventType.CallInvite,
|
||||
mx_EventTile_last: this.props.last,
|
||||
mx_EventTile_lastInSection: this.props.lastInSection,
|
||||
|
@ -1126,7 +1204,8 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
&& (this.props.alwaysShowTimestamps
|
||||
|| this.props.last
|
||||
|| 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
|
||||
const ts = this.context.timelineRenderingType !== TimelineRenderingType.ThreadsList
|
||||
|
@ -1197,6 +1276,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
href={permalink}
|
||||
onClick={this.onPermalinkClicked}
|
||||
aria-label={formatTime(new Date(this.props.mxEvent.getTs()), this.props.isTwelveHour)}
|
||||
onContextMenu={this.onTimestampContextMenu}
|
||||
>
|
||||
{ timestamp }
|
||||
</a>;
|
||||
|
@ -1252,12 +1332,17 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
</div>,
|
||||
<div className="mx_EventTile_senderDetails" key="mx_EventTile_senderDetails">
|
||||
{ avatar }
|
||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
<a
|
||||
href={permalink}
|
||||
onClick={this.onPermalinkClicked}
|
||||
onContextMenu={this.onTimestampContextMenu}
|
||||
>
|
||||
{ sender }
|
||||
{ timestamp }
|
||||
</a>
|
||||
</div>,
|
||||
<div className={lineClasses} key="mx_EventTile_line">
|
||||
<div className={lineClasses} key="mx_EventTile_line" onContextMenu={this.onContextMenu}>
|
||||
{ this.renderContextMenu() }
|
||||
{ renderTile(TimelineRenderingType.Notification, {
|
||||
...this.props,
|
||||
|
||||
|
@ -1298,7 +1383,8 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
{ avatar }
|
||||
{ sender }
|
||||
</div>,
|
||||
<div className={lineClasses} key="mx_EventTile_line">
|
||||
<div className={lineClasses} key="mx_EventTile_line" onContextMenu={this.onContextMenu}>
|
||||
{ this.renderContextMenu() }
|
||||
{ replyChain }
|
||||
{ renderTile(TimelineRenderingType.Thread, {
|
||||
...this.props,
|
||||
|
@ -1385,7 +1471,8 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
"aria-atomic": true,
|
||||
"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, {
|
||||
...this.props,
|
||||
|
||||
|
@ -1406,7 +1493,10 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
href={permalink}
|
||||
onClick={this.onPermalinkClicked}
|
||||
>
|
||||
<div className="mx_EventTile_senderDetails">
|
||||
<div
|
||||
className="mx_EventTile_senderDetails"
|
||||
onContextMenu={this.onTimestampContextMenu}
|
||||
>
|
||||
{ sender }
|
||||
{ timestamp }
|
||||
</div>
|
||||
|
@ -1434,7 +1524,8 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
{ sender }
|
||||
{ ircPadlock }
|
||||
{ avatar }
|
||||
<div className={lineClasses} key="mx_EventTile_line">
|
||||
<div className={lineClasses} key="mx_EventTile_line" onContextMenu={this.onContextMenu}>
|
||||
{ this.renderContextMenu() }
|
||||
{ groupTimestamp }
|
||||
{ groupPadlock }
|
||||
{ replyChain }
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue