Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Andrew 3313 link context menu #3931

Merged
Merged
Show file tree
Hide file tree
Changes from 12 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
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,50 @@ import React from 'react';
import {StyleSheet} from 'react-native';
import Text from '../../Text';
import {propTypes, defaultProps} from '../anchorForCommentsOnlyPropTypes';
import PressableWithSecondaryInteraction from '../../PressableWithSecondaryInteraction';
import {showContextMenu} from '../../../pages/home/report/ContextMenu/ReportActionContextMenu';
import {CONTEXT_MENU_TYPES} from '../../../pages/home/report/ContextMenu/ContextMenuActions';


/*
* This is a default anchor component for regular links.
*/
* This is a default anchor component for regular links.
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
*/
const BaseAnchorForCommentsOnly = ({
href,
rel,
target,
children,
style,
...props
}) => (
<Text
style={StyleSheet.flatten(style)}
accessibilityRole="link"
href={href}
hrefAttrs={{rel, target}}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
>
{children}
</Text>
);
}) => {
let linkRef;
return (
<PressableWithSecondaryInteraction
onSecondaryInteraction={
(event) => {
showContextMenu(
CONTEXT_MENU_TYPES.link,
event,
href,
linkRef.current,
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
);
}
}
>
<Text
ref={el => linkRef = el}
style={StyleSheet.flatten(style)}
accessibilityRole="link"
href={href}
hrefAttrs={{rel, target}}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
>
{children}
</Text>
</PressableWithSecondaryInteraction>
);
};

BaseAnchorForCommentsOnly.propTypes = propTypes;
BaseAnchorForCommentsOnly.defaultProps = defaultProps;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import {Linking, StyleSheet} from 'react-native';
import {propTypes, defaultProps} from '../anchorForCommentsOnlyPropTypes';
import fileDownload from '../../../libs/fileDownload';
import Text from '../../Text';
import PressableWithSecondaryInteraction from '../../PressableWithSecondaryInteraction';
import {showContextMenu} from '../../../pages/home/report/ContextMenu/ReportActionContextMenu';
import {CONTEXT_MENU_TYPES} from '../../../pages/home/report/ContextMenu/ContextMenuActions';

/*
* This is a default anchor component for regular links.
Expand All @@ -13,16 +16,33 @@ const BaseAnchorForCommentsOnly = ({
style,
shouldDownloadFile,
...props
}) => (
<Text
style={StyleSheet.flatten(style)}
onPress={() => (shouldDownloadFile ? fileDownload(href) : Linking.openURL(href))}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
>
{children}
</Text>
);
}) => {
let linkRef;
return (
<PressableWithSecondaryInteraction
onSecondaryInteraction={
(event) => {
showContextMenu(
CONTEXT_MENU_TYPES.link,
event,
href,
linkRef.current,
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
);
}
}
onPress={() => (shouldDownloadFile ? fileDownload(href) : Linking.openURL(href))}
>
<Text
ref={el => linkRef = el}
style={StyleSheet.flatten(style)}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
>
{children}
</Text>
</PressableWithSecondaryInteraction>
)
};

BaseAnchorForCommentsOnly.propTypes = propTypes;
BaseAnchorForCommentsOnly.defaultProps = defaultProps;
Expand Down
4 changes: 2 additions & 2 deletions src/components/CommunicationsLink.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ const CommunicationsLink = props => (
{props.children}
<ContextMenuItem
icon={ClipboardIcon}
text={props.translate('contextMenuItem.copyToClipboard')}
text={props.translate('reportActionContextMenu.copyToClipboard')}
successIcon={Checkmark}
successText={props.translate('contextMenuItem.copied')}
successText={props.translate('reportActionContextMenu.copied')}
isMini
autoReset
onPress={() => Clipboard.setString(props.value)}
Expand Down
2 changes: 2 additions & 0 deletions src/components/PressableWithSecondaryInteraction/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class PressableWithSecondaryInteraction extends Component {
*/
executeSecondaryInteractionOnContextMenu(e) {
const selection = window.getSelection().toString();
e.stopPropagation();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, why do we need this line? It seems to be unnecessary. cc @roryabraham @parasharrajat @puneetlath @Drewfergusson

Copy link
Member

@parasharrajat parasharrajat Sep 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This prevents event bubbling when this component is nested. We do not want nested and parent component both to trigger the action.

This happens for nested links in the message. When link context menu is triggered, message context menu should not override it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, so you mean by this case?
image

if (this.props.preventDefaultContentMenu) {
e.preventDefault();
}
Expand All @@ -44,6 +45,7 @@ class PressableWithSecondaryInteraction extends Component {
delayLongPress={200}
onLongPress={this.props.onSecondaryInteraction}
onPressOut={this.props.onPressOut}
onPress={this.props.onPress}
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
ref={el => this.pressableRef = el}
// eslint-disable-next-line react/jsx-props-no-spreading
{...defaultPressableProps}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {propTypes, defaultProps} from './pressableWithSecondaryInteractionPropTy
const PressableWithSecondaryInteraction = props => (
<Pressable
ref={props.forwardedRef}
onPress={props.onPress}
onPressIn={props.onPressIn}
delayLongPress={200}
onLongPress={(e) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import PropTypes from 'prop-types';

const propTypes = {
/** The function that should be called when this pressable is pressed */
onPress: PropTypes.func,

/** The function that should be called when this pressable is pressedIn */
onPressIn: PropTypes.func,

Expand Down
6 changes: 3 additions & 3 deletions src/languages/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,12 @@ export default {
roomIsArchived: 'This chat room has been deleted',
localTime: ({user, time}) => `It's ${time} for ${user}`,
},
contextMenuItem: {
reportActionContextMenu: {
copyToClipboard: 'Copy to Clipboard',
openLink: 'Open Link',
copied: 'Copied!',
},
reportActionContextMenu: {
copyLink: 'Copy Link',
copyURLToClipboard: 'Copy URL to Clipboard',
markAsUnread: 'Mark as Unread',
editComment: 'Edit Comment',
deleteComment: 'Delete Comment',
Expand Down
6 changes: 3 additions & 3 deletions src/languages/es.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,12 @@ export default {
roomIsArchived: 'Esta sala de chat ha sido eliminada',
localTime: ({user, time}) => `Son las ${time} para ${user}`,
},
contextMenuItem: {
reportActionContextMenu: {
copyToClipboard: 'Copiar al Portapapeles',
openLink: 'Abrir Enlace',
copied: '¡Copiado!',
},
reportActionContextMenu: {
copyLink: 'Copiar Enlace',
copyURLToClipboard: 'Copiar URL al Portapapeles',
markAsUnread: 'Marcar como no leído',
editComment: 'Editar Commentario',
deleteComment: 'Eliminar Comentario',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import {View} from 'react-native';
import _ from 'underscore';
import PropTypes from 'prop-types';
import getReportActionContextMenuStyles from '../../../../styles/getReportActionContextMenuStyles';
import ContextMenuItem from '../../../../components/ContextMenuItem';
import {
Expand All @@ -11,21 +12,23 @@ import withLocalize, {withLocalizePropTypes} from '../../../../components/withLo
import ContextMenuActions from './ContextMenuActions';

const propTypes = {
/** String representing the context menu type [LINK, REPORT_ACTION] which controls context menu choices */
type: PropTypes.string,
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
...GenericReportActionContextMenuPropTypes,
...withLocalizePropTypes,
};

class BaseReportActionContextMenu extends React.Component {
constructor(props) {
super(props);

this.wrapperStyle = getReportActionContextMenuStyles(this.props.isMini);
}

render() {
const shouldShowFilter = contextAction => contextAction.shouldShow(this.props.type, this.props.reportAction);

return this.props.isVisible && (
<View style={this.wrapperStyle}>
{_.map(ContextMenuActions, contextAction => contextAction.shouldShow(this.props.reportAction) && (
{ContextMenuActions.filter(shouldShowFilter).map(contextAction => (
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
<ContextMenuItem
icon={contextAction.icon}
text={this.props.translate(contextAction.textTranslateKey)}
Expand Down
44 changes: 36 additions & 8 deletions src/pages/home/report/ContextMenu/ContextMenuActions.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {Linking} from 'react-native';
import _ from 'underscore';
import lodashGet from 'lodash/get';
import Str from 'expensify-common/lib/str';
import {
Clipboard as ClipboardIcon, LinkCopy, Mail, Pencil, Trashcan, Checkmark,
Clipboard as ClipboardIcon, LinkCopy, Mail, Pencil, Trashcan, Checkmark, Link,
} from '../../../../components/Icon/Expensicons';
import {
setNewMarkerPosition, updateLastReadActionID, saveReportActionDraft,
Expand All @@ -22,15 +23,39 @@ function getActionText(reportAction) {
return lodashGet(message, 'html', '');
}

export const CONTEXT_MENU_TYPES = {
link: 'LINK',
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
reportAction: 'REPORT_ACTION',
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
};

// A list of all the context actions in this menu.
export default [
// Copy to clipboard
{
textTranslateKey: 'contextMenuItem.copyToClipboard',
textTranslateKey: 'reportActionContextMenu.openLink',
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
icon: Link,
shouldShow: type => type === CONTEXT_MENU_TYPES.link,
onPress: (closePopover, {selection}) => {
Linking.openURL(selection);
hideContextMenu(false, ReportActionComposeFocusManager.focus);
},
},
{
textTranslateKey: 'reportActionContextMenu.copyURLToClipboard',
icon: ClipboardIcon,
successTextTranslateKey: 'reportActionContextMenu.copied',
successIcon: Checkmark,
shouldShow: type => type === CONTEXT_MENU_TYPES.link,
onPress: (closePopover, {selection}) => {
Clipboard.setString(selection);
hideContextMenu(true, ReportActionComposeFocusManager.focus);
},
},
{
textTranslateKey: 'reportActionContextMenu.copyToClipboard',
icon: ClipboardIcon,
successTextTranslateKey: 'contextMenuItem.copied',
successTextTranslateKey: 'reportActionContextMenu.copied',
successIcon: Checkmark,
shouldShow: () => true,
shouldShow: type => type === CONTEXT_MENU_TYPES.reportAction,

// If return value is true, we switch the `text` and `icon` on
// `ContextMenuItem` with `successText` and `successIcon` which will fallback to
Expand Down Expand Up @@ -64,7 +89,7 @@ export default [
textTranslateKey: 'reportActionContextMenu.markAsUnread',
icon: Mail,
successIcon: Checkmark,
shouldShow: () => true,
shouldShow: type => type === CONTEXT_MENU_TYPES.reportAction,
onPress: (closePopover, {reportAction, reportID}) => {
updateLastReadActionID(reportID, reportAction.sequenceNumber);
setNewMarkerPosition(reportID, reportAction.sequenceNumber);
Expand All @@ -77,7 +102,9 @@ export default [
{
textTranslateKey: 'reportActionContextMenu.editComment',
icon: Pencil,
shouldShow: reportAction => canEditReportAction(reportAction),
shouldShow: (type, reportAction) => (
type === CONTEXT_MENU_TYPES.reportAction && canEditReportAction(reportAction)
),
onPress: (closePopover, {reportID, reportAction, draftMessage}) => {
const editAction = () => saveReportActionDraft(
reportID,
Expand All @@ -98,7 +125,8 @@ export default [
{
textTranslateKey: 'reportActionContextMenu.deleteComment',
icon: Trashcan,
shouldShow: reportAction => canDeleteReportAction(reportAction),
shouldShow: (type, reportAction) => type === CONTEXT_MENU_TYPES.reportAction
&& canDeleteReportAction(reportAction),
onPress: (closePopover, {reportID, reportAction}) => {
if (closePopover) {
// Hide popover, then call showDeleteConfirmModal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ class PopoverReportActionContextMenu extends React.Component {
/**
* Show the ReportActionContextMenu modal popover.
*
* @param {string} type - context menu type [LINK, REPORT_ACTION]
* @param {Object} [event] - A press event.
* @param {string} [selection] - A copy text.
* @param {Element} contextMenuAnchor - popoverAnchor
Expand All @@ -104,6 +105,7 @@ class PopoverReportActionContextMenu extends React.Component {
* @param {Function} [onHide] - Run a callback when Menu is hidden
*/
showContextMenu(
type,
event,
selection,
contextMenuAnchor,
Expand All @@ -126,6 +128,7 @@ class PopoverReportActionContextMenu extends React.Component {
horizontal: nativeEvent.pageX,
vertical: nativeEvent.pageY,
},
type,
reportID,
reportAction,
selection,
Expand Down Expand Up @@ -190,6 +193,7 @@ class PopoverReportActionContextMenu extends React.Component {
measureContent() {
return (
<BaseReportActionContextMenu
type={this.state.type}
isVisible
selection={this.state.selection}
reportID={this.state.reportID}
Expand Down Expand Up @@ -236,6 +240,7 @@ class PopoverReportActionContextMenu extends React.Component {
>
<BaseReportActionContextMenu
isVisible
type={this.state.type}
reportID={this.state.reportID}
reportAction={this.state.reportAction}
draftMessage={this.state.reportActionDraftMessage}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const contextMenuRef = React.createRef();
/**
* Show the ReportActionContextMenu modal popover.
*
* @param {string} type - the context menu type to display [LINK, REPORT_ACTION]
* @param {Object} [event] - A press event.
* @param {string} [selection] - A copy text.
* @param {Element} contextMenuAnchor - popoverAnchor
Expand All @@ -15,19 +16,21 @@ const contextMenuRef = React.createRef();
* @param {Function} [onHide=() => {}] - Run a callback when Menu is hidden
*/
function showContextMenu(
type,
event,
selection,
contextMenuAnchor,
reportID,
reportAction,
draftMessage,
reportID = 0,
reportAction = {},
draftMessage = '',
onShow = () => {},
onHide = () => {},
) {
if (!contextMenuRef.current) {
return;
}
contextMenuRef.current.showContextMenu(
type,
event,
selection,
contextMenuAnchor,
Expand Down
Loading