Closed kavimuru closed 1 year ago
Triggered auto assignment to @johnmlee101 (Engineering
), see https://stackoverflow.com/c/expensify/questions/4319 for more details.
@kavimuru I found this issue while working on https://github.com/Expensify/App/pull/8195 and reported on slack here: https://app.slack.com/client/T03SC9DTT/C01GTK53T8Q does that count for reporting bounty?
@mateusbra I don't have access to the slack. But as per this comment, https://github.com/Expensify/App/pull/8195#issuecomment-1070240184 yes. Also tester can repro this bug too https://github.com/Expensify/App/issues/8006#issuecomment-1069334038
Opening this up!
Triggered auto assignment to @Christinadobrzyn (External
), see https://stackoverflow.com/c/expensify/questions/8582 for more details.
This should stay daily priority
@mateusbra do you have interest in working on this one since you discovered it? Feel free to leave a proposal if so.
@kavimuru can you please make sure we at the "@ reported by" to a GH title if they are due the bug reporting bounty? Seems applicable in this case for @mateusbra but maybe I'm not understanding fully?
@Christinadobrzyn Uh oh! This issue is overdue by 2 days. Don't forget to update your issues!
Sorry for the delay, i was ooo. Posted job to Upwork;
Internal job - https://www.upwork.com/ab/applicants/1508625903649284096/job-details External job - https://www.upwork.com/jobs/~01bd32eda238a19082
It does look like @mateusbra found/reported this issue so added them to the OP as the reporter.
Triggered auto assignment to Contributor-plus team member for initial proposal review - @parasharrajat (Exported
)
Triggered auto assignment to @marcochavezf (Exported
), see https://stackoverflow.com/c/expensify/questions/7972 for more details.
Hi, This is Zamar from Upwork.com I will fix the issue by implementing URL parsing feature in ContextMenuAction. Now there is no checking parts for url/email when the context menu is shown. So there will be a validation for the clipboard text, which will check if it is an email address or an url; and switch the caption of the menu item according to the result. Thank you.
I don't see how this is different from https://github.com/Expensify/App/pull/8195. This should have been fixed on https://github.com/Expensify/App/issues/8311. cc: @Santhosh-Sellavel @mateusbra
why do we need a different issue?
@parasharrajat
copy URL
option instead of copy email
.https://github.com/Expensify/App/issues/8311 - But the existing copy URL itself not working on mWeb at all.
Not overdue, waiting for proposals.
I'm going to be ooo until April 26th so I'm going to assign a new CM to this issue. Thank you!
Triggered auto assignment to @arielgreen (External
), see https://stackoverflow.com/c/expensify/questions/8582 for more details.
Price increase to $500
waiting on proposals...
Root cause:
When we long press on an email/link, BaseAnchorForCommentsOnly's onSecondaryInteraction is fired, calling ReportActionContextMenu.showContextMenu
with a corresponding type (CONTEXT_MENU_TYPES.EMAIL
or CONTEXT_MENU_TYPES.LINK
).
Howewer, since BaseAnchorForCommentsOnly
is wrapped inside a second PressableWithSecondaryInteraction
on ReportActionItem page, it's showPopover method is also fired, calling ReportActionContextMenu.showContextMenu
with a CONTEXT_MENU_TYPES.REPORT_ACTION
type.
So, in short, when we long press, ReportActionContextMenu.showContextMenu
is called with the right type and then almost instantly it gets called with the wrong type, which results in the wrong context menu being shown.
Proposal: Throttle showContextMenu method, so the second call get's omitted.
showContextMenu = _.throttle((
type,
event,
selection,
contextMenuAnchor,
reportID,
reportAction,
draftMessage,
onShow = () => {},
onHide = () => {},
) => {
const nativeEvent = event.nativeEvent || {};
this.contextMenuAnchor = contextMenuAnchor;
// Singleton behaviour of ContextMenu creates race conditions when user requests multiple contextMenus.
// But it is possible that every new request registers new callbacks thus instanceID is used to corelate those callbacks
this.instanceID = Math.random().toString(36).substr(2, 5);
// Register the onHide callback only when Popover is shown to remove the race conditions when there are mutltiple popover open requests
this.onPopoverShow = () => {
onShow();
this.onPopoverHide = onHide;
};
this.getContextMenuMeasuredLocation().then(({x, y}) => {
this.setState({
cursorRelativePosition: {
horizontal: nativeEvent.pageX - x,
vertical: nativeEvent.pageY - y,
},
popoverAnchorPosition: {
horizontal: nativeEvent.pageX,
vertical: nativeEvent.pageY,
},
type,
reportID,
reportAction,
selection,
isPopoverVisible: true,
reportActionDraftMessage: draftMessage,
});
});
}, 100, {trailing: false})
Result:
Chrome on Android
https://user-images.githubusercontent.com/9059945/163683498-971dc82d-ae30-49a4-93df-ae03d8673527.mp4
Safari on iOS
https://user-images.githubusercontent.com/9059945/163683471-88facc87-5664-4af5-b166-934eddc856e3.mp4
Root cause seems correct but I don't like the proposal.
Root cause seems correct but I don't like the proposal.
I've found a simpler fix: The issue arises because in PressableWithSecondaryInteraction/index.js, we have a LongPressGestureHandler, which wraps Pressable, so essentially links are wrapped like this:
<LongPressGestureHandler>
<Pressable>
<LongPressGestureHandler>
<Pressable>
<TextOfTheLink>
</Pressable>
</LongPressGestureHandler>
</Pressable>
</LongPressGestureHandler>
To fix this, we need to:
longPressGestureHandlerEnabled={false}
to this line on ReportActionItem
(prop probably needs a better name)if (e.nativeEvent.state !== State.ACTIVE || !this.props.longPressGestureHandlerEnabled) {
To be honest, I'm not entirely sure why it works, but it does
https://user-images.githubusercontent.com/9059945/163777528-d0ace458-b6e2-40f8-91c7-534dce3f4870.mp4
Ok. What is the base of this solution? Why do you think we should/have to do this?
The proposal is still lacking the proper explanation of Why the solution?
Ok. What is the base of this solution? Why do you think we should/have to do this?
The proposal is still lacking the proper explanation of Why the solution?
Case 1: user long presses on a comment: LongPressGestureHandler's onHandlerStateChange
fires, which in turn calls onSecondaryInteraction={this.showPopover}
Case 2: user long presses on a link inside the comment: LongPressGestureHandler's onHandlerStateChange
fires, which in turn calls onSecondaryInteraction={(e) => ReportActionContextMenu.showContextMenu()}.
But, since in this case we have one LongPressGestureHandler's
nested inside another one, both of their onHandlerStateChange events are fired, one on BaseAnchorForCommentsOnly/index.js
(the right one) and one from ReportActionItem.js
(the wrong one).
Adding longPressGestureHandlerEnabled={false}
will essentially disable parent's onHandlerStateChange
in case it's child has already fired onHandlerStateChange
.
I understood the root cause. I got your solution and how this works. But what I am asking is why your solution(Workaround)?
Why do you think we should do either of your changes? Is there any systematic way of preventing event bubbling?
I think there is a better solution. I will wait to see that proposal before making the choice.
I understood the root cause. I got your solution and how this works. But what I am asking is why your solution(Workaround)?
Why do you think we should do either of your changes? Is there any systematic way of preventing event bubbling?
Got it, sorry for the misunderstanding. I'll try to find a more universal approach a bit later
Not overdue, waiting for proposals.
When we tap the email address, the inner PressableWithSecondaryInteraction component fire an event, in the event handler we call ReportActionContextMenu.showContextMenu() with type 'EMAIL': https://github.com/Expensify/App/blob/4c64bec3f38af925a01911155eab54feafc96cda/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly/index.js#L64-L76 And the outer PressableWithSecondaryInteraction component also fire an event after that, in the event handler we call ReportActionContextMenu.showContextMenu() with type 'REPORT_ACTION': https://github.com/Expensify/App/blob/4c64bec3f38af925a01911155eab54feafc96cda/src/pages/home/report/ReportActionItem.js#L99-L115 The outer component toggle the modal popover with regular context menu 'Copy to clipboard', so we can't see the modal popover with context menu 'Copy e-mail to clipboard'.
Since the inner PressableWithSecondaryInteraction already toggle the modal popover, and isPopoverVisible property stored in PopoverReportActionContextMenu's state, we can check isPopoverVisible is true in showContextMenu function, if it's true, do nothing and return. https://github.com/Expensify/App/blob/68f0bbf9c7fe8a95c7d65b0a9153229375863d1e/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js#L114-L124
+ if (this.state.isPopoverVisible) {
+ return;
+ }
Add a parameter to function showContextMenu to record the inner component or the outer component called this function, and store this property in PopoverReportActionContextMenu's state, check the state value first in showContextMenu function, if inner component has already called this function, do nothing and return.
Hmm! I strongly recommend looking back at the previous discussion. I asked
Why do you think we should do either of your changes? Is there any systematic way of preventing event bubbling?
@b1tjoy your proposal looks hacky to me.
Proposal for mWeb:
react-native-gesture-handler
. This is a minimal reproduction snack, running it on web you can see that when longpressing on text, two console statements appear, while there's should be only one ('pressed 2')mWeb
, we're interested in PressableWithSecondaryInteraction/index.js file.
LongPressGestureHandler was added to this file in this PR, which resolved #7462. However, #7462 was occuring only on native platforms, longpressing on images worked fine on mWeb
.LongPressGestureHandler
from PressableWithSecondaryInteraction/index.js and add onLongPress listener to Pressable:
onLongPress={this.callSecondaryInteractionWithMappedEvent}
https://user-images.githubusercontent.com/9059945/166672914-0de59ede-13f4-46db-97be-91b745f74f54.mp4
This resolves the current issue and doesn't break the #7462
But While testing this on other platforms i found a similar issue on iOS, which is similar to this, has different root cause, but still caused by LongPressGestureHandler. Pressing on plain link works fine on iOS, but then you have a message that contains text and link, regular context menu is displayed
https://user-images.githubusercontent.com/9059945/166672883-57751255-0649-41df-81b9-43ac39b65ea3.mov
But the cause is different from web, where both LongPressGestureHandler listeners are called, on iOS only the parent's listener get's called. However, i'm not sure this is not a fluke of dev mode/simulator, since I'm getting mixed results when testing this (sometimes it works as expected, sometimes it doesn't) and when running snack on iOS, i'm also getting random results.
Can someone plese check if this issue present when runnig production build on physical iOS device?
Thanks for the updated proposal @eVoloshchak. Seems to me the best proposal so far but let's discuss the iOS issue you mentioned first. Unfortunately, I don't have iOS. Let's see who can help us here to test it.
I tested it on your snack but I also found the same issue. After some digging, I found out that iOS handle events differently, and all the handlers are triggered simultaneously when touches are overlapping. https://github.com/software-mansion/react-native-gesture-handler/issues/1930#issuecomment-1068920275.
They suggest using https://docs.swmansion.com/react-native-gesture-handler/docs/gesture-composition/#simultaneous. I don't know how they can be used.
Let me know if you find something.
I found out that iOS handle events differently, and all the handlers are triggered simultaneously when touches are overlapping. software-mansion/react-native-gesture-handler#1930 (comment).
They suggest using https://docs.swmansion.com/react-native-gesture-handler/docs/gesture-composition/#simultaneous. I don't know how they can be used.
@parasharrajat , this is a great find actually, I was lookig for something like this, but never managed to find it. But we can't use this in our scenario (at least I couldn't figure out a way to). Besides, as it turns out, this is not iOS specific, this problem is also present on Android (the issue description should probably be updated). Same as with iOS, wrong context menu is displayed when we have a message with link and text.
https://user-images.githubusercontent.com/9059945/167257217-cbb1fb21-7723-4f77-bd3f-e996a0aaac8b.mp4
I've recreated out setup in this snack, you can see, that no matter where you long press, only the parent handler registers the touch. And this is not a problem with nested gesture handlers either. Even if you delete the parent handler (const App, line 18), the handler in AnchorRenderer doesn't get called. It starts working if you wrap Text into View in AnchorRenderer, but i'm guessing that would break #4911.
But as this is the case with mWeb, we can look into why LongPressHandler
was added in the first place.
As I mentioned previuosly, it was added to fix https://github.com/Expensify/App/issues/7462.
So actually, we need LongPressHandler
only for messages with attachments, not for all the messages.
That means we can:
Add an isAttachment
method to ReportActionItem.js
isAttachment() {
const message = _.last(lodashGet(this.props.action, 'message', [{}]));
const isAttachment = _.has(this.props.action, 'isAttachment')
? this.props.action.isAttachment
: ReportUtils.isReportMessageAttachment(message);
return isAttachment;
}
PressableWithSecondaryInteraction
here
useLongPressHandler={this.isAttachment()}
And in PressableWithSecondaryInteraction/index.native.js use LongPressHandler
only for attachments
const PressableWithSecondaryInteraction = (props) => {
// Use Text node for inline mode to prevent content overflow.
const Node = props.inline ? Text : Pressable;
if (props.useLongPressHandler) {
return (
<LongPressGestureHandler
onHandlerStateChange={(e) => {
if (e.nativeEvent.state !== State.ACTIVE) {
return;
}
e.preventDefault();
HapticFeedback.trigger();
props.onSecondaryInteraction(e);
}}
>
<Node
ref={props.forwardedRef}
onPress={props.onPress}
onPressIn={props.onPressIn}
onPressOut={props.onPressOut}
// eslint-disable-next-line react/jsx-props-no-spreading
{...(_.omit(props, 'onLongPress'))}
>
{props.children}
</Node>
</LongPressGestureHandler>
);
}
return (
<Node
ref={props.forwardedRef}
onPress={props.onPress}
onPressIn={props.onPressIn}
onPressOut={props.onPressOut}
onLongPress={(e) => {
e.preventDefault();
HapticFeedback.trigger();
props.onSecondaryInteraction(e);
}}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
>
{props.children}
</Node>
)
};
Android:
https://user-images.githubusercontent.com/9059945/167259600-8c55bf45-df90-4c3f-a40d-a9a636114c8d.mp4
iOS:
https://user-images.githubusercontent.com/9059945/167259617-e988c14f-a35d-4901-bbe3-26584e4d8fe2.mov
Thank you for looking into this. I need some time to research more on what you presented.
I haven't forgotten this. Please expect an update in < 20 hours.
Looking now.
@eVoloshchak this proposal somewhat matches with the approach from the previous proposal. This is doable but I don't think it would be much helpful. It is limited to this scenario only. If we are about to make any change it can break.
What if we manage the nested handlers. I have a crazy ideas.
LongPressGestureHandler
around the children.Array<{ref: Ref, level: Number}>
. we will push each instance on the context value.{ref: Ref, level: Number, active: Boolean}
.We can also cancel the event at any level. We just need to prevent bubbling.
What do you say? This will allow us to use this anywhere in the app.
What do you say? This will allow us to use this anywhere in the app.
Sounds fun :) I'll try to implement this, will come back with results in ~3 days
I am not saying that it is the way but thought of sharing. Thanks.
@parasharrajat , just realised this is not gonna work, since the issue isn't with nested gesture handlers.
Take a look at this snack, it has only one LongPressGestureHandler
, but still doesn't work.
However, if we take the same snack, but wrap our url Text
with View
, it starts working - snack.
I'll try to look into why wrapping View
with LongPressGestureHandler
works, but wrapping Text
doesn't
Not overdue, waiting for proposals.
I will open a slack discussion to discuss existing proposals. But this is still open for better proposals.
I will open a slack discussion to discuss existing proposals. But this is still open for better proposals.
@parasharrajat are we still waiting?
Yeah, I will open the discussion soon. Need to summarize the discussion. But I open to proposals.
I will open the discussion on slack shortly...
I will open the discussion on slack shortly...
Hi @parasharrajat! :) any update on this?
This got slipped my mind. Opening the discussion after a while (need to summarize the above proposals). But I am also open for new proposals.
Summarizing it. I will post it tomorrow.
If you haven’t already, check out our contributing guidelines for onboarding and email contributors@expensify.com to request to join our Slack channel!
Issue was found when executing #8195
Action Performed:
Expected Result:
The message "Copy e-mail to clipboard" displayed when you try to copy an email and "copy URL to clipboard" tapping URL
Actual Result:
The message "Copy to clipboard" displayed when you try to copy an email. Sometimes it shows as regular context menu
Workaround:
Unknown
Platform:
Where is this issue occurring?
Version Number: 1.1.46-0
Reproducible in staging?: y
Reproducible in production?: y (New feature)
Email or phone of affected tester (no customers):
Logs: https://stackoverflow.com/c/expensify/questions/4856
Notes/Photos/Videos:
https://user-images.githubusercontent.com/43996225/160028142-3fe96a20-6e66-4605-ae2a-618bc322f957.mp4
https://user-images.githubusercontent.com/43996225/160028204-1fe2a591-3466-430e-9802-147d0baa43ee.mp4
Expensify/Expensify Issue URL:
Issue reported by: Reported by @mateusbra https://expensify.slack.com/archives/C01GTK53T8Q/p1647474115298459
Slack conversation:
View all open jobs on GitHub Issue was found when executing #8195
Upwork Automation - Do Not Edit