Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -2243,6 +2243,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 @@ -168,6 +168,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 @@ -1448,6 +1451,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
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
11 changes: 8 additions & 3 deletions src/components/TabSelector/TabLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {StyleProp, TextStyle} from 'react-native';
import Text from '@components/Text';
import useThemeStyles from '@hooks/useThemeStyles';
import variables from '@styles/variables';
import type {TabSelectorSize} from './types';

type TabLabelProps = {
/** Title of the tab */
Expand All @@ -21,24 +22,28 @@ type TabLabelProps = {

/** Text style */
textStyle?: StyleProp<TextStyle>;

/** Size variant */
size?: TabSelectorSize;
};

function TabLabel({title = '', activeOpacity = 0, inactiveOpacity = 1, hasIcon = false, textStyle}: TabLabelProps) {
function TabLabel({title = '', activeOpacity = 0, inactiveOpacity = 1, hasIcon = false, textStyle, size}: TabLabelProps) {
const styles = useThemeStyles();
const smallTextStyle = size === 'small' ? styles.tabTextSmall : undefined;
return (
<View style={{maxWidth: variables.tabSelectorMaxTabLabelWidth}}>
<Animated.View style={[{opacity: activeOpacity}]}>
<Text
numberOfLines={1}
style={[styles.tabText(true, hasIcon), textStyle]}
style={[styles.tabText(true, hasIcon), smallTextStyle, textStyle]}
>
{title}
</Text>
</Animated.View>
<Animated.View style={[StyleSheet.absoluteFill, {opacity: inactiveOpacity}]}>
<Text
numberOfLines={1}
style={[styles.tabText(false, hasIcon), textStyle]}
style={[styles.tabText(false, hasIcon), smallTextStyle, textStyle]}
>
{title}
</Text>
Expand Down
4 changes: 3 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,
size,
shouldShowProductTrainingTooltip = false,
renderProductTrainingTooltip,
}: TabSelectorBaseProps) {
Expand Down Expand Up @@ -63,7 +64,7 @@ function TabSelectorBase({
}}
ref={containerRef}
style={styles.scrollableTabSelector}
contentContainerStyle={styles.tabSelectorContentContainer}
contentContainerStyle={[styles.tabSelectorContentContainer, size === 'small' && styles.tabSelectorContentContainerSmall]}
horizontal
showsHorizontalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
Expand Down Expand Up @@ -122,6 +123,7 @@ function TabSelectorBase({
shouldShowProductTrainingTooltip={shouldShowProductTrainingTooltip}
renderProductTrainingTooltip={renderProductTrainingTooltip}
equalWidth={equalWidth}
size={size}
badgeText={tab.badgeText}
pendingAction={tab.pendingAction}
isDisabled={tab.isDisabled}
Expand Down
3 changes: 3 additions & 0 deletions src/components/TabSelector/TabSelectorItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ function TabSelectorItem({
shouldShowProductTrainingTooltip = false,
renderProductTrainingTooltip,
equalWidth = false,
size,
badgeText,
isDisabled = false,
pendingAction,
Expand All @@ -58,6 +59,7 @@ function TabSelectorItem({
accessibilityRole={CONST.ROLE.TAB}
style={[
styles.tabSelectorButton,
size === 'small' && styles.tabSelectorButtonSmall,
styles.tabBackground(isHovered, isActive, isDisabled, backgroundColor),
styles.userSelectNone,
isOfflineWithPendingAction ? styles.offlineFeedbackPending : undefined,
Expand Down Expand Up @@ -89,6 +91,7 @@ function TabSelectorItem({
activeOpacity={styles.tabOpacity(isDisabled, isHovered, isActive, activeOpacity, inactiveOpacity).opacity}
inactiveOpacity={styles.tabOpacity(isDisabled, isHovered, isActive, inactiveOpacity, activeOpacity).opacity}
hasIcon={!!icon}
size={size}
/>
)}
{!!badgeText && (
Expand Down
10 changes: 9 additions & 1 deletion src/components/TabSelector/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ type TabSelectorBaseItem = WithSentryLabel & {
pendingAction?: PendingAction;
};

type TabSelectorSize = 'default' | 'small';

type TabSelectorBaseProps = {
/** Tabs to render. */
tabs: TabSelectorBaseItem[];
Expand All @@ -77,6 +79,9 @@ type TabSelectorBaseProps = {
/** Whether tabs should have equal width. */
equalWidth?: boolean;

/** Size variant for the tabs. 'small' uses a compact 28px height. */
size?: TabSelectorSize;

/** Determines whether the product training tooltip should be displayed to the user. */
shouldShowProductTrainingTooltip?: boolean;

Expand Down Expand Up @@ -121,6 +126,9 @@ type TabSelectorItemProps = WithSentryLabel & {
/** Whether tabs should have equal width */
equalWidth?: boolean;

/** Size variant for the tabs. */
size?: TabSelectorSize;

/** Determines whether the product training tooltip should be displayed to the user. */
shouldShowProductTrainingTooltip?: boolean;

Expand Down Expand Up @@ -182,4 +190,4 @@ type BackgroundColor = Animated.AnimatedInterpolation<string> | string;

type Opacity = 1 | 0 | Animated.AnimatedInterpolation<number>;

export type {TabSelectorProps, BackgroundColor, GetBackgroundColorConfig, Opacity, GetOpacityConfig, TabSelectorBaseProps, TabSelectorBaseItem, TabSelectorItemProps};
export type {TabSelectorProps, BackgroundColor, GetBackgroundColorConfig, Opacity, GetOpacityConfig, TabSelectorBaseProps, TabSelectorBaseItem, TabSelectorItemProps, TabSelectorSize};
50 changes: 40 additions & 10 deletions src/hooks/useSidebarOrderedReports.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React, {createContext, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import {setInboxTab} from '@libs/actions/User';
import Log from '@libs/Log';
import {getTransactionThreadReportID} from '@libs/MergeTransactionUtils';
import {isOneTransactionReport} from '@libs/ReportUtils';
Expand Down Expand Up @@ -33,23 +35,27 @@ type SidebarOrderedReportsStateContextValue = {
orderedReportIDs: string[];
currentReportID: string | undefined;
chatTabBrickRoad: BrickRoad;
activeTab: ValueOf<typeof CONST.INBOX_TAB>;
};

type SidebarOrderedReportsActionsContextValue = {
clearLHNCache: () => void;
setActiveTab: (tab: ValueOf<typeof CONST.INBOX_TAB>) => void;
};

type ReportsToDisplayInLHN = Record<string, OnyxTypes.Report & {hasErrorsOtherThanFailedReceipt?: boolean; requiresAttention?: boolean}>;
type ReportsToDisplayInLHN = Record<string, OnyxTypes.Report & {hasErrorsOtherThanFailedReceipt?: boolean; requiresAttention?: boolean; isUnreadReport?: boolean}>;

const SidebarOrderedReportsStateContext = createContext<SidebarOrderedReportsStateContextValue>({
orderedReports: [],
orderedReportIDs: [],
currentReportID: '',
chatTabBrickRoad: undefined,
activeTab: CONST.INBOX_TAB.ALL,
});

const SidebarOrderedReportsActionsContext = createContext<SidebarOrderedReportsActionsContextValue>({
clearLHNCache: () => {},
setActiveTab: () => {},
});

const policyMapper = (policy: OnyxEntry<OnyxTypes.Policy>): PartialPolicyForSidebar =>
Expand All @@ -74,6 +80,8 @@ function SidebarOrderedReportsContextProvider({
}: SidebarOrderedReportsContextProviderProps) {
const {localeCompare} = useLocalize();
const [priorityMode = CONST.PRIORITY_MODE.DEFAULT] = useOnyx(ONYXKEYS.NVP_PRIORITY_MODE);
const [inboxTab = CONST.INBOX_TAB.ALL] = useOnyx(ONYXKEYS.NVP_INBOX_TAB);
const activeTab = inboxTab ?? CONST.INBOX_TAB.ALL;
const [chatReports, {sourceValue: reportUpdates}] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
const [, {sourceValue: policiesUpdates}] = useMappedPolicies(policyMapper);
const [transactions, {sourceValue: transactionsUpdates}] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION);
Expand Down Expand Up @@ -286,7 +294,11 @@ function SidebarOrderedReportsContextProvider({

const orderedReportIDs = useMemo(() => getOrderedReportIDs(), [getOrderedReportIDs]);

// Get the actual reports based on the ordered IDs
// Narrow the ordered reports down to the ones belonging to the active Inbox tab. The "All" tab
// returns everything (still honoring Most Recent / Focus mode from the ordering above).
const filteredReportIDs = useMemo(() => SidebarUtils.filterReportsForInboxTab(orderedReportIDs, reportsToDisplayInLHN, activeTab), [orderedReportIDs, reportsToDisplayInLHN, activeTab]);

// Get the actual reports based on the filtered IDs
const getOrderedReports = useCallback(
(reportIDs: string[]): OnyxTypes.Report[] => {
if (!chatReports) {
Expand All @@ -297,14 +309,18 @@ function SidebarOrderedReportsContextProvider({
[chatReports],
);

const orderedReports = useMemo(() => getOrderedReports(orderedReportIDs), [getOrderedReports, orderedReportIDs]);
const orderedReports = useMemo(() => getOrderedReports(filteredReportIDs), [getOrderedReports, filteredReportIDs]);

const clearLHNCache = useCallback(() => {
Log.info('[useSidebarOrderedReports] Clearing sidebar cache manually via debug modal');
setCurrentReportsToDisplay({});
setClearCacheDummyCounter((current) => current + 1);
}, []);

const setActiveTab = useCallback((tab: ValueOf<typeof CONST.INBOX_TAB>) => {
setInboxTab(tab);
}, []);

const stateValue: SidebarOrderedReportsStateContextValue = useMemo(() => {
// We need to make sure the current report is in the list of reports, but we do not want
// to have to re-generate the list every time the currentReportID changes. To do that
Expand All @@ -317,30 +333,44 @@ function SidebarOrderedReportsContextProvider({
// any expense, a new LHN item is added in the list and is visible on web. But on mobile, we
// just navigate to the screen with expense details, so there seems no point to execute this logic on mobile.
if (
(!shouldUseNarrowLayout || orderedReportIDs.length === 0) &&
(!shouldUseNarrowLayout || filteredReportIDs.length === 0) &&
derivedCurrentReportID &&
derivedCurrentReportID !== '-1' &&
orderedReportIDs.indexOf(derivedCurrentReportID) === -1
filteredReportIDs.indexOf(derivedCurrentReportID) === -1
) {
const updatedReportIDs = getOrderedReportIDs();
const updatedReports = getOrderedReports(updatedReportIDs);
const updatedFilteredIDs = SidebarUtils.filterReportsForInboxTab(updatedReportIDs, reportsToDisplayInLHN, activeTab);
const updatedReports = getOrderedReports(updatedFilteredIDs);
return {
orderedReports: updatedReports,
orderedReportIDs: updatedReportIDs,
orderedReportIDs: updatedFilteredIDs,
currentReportID: derivedCurrentReportID,
chatTabBrickRoad: getChatTabBrickRoad(updatedReportIDs, reportAttributes),
activeTab,
};
}

return {
orderedReports,
orderedReportIDs,
orderedReportIDs: filteredReportIDs,
currentReportID: derivedCurrentReportID,
chatTabBrickRoad: getChatTabBrickRoad(orderedReportIDs, reportAttributes),
activeTab,
};
}, [getOrderedReportIDs, orderedReportIDs, derivedCurrentReportID, shouldUseNarrowLayout, getOrderedReports, orderedReports, reportAttributes]);
}, [
getOrderedReportIDs,
orderedReportIDs,
filteredReportIDs,
derivedCurrentReportID,
shouldUseNarrowLayout,
getOrderedReports,
orderedReports,
reportAttributes,
activeTab,
reportsToDisplayInLHN,
]);

const actionsValue: SidebarOrderedReportsActionsContextValue = useMemo(() => ({clearLHNCache}), [clearLHNCache]);
const actionsValue: SidebarOrderedReportsActionsContextValue = useMemo(() => ({clearLHNCache, setActiveTab}), [clearLHNCache, setActiveTab]);

useEffect(() => {
const hookExecutionDuration = performance.now() - hookStartTime.current;
Expand Down
15 changes: 10 additions & 5 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2889,6 +2889,16 @@ ${amount} für ${merchant} – ${date}`,
},
},
},
focusModeUpdateModal: {
title: 'Willkommen im #Fokusmodus!',
prompt: (priorityModePageUrl: string) =>
`Behalte den Überblick, indem du nur ungelesene Chats oder Chats siehst, die deine Aufmerksamkeit benötigen. Keine Sorge, du kannst das jederzeit in den <a href="${priorityModePageUrl}">Einstellungen</a> ändern.`,
},
inboxTabs: {
all: 'All',
todo: 'To-do',
unread: 'Unread',
},
reportDetailsPage: {
inWorkspace: (policyName: string) => `in ${policyName}`,
generatingPDF: 'PDF erstellen',
Expand Down Expand Up @@ -3442,11 +3452,6 @@ ${amount} für ${merchant} – ${date}`,
year: 'Jahr',
selectYear: 'Bitte ein Jahr auswählen',
},
focusModeUpdateModal: {
title: 'Willkommen im #Fokusmodus!',
prompt: (priorityModePageUrl: string) =>
`Behalte den Überblick, indem du nur ungelesene Chats oder Chats siehst, die deine Aufmerksamkeit benötigen. Keine Sorge, du kannst das jederzeit in den <a href="${priorityModePageUrl}">Einstellungen</a> ändern.`,
},
notFound: {
chatYouLookingForCannotBeFound: 'Der Chat, den du suchst, kann nicht gefunden werden.',
getMeOutOfHere: 'Hol mich hier raus',
Expand Down
15 changes: 10 additions & 5 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2960,6 +2960,16 @@ const translations = {
},
},
},
focusModeUpdateModal: {
title: 'Welcome to #focus mode!',
prompt: (priorityModePageUrl: string) =>
`Stay on top of things by only seeing unread chats or chats that need your attention. Don’t worry, you can change this at any point in <a href="${priorityModePageUrl}">settings</a>.`,
},
inboxTabs: {
all: 'All',
todo: 'To-do',
unread: 'Unread',
},
reportDetailsPage: {
inWorkspace: (policyName: string) => `in ${policyName}`,
generatingPDF: 'Generate PDF',
Expand Down Expand Up @@ -3522,11 +3532,6 @@ const translations = {
month: 'Month',
selectMonth: 'Please select a month',
},
focusModeUpdateModal: {
title: 'Welcome to #focus mode!',
prompt: (priorityModePageUrl: string) =>
`Stay on top of things by only seeing unread chats or chats that need your attention. Don’t worry, you can change this at any point in <a href="${priorityModePageUrl}">settings</a>.`,
},
notFound: {
chatYouLookingForCannotBeFound: 'The chat you are looking for cannot be found.',
getMeOutOfHere: 'Get me out of here',
Expand Down
Loading
Loading