From 280909e3327a95d775765024e4dfbf7eadee312b Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 8 Nov 2023 11:38:44 +0100 Subject: [PATCH 1/6] [TS migration] Migrate 'MultipleAvatars.js' component --- ...MultipleAvatars.js => MultipleAvatars.tsx} | 242 ++++++++++-------- src/components/Tooltip/index.js | 2 +- src/components/UserDetailsTooltip/index.js | 2 +- src/libs/UserUtils.ts | 3 + src/styles/StyleUtils.ts | 14 +- 5 files changed, 144 insertions(+), 119 deletions(-) rename src/components/{MultipleAvatars.js => MultipleAvatars.tsx} (57%) diff --git a/src/components/MultipleAvatars.js b/src/components/MultipleAvatars.tsx similarity index 57% rename from src/components/MultipleAvatars.js rename to src/components/MultipleAvatars.tsx index 209540189b69..00c591e3a580 100644 --- a/src/components/MultipleAvatars.js +++ b/src/components/MultipleAvatars.tsx @@ -1,77 +1,85 @@ -import PropTypes from 'prop-types'; import React, {memo, useMemo} from 'react'; -import {View} from 'react-native'; -import _ from 'underscore'; +import {StyleProp, View, ViewStyle} from 'react-native'; +import {ValueOf} from 'type-fest'; +import {AvatarSource} from '@libs/UserUtils'; import styles from '@styles/styles'; import * as StyleUtils from '@styles/StyleUtils'; import themeColors from '@styles/themes/default'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import Avatar from './Avatar'; -import avatarPropTypes from './avatarPropTypes'; import Text from './Text'; import Tooltip from './Tooltip'; import UserDetailsTooltip from './UserDetailsTooltip'; -const propTypes = { +type AvatarType = { + /** Avatar source to display */ + source?: AvatarSource; + + /** Denotes whether it is an avatar or a workspace avatar */ + type?: typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE; + + /** Owner of the avatar. If user, displayName. If workspace, policy name */ + name?: string; + + /** Avatar id */ + id?: number | string; + + /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ + fallbackIcon?: AvatarSource; +}; + +type MultipleAvatarsProps = { /** Array of avatar URLs or icons */ - icons: PropTypes.arrayOf(avatarPropTypes), + icons: AvatarType[]; /** Set the size of avatars */ - size: PropTypes.oneOf(_.values(CONST.AVATAR_SIZE)), + size: ValueOf; /** Style for Second Avatar */ - // eslint-disable-next-line react/forbid-prop-types - secondAvatarStyle: PropTypes.arrayOf(PropTypes.object), + secondAvatarStyle: StyleProp; /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ - fallbackIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + fallbackIcon?: AvatarSource; /** Prop to identify if we should load avatars vertically instead of diagonally */ - shouldStackHorizontally: PropTypes.bool, + shouldStackHorizontally?: boolean; /** Prop to identify if we should display avatars in rows */ - shouldDisplayAvatarsInRows: PropTypes.bool, + shouldDisplayAvatarsInRows?: boolean; /** Whether the avatars are hovered */ - isHovered: PropTypes.bool, + isHovered?: boolean; /** Whether the avatars are in an element being pressed */ - isPressed: PropTypes.bool, + isPressed?: boolean; /** Whether #focus mode is on */ - isFocusMode: PropTypes.bool, + isFocusMode?: boolean; /** Whether avatars are displayed within a reportAction */ - isInReportAction: PropTypes.bool, + isInReportAction?: boolean; /** Whether to show the toolip text */ - shouldShowTooltip: PropTypes.bool, + shouldShowTooltip?: boolean; /** Whether avatars are displayed with the highlighted background color instead of the app background color. This is primarily the case for IOU previews. */ - shouldUseCardBackground: PropTypes.bool, + shouldUseCardBackground?: boolean; /** Prop to limit the amount of avatars displayed horizontally */ - maxAvatarsInRow: PropTypes.number, + maxAvatarsInRow?: number; }; -const defaultProps = { - icons: [], - size: CONST.AVATAR_SIZE.DEFAULT, - secondAvatarStyle: [StyleUtils.getBackgroundAndBorderStyle(themeColors.componentBG)], - fallbackIcon: undefined, - shouldStackHorizontally: false, - shouldDisplayAvatarsInRows: false, - isHovered: false, - isPressed: false, - isFocusMode: false, - isInReportAction: false, - shouldShowTooltip: true, - shouldUseCardBackground: false, - maxAvatarsInRow: CONST.AVATAR_ROW_SIZE.DEFAULT, +type AvatarStyles = { + singleAvatarStyle: ViewStyle; + secondAvatarStyles: ViewStyle; }; -const avatarSizeToStylesMap = { +type AvatarSizeToStyles = typeof CONST.AVATAR_SIZE.SMALL | typeof CONST.AVATAR_SIZE.LARGE | typeof CONST.AVATAR_SIZE.DEFAULT; + +type AvatarSizeToStylesMap = Record; + +const avatarSizeToStylesMap: AvatarSizeToStylesMap = { [CONST.AVATAR_SIZE.SMALL]: { singleAvatarStyle: styles.singleAvatarSmall, secondAvatarStyles: styles.secondAvatarSmall, @@ -80,78 +88,93 @@ const avatarSizeToStylesMap = { singleAvatarStyle: styles.singleAvatarMedium, secondAvatarStyles: styles.secondAvatarMedium, }, - default: { + [CONST.AVATAR_SIZE.DEFAULT]: { singleAvatarStyle: styles.singleAvatar, secondAvatarStyles: styles.secondAvatar, }, }; -function MultipleAvatars(props) { - let avatarContainerStyles = StyleUtils.getContainerStyles(props.size, props.isInReportAction); - const {singleAvatarStyle, secondAvatarStyles} = useMemo(() => avatarSizeToStylesMap[props.size] || avatarSizeToStylesMap.default, [props.size]); - const tooltipTexts = props.shouldShowTooltip ? _.pluck(props.icons, 'name') : ['']; +function MultipleAvatars({ + fallbackIcon, + icons = [], + size = CONST.AVATAR_SIZE.DEFAULT, + secondAvatarStyle = [StyleUtils.getBackgroundAndBorderStyle(themeColors.componentBG)], + shouldStackHorizontally = false, + shouldDisplayAvatarsInRows = false, + isHovered = false, + isPressed = false, + isFocusMode = false, + isInReportAction = false, + shouldShowTooltip = true, + shouldUseCardBackground = false, + maxAvatarsInRow = CONST.AVATAR_ROW_SIZE.DEFAULT, +}: MultipleAvatarsProps) { + let avatarContainerStyles = StyleUtils.getContainerStyles(size, isInReportAction); + const {singleAvatarStyle, secondAvatarStyles} = useMemo(() => avatarSizeToStylesMap[size as AvatarSizeToStyles] ?? avatarSizeToStylesMap.default, [size]); + + const tooltipTexts = shouldShowTooltip ? icons.map((icon) => icon.name) : ['']; const avatarSize = useMemo(() => { - if (props.isFocusMode) { + if (isFocusMode) { return CONST.AVATAR_SIZE.MID_SUBSCRIPT; } - if (props.size === CONST.AVATAR_SIZE.LARGE) { + if (size === CONST.AVATAR_SIZE.LARGE) { return CONST.AVATAR_SIZE.MEDIUM; } return CONST.AVATAR_SIZE.SMALLER; - }, [props.isFocusMode, props.size]); + }, [isFocusMode, size]); 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 (!props.shouldDisplayAvatarsInRows || props.icons.length <= props.maxAvatarsInRow) { - return [props.icons]; + if (!shouldDisplayAvatarsInRows || icons.length <= maxAvatarsInRow) { + return [icons]; } // Calculate the size of each row - const rowSize = Math.min(Math.ceil(props.icons.length / 2), props.maxAvatarsInRow); + const rowSize = Math.min(Math.ceil(icons.length / 2), maxAvatarsInRow); // Slice the icons array into two rows - const firstRow = props.icons.slice(rowSize); - const secondRow = props.icons.slice(0, rowSize); + const firstRow = icons.slice(rowSize); + const secondRow = icons.slice(0, rowSize); // Update the state with the two rows as an array return [firstRow, secondRow]; - }, [props.icons, props.maxAvatarsInRow, props.shouldDisplayAvatarsInRows]); + }, [icons, maxAvatarsInRow, shouldDisplayAvatarsInRows]); - if (!props.icons.length) { + if (!icons.length) { return null; } - if (props.icons.length === 1 && !props.shouldStackHorizontally) { + if (icons.length === 1 && !shouldStackHorizontally) { return ( ); } - const oneAvatarSize = StyleUtils.getAvatarStyle(props.size); - const oneAvatarBorderWidth = StyleUtils.getAvatarBorderWidth(props.size).borderWidth; + const oneAvatarSize = StyleUtils.getAvatarStyle(size); + const oneAvatarBorderWidth = StyleUtils.getAvatarBorderWidth(size).borderWidth ?? 0; const overlapSize = oneAvatarSize.width / 3; - if (props.shouldStackHorizontally) { + if (shouldStackHorizontally) { // Height of one avatar + border space const height = oneAvatarSize.height + 2 * oneAvatarBorderWidth; avatarContainerStyles = StyleUtils.combineStyles([styles.alignItemsCenter, styles.flexRow, StyleUtils.getHeight(height)]); @@ -159,36 +182,37 @@ function MultipleAvatars(props) { return ( <> - {props.shouldStackHorizontally ? ( - _.map(avatarRows, (avatars, rowIndex) => ( + {shouldStackHorizontally ? ( + avatarRows.map((avatars, rowIndex) => ( - {_.map([...avatars].splice(0, props.maxAvatarsInRow), (icon, index) => ( + {[...avatars].splice(0, maxAvatarsInRow).map((icon, index) => ( - + ))} - {avatars.length > props.maxAvatarsInRow && ( + {avatars.length > maxAvatarsInRow && ( {`+${avatars.length - props.maxAvatarsInRow}`} + >{`+${avatars.length - maxAvatarsInRow}`} @@ -238,53 +262,47 @@ function MultipleAvatars(props) { )) ) : ( - + {/* View is necessary for tooltip to show for multiple avatars in LHN */} - - {props.icons.length === 2 ? ( + + {icons.length === 2 ? ( @@ -292,10 +310,10 @@ function MultipleAvatars(props) { - {`+${props.icons.length - 1}`} + {`+${icons.length - 1}`} @@ -308,8 +326,6 @@ function MultipleAvatars(props) { ); } -MultipleAvatars.defaultProps = defaultProps; -MultipleAvatars.propTypes = propTypes; MultipleAvatars.displayName = 'MultipleAvatars'; export default memo(MultipleAvatars); diff --git a/src/components/Tooltip/index.js b/src/components/Tooltip/index.js index 19a607220e1c..c7c4428e19a3 100644 --- a/src/components/Tooltip/index.js +++ b/src/components/Tooltip/index.js @@ -15,7 +15,7 @@ const defaultProps = { shouldRender: true, }; -function Tooltip({shouldRender, children, ...props}) { +function Tooltip({shouldRender = true, children, ...props}) { if (!shouldRender) { return children; } diff --git a/src/components/UserDetailsTooltip/index.js b/src/components/UserDetailsTooltip/index.js index ea5cd4337071..6e256ac9ba9e 100644 --- a/src/components/UserDetailsTooltip/index.js +++ b/src/components/UserDetailsTooltip/index.js @@ -15,7 +15,7 @@ const defaultProps = { shouldRender: true, }; -function UserDetailsTooltip({shouldRender, children, ...props}) { +function UserDetailsTooltip({shouldRender = true, children, ...props}) { if (!shouldRender) { return children; } diff --git a/src/libs/UserUtils.ts b/src/libs/UserUtils.ts index f7883609f625..1a5ced6c9f85 100644 --- a/src/libs/UserUtils.ts +++ b/src/libs/UserUtils.ts @@ -8,6 +8,8 @@ import hashCode from './hashCode'; type AvatarRange = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24; +type AvatarSource = React.FC | string; + type LoginListIndicator = ValueOf | ''; /** @@ -202,3 +204,4 @@ export { getFullSizeAvatar, generateAccountID, }; +export type {AvatarSource}; diff --git a/src/styles/StyleUtils.ts b/src/styles/StyleUtils.ts index 42b7860ee263..b1116b372c2f 100644 --- a/src/styles/StyleUtils.ts +++ b/src/styles/StyleUtils.ts @@ -17,6 +17,12 @@ import variables from './variables'; type AllStyles = ViewStyle | TextStyle | ImageStyle; type ParsableStyle = StyleProp | ((state: PressableStateCallbackType) => StyleProp); +type AvatarStyle = { + width: number; + height: number; + borderRadius: number; + backgroundColor: string; +}; type ColorValue = ValueOf; type AvatarSizeName = ValueOf; @@ -210,7 +216,7 @@ function getAvatarWidthStyle(size: AvatarSizeName): ViewStyle { /** * Return the style from an avatar size constant */ -function getAvatarStyle(size: AvatarSizeName): ViewStyle { +function getAvatarStyle(size: AvatarSizeName): AvatarStyle { const avatarSize = getAvatarSize(size); return { height: avatarSize, @@ -241,7 +247,7 @@ function getAvatarBorderWidth(size: AvatarSizeName): ViewStyle { /** * Return the border radius for an avatar */ -function getAvatarBorderRadius(size: AvatarSizeName, type: string): ViewStyle { +function getAvatarBorderRadius(size: AvatarSizeName, type?: string): ViewStyle { if (type === CONST.ICON_TYPE_WORKSPACE) { return {borderRadius: avatarBorderSizes[size]}; } @@ -1286,8 +1292,8 @@ function getAmountFontSizeAndLineHeight(baseFontSize: number, baseLineHeight: nu /** * Returns container styles for showing the icons in MultipleAvatars/SubscriptAvatar */ -function getContainerStyles(size: string, isInReportAction = false): Array { - let containerStyles: Array; +function getContainerStyles(size: string, isInReportAction = false): ViewStyle[] { + let containerStyles: ViewStyle[]; switch (size) { case CONST.AVATAR_SIZE.SMALL: From ab92c85b94f09b58e565ac66b15528b53affbdaf Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 8 Nov 2023 12:26:04 +0100 Subject: [PATCH 2/6] Update OnyxCommon Icon type --- src/components/MultipleAvatars.tsx | 20 ++------------------ src/types/onyx/OnyxCommon.ts | 19 +++++++++++++++---- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/components/MultipleAvatars.tsx b/src/components/MultipleAvatars.tsx index 00c591e3a580..e3d871e0f888 100644 --- a/src/components/MultipleAvatars.tsx +++ b/src/components/MultipleAvatars.tsx @@ -7,31 +7,15 @@ import * as StyleUtils from '@styles/StyleUtils'; import themeColors from '@styles/themes/default'; import variables from '@styles/variables'; import CONST from '@src/CONST'; +import type {Icon} from '@src/types/onyx/OnyxCommon'; import Avatar from './Avatar'; import Text from './Text'; import Tooltip from './Tooltip'; import UserDetailsTooltip from './UserDetailsTooltip'; -type AvatarType = { - /** Avatar source to display */ - source?: AvatarSource; - - /** Denotes whether it is an avatar or a workspace avatar */ - type?: typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE; - - /** Owner of the avatar. If user, displayName. If workspace, policy name */ - name?: string; - - /** Avatar id */ - id?: number | string; - - /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ - fallbackIcon?: AvatarSource; -}; - type MultipleAvatarsProps = { /** Array of avatar URLs or icons */ - icons: AvatarType[]; + icons: Icon[]; /** Set the size of avatars */ size: ValueOf; diff --git a/src/types/onyx/OnyxCommon.ts b/src/types/onyx/OnyxCommon.ts index ef2944d6af82..4d376b8b7da8 100644 --- a/src/types/onyx/OnyxCommon.ts +++ b/src/types/onyx/OnyxCommon.ts @@ -1,5 +1,5 @@ -import * as React from 'react'; import {ValueOf} from 'type-fest'; +import {AvatarSource} from '@libs/UserUtils'; import CONST from '@src/CONST'; type PendingAction = ValueOf; @@ -11,9 +11,20 @@ type ErrorFields = Record; type Icon = { - source: React.ReactNode | string; - type: 'avatar' | 'workspace'; - name: string; + /** Avatar source to display */ + source?: AvatarSource; + + /** Denotes whether it is an avatar or a workspace avatar */ + type?: typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE; + + /** Owner of the avatar. If user, displayName. If workspace, policy name */ + name?: string; + + /** Avatar id */ + id?: number | string; + + /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ + fallbackIcon?: AvatarSource; }; export type {Icon, PendingAction, PendingFields, ErrorFields, Errors}; From cf6410f9128a085ef065fd3441cd352885bb799a Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 8 Nov 2023 12:39:35 +0100 Subject: [PATCH 3/6] Add useMemo for tooltipTexts --- src/components/MultipleAvatars.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MultipleAvatars.tsx b/src/components/MultipleAvatars.tsx index e3d871e0f888..a86d4946856e 100644 --- a/src/components/MultipleAvatars.tsx +++ b/src/components/MultipleAvatars.tsx @@ -96,7 +96,7 @@ function MultipleAvatars({ let avatarContainerStyles = StyleUtils.getContainerStyles(size, isInReportAction); const {singleAvatarStyle, secondAvatarStyles} = useMemo(() => avatarSizeToStylesMap[size as AvatarSizeToStyles] ?? avatarSizeToStylesMap.default, [size]); - const tooltipTexts = shouldShowTooltip ? icons.map((icon) => icon.name) : ['']; + const tooltipTexts = useMemo(() => (shouldShowTooltip ? icons.map((icon) => icon.name) : ['']), [shouldShowTooltip, icons]); const avatarSize = useMemo(() => { if (isFocusMode) { return CONST.AVATAR_SIZE.MID_SUBSCRIPT; From bc3181ae12a98fd29add7af9e51847c6cc18a08a Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 8 Nov 2023 18:06:05 +0100 Subject: [PATCH 4/6] Make size and secondAvatarStyle to be optional --- src/components/MultipleAvatars.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/MultipleAvatars.tsx b/src/components/MultipleAvatars.tsx index a86d4946856e..4101fd381a3f 100644 --- a/src/components/MultipleAvatars.tsx +++ b/src/components/MultipleAvatars.tsx @@ -18,10 +18,10 @@ type MultipleAvatarsProps = { icons: Icon[]; /** Set the size of avatars */ - size: ValueOf; + size?: ValueOf; /** Style for Second Avatar */ - secondAvatarStyle: StyleProp; + secondAvatarStyle?: StyleProp; /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ fallbackIcon?: AvatarSource; From 22f237f122150cfa41698d19835c044e76460ea8 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 8 Nov 2023 18:50:06 +0100 Subject: [PATCH 5/6] Remove unused avatar props --- src/components/MultipleAvatars.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/MultipleAvatars.tsx b/src/components/MultipleAvatars.tsx index 4101fd381a3f..e867de7ddb97 100644 --- a/src/components/MultipleAvatars.tsx +++ b/src/components/MultipleAvatars.tsx @@ -137,7 +137,6 @@ function MultipleAvatars({ icon={icons[0]} fallbackUserDetails={{ displayName: icons[0].name, - avatar: icons[0].source, }} > @@ -180,7 +179,6 @@ function MultipleAvatars({ icon={icon} fallbackUserDetails={{ displayName: icon.name, - avatar: icon.source, }} > @@ -252,7 +250,6 @@ function MultipleAvatars({ icon={icons[0]} fallbackUserDetails={{ displayName: icons[0].name, - avatar: icons[0].source, }} > {/* View is necessary for tooltip to show for multiple avatars in LHN */} @@ -275,7 +272,6 @@ function MultipleAvatars({ icon={icons[1]} fallbackUserDetails={{ displayName: icons[1].name, - avatar: icons[1].source, }} > From 60f26d3968f16d800873adb53fefd1f46f1da8d0 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 15 Nov 2023 11:26:16 +0100 Subject: [PATCH 6/6] Update icon type --- src/types/onyx/OnyxCommon.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/types/onyx/OnyxCommon.ts b/src/types/onyx/OnyxCommon.ts index 4d376b8b7da8..ac69baed3ef1 100644 --- a/src/types/onyx/OnyxCommon.ts +++ b/src/types/onyx/OnyxCommon.ts @@ -12,16 +12,16 @@ type Errors = Record; type Icon = { /** Avatar source to display */ - source?: AvatarSource; + source: AvatarSource; /** Denotes whether it is an avatar or a workspace avatar */ - type?: typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE; + type: typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE; /** Owner of the avatar. If user, displayName. If workspace, policy name */ - name?: string; + name: string; /** Avatar id */ - id?: number | string; + id: number | string; /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ fallbackIcon?: AvatarSource;