-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Expand file tree
/
Copy pathScrollOffsetContextProvider.tsx
More file actions
156 lines (127 loc) · 6.64 KB
/
Copy pathScrollOffsetContextProvider.tsx
File metadata and controls
156 lines (127 loc) · 6.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import {findFocusedRoute} from '@react-navigation/native';
import type {ParamListBase} from '@react-navigation/native';
import React, {createContext, useCallback, useEffect, useMemo, useRef} from 'react';
import useOnyx from '@hooks/useOnyx';
import usePrevious from '@hooks/usePrevious';
import {isSidebarScreenName} from '@libs/Navigation/helpers/isNavigatorName';
import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types';
import type {NavigationPartialRoute, State} from '@libs/Navigation/types';
import ONYXKEYS from '@src/ONYXKEYS';
import SCREENS from '@src/SCREENS';
type ScrollOffsetContextValue = {
/** Save scroll offset of FlashList on given screen */
saveScrollOffset: (route: PlatformStackRouteProp<ParamListBase>, scrollOffset: number) => void;
/** Get scroll offset value for given screen */
getScrollOffset: (route: PlatformStackRouteProp<ParamListBase>) => number | undefined;
/** Save scroll index of FlashList on given screen */
saveScrollIndex: (route: PlatformStackRouteProp<ParamListBase>, scrollIndex: number) => void;
/** Get scroll index value for given screen */
getScrollIndex: (route: PlatformStackRouteProp<ParamListBase>) => number | undefined;
/** Clean scroll offsets of screen that aren't anymore in the state */
cleanStaleScrollOffsets: (state: State) => void;
};
type ScrollOffsetContextProviderProps = {
/** Actual content wrapped by this component */
children: React.ReactNode;
};
const defaultValue: ScrollOffsetContextValue = {
saveScrollOffset: () => {},
getScrollOffset: () => undefined,
saveScrollIndex: () => {},
getScrollIndex: () => undefined,
cleanStaleScrollOffsets: () => {},
};
const ScrollOffsetContext = createContext<ScrollOffsetContextValue>(defaultValue);
/** This function is prepared to work with HOME and SEARCH screens. */
function getKey(route: PlatformStackRouteProp<ParamListBase> | NavigationPartialRoute): string {
// Handle routes with direct policyID parameter (HOME screens)
if (route.params && 'policyID' in route.params && typeof route.params.policyID === 'string') {
return `${route.name}-${route.params.policyID}`;
}
// Handle SEARCH screens with query parameters
if (route.name === SCREENS.SEARCH.ROOT && route.params && 'q' in route.params && typeof route.params.q === 'string') {
// Encode the query to handle spaces and special characters
const encodedQuery = encodeURIComponent(route.params.q);
return `${route.name}-${encodedQuery}`;
}
// For other routes, just use route name
return `${route.name}-global`;
}
function ScrollOffsetContextProvider({children}: ScrollOffsetContextProviderProps) {
const [priorityMode] = useOnyx(ONYXKEYS.NVP_PRIORITY_MODE);
const [inboxTab] = useOnyx(ONYXKEYS.NVP_INBOX_TAB);
const scrollOffsetsRef = useRef<Record<string, number>>({});
const previousPriorityMode = usePrevious(priorityMode);
const previousInboxTab = usePrevious(inboxTab);
useEffect(() => {
const priorityModeChanged = previousPriorityMode !== null && previousPriorityMode !== priorityMode;
const inboxTabChanged = previousInboxTab !== null && previousInboxTab !== inboxTab;
if (!priorityModeChanged && !inboxTabChanged) {
return;
}
// If the priority mode or inbox tab changes, we need to clear the scroll offsets for the home and search screens because it affects the size of the elements and scroll positions wouldn't be correct.
for (const key of Object.keys(scrollOffsetsRef.current)) {
if (key.includes(SCREENS.INBOX) || key.includes(SCREENS.SEARCH.ROOT)) {
delete scrollOffsetsRef.current[key];
}
}
}, [priorityMode, previousPriorityMode, inboxTab, previousInboxTab]);
const saveScrollOffset: ScrollOffsetContextValue['saveScrollOffset'] = useCallback((route, scrollOffset) => {
scrollOffsetsRef.current[getKey(route)] = scrollOffset;
}, []);
const getScrollOffset: ScrollOffsetContextValue['getScrollOffset'] = useCallback((route) => {
if (!scrollOffsetsRef.current) {
return;
}
return scrollOffsetsRef.current[getKey(route)];
}, []);
const cleanScrollOffsets = useCallback((keys: string[], shouldDelete: (key: string) => boolean) => {
for (const key of keys) {
if (!shouldDelete(key)) {
continue;
}
delete scrollOffsetsRef.current[key];
}
}, []);
const cleanStaleScrollOffsets: ScrollOffsetContextValue['cleanStaleScrollOffsets'] = useCallback(
(state) => {
const sidebarRoutes = state.routes.filter((route) => isSidebarScreenName(route.name));
const existingScreenKeys = new Set(sidebarRoutes.map(getKey));
const focusedRoute = findFocusedRoute(state);
const routeName = focusedRoute?.name;
const isSearchScreen = routeName === SCREENS.SEARCH.ROOT;
const isSearchMoneyRequestReport =
routeName === SCREENS.RIGHT_MODAL.EXPENSE_REPORT || routeName === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT || routeName === SCREENS.RIGHT_MODAL.SEARCH_REPORT;
const scrollOffsetKeys = Object.keys(scrollOffsetsRef.current);
if (isSearchScreen || isSearchMoneyRequestReport) {
const currentKey = focusedRoute ? getKey(focusedRoute) : null;
cleanScrollOffsets(scrollOffsetKeys, (key) => key.startsWith(SCREENS.SEARCH.ROOT) && key !== currentKey && !isSearchMoneyRequestReport);
return;
}
cleanScrollOffsets(scrollOffsetKeys, (key) => !existingScreenKeys.has(key));
},
[cleanScrollOffsets],
);
const saveScrollIndex: ScrollOffsetContextValue['saveScrollIndex'] = useCallback((route, scrollIndex) => {
scrollOffsetsRef.current[getKey(route)] = scrollIndex;
}, []);
const getScrollIndex: ScrollOffsetContextValue['getScrollIndex'] = useCallback((route) => {
if (!scrollOffsetsRef.current) {
return;
}
return scrollOffsetsRef.current[getKey(route)];
}, []);
const contextValue = useMemo(
(): ScrollOffsetContextValue => ({
saveScrollOffset,
getScrollOffset,
cleanStaleScrollOffsets,
saveScrollIndex,
getScrollIndex,
}),
[saveScrollOffset, getScrollOffset, cleanStaleScrollOffsets, saveScrollIndex, getScrollIndex],
);
return <ScrollOffsetContext.Provider value={contextValue}>{children}</ScrollOffsetContext.Provider>;
}
export default ScrollOffsetContextProvider;
export {ScrollOffsetContext};