diff --git a/assets/images/avatars/fallback-avatar.svg b/assets/images/avatars/fallback-avatar.svg new file mode 100644 index 000000000000..dc1a1497cfe5 --- /dev/null +++ b/assets/images/avatars/fallback-avatar.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/images/avatars/fallback-workspace-avatar.svg b/assets/images/avatars/fallback-workspace-avatar.svg new file mode 100644 index 000000000000..ac2f58122a0f --- /dev/null +++ b/assets/images/avatars/fallback-workspace-avatar.svg @@ -0,0 +1,15 @@ + + + + + + + diff --git a/src/components/Avatar.js b/src/components/Avatar.js index 71ecd130ed9d..ff51d2c427b8 100644 --- a/src/components/Avatar.js +++ b/src/components/Avatar.js @@ -7,6 +7,7 @@ import Icon from './Icon'; import themeColors from '../styles/themes/default'; import CONST from '../CONST'; import * as StyleUtils from '../styles/StyleUtils'; +import * as Expensicons from './Icon/Expensicons'; const propTypes = { /** Source for the avatar. Can be a URL or an icon. */ @@ -23,6 +24,9 @@ const propTypes = { /** The fill color for the icon. Can be hex, rgb, rgba, or valid react-native named color such as 'red' or 'blue' */ fill: PropTypes.string, + + /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ + fallbackIcon: PropTypes.func, }; const defaultProps = { @@ -31,9 +35,17 @@ const defaultProps = { containerStyles: [], size: CONST.AVATAR_SIZE.DEFAULT, fill: themeColors.icon, + fallbackIcon: Expensicons.FallbackAvatar, }; class Avatar extends PureComponent { + constructor(props) { + super(props); + this.state = { + imageError: false, + }; + } + render() { if (!this.props.source) { return null; @@ -45,13 +57,21 @@ class Avatar extends PureComponent { ]; const iconSize = StyleUtils.getAvatarSize(this.props.size); + return ( - { - _.isFunction(this.props.source) - ? - : - } + {_.isFunction(this.props.source) || this.state.imageError + ? ( + + ) + : ( + this.setState({imageError: true})} /> + )} ); } diff --git a/src/components/AvatarWithImagePicker.js b/src/components/AvatarWithImagePicker.js index 02644c75fec2..2e0d4f3c8792 100644 --- a/src/components/AvatarWithImagePicker.js +++ b/src/components/AvatarWithImagePicker.js @@ -53,6 +53,9 @@ const propTypes = { /** Size of Indicator */ 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, + ...withLocalizePropTypes, }; @@ -65,6 +68,7 @@ const defaultProps = { isUsingDefaultAvatar: false, isUploading: false, size: CONST.AVATAR_SIZE.DEFAULT, + fallbackIcon: Expensicons.FallbackAvatar, }; class AvatarWithImagePicker extends React.Component { @@ -181,6 +185,8 @@ class AvatarWithImagePicker extends React.Component { containerStyles={styles.avatarLarge} imageStyles={[styles.avatarLarge, styles.alignSelfCenter]} source={this.props.avatarURL} + fallbackIcon={this.props.fallbackIcon} + size={this.props.size} /> ) : ( diff --git a/src/components/AvatarWithIndicator.js b/src/components/AvatarWithIndicator.js index 0db3ff1253ef..aa37769156e3 100644 --- a/src/components/AvatarWithIndicator.js +++ b/src/components/AvatarWithIndicator.js @@ -101,6 +101,7 @@ class AvatarWithIndicator extends PureComponent { diff --git a/src/components/Icon/Expensicons.js b/src/components/Icon/Expensicons.js index 90c47808b188..617bd2dcb7b5 100644 --- a/src/components/Icon/Expensicons.js +++ b/src/components/Icon/Expensicons.js @@ -76,6 +76,8 @@ import AdminRoomAvatar from '../../../assets/images/avatars/admin-room.svg'; import AnnounceRoomAvatar from '../../../assets/images/avatars/announce-room.svg'; import Connect from '../../../assets/images/connect.svg'; import DomainRoomAvatar from '../../../assets/images/avatars/domain-room.svg'; +import FallbackAvatar from '../../../assets/images/avatars/fallback-avatar.svg'; +import FallbackWorkspaceAvatar from '../../../assets/images/avatars/fallback-workspace-avatar.svg'; export { ActiveRoomAvatar, @@ -112,6 +114,8 @@ export { Eye, EyeDisabled, ExpensifyCard, + FallbackAvatar, + FallbackWorkspaceAvatar, Gallery, Gear, Hashtag, diff --git a/src/components/MenuItem.js b/src/components/MenuItem.js index 9e40683893ba..004fd45ad092 100644 --- a/src/components/MenuItem.js +++ b/src/components/MenuItem.js @@ -39,6 +39,7 @@ const defaultProps = { iconType: 'icon', onPress: () => {}, interactive: true, + fallbackIcon: Expensicons.FallbackAvatar, }; const MenuItem = props => ( @@ -87,6 +88,7 @@ const MenuItem = props => ( )} diff --git a/src/components/menuItemPropTypes.js b/src/components/menuItemPropTypes.js index 0ad5a43498af..0dd6b954aa4f 100644 --- a/src/components/menuItemPropTypes.js +++ b/src/components/menuItemPropTypes.js @@ -63,6 +63,9 @@ const propTypes = { /** Whether the menu item should be interactive at all */ interactive: PropTypes.bool, + + /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ + fallbackIcon: PropTypes.func, }; export default propTypes; diff --git a/src/pages/DetailsPage.js b/src/pages/DetailsPage.js index c0b3eb0827b9..b46e6dd68dfc 100755 --- a/src/pages/DetailsPage.js +++ b/src/pages/DetailsPage.js @@ -114,6 +114,7 @@ const DetailsPage = (props) => { containerStyles={[styles.avatarLarge, styles.mb3]} imageStyles={[styles.avatarLarge]} source={details.avatar} + size={CONST.AVATAR_SIZE.LARGE} /> )} diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js index 2bb4544fabb2..e9374be7f609 100755 --- a/src/pages/settings/InitialSettingsPage.js +++ b/src/pages/settings/InitialSettingsPage.js @@ -141,6 +141,7 @@ const InitialSettingsPage = (props) => { action: () => Navigation.navigate(ROUTES.getWorkspaceInitialRoute(policy.id)), iconStyles: [styles.popoverMenuIconEmphasized], iconFill: themeColors.iconReversed, + fallbackIcon: Expensicons.FallbackWorkspaceAvatar, })) .value(); menuItems.push(...defaultMenuItems); @@ -195,6 +196,7 @@ const InitialSettingsPage = (props) => { iconFill={item.iconFill} shouldShowRightIcon badgeText={(isPaymentItem && Permissions.canUseWallet(props.betas)) ? walletBalance : undefined} + fallbackIcon={item.fallbackIcon} /> ); })} diff --git a/src/pages/workspace/WorkspaceInitialPage.js b/src/pages/workspace/WorkspaceInitialPage.js index 3e43faf56c14..9cb442edb586 100644 --- a/src/pages/workspace/WorkspaceInitialPage.js +++ b/src/pages/workspace/WorkspaceInitialPage.js @@ -156,6 +156,8 @@ class WorkspaceInitialPage extends React.Component { containerStyles={styles.avatarLarge} imageStyles={[styles.avatarLarge, styles.alignSelfCenter]} source={this.props.policy.avatarURL} + fallbackIcon={Expensicons.FallbackWorkspaceAvatar} + size={CONST.AVATAR_SIZE.LARGE} /> ) : ( diff --git a/src/pages/workspace/WorkspaceSettingsPage.js b/src/pages/workspace/WorkspaceSettingsPage.js index 9f84eaf14b94..c9692d464ea4 100644 --- a/src/pages/workspace/WorkspaceSettingsPage.js +++ b/src/pages/workspace/WorkspaceSettingsPage.js @@ -153,6 +153,7 @@ class WorkspaceSettingsPage extends React.Component { fill={defaultTheme.iconSuccessFill} /> )} + fallbackIcon={Expensicons.FallbackWorkspaceAvatar} style={[styles.mb3]} anchorPosition={{top: 172, right: 18}} isUsingDefaultAvatar={!this.state.previewAvatarURL} diff --git a/src/styles/StyleUtils.js b/src/styles/StyleUtils.js index 7b1ced1f7ec6..4da69f75d427 100644 --- a/src/styles/StyleUtils.js +++ b/src/styles/StyleUtils.js @@ -36,6 +36,7 @@ function getAvatarStyle(size) { height: avatarSize, width: avatarSize, borderRadius: avatarSize, + backgroundColor: themeColors.offline, }; }