-
Notifications
You must be signed in to change notification settings - Fork 3.1k
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
base: main
Are you sure you want to change the base?
Changes from all commits
5aae219
4afbe65
0322399
b273fd1
2a2579c
0a6538f
587f3e1
c44c6c2
d2fbd67
5ddfe37
bcb7654
8d61c48
c281003
2278efe
f80434c
83081d1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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>) { | ||
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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -18,6 +84,7 @@ function BaseInvertedFlatList<T>(props: FlatListProps<T>, ref: ForwardedRef<Flat | |
autoscrollToTopThreshold: AUTOSCROLL_TO_TOP_THRESHOLD, | ||
}} | ||
inverted | ||
onScroll={handleScroll} | ||
/> | ||
); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import {FlatListProps} from 'react-native'; | ||
|
||
type InvertedFlatListProps<T> = FlatListProps<T> & { | ||
/** Handler called when the scroll actions ends */ | ||
onScrollEnd: () => void; | ||
}; | ||
|
||
export default InvertedFlatListProps; | ||
export type {InvertedFlatListProps}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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), | ||
|
@@ -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 = () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using the méthod 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you explain the purpose of this flag? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
}; | ||
|
@@ -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 | ||
|
@@ -503,6 +528,7 @@ function ReportActionsList({ | |
keyboardShouldPersistTaps="handled" | ||
onLayout={onLayoutInner} | ||
onScroll={trackVerticalScrolling} | ||
onScrollEnd={handleUnreadFloatingButton} | ||
onScrollToIndexFailed={() => {}} | ||
extraData={extraData} | ||
/> | ||
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
?