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

Add Context menu with Pin option to LHN #16079

Merged
merged 42 commits into from
Jun 1, 2023
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
fc9aec9
changing branchs
Gonals Mar 15, 2023
5bbf1db
Merge branch 'main' into alberto-pin
Gonals Mar 16, 2023
ffaf634
add PIN action
Gonals Mar 16, 2023
8789f75
correctly show menu
Gonals Mar 17, 2023
d3befa3
handle pin/unpin behavior
Gonals Mar 17, 2023
28079fe
no need fur success messages
Gonals Mar 17, 2023
c3c45ce
figure out what's going on
Gonals Mar 17, 2023
a6e1aec
make pin/unpin work fine
Gonals Mar 17, 2023
6b26871
cleanup
Gonals Mar 17, 2023
cc83d47
lint
Gonals Mar 17, 2023
6d65fb8
more lint cleanup
Gonals Mar 17, 2023
96c8256
update test
Gonals Mar 17, 2023
eafa87f
handle mobile devices
Gonals Mar 18, 2023
310ae83
Merge branch 'main' into alberto-pin
Gonals Mar 22, 2023
315fbc6
Use pressablewithSecondaryInteraction instead of touchableOpacity
Gonals Mar 22, 2023
7ef10b8
Turn Pressable into touchableOpacity
Gonals Mar 22, 2023
5803c47
cleanUp
Gonals Mar 22, 2023
9f4349d
update test
Gonals Mar 22, 2023
233ae47
Merge branch 'main' into alberto-pin
Gonals Apr 4, 2023
223a550
add better defaults
Gonals Apr 4, 2023
294ed23
Merge branch 'main' into alberto-pin
Gonals Apr 5, 2023
95de7c2
remove unneeded props
Gonals Apr 5, 2023
d4c619e
Merge branch 'main' into alberto-pin
Gonals Apr 10, 2023
7263848
conflicts
Gonals May 16, 2023
0c827dc
Merge branch 'main' into alberto-pin
Gonals May 16, 2023
b554112
more conflicts
Gonals May 16, 2023
fb6c9b4
back to pressable
Gonals May 16, 2023
cc01e7e
pass params better
Gonals May 16, 2023
8c45c6f
lint
Gonals May 17, 2023
7f61b2a
prettier
Gonals May 17, 2023
3775be7
conflicts
Gonals May 18, 2023
a0b72c6
Merge branch 'main' into alberto-pin
Gonals May 23, 2023
f3c9046
close popup if pin changes
Gonals May 23, 2023
c1a93de
remove unneeded import
Gonals May 23, 2023
beaa0e0
Merge branch 'main' into alberto-pin
Gonals May 26, 2023
698b876
remove measure
Gonals May 26, 2023
9e7ce9b
Merge branch 'main' into alberto-pin
Gonals May 29, 2023
9cab709
use pressable
Gonals May 29, 2023
f125e5d
conflicts
Gonals May 30, 2023
fddded2
conflicts
Gonals Jun 1, 2023
137631e
remove unused param
Gonals Jun 1, 2023
a433efb
prettier
Gonals Jun 1, 2023
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
40 changes: 34 additions & 6 deletions src/components/LHNOptionsList/OptionRowLHN.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import _ from 'underscore';
import React from 'react';
import PropTypes from 'prop-types';
import {
TouchableOpacity,
View,
StyleSheet,
} from 'react-native';
Expand All @@ -25,6 +24,9 @@ import themeColors from '../../styles/themes/default';
import SidebarUtils from '../../libs/SidebarUtils';
import TextPill from '../TextPill';
import OfflineWithFeedback from '../OfflineWithFeedback';
import PressableWithSecondaryInteraction from '../PressableWithSecondaryInteraction';
import * as ReportActionContextMenu from '../../pages/home/report/ContextMenu/ReportActionContextMenu';
import * as ContextMenuActions from '../../pages/home/report/ContextMenu/ContextMenuActions';

const propTypes = {
/** Style for hovered state */
Expand Down Expand Up @@ -62,7 +64,7 @@ const OptionRowLHN = (props) => {
return null;
}

let touchableRef = null;
let popoverAnchor = null;
const textStyle = props.isFocused
? styles.sidebarLinkActiveText
: styles.sidebarLinkText;
Expand Down Expand Up @@ -98,6 +100,28 @@ const OptionRowLHN = (props) => {

const avatarTooltips = !optionItem.isChatRoom && !optionItem.isArchivedRoom ? _.pluck(optionItem.displayNamesWithTooltips, 'tooltip') : undefined;

/**
* Show the ReportActionContextMenu modal popover.
*
* @param {Object} [event] - A press event.
*/
const showPopover = (event) => {
ReportActionContextMenu.showContextMenu(
ContextMenuActions.CONTEXT_MENU_TYPES.REPORT,
event,
'',
popoverAnchor,
props.reportID,
{},
'',
undefined,
undefined,
Gonals marked this conversation as resolved.
Show resolved Hide resolved
false,
false,
optionItem.isPinned,
);
};

return (
<OfflineWithFeedback
pendingAction={optionItem.pendingAction}
Expand All @@ -106,15 +130,19 @@ const OptionRowLHN = (props) => {
>
<Hoverable>
Copy link
Contributor

@0xmiros 0xmiros Apr 4, 2023

Choose a reason for hiding this comment

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

Let's use same pattern as others. What's the difference (pros/cons) between Hoverable inside PressableWithSecondaryInteraction and PressableWithSecondaryInteraction inside Hoverable?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We have hovered available for styling purposes. I think that's the main reason I have it this way

{hovered => (
<TouchableOpacity
ref={el => touchableRef = el}
<PressableWithSecondaryInteraction
pointerEvents="auto"
Gonals marked this conversation as resolved.
Show resolved Hide resolved
ref={el => popoverAnchor = el}
onPress={(e) => {
if (e) {
e.preventDefault();
}

props.onSelectRow(optionItem, touchableRef);
props.onSelectRow(optionItem, popoverAnchor);
}}
onSecondaryInteraction={e => showPopover(e)}
preventDefaultContentMenu
Gonals marked this conversation as resolved.
Show resolved Hide resolved
withoutFocusOnSecondaryInteraction
activeOpacity={0.8}
style={[
styles.flexRow,
Expand Down Expand Up @@ -235,7 +263,7 @@ const OptionRowLHN = (props) => {
</View>
)}
</View>
</TouchableOpacity>
</PressableWithSecondaryInteraction>
)}
</Hoverable>
</OfflineWithFeedback>
Expand Down
7 changes: 4 additions & 3 deletions src/components/PressableWithSecondaryInteraction/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import _ from 'underscore';
import React, {Component} from 'react';
import {Pressable} from 'react-native';
import {TouchableOpacity} from 'react-native';
import * as pressableWithSecondaryInteractionPropTypes from './pressableWithSecondaryInteractionPropTypes';
import styles from '../../styles/styles';
import * as DeviceCapabilities from '../../libs/DeviceCapabilities';
Expand Down Expand Up @@ -53,7 +53,7 @@ class PressableWithSecondaryInteraction extends Component {

// On Web, Text does not support LongPress events thus manage inline mode with styling instead of using Text.
return (
<Pressable
<TouchableOpacity
Copy link
Contributor

Choose a reason for hiding this comment

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

Touchable components will be deprecated - #14589.
Let's keep using Pressable

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We need this to be a TouchableOpacity for the ReportActions. It is not a "new" one, we are just moving it around. Once we decide how we want to handle those, then we'll replace it here.

Copy link
Contributor

Choose a reason for hiding this comment

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

Would you mind elaborating current issue with Pressable? so why you are forced to use TouchableOpacity

Copy link
Contributor

Choose a reason for hiding this comment

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

@0xmiroslav does this blurb from the PR description answer your question?

Surrounding the existing TouchableOpacity with a PressableWithSecondaryInteraction component did not work on mobile, as the triggered touch event was the TouchableOpacity one, so I replaced the TouchableOpacity with the PressableWithSecondaryInteraction and gave it opacity capabilities. All working nice now!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ReportActions currently use a TouchableOpacity, and the current UI is based on that. I'm moving that TouchableOpacity to the PressableWithSecondaryInteraction component (which currently surrounds the TouchableOpacity for ReportActions) so that it works correctly in all cases. Using Pressable here would mess the UI.

We are currently designing/developing a new component to replace TouchableOpacity. When that happens, it'll simply get replaced here too.

style={this.props.inline && styles.dInline}
onPressIn={this.props.onPressIn}
onLongPress={(e) => {
Expand All @@ -65,14 +65,15 @@ class PressableWithSecondaryInteraction extends Component {
}
this.props.onSecondaryInteraction(e);
}}
activeOpacity={this.props.activeOpacity}
onPressOut={this.props.onPressOut}
onPress={this.props.onPress}
ref={el => this.pressableRef = el}
// eslint-disable-next-line react/jsx-props-no-spreading
{...defaultPressableProps}
>
{this.props.children}
</Pressable>
</TouchableOpacity>
);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import _ from 'underscore';
import React, {forwardRef} from 'react';
import {Pressable} from 'react-native';
import {TouchableOpacity} from 'react-native';
Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry but I am still not convinced about this change. We're now actively deprecating all Touchable components to use Pressable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, yes, I changed this back in index.js, but forgot to do so for native. That's also what's causing the error.

import * as pressableWithSecondaryInteractionPropTypes from './pressableWithSecondaryInteractionPropTypes';
import Text from '../Text';
import HapticFeedback from '../../libs/HapticFeedback';
Expand All @@ -13,7 +13,7 @@ import HapticFeedback from '../../libs/HapticFeedback';
*/
const PressableWithSecondaryInteraction = (props) => {
// Use Text node for inline mode to prevent content overflow.
const Node = props.inline ? Text : Pressable;
const Node = props.inline ? Text : TouchableOpacity;
return (
<Node
ref={props.forwardedRef}
Expand All @@ -25,6 +25,7 @@ const PressableWithSecondaryInteraction = (props) => {
}}
onPressIn={props.onPressIn}
onPressOut={props.onPressOut}
activeOpacity={props.activeOpacity}
// eslint-disable-next-line react/jsx-props-no-spreading
{...(_.omit(props, 'onLongPress'))}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,17 @@ const propTypes = {
*
* - No support for delayLongPress.
* - No support for pressIn and pressOut events.
* - No support for opacity
*
* Note: Web uses styling instead of Text due to no support of LongPress. Thus above pointers are not valid for web.
*/
inline: PropTypes.bool,

/** Disable focus trap for the element on secondary interaction */
withoutFocusOnSecondaryInteraction: PropTypes.bool,

/** Opacity to reduce to when active */
activeOpacity: PropTypes.number,
};

const defaultProps = {
Expand All @@ -43,6 +47,7 @@ const defaultProps = {
preventDefaultContentMenu: true,
inline: false,
withoutFocusOnSecondaryInteraction: false,
activeOpacity: 1,
};

export {propTypes, defaultProps};
11 changes: 6 additions & 5 deletions src/libs/actions/Report.js
Original file line number Diff line number Diff line change
Expand Up @@ -600,22 +600,23 @@ function markCommentAsUnread(reportID, reportActionCreated) {
/**
* Toggles the pinned state of the report.
*
* @param {Object} report
* @param {Object} reportID
* @param {Boolean} isPinnedChat
*/
function togglePinnedState(report) {
const pinnedValue = !report.isPinned;
function togglePinnedState(reportID, isPinnedChat) {
const pinnedValue = !isPinnedChat;

// Optimistically pin/unpin the report before we send out the command
const optimisticData = [
{
onyxMethod: CONST.ONYX.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`,
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
value: {isPinned: pinnedValue},
},
];

API.write('TogglePinnedChat', {
reportID: report.reportID,
reportID,
pinnedValue,
}, {optimisticData});
}
Expand Down
2 changes: 1 addition & 1 deletion src/pages/home/HeaderView.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ const HeaderView = (props) => {
{shouldShowCallButton && <VideoChatButtonAndMenu isConcierge={isConcierge} guideCalendarLink={guideCalendarLink} />}
<Tooltip text={props.report.isPinned ? props.translate('common.unPin') : props.translate('common.pin')}>
<Pressable
onPress={() => Report.togglePinnedState(props.report)}
onPress={() => Report.togglePinnedState(props.report.reportID, props.report.isPinned)}
style={[styles.touchableButtonImage]}
>
<Icon src={Expensicons.Pin} fill={props.report.isPinned ? themeColors.heading : themeColors.icon} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class BaseReportActionContextMenu extends React.Component {
this.props.betas,
this.props.anchor,
this.props.isChronosReport,
this.props.isPinnedChat,
);

return (this.props.isVisible || this.state.shouldKeepOpen) && (
Expand Down
25 changes: 25 additions & 0 deletions src/pages/home/report/ContextMenu/ContextMenuActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const CONTEXT_MENU_TYPES = {
LINK: 'LINK',
REPORT_ACTION: 'REPORT_ACTION',
EMAIL: 'EMAIL',
REPORT: 'REPORT',
};

// A list of all the context actions in this menu.
Expand Down Expand Up @@ -254,6 +255,30 @@ export default [
},
getDescription: () => {},
},
{
textTranslateKey: 'common.pin',
icon: Expensicons.Pin,
shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, isPinnedChat) => type === CONTEXT_MENU_TYPES.REPORT && !isPinnedChat,
onPress: (closePopover, {reportID}) => {
Report.togglePinnedState(reportID, false);
if (closePopover) {
hideContextMenu(false);
}
},
getDescription: () => {},
},
{
textTranslateKey: 'common.unPin',
icon: Expensicons.Pin,
shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, isPinnedChat) => type === CONTEXT_MENU_TYPES.REPORT && isPinnedChat,
onPress: (closePopover, {reportID}) => {
Report.togglePinnedState(reportID, true);
if (closePopover) {
hideContextMenu(false);
}
},
getDescription: () => {},
},
];

export {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class PopoverReportActionContextMenu extends React.Component {
},
isArchivedRoom: false,
isChronosReport: false,
isPinnedChat: false,
};
this.onPopoverShow = () => {};
this.onPopoverHide = () => {};
Expand Down Expand Up @@ -124,6 +125,7 @@ class PopoverReportActionContextMenu extends React.Component {
* @param {Function} [onHide] - Run a callback when Menu is hidden
* @param {Boolean} isArchivedRoom - Whether the provided report is an archived room
* @param {Boolean} isChronosReport - Flag to check if the chat participant is Chronos
* @param {Boolean} isPinnedChat - Flag to check if the chat is pinned in the LHN. Used for the Pin/Unpin action
*/
showContextMenu(
type,
Expand All @@ -135,8 +137,9 @@ class PopoverReportActionContextMenu extends React.Component {
draftMessage,
onShow = () => {},
onHide = () => {},
isArchivedRoom,
isChronosReport,
isArchivedRoom = false,
isChronosReport = false,
isPinnedChat = false,
) {
const nativeEvent = event.nativeEvent || {};
this.contextMenuAnchor = contextMenuAnchor;
Expand Down Expand Up @@ -167,6 +170,7 @@ class PopoverReportActionContextMenu extends React.Component {
reportActionDraftMessage: draftMessage,
isArchivedRoom,
isChronosReport,
isPinnedChat,
});
});
}
Expand Down Expand Up @@ -241,6 +245,7 @@ class PopoverReportActionContextMenu extends React.Component {
reportAction={this.state.reportAction}
isArchivedRoom={this.state.isArchivedRoom}
isChronosReport={this.state.isChronosReport}
isPinnedChat={this.state.isPinnedChat}
anchor={this.contextMenuTargetNode}
contentRef={this.setContentRef}
/>
Expand Down Expand Up @@ -272,6 +277,7 @@ class PopoverReportActionContextMenu extends React.Component {
shouldSetModalVisibilityForDeleteConfirmation: true,
isArchivedRoom: false,
isChronosReport: false,
isPinnedChat: false,
});
}

Expand Down Expand Up @@ -318,6 +324,7 @@ class PopoverReportActionContextMenu extends React.Component {
draftMessage={this.state.reportActionDraftMessage}
isArchivedRoom={this.state.isArchivedRoom}
isChronosReport={this.state.isChronosReport}
isPinnedChat={this.state.isPinnedChat}
anchor={this.contextMenuTargetNode}
contentRef={this.contentRef}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const contextMenuRef = React.createRef();
/**
* Show the ReportActionContextMenu modal popover.
*
* @param {string} type - the context menu type to display [EMAIL, LINK, REPORT_ACTION]
* @param {string} type - the context menu type to display [EMAIL, LINK, REPORT_ACTION, REPORT]
* @param {Object} [event] - A press event.
* @param {String} [selection] - Copied content.
* @param {Element} contextMenuAnchor - popoverAnchor
Expand All @@ -16,6 +16,7 @@ const contextMenuRef = React.createRef();
* @param {Function} [onHide=() => {}] - Run a callback when Menu is hidden
* @param {Boolean} isArchivedRoom - Whether the provided report is an archived room
* @param {Boolean} isChronosReport - Flag to check if the chat participant is Chronos
* @param {Boolean} isPinnedChat - Flag to check if the chat is pinned in the LHN. Used for the Pin/Unpin action
*/
function showContextMenu(
type,
Expand All @@ -29,6 +30,7 @@ function showContextMenu(
onHide = () => {},
isArchivedRoom = false,
isChronosReport = false,
isPinnedChat = false,
) {
if (!contextMenuRef.current) {
return;
Expand All @@ -45,6 +47,7 @@ function showContextMenu(
onHide,
isArchivedRoom,
isChronosReport,
isPinnedChat,
);
}

Expand Down
6 changes: 1 addition & 5 deletions tests/actions/ReportTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,6 @@ describe('actions/Report', () => {
const TEST_USER_ACCOUNT_ID = 1;
const TEST_USER_LOGIN = 'test@test.com';
const REPORT_ID = '1';
const REPORT = {
reportID: REPORT_ID,
isPinned: false,
};

let reportIsPinned;
Onyx.connect({
Expand All @@ -162,7 +158,7 @@ describe('actions/Report', () => {
// Set up Onyx with some test user data
return TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN)
.then(() => {
Report.togglePinnedState(REPORT);
Report.togglePinnedState(REPORT_ID, false);
return waitForPromisesToResolve();
})
.then(() => {
Expand Down