diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index 3f89f4032061..c0fe0e2d26f8 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -395,6 +395,7 @@ function AttachmentModal(props) { file={file} onToggleKeyboard={updateConfirmButtonVisibility} isWorkspaceAvatar={props.isWorkspaceAvatar} + fallbackSource={props.fallbackSource} /> ) )} diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js index 7de4417a4efc..48ac954ced7f 100755 --- a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js @@ -12,13 +12,14 @@ const propTypes = { ...withLocalizePropTypes, }; -function AttachmentViewImage({source, file, isAuthTokenRequired, loadComplete, onPress, isImage, onScaleChanged, translate}) { +function AttachmentViewImage({source, file, isAuthTokenRequired, loadComplete, onPress, isImage, onScaleChanged, translate, onError}) { const children = ( ); return onPress ? ( diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js index 47353d915060..1fc579977c9d 100755 --- a/src/components/Attachments/AttachmentView/index.js +++ b/src/components/Attachments/AttachmentView/index.js @@ -17,6 +17,7 @@ import AttachmentViewPdf from './AttachmentViewPdf'; import addEncryptedAuthTokenToURL from '../../../libs/addEncryptedAuthTokenToURL'; import * as StyleUtils from '../../../styles/StyleUtils'; import {attachmentViewPropTypes, attachmentViewDefaultProps} from './propTypes'; +import useNetwork from '../../../hooks/useNetwork'; const propTypes = { ...attachmentViewPropTypes, @@ -62,9 +63,14 @@ function AttachmentView({ translate, isFocused, isWorkspaceAvatar, + fallbackSource, }) { const [loadComplete, setLoadComplete] = useState(false); + const [imageError, setImageError] = useState(false); + + useNetwork({onReconnect: () => setImageError(false)}); + // Handles case where source is a component (ex: SVG) if (_.isFunction(source)) { let iconFillColor = ''; @@ -113,7 +119,7 @@ function AttachmentView({ if (isImage || (file && Str.isImage(file.name))) { return ( { + setImageError(true); + }} /> ); } diff --git a/src/components/Avatar.js b/src/components/Avatar.js index b96e60dd56d1..4f0eb60eb2e0 100644 --- a/src/components/Avatar.js +++ b/src/components/Avatar.js @@ -1,4 +1,4 @@ -import React, {useState} from 'react'; +import React, {useEffect, useState} from 'react'; import {View} from 'react-native'; import PropTypes from 'prop-types'; import _ from 'underscore'; @@ -40,7 +40,7 @@ const propTypes = { /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. * If the avatar is type === workspace, this fallback icon will be ignored and decided based on the name prop. */ - fallbackIcon: PropTypes.func, + fallbackIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), /** Denotes whether it is an avatar or a workspace avatar */ type: PropTypes.oneOf([CONST.ICON_TYPE_AVATAR, CONST.ICON_TYPE_WORKSPACE]), @@ -66,6 +66,10 @@ function Avatar(props) { useNetwork({onReconnect: () => setImageError(false)}); + useEffect(() => { + setImageError(false); + }, [props.source]); + if (!props.source) { return null; } @@ -81,14 +85,14 @@ function Avatar(props) { const iconStyle = props.imageStyles && props.imageStyles.length ? [StyleUtils.getAvatarStyle(props.size), styles.bgTransparent, ...props.imageStyles] : undefined; const iconFillColor = isWorkspace ? StyleUtils.getDefaultWorkspaceAvatarColor(props.name).fill : props.fill; - const fallbackAvatar = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatar(props.name) : props.fallbackIcon; + const fallbackAvatar = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatar(props.name) : props.fallbackIcon || Expensicons.FallbackAvatar; return ( - {_.isFunction(props.source) || imageError ? ( + {_.isFunction(props.source) || (imageError && _.isFunction(fallbackAvatar)) ? ( setImageError(true)} /> diff --git a/src/components/AvatarWithImagePicker.js b/src/components/AvatarWithImagePicker.js index 4cc70f00d1ae..a44d1841bbb6 100644 --- a/src/components/AvatarWithImagePicker.js +++ b/src/components/AvatarWithImagePicker.js @@ -61,7 +61,7 @@ const propTypes = { size: PropTypes.oneOf([CONST.AVATAR_SIZE.LARGE, CONST.AVATAR_SIZE.DEFAULT]), /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ - fallbackIcon: PropTypes.func, + fallbackIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), /** Denotes whether it is an avatar or a workspace avatar */ type: PropTypes.oneOf([CONST.ICON_TYPE_AVATAR, CONST.ICON_TYPE_WORKSPACE]), @@ -296,6 +296,7 @@ class AvatarWithImagePicker extends React.Component { headerTitle={this.props.headerTitle} source={this.props.previewSource} originalFileName={this.props.originalFileName} + fallbackSource={this.props.fallbackIcon} > {({show}) => ( diff --git a/src/components/AvatarWithIndicator.js b/src/components/AvatarWithIndicator.js index b59f67a45b7b..5e7b8d1ee632 100644 --- a/src/components/AvatarWithIndicator.js +++ b/src/components/AvatarWithIndicator.js @@ -6,6 +6,7 @@ import styles from '../styles/styles'; import Tooltip from './Tooltip'; import * as UserUtils from '../libs/UserUtils'; import Indicator from './Indicator'; +import * as Expensicons from './Icon/Expensicons'; const propTypes = { /** URL for the avatar */ @@ -13,17 +14,24 @@ const propTypes = { /** To show a tooltip on hover */ tooltipText: PropTypes.string, + + /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ + fallbackIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), }; const defaultProps = { tooltipText: '', + fallbackIcon: Expensicons.FallbackAvatar, }; function AvatarWithIndicator(props) { return ( - + diff --git a/src/components/ImageView/index.js b/src/components/ImageView/index.js index e0dce180043b..1d90a5723016 100644 --- a/src/components/ImageView/index.js +++ b/src/components/ImageView/index.js @@ -22,13 +22,16 @@ const propTypes = { /** image file name */ fileName: PropTypes.string.isRequired, + + onError: PropTypes.func, }; const defaultProps = { isAuthTokenRequired: false, + onError: () => {}, }; -function ImageView({isAuthTokenRequired, url, fileName}) { +function ImageView({isAuthTokenRequired, url, fileName, onError}) { const [isLoading, setIsLoading] = useState(true); const [containerHeight, setContainerHeight] = useState(0); const [containerWidth, setContainerWidth] = useState(0); @@ -238,6 +241,7 @@ function ImageView({isAuthTokenRequired, url, fileName}) { resizeMode={zoomScale > 1 ? Image.resizeMode.center : Image.resizeMode.contain} onLoadStart={imageLoadingStart} onLoad={imageLoad} + onError={onError} /> {(isLoading || zoomScale === 0) && } @@ -268,6 +272,7 @@ function ImageView({isAuthTokenRequired, url, fileName}) { resizeMode={Image.resizeMode.contain} onLoadStart={imageLoadingStart} onLoad={imageLoad} + onError={onError} /> diff --git a/src/components/LHNOptionsList/OptionRowLHNData.js b/src/components/LHNOptionsList/OptionRowLHNData.js index cb9d927c505c..87358f05b9c9 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.js +++ b/src/components/LHNOptionsList/OptionRowLHNData.js @@ -156,6 +156,7 @@ const personalDetailsSelector = (personalDetails) => firstName: personalData.firstName, status: personalData.status, avatar: UserUtils.getAvatar(personalData.avatar, personalData.accountID), + fallbackIcon: personalData.fallbackIcon, }; return finalPersonalDetails; }, diff --git a/src/components/MentionSuggestions.js b/src/components/MentionSuggestions.js index 11df8a597ded..b3374279f66b 100644 --- a/src/components/MentionSuggestions.js +++ b/src/components/MentionSuggestions.js @@ -81,6 +81,7 @@ function MentionSuggestions(props) { name={item.icons[0].name} type={item.icons[0].type} fill={themeColors.success} + fallbackIcon={item.icons[0].fallbackIcon} /> @@ -184,6 +185,7 @@ function MultipleAvatars(props) { size={props.size} name={icon.name} type={icon.type} + fallbackIcon={icon.fallbackIcon} /> @@ -249,6 +251,7 @@ function MultipleAvatars(props) { imageStyles={[singleAvatarStyle]} name={props.icons[0].name} type={props.icons[0].type} + fallbackIcon={props.icons[0].fallbackIcon} /> @@ -270,6 +273,7 @@ function MultipleAvatars(props) { imageStyles={[singleAvatarStyle]} name={props.icons[1].name} type={props.icons[1].type} + fallbackIcon={props.icons[1].fallbackIcon} /> diff --git a/src/components/RoomHeaderAvatars.js b/src/components/RoomHeaderAvatars.js index af594dc2415a..92f294f056c7 100644 --- a/src/components/RoomHeaderAvatars.js +++ b/src/components/RoomHeaderAvatars.js @@ -49,6 +49,7 @@ function RoomHeaderAvatars(props) { size={CONST.AVATAR_SIZE.LARGE} name={props.icons[0].name} type={props.icons[0].type} + fallbackIcon={props.icons[0].fallbackIcon} /> )} @@ -93,6 +94,7 @@ function RoomHeaderAvatars(props) { containerStyles={[...iconStyle, StyleUtils.getAvatarBorderRadius(CONST.AVATAR_SIZE.LARGE_BORDERED, icon.type)]} name={icon.name} type={icon.type} + fallbackIcon={icon.fallbackIcon} /> )} diff --git a/src/components/SelectionList/UserListItem.js b/src/components/SelectionList/UserListItem.js index 98241c91deb1..0d37162a7995 100644 --- a/src/components/SelectionList/UserListItem.js +++ b/src/components/SelectionList/UserListItem.js @@ -25,6 +25,7 @@ function UserListItem({item, isFocused = false, showTooltip, onSelectRow, onDism source={lodashGet(item, 'avatar.source', '')} name={lodashGet(item, 'avatar.name', item.text)} type={lodashGet(item, 'avatar.type', CONST.ICON_TYPE_AVATAR)} + fallbackIcon={lodashGet(item, 'avatar.fallbackIcon')} /> ); diff --git a/src/components/SubscriptAvatar.js b/src/components/SubscriptAvatar.js index 038484e3f42d..81864d6e5af2 100644 --- a/src/components/SubscriptAvatar.js +++ b/src/components/SubscriptAvatar.js @@ -60,6 +60,7 @@ function SubscriptAvatar(props) { size={props.size || CONST.AVATAR_SIZE.DEFAULT} name={props.mainAvatar.name} type={props.mainAvatar.type} + fallbackIcon={props.mainAvatar.fallbackIcon} /> @@ -83,6 +84,7 @@ function SubscriptAvatar(props) { fill={themeColors.iconSuccessFill} name={props.secondaryAvatar.name} type={props.secondaryAvatar.type} + fallbackIcon={props.secondaryAvatar.fallbackIcon} /> diff --git a/src/components/UserDetailsTooltip/index.web.js b/src/components/UserDetailsTooltip/index.web.js index 1a78459d30a6..e961c237ae5f 100644 --- a/src/components/UserDetailsTooltip/index.web.js +++ b/src/components/UserDetailsTooltip/index.web.js @@ -48,6 +48,7 @@ function UserDetailsTooltip(props) { source={props.icon ? props.icon.source : UserUtils.getAvatar(userAvatar, userAccountID)} type={props.icon ? props.icon.type : CONST.ICON_TYPE_AVATAR} name={props.icon ? props.icon.name : userLogin} + fallbackIcon={lodashGet(props.icon, 'fallbackIcon')} /> {title} diff --git a/src/components/avatarPropTypes.js b/src/components/avatarPropTypes.js index 12ee5c622b4f..915eac995fcb 100644 --- a/src/components/avatarPropTypes.js +++ b/src/components/avatarPropTypes.js @@ -6,4 +6,5 @@ export default PropTypes.shape({ type: PropTypes.oneOf([CONST.ICON_TYPE_AVATAR, CONST.ICON_TYPE_WORKSPACE]), name: PropTypes.string, id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + fallbackIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), }); diff --git a/src/components/menuItemPropTypes.js b/src/components/menuItemPropTypes.js index 53216ab7cdc7..6272a7a2ef7d 100644 --- a/src/components/menuItemPropTypes.js +++ b/src/components/menuItemPropTypes.js @@ -98,7 +98,7 @@ const propTypes = { interactive: PropTypes.bool, /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ - fallbackIcon: PropTypes.func, + fallbackIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), /** Avatars to show on the right of the menu item */ floatRightAvatars: PropTypes.arrayOf(avatarPropTypes), diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 5924023bc8b1..173ed801136d 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -949,7 +949,7 @@ function getIconsForParticipants(participants, personalDetails) { const accountID = participantsList[i]; const avatarSource = UserUtils.getAvatar(lodashGet(personalDetails, [accountID, 'avatar'], ''), accountID); const displayNameLogin = lodashGet(personalDetails, [accountID, 'displayName']) || lodashGet(personalDetails, [accountID, 'login'], ''); - participantDetails.push([accountID, displayNameLogin, avatarSource]); + participantDetails.push([accountID, displayNameLogin, avatarSource, lodashGet(personalDetails, [accountID, 'fallBackIcon'])]); } const sortedParticipantDetails = _.chain(participantDetails) @@ -975,6 +975,7 @@ function getIconsForParticipants(participants, personalDetails) { source: sortedParticipantDetails[i][2], type: CONST.ICON_TYPE_AVATAR, name: sortedParticipantDetails[i][1], + fallBackIcon: sortedParticipantDetails[i][3], }; avatars.push(userIcon); } @@ -1031,6 +1032,7 @@ function getIcons(report, personalDetails, defaultIcon = null, defaultName = '', id: parentReportAction.actorAccountID, type: CONST.ICON_TYPE_AVATAR, name: lodashGet(personalDetails, [parentReportAction.actorAccountID, 'displayName'], ''), + fallbackIcon: lodashGet(personalDetails, [parentReportAction.actorAccountID, 'fallbackIcon']), }; return [memberIcon, workspaceIcon]; @@ -1045,6 +1047,7 @@ function getIcons(report, personalDetails, defaultIcon = null, defaultName = '', source: UserUtils.getAvatar(lodashGet(personalDetails, [actorAccountID, 'avatar']), actorAccountID), name: actorDisplayName, type: CONST.ICON_TYPE_AVATAR, + fallbackIcon: lodashGet(personalDetails, [parentReportAction.actorAccountID, 'fallbackIcon']), }; if (isWorkspaceThread(report)) { @@ -1059,6 +1062,7 @@ function getIcons(report, personalDetails, defaultIcon = null, defaultName = '', source: UserUtils.getAvatar(lodashGet(personalDetails, [report.ownerAccountID, 'avatar']), report.ownerAccountID), type: CONST.ICON_TYPE_AVATAR, name: lodashGet(personalDetails, [report.ownerAccountID, 'displayName'], ''), + fallbackIcon: lodashGet(personalDetails, [report.ownerAccountID, 'fallbackIcon']), }; if (isWorkspaceTaskReport(report)) { @@ -1091,6 +1095,7 @@ function getIcons(report, personalDetails, defaultIcon = null, defaultName = '', id: report.ownerAccountID, type: CONST.ICON_TYPE_AVATAR, name: lodashGet(personalDetails, [report.ownerAccountID, 'displayName'], ''), + fallbackIcon: lodashGet(personalDetails, [report.ownerAccountID, 'fallbackIcon']), }; return isExpenseReport(report) ? [memberIcon, workspaceIcon] : [workspaceIcon, memberIcon]; } @@ -1100,12 +1105,14 @@ function getIcons(report, personalDetails, defaultIcon = null, defaultName = '', id: report.managerID, type: CONST.ICON_TYPE_AVATAR, name: lodashGet(personalDetails, [report.managerID, 'displayName'], ''), + fallbackIcon: lodashGet(personalDetails, [report.managerID, 'fallbackIcon']), }; const ownerIcon = { id: report.ownerAccountID, source: UserUtils.getAvatar(lodashGet(personalDetails, [report.ownerAccountID, 'avatar']), report.ownerAccountID), type: CONST.ICON_TYPE_AVATAR, name: lodashGet(personalDetails, [report.ownerAccountID, 'displayName'], ''), + fallbackIcon: lodashGet(personalDetails, [report.ownerAccountID, 'fallbackIcon']), }; const isPayer = currentUserAccountID === report.managerID; diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js index dd80992e64a4..69cf05b89b34 100644 --- a/src/libs/actions/PersonalDetails.js +++ b/src/libs/actions/PersonalDetails.js @@ -429,6 +429,7 @@ function updateAvatar(file) { avatar: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, originalFileName: null, }, + fallbackIcon: file.uri, }, }, }, @@ -479,6 +480,7 @@ function deleteAvatar() { value: { [currentUserAccountID]: { avatar: defaultAvatar, + fallbackIcon: null, }, }, }, @@ -490,6 +492,7 @@ function deleteAvatar() { value: { [currentUserAccountID]: { avatar: allPersonalDetails[currentUserAccountID].avatar, + fallbackIcon: allPersonalDetails[currentUserAccountID].fallbackIcon, }, }, }, diff --git a/src/pages/DetailsPage.js b/src/pages/DetailsPage.js index 058ceef16b4c..2f8d42c686a9 100755 --- a/src/pages/DetailsPage.js +++ b/src/pages/DetailsPage.js @@ -155,6 +155,7 @@ function DetailsPage(props) { imageStyles={[styles.avatarLarge]} source={UserUtils.getAvatar(details.avatar, details.accountID)} size={CONST.AVATAR_SIZE.LARGE} + fallbackIcon={details.fallbackIcon} /> diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js index 60037e4755c0..7e93f4d99be2 100755 --- a/src/pages/ProfilePage.js +++ b/src/pages/ProfilePage.js @@ -103,6 +103,7 @@ function ProfilePage(props) { const displayName = details.displayName ? details.displayName : props.translate('common.hidden'); const avatar = lodashGet(details, 'avatar', UserUtils.getDefaultAvatar()); + const fallbackIcon = lodashGet(details, 'fallbackIcon', ''); const originalFileName = lodashGet(details, 'originalFileName', ''); const login = lodashGet(details, 'login', ''); const timezone = lodashGet(details, 'timezone', {}); @@ -161,6 +162,7 @@ function ProfilePage(props) { source={UserUtils.getFullSizeAvatar(avatar, accountID)} isAuthTokenRequired originalFileName={originalFileName} + fallbackSource={fallbackIcon} > {({show}) => ( diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.js b/src/pages/home/report/ReportActionCompose/SuggestionMention.js index 08afc8179626..62e98a697709 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionMention.js +++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.js @@ -169,6 +169,7 @@ function SuggestionMention({ name: detail.login, source: UserUtils.getAvatar(detail.avatar, detail.accountID), type: 'avatar', + fallbackIcon: detail.fallbackIcon, }, ], }); diff --git a/src/pages/home/report/ReportActionItemSingle.js b/src/pages/home/report/ReportActionItemSingle.js index 86e5e75522e9..ca0467143e98 100644 --- a/src/pages/home/report/ReportActionItemSingle.js +++ b/src/pages/home/report/ReportActionItemSingle.js @@ -90,7 +90,7 @@ const showWorkspaceDetails = (reportID) => { function ReportActionItemSingle(props) { const actorAccountID = props.action.actorAccountID; let {displayName} = props.personalDetailsList[actorAccountID] || {}; - const {avatar, login, pendingFields, status} = props.personalDetailsList[actorAccountID] || {}; + const {avatar, login, pendingFields, status, fallbackIcon} = props.personalDetailsList[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); @@ -200,6 +200,7 @@ function ReportActionItemSingle(props) { source={icon.source} type={icon.type} name={icon.name} + fallbackIcon={fallbackIcon} /> diff --git a/src/pages/home/sidebar/PressableAvatarWithIndicator.js b/src/pages/home/sidebar/PressableAvatarWithIndicator.js index ef6e663ce705..7b240c108e4e 100644 --- a/src/pages/home/sidebar/PressableAvatarWithIndicator.js +++ b/src/pages/home/sidebar/PressableAvatarWithIndicator.js @@ -52,6 +52,7 @@ function PressableAvatarWithIndicator({isCreateMenuOpen, currentUserPersonalDeta diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js index d10779210b09..86eff304df9b 100755 --- a/src/pages/settings/InitialSettingsPage.js +++ b/src/pages/settings/InitialSettingsPage.js @@ -348,6 +348,7 @@ function InitialSettingsPage(props) { imageStyles={[styles.avatarXLarge]} source={UserUtils.getAvatar(props.currentUserPersonalDetails.avatar, props.session.accountID)} size={CONST.AVATAR_SIZE.XLARGE} + fallbackIcon={props.currentUserPersonalDetails.fallbackIcon} /> diff --git a/src/pages/settings/Profile/LoungeAccessPage.js b/src/pages/settings/Profile/LoungeAccessPage.js index c322c8e426f3..035dca3f5cbe 100644 --- a/src/pages/settings/Profile/LoungeAccessPage.js +++ b/src/pages/settings/Profile/LoungeAccessPage.js @@ -74,6 +74,7 @@ function LoungeAccessPage(props) { imageStyles={[styles.avatarLarge]} source={UserUtils.getAvatar(props.currentUserPersonalDetails.avatar, props.session.accountID)} size={CONST.AVATAR_SIZE.LARGE} + fallbackIcon={props.currentUserPersonalDetails.fallbackIcon} /> {_.map(profileSettingsOptions, (detail, index) => (