-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Updated logic to handle unread messages case #25935
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
Changes from all commits
fab7506
47dd5cd
922fbf3
85dddae
b9f1f17
ec2e0fb
3646848
c71ecae
ebdeddb
de5b8fe
c483c10
8925515
7149558
89d15d2
748c8ad
a434c3a
f40773b
c784057
51f8117
eee1e83
f1de924
c8d4853
c9722d9
8782bf7
d3ddefd
cea0759
3ff86e2
3630dac
57978b6
6546ba5
6b2beed
dc93b9e
95cc451
cdb1b80
5dde6f2
2b83ba6
c4541fb
58e2f9c
12a92a7
823f3b7
d1a347c
0a578dd
8bb02a4
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 |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ import {useRoute} from '@react-navigation/native'; | |
| import lodashGet from 'lodash/get'; | ||
| import PropTypes from 'prop-types'; | ||
| import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; | ||
| import {DeviceEventEmitter} from 'react-native'; | ||
| import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; | ||
| import _ from 'underscore'; | ||
| import InvertedFlatList from '@components/InvertedFlatList'; | ||
|
|
@@ -82,16 +83,23 @@ const defaultProps = { | |
| const VERTICAL_OFFSET_THRESHOLD = 200; | ||
| const MSG_VISIBLE_THRESHOLD = 250; | ||
|
|
||
| // Seems that there is an architecture issue that prevents us from using the reportID with useRef | ||
| // the useRef value gets reset when the reportID changes, so we use a global variable to keep track | ||
| let prevReportID = null; | ||
|
|
||
| // In the component we are subscribing to the arrival of new actions. | ||
| // As there is the possibility that there are multiple instances of a ReportScreen | ||
| // for the same report, we only ever want one subscription to be active, as | ||
| // the subscriptions could otherwise be conflicting. | ||
| const newActionUnsubscribeMap = {}; | ||
|
|
||
| // Caching the reportID and reportActionID for unread markers ensures persistent tracking | ||
| // across multiple reports, preserving the green line placement and allowing retrieval | ||
| // of the relevant reportActionID for displaying the green line. | ||
| // We need to persist it across reports because there are at least 3 ReportScreen components created so the | ||
| // internal states are resetted or recreated. | ||
| const cacheUnreadMarkers = new Map(); | ||
|
Contributor
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. @gedu @MonilBhavsar Hey I'm working on a bug related to unread markers and I'm having trouble wrapping my head around this cache. What problem/scenario does it fix specifically? Like is there a scenario where when we switch away from a report and then navigate back, we want the unread marker to persist? |
||
|
|
||
| // Seems that there is an architecture issue that prevents us from using the reportID with useRef | ||
| // the useRef value gets reset when the reportID changes, so we use a global variable to keep track | ||
| let prevReportID = null; | ||
|
|
||
| /** | ||
| * Create a unique key for each action in the FlatList. | ||
| * We use the reportActionID that is a string representation of a random 64-bit int, which should be | ||
|
|
@@ -137,12 +145,21 @@ function ReportActionsList({ | |
| const route = useRoute(); | ||
| const opacity = useSharedValue(0); | ||
| const userActiveSince = useRef(null); | ||
| const [currentUnreadMarker, setCurrentUnreadMarker] = useState(null); | ||
| const unreadActionSubscription = useRef(null); | ||
| const markerInit = () => { | ||
| if (!cacheUnreadMarkers.has(report.reportID)) { | ||
| return null; | ||
| } | ||
| return cacheUnreadMarkers.get(report.reportID); | ||
| }; | ||
| const [currentUnreadMarker, setCurrentUnreadMarker] = useState(markerInit); | ||
| const scrollingVerticalOffset = useRef(0); | ||
| const readActionSkipped = useRef(false); | ||
| const hasHeaderRendered = useRef(false); | ||
| const hasFooterRendered = useRef(false); | ||
| const reportActionSize = useRef(sortedReportActions.length); | ||
| const lastReadTimeRef = useRef(report.lastReadTime); | ||
|
|
||
| const linkedReportActionID = lodashGet(route, 'params.reportActionID', ''); | ||
|
|
||
| // This state is used to force a re-render when the user manually marks a message as unread | ||
|
|
@@ -186,25 +203,41 @@ function ReportActionsList({ | |
| return; | ||
| } | ||
|
|
||
| cacheUnreadMarkers.delete(report.reportID); | ||
| reportActionSize.current = sortedReportActions.length; | ||
| setCurrentUnreadMarker(null); | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [sortedReportActions.length, report.reportID]); | ||
|
|
||
| useEffect(() => { | ||
| const didManuallyMarkReportAsUnread = report.lastReadTime < DateUtils.getDBTime() && ReportUtils.isUnread(report); | ||
| if (didManuallyMarkReportAsUnread) { | ||
| // Clearing the current unread marker so that it can be recalculated | ||
| setCurrentUnreadMarker(null); | ||
| setMessageManuallyMarkedUnread(new Date().getTime()); | ||
| if (!userActiveSince.current || report.reportID !== prevReportID) { | ||
| return; | ||
| } | ||
|
|
||
| if (!messageManuallyMarkedUnread && lastReadTimeRef.current && lastReadTimeRef.current < report.lastReadTime) { | ||
| cacheUnreadMarkers.delete(report.reportID); | ||
| } | ||
| lastReadTimeRef.current = report.lastReadTime; | ||
| setMessageManuallyMarkedUnread(0); | ||
|
|
||
| // We only care when a new lastReadTime is set in the report | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [report.lastReadTime]); | ||
| }, [report.lastReadTime, report.reportID]); | ||
|
|
||
| useEffect(() => { | ||
| // If the reportID changes, we reset the userActiveSince to null, we need to do it because | ||
| // this component doesn't unmount when the reportID changes | ||
| if (unreadActionSubscription.current) { | ||
| unreadActionSubscription.current.remove(); | ||
| unreadActionSubscription.current = null; | ||
| } | ||
|
|
||
| // Listen to specific reportID for unread event and set the marker to new message | ||
| unreadActionSubscription.current = DeviceEventEmitter.addListener(`unreadAction_${report.reportID}`, (newLastReadTime) => { | ||
| cacheUnreadMarkers.delete(report.reportID); | ||
| lastReadTimeRef.current = newLastReadTime; | ||
| setCurrentUnreadMarker(null); | ||
| setMessageManuallyMarkedUnread(new Date().getTime()); | ||
| }); | ||
| }, [report.reportID]); | ||
|
|
||
| useEffect(() => { | ||
| // Why are we doing this, when in the cleanup of the useEffect we are already calling the unsubscribe function? | ||
|
|
@@ -303,17 +336,21 @@ function ReportActionsList({ | |
| let shouldDisplay = false; | ||
| if (!currentUnreadMarker) { | ||
| const nextMessage = sortedReportActions[index + 1]; | ||
| const isCurrentMessageUnread = isMessageUnread(reportAction, report.lastReadTime); | ||
| shouldDisplay = isCurrentMessageUnread && (!nextMessage || !isMessageUnread(nextMessage, report.lastReadTime)); | ||
| const isCurrentMessageUnread = isMessageUnread(reportAction, lastReadTimeRef.current); | ||
| shouldDisplay = isCurrentMessageUnread && (!nextMessage || !isMessageUnread(nextMessage, lastReadTimeRef.current)); | ||
| if (!messageManuallyMarkedUnread) { | ||
| shouldDisplay = shouldDisplay && reportAction.actorAccountID !== Report.getCurrentUserAccountID(); | ||
| } | ||
| if (shouldDisplay) { | ||
| cacheUnreadMarkers.set(report.reportID, reportAction.reportActionID); | ||
| } | ||
| } else { | ||
| shouldDisplay = reportAction.reportActionID === currentUnreadMarker; | ||
| } | ||
|
|
||
|
Contributor
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. NAB, we can remove this line |
||
| return shouldDisplay; | ||
| }, | ||
| [currentUnreadMarker, sortedReportActions, report.lastReadTime, messageManuallyMarkedUnread], | ||
| [currentUnreadMarker, sortedReportActions, report.reportID, messageManuallyMarkedUnread], | ||
| ); | ||
|
|
||
| useEffect(() => { | ||
|
|
@@ -327,13 +364,14 @@ function ReportActionsList({ | |
| } | ||
| markerFound = true; | ||
| if (!currentUnreadMarker && currentUnreadMarker !== reportAction.reportActionID) { | ||
| cacheUnreadMarkers.set(report.reportID, reportAction.reportActionID); | ||
| setCurrentUnreadMarker(reportAction.reportActionID); | ||
| } | ||
| }); | ||
| if (!markerFound) { | ||
| setCurrentUnreadMarker(null); | ||
| } | ||
| }, [sortedReportActions, report.lastReadTime, messageManuallyMarkedUnread, shouldDisplayNewMarker, currentUnreadMarker]); | ||
| }, [sortedReportActions, report.lastReadTime, report.reportID, messageManuallyMarkedUnread, shouldDisplayNewMarker, currentUnreadMarker]); | ||
|
|
||
| const renderItem = useCallback( | ||
| ({item: reportAction, index}) => ( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,7 @@ import {addSeconds, format, subMinutes, subSeconds} from 'date-fns'; | |
| import {utcToZonedTime} from 'date-fns-tz'; | ||
| import lodashGet from 'lodash/get'; | ||
| import React from 'react'; | ||
| import {AppState, Linking} from 'react-native'; | ||
| import {AppState, DeviceEventEmitter, Linking} from 'react-native'; | ||
| import Onyx from 'react-native-onyx'; | ||
| import App from '../../src/App'; | ||
| import CONFIG from '../../src/CONFIG'; | ||
|
|
@@ -78,7 +78,7 @@ function scrollUpToRevealNewMessagesBadge() { | |
| function isNewMessagesBadgeVisible() { | ||
| const hintText = Localize.translateLocal('accessibilityHints.scrollToNewestMessages'); | ||
| const badge = screen.queryByAccessibilityHint(hintText); | ||
| return Math.round(badge.props.style.transform[0].translateY) === 10; | ||
| return Math.round(badge.props.style.transform[0].translateY) === -40; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -249,8 +249,12 @@ describe('Unread Indicators', () => { | |
| signInAndGetAppWithUnreadChat() | ||
| // Navigate to the unread chat from the sidebar | ||
| .then(() => navigateToSidebarOption(0)) | ||
| // Navigate to the unread chat from the sidebar | ||
| .then(() => navigateToSidebarOption(0)) | ||
| .then(() => { | ||
| // Verify the unread indicator is present | ||
| const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); | ||
| const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); | ||
| expect(unreadIndicator).toHaveLength(1); | ||
| }) | ||
| .then(() => { | ||
| expect(areYouOnChatListScreen()).toBe(false); | ||
| // Then navigate back to the sidebar | ||
|
|
@@ -259,9 +263,15 @@ describe('Unread Indicators', () => { | |
| .then(() => { | ||
| // Verify the LHN is now open | ||
| expect(areYouOnChatListScreen()).toBe(true); | ||
|
|
||
| // Tap on the chat again | ||
| return navigateToSidebarOption(0); | ||
| }) | ||
| .then(() => { | ||
| // Sending event to clear the unread indicator cache, given that the test doesn't behave as the app | ||
|
Contributor
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.
Contributor
Author
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. When there is a single panel, in the tests, seems that it keeps all components in memory, so I need to reset the state to make it work, "simulating" the same as what the app does |
||
| DeviceEventEmitter.emit(`unreadAction_${REPORT_ID}`, format(new Date(), CONST.DATE.FNS_DB_FORMAT_STRING)); | ||
| return waitForBatchedUpdatesWithAct(); | ||
| }) | ||
| .then(() => { | ||
| // Verify the unread indicator is not present | ||
| const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.