diff --git a/src/languages/de.ts b/src/languages/de.ts index 9de8a6238564..94b8738606a7 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -2050,6 +2050,8 @@ const translations: TranslationDeepObject = { profileAvatar: 'Profil-Avatar', customInstructions: 'Benutzerdefinierte Anweisungen', copilotIntoAccount: 'Copilot in Konto', + viewUserHistory: 'Nutzerverlauf anzeigen', + viewAgentHistory: 'Agentenverlauf anzeigen', publicSection: { title: 'Öffentlich', subtitle: 'Diese Angaben werden in deinem öffentlichen Profil angezeigt. Jede:r kann sie sehen.', diff --git a/src/languages/el.ts b/src/languages/el.ts index 81d3aeb08997..6e9e66d59b93 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -2099,6 +2099,8 @@ const translations: TranslationDeepObject = { profileAvatar: 'Εικόνα προφίλ', customInstructions: 'Προσαρμοσμένες οδηγίες', copilotIntoAccount: 'Οδηγός εντός λογαριασμού', + viewUserHistory: 'Προβολή ιστορικού χρήστη', + viewAgentHistory: 'Προβολή ιστορικού αντιπροσώπου', publicSection: { title: 'Δημόσιο', subtitle: 'Αυτές οι πληροφορίες εμφανίζονται στο δημόσιο προφίλ σας. Μπορεί να τις δει οποιοσδήποτε.', diff --git a/src/languages/en.ts b/src/languages/en.ts index 774e7a4b5a93..3f80a9939ac3 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2143,6 +2143,8 @@ const translations = { profileAvatar: 'Profile avatar', customInstructions: 'Custom instructions', copilotIntoAccount: 'Copilot into account', + viewUserHistory: 'View user history', + viewAgentHistory: 'View agent history', publicSection: { title: 'Public', subtitle: 'These details are displayed on your public profile. Anyone can see them.', diff --git a/src/languages/es.ts b/src/languages/es.ts index d0af721f6450..994e55c8e6ba 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1972,6 +1972,8 @@ const translations: TranslationDeepObject = { profileAvatar: 'Perfil avatar', customInstructions: 'Instrucciones personalizadas', copilotIntoAccount: 'Copilot a la cuenta', + viewUserHistory: 'Ver historial del usuario', + viewAgentHistory: 'Ver historial del agente', publicSection: { title: 'Público', subtitle: 'Estos detalles se muestran en tu perfil público, a disposición de los demás.', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 42787610f428..d20fdb5eace3 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -2055,6 +2055,8 @@ const translations: TranslationDeepObject = { profileAvatar: 'Avatar de profil', customInstructions: 'Instructions personnalisées', copilotIntoAccount: 'Copilot dans le compte', + viewUserHistory: 'Voir l’historique de l’utilisateur', + viewAgentHistory: 'Voir l’historique de l’agent', publicSection: { title: 'Public', subtitle: 'Ces informations sont affichées sur votre profil public. Tout le monde peut les voir.', diff --git a/src/languages/it.ts b/src/languages/it.ts index 6a362596b501..5a83f1b7cc04 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -2047,6 +2047,8 @@ const translations: TranslationDeepObject = { profileAvatar: 'Avatar profilo', customInstructions: 'Istruzioni personalizzate', copilotIntoAccount: "Copilot nell'account", + viewUserHistory: 'Visualizza cronologia utente', + viewAgentHistory: 'Visualizza cronologia agente', publicSection: { title: 'Pubblico', subtitle: 'Questi dettagli vengono visualizzati nel tuo profilo pubblico. Chiunque può vederli.', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 9a5267c3cefe..88a4e111d28a 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -2028,6 +2028,8 @@ const translations: TranslationDeepObject = { profileAvatar: 'プロフィールアバター', customInstructions: 'カスタム指示', copilotIntoAccount: 'アカウントにCopilot', + viewUserHistory: 'ユーザー履歴を表示', + viewAgentHistory: 'エージェント履歴を表示', publicSection: { title: '公開', subtitle: 'これらの詳細はあなたの公開プロフィールに表示され、誰でも閲覧できます。', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 3cd6ee5a3860..9f473de6dd5a 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -2042,6 +2042,8 @@ const translations: TranslationDeepObject = { profileAvatar: 'Profielavatar', customInstructions: 'Aangepaste instructies', copilotIntoAccount: 'Copilot naar account', + viewUserHistory: 'Gebruikersgeschiedenis bekijken', + viewAgentHistory: 'Agentgeschiedenis bekijken', publicSection: { title: 'Openbaar', subtitle: 'Deze gegevens worden weergegeven op je openbare profiel. Iedereen kan ze zien.', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 303b2c3eb23d..e74530afd27a 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -2040,6 +2040,8 @@ const translations: TranslationDeepObject = { profileAvatar: 'Awatar profilu', customInstructions: 'Niestandardowe instrukcje', copilotIntoAccount: 'Copilot do konta', + viewUserHistory: 'Zobacz historię użytkownika', + viewAgentHistory: 'Zobacz historię agenta', publicSection: { title: 'Public', subtitle: 'Te dane są wyświetlane w Twoim publicznym profilu. Każdy może je zobaczyć.', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 8f41bd76602d..ff09f7b17864 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -2037,6 +2037,8 @@ const translations: TranslationDeepObject = { profileAvatar: 'Avatar do perfil', customInstructions: 'Instruções personalizadas', copilotIntoAccount: 'Copilot na conta', + viewUserHistory: 'Ver histórico do usuário', + viewAgentHistory: 'Ver histórico do agente', publicSection: { title: 'Público', subtitle: 'Esses detalhes são exibidos no seu perfil público. Qualquer pessoa pode vê-los.', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index e09675eb46e8..12ad8386f4af 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -1973,6 +1973,8 @@ const translations: TranslationDeepObject = { profileAvatar: '个人头像', customInstructions: '自定义指令', copilotIntoAccount: 'Copilot 到账户', + viewUserHistory: '查看用户历史记录', + viewAgentHistory: '查看代理历史记录', publicSection: { title: '公开', subtitle: '这些详细信息会显示在你的公开资料中,任何人都可以看到。', diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx index 3a9f37c8b6a8..61f3c91caa1a 100755 --- a/src/pages/ProfilePage.tsx +++ b/src/pages/ProfilePage.tsx @@ -35,6 +35,8 @@ import { isHiddenForCurrentUser as isReportHiddenForCurrentUser, navigateToPrivateNotes, } from '@libs/ReportUtils'; +import {buildQueryStringFromFilterFormValues} from '@libs/SearchQueryUtils'; +import {isAgentEmail} from '@libs/SessionUtils'; import {generateAccountID} from '@libs/UserUtils'; import {isValidAccountRoute} from '@libs/ValidationUtils'; @@ -92,7 +94,7 @@ function ProfilePage({route}: ProfilePageProps) { const [guidedSetupAndTourStatus] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: guidedSetupAndTourStatusSelector}); const switchToDelegator = useSwitchToDelegator(); const guideCalendarLink = account?.guideDetails?.calendarLink ?? ''; - const expensifyIcons = useMemoizedLazyExpensifyIcons(['Bug', 'Pencil', 'Phone', 'UserPlus']); + const expensifyIcons = useMemoizedLazyExpensifyIcons(['Bug', 'MagnifyingGlass', 'Pencil', 'Phone', 'UserPlus']); const accountID = Number(route.params?.accountID ?? CONST.DEFAULT_NUMBER_ID); const [agentPrompt] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_AGENT_PROMPT}${accountID}`); const isCurrentUser = currentUserAccountID === accountID; @@ -283,6 +285,18 @@ function ProfilePage({route}: ProfilePageProps) { ) : null} {shouldShowLocalTime && } + {shouldShowNotificationPreference && ( + + { + Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.NOTIFICATION_PREFERENCES.getRoute(report.reportID))); + }} + /> + + )} {isCurrentUser && ( switchToDelegator(login))} /> )} - {shouldShowNotificationPreference && ( - { - Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.NOTIFICATION_PREFERENCES.getRoute(report.reportID))); + const query = buildQueryStringFromFilterFormValues({ + type: CONST.SEARCH.DATA_TYPES.CHAT, + from: [String(accountID)], + }); + Navigation.navigate(ROUTES.SEARCH_ROOT.getRoute({query, rawQuery: query})); }} /> )} diff --git a/tests/ui/ProfilePageTest.tsx b/tests/ui/ProfilePageTest.tsx index e4ef81203eef..fdc8900701cf 100644 --- a/tests/ui/ProfilePageTest.tsx +++ b/tests/ui/ProfilePageTest.tsx @@ -9,14 +9,16 @@ import OnyxListItemProvider from '@components/OnyxListItemProvider'; import {CurrentReportIDContextProvider} from '@hooks/useCurrentReportID'; import * as AgentActions from '@libs/actions/Agent'; -import {navigationRef} from '@libs/Navigation/Navigation'; +import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; import createPlatformStackNavigator from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator'; -import type {SettingsSplitNavigatorParamList} from '@libs/Navigation/types'; +import type {ProfileNavigatorParamList, SettingsSplitNavigatorParamList} from '@libs/Navigation/types'; +import PublicProfilePage from '@pages/ProfilePage'; import ProfilePage from '@pages/settings/Profile/ProfilePage'; -import type CONST from '@src/CONST'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import type {PersonalDetails, PersonalDetailsList} from '@src/types/onyx'; @@ -64,6 +66,9 @@ jest.mock('@react-navigation/native', () => { useRoute: jest.fn(() => ({params: {}})), createNavigationContainerRef: () => ({ getState: () => jest.fn(), + // useRootNavigationState reads the ref on mount and subscribes to it, so the stub has to answer both. + isReady: () => false, + addListener: () => () => {}, }), usePreventRemove: jest.fn(), }; @@ -479,6 +484,135 @@ describe('ProfilePage - agent account', () => { }); }); +const PublicProfileStack = createPlatformStackNavigator(); + +const CURRENT_USER_ACCOUNT_ID = 1; +const CURRENT_USER_EMAIL = 'current@expensify.com'; +const PUBLIC_PROFILE_ACCOUNT_ID = 123; + +describe('ProfilePage - View user history', () => { + beforeAll(async () => { + Onyx.init({ + keys: ONYXKEYS, + }); + + await act(async () => { + await Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, 'en' as const); + }); + await waitForBatchedUpdatesWithAct(); + }); + + afterEach(async () => { + jest.clearAllMocks(); + await Onyx.clear(); + await waitForBatchedUpdatesWithAct(); + }); + + async function setUpPublicProfile(login: string) { + await TestHelper.signInWithTestUser(CURRENT_USER_ACCOUNT_ID, CURRENT_USER_EMAIL); + + const personalDetails: PersonalDetailsList = { + [CURRENT_USER_ACCOUNT_ID]: { + accountID: CURRENT_USER_ACCOUNT_ID, + login: CURRENT_USER_EMAIL, + displayName: CURRENT_USER_EMAIL, + } as PersonalDetails, + [PUBLIC_PROFILE_ACCOUNT_ID]: { + accountID: PUBLIC_PROFILE_ACCOUNT_ID, + login, + displayName: login, + avatar: 'https://example.com/avatar.png', + } as PersonalDetails, + }; + + await act(async () => { + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, personalDetails); + await Onyx.merge(ONYXKEYS.IS_LOADING_APP, false); + }); + await waitForBatchedUpdatesWithAct(); + } + + function renderPublicProfilePage() { + return render( + + + + + + + + + , + ); + } + + it('shows the search entry for a regular user', async () => { + await setUpPublicProfile('user@expensify.com'); + + renderPublicProfilePage(); + await waitForBatchedUpdatesWithAct(); + + expect(screen.getByText('View user history')).toBeOnTheScreen(); + expect(screen.queryByText('View agent history')).not.toBeOnTheScreen(); + }); + + it('shows the agent wording for an agent account', async () => { + await setUpPublicProfile('agent_456@expensify.ai'); + + renderPublicProfilePage(); + await waitForBatchedUpdatesWithAct(); + + expect(screen.getByText('View agent history')).toBeOnTheScreen(); + expect(screen.queryByText('View user history')).not.toBeOnTheScreen(); + }); + + it('navigates to a chat search filtered by the profile account when pressed', async () => { + await setUpPublicProfile('user@expensify.com'); + + renderPublicProfilePage(); + await waitForBatchedUpdatesWithAct(); + + // MenuItem only forwards the press to onPress when it receives an event, so pass a minimal one. + fireEvent.press(screen.getByText('View user history'), {nativeEvent: {}}); + await waitForBatchedUpdatesWithAct(); + + expect(Navigation.navigate).toHaveBeenCalledWith( + ROUTES.SEARCH_ROOT.getRoute({ + query: `type:${CONST.SEARCH.DATA_TYPES.CHAT} from:${PUBLIC_PROFILE_ACCOUNT_ID}`, + rawQuery: `type:${CONST.SEARCH.DATA_TYPES.CHAT} from:${PUBLIC_PROFILE_ACCOUNT_ID}`, + }), + ); + }); + + it('hides the search entry for an anonymous session', async () => { + await setUpPublicProfile('user@expensify.com'); + + await act(async () => { + await Onyx.merge(ONYXKEYS.SESSION, {authTokenType: CONST.AUTH_TOKEN_TYPES.ANONYMOUS}); + }); + await waitForBatchedUpdatesWithAct(); + + renderPublicProfilePage(); + await waitForBatchedUpdatesWithAct(); + + expect(screen.queryByText('View user history')).not.toBeOnTheScreen(); + }); + + it('hides the search entry when the account has no login', async () => { + await setUpPublicProfile(''); + + renderPublicProfilePage(); + await waitForBatchedUpdatesWithAct(); + + expect(screen.queryByText('View user history')).not.toBeOnTheScreen(); + expect(screen.queryByText('View agent history')).not.toBeOnTheScreen(); + }); +}); + describe('ProfilePage - SMS domain handling', () => { beforeAll(async () => { Onyx.init({