Skip to content

Commit 4d981e6

Browse files
Merge upstream develop
2 parents 9587648 + fee6bbe commit 4d981e6

33 files changed

Lines changed: 2072 additions & 236 deletions
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import ProfileScreen from '@/src/features/profile/ProfileScreen';
2+
3+
// The profile's Verifications tab; `/[identityId]` is the Posts tab.
4+
export default function ProfileVerificationsRoute() {
5+
return <ProfileScreen tab="verifications" />;
6+
}

apps/polycentric/src/common/components/Icon.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ function defineIcon<G extends string>(
2222
*/
2323
export const IconsMap = {
2424
add: defineIcon(Ionicons, 'add'),
25+
addCircleOutline: defineIcon(MaterialCommunityIcons, 'plus-circle-outline'),
2526
addOutline: defineIcon(Ionicons, 'add-circle-outline'),
2627
arrowBack: defineIcon(Ionicons, 'arrow-back'),
2728
at: defineIcon(Ionicons, 'at'),
@@ -50,7 +51,7 @@ export const IconsMap = {
5051
home: defineIcon(Ionicons, 'home-outline'),
5152
image: defineIcon(Ionicons, 'image-outline'),
5253
images: defineIcon(Ionicons, 'images-outline'),
53-
infoOutline: defineIcon(Ionicons, 'information-circle-outline'),
54+
infoOutline: defineIcon(MaterialCommunityIcons, 'information-outline'),
5455
key: defineIcon(MaterialIcons, 'vpn-key'),
5556
more: defineIcon(Ionicons, 'ellipsis-horizontal'),
5657
notification: defineIcon(MaterialCommunityIcons, 'bell-outline'),

apps/polycentric/src/common/components/InfoTooltip.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,19 +47,22 @@ export function InfoTooltip({
4747
const hide = () => setOpen(false);
4848

4949
const bubbleStyle = [
50-
Atoms.p_sm,
50+
Atoms.pt_sm,
51+
Atoms.pb_sm,
52+
Atoms.pl_md,
53+
Atoms.pr_md,
54+
Atoms.rounded_md,
5155
{
5256
width: BUBBLE_WIDTH,
5357
maxWidth: BUBBLE_WIDTH,
54-
borderRadius: 8,
5558
borderWidth: 1,
56-
borderColor: theme.palette.neutral_300,
57-
backgroundColor: theme.palette.background_secondary,
59+
borderColor: theme.palette.neutral_50,
60+
backgroundColor: theme.palette.neutral_25,
5861
},
5962
] as const;
6063

6164
const bubbleBody = (
62-
<Text variant="small" color="neutral_900">
65+
<Text variant="small" color="neutral_600" fontWeight="semibold">
6366
{text}
6467
</Text>
6568
);

apps/polycentric/src/common/components/List.tsx

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ import React, {
1717
} from 'react';
1818
import { View } from 'react-native';
1919
import Animated from 'react-native-reanimated';
20-
import { Atoms } from '../theme';
20+
import { Atoms, useTheme } from '../theme';
2121
import { HidingHeader, renderNode, useHidingHeader } from './HidingHeader';
22+
import { InfoTooltip } from './InfoTooltip';
23+
import { Text } from './primitives';
2224

2325
// A reanimated-compatible FlashList.
2426
const AnimatedFlashList = Animated.createAnimatedComponent(FlashList);
@@ -28,6 +30,38 @@ const WEB_PAGE_SIZE = 12;
2830

2931
export type { FlashListProps, ListRenderItem, ListRenderItemInfo };
3032

33+
// A list section header row, with an optional explanatory tooltip.
34+
export function SectionHeader({
35+
title,
36+
tooltip,
37+
}: {
38+
title: string;
39+
tooltip?: string;
40+
}) {
41+
const { theme } = useTheme();
42+
return (
43+
<View
44+
style={[
45+
Atoms.flex_row,
46+
Atoms.align_center,
47+
Atoms.gap_xs,
48+
Atoms.px_lg,
49+
Atoms.pt_xl,
50+
Atoms.pb_sm,
51+
]}
52+
>
53+
<Text
54+
variant="small"
55+
style={theme.atoms.text_neutral_medium}
56+
fontWeight="semibold"
57+
>
58+
{title}
59+
</Text>
60+
{tooltip ? <InfoTooltip text={tooltip} size={14} /> : null}
61+
</View>
62+
);
63+
}
64+
3165
export type ListProps<T> = FlashListProps<T> & {
3266
HeaderComponent?:
3367
| React.ComponentType<any>

apps/polycentric/src/common/constants/routes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export const Routes = {
3636
claims: '/claims',
3737
identitySwitch: '/identity/switch',
3838
profile: (identityId: string) => `/${identityId}` as const,
39+
profileVerifications: (identityId: string) =>
40+
`/${identityId}/verifications` as const,
3941
editProfile: (identityId: string) => `/${identityId}/edit` as const,
4042
post: (identityId: string, keyFingerprint: string, sequence: string) =>
4143
`/${identityId}/post/${keyFingerprint}/${sequence}` as const,

apps/polycentric/src/features/notifications/Notification.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ function InteractionNotification({
9595
router.push(route ?? Routes.tabs.profile(notification.fromIdentity));
9696
}, [notification]);
9797

98+
const openProfile = useCallback(
99+
() => router.push(Routes.tabs.profile(notification.fromIdentity)),
100+
[notification.fromIdentity],
101+
);
102+
98103
const dim = withHexOpacity(theme.palette.neutral_500, '40');
99104

100105
return (
@@ -110,10 +115,17 @@ function InteractionNotification({
110115
},
111116
]}
112117
>
113-
<ProfileAvatar identityKey={notification.fromIdentity} size="md" />
118+
<ProfileAvatar
119+
identityKey={notification.fromIdentity}
120+
size="md"
121+
onPress={openProfile}
122+
/>
114123
<View style={[Atoms.flex_1, Atoms.gap_xs]}>
115124
<Text>
116-
<Text fontWeight="bold">{name}</Text> {summary(notification)}
125+
<Text fontWeight="bold" onPress={openProfile}>
126+
{name}
127+
</Text>{' '}
128+
{summary(notification)}
117129
{notification.createdAt > 0 ? (
118130
<Text color="neutral_500">
119131
{' '}

apps/polycentric/src/features/profile/ProfileContext.tsx

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1+
import { Routes } from '@/src/common/constants/routes';
12
import { useCurrentIdentity } from '@/src/common/lib/polycentric-hooks';
3+
import { router } from 'expo-router';
24
import {
35
createContext,
6+
useCallback,
47
useContext,
5-
useEffect,
68
useMemo,
7-
useState,
89
type ReactNode,
910
} from 'react';
1011

11-
type ActiveFeed = 'posts' | 'likes';
12+
export type ActiveFeed = 'posts' | 'verifications';
1213

1314
interface ProfileContextValue {
1415
identityKey: string | null;
@@ -25,23 +26,36 @@ const ProfileContext = createContext<ProfileContextValue | null>(null);
2526
export function ProfileProvider({
2627
identityKey,
2728
alias = null,
29+
activeFeed = 'posts',
2830
children,
2931
}: {
3032
identityKey: string | null;
3133
alias?: string | null;
34+
// Which tab's route rendered this profile.
35+
activeFeed?: ActiveFeed;
3236
children: ReactNode;
3337
}) {
3438
const { identity: selfIdentity } = useCurrentIdentity();
3539
const isSelf = !!identityKey && selfIdentity?.identityKey === identityKey;
3640

37-
const [activeFeed, setActiveFeed] = useState<ActiveFeed>('posts');
38-
useEffect(() => {
39-
if (!isSelf) setActiveFeed('posts');
40-
}, [isSelf]);
41+
// Tabs are routes; switching replaces the URL, keeping the alias when
42+
// the profile was reached via one.
43+
const setActiveFeed = useCallback(
44+
(tab: ActiveFeed) => {
45+
const profileId = alias ?? identityKey;
46+
if (tab === activeFeed || !profileId) return;
47+
router.replace(
48+
tab === 'verifications'
49+
? Routes.tabs.profileVerifications(profileId)
50+
: Routes.tabs.profile(profileId),
51+
);
52+
},
53+
[activeFeed, alias, identityKey],
54+
);
4155

4256
const value = useMemo<ProfileContextValue>(
4357
() => ({ identityKey, isSelf, activeFeed, setActiveFeed, alias }),
44-
[identityKey, isSelf, activeFeed, alias],
58+
[identityKey, isSelf, activeFeed, setActiveFeed, alias],
4559
);
4660

4761
return (

apps/polycentric/src/features/profile/ProfileHeader.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
truncateName,
1515
useUsername,
1616
} from '@/src/common/lib/polycentric-hooks';
17+
import { Tabs } from '@/src/common/components/Tabs';
1718
import { Atoms, useTheme } from '@/src/common/theme';
1819
import { useProfile } from '@/src/features/profile/hooks/useProfile';
1920
import { FetchMode } from '@polycentric/react-native';
@@ -150,6 +151,21 @@ function ProfileHeaderInner({ bannerColors, onBack }: ProfileHeaderProps) {
150151
)}
151152
</View>
152153
</View>
154+
155+
<Tabs>
156+
<Tabs.Tab
157+
active={activeFeed === 'posts'}
158+
onPress={() => setActiveFeed('posts')}
159+
>
160+
Posts
161+
</Tabs.Tab>
162+
<Tabs.Tab
163+
active={activeFeed === 'verifications'}
164+
onPress={() => setActiveFeed('verifications')}
165+
>
166+
Verifications
167+
</Tabs.Tab>
168+
</Tabs>
153169
</View>
154170
);
155171
}

apps/polycentric/src/features/profile/ProfileScreen.test.tsx

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,12 @@ jest.mock('./ProfileContext', () => {
6060
// Inert stubs for the module-level imports the screen pulls in.
6161
jest.mock('./ProfileHeader', () => ({ ProfileHeader: () => null }));
6262
jest.mock('./ProfileFeedSwitcher', () => ({ ProfileFeedSwitcher: () => null }));
63+
jest.mock('./ProfileVerificationsList', () => ({
64+
ProfileVerificationsList: () => null,
65+
}));
6366
jest.mock('@/src/features/feed/hooks/useIdentityFeed', () => ({
6467
useIdentityFeed: () => ({ refresh: () => undefined }),
6568
}));
66-
jest.mock('@/src/features/feed/hooks/useLikesFeed', () => ({
67-
useLikesFeed: () => ({ refresh: () => undefined }),
68-
}));
6969
jest.mock('@/src/common/lib/navigation/useFocusedRefresh', () => ({
7070
useFocusedRefresh: () => undefined,
7171
}));
@@ -192,12 +192,7 @@ describe('IdentityProfile (identity path)', () => {
192192

193193
await render(<ProfileScreen />);
194194

195-
await waitFor(() =>
196-
expect(mockReplace).toHaveBeenCalledWith({
197-
pathname: '/[identityId]',
198-
params: { identityId: ALIAS },
199-
}),
200-
);
195+
await waitFor(() => expect(mockReplace).toHaveBeenCalledWith(`/${ALIAS}`));
201196
expect(mockRecord).toHaveBeenCalledWith(ALIAS, IDENTITY);
202197
});
203198

@@ -216,12 +211,7 @@ describe('IdentityProfile (identity path)', () => {
216211

217212
await render(<ProfileScreen />);
218213

219-
await waitFor(() =>
220-
expect(mockReplace).toHaveBeenCalledWith({
221-
pathname: '/[identityId]',
222-
params: { identityId: ALIAS },
223-
}),
224-
);
214+
await waitFor(() => expect(mockReplace).toHaveBeenCalledWith(`/${ALIAS}`));
225215
expect(mockResolve).not.toHaveBeenCalled();
226216
});
227217

0 commit comments

Comments
 (0)