diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 4a875ea7bf84..24d0050997f7 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -16,6 +16,7 @@ import ROUTES from '@src/ROUTES'; import {Beta, Login, PersonalDetails, PersonalDetailsList, Policy, Report, ReportAction, ReportMetadata, Session, Transaction} from '@src/types/onyx'; import {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; import {IOUMessage, OriginalMessageActionName, OriginalMessageCreated} from '@src/types/onyx/OriginalMessage'; +import {Status} from '@src/types/onyx/PersonalDetails'; import {NotificationPreference} from '@src/types/onyx/Report'; import {Message, ReportActionBase, ReportActions} from '@src/types/onyx/ReportAction'; import {Receipt, WaypointCollection} from '@src/types/onyx/Transaction'; @@ -329,7 +330,7 @@ type OptionData = { login?: string | null; accountID?: number | null; pronouns?: string; - status?: string | null; + status?: Status | null; phoneNumber?: string | null; isUnread?: boolean | null; isUnreadWithMention?: boolean | null; diff --git a/src/pages/home/report/ReportActionItemSingle.js b/src/pages/home/report/ReportActionItemSingle.tsx similarity index 57% rename from src/pages/home/report/ReportActionItemSingle.js rename to src/pages/home/report/ReportActionItemSingle.tsx index c47274fa5f94..69bbd924caef 100644 --- a/src/pages/home/report/ReportActionItemSingle.js +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -1,8 +1,5 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useCallback, useMemo} from 'react'; -import {View} from 'react-native'; -import _ from 'underscore'; +import {StyleProp, View, ViewStyle} from 'react-native'; import Avatar from '@components/Avatar'; import MultipleAvatars from '@components/MultipleAvatars'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -12,7 +9,7 @@ import SubscriptAvatar from '@components/SubscriptAvatar'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; import UserDetailsTooltip from '@components/UserDetailsTooltip'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -21,112 +18,136 @@ import DateUtils from '@libs/DateUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import * as UserUtils from '@libs/UserUtils'; -import reportPropTypes from '@pages/reportPropTypes'; -import stylePropTypes from '@styles/stylePropTypes'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; +import type {Report, ReportAction} from '@src/types/onyx'; +import {AvatarType} from '@src/types/onyx/OnyxCommon'; import ReportActionItemDate from './ReportActionItemDate'; import ReportActionItemFragment from './ReportActionItemFragment'; -import reportActionPropTypes from './reportActionPropTypes'; -const propTypes = { +type ReportActionItemSingleProps = { /** All the data of the action */ - action: PropTypes.shape(reportActionPropTypes).isRequired, + action: ReportAction; /** Styles for the outermost View */ - wrapperStyle: stylePropTypes, + wrapperStyle?: StyleProp; /** Children view component for this action item */ - children: PropTypes.node.isRequired, + children: React.ReactNode; /** Report for this action */ - report: reportPropTypes, + report: Report; /** IOU Report for this action, if any */ - iouReport: reportPropTypes, + iouReport?: Report; /** Show header for action */ - showHeader: PropTypes.bool, + showHeader?: boolean; /** Determines if the avatar is displayed as a subscript (positioned lower than normal) */ - shouldShowSubscriptAvatar: PropTypes.bool, + shouldShowSubscriptAvatar?: boolean; /** If the message has been flagged for moderation */ - hasBeenFlagged: PropTypes.bool, + hasBeenFlagged?: boolean; /** If the action is being hovered */ - isHovered: PropTypes.bool, - - ...withLocalizePropTypes, + isHovered?: boolean; }; -const defaultProps = { - wrapperStyle: undefined, - showHeader: true, - shouldShowSubscriptAvatar: false, - hasBeenFlagged: false, - report: undefined, - iouReport: undefined, - isHovered: false, +type SubAvatar = { + /** Avatar source to display */ + source: UserUtils.AvatarSource; + + /** Denotes whether it is a user avatar or a workspace avatar */ + type: AvatarType; + + /** 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?: UserUtils.AvatarSource; }; -const showUserDetails = (accountID) => { +const showUserDetails = (accountID: string) => { Navigation.navigate(ROUTES.PROFILE.getRoute(accountID)); }; -const showWorkspaceDetails = (reportID) => { +const showWorkspaceDetails = (reportID: string) => { Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(reportID)); }; -function ReportActionItemSingle(props) { +function ReportActionItemSingle({ + action, + children, + wrapperStyle, + showHeader = true, + shouldShowSubscriptAvatar = false, + hasBeenFlagged = false, + report, + iouReport, + isHovered = false, +}: ReportActionItemSingleProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; - const actorAccountID = props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && props.iouReport ? props.iouReport.managerID : props.action.actorAccountID; + const {translate} = useLocalize(); + const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT; + const actorAccountID = action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && iouReport ? iouReport.managerID : action.actorAccountID; let displayName = ReportUtils.getDisplayNameForParticipant(actorAccountID); - const {avatar, login, pendingFields, status, fallbackIcon} = personalDetails[actorAccountID] || {}; - let actorHint = (login || displayName || '').replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, ''); - const displayAllActors = useMemo(() => props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && props.iouReport, [props.action.actionName, props.iouReport]); - const isWorkspaceActor = ReportUtils.isPolicyExpenseChat(props.report) && (!actorAccountID || displayAllActors); - let avatarSource = UserUtils.getAvatar(avatar, actorAccountID); + const {avatar, login, pendingFields, status, fallbackIcon} = personalDetails[actorAccountID ?? -1] ?? {}; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + let actorHint = (login || (displayName ?? '')).replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, ''); + const displayAllActors = useMemo(() => action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && iouReport, [action.actionName, iouReport]); + const isWorkspaceActor = ReportUtils.isPolicyExpenseChat(report) && (!actorAccountID || displayAllActors); + let avatarSource = UserUtils.getAvatar(avatar ?? '', actorAccountID); if (isWorkspaceActor) { - displayName = ReportUtils.getPolicyName(props.report); + displayName = ReportUtils.getPolicyName(report); actorHint = displayName; - avatarSource = ReportUtils.getWorkspaceAvatar(props.report); - } else if (props.action.delegateAccountID && personalDetails[props.action.delegateAccountID]) { + avatarSource = ReportUtils.getWorkspaceAvatar(report); + } else if (action.delegateAccountID && personalDetails[action.delegateAccountID]) { // We replace the actor's email, name, and avatar with the Copilot manually for now. And only if we have their // details. This will be improved upon when the Copilot feature is implemented. - const delegateDetails = personalDetails[props.action.delegateAccountID]; - const delegateDisplayName = delegateDetails.displayName; - actorHint = `${delegateDisplayName} (${props.translate('reportAction.asCopilot')} ${displayName})`; + const delegateDetails = personalDetails[action.delegateAccountID]; + const delegateDisplayName = delegateDetails?.displayName; + actorHint = `${delegateDisplayName} (${translate('reportAction.asCopilot')} ${displayName})`; displayName = actorHint; - avatarSource = UserUtils.getAvatar(delegateDetails.avatar, props.action.delegateAccountID); + avatarSource = UserUtils.getAvatar(delegateDetails?.avatar ?? '', Number(action.delegateAccountID)); } // If this is a report preview, display names and avatars of both people involved - let secondaryAvatar = {}; + let secondaryAvatar: SubAvatar; const primaryDisplayName = displayName; if (displayAllActors) { // The ownerAccountID and actorAccountID can be the same if the a user requests money back from the IOU's original creator, in that case we need to use managerID to avoid displaying the same user twice - const secondaryAccountId = props.iouReport.ownerAccountID === actorAccountID ? props.iouReport.managerID : props.iouReport.ownerAccountID; - const secondaryUserDetails = personalDetails[secondaryAccountId] || {}; + const secondaryAccountId = iouReport?.ownerAccountID === actorAccountID ? iouReport?.managerID : iouReport?.ownerAccountID; + const secondaryUserAvatar = personalDetails?.[secondaryAccountId ?? -1]?.avatar ?? ''; const secondaryDisplayName = ReportUtils.getDisplayNameForParticipant(secondaryAccountId); displayName = `${primaryDisplayName} & ${secondaryDisplayName}`; secondaryAvatar = { - source: UserUtils.getAvatar(secondaryUserDetails.avatar, secondaryAccountId), + source: UserUtils.getAvatar(secondaryUserAvatar, secondaryAccountId), type: CONST.ICON_TYPE_AVATAR, - name: secondaryDisplayName, + name: secondaryDisplayName ?? '', id: secondaryAccountId, }; } else if (!isWorkspaceActor) { - const avatarIconIndex = props.report.isOwnPolicyExpenseChat || ReportUtils.isPolicyExpenseChat(props.report) ? 0 : 1; - const reportIcons = ReportUtils.getIcons(props.report, {}); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const avatarIconIndex = report.isOwnPolicyExpenseChat || ReportUtils.isPolicyExpenseChat(report) ? 0 : 1; + const reportIcons = ReportUtils.getIcons(report, {}); secondaryAvatar = reportIcons[avatarIconIndex]; + } else { + secondaryAvatar = {name: '', source: '', type: 'avatar'}; } - const icon = {source: avatarSource, type: isWorkspaceActor ? CONST.ICON_TYPE_WORKSPACE : CONST.ICON_TYPE_AVATAR, name: primaryDisplayName, id: isWorkspaceActor ? '' : actorAccountID}; + const icon = { + source: avatarSource, + type: isWorkspaceActor ? CONST.ICON_TYPE_WORKSPACE : CONST.ICON_TYPE_AVATAR, + name: primaryDisplayName ?? '', + id: isWorkspaceActor ? '' : actorAccountID, + }; // Since the display name for a report action message is delivered with the report history as an array of fragments // we'll need to take the displayName from personal details and have it be in the same format for now. Eventually, @@ -138,29 +159,29 @@ function ReportActionItemSingle(props) { text: displayName, }, ] - : props.action.person; + : action.person; - const reportID = props.report && props.report.reportID; - const iouReportID = props.iouReport && props.iouReport.reportID; + const reportID = report?.reportID; + const iouReportID = iouReport?.reportID; const showActorDetails = useCallback(() => { if (isWorkspaceActor) { showWorkspaceDetails(reportID); } else { // Show participants page IOU report preview - if (displayAllActors) { + if (iouReportID && displayAllActors) { Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(iouReportID)); return; } - showUserDetails(props.action.delegateAccountID ? props.action.delegateAccountID : actorAccountID); + showUserDetails(action.delegateAccountID ? action.delegateAccountID : String(actorAccountID)); } - }, [isWorkspaceActor, reportID, actorAccountID, props.action.delegateAccountID, iouReportID, displayAllActors]); + }, [isWorkspaceActor, reportID, actorAccountID, action.delegateAccountID, iouReportID, displayAllActors]); const shouldDisableDetailPage = useMemo( () => actorAccountID === CONST.ACCOUNT_ID.NOTIFICATIONS || - (!isWorkspaceActor && ReportUtils.isOptimisticPersonalDetail(props.action.delegateAccountID ? props.action.delegateAccountID : actorAccountID)), - [props.action, isWorkspaceActor, actorAccountID], + (!isWorkspaceActor && ReportUtils.isOptimisticPersonalDetail(action.delegateAccountID ? Number(action.delegateAccountID) : actorAccountID ?? -1)), + [action, isWorkspaceActor, actorAccountID], ); const getAvatar = () => { @@ -170,17 +191,15 @@ function ReportActionItemSingle(props) { icons={[icon, secondaryAvatar]} isInReportAction shouldShowTooltip - secondAvatarStyle={[StyleUtils.getBackgroundAndBorderStyle(theme.appBG), props.isHovered ? StyleUtils.getBackgroundAndBorderStyle(theme.hoverComponentBG) : undefined]} + secondAvatarStyle={[StyleUtils.getBackgroundAndBorderStyle(theme.appBG), isHovered ? StyleUtils.getBackgroundAndBorderStyle(theme.hoverComponentBG) : undefined]} /> ); } - if (props.shouldShowSubscriptAvatar) { + if (shouldShowSubscriptAvatar) { return ( ); @@ -188,7 +207,7 @@ function ReportActionItemSingle(props) { return ( @@ -203,13 +222,13 @@ function ReportActionItemSingle(props) { ); }; - const hasEmojiStatus = !displayAllActors && status && status.emojiCode; - const formattedDate = DateUtils.getStatusUntilDate(lodashGet(status, 'clearAfter')); - const statusText = lodashGet(status, 'text', ''); + const hasEmojiStatus = !displayAllActors && status?.emojiCode; + const formattedDate = DateUtils.getStatusUntilDate(status?.clearAfter ?? ''); + const statusText = status?.text ?? ''; const statusTooltipText = formattedDate ? `${statusText ? `${statusText} ` : ''}(${formattedDate})` : statusText; return ( - + - {getAvatar()} + {getAvatar()} - {props.showHeader ? ( + {showHeader ? ( - {_.map(personArray, (fragment, index) => ( + {personArray?.map((fragment, index) => ( @@ -249,20 +269,18 @@ function ReportActionItemSingle(props) { {`${status.emojiCode}`} + >{`${status?.emojiCode}`} )} - + ) : null} - {props.children} + {children} ); } -ReportActionItemSingle.propTypes = propTypes; -ReportActionItemSingle.defaultProps = defaultProps; ReportActionItemSingle.displayName = 'ReportActionItemSingle'; -export default withLocalize(ReportActionItemSingle); +export default ReportActionItemSingle; diff --git a/src/types/onyx/PersonalDetails.ts b/src/types/onyx/PersonalDetails.ts index 9f613cbf4f1e..03aa7e4ec928 100644 --- a/src/types/onyx/PersonalDetails.ts +++ b/src/types/onyx/PersonalDetails.ts @@ -12,6 +12,17 @@ type Timezone = { automatic?: boolean; }; +type Status = { + /** The emoji code of the status */ + emojiCode: string; + + /** The text of the draft status */ + text?: string; + + /** The timestamp of when the status should be cleared */ + clearAfter: string; // ISO 8601 format; +}; + type PersonalDetails = { /** ID of the current user from their personal details */ accountID: number; @@ -70,11 +81,11 @@ type PersonalDetails = { fallbackIcon?: string; /** Status of the current user from their personal details */ - status?: string; + status?: Status; }; type PersonalDetailsList = Record; export default PersonalDetails; -export type {Timezone, SelectedTimezone, PersonalDetailsList}; +export type {Timezone, Status, SelectedTimezone, PersonalDetailsList};