Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2050,6 +2050,8 @@ const translations: TranslationDeepObject<typeof en> = {
profileAvatar: 'Profil-Avatar',
customInstructions: 'Benutzerdefinierte Anweisungen',
copilotIntoAccount: 'Copilot in Konto',
searchThisUser: 'Nutzerverlauf anzeigen',
searchThisAgent: 'Agentenverlauf anzeigen',
publicSection: {
title: 'Öffentlich',
subtitle: 'Diese Angaben werden in deinem öffentlichen Profil angezeigt. Jede:r kann sie sehen.',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/el.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2099,6 +2099,8 @@ const translations: TranslationDeepObject<typeof en> = {
profileAvatar: 'Εικόνα προφίλ',
customInstructions: 'Προσαρμοσμένες οδηγίες',
copilotIntoAccount: 'Οδηγός εντός λογαριασμού',
searchThisUser: 'Προβολή ιστορικού χρήστη',
searchThisAgent: 'Προβολή ιστορικού αντιπροσώπου',
publicSection: {
title: 'Δημόσιο',
subtitle: 'Αυτές οι πληροφορίες εμφανίζονται στο δημόσιο προφίλ σας. Μπορεί να τις δει οποιοσδήποτε.',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2143,6 +2143,8 @@ const translations = {
profileAvatar: 'Profile avatar',
customInstructions: 'Custom instructions',
copilotIntoAccount: 'Copilot into account',
searchThisUser: 'View user history',
searchThisAgent: 'View agent history',
Comment on lines +2146 to +2147

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NAB: Key name doesn't match the copy (naming nit, not a STYLE.md blocker)

These keys are named searchThisUser / searchThisAgent, but the value in every locale reads "View … history" (e.g. de.ts: 'Nutzerverlauf anzeigen', es.ts: 'Ver historial del usuario'). The naming is at least consistent across all 11 locale files, so it's purely cosmetic — but anyone grepping for the "search" copy won't find it, and vice-versa.

MY Suggestion

  • Rename the keys to viewUserHistory / viewAgentHistory (matches the copy

publicSection: {
title: 'Public',
subtitle: 'These details are displayed on your public profile. Anyone can see them.',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1972,6 +1972,8 @@ const translations: TranslationDeepObject<typeof en> = {
profileAvatar: 'Perfil avatar',
customInstructions: 'Instrucciones personalizadas',
copilotIntoAccount: 'Copilot a la cuenta',
searchThisUser: 'Ver historial del usuario',
searchThisAgent: '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.',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2055,6 +2055,8 @@ const translations: TranslationDeepObject<typeof en> = {
profileAvatar: 'Avatar de profil',
customInstructions: 'Instructions personnalisées',
copilotIntoAccount: 'Copilot dans le compte',
searchThisUser: 'Voir l’historique de l’utilisateur',
searchThisAgent: '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.',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2047,6 +2047,8 @@ const translations: TranslationDeepObject<typeof en> = {
profileAvatar: 'Avatar profilo',
customInstructions: 'Istruzioni personalizzate',
copilotIntoAccount: "Copilot nell'account",
searchThisUser: 'Visualizza cronologia utente',
searchThisAgent: 'Visualizza cronologia agente',
publicSection: {
title: 'Pubblico',
subtitle: 'Questi dettagli vengono visualizzati nel tuo profilo pubblico. Chiunque può vederli.',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2028,6 +2028,8 @@ const translations: TranslationDeepObject<typeof en> = {
profileAvatar: 'プロフィールアバター',
customInstructions: 'カスタム指示',
copilotIntoAccount: 'アカウントにCopilot',
searchThisUser: 'ユーザー履歴を表示',
searchThisAgent: 'エージェント履歴を表示',
publicSection: {
title: '公開',
subtitle: 'これらの詳細はあなたの公開プロフィールに表示され、誰でも閲覧できます。',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2042,6 +2042,8 @@ const translations: TranslationDeepObject<typeof en> = {
profileAvatar: 'Profielavatar',
customInstructions: 'Aangepaste instructies',
copilotIntoAccount: 'Copilot naar account',
searchThisUser: 'Gebruikersgeschiedenis bekijken',
searchThisAgent: 'Agentgeschiedenis bekijken',
publicSection: {
title: 'Openbaar',
subtitle: 'Deze gegevens worden weergegeven op je openbare profiel. Iedereen kan ze zien.',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2040,6 +2040,8 @@ const translations: TranslationDeepObject<typeof en> = {
profileAvatar: 'Awatar profilu',
customInstructions: 'Niestandardowe instrukcje',
copilotIntoAccount: 'Copilot do konta',
searchThisUser: 'Zobacz historię użytkownika',
searchThisAgent: 'Zobacz historię agenta',
publicSection: {
title: 'Public',
subtitle: 'Te dane są wyświetlane w Twoim publicznym profilu. Każdy może je zobaczyć.',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2037,6 +2037,8 @@ const translations: TranslationDeepObject<typeof en> = {
profileAvatar: 'Avatar do perfil',
customInstructions: 'Instruções personalizadas',
copilotIntoAccount: 'Copilot na conta',
searchThisUser: 'Ver histórico do usuário',
searchThisAgent: '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.',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1973,6 +1973,8 @@ const translations: TranslationDeepObject<typeof en> = {
profileAvatar: '个人头像',
customInstructions: '自定义指令',
copilotIntoAccount: 'Copilot 到账户',
searchThisUser: '查看用户历史记录',
searchThisAgent: '查看代理历史记录',
publicSection: {
title: '公开',
subtitle: '这些详细信息会显示在你的公开资料中,任何人都可以看到。',
Expand Down
30 changes: 24 additions & 6 deletions src/pages/ProfilePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -283,6 +285,18 @@ function ProfilePage({route}: ProfilePageProps) {
) : null}
{shouldShowLocalTime && <AutoUpdateTime timezone={timezone} />}
</View>
{shouldShowNotificationPreference && (
<View style={[styles.w100, styles.detailsPageSectionContainer]}>
<MenuItemWithTopDescription
shouldShowRightIcon
title={notificationPreference}
description={translate('notificationPreferencesPage.label')}
onPress={() => {
Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.NOTIFICATION_PREFERENCES.getRoute(report.reportID)));
}}
/>
</View>
)}
Comment thread
hungvu193 marked this conversation as resolved.
{isCurrentUser && (
<MenuItem
shouldShowRightIcon
Expand Down Expand Up @@ -312,13 +326,17 @@ function ProfilePage({route}: ProfilePageProps) {
onPress={callFunctionIfActionIsAllowed(() => switchToDelegator(login))}
/>
)}
{shouldShowNotificationPreference && (
<MenuItemWithTopDescription
{!!accountID && !isAnonymousUserSession() && !!login && (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also renders on the current user's own profile.

There's no !isCurrentUser guard, so viewing your own profile shows "Search this user" (searching your own messages). The QA steps only cover other users/agents. The issue says "all profiles", so this may be intended @Expensify/design could you please confirm this. thanks.

F1.mov

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hungvu193, this is still not clear is it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is expected

Comment thread
hungvu193 marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test — the !!login false-branch of this guard is uncovered (failure path).

This guard has three && conditions, but the tests only exercise the TRUE render and the isAnonymousUserSession() hide. The !!login branch is untested and it's genuinely reachable — login = details?.login ?? '', so any profiled account whose personal details lack a login (e.g. an optimistic/unvalidated account) will hide this entry.

Please add a test in tests/ui/ProfilePageTest.tsx, inside the ProfilePage - View user history describe:

it('hides the search entry when the profile has no login', async () => {
    // seed personalDetails[123] WITHOUT a login → login resolves to ''
    // render, then:
    expect(screen.queryByText('View user history')).not.toBeOnTheScreen();
    expect(screen.queryByText('View agent history')).not.toBeOnTheScreen();
});

Comment thread
hungvu193 marked this conversation as resolved.
<MenuItem
shouldShowRightIcon
title={notificationPreference}
description={translate('notificationPreferencesPage.label')}
title={translate(isAgentEmail(login) ? 'profilePage.searchThisAgent' : 'profilePage.searchThisUser')}
Comment thread
hungvu193 marked this conversation as resolved.
icon={expensifyIcons.MagnifyingGlass}
onPress={() => {
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}));
Comment on lines +335 to +339

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low Consistency question rawQuery

Other Search entry points pass both query and rawQuery, e.g. CardSection.tsx:123:

ROUTES.SEARCH_ROOT.getRoute({query, rawQuery: query})

Here only query is passed. Is omitting rawQuery intentional for this chat search, or should it match the other call sites?

}}
/>
)}
Expand Down
125 changes: 122 additions & 3 deletions tests/ui/ProfilePageTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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(),
};
Expand Down Expand Up @@ -479,6 +484,120 @@ describe('ProfilePage - agent account', () => {
});
});

const PublicProfileStack = createPlatformStackNavigator<ProfileNavigatorParamList>();

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(
<ComposeProviders components={[OnyxListItemProvider, CurrentUserPersonalDetailsProvider, LocaleContextProvider, CurrentReportIDContextProvider]}>
<PortalProvider>
<NavigationContainer>
<PublicProfileStack.Navigator>
<PublicProfileStack.Screen
name={SCREENS.DYNAMIC_PROFILE}
component={PublicProfilePage}
initialParams={{accountID: String(PUBLIC_PROFILE_ACCOUNT_ID), reportID: ''}}
/>
</PublicProfileStack.Navigator>
</NavigationContainer>
</PortalProvider>
</ComposeProviders>,
);
}

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}`}));
});

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();
});
});

describe('ProfilePage - SMS domain handling', () => {
beforeAll(async () => {
Onyx.init({
Expand Down
Loading