diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 52b1994f6fe6..4a1a82259bef 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -4232,21 +4232,21 @@ const CONST = { }, AVATAR_SIZE: { - X_LARGE: 'xlarge', - LARGE: 'large', - MEDIUM: 'medium', - DEFAULT: 'default', + XXXXX_SMALL: 'xxxxx-small', + XXXX_SMALL: 'xxxx-small', + XXX_SMALL: 'xxx-small', + XX_SMALL: 'xx-small', + X_SMALL: 'x-small', SMALL: 'small', - SMALLER: 'smaller', - SUBSCRIPT: 'subscript', - SMALL_SUBSCRIPT: 'small-subscript', - MID_SUBSCRIPT: 'mid-subscript', - LARGE_BORDERED: 'large-bordered', - MEDIUM_LARGE: 'medium-large', - HEADER: 'header', - MENTION_ICON: 'mention-icon', - SMALL_NORMAL: 'small-normal', - LARGE_NORMAL: 'large-normal', + MEDIUM: 'medium', + // Alias of MEDIUM, used as the default avatar size + DEFAULT: 'medium', + LARGE: 'large', + X_LARGE: 'x-large', + XX_LARGE: 'xx-large', + XXX_LARGE: 'xxx-large', + XXXX_LARGE: 'xxxx-large', + XXXXX_LARGE: 'xxxxx-large', }, COMPANY_CARD: { diff --git a/src/components/AvatarButtonWithIcon.tsx b/src/components/AvatarButtonWithIcon.tsx index cc17434816f9..150828f30570 100644 --- a/src/components/AvatarButtonWithIcon.tsx +++ b/src/components/AvatarButtonWithIcon.tsx @@ -46,7 +46,7 @@ type AvatarButtonWithIconProps = WithSentryLabel & { DefaultAvatar?: () => React.ReactNode; /** Size of Indicator */ - size?: typeof CONST.AVATAR_SIZE.X_LARGE | typeof CONST.AVATAR_SIZE.LARGE | typeof CONST.AVATAR_SIZE.DEFAULT; + size?: typeof CONST.AVATAR_SIZE.XXXXX_LARGE | typeof CONST.AVATAR_SIZE.XXX_LARGE | typeof CONST.AVATAR_SIZE.DEFAULT; /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ fallbackIcon?: AvatarSource; diff --git a/src/components/AvatarSelector.tsx b/src/components/AvatarSelector.tsx index 8fee658c1412..eff7a62bb294 100644 --- a/src/components/AvatarSelector.tsx +++ b/src/components/AvatarSelector.tsx @@ -33,7 +33,7 @@ const SPACER_SIZE = 10; /** * AvatarSelector — renders a grid of selectable avatars. */ -function AvatarSelector({selectedID, onSelect, label, name, size = CONST.AVATAR_SIZE.MEDIUM}: AvatarSelectorProps) { +function AvatarSelector({selectedID, onSelect, label, name, size = CONST.AVATAR_SIZE.X_LARGE}: AvatarSelectorProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); diff --git a/src/components/AvatarSkeleton.tsx b/src/components/AvatarSkeleton.tsx index 62b79d25946e..5de14100cfa9 100644 --- a/src/components/AvatarSkeleton.tsx +++ b/src/components/AvatarSkeleton.tsx @@ -13,7 +13,7 @@ type AvatarSkeletonProps = { reasonAttributes: SkeletonSpanReasonAttributes; }; -function AvatarSkeleton({size = CONST.AVATAR_SIZE.SMALL, reasonAttributes}: AvatarSkeletonProps) { +function AvatarSkeleton({size = CONST.AVATAR_SIZE.X_SMALL, reasonAttributes}: AvatarSkeletonProps) { const theme = useTheme(); useSkeletonSpan('AvatarSkeleton', reasonAttributes); const StyleUtils = useStyleUtils(); diff --git a/src/components/AvatarWithIndicator.tsx b/src/components/AvatarWithIndicator.tsx index 543627335c69..fd06c0d2e7da 100644 --- a/src/components/AvatarWithIndicator.tsx +++ b/src/components/AvatarWithIndicator.tsx @@ -39,7 +39,7 @@ function AvatarWithIndicator({source, accountID, tooltipText = '', fallbackIcon, ) : ( <> ; + + /** When omitted, inferred from icons count: 1 icon -> Single, 2+ icons -> Diagonal */ + avatarType?: ValueOf; + + /** Whether to show the user-details tooltip on hover */ + shouldShowTooltip?: boolean; + + /** Whether to use mid subscript size for diagonal avatars */ + useMidSubscriptSize?: boolean; + + /** Style for secondary avatar container in diagonal layout */ + secondaryAvatarContainerStyle?: StyleProp; + + /** Border color for the subscript avatar */ + subscriptAvatarBorderColor?: ColorValue; + + /** Report ID for navigation */ + reportID?: string; + + /** Account ID for single avatar tooltip */ + accountID?: number; + + /** Delegate account ID for single avatar tooltip */ + delegateAccountID?: number; + + /** Single avatar container styles */ + singleAvatarContainerStyle?: ViewStyle[]; +}; + +/** Presentational component that renders the correct avatar layout primitive based on pre-computed icons[]. */ +function IconsAvatar({ + icons, + avatarType, + size, + shouldShowTooltip = true, + useMidSubscriptSize = false, + secondaryAvatarContainerStyle, + subscriptAvatarBorderColor, + reportID, + accountID, + delegateAccountID, + singleAvatarContainerStyle, +}: IconsAvatarProps) { + const primaryIcon = icons.at(0); + + if (!primaryIcon) { + return null; + } + + const secondaryIcon = icons.at(1); + const resolvedType = avatarType ?? (icons.length >= 2 ? CONST.REPORT_ACTION_AVATARS.TYPE.MULTIPLE_DIAGONAL : CONST.REPORT_ACTION_AVATARS.TYPE.SINGLE); + + if (resolvedType === CONST.REPORT_ACTION_AVATARS.TYPE.SUBSCRIPT && secondaryIcon) { + return ( + + ); + } + + if (resolvedType === CONST.REPORT_ACTION_AVATARS.TYPE.MULTIPLE_DIAGONAL && secondaryIcon) { + return ( + + ); + } + + return ( + + ); +} + +export default IconsAvatar; diff --git a/src/components/Avatars/Primitives/DiagonalAvatars.tsx b/src/components/Avatars/Primitives/DiagonalAvatars.tsx new file mode 100644 index 000000000000..045222ff8a25 --- /dev/null +++ b/src/components/Avatars/Primitives/DiagonalAvatars.tsx @@ -0,0 +1,182 @@ +import React from 'react'; +import type {ImageStyle, StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; +import {WorkspaceBuilding} from '@components/Icon/WorkspaceDefaultAvatars'; +import Text from '@components/Text'; +import Tooltip from '@components/Tooltip'; +import UserDetailsTooltip from '@components/UserDetailsTooltip'; +import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {getUserDetailTooltipText} from '@libs/ReportUtils'; +import CONST from '@src/CONST'; +import ProfileAvatar from './ProfileAvatar'; +import type {MultipleAvatarsProps} from './types'; + +type AvatarStyles = { + singleAvatarStyle: ViewStyle & ImageStyle; + secondAvatarStyles: ViewStyle & ImageStyle; +}; + +type AvatarSizeToStyles = typeof CONST.AVATAR_SIZE.X_SMALL | typeof CONST.AVATAR_SIZE.XXX_LARGE | typeof CONST.AVATAR_SIZE.XXXXX_LARGE | typeof CONST.AVATAR_SIZE.DEFAULT; + +type AvatarSizeToStylesMap = Record; + +type DiagonalAvatarsProps = MultipleAvatarsProps & { + /** Whether to use the mid-subscript size for the avatars */ + useMidSubscriptSize: boolean; + + /** Style for the secondary avatar container */ + secondaryAvatarContainerStyle?: StyleProp; + + /** Whether the avatars are hovered */ + isHovered?: boolean; +}; + +function DiagonalAvatars({ + size, + shouldShowTooltip, + icons, + isInReportAction, + useMidSubscriptSize, + secondaryAvatarContainerStyle, + isHovered = false, + useProfileNavigationWrapper, + fallbackDisplayName, + reportID, +}: DiagonalAvatarsProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {formatPhoneNumber} = useLocalize(); + + const primaryIcon = icons.at(0); + const secondaryIcon = icons.at(1); + + const tooltipTexts = shouldShowTooltip ? icons.map((icon) => getUserDetailTooltipText(Number(icon.id), formatPhoneNumber, icon.name)) : ['']; + const removeRightMargin = icons.length === 2 && size === CONST.AVATAR_SIZE.XXXXX_LARGE; + const avatarContainerStyles = StyleUtils.getContainerStyles(size, isInReportAction); + + const avatarSizeToStylesMap: AvatarSizeToStylesMap = { + [CONST.AVATAR_SIZE.X_SMALL]: { + singleAvatarStyle: styles.singleAvatarXxxxSmall, + secondAvatarStyles: styles.secondAvatarXxxxSmall, + }, + [CONST.AVATAR_SIZE.XXX_LARGE]: { + singleAvatarStyle: styles.singleAvatarXLarge, + secondAvatarStyles: styles.secondAvatarXLarge, + }, + [CONST.AVATAR_SIZE.XXXXX_LARGE]: { + singleAvatarStyle: styles.singleAvatarXxLarge, + secondAvatarStyles: styles.secondAvatarXxLarge, + }, + [CONST.AVATAR_SIZE.DEFAULT]: { + singleAvatarStyle: styles.singleAvatarXxSmall, + secondAvatarStyles: styles.secondAvatarXxSmall, + }, + }; + + let avatarSize; + if (useMidSubscriptSize) { + avatarSize = CONST.AVATAR_SIZE.XXXX_SMALL; + } else if (size === CONST.AVATAR_SIZE.XXX_LARGE) { + avatarSize = CONST.AVATAR_SIZE.X_LARGE; + } else if (size === CONST.AVATAR_SIZE.XXXXX_LARGE) { + avatarSize = CONST.AVATAR_SIZE.XX_LARGE; + } else { + avatarSize = CONST.AVATAR_SIZE.XX_SMALL; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + const {singleAvatarStyle, secondAvatarStyles} = avatarSizeToStylesMap[size as AvatarSizeToStyles] ?? avatarSizeToStylesMap[CONST.AVATAR_SIZE.DEFAULT]; + const secondaryAvatarContainerStyles = secondaryAvatarContainerStyle ?? [StyleUtils.getBackgroundAndBorderStyle(isHovered ? theme.activeComponentBG : theme.componentBG)]; + + return ( + + + + {/* View is necessary for tooltip to show for multiple avatars in LHN */} + + + + + + {icons.length === 2 ? ( + + + + + + ) : ( + + + + {`+${icons.length - 1}`} + + + + )} + + + + ); +} + +export default DiagonalAvatars; diff --git a/src/components/Avatars/Primitives/HorizontalAvatars.tsx b/src/components/Avatars/Primitives/HorizontalAvatars.tsx new file mode 100644 index 000000000000..0fdfae11229c --- /dev/null +++ b/src/components/Avatars/Primitives/HorizontalAvatars.tsx @@ -0,0 +1,146 @@ +import React from 'react'; +import {View} from 'react-native'; +import {WorkspaceBuilding} from '@components/Icon/WorkspaceDefaultAvatars'; +import Text from '@components/Text'; +import Tooltip from '@components/Tooltip'; +import UserDetailsTooltip from '@components/UserDetailsTooltip'; +import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {getUserDetailTooltipText} from '@libs/ReportUtils'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import ProfileAvatar from './ProfileAvatar'; +import type {MultipleAvatarsProps} from './types'; + +type HorizontalStackingOptions = Partial<{ + displayInRows: boolean; + isHovered: boolean; + isActive: boolean; + isPressed: boolean; + overlapDivider: number; + maxAvatarsInRow: number; + useCardBG: boolean; +}>; + +type HorizontalAvatarsProps = HorizontalStackingOptions & MultipleAvatarsProps; + +function HorizontalAvatars({ + isHovered = false, + isActive = false, + isPressed = false, + maxAvatarsInRow = CONST.AVATAR_ROW_SIZE.DEFAULT, + displayInRows: shouldDisplayAvatarsInRows = false, + useCardBG: shouldUseCardBackground = false, + overlapDivider = 3, + size, + shouldShowTooltip, + icons, + isInReportAction, + useProfileNavigationWrapper, + fallbackDisplayName, + reportID, +}: HorizontalAvatarsProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {formatPhoneNumber} = useLocalize(); + + const oneAvatarSize = StyleUtils.getAvatarStyle(size); + const overlapSize = oneAvatarSize.width / overlapDivider; + const oneAvatarBorderWidth = StyleUtils.getAvatarBorderWidth(size).borderWidth ?? 0; + const height = oneAvatarSize.height + 2 * oneAvatarBorderWidth; + const avatarContainerStyles = StyleUtils.combineStyles([styles.alignItemsCenter, styles.flexRow, StyleUtils.getHeight(height)]); + + let avatarRows; + if (!shouldDisplayAvatarsInRows || icons.length <= maxAvatarsInRow) { + avatarRows = [icons]; + } else { + const rowSize = Math.min(Math.ceil(icons.length / 2), maxAvatarsInRow); + avatarRows = [icons.slice(0, rowSize), icons.slice(rowSize)]; + } + + const tooltipTexts = shouldShowTooltip ? icons.map((icon) => getUserDetailTooltipText(Number(icon.id), formatPhoneNumber, icon.name)) : ['']; + + return avatarRows.map((avatars, rowIndex) => ( + + {[...avatars].splice(0, maxAvatarsInRow).map((icon, index) => ( + + + + + + ))} + {avatars.length > maxAvatarsInRow && ( + + + + {`+${avatars.length - maxAvatarsInRow}`} + + + + )} + + )); +} + +export default HorizontalAvatars; +export type {HorizontalStackingOptions}; diff --git a/src/components/Avatars/Primitives/ProfileAvatar.tsx b/src/components/Avatars/Primitives/ProfileAvatar.tsx new file mode 100644 index 000000000000..a362a9ad2fc1 --- /dev/null +++ b/src/components/Avatars/Primitives/ProfileAvatar.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import type {UpperCaseCharacters} from 'type-fest/source/internal'; +import Avatar from '@components/Avatar'; +import PressableWithoutFocus from '@components/Pressable/PressableWithoutFocus'; +import useLocalize from '@hooks/useLocalize'; +import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; +import Navigation from '@navigation/Navigation'; +import CONST from '@src/CONST'; +import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES'; + +type ProfileAvatarProps = Parameters[0] & { + /** Whether clicking the avatar navigates to the profile/workspace page */ + useProfileNavigationWrapper?: boolean; + + /** Report ID used for avatar navigation */ + reportID?: string; +}; + +/** `ProfileAvatar` wraps an `Avatar` in a pressable that navigates to the correct "view avatar" route. + * The branch it picks depends on `type` (workspace vs user) and whether a `reportID` is provided. + */ +function ProfileAvatar(props: ProfileAvatarProps) { + const {translate} = useLocalize(); + const {avatarID, useProfileNavigationWrapper, type, name, reportID} = props; + + if (!useProfileNavigationWrapper) { + return ; + } + + const isWorkspace = type === CONST.ICON_TYPE_WORKSPACE; + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + const firstLetter = (name?.at(0) ?? 'A').toUpperCase() as UpperCaseCharacters; + + const onPress = () => { + if (isWorkspace) { + if (reportID) { + Navigation.navigate(ROUTES.REPORT_AVATAR.getRoute(reportID, String(avatarID))); + return; + } + return Navigation.navigate(ROUTES.WORKSPACE_AVATAR.getRoute(String(avatarID), firstLetter)); + } + + return Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.PROFILE_AVATAR.getRoute(Number(avatarID)))); + }; + + return ( + + + + ); +} + +export default ProfileAvatar; diff --git a/src/components/Avatars/Primitives/SingleAvatar.tsx b/src/components/Avatars/Primitives/SingleAvatar.tsx new file mode 100644 index 000000000000..921f6bd26834 --- /dev/null +++ b/src/components/Avatars/Primitives/SingleAvatar.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; +import UserDetailsTooltip from '@components/UserDetailsTooltip'; +import useStyleUtils from '@hooks/useStyleUtils'; +import type {AvatarSource} from '@libs/UserAvatarUtils'; +import CONST from '@src/CONST'; +import type {Icon as IconType} from '@src/types/onyx/OnyxCommon'; +import ProfileAvatar from './ProfileAvatar'; +import type {BaseAvatarProps} from './types'; + +type SingleAvatarProps = BaseAvatarProps & { + /** The resolved avatar icon to render */ + avatar: IconType | undefined; + + /** Container styles for the avatar */ + containerStyles?: StyleProp; + + /** Account ID used for the tooltip */ + accountID: number; + + /** Delegate account ID used for the tooltip */ + delegateAccountID?: number; + + /** Icon shown when the avatar source fails to load */ + fallbackIcon?: AvatarSource; + + /** Whether the avatar is displayed within a report action */ + isInReportAction?: boolean; +}; + +function SingleAvatar({ + avatar, + size, + containerStyles, + shouldShowTooltip, + delegateAccountID, + accountID, + fallbackIcon, + isInReportAction, + useProfileNavigationWrapper, + fallbackDisplayName, + reportID, +}: SingleAvatarProps) { + const StyleUtils = useStyleUtils(); + const avatarContainerStyles = StyleUtils.getContainerStyles(size, isInReportAction); + + return ( + + + + + + ); +} + +export default SingleAvatar; diff --git a/src/components/Avatars/Primitives/SubscriptAvatar.tsx b/src/components/Avatars/Primitives/SubscriptAvatar.tsx new file mode 100644 index 000000000000..5764d8e910fc --- /dev/null +++ b/src/components/Avatars/Primitives/SubscriptAvatar.tsx @@ -0,0 +1,157 @@ +import React from 'react'; +import type {ColorValue} from 'react-native'; +import {View} from 'react-native'; +import Icon from '@components/Icon'; +import UserDetailsTooltip from '@components/UserDetailsTooltip'; +import {useCompanyCardFeedIcons} from '@hooks/useCompanyCardIcons'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; +import useThemeIllustrations from '@hooks/useThemeIllustrations'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {getCardFeedIcon} from '@libs/CardUtils'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import type {CardFeed} from '@src/types/onyx/CardFeeds'; +import type {Icon as IconType} from '@src/types/onyx/OnyxCommon'; +import ProfileAvatar from './ProfileAvatar'; +import type {BaseAvatarProps} from './types'; + +type SubscriptAvatarProps = BaseAvatarProps & { + /** The primary (main) avatar icon */ + primaryAvatar: IconType; + + /** The secondary (subscript) avatar icon */ + secondaryAvatar?: IconType; + + /** Whether to remove the right margin on the container */ + noRightMarginOnContainer?: boolean; + + /** Border color for the subscript avatar */ + subscriptAvatarBorderColor?: ColorValue; + + /** Card feed to display as the subscript instead of the secondary avatar */ + subscriptCardFeed?: CardFeed; + + /** Size of the subscript card feed icon */ + subscriptCardFeedIconSize?: {width: number; height: number}; +}; + +function SubscriptAvatar({ + primaryAvatar, + secondaryAvatar, + size, + shouldShowTooltip, + noRightMarginOnContainer, + subscriptAvatarBorderColor, + subscriptCardFeed, + fallbackDisplayName, + useProfileNavigationWrapper, + reportID, + subscriptCardFeedIconSize = { + width: variables.cardAvatarWidth, + height: variables.cardAvatarHeight, + }, +}: SubscriptAvatarProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const illustrations = useThemeIllustrations(); + const companyCardFeedIcons = useCompanyCardFeedIcons(); + + const isSmall = size === CONST.AVATAR_SIZE.X_SMALL; + const containerStyle = StyleUtils.getContainerStyles(size); + + let subscriptAvatarStyle; + if (size === CONST.AVATAR_SIZE.X_SMALL) { + subscriptAvatarStyle = styles.secondAvatarSubscriptXSmall; + } else if (size === CONST.AVATAR_SIZE.SMALL) { + subscriptAvatarStyle = styles.secondAvatarSubscriptSmall; + } else if (size === CONST.AVATAR_SIZE.XXXXX_LARGE) { + subscriptAvatarStyle = styles.secondAvatarSubscriptXxxxxLarge; + } else { + subscriptAvatarStyle = styles.secondAvatarSubscript; + } + + const subscriptAvatarSize = size === CONST.AVATAR_SIZE.XXXXX_LARGE ? CONST.AVATAR_SIZE.MEDIUM : CONST.AVATAR_SIZE.XXX_SMALL; + + return ( + + + + + + + {!!secondaryAvatar && !subscriptCardFeed && ( + + + + + + )} + {!!subscriptCardFeed && ( + + + + )} + + ); +} + +export default SubscriptAvatar; diff --git a/src/components/Avatars/Primitives/types.ts b/src/components/Avatars/Primitives/types.ts new file mode 100644 index 000000000000..e0e3a56f28dc --- /dev/null +++ b/src/components/Avatars/Primitives/types.ts @@ -0,0 +1,32 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; +import type {Icon as IconType} from '@src/types/onyx/OnyxCommon'; + +/** Props shared by every avatar layout primitive */ +type BaseAvatarProps = { + /** Size of the avatar(s) to render */ + size: ValueOf; + + /** Whether to show the user-details tooltip on hover */ + shouldShowTooltip: boolean; + + /** Display name used as a fallback for the avatar tooltip */ + fallbackDisplayName?: string; + + /** Whether clicking the avatar navigates to the profile/workspace page */ + useProfileNavigationWrapper?: boolean; + + /** Report ID used for avatar navigation */ + reportID?: string; +}; + +/** Props shared by the multi-avatar primitives (diagonal and horizontal stacks) */ +type MultipleAvatarsProps = BaseAvatarProps & { + /** Resolved avatar icons to render */ + icons: IconType[]; + + /** Whether the avatars are displayed within a report action */ + isInReportAction: boolean; +}; + +export type {BaseAvatarProps, MultipleAvatarsProps}; diff --git a/src/components/ColoredLetterAvatar.tsx b/src/components/ColoredLetterAvatar.tsx index 38ab111caac0..1951b47b1e22 100644 --- a/src/components/ColoredLetterAvatar.tsx +++ b/src/components/ColoredLetterAvatar.tsx @@ -21,7 +21,7 @@ type ColoredLetterAvatarProps = { * ColoredLetterAvatar renders an SVG component with a colored circular background. * Used for letter avatars and other colored icon avatars. */ -function ColoredLetterAvatar({component, backgroundColor, fillColor, size = CONST.AVATAR_SIZE.MEDIUM}: ColoredLetterAvatarProps) { +function ColoredLetterAvatar({component, backgroundColor, fillColor, size = CONST.AVATAR_SIZE.X_LARGE}: ColoredLetterAvatarProps) { const StyleUtils = useStyleUtils(); const avatarSize = StyleUtils.getAvatarSize(size); return ( diff --git a/src/components/ImportedFromAccountingSoftware.tsx b/src/components/ImportedFromAccountingSoftware.tsx index 8fabe6997d5a..460279bab46f 100644 --- a/src/components/ImportedFromAccountingSoftware.tsx +++ b/src/components/ImportedFromAccountingSoftware.tsx @@ -64,7 +64,7 @@ function ImportedFromAccountingSoftware({policyID, currentConnectionName, transl src={icon} height={variables.iconSizeMedium} width={variables.iconSizeMedium} - additionalStyles={[StyleUtils.getAvatarBorderStyle(CONST.AVATAR_SIZE.SMALLER, ''), styles.appBG]} + additionalStyles={[StyleUtils.getAvatarBorderStyle(CONST.AVATAR_SIZE.XX_SMALL, ''), styles.appBG]} /> ) : undefined } diff --git a/src/components/LHNOptionsList/LHNAvatar.tsx b/src/components/LHNOptionsList/LHNAvatar.tsx deleted file mode 100644 index dead867335f1..000000000000 --- a/src/components/LHNOptionsList/LHNAvatar.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import React from 'react'; -import type {ColorValue, ViewStyle} from 'react-native'; -import type {ValueOf} from 'type-fest'; -import ReportActionAvatar from '@components/ReportActionAvatars/ReportActionAvatar'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useTheme from '@hooks/useTheme'; -import CONST from '@src/CONST'; -import type {Icon} from '@src/types/onyx/OnyxCommon'; - -type LHNAvatarProps = { - icons: Icon[]; - shouldShowSubscript: boolean; - size: ValueOf; - secondaryAvatarBackgroundColor?: ColorValue; - singleAvatarContainerStyle?: ViewStyle[]; - subscriptAvatarBorderColor?: ColorValue; - useMidSubscriptSize?: boolean; - shouldShowTooltip?: boolean; - delegateAccountID?: number; - delegateTooltipAccountID?: number; -}; - -/** - * Lightweight avatar component for LHN rows. Reuses ReportActionAvatar sub-components - * with pre-computed icons, avoiding the heavy hooks in ReportActionAvatars. - */ -function LHNAvatar({ - icons, - shouldShowSubscript, - size, - secondaryAvatarBackgroundColor, - singleAvatarContainerStyle, - subscriptAvatarBorderColor, - useMidSubscriptSize, - shouldShowTooltip = false, - delegateAccountID, - delegateTooltipAccountID, -}: LHNAvatarProps) { - const theme = useTheme(); - const StyleUtils = useStyleUtils(); - - const primaryIcon = icons.at(0); - const secondaryIcon = icons.at(1); - - if (!primaryIcon) { - return null; - } - - if (icons.length === 1 || !secondaryIcon) { - return ( - - ); - } - - if (shouldShowSubscript) { - return ( - - ); - } - - const secondaryAvatarContainerStyle = secondaryAvatarBackgroundColor ? StyleUtils.getBackgroundAndBorderStyle(secondaryAvatarBackgroundColor) : undefined; - - return ( - - ); -} - -export default LHNAvatar; diff --git a/src/components/LHNOptionsList/OptionRowLHN/OptionRow/Avatar.tsx b/src/components/LHNOptionsList/OptionRowLHN/OptionRow/Avatar.tsx index a92e3c869e7c..3ceda26cd35d 100644 --- a/src/components/LHNOptionsList/OptionRowLHN/OptionRow/Avatar.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN/OptionRow/Avatar.tsx @@ -1,8 +1,9 @@ import React from 'react'; import type {ColorValue} from 'react-native'; import type {ValueOf} from 'type-fest'; -import LHNAvatar from '@components/LHNOptionsList/LHNAvatar'; +import IconsAvatar from '@components/Avatars/IconsAvatar'; import {usePersonalDetails} from '@components/OnyxListItemProvider'; +import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import {shouldOptionShowTooltip} from '@libs/OptionsListUtils'; import {getDelegateAccountIDFromReportAction} from '@libs/ReportActionsUtils'; @@ -24,6 +25,7 @@ type AvatarProps = { function AvatarInner({optionItem, viewMode, avatarBackgroundColor}: AvatarProps) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const personalDetails = usePersonalDetails(); const isInFocusMode = viewMode === CONST.OPTION_MODE.COMPACT; @@ -60,17 +62,17 @@ function AvatarInner({optionItem, viewMode, avatarBackgroundColor}: AvatarProps) } return ( - ); } diff --git a/src/components/MentionSuggestions.tsx b/src/components/MentionSuggestions.tsx index c599a0623cf2..76fd429318c4 100644 --- a/src/components/MentionSuggestions.tsx +++ b/src/components/MentionSuggestions.tsx @@ -92,7 +92,7 @@ function MentionSuggestions({ ( [ @@ -1127,7 +1127,7 @@ function MenuItem({ subscriptAvatarBorderColor={isHovered ? theme.activeComponentBG : theme.componentBG} singleAvatarContainerStyle={[styles.actionAvatar, styles.mr2]} reportID={rightIconReportID} - size={CONST.AVATAR_SIZE.SMALL} + size={CONST.AVATAR_SIZE.X_SMALL} horizontalStacking={ shouldStackHorizontally && { isHovered, diff --git a/src/components/OnboardingHelpDropdownButton.tsx b/src/components/OnboardingHelpDropdownButton.tsx index f46882e571db..dcd7c120f0a6 100644 --- a/src/components/OnboardingHelpDropdownButton.tsx +++ b/src/components/OnboardingHelpDropdownButton.tsx @@ -69,7 +69,7 @@ function OnboardingHelpDropdownButton({reportID, shouldUseNarrowLayout, shouldSh }, }); } - + console.log('hasActiveScheduledCall', hasActiveScheduledCall, latestScheduledCall); if (hasActiveScheduledCall && latestScheduledCall) { options.push({ text: `${DateUtils.formatInTimeZoneWithFallback(latestScheduledCall.eventTime, userTimezone, CONST.DATE.WEEKDAY_TIME_FORMAT)}, ${DateUtils.formatInTimeZoneWithFallback( @@ -86,12 +86,12 @@ function OnboardingHelpDropdownButton({reportID, shouldUseNarrowLayout, shouldSh descriptionTextStyle: [styles.themeTextColor, styles.ml2], displayInDefaultIconColor: true, icon: illustrations.HeadSet, - iconWidth: variables.avatarSizeLargeNormal, - iconHeight: variables.avatarSizeLargeNormal, + iconWidth: variables.avatarSizeLarge, + iconHeight: variables.avatarSizeLarge, wrapperStyle: [styles.mb3, styles.pl4, styles.pr5, styles.pt3, styles.pb6, styles.borderBottom], interactive: false, titleStyle: styles.ml2, - avatarSize: CONST.AVATAR_SIZE.LARGE_NORMAL, + avatarSize: CONST.AVATAR_SIZE.LARGE, }); options.push({ text: translate('common.reschedule'), diff --git a/src/components/ReceiptEmptyState.tsx b/src/components/ReceiptEmptyState.tsx index 8986ddcdcd39..a627a3fe974c 100644 --- a/src/components/ReceiptEmptyState.tsx +++ b/src/components/ReceiptEmptyState.tsx @@ -133,11 +133,11 @@ function ReceiptEmptyState({ height={variables.eReceiptEmptyIconWidth} /> {!isThumbnail && ( - + )} diff --git a/src/components/ReportActionAvatars/ReportActionAvatar.tsx b/src/components/ReportActionAvatars/ReportActionAvatar.tsx deleted file mode 100644 index b207033223e8..000000000000 --- a/src/components/ReportActionAvatars/ReportActionAvatar.tsx +++ /dev/null @@ -1,628 +0,0 @@ -import lodashSortBy from 'lodash/sortBy'; -import React, {useMemo} from 'react'; -import type {ColorValue, ImageStyle, StyleProp, ViewStyle} from 'react-native'; -import {View} from 'react-native'; -import type {ValueOf} from 'type-fest'; -import type {UpperCaseCharacters} from 'type-fest/source/internal'; -import Avatar from '@components/Avatar'; -import Icon from '@components/Icon'; -import {WorkspaceBuilding} from '@components/Icon/WorkspaceDefaultAvatars'; -import PressableWithoutFocus from '@components/Pressable/PressableWithoutFocus'; -import Text from '@components/Text'; -import Tooltip from '@components/Tooltip'; -import UserDetailsTooltip from '@components/UserDetailsTooltip'; -import {useCompanyCardFeedIcons} from '@hooks/useCompanyCardIcons'; -import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useTheme from '@hooks/useTheme'; -import useThemeIllustrations from '@hooks/useThemeIllustrations'; -import useThemeStyles from '@hooks/useThemeStyles'; -import {getCardFeedIcon} from '@libs/CardUtils'; -import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; -import {getUserDetailTooltipText, sortIconsByName} from '@libs/ReportUtils'; -import type {AvatarSource} from '@libs/UserAvatarUtils'; -import Navigation from '@navigation/Navigation'; -import variables from '@styles/variables'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES'; -import type {CardFeed} from '@src/types/onyx/CardFeeds'; -import type {Icon as IconType} from '@src/types/onyx/OnyxCommon'; - -type SortingOptions = ValueOf; - -type HorizontalStacking = Partial<{ - /** Prop to identify if we should display avatars in rows */ - displayInRows: boolean; - - /** Whether the avatars are hovered */ - isHovered: boolean; - - /** Whether the avatars are active */ - isActive: boolean; - - /** Whether the avatars are in an element being pressed */ - isPressed: boolean; - - /** Prop to limit the amount of avatars displayed horizontally */ - overlapDivider: number; - - /** Prop to limit the amount of avatars displayed horizontally */ - maxAvatarsInRow: number; - - /** Whether avatars are displayed with the highlighted background color instead of the app background color. This is primarily the case for IOU previews. */ - useCardBG: boolean; - - /** Prop to sort the avatars */ - sort: SortingOptions | SortingOptions[]; -}>; - -type AvatarStyles = { - singleAvatarStyle: ViewStyle & ImageStyle; - secondAvatarStyles: ViewStyle & ImageStyle; -}; - -type AvatarSizeToStyles = typeof CONST.AVATAR_SIZE.SMALL | typeof CONST.AVATAR_SIZE.LARGE | typeof CONST.AVATAR_SIZE.DEFAULT; - -type AvatarSizeToStylesMap = Record; - -function ProfileAvatar(props: Parameters[0] & {useProfileNavigationWrapper?: boolean; reportID?: string}) { - const {translate} = useLocalize(); - const {avatarID, useProfileNavigationWrapper, type, name, reportID} = props; - - if (!useProfileNavigationWrapper) { - return ; - } - - const isWorkspace = type === CONST.ICON_TYPE_WORKSPACE; - const firstLetter = (name?.at(0) ?? 'A').toUpperCase() as UpperCaseCharacters; - - const onPress = () => { - if (isWorkspace) { - if (reportID) { - Navigation.navigate(ROUTES.REPORT_AVATAR.getRoute(reportID, String(avatarID))); - return; - } - return Navigation.navigate(ROUTES.WORKSPACE_AVATAR.getRoute(String(avatarID), firstLetter)); - } - - return Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.PROFILE_AVATAR.getRoute(Number(avatarID)))); - }; - - return ( - - - - ); -} - -function ReportActionAvatarSingle({ - avatar, - size, - containerStyles, - shouldShowTooltip, - delegateAccountID, - accountID, - fallbackIcon, - isInReportAction, - useProfileNavigationWrapper, - fallbackDisplayName, - reportID, -}: { - avatar: IconType | undefined; - size: ValueOf; - containerStyles?: StyleProp; - shouldShowTooltip: boolean; - accountID: number; - delegateAccountID?: number; - fallbackIcon?: AvatarSource; - isInReportAction?: boolean; - useProfileNavigationWrapper?: boolean; - fallbackDisplayName?: string; - reportID?: string; -}) { - const StyleUtils = useStyleUtils(); - const avatarContainerStyles = StyleUtils.getContainerStyles(size, isInReportAction); - - return ( - - - - - - ); -} - -function ReportActionAvatarSubscript({ - primaryAvatar, - secondaryAvatar, - size, - shouldShowTooltip, - noRightMarginOnContainer, - subscriptAvatarBorderColor, - subscriptCardFeed, - fallbackDisplayName, - useProfileNavigationWrapper, - reportID, - subscriptCardFeedIconSize = { - width: variables.cardAvatarWidth, - height: variables.cardAvatarHeight, - }, -}: { - primaryAvatar: IconType; - secondaryAvatar: IconType; - size: ValueOf; - shouldShowTooltip: boolean; - noRightMarginOnContainer?: boolean; - subscriptAvatarBorderColor?: ColorValue; - subscriptCardFeed?: CardFeed; - subscriptCardFeedIconSize?: {width: number; height: number}; - fallbackDisplayName?: string; - useProfileNavigationWrapper?: boolean; - reportID?: string; -}) { - const theme = useTheme(); - const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); - const illustrations = useThemeIllustrations(); - const companyCardFeedIcons = useCompanyCardFeedIcons(); - - const isSmall = size === CONST.AVATAR_SIZE.SMALL; - const containerStyle = StyleUtils.getContainerStyles(size); - - const subscriptAvatarStyle = useMemo(() => { - if (size === CONST.AVATAR_SIZE.SMALL) { - return styles.secondAvatarSubscriptCompact; - } - - if (size === CONST.AVATAR_SIZE.SMALL_NORMAL) { - return styles.secondAvatarSubscriptSmallNormal; - } - - if (size === CONST.AVATAR_SIZE.X_LARGE) { - return styles.secondAvatarSubscriptXLarge; - } - - return styles.secondAvatarSubscript; - }, [size, styles]); - - const subscriptAvatarSize = size === CONST.AVATAR_SIZE.X_LARGE ? CONST.AVATAR_SIZE.HEADER : CONST.AVATAR_SIZE.SUBSCRIPT; - - return ( - - - - - - - {!!secondaryAvatar && !subscriptCardFeed && ( - - - - - - )} - {!!subscriptCardFeed && ( - - - - )} - - ); -} - -function ReportActionAvatarMultipleHorizontal({ - isHovered = false, - isActive = false, - isPressed = false, - maxAvatarsInRow = CONST.AVATAR_ROW_SIZE.DEFAULT, - displayInRows: shouldDisplayAvatarsInRows = false, - useCardBG: shouldUseCardBackground = false, - overlapDivider = 3, - size, - shouldShowTooltip, - icons: unsortedIcons, - isInReportAction, - sort: sortAvatars, - useProfileNavigationWrapper, - fallbackDisplayName, - reportID, -}: HorizontalStacking & { - size: ValueOf; - shouldShowTooltip: boolean; - icons: IconType[]; - isInReportAction: boolean; - fallbackDisplayName?: string; - useProfileNavigationWrapper?: boolean; - reportID?: string; -}) { - const theme = useTheme(); - const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); - const {localeCompare, formatPhoneNumber} = useLocalize(); - - const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); - - const oneAvatarSize = StyleUtils.getAvatarStyle(size); - const overlapSize = oneAvatarSize.width / overlapDivider; - const oneAvatarBorderWidth = StyleUtils.getAvatarBorderWidth(size).borderWidth ?? 0; - const height = oneAvatarSize.height + 2 * oneAvatarBorderWidth; - const avatarContainerStyles = StyleUtils.combineStyles([styles.alignItemsCenter, styles.flexRow, StyleUtils.getHeight(height)]); - - const icons = useMemo(() => { - let avatars: IconType[] = unsortedIcons; - - if (sortAvatars?.includes(CONST.REPORT_ACTION_AVATARS.SORT_BY.NAME)) { - avatars = sortIconsByName(unsortedIcons, personalDetails, localeCompare); - } else if (sortAvatars?.includes(CONST.REPORT_ACTION_AVATARS.SORT_BY.ID)) { - avatars = lodashSortBy(unsortedIcons, (icon) => icon.id); - } - - return sortAvatars?.includes(CONST.REPORT_ACTION_AVATARS.SORT_BY.REVERSE) ? avatars.reverse() : avatars; - }, [unsortedIcons, personalDetails, sortAvatars, localeCompare]); - - const avatarRows = useMemo(() => { - // If we're not displaying avatars in rows or the number of icons is less than or equal to the max avatars in a row, return a single row - if (!shouldDisplayAvatarsInRows || icons.length <= maxAvatarsInRow) { - return [icons]; - } - - // Calculate the size of each row - const rowSize = Math.min(Math.ceil(icons.length / 2), maxAvatarsInRow); - - // Slice the icons array into two rows - const firstRow = icons.slice(0, rowSize); - const secondRow = icons.slice(rowSize); - - // Update the state with the two rows as an array - return [firstRow, secondRow]; - }, [icons, maxAvatarsInRow, shouldDisplayAvatarsInRows]); - - const tooltipTexts = useMemo( - () => (shouldShowTooltip ? icons.map((icon) => getUserDetailTooltipText(Number(icon.id), formatPhoneNumber, icon.name)) : ['']), - [shouldShowTooltip, icons, formatPhoneNumber], - ); - - return avatarRows.map((avatars, rowIndex) => ( - - {[...avatars].splice(0, maxAvatarsInRow).map((icon, index) => ( - - - - - - ))} - {avatars.length > maxAvatarsInRow && ( - - - - {`+${avatars.length - maxAvatarsInRow}`} - - - - )} - - )); -} - -function ReportActionAvatarMultipleDiagonal({ - size, - shouldShowTooltip, - icons, - isInReportAction, - useMidSubscriptSize, - secondaryAvatarContainerStyle, - isHovered = false, - useProfileNavigationWrapper, - fallbackDisplayName, - reportID, -}: { - size: ValueOf; - shouldShowTooltip: boolean; - icons: IconType[]; - isInReportAction: boolean; - useMidSubscriptSize: boolean; - secondaryAvatarContainerStyle?: StyleProp; - isHovered?: boolean; - useProfileNavigationWrapper?: boolean; - fallbackDisplayName?: string; - reportID?: string; -}) { - const theme = useTheme(); - const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); - const {formatPhoneNumber} = useLocalize(); - - const tooltipTexts = useMemo( - () => (shouldShowTooltip ? icons.map((icon) => getUserDetailTooltipText(Number(icon.id), formatPhoneNumber, icon.name)) : ['']), - [shouldShowTooltip, icons, formatPhoneNumber], - ); - const removeRightMargin = icons.length === 2 && size === CONST.AVATAR_SIZE.X_LARGE; - const avatarContainerStyles = StyleUtils.getContainerStyles(size, isInReportAction); - - const avatarSizeToStylesMap: AvatarSizeToStylesMap = useMemo( - () => ({ - [CONST.AVATAR_SIZE.SMALL]: { - singleAvatarStyle: styles.singleAvatarSmall, - secondAvatarStyles: styles.secondAvatarSmall, - }, - [CONST.AVATAR_SIZE.LARGE]: { - singleAvatarStyle: styles.singleAvatarMedium, - secondAvatarStyles: styles.secondAvatarMedium, - }, - [CONST.AVATAR_SIZE.X_LARGE]: { - singleAvatarStyle: styles.singleAvatarMediumLarge, - secondAvatarStyles: styles.secondAvatarMediumLarge, - }, - [CONST.AVATAR_SIZE.DEFAULT]: { - singleAvatarStyle: styles.singleAvatar, - secondAvatarStyles: styles.secondAvatar, - }, - }), - [styles], - ); - - const avatarSize = useMemo(() => { - if (useMidSubscriptSize) { - return CONST.AVATAR_SIZE.MID_SUBSCRIPT; - } - - if (size === CONST.AVATAR_SIZE.LARGE) { - return CONST.AVATAR_SIZE.MEDIUM; - } - - if (size === CONST.AVATAR_SIZE.X_LARGE) { - return CONST.AVATAR_SIZE.MEDIUM_LARGE; - } - - return CONST.AVATAR_SIZE.SMALLER; - }, [useMidSubscriptSize, size]); - - const {singleAvatarStyle, secondAvatarStyles} = useMemo(() => avatarSizeToStylesMap[size as AvatarSizeToStyles] ?? avatarSizeToStylesMap.default, [size, avatarSizeToStylesMap]); - const secondaryAvatarContainerStyles = secondaryAvatarContainerStyle ?? [StyleUtils.getBackgroundAndBorderStyle(isHovered ? theme.activeComponentBG : theme.componentBG)]; - - return ( - - - - {/* View is necessary for tooltip to show for multiple avatars in LHN */} - - - - - - {icons.length === 2 ? ( - - - - - - ) : ( - - - - {`+${icons.length - 1}`} - - - - )} - - - - ); -} - -/** - * This component should not be used outside ReportActionAvatars; its sole purpose is to render the ReportActionAvatars UI. - * To render user avatars, use ReportActionAvatars. - */ -export default { - Single: ReportActionAvatarSingle, - Subscript: ReportActionAvatarSubscript, - Multiple: { - Diagonal: ReportActionAvatarMultipleDiagonal, - Horizontal: ReportActionAvatarMultipleHorizontal, - }, -}; - -export type {HorizontalStacking}; diff --git a/src/components/ReportActionAvatars/SearchReportAvatar.tsx b/src/components/ReportActionAvatars/SearchReportAvatar.tsx deleted file mode 100644 index 9c1126ae2182..000000000000 --- a/src/components/ReportActionAvatars/SearchReportAvatar.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import React from 'react'; -import type {ColorValue} from 'react-native'; -import type {ValueOf} from 'type-fest'; -import CONST from '@src/CONST'; -import type {Icon} from '@src/types/onyx/OnyxCommon'; -import ReportActionAvatar from './ReportActionAvatar'; - -type SearchReportAvatarProps = { - primaryAvatar?: Icon; - secondaryAvatar?: Icon; - avatarType?: ValueOf; - shouldShowTooltip: boolean; - subscriptAvatarBorderColor: ColorValue; - reportID: string; - isLargeScreenWidth?: boolean; -}; - -function SearchReportAvatar({primaryAvatar, secondaryAvatar, avatarType, shouldShowTooltip, subscriptAvatarBorderColor, reportID, isLargeScreenWidth}: SearchReportAvatarProps) { - const avatarSize = isLargeScreenWidth ? CONST.AVATAR_SIZE.SMALL : CONST.AVATAR_SIZE.DEFAULT; - - if (!primaryAvatar) { - return null; - } - - if (avatarType === CONST.REPORT_ACTION_AVATARS.TYPE.SUBSCRIPT && secondaryAvatar) { - return ( - - ); - } - - if (avatarType === CONST.REPORT_ACTION_AVATARS.TYPE.MULTIPLE_DIAGONAL && secondaryAvatar) { - return ( - - ); - } - - return ( - - ); -} - -export default SearchReportAvatar; diff --git a/src/components/ReportActionAvatars/index.tsx b/src/components/ReportActionAvatars/index.tsx index 4b112f6a7bb5..4eb16c7610a9 100644 --- a/src/components/ReportActionAvatars/index.tsx +++ b/src/components/ReportActionAvatars/index.tsx @@ -1,16 +1,30 @@ +import lodashSortBy from 'lodash/sortBy'; import React from 'react'; import type {ColorValue, StyleProp, ViewStyle} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; +import DiagonalAvatars from '@components/Avatars/Primitives/DiagonalAvatars'; +import HorizontalAvatars from '@components/Avatars/Primitives/HorizontalAvatars'; +import type {HorizontalStackingOptions} from '@components/Avatars/Primitives/HorizontalAvatars'; +import SingleAvatar from '@components/Avatars/Primitives/SingleAvatar'; +import SubscriptAvatar from '@components/Avatars/Primitives/SubscriptAvatar'; +import {usePersonalDetails} from '@components/OnyxListItemProvider'; +import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import {sortIconsByName} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {InvitedEmailsToAccountIDs, Policy, Report, ReportAction} from '@src/types/onyx'; import type {CardFeed} from '@src/types/onyx/CardFeeds'; -import type {HorizontalStacking} from './ReportActionAvatar'; -import ReportActionAvatar from './ReportActionAvatar'; +import type {Icon as IconType} from '@src/types/onyx/OnyxCommon'; import useReportActionAvatars from './useReportActionAvatars'; +type SortingOptions = ValueOf; + +type HorizontalStacking = HorizontalStackingOptions & { + sort?: SortingOptions | SortingOptions[]; +}; + type ReportActionAvatarsProps = { horizontalStacking?: HorizontalStacking | boolean; @@ -116,6 +130,8 @@ function ReportActionAvatars({ shouldUseRealActor = false, }: ReportActionAvatarsProps) { const accountIDs = passedAccountIDs.filter((accountID) => accountID !== CONST.DEFAULT_NUMBER_ID); + const allPersonalDetails = usePersonalDetails(); + const {localeCompare} = useLocalize(); const reportID = potentialReportID ?? @@ -130,11 +146,11 @@ function ReportActionAvatars({ const shouldStackHorizontally = !!horizontalStacking; const isHorizontalStackingAnObject = shouldStackHorizontally && typeof horizontalStacking !== 'boolean'; - const {isHovered = false} = isHorizontalStackingAnObject ? horizontalStacking : {}; + const {isHovered = false, sort: sortAvatars, ...horizontalStackingRest} = isHorizontalStackingAnObject ? horizontalStacking : ({} as HorizontalStacking); const { avatarType: notPreciseAvatarType, - avatars: icons, + avatars: unsortedIcons, details: {delegateAccountID}, source, } = useReportActionAvatars({ @@ -152,6 +168,19 @@ function ReportActionAvatars({ shouldUseRealActor, }); + let icons: IconType[] = unsortedIcons; + if (sortAvatars) { + if (sortAvatars.includes(CONST.REPORT_ACTION_AVATARS.SORT_BY.NAME)) { + icons = sortIconsByName(unsortedIcons, allPersonalDetails, localeCompare); + } else if (sortAvatars.includes(CONST.REPORT_ACTION_AVATARS.SORT_BY.ID)) { + icons = lodashSortBy(unsortedIcons, (icon) => icon.id); + } + + if (sortAvatars.includes(CONST.REPORT_ACTION_AVATARS.SORT_BY.REVERSE)) { + icons = icons.reverse(); + } + } + let avatarType: ValueOf = notPreciseAvatarType; if (avatarType === CONST.REPORT_ACTION_AVATARS.TYPE.MULTIPLE && !icons.length) { @@ -166,7 +195,7 @@ function ReportActionAvatars({ if (avatarType === CONST.REPORT_ACTION_AVATARS.TYPE.SUBSCRIPT && (!!secondaryAvatar?.name || !!subscriptCardFeed)) { return ( - 0; const personalDetails = usePersonalDetails(); const avatar = personalDetails?.[taskAssigneeAccountID]?.avatar ?? icons.FallbackAvatar; - const avatarSize = CONST.AVATAR_SIZE.SMALL; + const avatarSize = CONST.AVATAR_SIZE.X_SMALL; const isDeletedParentAction = isCanceledTaskReport(taskReport, action); const iconWrapperStyle = StyleUtils.getTaskPreviewIconWrapper(hasAssignee ? avatarSize : undefined); diff --git a/src/components/ReportActionItem/TaskView.tsx b/src/components/ReportActionItem/TaskView.tsx index 612eb4d5a0c7..227e265e3e40 100644 --- a/src/components/ReportActionItem/TaskView.tsx +++ b/src/components/ReportActionItem/TaskView.tsx @@ -269,7 +269,7 @@ function TaskView({report, parentReport, action}: TaskViewProps) { title={getDisplayNameForParticipant({accountID: report.managerID, formatPhoneNumber})} iconAccountID={report.managerID} iconType={CONST.ICON_TYPE_AVATAR} - avatarSize={CONST.AVATAR_SIZE.SMALLER} + avatarSize={CONST.AVATAR_SIZE.XX_SMALL} titleStyle={styles.assigneeTextStyle} onPress={() => Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.TASK_ASSIGNEE.path))} shouldShowRightIcon={!isDisableInteractive} diff --git a/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx b/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx index 83220f42465c..f557352f9f4d 100644 --- a/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx +++ b/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx @@ -299,7 +299,7 @@ function TransactionPreviewContent({ participantFromDisplayName={from.displayName ?? from.login ?? translate('common.hidden')} participantToDisplayName={to.displayName ?? to.login ?? translate('common.hidden')} participantTo={to} - avatarSize="mid-subscript" + avatarSize={CONST.AVATAR_SIZE.XXXX_SMALL} infoCellsTextStyle={{...styles.textMicroBold, lineHeight: 14}} infoCellsAvatarStyle={styles.pr1} style={[styles.flex1, styles.dFlex, styles.alignItemsCenter, styles.gap2, styles.flexRow]} @@ -316,7 +316,7 @@ function TransactionPreviewContent({ sort: CONST.REPORT_ACTION_AVATARS.SORT_BY.ID, useCardBG: true, }} - size={CONST.AVATAR_SIZE.SUBSCRIPT} + size={CONST.AVATAR_SIZE.XXX_SMALL} /> )} diff --git a/src/components/ReportActionItem/TransactionPreview/index.tsx b/src/components/ReportActionItem/TransactionPreview/index.tsx index 9fb612685451..d499e4104986 100644 --- a/src/components/ReportActionItem/TransactionPreview/index.tsx +++ b/src/components/ReportActionItem/TransactionPreview/index.tsx @@ -88,7 +88,6 @@ function TransactionPreview(props: TransactionPreviewProps) { const shouldDisableOnPress = isBillSplit && isEmptyObject(transaction); const isReviewDuplicateTransactionPage = route.name === SCREENS.TRANSACTION_DUPLICATE.REVIEW; - if (onPreviewPressed) { return ( Navigation.navigate(ROUTES.REPORT_AVATAR.getRoute(report.reportID))} onImageRemoved={() => updatePolicyRoomAvatar(report.reportID, currentUserAccountID, report.avatarUrl)} onImageSelected={(file) => updatePolicyRoomAvatar(report.reportID, currentUserAccountID, report.avatarUrl, file)} @@ -89,8 +89,8 @@ function RoomHeaderAvatars({icons, report, policy, participants, currentUserAcco > @@ -118,7 +118,7 @@ function RoomHeaderAvatars({icons, report, policy, participants, currentUserAcco style={[styles.justifyContentCenter, styles.alignItemsCenter]} > navigateToAvatarPage(icon)} accessibilityRole={CONST.ROLE.BUTTON} accessibilityLabel={icon.name ?? ''} @@ -126,8 +126,8 @@ function RoomHeaderAvatars({icons, report, policy, participants, currentUserAcco > diff --git a/src/components/Search/SearchList/ListItem/AttendeesCell.tsx b/src/components/Search/SearchList/ListItem/AttendeesCell.tsx index 4f66df681c90..ea307173f186 100644 --- a/src/components/Search/SearchList/ListItem/AttendeesCell.tsx +++ b/src/components/Search/SearchList/ListItem/AttendeesCell.tsx @@ -41,7 +41,7 @@ function AttendeesCell({attendees, isHovered, isPressed}: AttendeesCellProps) { const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); - const size = CONST.AVATAR_SIZE.SMALLER; + const size = CONST.AVATAR_SIZE.XX_SMALL; const maxAvatarsInRow = CONST.AVATAR_ROW_SIZE.DEFAULT; const oneAvatarSize = StyleUtils.getAvatarStyle(size); const oneAvatarBorderWidth = StyleUtils.getAvatarBorderWidth(size).borderWidth ?? 0; diff --git a/src/components/Search/SearchList/ListItem/AvatarWithTextCell.tsx b/src/components/Search/SearchList/ListItem/AvatarWithTextCell.tsx index 89e876e8cd4d..a0b1becf6331 100644 --- a/src/components/Search/SearchList/ListItem/AvatarWithTextCell.tsx +++ b/src/components/Search/SearchList/ListItem/AvatarWithTextCell.tsx @@ -28,7 +28,7 @@ function AvatarWithTextCell({reportName, icon, isLargeScreenWidth}: AvatarWithTe avatarID={icon.id} type={icon.type} fallbackIcon={icon.fallbackIcon} - size={CONST.AVATAR_SIZE.MID_SUBSCRIPT} + size={CONST.AVATAR_SIZE.XXXX_SMALL} containerStyles={[styles.pr2]} /> )} diff --git a/src/components/Search/SearchList/ListItem/CardListItemHeader.tsx b/src/components/Search/SearchList/ListItem/CardListItemHeader.tsx index 14024d29b9c9..902f55de21c6 100644 --- a/src/components/Search/SearchList/ListItem/CardListItemHeader.tsx +++ b/src/components/Search/SearchList/ListItem/CardListItemHeader.tsx @@ -85,7 +85,7 @@ function CardListItemHeader({ subscriptAvatarBorderColor={backgroundColor} noRightMarginOnSubscriptContainer accountIDs={[cardItem.accountID]} - size={CONST.AVATAR_SIZE.SMALL} + size={CONST.AVATAR_SIZE.X_SMALL} /> diff --git a/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/ExpenseReportListItemAvatar.tsx b/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/ExpenseReportListItemAvatar.tsx index 8bc56c97dc47..4102e99ad09b 100644 --- a/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/ExpenseReportListItemAvatar.tsx +++ b/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/ExpenseReportListItemAvatar.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {View} from 'react-native'; -import SearchReportAvatar from '@components/ReportActionAvatars/SearchReportAvatar'; +import IconsAvatar from '@components/Avatars/IconsAvatar'; import type {ExpenseReportListItemType} from '@components/Search/SearchList/ListItem/types'; import {useRowSelection} from '@components/Search/SearchSelectionProvider'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -25,16 +25,18 @@ function ExpenseReportListItemAvatar({item, showTooltip, isHovered = false, isFo const finalAvatarBorderColor = StyleUtils.getItemBackgroundColorStyle(isSelected, isFocused || isHovered, !!item.isDisabled, theme.activeComponentBG, theme.hoverComponentBG)?.backgroundColor ?? theme.highlightBG; + const icons = [item.primaryAvatar, item.secondaryAvatar].filter((icon) => icon !== undefined); + const avatarSize = isLargeScreenWidth ? CONST.AVATAR_SIZE.X_SMALL : CONST.AVATAR_SIZE.DEFAULT; + return ( - ); diff --git a/src/components/Search/SearchList/ListItem/ExportedIconCell.tsx b/src/components/Search/SearchList/ListItem/ExportedIconCell.tsx index 0f7b44ce5791..f421f72f82bd 100644 --- a/src/components/Search/SearchList/ListItem/ExportedIconCell.tsx +++ b/src/components/Search/SearchList/ListItem/ExportedIconCell.tsx @@ -82,49 +82,49 @@ function ExportedIconCell({reportActions}: ExportedIconCellProps) { )} {isExportedToXero && ( )} {isExportedToIntacct && ( )} {(isExportedToQuickbooksOnline || isExportedToQuickbooksDesktop) && ( )} {isExportedToCertinia && ( )} {isExportedToBillCom && ( )} {isExportedToZenefits && ( )} diff --git a/src/components/Search/SearchList/ListItem/MemberListItemHeader.tsx b/src/components/Search/SearchList/ListItem/MemberListItemHeader.tsx index d69bce9e9b44..cfd943e8e188 100644 --- a/src/components/Search/SearchList/ListItem/MemberListItemHeader.tsx +++ b/src/components/Search/SearchList/ListItem/MemberListItemHeader.tsx @@ -79,7 +79,7 @@ function MemberListItemHeader({ type={CONST.ICON_TYPE_AVATAR} name={formattedDisplayName} avatarID={memberItem.accountID} - size={CONST.AVATAR_SIZE.SMALL} + size={CONST.AVATAR_SIZE.X_SMALL} /> diff --git a/src/components/Search/SearchList/ListItem/TaskListItemRow.tsx b/src/components/Search/SearchList/ListItem/TaskListItemRow.tsx index 81ee1b753682..81fb6e110bc3 100644 --- a/src/components/Search/SearchList/ListItem/TaskListItemRow.tsx +++ b/src/components/Search/SearchList/ListItem/TaskListItemRow.tsx @@ -187,7 +187,7 @@ function TaskListItemRow({item, containerStyle, showTooltip}: TaskListItemRowPro {!!item.assignee.accountID && ( ({item, wrapperSt avatarID={icon.id} type={icon.type ?? CONST.ICON_TYPE_AVATAR} fallbackIcon={icon.fallbackIcon} - iconAdditionalStyles={[{width: variables.avatarSizeNormal, height: variables.avatarSizeNormal}, styles.mr3]} + iconAdditionalStyles={[{width: variables.avatarSizeMedium, height: variables.avatarSizeMedium}, styles.mr3]} /> ); diff --git a/src/components/SelectionList/ListItem/UserSelectionListItem.tsx b/src/components/SelectionList/ListItem/UserSelectionListItem.tsx index 8a0e1cde82e2..bca8ff40e5aa 100644 --- a/src/components/SelectionList/ListItem/UserSelectionListItem.tsx +++ b/src/components/SelectionList/ListItem/UserSelectionListItem.tsx @@ -81,7 +81,7 @@ function UserSelectionListItem({ @@ -190,7 +190,7 @@ export default function WorkspaceRow({item, shouldUseNarrowTableLayout, rowIndex source={item.ownerAvatar} avatarID={item.ownerAccountID} type={CONST.ICON_TYPE_AVATAR} - size={CONST.AVATAR_SIZE.MID_SUBSCRIPT} + size={CONST.AVATAR_SIZE.XXXX_SMALL} /> )} ( ), - [name, policyID, source, styles.alignSelfCenter, styles.avatarXLarge, icons.FallbackWorkspaceAvatar], + [name, policyID, source, styles.alignSelfCenter, styles.avatarXxxxxLarge, icons.FallbackWorkspaceAvatar], ); } diff --git a/src/pages/DynamicReportDetailsPage.tsx b/src/pages/DynamicReportDetailsPage.tsx index cde9780044ad..9863167c5f7b 100644 --- a/src/pages/DynamicReportDetailsPage.tsx +++ b/src/pages/DynamicReportDetailsPage.tsx @@ -738,7 +738,7 @@ function DynamicReportDetailsPage({policy, report, route, reportMetadata, report Navigation.navigate(ROUTES.REPORT_AVATAR.getRoute(report.reportID))} onImageRemoved={() => { // Calling this without a file will remove the avatar @@ -775,7 +775,7 @@ function DynamicReportDetailsPage({policy, report, route, reportMetadata, report isGroupChat, icons, report, - styles.avatarXLarge, + styles.avatarXxxxxLarge, styles.smallEditIconAccount, styles.mt6, styles.w100, diff --git a/src/pages/DynamicReportParticipantDetailsPage.tsx b/src/pages/DynamicReportParticipantDetailsPage.tsx index 4091481f3d74..0d4e17251671 100644 --- a/src/pages/DynamicReportParticipantDetailsPage.tsx +++ b/src/pages/DynamicReportParticipantDetailsPage.tsx @@ -83,12 +83,12 @@ function DynamicReportParticipantDetails({report, route}: DynamicReportParticipa {!!(displayName ?? '') && ( diff --git a/src/pages/DynamicRoomMemberDetailsPage.tsx b/src/pages/DynamicRoomMemberDetailsPage.tsx index 9660de20add5..d920655fe763 100644 --- a/src/pages/DynamicRoomMemberDetailsPage.tsx +++ b/src/pages/DynamicRoomMemberDetailsPage.tsx @@ -77,12 +77,12 @@ function DynamicRoomMemberDetailsPage({report, route}: DynamicRoomMemberDetailsP {!!(details.displayName ?? '') && ( diff --git a/src/pages/NewChatConfirmPage.tsx b/src/pages/NewChatConfirmPage.tsx index c93186e2bdbe..c48a0b531108 100644 --- a/src/pages/NewChatConfirmPage.tsx +++ b/src/pages/NewChatConfirmPage.tsx @@ -90,8 +90,8 @@ function AvatarAndGroupNameSection({setAvatarFile, optimisticReportID}: AvatarAn setAvatarFile(undefined); setGroupDraft({avatarUri: null, avatarFileName: null, avatarFileType: null}); }} - size={CONST.AVATAR_SIZE.X_LARGE} - avatarStyle={styles.avatarXLarge} + size={CONST.AVATAR_SIZE.XXXXX_LARGE} + avatarStyle={styles.avatarXxxxxLarge} editIcon={icons.Camera} editIconStyle={styles.smallEditIconAccount} style={styles.w100} diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx index b79a7d721341..d78b6df0a95c 100755 --- a/src/pages/ProfilePage.tsx +++ b/src/pages/ProfilePage.tsx @@ -208,12 +208,12 @@ function ProfilePage({route}: ProfilePageProps) { > diff --git a/src/pages/domain/BaseDomainMemberDetailsComponent.tsx b/src/pages/domain/BaseDomainMemberDetailsComponent.tsx index 0bb491991ac1..19745bf98463 100644 --- a/src/pages/domain/BaseDomainMemberDetailsComponent.tsx +++ b/src/pages/domain/BaseDomainMemberDetailsComponent.tsx @@ -66,12 +66,12 @@ function BaseDomainMemberDetailsComponent({domainAccountID, accountID, children, diff --git a/src/pages/inbox/HeaderView.tsx b/src/pages/inbox/HeaderView.tsx index 78fda0816ff0..33b9b0f718d9 100644 --- a/src/pages/inbox/HeaderView.tsx +++ b/src/pages/inbox/HeaderView.tsx @@ -234,7 +234,8 @@ function HeaderView({onNavigationMenuButtonClicked, reportID}: HeaderViewProps) ); const shouldShowSubscript = shouldReportShowSubscript(report, isReportArchived); - const defaultSubscriptSize = isExpenseRequest(report) ? CONST.AVATAR_SIZE.SMALL_NORMAL : CONST.AVATAR_SIZE.DEFAULT; + const defaultSubscriptSize = CONST.AVATAR_SIZE.SMALL; + // const defaultSubscriptSize = isExpenseRequest(report) ? CONST.AVATAR_SIZE.SMALL : CONST.AVATAR_SIZE.DEFAULT; const brickRoadIndicator = hasReportNameError(report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; const shouldDisableDetailPage = shouldDisableDetailPageReportUtils(report, isParticipantOptimistic); const shouldUseGroupTitle = isGroupChat && (!!report?.reportName || !isMultipleParticipant); @@ -255,7 +256,7 @@ function HeaderView({onNavigationMenuButtonClicked, reportID}: HeaderViewProps) (introSelected?.companySize === CONST.ONBOARDING_COMPANY_SIZE.MICRO || introSelected?.companySize === CONST.ONBOARDING_COMPANY_SIZE.MICRO_SMALL) && (isChatUsedForOnboarding || (isAdminRoom(report) && !isChatThread)) && !isInSidePanel; - const shouldShowOnBoardingHelpDropdownButton = (shouldShowRegisterForWebinar || shouldShowGuideBooking) && !isReportArchived && !isInSidePanel; + const shouldShowOnBoardingHelpDropdownButton = true; // (shouldShowRegisterForWebinar || shouldShowGuideBooking) && !isReportArchived && !isInSidePanel; const shouldShowEarlyDiscountBanner = shouldShowDiscount && isChatUsedForOnboarding && !isInSidePanel; const latestScheduledCall = reportNameValuePairs?.calendlyCalls?.at(-1); const hasActiveScheduledCall = latestScheduledCall && !isPast(latestScheduledCall.eventTime) && latestScheduledCall.status !== CONST.SCHEDULE_CALL_STATUS.CANCELLED; diff --git a/src/pages/inbox/report/ReportActionItemCreated.tsx b/src/pages/inbox/report/ReportActionItemCreated.tsx index 2bea42d2dc54..5dbca005189c 100644 --- a/src/pages/inbox/report/ReportActionItemCreated.tsx +++ b/src/pages/inbox/report/ReportActionItemCreated.tsx @@ -97,7 +97,7 @@ function ReportActionItemCreated({reportID, policyID}: ReportActionItemCreatedPr >