Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
4 changes: 4 additions & 0 deletions .storybook/webpack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
100 changes: 73 additions & 27 deletions src/components/BlockingViews/BlockingView.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<SvgProps> | ImageSourcePropType;

/** Color for the icon (should be from theme) */
iconColor?: string;

type BaseBlockingViewProps = {
/** Title message below the icon */
title: string;

Expand All @@ -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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tienifr Using camelCase convention

@tienifr tienifr Apr 1, 2024

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We used PascalCase for component props like here, here, here and here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PascalCase is correct for component props

};

type BlockingViewIconProps = {
/** Expensicon for the page */
icon: React.FC<SvgProps> | 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<ViewStyle>;
Comment thread
tienifr marked this conversation as resolved.

/** Style for the animation on web */
animationWebStyle?: WebStyle;
};

// This page requires either an icon or an animation, but not both
type BlockingViewProps = BaseBlockingViewProps & MergeExclusive<BlockingViewIconProps, BlockingViewAnimationProps>;

function BlockingView({
animation,
icon,
iconColor,
title,
Expand All @@ -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(
() => (
<>
<AutoEmailLink
style={[styles.textAlignCenter]}
Expand All @@ -74,25 +101,44 @@ function BlockingView({
</TextLink>
) : null}
</>
),
[styles, subtitle, shouldShowLink, linkKey, onLinkPress, translate],
);

const subtitleContent = useMemo(() => {
if (CustomSubtitle) {
return CustomSubtitle;
}
return shouldEmbedLinkWithSubtitle ? (
<Text style={[styles.textAlignCenter]}>{subtitleText}</Text>
) : (
<View style={[styles.alignItemsCenter, styles.justifyContentCenter]}>{subtitleText}</View>
);
}
}, [styles, subtitleText, shouldEmbedLinkWithSubtitle, CustomSubtitle]);

return (
<View style={[styles.flex1, styles.alignItemsCenter, styles.justifyContentCenter, styles.ph10]}>
<Icon
src={icon}
fill={iconColor}
width={iconWidth}
height={iconHeight}
/>
{animation && (
<Lottie
source={animation}
loop
autoPlay
style={animationStyles}
webStyle={animationWebStyle}
/>
)}
{icon && (
<Icon
src={icon}
fill={iconColor}
width={iconWidth}
height={iconHeight}
/>
)}
<View>
<Text style={[styles.notFoundTextHeader]}>{title}</Text>

{shouldEmbedLinkWithSubtitle ? (
<Text style={[styles.textAlignCenter]}>{renderContent()}</Text>
) : (
<View style={[styles.alignItemsCenter, styles.justifyContentCenter]}>{renderContent()}</View>
)}
{subtitleContent}
</View>
</View>
);
Expand Down
89 changes: 73 additions & 16 deletions src/components/LHNOptionsList/LHNOptionsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -39,8 +47,12 @@ function LHNOptionsList({
const flashListRef = useRef<FlashList<string>>(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.
Expand All @@ -54,6 +66,40 @@ function LHNOptionsList({
onFirstItemRendered();
}, [onFirstItemRendered]);

const emptyLHNSubtitle = useMemo(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UI was broken on Android devices. Let's make sure we test this so that the UI looks good on each platform. #41349

() => (
<View>
<Text
color={theme.placeholderText}
style={[styles.textAlignCenter]}
>
{translate('common.emptyLHN.subtitleText1')}
<Icon
src={Expensicons.MagnifyingGlass}
width={variables.emptyLHNIconWidth}
height={variables.emptyLHNIconHeight}
small
inline
fill={theme.icon}
additionalStyles={styles.alignItemsCenter}
/>
{translate('common.emptyLHN.subtitleText2')}
<Icon
src={Expensicons.Plus}
width={variables.emptyLHNIconWidth}
height={variables.emptyLHNIconHeight}
small
inline
fill={theme.icon}
additionalStyles={styles.alignItemsCenter}
/>
{translate('common.emptyLHN.subtitleText3')}
</Text>
</View>
),
[theme, styles.alignItemsCenter, styles.textAlignCenter, translate],
);

/**
* Function which renders a row in the list
*/
Expand Down Expand Up @@ -163,22 +209,33 @@ function LHNOptionsList({
}, [route, flashListRef, getScrollOffset]);

return (
<View style={style ?? styles.flex1}>
<FlashList
ref={flashListRef}
indicatorStyle="white"
keyboardShouldPersistTaps="always"
contentContainerStyle={StyleSheet.flatten(contentContainerStyles)}
data={data}
testID="lhn-options-list"
keyExtractor={keyExtractor}
renderItem={renderItem}
estimatedItemSize={optionMode === CONST.OPTION_MODE.COMPACT ? variables.optionRowHeightCompact : variables.optionRowHeight}
extraData={extraData}
showsVerticalScrollIndicator={false}
onLayout={onLayout}
onScroll={onScroll}
/>
<View style={[style ?? styles.flex1, shouldShowEmptyLHN ? styles.emptyLHNWrapper : undefined]}>
{shouldShowEmptyLHN ? (
<BlockingView
animation={LottieAnimations.Fireworks}
animationStyles={styles.emptyLHNAnimation}
animationWebStyle={styles.emptyLHNAnimation}
title={translate('common.emptyLHN.title')}
shouldShowLink={false}
CustomSubtitle={emptyLHNSubtitle}
/>
) : (
<FlashList
ref={flashListRef}
indicatorStyle="white"
keyboardShouldPersistTaps="always"
contentContainerStyle={StyleSheet.flatten(contentContainerStyles)}
data={data}
testID="lhn-options-list"
keyExtractor={keyExtractor}
renderItem={renderItem}
estimatedItemSize={optionMode === CONST.OPTION_MODE.COMPACT ? variables.optionRowHeightCompact : variables.optionRowHeight}
extraData={extraData}
showsVerticalScrollIndicator={false}
onLayout={onLayout}
onScroll={onScroll}
/>
)}
</View>
);
}
Expand Down
6 changes: 6 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
6 changes: 6 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,12 @@ export default {
member: 'Miembro',
role: 'Role',
currency: 'Divisa',
emptyLHN: {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy was verified here.

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',
Expand Down
8 changes: 0 additions & 8 deletions src/libs/SidebarUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions src/styles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions src/styles/variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ export default {
avatarChatSpacing: 12,
chatInputSpacing: 52, // 40 + avatarChatSpacing
borderTopWidth: 1,
emptyLHNIconWidth: 24, // iconSizeSmall + 4*2 horizontal margin
Comment thread
tienifr marked this conversation as resolved.
emptyLHNIconHeight: 16,
emptyWorkspaceIconWidth: 84,
emptyWorkspaceIconHeight: 84,
modalTopIconWidth: 200,
Expand Down