diff --git a/.storybook/webpack.config.ts b/.storybook/webpack.config.ts index 3b93756f1df5..c8462db8c40b 100644 --- a/.storybook/webpack.config.ts +++ b/.storybook/webpack.config.ts @@ -54,6 +54,10 @@ const webpackConfig = ({config}: {config: Configuration}) => { ...custom.resolve.alias, }; + // We can ignore the "module not installed" warning from lottie-react-native + // because we are not using the library for JSON format of Lottie animations. + config.ignoreWarnings = [{module: new RegExp('node_modules/lottie-react-native/lib/module/LottieView/index.web.js')}]; + // Necessary to overwrite the values in the existing DefinePlugin hardcoded to the Config staging values const definePluginIndex = config.plugins.findIndex((plugin) => plugin instanceof DefinePlugin); if (definePluginIndex !== -1 && config.plugins[definePluginIndex] instanceof DefinePlugin) { diff --git a/src/components/BlockingViews/BlockingView.tsx b/src/components/BlockingViews/BlockingView.tsx index 7b33c8054950..363342778fc0 100644 --- a/src/components/BlockingViews/BlockingView.tsx +++ b/src/components/BlockingViews/BlockingView.tsx @@ -1,9 +1,12 @@ -import React from 'react'; -import type {ImageSourcePropType} from 'react-native'; +import React, {useMemo} from 'react'; +import type {ImageSourcePropType, StyleProp, ViewStyle, WebStyle} from 'react-native'; import {View} from 'react-native'; import type {SvgProps} from 'react-native-svg'; +import type {MergeExclusive} from 'type-fest'; import AutoEmailLink from '@components/AutoEmailLink'; import Icon from '@components/Icon'; +import Lottie from '@components/Lottie'; +import type DotLottieAnimation from '@components/LottieAnimations/types'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; @@ -12,13 +15,7 @@ import Navigation from '@libs/Navigation/Navigation'; import variables from '@styles/variables'; import type {TranslationPaths} from '@src/languages/types'; -type BlockingViewProps = { - /** Expensicon for the page */ - icon: React.FC | ImageSourcePropType; - - /** Color for the icon (should be from theme) */ - iconColor?: string; - +type BaseBlockingViewProps = { /** Title message below the icon */ title: string; @@ -31,20 +28,46 @@ type BlockingViewProps = { /** Whether we should show a link to navigate elsewhere */ shouldShowLink?: boolean; + /** Function to call when pressing the navigation link */ + onLinkPress?: () => void; + + /** Whether we should embed the link with subtitle */ + shouldEmbedLinkWithSubtitle?: boolean; + + /** Render custom subtitle */ + CustomSubtitle?: React.ReactElement; +}; + +type BlockingViewIconProps = { + /** Expensicon for the page */ + icon: React.FC | ImageSourcePropType; + /** The custom icon width */ iconWidth?: number; /** The custom icon height */ iconHeight?: number; - /** Function to call when pressing the navigation link */ - onLinkPress?: () => void; + /** Color for the icon (should be from theme) */ + iconColor?: string; +}; - /** Whether we should embed the link with subtitle */ - shouldEmbedLinkWithSubtitle?: boolean; +type BlockingViewAnimationProps = { + /** Animation for the page */ + animation: DotLottieAnimation; + + /** Style for the animation */ + animationStyles?: StyleProp; + + /** Style for the animation on web */ + animationWebStyle?: WebStyle; }; +// This page requires either an icon or an animation, but not both +type BlockingViewProps = BaseBlockingViewProps & MergeExclusive; + function BlockingView({ + animation, icon, iconColor, title, @@ -55,11 +78,15 @@ function BlockingView({ iconHeight = variables.iconSizeSuperLarge, onLinkPress = () => Navigation.dismissModal(), shouldEmbedLinkWithSubtitle = false, + animationStyles = [], + animationWebStyle = {}, + CustomSubtitle, }: BlockingViewProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - function renderContent() { - return ( + + const subtitleText = useMemo( + () => ( <> ) : null} + ), + [styles, subtitle, shouldShowLink, linkKey, onLinkPress, translate], + ); + + const subtitleContent = useMemo(() => { + if (CustomSubtitle) { + return CustomSubtitle; + } + return shouldEmbedLinkWithSubtitle ? ( + {subtitleText} + ) : ( + {subtitleText} ); - } + }, [styles, subtitleText, shouldEmbedLinkWithSubtitle, CustomSubtitle]); return ( - + {animation && ( + + )} + {icon && ( + + )} {title} - {shouldEmbedLinkWithSubtitle ? ( - {renderContent()} - ) : ( - {renderContent()} - )} + {subtitleContent} ); diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 04573c8bccac..e241f65bc646 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -5,10 +5,18 @@ import type {ReactElement} from 'react'; import React, {memo, useCallback, useContext, useEffect, useMemo, useRef} from 'react'; import {StyleSheet, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; +import BlockingView from '@components/BlockingViews/BlockingView'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import LottieAnimations from '@components/LottieAnimations'; import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -39,8 +47,12 @@ function LHNOptionsList({ const flashListRef = useRef>(null); const route = useRoute(); + const theme = useTheme(); const styles = useThemeStyles(); const {canUseViolations} = usePermissions(); + const {translate} = useLocalize(); + const {isSmallScreenWidth} = useWindowDimensions(); + const shouldShowEmptyLHN = isSmallScreenWidth && data.length === 0; // When the first item renders we want to call the onFirstItemRendered callback. // At this point in time we know that the list is actually displaying items. @@ -54,6 +66,40 @@ function LHNOptionsList({ onFirstItemRendered(); }, [onFirstItemRendered]); + const emptyLHNSubtitle = useMemo( + () => ( + + + {translate('common.emptyLHN.subtitleText1')} + + {translate('common.emptyLHN.subtitleText2')} + + {translate('common.emptyLHN.subtitleText3')} + + + ), + [theme, styles.alignItemsCenter, styles.textAlignCenter, translate], + ); + /** * Function which renders a row in the list */ @@ -163,22 +209,33 @@ function LHNOptionsList({ }, [route, flashListRef, getScrollOffset]); return ( - - + + {shouldShowEmptyLHN ? ( + + ) : ( + + )} ); } diff --git a/src/languages/en.ts b/src/languages/en.ts index 31f98b637746..fd2daa50942f 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -314,6 +314,12 @@ export default { member: 'Member', role: 'Role', currency: 'Currency', + emptyLHN: { + title: 'Woohoo! All caught up.', + subtitleText1: 'Find a chat using the', + subtitleText2: 'button above, or create something using the', + subtitleText3: 'button below.', + }, }, location: { useCurrent: 'Use current location', diff --git a/src/languages/es.ts b/src/languages/es.ts index 43380221232e..1482ad5e0c5c 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -304,6 +304,12 @@ export default { member: 'Miembro', role: 'Role', currency: 'Divisa', + emptyLHN: { + title: 'Woohoo! Todo al día.', + subtitleText1: 'Encuentra un chat usando el botón', + subtitleText2: 'o crea algo usando el botón', + subtitleText3: '.', + }, }, location: { useCurrent: 'Usar ubicación actual', diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 4803067267d0..2c628f397390 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -108,14 +108,6 @@ function getOrderedReportIDs( }); }); - if (reportsToDisplay.length === 0) { - // Display Concierge chat report when there is no report to be displayed - const conciergeChatReport = allReportsDictValues.find(ReportUtils.isConciergeChatReport); - if (conciergeChatReport) { - reportsToDisplay.push(conciergeChatReport); - } - } - // The LHN is split into four distinct groups, and each group is sorted a little differently. The groups will ALWAYS be in this order: // 1. Pinned/GBR - Always sorted by reportDisplayName // 2. Drafts - Always sorted by reportDisplayName diff --git a/src/styles/index.ts b/src/styles/index.ts index d6dbb539e44d..a736bc537fa6 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -3212,6 +3212,15 @@ const styles = (theme: ThemeColors) => alignItems: 'center', }, + emptyLHNWrapper: { + marginBottom: variables.bottomTabHeight, + }, + + emptyLHNAnimation: { + width: 180, + height: 180, + }, + locationErrorLinkText: { textAlignVertical: 'center', fontSize: variables.fontSizeLabel, diff --git a/src/styles/variables.ts b/src/styles/variables.ts index 50dc4cbd34fa..ac04a436f72e 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -125,6 +125,8 @@ export default { avatarChatSpacing: 12, chatInputSpacing: 52, // 40 + avatarChatSpacing borderTopWidth: 1, + emptyLHNIconWidth: 24, // iconSizeSmall + 4*2 horizontal margin + emptyLHNIconHeight: 16, emptyWorkspaceIconWidth: 84, emptyWorkspaceIconHeight: 84, modalTopIconWidth: 200,