Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 32 additions & 20 deletions src/components/views/context_menus/MessageContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ interface IProps extends IPosition {
rightClick?: boolean;
// The Relations model from the JS SDK for reactions to `mxEvent`
reactions?: Relations;
// A permalink to the event
showPermalink?: boolean;
// A permalink to this event or an href of an anchor element the user has clicked
link?: string;

getRelationsForEvent?: GetRelationsForEvent;
}
Expand Down Expand Up @@ -227,7 +227,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
this.closeMenu();
};

private onPermalinkClick = (e: React.MouseEvent): void => {
private onShareClick = (e: React.MouseEvent): void => {
e.preventDefault();
Modal.createTrackedDialog('share room message dialog', '', ShareDialog, {
target: this.props.mxEvent,
Expand All @@ -236,9 +236,9 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
this.closeMenu();
};

private onCopyPermalinkClick = (e: ButtonEvent): void => {
private onCopyLinkClick = (e: ButtonEvent): void => {
e.preventDefault(); // So that we don't open the permalink
copyPlaintext(this.getPermalink());
copyPlaintext(this.props.link);
this.closeMenu();
};

Expand Down Expand Up @@ -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[] {
return this.getReactions(e => e.status === EventStatus.NOT_SENT);
}
Expand All @@ -318,11 +313,11 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
public render(): JSX.Element {
const cli = MatrixClientPeg.get();
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 unsentReactionsCount = this.getUnsentReactions().length;
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
const isSent = !eventStatus || eventStatus === EventStatus.SENT;
const { timelineRenderingType, canReact, canSendMessages } = this.context;
Expand Down Expand Up @@ -420,17 +415,13 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
if (permalink) {
permalinkButton = (
<IconizedContextMenuOption
iconClassName={showPermalink
? "mx_MessageContextMenu_iconCopy"
: "mx_MessageContextMenu_iconPermalink"
}
onClick={showPermalink ? this.onCopyPermalinkClick : this.onPermalinkClick}
label={showPermalink ? _t('Copy link') : _t('Share')}
iconClassName="mx_MessageContextMenu_iconPermalink"
onClick={this.onShareClick}
label={_t('Share')}
element="a"
{
// XXX: Typescript signature for AccessibleButton doesn't work properly for non-inputs like `a`
...{

href: permalink,
target: "_blank",
rel: "noreferrer noopener",
Expand Down Expand Up @@ -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;
if (rightClick && getSelectedText()) {
copyButton = (
Expand Down Expand Up @@ -566,10 +577,11 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
}

let nativeItemsList: JSX.Element;
if (copyButton) {
if (copyButton || copyLinkButton) {
nativeItemsList = (
<IconizedContextMenuOptionList>
{ copyButton }
{ copyLinkButton }
</IconizedContextMenuOptionList>
);
}
Expand Down
29 changes: 15 additions & 14 deletions src/components/views/rooms/EventTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ interface IState {
// Position of the context menu
contextMenu?: {
position: Pick<DOMRect, "top" | "left" | "bottom">;
showPermalink?: boolean;
link?: string;
};

isQuoteExpanded?: boolean;
Expand Down Expand Up @@ -842,26 +842,27 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
};

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
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
// selected text, as in those cases we want to use the native browser
// menu
const clickTarget = ev.target as HTMLElement;
if (
!PlatformPeg.get().allowOverridingNativeContextMenus() &&
(clickTarget.tagName === "a" || clickTarget.closest("a") || getSelectedText())
) return;
// Try to find an anchor element
const anchorElement = (clickTarget instanceof HTMLAnchorElement) ? clickTarget : clickTarget.closest("a");

// 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 (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
if (this.props.editState) return;
Expand All @@ -875,7 +876,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
top: ev.clientY,
bottom: ev.clientY,
},
showPermalink: showPermalink,
link: anchorElement?.href || permalink,
},
actionBarFocused: true,
});
Expand Down Expand Up @@ -924,7 +925,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
onFinished={this.onCloseMenu}
rightClick={true}
reactions={this.state.reactions}
showPermalink={this.state.contextMenu.showPermalink}
link={this.state.contextMenu.link}
/>
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -2921,10 +2921,10 @@
"Forward": "Forward",
"View source": "View source",
"Show preview": "Show preview",
"Copy link": "Copy link",
"Source URL": "Source URL",
"Collapse reply thread": "Collapse reply thread",
"Report": "Report",
"Copy link": "Copy link",
"Forget": "Forget",
"Mentions only": "Mentions only",
"See room timeline (devtools)": "See room timeline (devtools)",
Expand Down