From 9d0c84a2394410841dc12f83bd46487a1c5e8246 Mon Sep 17 00:00:00 2001 From: Nik Rozman Date: Thu, 17 Apr 2025 23:36:41 +0200 Subject: [PATCH] feat: shift modifier shortcut for message row --- res/css/views/messages/_MessageActionBar.pcss | 10 ++- .../views/messages/MessageActionBar.tsx | 76 +++++++++++++++---- src/components/views/rooms/EventTile.tsx | 56 +++++++++++--- 3 files changed, 115 insertions(+), 27 deletions(-) diff --git a/res/css/views/messages/_MessageActionBar.pcss b/res/css/views/messages/_MessageActionBar.pcss index 61897bb34f..e2a19be265 100644 --- a/res/css/views/messages/_MessageActionBar.pcss +++ b/res/css/views/messages/_MessageActionBar.pcss @@ -134,4 +134,12 @@ Please see LICENSE files in the repository root for full details. .mx_MessageActionBar_optionsButton { --MessageActionBar-icon-size: 22px; } -} + + .mx_MessageActionBar_deleteButton { + color: var(--cpd-color-red-800); + + &:hover { + color: var(--cpd-color-red-600); + } + } +} \ No newline at end of file diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx index eb109028d9..d62065c067 100644 --- a/src/components/views/messages/MessageActionBar.tsx +++ b/src/components/views/messages/MessageActionBar.tsx @@ -53,8 +53,6 @@ import type ReplyChain from "../elements/ReplyChain"; import ReactionPicker from "../emojipicker/ReactionPicker"; import { CardContext } from "../right_panel/context"; import { shouldDisplayReply } from "../../../utils/Reply"; -import { Key } from "../../../Keyboard"; -import { ALTERNATE_KEY_NAME } from "../../../accessibility/KeyboardShortcuts"; import { Action } from "../../../dispatcher/actions"; import { type ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload"; import { type GetRelationsForEvent, type IEventTileType } from "../rooms/EventTile"; @@ -62,6 +60,8 @@ import { type ButtonEvent } from "../elements/AccessibleButton"; import PinningUtils from "../../../utils/PinningUtils"; import PosthogTrackers from "../../../PosthogTrackers.ts"; import { HideActionButton } from "./HideActionButton.tsx"; +import { ALTERNATE_KEY_NAME } from "../../../accessibility/KeyboardShortcuts.ts"; +import { Key } from "../../../Keyboard.ts"; interface IOptionsButtonProps { mxEvent: MatrixEvent; @@ -258,12 +258,27 @@ interface IMessageActionBarProps { toggleThreadExpanded: () => void; isQuoteExpanded?: boolean; getRelationsForEvent?: GetRelationsForEvent; + isShiftHeld?: boolean; + setupShiftKeyListener?: () => void; + removeShiftKeyListener?: () => void; } export default class MessageActionBar extends React.PureComponent { public static contextType = RoomContext; declare public context: React.ContextType; + private onDelete = async (ev: ButtonEvent): Promise => { + ev.preventDefault(); + ev.stopPropagation(); + + const cli = MatrixClientPeg.safeGet(); + try { + await cli.redactEvent(this.props.mxEvent.getRoomId()!, this.props.mxEvent.getId()!); + } catch (e) { + console.error("Error redacting event: ", e); + } + }; + public componentDidMount(): void { if (this.props.mxEvent.status && this.props.mxEvent.status !== EventStatus.SENT) { this.props.mxEvent.on(MatrixEventEvent.Status, this.onSent); @@ -410,7 +425,17 @@ export default class MessageActionBar extends React.PureComponent { + this.props.setupShiftKeyListener?.(); + }; + + private onMouseLeave = (): void => { + this.props.removeShiftKeyListener?.(); + }; + public render(): React.ReactNode { + const isRedacted = this.props.mxEvent.isRedacted(); + const toolbarOpts: JSX.Element[] = []; if (canEditContent(MatrixClientPeg.safeGet(), this.props.mxEvent)) { toolbarOpts.push( @@ -577,23 +602,44 @@ export default class MessageActionBar extends React.PureComponent, - ); + // Replace the menu button with delete button when shift is held + if (this.props.isShiftHeld && !isRedacted) { + toolbarOpts.push( + + + , + ); + } else { + toolbarOpts.push( + , + ); + } } // aria-live=off to not have this read out automatically as navigating around timeline, gets repetitive. return ( - + {toolbarOpts} ); diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 7baa127f16..c45ce36490 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -261,6 +261,8 @@ interface IState { thread: Thread | null; threadNotification?: NotificationCountType; + + isShiftHeld: boolean; } /** @@ -315,6 +317,8 @@ export class UnwrappedEventTile extends React.Component hover: false, thread, + + isShiftHeld: false, }; // don't do RR animations until we are mounted @@ -441,6 +445,7 @@ export class UnwrappedEventTile extends React.Component this.props.mxEvent.off(ThreadEvent.Update, this.updateThread); this.unmounted = false; if (this.props.resizeObserver && this.ref.current) this.props.resizeObserver.unobserve(this.ref.current); + this.removeShiftKeyListener(); } public componentDidUpdate(prevProps: Readonly, prevState: Readonly): void { @@ -798,6 +803,34 @@ export class UnwrappedEventTile extends React.Component this.setState({ actionBarFocused }); }; + private checkShiftKey = (): void => { + if (window.event instanceof MouseEvent) { + this.setState({ isShiftHeld: (window.event as unknown as MouseEvent).shiftKey }); + } + }; + + private setupShiftKeyListener = (): void => { + document.addEventListener("keydown", this.checkShiftKey); + document.addEventListener("keyup", this.checkShiftKey); + this.checkShiftKey(); + }; + + private removeShiftKeyListener = (): void => { + document.removeEventListener("keydown", this.checkShiftKey); + document.removeEventListener("keyup", this.checkShiftKey); + this.setState({ isShiftHeld: false }); + }; + + private onMouseEnter = (): void => { + this.setState({ hover: true }); + this.setupShiftKeyListener(); + }; + + private onMouseLeave = (): void => { + this.setState({ hover: false }); + this.removeShiftKeyListener(); + }; + private getTile: () => IEventTileType | null = () => this.tile.current; private getReplyChain = (): ReplyChain | null => this.replyChain.current; @@ -1102,6 +1135,9 @@ export class UnwrappedEventTile extends React.Component isQuoteExpanded={isQuoteExpanded} toggleThreadExpanded={() => this.setQuoteExpanded(!isQuoteExpanded)} getRelationsForEvent={this.props.getRelationsForEvent} + isShiftHeld={this.state.isShiftHeld} + setupShiftKeyListener={this.setupShiftKeyListener} + removeShiftKeyListener={this.removeShiftKeyListener} /> ) : undefined; @@ -1224,8 +1260,8 @@ export class UnwrappedEventTile extends React.Component "data-layout": this.props.layout, "data-self": isOwnEvent, "data-event-id": this.props.mxEvent.getId(), - "onMouseEnter": () => this.setState({ hover: true }), - "onMouseLeave": () => this.setState({ hover: false }), + "onMouseEnter": this.onMouseEnter, + "onMouseLeave": this.onMouseLeave, }, [
@@ -1284,8 +1320,8 @@ export class UnwrappedEventTile extends React.Component "data-shape": this.context.timelineRenderingType, "data-self": isOwnEvent, "data-has-reply": !!replyChain, - "onMouseEnter": () => this.setState({ hover: true }), - "onMouseLeave": () => this.setState({ hover: false }), + "onMouseEnter": this.onMouseEnter, + "onMouseLeave": this.onMouseLeave, "onClick": (ev: MouseEvent) => { const target = ev.currentTarget as HTMLElement; let index = -1; @@ -1418,8 +1454,8 @@ export class UnwrappedEventTile extends React.Component "data-self": isOwnEvent, "data-event-id": this.props.mxEvent.getId(), "data-has-reply": !!replyChain, - "onMouseEnter": () => this.setState({ hover: true }), - "onMouseLeave": () => this.setState({ hover: false }), + "onMouseEnter": this.onMouseEnter, + "onMouseLeave": this.onMouseLeave, }, <> {ircTimestamp} @@ -1484,11 +1520,9 @@ export class UnwrappedEventTile extends React.Component // Wrap all event tiles with the tile error boundary so that any throws even during construction are captured const SafeEventTile = forwardRef((props, ref) => { return ( - <> - - - - + <><> + + ); }); export default SafeEventTile;