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:
parent
dfc7224fc7
commit
b1daf3fec2
3 changed files with 48 additions and 35 deletions
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)",
|
||||||
|
|
Loading…
Reference in a new issue