Skip to content

Commit

Permalink
Merge pull request #2088 from parasharrajat/parasharrajat/clipboard
Browse files Browse the repository at this point in the history
Copy to clipboard functionality
  • Loading branch information
marcaaron authored Apr 1, 2021
2 parents edf3b3a + 4bf4a2a commit 366839d
Show file tree
Hide file tree
Showing 11 changed files with 156 additions and 45 deletions.
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@babel/preset-flow": "^7.12.13",
"@react-native-community/async-storage": "^1.11.0",
"@react-native-community/cli": "4.13.1",
"@react-native-community/clipboard": "^1.5.1",
"@react-native-community/masked-view": "^0.1.10",
"@react-native-community/netinfo": "^5.9.10",
"@react-native-community/progress-bar-android": "^1.0.4",
Expand Down
1 change: 1 addition & 0 deletions src/CONST.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const CONST = {
DEFAULT: 'default',
HOVERED: 'hovered',
PRESSED: 'pressed',
COMPLETE: 'complete',
},
CLOUDFRONT_URL,
NEW_ZOOM_MEETING_URL: 'https://zoom.us/start/videomeeting',
Expand Down
4 changes: 4 additions & 0 deletions src/libs/Clipboard/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// on Web/desktop this import will be replaced with `react-native-web`
import {Clipboard} from 'react-native-web';

export default Clipboard;
3 changes: 3 additions & 0 deletions src/libs/Clipboard/index.native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Clipboard from '@react-native-community/clipboard';

export default Clipboard;
3 changes: 2 additions & 1 deletion src/libs/actions/Report.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import Timing from './Timing';
import * as API from '../API';
import CONST from '../../CONST';
import Log from '../Log';
import {isReportMessageAttachment} from '../reportUtils';

let currentUserEmail;
let currentUserAccountID;
Expand Down Expand Up @@ -409,7 +410,7 @@ function updateReportWithNewAction(reportID, reportAction) {
// Add the action into Onyx
reportActionsToMerge[reportAction.sequenceNumber] = {
...reportAction,
isAttachment: messageText === '[Attachment]',
isAttachment: isReportMessageAttachment(messageText),
loading: false,
};

Expand Down
12 changes: 11 additions & 1 deletion src/libs/reportUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,17 @@ function getReportParticipantsTitle(logins) {
return _.map(logins, login => Str.removeSMSDomain(login)).join(', ');
}

/**
* Check whether a report action is Attachment is not.
*
* @param {Object} reportMessageText report action's message as text
* @returns {Boolean}
*/
function isReportMessageAttachment(reportMessageText) {
return reportMessageText === '[Attachment]';
}

export {
// eslint-disable-next-line import/prefer-default-export
getReportParticipantsTitle,
isReportMessageAttachment,
};
40 changes: 35 additions & 5 deletions src/pages/home/report/ReportActionContextMenu.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import _ from 'underscore';
import React from 'react';
import {View} from 'react-native';
import PropTypes from 'prop-types';
import lodashGet from 'lodash/get';
import {
Clipboard, LinkCopy, Mail, Pencil, Trashcan,
Clipboard as ClipboardIcon, LinkCopy, Mail, Pencil, Trashcan, Checkmark,
} from '../../../components/Icon/Expensicons';
import getReportActionContextMenuStyles from '../../../styles/getReportActionContextMenuStyles';
import ReportActionContextMenuItem from './ReportActionContextMenuItem';
import ReportActionPropTypes from './ReportActionPropTypes';
import Clipboard from '../../../libs/Clipboard';
import {isReportMessageAttachment} from '../../../libs/reportUtils';

/**
* A list of all the context actions in this menu.
Expand All @@ -14,31 +19,54 @@ const CONTEXT_ACTIONS = [
// Copy to clipboard
{
text: 'Copy to Clipboard',
icon: Clipboard,
icon: ClipboardIcon,
successText: 'Copied!',
successIcon: Checkmark,

// If return value is true, we switch the `text` and `icon` on
// `ReportActionContextMenuItem` with `successText` and `successIcon` which will fallback to
// the `text` and `icon`
onPress: (action) => {
const message = _.last(lodashGet(action, 'message', null));
const html = lodashGet(message, 'html', '');
const text = lodashGet(message, 'text', '');
const isAttachment = _.has(action, 'isAttachment')
? action.isAttachment
: isReportMessageAttachment(text);
if (!isAttachment) {
Clipboard.setString(text);
} else {
Clipboard.setString(html);
}
},
},

// Copy chat link
{
text: 'Copy Link',
icon: LinkCopy,
onPress: () => {},
},

// Mark as Unread
{
text: 'Mark as Unread',
icon: Mail,
onPress: () => {},
},

// Edit Comment
{
text: 'Edit Comment',
icon: Pencil,
onPress: () => {},
},

// Delete Comment
{
text: 'Delete Comment',
icon: Trashcan,
onPress: () => {},
},
];

Expand All @@ -47,9 +75,8 @@ const propTypes = {
// eslint-disable-next-line react/no-unused-prop-types
reportID: PropTypes.number.isRequired,

// The ID of the report action this context menu is attached to.
// eslint-disable-next-line react/no-unused-prop-types
reportActionID: PropTypes.number.isRequired,
// The report action this context menu is attached to.
reportAction: PropTypes.shape(ReportActionPropTypes).isRequired,

// If true, this component will be a small, row-oriented menu that displays icons but not text.
// If false, this component will be a larger, column-oriented menu that displays icons alongside text in each row.
Expand All @@ -72,7 +99,10 @@ const ReportActionContextMenu = (props) => {
<ReportActionContextMenuItem
icon={contextAction.icon}
text={contextAction.text}
successIcon={contextAction.successIcon}
successText={contextAction.successText}
isMini={props.isMini}
onPress={() => contextAction.onPress(props.reportAction)}
key={contextAction.text}
/>
))}
Expand Down
124 changes: 89 additions & 35 deletions src/pages/home/report/ReportActionContextMenuItem.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {memo} from 'react';
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {Pressable, Text} from 'react-native';
import Tooltip from '../../../components/Tooltip';
Expand All @@ -12,9 +12,14 @@ import styles from '../../../styles/styles';
*
* @param {Boolean} [isHovered]
* @param {Boolean} [isPressed]
* @param {Boolean} [isComplete]
* @returns {String}
*/
function getButtonState(isHovered = false, isPressed = false) {
function getButtonState(isHovered = false, isPressed = false, isComplete = false) {
if (isComplete) {
return CONST.BUTTON_STATES.COMPLETE;
}

if (isPressed) {
return CONST.BUTTON_STATES.PRESSED;
}
Expand All @@ -29,52 +34,101 @@ function getButtonState(isHovered = false, isPressed = false) {
const propTypes = {
icon: PropTypes.elementType.isRequired,
text: PropTypes.string.isRequired,
successIcon: PropTypes.elementType,
successText: PropTypes.string,
isMini: PropTypes.bool,
onPress: PropTypes.func.isRequired,
};

const defaultProps = {
isMini: false,
successIcon: null,
successText: '',
};

const ReportActionContextMenuItem = (props) => {
const {getButtonStyle, getIconFillColor} = getReportActionContextMenuItemStyles(props.isMini);
return (
props.isMini
? (
<Tooltip text={props.text}>
<Pressable style={({hovered, pressed}) => getButtonStyle(getButtonState(hovered, pressed))}>
class ReportActionContextMenuItem extends Component {
constructor(props) {
super(props);
this.state = {
success: false,
};
this.triggerPressAndUpdateSuccess = this.triggerPressAndUpdateSuccess.bind(this);
}

/**
* Called on button press and mark the run
*/
triggerPressAndUpdateSuccess() {
if (this.state.success) {
return;
}
this.props.onPress();

// We only set the success state when we have icon or text to represent the success state
// We may want to replace this check by checking the Result from OnPress Callback in future.
if (this.props.successIcon || this.props.successText) {
this.setState({
success: true,
});
}
}

render() {
const {getButtonStyle, getIconFillColor} = getReportActionContextMenuItemStyles(this.props.isMini);
const icon = this.state.success ? this.props.successIcon || this.props.icon : this.props.icon;
const text = this.state.success ? this.props.successText || this.props.text : this.props.text;
return (
this.props.isMini
? (
<Tooltip text={text}>
<Pressable
onPress={this.triggerPressAndUpdateSuccess}
style={
({hovered, pressed}) => getButtonStyle(
getButtonState(hovered, pressed, this.state.success),
)
}
>
{({hovered, pressed}) => (
<Icon
src={icon}
fill={getIconFillColor(getButtonState(hovered, pressed, this.state.success))}
/>
)}
</Pressable>
</Tooltip>
) : (
<Pressable
onPress={this.triggerPressAndUpdateSuccess}
style={
({hovered, pressed}) => getButtonStyle(
getButtonState(hovered, pressed, this.state.success),
)
}
>
{({hovered, pressed}) => (
<Icon
src={props.icon}
fill={getIconFillColor(getButtonState(hovered, pressed))}
/>
<>
<Icon
src={icon}
fill={getIconFillColor(getButtonState(hovered, pressed, this.state.success))}
/>
<Text
style={styles.reportActionContextMenuText}
selectable={false}
>
{text}
</Text>
</>
)}
</Pressable>
</Tooltip>
) : (
<Pressable style={({hovered, pressed}) => getButtonStyle(getButtonState(hovered, pressed))}>
{({hovered, pressed}) => (
<>
<Icon
src={props.icon}
fill={getIconFillColor(getButtonState(hovered, pressed))}
/>
<Text
style={styles.reportActionContextMenuText}
selectable={false}
>
{props.text}
</Text>
</>
)}
</Pressable>
)
);
};
)
);
}
}


ReportActionContextMenuItem.propTypes = propTypes;
ReportActionContextMenuItem.defaultProps = defaultProps;
ReportActionContextMenuItem.displayName = 'ReportActionContextMenuItem';

export default memo(ReportActionContextMenuItem);
export default ReportActionContextMenuItem;
6 changes: 3 additions & 3 deletions src/pages/home/report/ReportActionItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ class ReportActionItem extends Component {
<View style={getMiniReportActionContextMenuWrapperStyle(this.props.displayAsGroup)}>
<ReportActionContextMenu
reportID={this.props.reportID}
reportActionID={this.props.action.sequenceNumber}
reportAction={this.props.action}
isVisible={
hovered
&& this.isInReportActionContextMenuBeta()
Expand All @@ -135,14 +135,14 @@ class ReportActionItem extends Component {
<ReportActionContextMenu
isVisible
reportID={-1}
reportActionID={-1}
reportAction={{}}
/>
)}
>
<ReportActionContextMenu
isVisible={this.state.isPopoverVisible}
reportID={this.props.reportID}
reportActionID={this.props.action.sequenceNumber}
reportAction={this.props.action}
/>
</PopoverWithMeasuredContent>
</View>
Expand Down
2 changes: 2 additions & 0 deletions src/styles/getReportActionContextMenuItemStyles.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ function getIconFillColor(buttonState = CONST.BUTTON_STATES.DEFAULT) {
return themeColors.text;
case CONST.BUTTON_STATES.PRESSED:
return themeColors.heading;
case CONST.BUTTON_STATES.COMPLETE:
return themeColors.iconSuccessFill;
case CONST.BUTTON_STATES.DEFAULT:
default:
return themeColors.icon;
Expand Down

0 comments on commit 366839d

Please sign in to comment.