Page MenuHomePhabricator

[native] Unblock scrolling when continuing editing
ClosedPublic

Authored by kuba on Jun 21 2023, 3:03 AM.
Tags
None
Referenced Files
F1700885: D8271.diff
Sat, May 4, 2:47 PM
Unknown Object (File)
Wed, Apr 24, 7:01 AM
Unknown Object (File)
Wed, Apr 24, 7:01 AM
Unknown Object (File)
Wed, Apr 24, 7:01 AM
Unknown Object (File)
Wed, Apr 24, 7:01 AM
Unknown Object (File)
Wed, Apr 24, 7:01 AM
Unknown Object (File)
Wed, Apr 24, 7:01 AM
Unknown Object (File)
Wed, Apr 24, 7:01 AM
Subscribers
None

Details

Summary

After the user decides to continue editing if tried to open the modal, he wasn't able to scroll anymore. This diff addresses that, by unlocking scrolling after closing the alert.

Test Plan
  • Enter edit mode and make changes to the message,
  • Try to open the tooltip modal
  • Press continue editing
  • Check if the user still can scroll the messages list

Diff Detail

Repository
rCOMM Comm
Lint
Lint Not Applicable
Unit
Tests Not Applicable

Event Timeline

tomek added inline comments.
native/utils/edit-messages-utils.js
5–13 ↗(On Diff #27922)

Maybe we should be more explicit in naming these

This revision is now accepted and ready to land.Jun 23 2023, 6:51 AM
ashoat requested changes to this revision.Jun 27 2023, 7:59 PM

This appears to be the only place in the codebase where we need to manually call setScrollBlockingModalStatus('closed'). All other places are handled directly by the code inside OverlayNavigator.

Can you explain why this is necessary in this case, and why this case can't be handled by the normal codepaths? I understand that you were able to get things "to work" with this, but based on the lack of details in the diff description I suspect that we don't have a full understanding of why things weren't working in this first place, which makes it hard to confidently conclude that this is the correct solution (as opposed to, say, adjusting the code in OverlayNavigator).

native/utils/edit-messages-utils.js
6–7 ↗(On Diff #28099)

This contract is confusing all the callsite. We are passing two functions in, but it's not clear which one does which. Can we modify this function to take an object with named parameters, so that it's more clear at the callsite what's going on?

This revision now requires changes to proceed.Jun 27 2023, 7:59 PM
tomek requested changes to this revision.Jun 29 2023, 6:55 AM
tomek added inline comments.
native/chat/chat-input-bar.react.js
899–901 ↗(On Diff #28099)

Maybe we should be able to replace this with modifying getScrollBlockingModalStatus of OverlayNavigator?

kuba marked an inline comment as done.

Address review

Updated with a new approach:
when the user is in edit mode the modals scrolling is always unlocked.

native/chat/chat-input-bar.react.js
899–901 ↗(On Diff #28099)

This function (getScrollBlockingModalStatus) is not called when the alert is displayed or closed by continuing editing. But probably replacing scrollBlockingModalStatus will work here.

native/utils/edit-messages-utils.js
6–7 ↗(On Diff #28099)

Refactored in the previous diff.

ashoat requested changes to this revision.Jul 4 2023, 6:52 PM

I really want to accept this, but frankly I'm confused about this diff.

Realize I should've asked this earlier, but I don't think I understand the initial purpose of the diff. The first sentence in the diff description isn't clear, and I think has some English errors. The second sentence isn't really that specific.

More importantly, I still can't make sense of why this approach is the right one. As a starting point, why do we need this special "hack" at all? It appears that normally closed gets set automatically. Can you explain (in detail) why the existing functionality is unsufficient, and why the best solution is this hack?

Finally, I am not sure what makes the new solution better than the original one.

@kuba, the following would be helpful:

  1. A video showing the bug you are fixing
  2. A detailed explanation explaining how and why the bug is occurring
  3. An explanation for how you are choosing to "fix" the bug, and the other options you've considered
This revision now requires changes to proceed.Jul 4 2023, 6:52 PM

Hey, sorry for the miscommunication. Here are answers to your questions:

  1. A video showing the bug you are fixing

It occurs when the user wants to open a modal in edit mode but decides to continue editing. In that situation, he can't scroll the chat, and he still can't scroll the chat when he discards edit by i.g. pressing the cross button.

  1. A detailed explanation explaining how and why the bug is occurring

The bug on video occurs because the text-message.react.js sets the modal status to open before firing the navigate function:

overlayContext.setScrollBlockingModalStatus('open');

A similar situation is in:

  • multimedia-message-multimedia.react.js,
  • and probably in the robotext-message.react.js, but I haven't tested it.
  1. An explanation for how you are choosing to "fix" the bug, and the other options you've considered

Unfortunately, when making the video, I found out that after exiting the edit mode the open modal status remains, and blocks the scrolling.

Solutions I tried:

  • current one: appeared it doesn't unblock scrolling after leaving edit mode,
  • the previous one - when the user decides to continue editing, we just set the modal status to closed, and unblock the scrolling as expected,
  • I tried wrapping the setScrollBlockingModalStatus function, and preventing changing it in the edit state, but it didn't work for 2 reasons:
    • the InputStateContext was not always available here,
    • I can't determine, what should be status of the scrollBlockingModalStatus after leaving edit mode: it depends on what the user chooses to do, if the user opens the modal, we want to leave edit mode with open modal blocking state, otherwise it should be closed.

I believe, that the previous solution was the simplest one, and covered all cases.

It should work in all considered situations:

  • the user decides to discard the edit by opening the modal -> scrolling blocked,
  • the user decides to continue editing -> scrolling unblocked,
  • the user decides to discard the edit by i.g. pressing the cross button (or simply sends the edited message) -> scrolling unblocked.

Removed unused OverlayContext

I still can't tell what makes this case "special", and what justifies this hack. In no other place do we have to manually set closed. Why is this case different? Why can't we continue to rely on the standard mechanism for setting closed?

ashoat requested changes to this revision.Jul 5 2023, 11:54 AM
This revision now requires changes to proceed.Jul 5 2023, 11:54 AM

Requesting review to

I still can't tell what makes this case "special", and what justifies this hack. In no other place do we have to manually set closed? Why is this case different? Why can't we continue to rely on the standard mechanism for setting closed

It is special because the i.g. text-message.react.js:

  • sets scroll blocking modal to open before calling the navigate:
overlayContext.setScrollBlockingModalStatus('open');
  • we cancel the navigate action, so the modal doesn't open,
  • the scroll blocking modal status is normally set to closed after closing the modal,
  • closing the alert doesn't trigger rerender -> the getScrollBlockingModalStatus is not called -> standard mechanism doesn't work.

In that case, we don't open the modal at all, so it can't close. But the open status remains.

Thanks for explaining! Your explanation gave me some helpful pointers, but I still had to do some investigation on my own to get the whole story.

I'll try to explain the whole story as to why we need a "hack" here:

  1. In some places in the codebase, we preemptively call setScrollBlockingModalStatus('open'). OverlayNavigator would normally handle this via getScrollBlockingModalStatus, but this only gets called when the screen is being displayed, which leaves some time after measurement where the scroll position can change. By preemptively calling setScrollBlockingModalStatus('open') right before we measure, we guarantee that the measure location will be accurate.
  2. By the time we call setScrollBlockingModalStatus('open'), we know we are going to display the modal. We know that when we dismiss the modal, OverlayNavigator will set it back to closed via getScrollBlockingModalStatus. So this is normally safe.
  3. However, the changes in D8270 complicate this. There we add functionality to OverlayRouter to "cancel" the navigation action when !removeEditMode(action). In that case, there is nothing to undo the preemptive call to setScrollBlockingModalStatus('open'). So now we must handle this ourselves.

I believe, that the previous solution was the simplest one, and covered all cases.

That makes sense. Let's evaluate the previous solution.

  1. Your approach of calling setScrollBlockingModalStatus('closed') when the user chooses to cancel the action is a good way to "undo" the preemptive setScrollBlockingModalStatus('open') call.
  2. We might be worried that this will happen even if the modal is not in scrollBlockingModals. Before this change, we would never call setScrollBlockingModalStatus for modals that aren't in scrollBlockingModals. Is there a risk now that we are potentially calling setScrollBlockingModalStatus for other modals?
    • We could imagine a scroll blocking modal being displayed, and then a non-scroll blocking modal being displayed over it. If edit mode blocks the non-scrolling blocking modal from displaying, we would not want it to call setScrollBlockingModalStatus('closed'), since that would mess with the scroll blocking modal at the bottom.
    • The naive solution would be to check if the modal is not in scrollBlockingModals. But unfortunately, this is not enough, as we don't ALWAYS preemptively call setScrollBlockingModalStatus('open'): for instance, ThreadSettingsMediaGallery does not do this preemptive call when displaying ImageModal. We could imagine a case where a second scroll-blocking modal is blocked from displaying due to edit mode, but it doesn't do the preemptive call... in that case, our call setScrollBlockingModalStatus('closed') would mess with the first (already-displayed) scroll-blocking modal, which is not what we want.
    • getScrollBlockingModalStatus handles this better by checking the whole stack of modals and looking for any scroll-blocking modal. Setting open is safer than setting closed, since we want to set open if ANY scroll-blocking modal is present, but we only want to set closed if NO scroll-blocking modal is present.
    • Ultimately we could consider ourselves "safe" because of the details on when we expect the edit mode to appear. But this "hack" is still breaking some assumptions regarding the scroll-blocking modal status.

To summarize, the big concern is this one:

Setting open is safer than setting closed, since we want to set open if ANY scroll-blocking modal is present, but we only want to set closed if NO scroll-blocking modal is present.

I'd like to consider another solution: triggering getScrollBlockingModalStatus in this case.

  1. getScrollBlockingModalStatus already has the logic to set the scroll-blocking modal status correctly. If it's called when there is no scroll-blocking modal in the stack, it will correctly clear it. But if there is still a scroll-blocking modal, it will not clear it.
    • In other words, calling getScrollBlockingModalStatus can be thought of as "resetting" the scroll-blocking modal status to match the React Navigation state.
    • However, getScrollBlockingModalStatus has the "negative" effect of clearing our preemptive override, so we only call it when something changes (eg. a modal appears or dismisses).
    • But "resetting" the scroll-blocking modal status to match React Navigation state is exactly what we want here!
  2. My concrete proposal: can we expose a function called resetScrollBlockingModalStatus() in OverlayContext?
    • This is already where we expose setScrollBlockingModalStatus, so I think it's a good place.
    • We could define resetScrollBlockingModalStatus() as () => setScrollBlockingModalStatus(getScrollBlockingModalStatus(sceneList))
    • Then we can use @kuba's previous solution but call resetScrollBlockingModalStatus() instead of setScrollBlockingModalStatus('closed')

Does that makes sense?

Add resetScrollBlockingModalStatus function

Sorry, for the delay in the response, I had an exam session.

Thanks for the detailed response and for clarifying why setting the modal status to closed was unsafe. That was probably the part I was missing.

Does that makes sense?

Yes, it does. I've updated the diff.

native/navigation/overlay-navigator.react.js
161–162 ↗(On Diff #28567)

Shouldn't we wrap this with a callback?

native/navigation/overlay-navigator.react.js
161–162 ↗(On Diff #28567)

I'm not sure. I don't have time right now, but I simply changed it into:

const resetScrollBlockingModalStatus = React.useCallback(
  () =>
    setScrollBlockingModalStatus(getScrollBlockingModalStatus(sceneList)),
  [sceneList],
);

but it didn't work:

Cannot use variable  `sceneList` [1] because the declaration either comes later or was skipped.Flow(reference-before-declaration)

I had little time to investigate it, just giving an update.
If needed, I will have a look at it tomorrow.

ashoat requested changes to this revision.Jul 11 2023, 9:57 AM

I think we should actually use prevSceneDataRef.current instead of sceneList, and I agree that resetScrollBlockingModalStatus needs to be defined via React.useCallback

native/navigation/overlay-navigator.react.js
161–162 ↗(On Diff #28567)

@tomek is right that we should never define a function this way, without wrapping it in React.useCallback. But this points to a more serious problem.

In retrospect, my suggestion of sceneList here was not a good one for two reasons:

  1. sceneList is defined way lower, which is what leads to the Flow error when sceneList is directly invoked in the dep list. I think JS doesn't mind if you bind it into a function definition... it only cares if you directly invoke it, which is why the error doesn't appear without React.useCallback
  2. We should avoid recalculating resetScrollBlockingModalStatus whenever sceneList changes, as sceneList recalculates on each render. Ideally we can make sure resetScrollBlockingModalStatus never recalculates

In order to make sure resetScrollBlockingModalStatus never recalculates, we have two options:

  1. Use a ref in the definition rather than a variable derived from state
  2. Have the function set some state directly, which will later trigger scrollBlockingModalStatus to reset during OverlayNavigator's render

I think option 1 is more simple. Instead of sceneList, I think we can use prevSceneDataRef.current.

This revision now requires changes to proceed.Jul 11 2023, 9:57 AM

Apologies for the continued churn, but I'm pretty sure this will be the last revision :)

kuba marked 2 inline comments as done.

Use useCallback

native/navigation/overlay-navigator.react.js
164 ↗(On Diff #28609)

sceneList was created from updated scene data by taking values and sorting values(updatedSceneData).sort. Can we use values instead of _values? Should we also sort?

See inline comment... I think we can probably just go with prevScenesRef.current. @kuba, if you could make that update and test this one final time before landing, that would be deeply appreciated!

native/navigation/overlay-navigator.react.js
164 ↗(On Diff #28609)
  • From my analysis of getScrollBlockingModalStatus, I don't think we need to sort
  • I agree we should use values instead of _values
  • We could consider using prevScenesRef.current instead of values(prevSceneDataRef.current). That also has the benefit that prevScenesRef is defined above this line
  • If we stick with values(prevSceneDataRef.current), I think it would be good to move the definition of prevSceneDataRef above this line

Probably easiest to just go with prevScenesRef.current:

const resetScrollBlockingModalStatus = React.useCallback(() => {
  setScrollBlockingModalStatus(
    getScrollBlockingModalStatus(prevScenesRef.current),
  );
}, []);
kuba marked 2 inline comments as done.

Address review

tomek added inline comments.
native/navigation/overlay-navigator.react.js
162 ↗(On Diff #28642)

Wondering if it is safe to early exit when !prevScenesRef.current. I guess it could happen only in the first render, but it might be safer to have e.g. getScrollBlockingModalStatus(prevScenesRef.current ?? []),

This revision is now accepted and ready to land.Jul 13 2023, 1:17 AM
kuba marked an inline comment as done.

Review