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:
Šimon Brandner 2022-04-15 16:22:59 +02:00 committed by GitHub
parent 77b0addbc7
commit d162e021e1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 408 additions and 123 deletions

View file

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