Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
4cc504a
Replace Priority Mode with Inbox Tab Filters
shawnborton Apr 17, 2026
9af6b20
Update tabs to All/To do/Expenses/DMs and add Unreads toggle
shawnborton Apr 17, 2026
8634be9
Change 'To do' tab label to 'To-do'
shawnborton Apr 17, 2026
e2ebe46
Replace Priority Mode with Inbox Tab Filters
shawnborton Apr 21, 2026
682f2e6
Merge main into shawn-inboxTabs
shawnborton Apr 21, 2026
fad08c4
Merge main and keep priority mode
youssef-lr May 30, 2026
53cd10f
Remove DM & expense filters
youssef-lr May 30, 2026
d801535
Perf: improve computing of unread reports
youssef-lr May 30, 2026
a45d7d4
Fix checks
youssef-lr May 31, 2026
efb10fa
Add count badges
youssef-lr Jun 1, 2026
2b36db9
Change copy
youssef-lr Jun 1, 2026
7216fe1
Adjust styles
youssef-lr Jun 1, 2026
a81d7a6
Remove count badge from the "All" tab
youssef-lr Jun 1, 2026
a85f0f3
Translations update
youssef-lr Jun 1, 2026
1f70060
Merge branch 'main' into youssef_inbox_filters
youssef-lr Jun 2, 2026
600c296
Use regular sized tabs
youssef-lr Jun 2, 2026
35feb10
Adjust styles
youssef-lr Jun 2, 2026
f2c1404
Remove unused type
youssef-lr Jun 2, 2026
7b5b99c
Merge remote-tracking branch 'origin/main' into youssef_inbox_filters
youssef-lr Jun 2, 2026
5ab1b65
Reduce bottom padding
youssef-lr Jun 3, 2026
8cd8c69
Increase gap size between text and badge
youssef-lr Jun 3, 2026
6e3da42
Merge remote-tracking branch 'origin/main' into youssef_inbox_filters
youssef-lr Jun 3, 2026
bfca90f
Address comments and fix bug
youssef-lr Jun 3, 2026
be8fdad
Fix empty view flashing briefly when opening the only unread/todo report
youssef-lr Jun 4, 2026
7afa1ab
Address comment
youssef-lr Jun 4, 2026
c77f44d
Merge remote-tracking branch 'origin/main' into youssef_inbox_filters
youssef-lr Jun 4, 2026
fb269bc
Merge remote-tracking branch 'origin/main' into youssef_inbox_filters
youssef-lr Jun 5, 2026
6f2fa59
Fix unequal tab width in native
youssef-lr Jun 5, 2026
031aa66
Update empty view copy and add link to the "all" tab
youssef-lr Jun 8, 2026
fcfddc7
Translations
youssef-lr Jun 8, 2026
6f9c5b3
Allow parent chat to show up in LHN when clicked from subtitle
youssef-lr Jun 9, 2026
ec5ee16
Revert parent chat fix
youssef-lr Jun 9, 2026
2c1905c
Adjust spacing to match the mock
youssef-lr Jun 9, 2026
b6515d7
Lint
youssef-lr Jun 9, 2026
3adbe31
Merge remote-tracking branch 'origin/main' into youssef_inbox_filters
youssef-lr Jun 9, 2026
4e3c0f9
Adjust link style
youssef-lr Jun 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2327,6 +2327,11 @@ const CONST = {
GSD: 'gsd',
DEFAULT: 'default',
},
INBOX_TAB: {
ALL: 'all',
TODO: 'todo',
UNREAD: 'unread',
},
THEME: {
DEFAULT: 'system',
FALLBACK: 'dark',
Expand Down
4 changes: 4 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,9 @@ const ONYXKEYS = {
/** Contains the user preference for the LHN priority mode */
NVP_PRIORITY_MODE: 'nvp_priorityMode',

/** Contains the user preference for the active inbox tab filter */
NVP_INBOX_TAB: 'nvp_inboxTab',

/** Contains the users's block expiration (if they have one) */
NVP_BLOCKED_FROM_CONCIERGE: 'nvp_private_blockedFromConcierge',

Expand Down Expand Up @@ -1492,6 +1495,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.BETA_CONFIGURATION]: OnyxTypes.BetaConfiguration;
[ONYXKEYS.NVP_MUTED_PLATFORMS]: Partial<Record<Platform, true>>;
[ONYXKEYS.NVP_PRIORITY_MODE]: ValueOf<typeof CONST.PRIORITY_MODE>;
[ONYXKEYS.NVP_INBOX_TAB]: ValueOf<typeof CONST.INBOX_TAB>;
[ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE]: OnyxTypes.BlockedFromConcierge;
[ONYXKEYS.QUEUE_FLUSHED_DATA]: AnyOnyxUpdate[];
[ONYXKEYS.TRANSACTIONS_PENDING_3DS_REVIEW]: OnyxTypes.TransactionsPending3DSReview;
Expand Down
35 changes: 33 additions & 2 deletions src/components/LHNOptionsList/LHNEmptyState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,51 @@ import {View} from 'react-native';
import type {BlockingViewProps} from '@components/BlockingViews/BlockingView';
import BlockingView from '@components/BlockingViews/BlockingView';
import Icon from '@components/Icon';
import Text from '@components/Text';
import TextBlock from '@components/TextBlock';
import TextLink from '@components/TextLink';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import {useSidebarOrderedReportsActions, useSidebarOrderedReportsState} from '@hooks/useSidebarOrderedReports';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import useEmptyLHNIllustration from './useEmptyLHNIllustration';

function LHNEmptyState() {
const theme = useTheme();
const styles = useThemeStyles();
const {translate} = useLocalize();
const expensifyIcons = useMemoizedLazyExpensifyIcons(['MagnifyingGlass', 'Plus']);
const emptyLHNIllustration = useEmptyLHNIllustration();
const emptyLHNIllustration = useEmptyLHNIllustration() as BlockingViewProps;
const {activeTab} = useSidebarOrderedReportsState();
const {setActiveTab} = useSidebarOrderedReportsActions();

if (activeTab === CONST.INBOX_TAB.UNREAD || activeTab === CONST.INBOX_TAB.TODO) {
const title = activeTab === CONST.INBOX_TAB.UNREAD ? translate('common.emptyLHN.noUnreadChats') : translate('common.emptyLHN.noTodos');
const caughtUpSubtitle = (
<View style={[styles.alignItemsCenter, styles.justifyContentCenter]}>
<Text style={[styles.textAlignCenter, styles.textSupporting]}>{translate('common.emptyLHN.caughtUp')}</Text>
<TextLink
onPress={() => setActiveTab(CONST.INBOX_TAB.ALL)}
style={[styles.textStrong, styles.mt5, styles.ph4, styles.textAlignCenter]}
>
{translate('common.emptyLHN.seeAllChats')}
</TextLink>
</View>
);

return (
<BlockingView
{...emptyLHNIllustration}
title={title}
titleStyles={styles.mb2}
CustomSubtitle={caughtUpSubtitle}
accessibilityLabel={title}
/>
);
}

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

return (
<BlockingView
{...(emptyLHNIllustration as BlockingViewProps)}
{...emptyLHNIllustration}
title={translate('common.emptyLHN.title')}
CustomSubtitle={subtitle}
accessibilityLabel={translate('common.emptyLHN.title')}
Expand Down
10 changes: 7 additions & 3 deletions src/components/ScrollOffsetContextProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,21 +61,25 @@ function getKey(route: PlatformStackRouteProp<ParamListBase> | NavigationPartial

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(() => {
if (previousPriorityMode === null || previousPriorityMode === priorityMode) {
const priorityModeChanged = previousPriorityMode !== null && previousPriorityMode !== priorityMode;
const inboxTabChanged = previousInboxTab !== null && previousInboxTab !== inboxTab;
if (!priorityModeChanged && !inboxTabChanged) {
return;
}

// 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.
// 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]);
}, [priorityMode, previousPriorityMode, inboxTab, previousInboxTab]);

const saveScrollOffset: ScrollOffsetContextValue['saveScrollOffset'] = useCallback((route, scrollOffset) => {
scrollOffsetsRef.current[getKey(route)] = scrollOffset;
Expand Down
9 changes: 8 additions & 1 deletion src/components/TabSelector/TabSelectorBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ function TabSelectorBase({
position,
shouldShowLabelWhenInactive = true,
equalWidth = false,
contentContainerStyles,
}: TabSelectorBaseProps) {
const theme = useTheme();
const styles = useThemeStyles();
Expand Down Expand Up @@ -61,7 +62,11 @@ function TabSelectorBase({
}}
ref={containerRef}
style={styles.scrollableTabSelector}
contentContainerStyle={styles.tabSelectorContentContainer}
// On iOS a horizontal ScrollView lays out its content along an unbounded main axis, so flex-1 tabs
// (equalWidth) divide their intrinsic content width instead of the viewport. Giving the content
// container a definite width lets the flex children split it evenly. Scoped to equalWidth so normal
// overflowing/scrollable tab rows are not constrained.
contentContainerStyle={[styles.tabSelectorContentContainer, equalWidth && styles.w100, contentContainerStyles]}
horizontal
showsHorizontalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
Expand Down Expand Up @@ -119,6 +124,8 @@ function TabSelectorBase({
shouldShowLabelWhenInactive={shouldShowLabelWhenInactive}
equalWidth={equalWidth}
badgeText={tab.badgeText}
isBadgeCondensed={tab.isBadgeCondensed}
badgeStyles={tab.badgeStyles}
pendingAction={tab.pendingAction}
isDisabled={tab.isDisabled}
/>
Expand Down
4 changes: 4 additions & 0 deletions src/components/TabSelector/TabSelectorItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ function TabSelectorItem({
sentryLabel,
equalWidth = false,
badgeText,
isBadgeCondensed = false,
badgeStyles,
isDisabled = false,
pendingAction,
}: TabSelectorItemProps) {
Expand Down Expand Up @@ -90,6 +92,8 @@ function TabSelectorItem({
<Badge
text={badgeText}
success
isCondensed={isBadgeCondensed}
badgeStyles={badgeStyles}
/>
)}
</AnimatedPressableWithFeedback>
Expand Down
17 changes: 16 additions & 1 deletion src/components/TabSelector/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type {MaterialTopTabBarProps} from '@react-navigation/material-top-tabs';
// eslint-disable-next-line no-restricted-imports
import type {Animated} from 'react-native';
import type {Animated, StyleProp, ViewStyle} from 'react-native';
import type {ThemeColors} from '@styles/theme/types';
import type {PendingAction} from '@src/types/onyx/OnyxCommon';
import type IconAsset from '@src/types/utils/IconAsset';
Expand Down Expand Up @@ -39,6 +39,12 @@ type TabSelectorBaseItem = WithSentryLabel & {
/** Text to display on the badge on the tab. */
badgeText?: string;

/** Whether the tab's badge should use the condensed (smaller) style. */
isBadgeCondensed?: boolean;

/** Additional styles for the tab's badge. */
badgeStyles?: StyleProp<ViewStyle>;

/** Whether this tab is disabled */
isDisabled?: boolean;

Expand Down Expand Up @@ -70,6 +76,9 @@ type TabSelectorBaseProps = {

/** Whether tabs should have equal width. */
equalWidth?: boolean;

/** Additional styles for the tabs' scroll content container. */
contentContainerStyles?: StyleProp<ViewStyle>;
};

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

/** Whether the tab's badge should use the condensed (smaller) style. */
isBadgeCondensed?: boolean;

/** Additional styles for the tab's badge. */
badgeStyles?: StyleProp<ViewStyle>;

/** Whether this tab is disabled */
isDisabled?: boolean;

Expand Down
Loading
Loading