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

Fix tapping on new messages after marking a message unread leads to a blank page #32630

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
73 changes: 70 additions & 3 deletions src/components/InvertedFlatList/BaseInvertedFlatList.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,78 @@
import type {ForwardedRef} from 'react';
import React, {forwardRef} from 'react';
import type {FlatListProps} from 'react-native';
import React, {forwardRef, useEffect, useRef} from 'react';
import type {NativeScrollEvent, NativeSyntheticEvent} from 'react-native';
import FlatList from '@components/FlatList';
import type {InvertedFlatListProps} from './types';

const WINDOW_SIZE = 15;
const AUTOSCROLL_TO_TOP_THRESHOLD = 128;

function BaseInvertedFlatList<T>(props: FlatListProps<T>, ref: ForwardedRef<FlatList>) {
function BaseInvertedFlatList<T>({onScroll: onScrollProp = () => {}, onScrollEnd: onScrollEndProp = () => {}, ...props}: InvertedFlatListProps<T>, ref: ForwardedRef<FlatList>) {
Copy link
Member

Choose a reason for hiding this comment

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

No need to give inline type when have already defined that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry to ask @parasharrajat I didn't understand. Do you mean to not give inline type onScrollProp?

const lastScrollEvent = useRef<number | null>(null);
const scrollEndTimeout = useRef<NodeJS.Timeout | null>(null);

useEffect(
() => () => {
if (!scrollEndTimeout.current) {
return;
}

clearTimeout(scrollEndTimeout.current);
},
[ref],
);

/**
* Emits when the scrolling is in progress. Also,
* invokes the onScroll callback function from props.
*/
const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
onScrollProp(event);
};

/**
* Emits when the scrolling has ended. Also,
* invokes the onScrollEnd callback function from props.
*/
const onScrollEnd = () => {
onScrollEndProp();
};

/**
* Decides whether the scrolling has ended or not. If it has ended,
* then it calls the onScrollEnd function. Otherwise, it calls the
* onScroll function and pass the event to it.
*
* This is a temporary work around, since react-native-web doesn't
* support onScrollBeginDrag and onScrollEndDrag props for FlatList.
Copy link
Member

Choose a reason for hiding this comment

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

I think native has this method so we should use those instead for the native part.

* More info:
* https://github.com/necolas/react-native-web/pull/1305
*
* This workaround is taken from below and refactored to fit our needs:
* https://github.com/necolas/react-native-web/issues/1021#issuecomment-984151185
*
*/
const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
onScroll(event);

const timestamp = Date.now();

if (scrollEndTimeout.current) {
clearTimeout(scrollEndTimeout.current);
}

scrollEndTimeout.current = setTimeout(() => {
if (lastScrollEvent.current !== timestamp) {
return;
}
// Scroll has ended
lastScrollEvent.current = null;
onScrollEnd();
}, 250);

lastScrollEvent.current = timestamp;
};

return (
<FlatList
// eslint-disable-next-line react/jsx-props-no-spreading
Expand All @@ -18,6 +84,7 @@ function BaseInvertedFlatList<T>(props: FlatListProps<T>, ref: ForwardedRef<Flat
autoscrollToTopThreshold: AUTOSCROLL_TO_TOP_THRESHOLD,
}}
inverted
onScroll={handleScroll}
/>
);
}
Expand Down
5 changes: 3 additions & 2 deletions src/components/InvertedFlatList/index.native.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type {ForwardedRef} from 'react';
import React, {forwardRef} from 'react';
import type {FlatList, FlatListProps} from 'react-native';
import type {FlatList} from 'react-native';
import BaseInvertedFlatList from './BaseInvertedFlatList';
import CellRendererComponent from './CellRendererComponent';
import type {InvertedFlatListProps} from './types';

function BaseInvertedFlatListWithRef<T>(props: FlatListProps<T>, ref: ForwardedRef<FlatList>) {
function BaseInvertedFlatListWithRef<T>(props: InvertedFlatListProps<T>, ref: ForwardedRef<FlatList>) {
return (
<BaseInvertedFlatList
// eslint-disable-next-line react/jsx-props-no-spreading
Expand Down
63 changes: 10 additions & 53 deletions src/components/InvertedFlatList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,24 @@
import type {ForwardedRef} from 'react';
import React, {forwardRef, useEffect, useRef} from 'react';
import type {FlatList, FlatListProps, NativeScrollEvent, NativeSyntheticEvent} from 'react-native';
import React, {forwardRef, useRef} from 'react';
import type {FlatList, NativeScrollEvent, NativeSyntheticEvent} from 'react-native';
import {DeviceEventEmitter} from 'react-native';
import CONST from '@src/CONST';
import BaseInvertedFlatList from './BaseInvertedFlatList';
import type {InvertedFlatListProps} from './types';
import CellRendererComponent from './CellRendererComponent';

// This is adapted from https://codesandbox.io/s/react-native-dsyse
// It's a HACK alert since FlatList has inverted scrolling on web
function InvertedFlatList<T>({onScroll: onScrollProp = () => {}, ...props}: FlatListProps<T>, ref: ForwardedRef<FlatList>) {
const lastScrollEvent = useRef<number | null>(null);
const scrollEndTimeout = useRef<NodeJS.Timeout | null>(null);
function InvertedFlatList<T>({onScroll: onScrollProp = () => {}, onScrollEnd: onScrollEndProp = () => {}, ...props}: InvertedFlatListProps<T>, ref: ForwardedRef<FlatList>) {
const updateInProgress = useRef<boolean>(false);

useEffect(
() => () => {
if (!scrollEndTimeout.current) {
return;
}
clearTimeout(scrollEndTimeout.current);
},
[ref],
);

/**
* Emits when the scrolling is in progress. Also,
* invokes the onScroll callback function from props.
*
* @param event - The onScroll event from the FlatList
*/
const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
onScrollProp(event);

if (!updateInProgress.current) {
Expand All @@ -39,47 +28,14 @@ function InvertedFlatList<T>({onScroll: onScrollProp = () => {}, ...props}: Flat
};

/**
* Emits when the scrolling has ended.
* Emits when the scrolling has ended. Also,
* invokes the onScrollEnd callback function from props.
*/
const onScrollEnd = () => {
const handleScrollEnd = () => {
DeviceEventEmitter.emit(CONST.EVENTS.SCROLLING, false);
updateInProgress.current = false;
};

/**
* Decides whether the scrolling has ended or not. If it has ended,
* then it calls the onScrollEnd function. Otherwise, it calls the
* onScroll function and pass the event to it.
*
* This is a temporary work around, since react-native-web doesn't
* support onScrollBeginDrag and onScrollEndDrag props for FlatList.
* More info:
* https://github.com/necolas/react-native-web/pull/1305
*
* This workaround is taken from below and refactored to fit our needs:
* https://github.com/necolas/react-native-web/issues/1021#issuecomment-984151185
*
*/
const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
onScroll(event);
const timestamp = Date.now();

if (scrollEndTimeout.current) {
clearTimeout(scrollEndTimeout.current);
}

if (lastScrollEvent.current) {
scrollEndTimeout.current = setTimeout(() => {
if (lastScrollEvent.current !== timestamp) {
return;
}
// Scroll has ended
lastScrollEvent.current = null;
onScrollEnd();
}, 250);
}

lastScrollEvent.current = timestamp;
onScrollEndProp();
};

return (
Expand All @@ -88,6 +44,7 @@ function InvertedFlatList<T>({onScroll: onScrollProp = () => {}, ...props}: Flat
{...props}
ref={ref}
onScroll={handleScroll}
onScrollEnd={handleScrollEnd}
CellRendererComponent={CellRendererComponent}
/>
);
Expand Down
9 changes: 9 additions & 0 deletions src/components/InvertedFlatList/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {FlatListProps} from 'react-native';

Check failure on line 1 in src/components/InvertedFlatList/types.ts

View workflow job for this annotation

GitHub Actions / lint

All imports in the declaration are only used as types. Use `import type`

type InvertedFlatListProps<T> = FlatListProps<T> & {
/** Handler called when the scroll actions ends */
onScrollEnd: () => void;
};

export default InvertedFlatListProps;
export type {InvertedFlatListProps};
42 changes: 34 additions & 8 deletions src/pages/home/report/ReportActionsList.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ function ReportActionsList({
const hasFooterRendered = useRef(false);
const lastVisibleActionCreatedRef = useRef(report.lastVisibleActionCreated);
const lastReadTimeRef = useRef(report.lastReadTime);
const isUnreadMessageFocused = useRef(false);

const sortedVisibleReportActions = useMemo(
() => _.filter(sortedReportActions, (s) => isOffline || s.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || s.errors),
Expand Down Expand Up @@ -307,31 +308,55 @@ function ReportActionsList({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [report.reportID]);

/**
* Check if the new floating message counter needs to be shown/hidden.
* @returns {boolean}
*/
const showFloatingMessageCounter = () => {
const hasUnreadMessageAndFloatingMessageIsHidden = !isFloatingMessageCounterVisible && !!currentUnreadMarker;
const isFocusOutsideUnreadMessage = isFloatingMessageCounterVisible && !isUnreadMessageFocused.current;

if (isFloatingMessageCounterVisible && isUnreadMessageFocused.current) {
isUnreadMessageFocused.current = false;
}

return hasUnreadMessageAndFloatingMessageIsHidden || isFocusOutsideUnreadMessage;
};

/**
* Show/hide the new floating message counter when user is scrolling back/forth in the history of messages.
*/
const handleUnreadFloatingButton = () => {
if (scrollingVerticalOffset.current > VERTICAL_OFFSET_THRESHOLD && !isFloatingMessageCounterVisible && !!currentUnreadMarker) {
setIsFloatingMessageCounterVisible(true);
}
const showMessageCounter = showFloatingMessageCounter();

setIsFloatingMessageCounterVisible(showMessageCounter);

if (scrollingVerticalOffset.current < VERTICAL_OFFSET_THRESHOLD && isFloatingMessageCounterVisible) {
if (readActionSkipped.current) {
readActionSkipped.current = false;
Report.readNewestAction(report.reportID);
}
setIsFloatingMessageCounterVisible(false);
}
};

const trackVerticalScrolling = (event) => {
scrollingVerticalOffset.current = event.nativeEvent.contentOffset.y;
handleUnreadFloatingButton();
onScroll(event);
};

const scrollToBottomAndMarkReportAsRead = () => {
reportScrollManager.scrollToBottom();
const getScrollIndex = () => {
const bottomIndex = 0;
const firstUnreadMessageIndex = _.findIndex(sortedReportActions, (reportAction) => reportAction.reportActionID === currentUnreadMarker);

return firstUnreadMessageIndex > -1 ? firstUnreadMessageIndex : bottomIndex;
};

const scrollToUnreadMessageAndMarkReportAsRead = () => {
Copy link
Member

Choose a reason for hiding this comment

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

Let's make sure that the unread message is fully visible on the window.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Using the méthod reportScrollManager.scrollToIndex the behavior is to put on the bottom the unread message and the user can see all the message content.

I don't know if your suggestion is to scroll and show this unread message in a particular place on the screen, for example in the middle of the screen. If so, we need to improve the method reportScrollManager.scrollToIndex to receive the viewPosition (scrolltoindex) with a number that indicates the place we want to show this message.

Copy link
Member

Choose a reason for hiding this comment

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

I don't know if your suggestion is to scroll and show this unread message in a particular place on the screen, for example in the middle of the screen. If so, we need to improve the method reportScrollManager.scrollToIndex to receive the viewPosition (scrolltoindex) with a number that indicates the place we want to show this message.

This is a good suggestion when there are multiple unread messages. so I would say let's leave that part for now. No added complexity.

const scrollIndex = getScrollIndex();

isUnreadMessageFocused.current = true;
Copy link
Member

Choose a reason for hiding this comment

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

can you explain the purpose of this flag?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This flag is used to avoid showing the "New Message" button when the scroll ends after the user clicks the "New Message" button.

When the user clicks on the "New Message" button the scroll occurs and we don't need to show again the "New Message" button because this current scroll was triggered by the "New Message" button action.

Copy link
Member

Choose a reason for hiding this comment

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

Ok. I think you tried you mimic Slack behaviour but it is still different from slack. On Slack, when an unread message is visible on the screen marker remains hidden.


reportScrollManager.scrollToIndex(scrollIndex, false);
readActionSkipped.current = false;
Report.readNewestAction(report.reportID);
};
Expand Down Expand Up @@ -481,7 +506,7 @@ function ReportActionsList({
<>
<FloatingMessageCounter
isActive={isFloatingMessageCounterVisible && !!currentUnreadMarker}
onClick={scrollToBottomAndMarkReportAsRead}
onClick={scrollToUnreadMessageAndMarkReportAsRead}
/>
<Animated.View style={[animatedStyles, styles.flex1, !shouldShowReportRecipientLocalTime && !hideComposer ? styles.pb4 : {}]}>
<InvertedFlatList
Expand All @@ -503,6 +528,7 @@ function ReportActionsList({
keyboardShouldPersistTaps="handled"
onLayout={onLayoutInner}
onScroll={trackVerticalScrolling}
onScrollEnd={handleUnreadFloatingButton}
onScrollToIndexFailed={() => {}}
extraData={extraData}
/>
Expand Down
Loading