Skip to content
2 changes: 2 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const restrictedImportPaths = [
'Pressable',
'Text',
'ScrollView',
'ActivityIndicator',
'Animated',
'findNodeHandle',
],
Expand All @@ -23,6 +24,7 @@ const restrictedImportPaths = [
"For 'StatusBar', please use '@libs/StatusBar' instead.",
"For 'Text', please use '@components/Text' instead.",
"For 'ScrollView', please use '@components/ScrollView' instead.",
"For 'ActivityIndicator', please use '@components/ActivityIndicator' instead.",
"For 'Animated', please use 'Animated' from 'react-native-reanimated' instead.",
].join('\n'),
},
Expand Down
1 change: 1 addition & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1601,6 +1601,7 @@ const CONST = {
FAST_SEARCH_TREE_CREATION: 'fast_search_tree_creation',
SHOW_HOVER_PREVIEW_DELAY: 270,
SHOW_HOVER_PREVIEW_ANIMATION_DURATION: 250,
ACTIVITY_INDICATOR_TIMEOUT: 10000,
},
PRIORITY_MODE: {
GSD: 'gsd',
Expand Down
43 changes: 43 additions & 0 deletions src/components/ActivityIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React, {useEffect} from 'react';
import type {ActivityIndicatorProps as RNActivityIndicatorProps} from 'react-native';
// eslint-disable-next-line no-restricted-imports
import {ActivityIndicator as RNActivityIndicator} from 'react-native';
import useTheme from '@hooks/useTheme';
import Log from '@libs/Log';
import CONST from '@src/CONST';

type ActivityIndicatorProps = RNActivityIndicatorProps & {
/** The ID of the test to be used for testing */
testID?: string;

/** Timeout for the activity indicator after which we fire a log about abnormally long loading */
timeout?: number;
};

function ActivityIndicator({timeout = CONST.TIMING.ACTIVITY_INDICATOR_TIMEOUT, ...rest}: ActivityIndicatorProps) {
const theme = useTheme();

useEffect(() => {
const timeoutId = setTimeout(() => {
Log.warn('ActivityIndicator has been shown for longer than expected', {
timeoutMs: timeout,
});
}, timeout);

return () => {
clearTimeout(timeoutId);
};
}, [timeout]);

return (
<RNActivityIndicator
color={theme.spinner}
// eslint-disable-next-line react/jsx-props-no-spreading
{...rest}
/>
);
}

ActivityIndicator.displayName = 'ActivityIndicator';

export default ActivityIndicator;
10 changes: 3 additions & 7 deletions src/components/AddPlaidBankAccount.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {ActivityIndicator, View} from 'react-native';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {handlePlaidError, openPlaidBankAccountSelector, openPlaidBankLogin, setPlaidEvent} from '@libs/actions/BankAccounts';
import KeyboardShortcut from '@libs/KeyboardShortcut';
Expand All @@ -14,6 +13,7 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {PlaidData} from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import ActivityIndicator from './ActivityIndicator';
import FullPageOfflineBlockingView from './BlockingViews/FullPageOfflineBlockingView';
import FormHelpMessage from './FormHelpMessage';
import Icon from './Icon';
Expand Down Expand Up @@ -74,7 +74,6 @@ function AddPlaidBankAccount({
onInputChange = () => {},
isDisplayedInWalletFlow = false,
}: AddPlaidBankAccountProps) {
const theme = useTheme();
const styles = useThemeStyles();
const plaidBankAccounts = plaidData?.bankAccounts ?? [];
const defaultSelectedPlaidAccount = plaidBankAccounts.find((account) => account.plaidAccountID === selectedPlaidAccountID);
Expand Down Expand Up @@ -230,10 +229,7 @@ function AddPlaidBankAccount({
if (plaidData?.isLoading) {
return (
<View style={[styles.flex1, styles.alignItemsCenter, styles.justifyContentCenter]}>
<ActivityIndicator
color={theme.spinner}
size="large"
/>
<ActivityIndicator size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE} />
</View>
);
}
Expand Down
12 changes: 3 additions & 9 deletions src/components/AddToWalletButton/index.native.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {AddToWalletButton as RNAddToWalletButton} from '@expensify/react-native-wallet';
import type {TokenizationStatus} from '@expensify/react-native-wallet';
import React, {useCallback, useEffect, useState} from 'react';
import {ActivityIndicator, Alert, View} from 'react-native';
import {Alert, View} from 'react-native';
import ActivityIndicator from '@components/ActivityIndicator';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {getPaymentMethods} from '@libs/actions/PaymentMethods';
import getPlatform from '@libs/getPlatform';
Expand All @@ -19,7 +19,6 @@ function AddToWalletButton({card, cardHolderName, cardDescription, buttonStyle}:
const {translate} = useLocalize();
const isCardAvailable = card.state === CONST.EXPENSIFY_CARD.STATE.OPEN;
const [isLoading, setIsLoading] = useState(false);
const theme = useTheme();
const platform = getPlatform() === CONST.PLATFORM.IOS ? 'Apple' : 'Google';
const styles = useThemeStyles();

Expand Down Expand Up @@ -81,12 +80,7 @@ function AddToWalletButton({card, cardHolderName, cardDescription, buttonStyle}:
}

if (isLoading) {
return (
<ActivityIndicator
size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE}
color={theme.spinner}
/>
);
return <ActivityIndicator size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE} />;
}

if (isInWallet) {
Expand Down
10 changes: 4 additions & 6 deletions src/components/AddressSearch/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React, {forwardRef, useEffect, useMemo, useRef, useState} from 'react';
import type {ForwardedRef} from 'react';
import {ActivityIndicator, Keyboard, LogBox, View} from 'react-native';
import {Keyboard, LogBox, View} from 'react-native';
import type {LayoutChangeEvent} from 'react-native';
import {GooglePlacesAutocomplete} from 'react-native-google-places-autocomplete';
import type {GooglePlaceData, GooglePlaceDetail} from 'react-native-google-places-autocomplete';
import ActivityIndicator from '@components/ActivityIndicator';
import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import LocationErrorMessage from '@components/LocationErrorMessage';
import ScrollView from '@components/ScrollView';
Expand Down Expand Up @@ -339,13 +340,10 @@ function AddressSearch(
const listLoader = useMemo(
() => (
<View style={[styles.pv4]}>
<ActivityIndicator
color={theme.spinner}
size="small"
/>
<ActivityIndicator />
</View>
),
[styles.pv4, theme.spinner],
[styles.pv4],
);

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
import {ActivityIndicator, View} from 'react-native';
import {View} from 'react-native';
import ActivityIndicator from '@components/ActivityIndicator';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import Text from '@components/Text';
Expand Down Expand Up @@ -61,7 +62,6 @@ function DefaultAttachmentView({fileName = '', shouldShowLoadingSpinnerIcon = fa
<View style={styles.ml2}>
<Tooltip text={isUploading ? translate('common.uploading') : translate('common.downloading')}>
<ActivityIndicator
size="small"
color={theme.textSupporting}
testID="attachment-loading-spinner"
/>
Expand Down
6 changes: 3 additions & 3 deletions src/components/AvatarCropModal/AvatarCropModal.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React, {useCallback, useEffect, useState} from 'react';
import {ActivityIndicator, InteractionManager, View} from 'react-native';
import {InteractionManager, View} from 'react-native';
import type {LayoutChangeEvent} from 'react-native';
import {Gesture, GestureHandlerRootView} from 'react-native-gesture-handler';
import type {GestureUpdateEvent, PanGestureChangeEventPayload, PanGestureHandlerEventPayload} from 'react-native-gesture-handler';
import ImageSize from 'react-native-image-size';
import {interpolate, runOnUI, useSharedValue} from 'react-native-reanimated';
import ActivityIndicator from '@components/ActivityIndicator';
import Button from '@components/Button';
import HeaderGap from '@components/HeaderGap';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
Expand Down Expand Up @@ -375,9 +376,8 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose
{/* To avoid layout shift we should hide this component until the image container & image is initialized */}
{!isImageInitialized || !isImageContainerInitialized ? (
<ActivityIndicator
color={theme.spinner}
style={[styles.flex1]}
size="large"
size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE}
/>
) : (
<>
Expand Down
3 changes: 2 additions & 1 deletion src/components/Button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import {useIsFocused} from '@react-navigation/native';
import type {ForwardedRef} from 'react';
import React, {useCallback, useMemo, useState} from 'react';
import type {GestureResponderEvent, LayoutChangeEvent, StyleProp, TextStyle, ViewStyle} from 'react-native';
import {ActivityIndicator, StyleSheet, View} from 'react-native';
import {StyleSheet, View} from 'react-native';
import ActivityIndicator from '@components/ActivityIndicator';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import type {PressableRef} from '@components/Pressable/GenericPressable/types';
Expand Down
14 changes: 9 additions & 5 deletions src/components/FullscreenLoadingIndicator.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
import React from 'react';
import type {ActivityIndicatorProps, StyleProp, ViewStyle} from 'react-native';
import {ActivityIndicator, StyleSheet, View} from 'react-native';
import useTheme from '@hooks/useTheme';
import {StyleSheet, View} from 'react-native';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
import ActivityIndicator from './ActivityIndicator';

type FullScreenLoadingIndicatorIconSize = ActivityIndicatorProps['size'];

type FullScreenLoadingIndicatorProps = {
/** Styles of the outer view */
style?: StyleProp<ViewStyle>;

/** Size of the icon */
iconSize?: FullScreenLoadingIndicatorIconSize;

/** The ID of the test to be used for testing */
testID?: string;
};

function FullScreenLoadingIndicator({style, iconSize = 'large', testID = ''}: FullScreenLoadingIndicatorProps) {
const theme = useTheme();
function FullScreenLoadingIndicator({style, iconSize = CONST.ACTIVITY_INDICATOR_SIZE.LARGE, testID = ''}: FullScreenLoadingIndicatorProps) {
const styles = useThemeStyles();
return (
<View style={[StyleSheet.absoluteFillObject, styles.fullScreenLoading, style]}>
<ActivityIndicator
color={theme.spinner}
size={iconSize}
testID={testID}
/>
Expand Down
9 changes: 3 additions & 6 deletions src/components/HeaderWithBackButton/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, {useMemo} from 'react';
import {ActivityIndicator, Keyboard, StyleSheet, View} from 'react-native';
import {Keyboard, StyleSheet, View} from 'react-native';
import type {SvgProps} from 'react-native-svg';
import ActivityIndicator from '@components/ActivityIndicator';
import Avatar from '@components/Avatar';
import AvatarWithDisplayName from '@components/AvatarWithDisplayName';
import Header from '@components/Header';
Expand Down Expand Up @@ -274,11 +275,7 @@ function HeaderWithBackButton({
</PressableWithoutFeedback>
</Tooltip>
) : (
<ActivityIndicator
style={[styles.touchableButtonImage]}
size="small"
color={theme.spinner}
/>
<ActivityIndicator style={[styles.touchableButtonImage]} />
))}
{shouldShowPinButton && !!report && <PinButton report={report} />}
</View>
Expand Down
6 changes: 4 additions & 2 deletions src/components/Lightbox/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
import type {LayoutChangeEvent, StyleProp, ViewStyle} from 'react-native';
import {ActivityIndicator, PixelRatio, StyleSheet, View} from 'react-native';
import {PixelRatio, StyleSheet, View} from 'react-native';
import {useSharedValue} from 'react-native-reanimated';
import ActivityIndicator from '@components/ActivityIndicator';
import AttachmentOfflineIndicator from '@components/AttachmentOfflineIndicator';
import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext';
import Image from '@components/Image';
Expand All @@ -13,6 +14,7 @@ import useNetwork from '@hooks/useNetwork';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import {isLocalFile} from '@libs/fileDownload/FileUtils';
import CONST from '@src/CONST';
import NUMBER_OF_CONCURRENT_LIGHTBOXES from './numberOfConcurrentLightboxes';

const cachedImageDimensions = new Map<string, ContentSize | undefined>();
Expand Down Expand Up @@ -269,7 +271,7 @@ function Lightbox({isAuthTokenRequired = false, uri, onScaleChanged: onScaleChan
{/* Show activity indicator while the lightbox is still loading the image. */}
{isLoading && (!isOffline || isALocalFile) && (
<ActivityIndicator
size="large"
size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE}
style={StyleSheet.absoluteFill}
/>
)}
Expand Down
8 changes: 3 additions & 5 deletions src/components/MenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type {ImageContentFit} from 'expo-image';
import type {ReactElement, ReactNode, Ref} from 'react';
import React, {useContext, useMemo, useRef} from 'react';
import type {GestureResponderEvent, StyleProp, TextStyle, ViewStyle} from 'react-native';
import {ActivityIndicator, View} from 'react-native';
import {View} from 'react-native';
import type {ValueOf} from 'type-fest';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useStyleUtils from '@hooks/useStyleUtils';
Expand All @@ -24,6 +24,7 @@ import CONST from '@src/CONST';
import type {Icon as IconType} from '@src/types/onyx/OnyxCommon';
import type {TooltipAnchorAlignment} from '@src/types/utils/AnchorAlignment';
import type IconAsset from '@src/types/utils/IconAsset';
import ActivityIndicator from './ActivityIndicator';
import Avatar from './Avatar';
import Badge from './Badge';
import CopyTextToClipboard from './CopyTextToClipboard';
Expand Down Expand Up @@ -777,10 +778,7 @@ function MenuItem({
additionalStyles={additionalIconStyles}
/>
) : (
<ActivityIndicator
size="small"
color={theme.textSupporting}
/>
<ActivityIndicator color={theme.textSupporting} />
))}
{!!icon && iconType === CONST.ICON_TYPE_WORKSPACE && (
<Avatar
Expand Down
3 changes: 2 additions & 1 deletion src/components/MoneyReportHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {useRoute} from '@react-navigation/native';
import {isUserValidatedSelector} from '@selectors/Account';
import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
import {ActivityIndicator, InteractionManager, View} from 'react-native';
import {InteractionManager, View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import useDuplicateTransactionsAndViolations from '@hooks/useDuplicateTransactionsAndViolations';
Expand Down Expand Up @@ -109,6 +109,7 @@ import type * as OnyxTypes from '@src/types/onyx';
import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
import type IconAsset from '@src/types/utils/IconAsset';
import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
import ActivityIndicator from './ActivityIndicator';
import AnimatedSubmitButton from './AnimatedSubmitButton';
import BrokenConnectionDescription from './BrokenConnectionDescription';
import Button from './Button';
Expand Down
10 changes: 3 additions & 7 deletions src/components/PlaidCardFeedIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React, {useEffect, useState} from 'react';
import {ActivityIndicator, View} from 'react-native';
import {View} from 'react-native';
import type {StyleProp, ViewStyle} from 'react-native';
import useTheme from '@hooks/useTheme';
import useThemeIllustrations from '@hooks/useThemeIllustrations';
import useThemeStyles from '@hooks/useThemeStyles';
import variables from '@styles/variables';
import ActivityIndicator from './ActivityIndicator';
import Icon from './Icon';
import * as Illustrations from './Icon/Illustrations';
import Image from './Image';
Expand All @@ -20,7 +20,6 @@ function PlaidCardFeedIcon({plaidUrl, style, isLarge, isSmall}: PlaidCardFeedIco
const [isBrokenImage, setIsBrokenImage] = useState<boolean>(false);
const styles = useThemeStyles();
const illustrations = useThemeIllustrations();
const theme = useTheme();
const width = isLarge ? variables.cardPreviewWidth : variables.cardIconWidth;
const height = isLarge ? variables.cardPreviewHeight : variables.cardIconHeight;
const [loading, setLoading] = useState<boolean>(true);
Expand Down Expand Up @@ -57,10 +56,7 @@ function PlaidCardFeedIcon({plaidUrl, style, isLarge, isSmall}: PlaidCardFeedIco
/>
{loading ? (
<View style={[styles.justifyContentCenter, {width: iconWidth, height: iconHeight}]}>
<ActivityIndicator
color={theme.spinner}
size={isSmall ? 10 : 20}
/>
<ActivityIndicator size={isSmall ? 10 : 20} />
</View>
) : (
<Icon
Expand Down
Loading
Loading