Skip to content

Commit

Permalink
Refactor unread marker effect
Browse files Browse the repository at this point in the history
  • Loading branch information
zukilover committed Sep 29, 2023
1 parent 249cd27 commit db64659
Showing 1 changed file with 76 additions and 64 deletions.
140 changes: 76 additions & 64 deletions src/pages/home/report/ReportActionsList.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ const propTypes = {
/** The ID of the most recent IOU report action connected with the shown report */
mostRecentIOUReportActionID: PropTypes.string,

/** The report metadata loading states */
isLoadingReportActions: PropTypes.bool,

/** Are we loading more report actions? */
isLoadingMoreReportActions: PropTypes.bool,

Expand Down Expand Up @@ -61,6 +64,7 @@ const defaultProps = {
personalDetails: {},
onScroll: () => {},
mostRecentIOUReportActionID: '',
isLoadingReportActions: false,
isLoadingMoreReportActions: false,
...withCurrentUserPersonalDetailsDefaultProps,
};
Expand Down Expand Up @@ -96,6 +100,8 @@ function isMessageUnread(message, lastReadTime) {

function ReportActionsList({
report,
isLoadingReportActions,
isLoadingMoreReportActions,
sortedReportActions,
windowHeight,
onScroll,
Expand All @@ -117,10 +123,11 @@ function ReportActionsList({
const scrollingVerticalOffset = useRef(0);
const readActionSkipped = useRef(false);
const reportActionSize = useRef(sortedReportActions.length);
const firstRenderRef = useRef(true);

// Considering that renderItem is enclosed within a useCallback, marking it as "read" twice will retain the value as "true," preventing the useCallback from re-executing.
// However, if we create and listen to an object, it will lead to a new useCallback execution.
const [messageManuallyMarked, setMessageManuallyMarked] = useState({read: false});
// This state is used to force a re-render when the user manually marks a message as unread
// by using a timestamp you can force re-renders without having to worry about if another message was marked as unread before
const [messageManuallyMarkedUnread, setMessageManuallyMarkedUnread] = useState(0);
const [isFloatingMessageCounterVisible, setIsFloatingMessageCounterVisible] = useState(false);
const animatedStyles = useAnimatedStyle(() => ({
opacity: opacity.value,
Expand All @@ -129,7 +136,6 @@ function ReportActionsList({
useEffect(() => {
opacity.value = withTiming(1, {duration: 100});
}, [opacity]);
const [skeletonViewHeight, setSkeletonViewHeight] = useState(0);

useEffect(() => {
// If the reportID changes, we reset the userActiveSince to null, we need to do it because
Expand Down Expand Up @@ -167,14 +173,14 @@ function ReportActionsList({

useEffect(() => {
const didManuallyMarkReportAsUnread = report.lastReadTime < DateUtils.getDBTime() && ReportUtils.isUnread(report);
if (!didManuallyMarkReportAsUnread) {
setMessageManuallyMarked({read: false});
if (didManuallyMarkReportAsUnread) {
// Clearing the current unread marker so that it can be recalculated
setCurrentUnreadMarker(null);
setMessageManuallyMarkedUnread(new Date().getTime());
return;
}

// Clearing the current unread marker so that it can be recalculated
setCurrentUnreadMarker(null);
setMessageManuallyMarked({read: true});
setMessageManuallyMarkedUnread(0);

// We only care when a new lastReadTime is set in the report
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand Down Expand Up @@ -268,46 +274,44 @@ function ReportActionsList({
);

/**
* Evaluate new unread marker visibility for each of the report actions.
* @returns boolean
* @param {Object} args
* @param {Number} args.index
* @returns {React.Component}
*/

const shouldDisplayNewMarker = useCallback(
(reportAction, index) => {
let shouldDisplay = false;
const renderItem = useCallback(
({item: reportAction, index}) => {
let shouldDisplayNewMarker = false;

if (!currentUnreadMarker) {
const nextMessage = sortedReportActions[index + 1];
const isCurrentMessageUnread = isMessageUnread(reportAction, report.lastReadTime);
shouldDisplay = isCurrentMessageUnread && !isMessageUnread(nextMessage, report.lastReadTime);
if (!messageManuallyMarked.read) {
shouldDisplay = shouldDisplay && reportAction.actorAccountID !== Report.getCurrentUserAccountID();
shouldDisplayNewMarker = isCurrentMessageUnread && !isMessageUnread(nextMessage, report.lastReadTime);

if (!messageManuallyMarkedUnread) {
shouldDisplayNewMarker = shouldDisplayNewMarker && reportAction.actorAccountID !== Report.getCurrentUserAccountID();
}
const canDisplayMarker = scrollingVerticalOffset.current < MSG_VISIBLE_THRESHOLD ? reportAction.created < userActiveSince.current : true;

if (!currentUnreadMarker && shouldDisplayNewMarker && canDisplayMarker) {
setCurrentUnreadMarker(reportAction.reportActionID);
}
} else {
shouldDisplay = reportAction.reportActionID === currentUnreadMarker;
}
if (shouldDisplay) {
setCurrentUnreadMarker(reportAction.reportActionID);
shouldDisplayNewMarker = reportAction.reportActionID === currentUnreadMarker;
}
return shouldDisplay;
return (
<ReportActionsListItemRenderer
reportAction={reportAction}
index={index}
report={report}
hasOutstandingIOU={hasOutstandingIOU}
sortedReportActions={sortedReportActions}
mostRecentIOUReportActionID={mostRecentIOUReportActionID}
shouldHideThreadDividerLine={shouldHideThreadDividerLine}
shouldDisplayNewMarker={shouldDisplayNewMarker}
/>
);
},
[currentUnreadMarker, sortedReportActions, report.lastReadTime, messageManuallyMarked.read],
);

const renderItem = useCallback(
({item: reportAction, index}) => (
<ReportActionsListItemRenderer
reportAction={reportAction}
index={index}
report={report}
hasOutstandingIOU={hasOutstandingIOU}
sortedReportActions={sortedReportActions}
mostRecentIOUReportActionID={mostRecentIOUReportActionID}
shouldHideThreadDividerLine={shouldHideThreadDividerLine}
shouldDisplayNewMarker={shouldDisplayNewMarker(reportAction, index)}
/>
),
[report, hasOutstandingIOU, sortedReportActions, mostRecentIOUReportActionID, shouldHideThreadDividerLine, shouldDisplayNewMarker],
[report, hasOutstandingIOU, sortedReportActions, mostRecentIOUReportActionID, messageManuallyMarkedUnread, shouldHideThreadDividerLine, currentUnreadMarker],
);

// Native mobile does not render updates flatlist the changes even though component did update called.
Expand All @@ -316,6 +320,36 @@ function ReportActionsList({
const hideComposer = ReportUtils.shouldDisableWriteActions(report);
const shouldShowReportRecipientLocalTime = ReportUtils.canShowReportRecipientLocalTime(personalDetailsList, report, currentUserPersonalDetails.accountID) && !isComposerFullSize;

const renderFooter = useCallback(() => {
// Skip this hook on the first render, as we are not sure if more actions are going to be loaded
// Therefore showing the skeleton on footer might be misleading
if (firstRenderRef.current) {
firstRenderRef.current = false;
return null;
}

if (isLoadingMoreReportActions) {
return <ReportActionsSkeletonView />;
}

// Make sure the oldest report action loaded is not the first. This is so we do not show the
// skeleton view above the created action in a newly generated optimistic chat or one with not
// that many comments.
const lastReportAction = _.last(sortedReportActions) || {};
if (isLoadingReportActions && lastReportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED) {
return <ReportActionsSkeletonView animate={!isOffline} />;
}

return null;
}, [isLoadingMoreReportActions, isLoadingReportActions, sortedReportActions, isOffline]);

const onLayoutInner = useCallback(
(event) => {
onLayout(event);
},
[onLayout],
);

return (
<>
<FloatingMessageCounter
Expand All @@ -335,31 +369,9 @@ function ReportActionsList({
initialNumToRender={initialNumToRender}
onEndReached={loadMoreChats}
onEndReachedThreshold={0.75}
ListFooterComponent={() => {
if (report.isLoadingMoreReportActions) {
return <ReportActionsSkeletonView containerHeight={CONST.CHAT_SKELETON_VIEW.AVERAGE_ROW_HEIGHT * 3} />;
}

// Make sure the oldest report action loaded is not the first. This is so we do not show the
// skeleton view above the created action in a newly generated optimistic chat or one with not
// that many comments.
const lastReportAction = _.last(sortedReportActions) || {};
if (report.isLoadingReportActions && lastReportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED) {
return (
<ReportActionsSkeletonView
containerHeight={skeletonViewHeight}
animate={!isOffline}
/>
);
}

return null;
}}
ListFooterComponent={renderFooter}
keyboardShouldPersistTaps="handled"
onLayout={(event) => {
setSkeletonViewHeight(event.nativeEvent.layout.height);
onLayout(event);
}}
onLayout={onLayoutInner}
onScroll={trackVerticalScrolling}
extraData={extraData}
/>
Expand Down

0 comments on commit db64659

Please sign in to comment.