feat: shift modifier shortcut for message row
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<IMessageActionBarProps> {
|
||||
public static contextType = RoomContext;
|
||||
declare public context: React.ContextType<typeof RoomContext>;
|
||||
|
||||
private onDelete = async (ev: ButtonEvent): Promise<void> => {
|
||||
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<IMessageAction
|
||||
PosthogTrackers.trackPinUnpinMessage(isPinned ? "Pin" : "Unpin", "Timeline");
|
||||
};
|
||||
|
||||
private onMouseEnter = (): void => {
|
||||
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<IMessageAction
|
||||
);
|
||||
}
|
||||
|
||||
// The menu button should be last, so dump it there.
|
||||
toolbarOpts.push(
|
||||
<OptionsButton
|
||||
mxEvent={this.props.mxEvent}
|
||||
getReplyChain={this.props.getReplyChain}
|
||||
getTile={this.props.getTile}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
onFocusChange={this.onFocusChange}
|
||||
key="menu"
|
||||
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||
/>,
|
||||
);
|
||||
// Replace the menu button with delete button when shift is held
|
||||
if (this.props.isShiftHeld && !isRedacted) {
|
||||
toolbarOpts.push(
|
||||
<RovingAccessibleButton
|
||||
className="mx_MessageActionBar_iconButton mx_MessageActionBar_deleteButton"
|
||||
title={_t("action|delete")}
|
||||
onClick={this.onDelete}
|
||||
onContextMenu={this.onDelete}
|
||||
key="delete"
|
||||
placement="left"
|
||||
>
|
||||
<DeleteIcon />
|
||||
</RovingAccessibleButton>,
|
||||
);
|
||||
} else {
|
||||
toolbarOpts.push(
|
||||
<OptionsButton
|
||||
mxEvent={this.props.mxEvent}
|
||||
getReplyChain={this.props.getReplyChain}
|
||||
getTile={this.props.getTile}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
onFocusChange={this.onFocusChange}
|
||||
key="menu"
|
||||
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// aria-live=off to not have this read out automatically as navigating around timeline, gets repetitive.
|
||||
return (
|
||||
<Toolbar className="mx_MessageActionBar" aria-label={_t("timeline|mab|label")} aria-live="off">
|
||||
<Toolbar
|
||||
className="mx_MessageActionBar"
|
||||
aria-label={_t("timeline|mab|label")}
|
||||
aria-live="off"
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
>
|
||||
{toolbarOpts}
|
||||
</Toolbar>
|
||||
);
|
||||
|
||||
@@ -261,6 +261,8 @@ interface IState {
|
||||
|
||||
thread: Thread | null;
|
||||
threadNotification?: NotificationCountType;
|
||||
|
||||
isShiftHeld: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -315,6 +317,8 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
hover: false,
|
||||
|
||||
thread,
|
||||
|
||||
isShiftHeld: false,
|
||||
};
|
||||
|
||||
// don't do RR animations until we are mounted
|
||||
@@ -441,6 +445,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
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<EventTileProps>, prevState: Readonly<IState>): void {
|
||||
@@ -798,6 +803,34 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
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<EventTileProps, IState>
|
||||
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<EventTileProps, IState>
|
||||
"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,
|
||||
},
|
||||
[
|
||||
<div className="mx_EventTile_senderDetails" key="mx_EventTile_senderDetails">
|
||||
@@ -1284,8 +1320,8 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
"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<EventTileProps, IState>
|
||||
"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<EventTileProps, IState>
|
||||
// Wrap all event tiles with the tile error boundary so that any throws even during construction are captured
|
||||
const SafeEventTile = forwardRef<UnwrappedEventTile, EventTileProps>((props, ref) => {
|
||||
return (
|
||||
<>
|
||||
<TileErrorBoundary mxEvent={props.mxEvent} layout={props.layout ?? Layout.Group}>
|
||||
<UnwrappedEventTile ref={ref} {...props} />
|
||||
</TileErrorBoundary>
|
||||
</>
|
||||
<><></><TileErrorBoundary mxEvent={props.mxEvent} layout={props.layout ?? Layout.Group}>
|
||||
<UnwrappedEventTile ref={ref} {...props} />
|
||||
</TileErrorBoundary></>
|
||||
);
|
||||
});
|
||||
export default SafeEventTile;
|
||||
|
||||
Reference in New Issue
Block a user