Add a Copy link button to the right-click message context-menu labs feature (#8527)

* Simplify `Share` button

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Add proper `Copy link` button

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* i18n

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
Šimon Brandner 2022-05-09 08:25:14 +02:00 committed by GitHub
parent dfc7224fc7
commit b1daf3fec2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 48 additions and 35 deletions

View file

@ -70,8 +70,8 @@ interface IProps extends IPosition {
rightClick?: boolean; rightClick?: boolean;
// The Relations model from the JS SDK for reactions to `mxEvent` // The Relations model from the JS SDK for reactions to `mxEvent`
reactions?: Relations; reactions?: Relations;
// A permalink to the event // A permalink to this event or an href of an anchor element the user has clicked
showPermalink?: boolean; link?: string;
getRelationsForEvent?: GetRelationsForEvent; getRelationsForEvent?: GetRelationsForEvent;
} }
@ -227,7 +227,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
this.closeMenu(); this.closeMenu();
}; };
private onPermalinkClick = (e: React.MouseEvent): void => { private onShareClick = (e: React.MouseEvent): void => {
e.preventDefault(); e.preventDefault();
Modal.createTrackedDialog('share room message dialog', '', ShareDialog, { Modal.createTrackedDialog('share room message dialog', '', ShareDialog, {
target: this.props.mxEvent, target: this.props.mxEvent,
@ -236,9 +236,9 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
this.closeMenu(); this.closeMenu();
}; };
private onCopyPermalinkClick = (e: ButtonEvent): void => { private onCopyLinkClick = (e: ButtonEvent): void => {
e.preventDefault(); // So that we don't open the permalink e.preventDefault(); // So that we don't open the permalink
copyPlaintext(this.getPermalink()); copyPlaintext(this.props.link);
this.closeMenu(); this.closeMenu();
}; };
@ -295,11 +295,6 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
}); });
} }
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);
} }
@ -318,11 +313,11 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
public render(): JSX.Element { public render(): JSX.Element {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const me = cli.getUserId(); const me = cli.getUserId();
const { mxEvent, rightClick, showPermalink, eventTileOps, reactions, collapseReplyChain } = this.props; const { mxEvent, rightClick, link, 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 contentActionable = isContentActionable(mxEvent);
const permalink = this.getPermalink(); const permalink = this.props.permalinkCreator?.forEvent(this.props.mxEvent.getId());
// status is SENT before remote-echo, null after // status is SENT before remote-echo, null after
const isSent = !eventStatus || eventStatus === EventStatus.SENT; const isSent = !eventStatus || eventStatus === EventStatus.SENT;
const { timelineRenderingType, canReact, canSendMessages } = this.context; const { timelineRenderingType, canReact, canSendMessages } = this.context;
@ -420,17 +415,13 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
if (permalink) { if (permalink) {
permalinkButton = ( permalinkButton = (
<IconizedContextMenuOption <IconizedContextMenuOption
iconClassName={showPermalink iconClassName="mx_MessageContextMenu_iconPermalink"
? "mx_MessageContextMenu_iconCopy" onClick={this.onShareClick}
: "mx_MessageContextMenu_iconPermalink" label={_t('Share')}
}
onClick={showPermalink ? this.onCopyPermalinkClick : this.onPermalinkClick}
label={showPermalink ? _t('Copy link') : _t('Share')}
element="a" element="a"
{ {
// XXX: Typescript signature for AccessibleButton doesn't work properly for non-inputs like `a` // XXX: Typescript signature for AccessibleButton doesn't work properly for non-inputs like `a`
...{ ...{
href: permalink, href: permalink,
target: "_blank", target: "_blank",
rel: "noreferrer noopener", rel: "noreferrer noopener",
@ -508,6 +499,26 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
); );
} }
let copyLinkButton: JSX.Element;
if (link) {
copyLinkButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconCopy"
onClick={this.onCopyLinkClick}
label={_t('Copy link')}
element="a"
{
// XXX: Typescript signature for AccessibleButton doesn't work properly for non-inputs like `a`
...{
href: link,
target: "_blank",
rel: "noreferrer noopener",
}
}
/>
);
}
let copyButton: JSX.Element; let copyButton: JSX.Element;
if (rightClick && getSelectedText()) { if (rightClick && getSelectedText()) {
copyButton = ( copyButton = (
@ -566,10 +577,11 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
} }
let nativeItemsList: JSX.Element; let nativeItemsList: JSX.Element;
if (copyButton) { if (copyButton || copyLinkButton) {
nativeItemsList = ( nativeItemsList = (
<IconizedContextMenuOptionList> <IconizedContextMenuOptionList>
{ copyButton } { copyButton }
{ copyLinkButton }
</IconizedContextMenuOptionList> </IconizedContextMenuOptionList>
); );
} }

View file

@ -234,7 +234,7 @@ interface IState {
// Position of the context menu // Position of the context menu
contextMenu?: { contextMenu?: {
position: Pick<DOMRect, "top" | "left" | "bottom">; position: Pick<DOMRect, "top" | "left" | "bottom">;
showPermalink?: boolean; link?: string;
}; };
isQuoteExpanded?: boolean; isQuoteExpanded?: boolean;
@ -842,26 +842,27 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
}; };
private onTimestampContextMenu = (ev: React.MouseEvent): void => { private onTimestampContextMenu = (ev: React.MouseEvent): void => {
this.showContextMenu(ev, true); this.showContextMenu(ev, this.props.permalinkCreator?.forEvent(this.props.mxEvent.getId()));
}; };
private showContextMenu(ev: React.MouseEvent, showPermalink?: boolean): void { private showContextMenu(ev: React.MouseEvent, permalink?: string): void {
const clickTarget = ev.target as HTMLElement;
// Return if message right-click context menu isn't enabled // Return if message right-click context menu isn't enabled
if (!SettingsStore.getValue("feature_message_right_click_context_menu")) return; if (!SettingsStore.getValue("feature_message_right_click_context_menu")) return;
// Return if we're in a browser and click either an a tag or we have // Try to find an anchor element
// selected text, as in those cases we want to use the native browser const anchorElement = (clickTarget instanceof HTMLAnchorElement) ? clickTarget : clickTarget.closest("a");
// menu
const clickTarget = ev.target as HTMLElement;
if (
!PlatformPeg.get().allowOverridingNativeContextMenus() &&
(clickTarget.tagName === "a" || clickTarget.closest("a") || getSelectedText())
) return;
// There is no way to copy non-PNG images into clipboard, so we can't // 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 // have our own handling for copying images, so we leave it to the
// Electron layer (webcontents-handler.ts) // Electron layer (webcontents-handler.ts)
if (ev.target instanceof HTMLImageElement) return; if (clickTarget instanceof HTMLImageElement) return;
// Return if we're in a browser and click either an a tag or we have
// selected text, as in those cases we want to use the native browser
// menu
if (!PlatformPeg.get().allowOverridingNativeContextMenus() && (getSelectedText() || anchorElement)) return;
// We don't want to show the menu when editing a message // We don't want to show the menu when editing a message
if (this.props.editState) return; if (this.props.editState) return;
@ -875,7 +876,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
top: ev.clientY, top: ev.clientY,
bottom: ev.clientY, bottom: ev.clientY,
}, },
showPermalink: showPermalink, link: anchorElement?.href || permalink,
}, },
actionBarFocused: true, actionBarFocused: true,
}); });
@ -924,7 +925,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
onFinished={this.onCloseMenu} onFinished={this.onCloseMenu}
rightClick={true} rightClick={true}
reactions={this.state.reactions} reactions={this.state.reactions}
showPermalink={this.state.contextMenu.showPermalink} link={this.state.contextMenu.link}
/> />
); );
} }

View file

@ -2921,10 +2921,10 @@
"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",
"Copy link": "Copy link",
"Forget": "Forget", "Forget": "Forget",
"Mentions only": "Mentions only", "Mentions only": "Mentions only",
"See room timeline (devtools)": "See room timeline (devtools)", "See room timeline (devtools)": "See room timeline (devtools)",