diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 5ebe05eb93e2..42123aa9b4a4 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -29,6 +29,10 @@ const ROUTES = { route: 'a/:accountID', getRoute: (accountID: string | number, backTo?: string) => getUrlWithBackToParam(`a/${accountID}`, backTo), }, + PROFILE_AVATAR: { + route: 'a/:accountID/avatar', + getRoute: (accountID: string) => `a/${accountID}/avatar` as const, + }, TRANSITION_BETWEEN_APPS: 'transition', VALIDATE_LOGIN: 'v/:accountID/:validateCode', @@ -140,6 +144,7 @@ const ROUTES = { getRoute: (backTo?: string) => getUrlWithBackToParam('settings/security/two-factor-auth', backTo), }, SETTINGS_STATUS: 'settings/profile/status', + SETTINGS_STATUS_CLEAR_AFTER: 'settings/profile/status/clear-after', SETTINGS_STATUS_CLEAR_AFTER_DATE: 'settings/profile/status/clear-after/date', SETTINGS_STATUS_CLEAR_AFTER_TIME: 'settings/profile/status/clear-after/time', @@ -155,6 +160,10 @@ const ROUTES = { route: 'r/:reportID?/:reportActionID?', getRoute: (reportID: string) => `r/${reportID}` as const, }, + REPORT_AVATAR: { + route: 'r/:reportID/avatar', + getRoute: (reportID: string) => `r/${reportID}/avatar` as const, + }, EDIT_REQUEST: { route: 'r/:threadReportID/edit/:field', getRoute: (threadReportID: string, field: ValueOf) => `r/${threadReportID}/edit/${field}` as const, @@ -438,6 +447,10 @@ const ROUTES = { route: 'workspace/:policyID/settings', getRoute: (policyID: string) => `workspace/${policyID}/settings` as const, }, + WORKSPACE_AVATAR: { + route: 'workspace/:policyID/avatar', + getRoute: (policyID: string) => `workspace/${policyID}/avatar` as const, + }, WORKSPACE_SETTINGS_CURRENCY: { route: 'workspace/:policyID/settings/currency', getRoute: (policyID: string) => `workspace/${policyID}/settings/currency` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index e2f4a849d4aa..960991eb277b 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -13,6 +13,9 @@ const PROTECTED_SCREENS = { const SCREENS = { ...PROTECTED_SCREENS, REPORT: 'Report', + PROFILE_AVATAR: 'ProfileAvatar', + WORKSPACE_AVATAR: 'WorkspaceAvatar', + REPORT_AVATAR: 'ReportAvatar', NOT_FOUND: 'not-found', TRANSITION_BETWEEN_APPS: 'TransitionBetweenApps', VALIDATE_LOGIN: 'ValidateLogin', diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index bd8d535e540f..fdc7840c9b38 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -22,17 +22,21 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import useNativeDriver from '@libs/useNativeDriver'; import reportPropTypes from '@pages/reportPropTypes'; +import variables from '@styles/variables'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import AttachmentCarousel from './Attachments/AttachmentCarousel'; import AttachmentView from './Attachments/AttachmentView'; +import BlockingView from './BlockingViews/BlockingView'; import Button from './Button'; import ConfirmModal from './ConfirmModal'; +import FullScreenLoadingIndicator from './FullscreenLoadingIndicator'; import HeaderGap from './HeaderGap'; import HeaderWithBackButton from './HeaderWithBackButton'; import * as Expensicons from './Icon/Expensicons'; +import * as Illustrations from './Icon/Illustrations'; import sourcePropTypes from './Image/sourcePropTypes'; import Modal from './Modal'; import SafeAreaConsumer from './SafeAreaConsumer'; @@ -61,6 +65,9 @@ const propTypes = { /** Optional callback to fire when we want to do something after modal hide. */ onModalHide: PropTypes.func, + /** Trigger when we explicity click close button in ProfileAttachment modal */ + onModalClose: PropTypes.func, + /** Optional callback to fire when we want to do something after attachment carousel changes. */ onCarouselAttachmentChange: PropTypes.func, @@ -85,6 +92,12 @@ const propTypes = { /** The transaction associated with the receipt attachment, if any */ transaction: transactionPropTypes, + /** The data is loading or not */ + isLoading: PropTypes.bool, + + /** Should display not found page or not */ + shouldShowNotFoundPage: PropTypes.bool, + ...withLocalizePropTypes, ...windowDimensionsPropTypes, @@ -114,6 +127,9 @@ const defaultProps = { onModalHide: () => {}, onCarouselAttachmentChange: () => {}, isWorkspaceAvatar: false, + onModalClose: () => {}, + isLoading: false, + shouldShowNotFoundPage: false, isReceiptAttachment: false, canEditReceipt: false, }; @@ -352,7 +368,13 @@ function AttachmentModal(props) { */ const closeModal = useCallback(() => { setIsModalOpen(false); - }, []); + + if (typeof props.onModalClose === 'function') { + props.onModalClose(); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [props.onModalClose]); /** * open the modal @@ -461,6 +483,19 @@ function AttachmentModal(props) { shouldOverlay /> + {props.isLoading && } + {props.shouldShowNotFoundPage && !props.isLoading && ( + Navigation.dismissModal()} + /> + )} {!_.isEmpty(props.report) && !props.isReceiptAttachment ? ( ) : ( Boolean(sourceForAttachmentView) && - shouldLoadAttachment && ( + shouldLoadAttachment && + !props.isLoading && + !props.shouldShowNotFoundPage && ( { + if (typeof onViewPhotoPress !== 'function') { + show(); + return; + } + onViewPhotoPress(); + }, }); } diff --git a/src/components/RoomHeaderAvatars.js b/src/components/RoomHeaderAvatars.js index a3394fe71539..64cc9ac7abf3 100644 --- a/src/components/RoomHeaderAvatars.js +++ b/src/components/RoomHeaderAvatars.js @@ -4,9 +4,9 @@ import {View} from 'react-native'; import _ from 'underscore'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as UserUtils from '@libs/UserUtils'; +import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; -import AttachmentModal from './AttachmentModal'; +import ROUTES from '@src/ROUTES'; import Avatar from './Avatar'; import avatarPropTypes from './avatarPropTypes'; import PressableWithoutFocus from './Pressable/PressableWithoutFocus'; @@ -14,13 +14,23 @@ import Text from './Text'; const propTypes = { icons: PropTypes.arrayOf(avatarPropTypes), + reportID: PropTypes.string, }; const defaultProps = { icons: [], + reportID: '', }; function RoomHeaderAvatars(props) { + const navigateToAvatarPage = (icon) => { + if (icon.type === CONST.ICON_TYPE_WORKSPACE) { + Navigation.navigate(ROUTES.REPORT_AVATAR.getRoute(props.reportID)); + return; + } + Navigation.navigate(ROUTES.PROFILE_AVATAR.getRoute(icon.id)); + }; + const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); if (!props.icons.length) { @@ -29,31 +39,21 @@ function RoomHeaderAvatars(props) { if (props.icons.length === 1) { return ( - navigateToAvatarPage(props.icons[0])} + accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} + accessibilityLabel={props.icons[0].name} > - {({show}) => ( - - - - )} - + + ); } @@ -73,31 +73,21 @@ function RoomHeaderAvatars(props) { key={`${icon.id}${index}`} style={[styles.justifyContentCenter, styles.alignItemsCenter]} > - navigateToAvatarPage(icon)} + accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} + accessibilityLabel={icon.name} > - {({show}) => ( - - - - )} - + + {index === CONST.REPORT.MAX_PREVIEW_AVATARS - 1 && props.icons.length - CONST.REPORT.MAX_PREVIEW_AVATARS !== 0 && ( <> require('../../../pages/home/sidebar/SidebarScre const loadValidateLoginPage = () => require('../../../pages/ValidateLoginPage').default as React.ComponentType; const loadLogOutPreviousUserPage = () => require('../../../pages/LogOutPreviousUserPage').default as React.ComponentType; const loadConciergePage = () => require('../../../pages/ConciergePage').default as React.ComponentType; +const loadProfileAvatar = () => require('../../../pages/settings/Profile/ProfileAvatar').default as React.ComponentType; +const loadWorkspaceAvatar = () => require('../../../pages/workspace/WorkspaceAvatar').default as React.ComponentType; +const loadReportAvatar = () => require('../../../pages/ReportAvatar').default as React.ComponentType; let timezone: Timezone | null; let currentAccountID = -1; @@ -298,6 +301,33 @@ function AuthScreens({session, lastOpenedPublicRoomID, isUsingMemoryOnlyKeys = f getComponent={loadReportAttachments} listeners={modalScreenListeners} /> + + + = { [SCREENS.SAML_SIGN_IN]: ROUTES.SAML_SIGN_IN, [SCREENS.DESKTOP_SIGN_IN_REDIRECT]: ROUTES.DESKTOP_SIGN_IN_REDIRECT, [SCREENS.REPORT_ATTACHMENTS]: ROUTES.REPORT_ATTACHMENTS.route, + [SCREENS.PROFILE_AVATAR]: ROUTES.PROFILE_AVATAR.route, + [SCREENS.WORKSPACE_AVATAR]: ROUTES.WORKSPACE_AVATAR.route, + [SCREENS.REPORT_AVATAR]: ROUTES.REPORT_AVATAR.route, // Sidebar [SCREENS.HOME]: { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index aa5ab47ad9ba..dd5a7720f00d 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -418,6 +418,15 @@ type AuthScreensParamList = { reportID: string; source: string; }; + [SCREENS.PROFILE_AVATAR]: { + accountID: string; + }; + [SCREENS.WORKSPACE_AVATAR]: { + policyID: string; + }; + [SCREENS.REPORT_AVATAR]: { + reportID: string; + }; [SCREENS.NOT_FOUND]: undefined; [NAVIGATORS.LEFT_MODAL_NAVIGATOR]: NavigatorScreenParams; [NAVIGATORS.RIGHT_MODAL_NAVIGATOR]: NavigatorScreenParams; diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js index c0c782f176ca..f054eaf4ad07 100755 --- a/src/pages/ProfilePage.js +++ b/src/pages/ProfilePage.js @@ -5,7 +5,6 @@ import React, {useEffect} from 'react'; import {ScrollView, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; -import AttachmentModal from '@components/AttachmentModal'; import AutoUpdateTime from '@components/AutoUpdateTime'; import Avatar from '@components/Avatar'; import BlockingView from '@components/BlockingViews/BlockingView'; @@ -103,7 +102,6 @@ function ProfilePage(props) { const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(details); 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', {}); @@ -154,32 +152,22 @@ function ProfilePage(props) { {hasMinimumDetails && ( - Navigation.navigate(ROUTES.PROFILE_AVATAR.getRoute(String(accountID)))} + accessibilityLabel={props.translate('common.profile')} + accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} > - {({show}) => ( - - - - - - )} - + + + + {Boolean(displayName) && ( ; + isLoadingApp: OnyxEntry; + policies: OnyxCollection; +}; + +type ReportAvatarProps = ReportAvatarOnyxProps & StackScreenProps; + +function ReportAvatar({report = {} as Report, policies, isLoadingApp = true}: ReportAvatarProps) { + const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID ?? '0'}`]; + const isArchivedRoom = ReportUtils.isArchivedRoom(report); + const policyName = isArchivedRoom ? report?.oldPolicyName : policy?.name; + const avatarURL = policy?.avatar ?? '' ? policy?.avatar ?? '' : ReportUtils.getDefaultWorkspaceAvatar(policyName); + + return ( + { + Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report?.reportID ?? '')); + }} + isWorkspaceAvatar + originalFileName={policy?.originalFileName ?? policyName} + shouldShowNotFoundPage={!report?.reportID && !isLoadingApp} + isLoading={(!report?.reportID || !policy?.id) && isLoadingApp} + /> + ); +} + +ReportAvatar.displayName = 'ReportAvatar'; + +export default withOnyx({ + report: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID ?? ''}`, + }, + isLoadingApp: { + key: ONYXKEYS.IS_LOADING_APP, + }, + policies: { + key: ONYXKEYS.COLLECTION.POLICY, + }, +})(ReportAvatar); diff --git a/src/pages/ReportDetailsPage.js b/src/pages/ReportDetailsPage.js index ff9ed62c6a65..3e682d592370 100644 --- a/src/pages/ReportDetailsPage.js +++ b/src/pages/ReportDetailsPage.js @@ -197,7 +197,10 @@ function ReportDetailsPage(props) { size={CONST.AVATAR_SIZE.LARGE} /> ) : ( - + )} diff --git a/src/pages/settings/Profile/ProfileAvatar.tsx b/src/pages/settings/Profile/ProfileAvatar.tsx new file mode 100644 index 000000000000..2aa0f52609c1 --- /dev/null +++ b/src/pages/settings/Profile/ProfileAvatar.tsx @@ -0,0 +1,60 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useEffect} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import AttachmentModal from '@components/AttachmentModal'; +import Navigation from '@libs/Navigation/Navigation'; +import type {AuthScreensParamList} from '@libs/Navigation/types'; +import * as UserUtils from '@libs/UserUtils'; +import * as ValidationUtils from '@libs/ValidationUtils'; +import * as PersonalDetails from '@userActions/PersonalDetails'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import type {PersonalDetailsList} from '@src/types/onyx'; + +type ProfileAvatarOnyxProps = { + personalDetails: OnyxEntry; + isLoadingApp: OnyxEntry; +}; + +type ProfileAvatarProps = ProfileAvatarOnyxProps & StackScreenProps; + +function ProfileAvatar({route, personalDetails, isLoadingApp = true}: ProfileAvatarProps) { + const personalDetail = personalDetails?.[route.params.accountID]; + const avatarURL = personalDetail?.avatar ?? ''; + const accountID = Number(route.params.accountID ?? ''); + const isLoading = personalDetail?.isLoading ?? (isLoadingApp && !Object.keys(personalDetail ?? {}).length); + + useEffect(() => { + if (!ValidationUtils.isValidAccountRoute(Number(accountID)) ?? !!avatarURL) { + return; + } + PersonalDetails.openPublicProfilePage(accountID); + }, [accountID, avatarURL]); + + return ( + { + Navigation.goBack(); + }} + originalFileName={personalDetail?.originalFileName ?? ''} + isLoading={isLoading} + shouldShowNotFoundPage={!avatarURL} + /> + ); +} + +ProfileAvatar.displayName = 'ProfileAvatar'; + +export default withOnyx({ + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, + isLoadingApp: { + key: ONYXKEYS.IS_LOADING_APP, + }, +})(ProfileAvatar); diff --git a/src/pages/settings/Profile/ProfilePage.js b/src/pages/settings/Profile/ProfilePage.js index 89dfa4f0e419..99cc5cf7e35a 100755 --- a/src/pages/settings/Profile/ProfilePage.js +++ b/src/pages/settings/Profile/ProfilePage.js @@ -127,6 +127,7 @@ function ProfilePage(props) { errors={lodashGet(props.currentUserPersonalDetails, 'errorFields.avatar', null)} errorRowStyles={[styles.mt6]} onErrorClose={PersonalDetails.clearAvatarErrors} + onViewPhotoPress={() => Navigation.navigate(ROUTES.PROFILE_AVATAR.getRoute(accountID))} previewSource={UserUtils.getFullSizeAvatar(avatarURL, accountID)} originalFileName={currentUserDetails.originalFileName} headerTitle={props.translate('profilePage.profileAvatar')} diff --git a/src/pages/workspace/WorkspaceAvatar.tsx b/src/pages/workspace/WorkspaceAvatar.tsx new file mode 100644 index 000000000000..1a420ee0fbd3 --- /dev/null +++ b/src/pages/workspace/WorkspaceAvatar.tsx @@ -0,0 +1,51 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import AttachmentModal from '@components/AttachmentModal'; +import Navigation from '@libs/Navigation/Navigation'; +import type {AuthScreensParamList} from '@libs/Navigation/types'; +import * as ReportUtils from '@libs/ReportUtils'; +import * as UserUtils from '@libs/UserUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import type {Policy} from '@src/types/onyx'; + +type WorkspaceAvatarOnyxProps = { + policy: OnyxEntry; + isLoadingApp: OnyxEntry; +}; + +type WorkspaceAvatarProps = WorkspaceAvatarOnyxProps & StackScreenProps; + +function WorkspaceAvatar({route, policy, isLoadingApp = true}: WorkspaceAvatarProps) { + const avatarURL = policy?.avatar ?? '' ? policy?.avatar ?? '' : ReportUtils.getDefaultWorkspaceAvatar(policy?.name ?? ''); + + return ( + { + Navigation.goBack(ROUTES.WORKSPACE_SETTINGS.getRoute(route.params.policyID ?? '')); + }} + isWorkspaceAvatar + originalFileName={policy?.originalFileName ?? policy?.name ?? ''} + shouldShowNotFoundPage={!Object.keys(policy ?? {}).length && !isLoadingApp} + isLoading={!Object.keys(policy ?? {}).length && isLoadingApp} + /> + ); +} + +WorkspaceAvatar.displayName = 'WorkspaceAvatar'; + +export default withOnyx({ + policy: { + key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY}${route.params.policyID ?? ''}`, + }, + isLoadingApp: { + key: ONYXKEYS.IS_LOADING_APP, + }, +})(WorkspaceAvatar); diff --git a/src/pages/workspace/WorkspaceSettingsPage.js b/src/pages/workspace/WorkspaceSettingsPage.js index 737a5f1b7cf6..f15d0228aec4 100644 --- a/src/pages/workspace/WorkspaceSettingsPage.js +++ b/src/pages/workspace/WorkspaceSettingsPage.js @@ -112,6 +112,7 @@ function WorkspaceSettingsPage({policy, currencyList, windowWidth, route}) { enabledWhenOffline > Navigation.navigate(ROUTES.WORKSPACE_AVATAR.getRoute(policy.id))} source={lodashGet(policy, 'avatar')} size={CONST.AVATAR_SIZE.LARGE} DefaultAvatar={() => (