Skip to content

Commit 462368d

Browse files
authored
Merge pull request #92146 from Expensify/youssef_inbox_filters
[Payment due @parasharrajat] Inbox filters
2 parents 3a8833f + 4e3c0f9 commit 462368d

26 files changed

Lines changed: 449 additions & 88 deletions

src/CONST/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2334,6 +2334,11 @@ const CONST = {
23342334
GSD: 'gsd',
23352335
DEFAULT: 'default',
23362336
},
2337+
INBOX_TAB: {
2338+
ALL: 'all',
2339+
TODO: 'todo',
2340+
UNREAD: 'unread',
2341+
},
23372342
THEME: {
23382343
DEFAULT: 'system',
23392344
FALLBACK: 'dark',

src/ONYXKEYS.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,9 @@ const ONYXKEYS = {
172172
/** Contains the user preference for the LHN priority mode */
173173
NVP_PRIORITY_MODE: 'nvp_priorityMode',
174174

175+
/** Contains the user preference for the active inbox tab filter */
176+
NVP_INBOX_TAB: 'nvp_inboxTab',
177+
175178
/** Contains the users's block expiration (if they have one) */
176179
NVP_BLOCKED_FROM_CONCIERGE: 'nvp_private_blockedFromConcierge',
177180

@@ -1477,6 +1480,7 @@ type OnyxValuesMapping = {
14771480
[ONYXKEYS.BETA_CONFIGURATION]: OnyxTypes.BetaConfiguration;
14781481
[ONYXKEYS.NVP_MUTED_PLATFORMS]: Partial<Record<Platform, true>>;
14791482
[ONYXKEYS.NVP_PRIORITY_MODE]: ValueOf<typeof CONST.PRIORITY_MODE>;
1483+
[ONYXKEYS.NVP_INBOX_TAB]: ValueOf<typeof CONST.INBOX_TAB>;
14801484
[ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE]: OnyxTypes.BlockedFromConcierge;
14811485
[ONYXKEYS.QUEUE_FLUSHED_DATA]: AnyOnyxUpdate[];
14821486
[ONYXKEYS.TRANSACTIONS_PENDING_3DS_REVIEW]: OnyxTypes.TransactionsPending3DSReview;

src/components/LHNOptionsList/LHNEmptyState.tsx

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,51 @@ import {View} from 'react-native';
33
import type {BlockingViewProps} from '@components/BlockingViews/BlockingView';
44
import BlockingView from '@components/BlockingViews/BlockingView';
55
import Icon from '@components/Icon';
6+
import Text from '@components/Text';
67
import TextBlock from '@components/TextBlock';
8+
import TextLink from '@components/TextLink';
79
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
810
import useLocalize from '@hooks/useLocalize';
11+
import {useSidebarOrderedReportsActions, useSidebarOrderedReportsState} from '@hooks/useSidebarOrderedReports';
912
import useTheme from '@hooks/useTheme';
1013
import useThemeStyles from '@hooks/useThemeStyles';
1114
import variables from '@styles/variables';
15+
import CONST from '@src/CONST';
1216
import useEmptyLHNIllustration from './useEmptyLHNIllustration';
1317

1418
function LHNEmptyState() {
1519
const theme = useTheme();
1620
const styles = useThemeStyles();
1721
const {translate} = useLocalize();
1822
const expensifyIcons = useMemoizedLazyExpensifyIcons(['MagnifyingGlass', 'Plus']);
19-
const emptyLHNIllustration = useEmptyLHNIllustration();
23+
const emptyLHNIllustration = useEmptyLHNIllustration() as BlockingViewProps;
24+
const {activeTab} = useSidebarOrderedReportsState();
25+
const {setActiveTab} = useSidebarOrderedReportsActions();
26+
27+
if (activeTab === CONST.INBOX_TAB.UNREAD || activeTab === CONST.INBOX_TAB.TODO) {
28+
const title = activeTab === CONST.INBOX_TAB.UNREAD ? translate('common.emptyLHN.noUnreadChats') : translate('common.emptyLHN.noTodos');
29+
const caughtUpSubtitle = (
30+
<View style={[styles.alignItemsCenter, styles.justifyContentCenter]}>
31+
<Text style={[styles.textAlignCenter, styles.textSupporting]}>{translate('common.emptyLHN.caughtUp')}</Text>
32+
<TextLink
33+
onPress={() => setActiveTab(CONST.INBOX_TAB.ALL)}
34+
style={[styles.textStrong, styles.mt5, styles.ph4, styles.textAlignCenter]}
35+
>
36+
{translate('common.emptyLHN.seeAllChats')}
37+
</TextLink>
38+
</View>
39+
);
40+
41+
return (
42+
<BlockingView
43+
{...emptyLHNIllustration}
44+
title={title}
45+
titleStyles={styles.mb2}
46+
CustomSubtitle={caughtUpSubtitle}
47+
accessibilityLabel={title}
48+
/>
49+
);
50+
}
2051

2152
const subtitle = (
2253
<View style={[styles.alignItemsCenter, styles.flexRow, styles.justifyContentCenter, styles.flexWrap, styles.textAlignCenter]}>
@@ -56,7 +87,7 @@ function LHNEmptyState() {
5687

5788
return (
5889
<BlockingView
59-
{...(emptyLHNIllustration as BlockingViewProps)}
90+
{...emptyLHNIllustration}
6091
title={translate('common.emptyLHN.title')}
6192
CustomSubtitle={subtitle}
6293
accessibilityLabel={translate('common.emptyLHN.title')}

src/components/ScrollOffsetContextProvider.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,21 +61,25 @@ function getKey(route: PlatformStackRouteProp<ParamListBase> | NavigationPartial
6161

6262
function ScrollOffsetContextProvider({children}: ScrollOffsetContextProviderProps) {
6363
const [priorityMode] = useOnyx(ONYXKEYS.NVP_PRIORITY_MODE);
64+
const [inboxTab] = useOnyx(ONYXKEYS.NVP_INBOX_TAB);
6465
const scrollOffsetsRef = useRef<Record<string, number>>({});
6566
const previousPriorityMode = usePrevious(priorityMode);
67+
const previousInboxTab = usePrevious(inboxTab);
6668

6769
useEffect(() => {
68-
if (previousPriorityMode === null || previousPriorityMode === priorityMode) {
70+
const priorityModeChanged = previousPriorityMode !== null && previousPriorityMode !== priorityMode;
71+
const inboxTabChanged = previousInboxTab !== null && previousInboxTab !== inboxTab;
72+
if (!priorityModeChanged && !inboxTabChanged) {
6973
return;
7074
}
7175

72-
// If the priority mode 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.
76+
// 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.
7377
for (const key of Object.keys(scrollOffsetsRef.current)) {
7478
if (key.includes(SCREENS.INBOX) || key.includes(SCREENS.SEARCH.ROOT)) {
7579
delete scrollOffsetsRef.current[key];
7680
}
7781
}
78-
}, [priorityMode, previousPriorityMode]);
82+
}, [priorityMode, previousPriorityMode, inboxTab, previousInboxTab]);
7983

8084
const saveScrollOffset: ScrollOffsetContextValue['saveScrollOffset'] = useCallback((route, scrollOffset) => {
8185
scrollOffsetsRef.current[getKey(route)] = scrollOffset;

src/components/TabSelector/TabSelectorBase.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ function TabSelectorBase({
2626
position,
2727
shouldShowLabelWhenInactive = true,
2828
equalWidth = false,
29+
contentContainerStyles,
2930
}: TabSelectorBaseProps) {
3031
const theme = useTheme();
3132
const styles = useThemeStyles();
@@ -61,7 +62,11 @@ function TabSelectorBase({
6162
}}
6263
ref={containerRef}
6364
style={styles.scrollableTabSelector}
64-
contentContainerStyle={styles.tabSelectorContentContainer}
65+
// On iOS a horizontal ScrollView lays out its content along an unbounded main axis, so flex-1 tabs
66+
// (equalWidth) divide their intrinsic content width instead of the viewport. Giving the content
67+
// container a definite width lets the flex children split it evenly. Scoped to equalWidth so normal
68+
// overflowing/scrollable tab rows are not constrained.
69+
contentContainerStyle={[styles.tabSelectorContentContainer, equalWidth && styles.w100, contentContainerStyles]}
6570
horizontal
6671
showsHorizontalScrollIndicator={false}
6772
keyboardShouldPersistTaps="handled"
@@ -119,6 +124,8 @@ function TabSelectorBase({
119124
shouldShowLabelWhenInactive={shouldShowLabelWhenInactive}
120125
equalWidth={equalWidth}
121126
badgeText={tab.badgeText}
127+
isBadgeCondensed={tab.isBadgeCondensed}
128+
badgeStyles={tab.badgeStyles}
122129
pendingAction={tab.pendingAction}
123130
isDisabled={tab.isDisabled}
124131
/>

src/components/TabSelector/TabSelectorItem.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ function TabSelectorItem({
3131
sentryLabel,
3232
equalWidth = false,
3333
badgeText,
34+
isBadgeCondensed = false,
35+
badgeStyles,
3436
isDisabled = false,
3537
pendingAction,
3638
}: TabSelectorItemProps) {
@@ -90,6 +92,8 @@ function TabSelectorItem({
9092
<Badge
9193
text={badgeText}
9294
success
95+
isCondensed={isBadgeCondensed}
96+
badgeStyles={badgeStyles}
9397
/>
9498
)}
9599
</AnimatedPressableWithFeedback>

src/components/TabSelector/types.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type {MaterialTopTabBarProps} from '@react-navigation/material-top-tabs';
22
// eslint-disable-next-line no-restricted-imports
3-
import type {Animated} from 'react-native';
3+
import type {Animated, StyleProp, ViewStyle} from 'react-native';
44
import type {ThemeColors} from '@styles/theme/types';
55
import type {PendingAction} from '@src/types/onyx/OnyxCommon';
66
import type IconAsset from '@src/types/utils/IconAsset';
@@ -39,6 +39,12 @@ type TabSelectorBaseItem = WithSentryLabel & {
3939
/** Text to display on the badge on the tab. */
4040
badgeText?: string;
4141

42+
/** Whether the tab's badge should use the condensed (smaller) style. */
43+
isBadgeCondensed?: boolean;
44+
45+
/** Additional styles for the tab's badge. */
46+
badgeStyles?: StyleProp<ViewStyle>;
47+
4248
/** Whether this tab is disabled */
4349
isDisabled?: boolean;
4450

@@ -70,6 +76,9 @@ type TabSelectorBaseProps = {
7076

7177
/** Whether tabs should have equal width. */
7278
equalWidth?: boolean;
79+
80+
/** Additional styles for the tabs' scroll content container. */
81+
contentContainerStyles?: StyleProp<ViewStyle>;
7382
};
7483

7584
type TabSelectorItemProps = WithSentryLabel & {
@@ -112,6 +121,12 @@ type TabSelectorItemProps = WithSentryLabel & {
112121
/** Text to display on the badge on the tab. */
113122
badgeText?: string;
114123

124+
/** Whether the tab's badge should use the condensed (smaller) style. */
125+
isBadgeCondensed?: boolean;
126+
127+
/** Additional styles for the tab's badge. */
128+
badgeStyles?: StyleProp<ViewStyle>;
129+
115130
/** Whether this tab is disabled */
116131
isDisabled?: boolean;
117132

0 commit comments

Comments
 (0)