Skip to content

Commit 67d33a8

Browse files
Merge upstream develop
2 parents fb3d941 + a0ea2f7 commit 67d33a8

42 files changed

Lines changed: 3924 additions & 530 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/polycentric/src/features/follow/FollowListScreen.tsx

Lines changed: 7 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { Text } from '@/src/common/components';
2-
import { ProfileAvatar } from '@/src/common/components/Avatar/ProfileAvatar';
32
import { Screen } from '@/src/common/components/layout';
43
import Topbar from '@/src/common/components/layout/Topbar';
54
import { List } from '@/src/common/components/List';
@@ -15,13 +14,9 @@ import {
1514
import { Atoms, Spacing, useTheme } from '@/src/common/theme';
1615
import { isWeb } from '@/src/common/util/platform';
1716
import { useProfile } from '@/src/features/profile/hooks/useProfile';
17+
import { ProfileRow } from '@/src/features/profile/ProfileRow';
1818
import { router, useLocalSearchParams } from 'expo-router';
19-
import {
20-
ActivityIndicator,
21-
Pressable,
22-
RefreshControl,
23-
View,
24-
} from 'react-native';
19+
import { ActivityIndicator, RefreshControl, View } from 'react-native';
2520
import { useSafeAreaInsets } from 'react-native-safe-area-context';
2621
import FollowButton from './FollowButton';
2722
import {
@@ -130,52 +125,15 @@ export default function FollowListScreen({ mode }: { mode: FollowListMode }) {
130125
// Avatar + name row linking to the identity's profile.
131126
function IdentityRow({ identity }: { identity: string }) {
132127
const { theme } = useTheme();
133-
const profile = useProfile(identity);
134128
const { identityKey } = useCurrentIdentity();
135-
const name = profile.name ? truncateName(profile.name, 32) : null;
136129
const isSelf = identityKey === identity;
137130

138131
return (
139-
<Pressable
132+
<ProfileRow
133+
identity={identity}
140134
onPress={() => router.push(Routes.tabs.profile(identity))}
141-
style={({ hovered, pressed }) => [
142-
(hovered || pressed) && {
143-
backgroundColor: theme.palette.neutral_25,
144-
},
145-
]}
146-
>
147-
<View
148-
style={[
149-
Atoms.flex_row,
150-
Atoms.align_center,
151-
Atoms.gap_md,
152-
Atoms.px_lg,
153-
Atoms.py_md,
154-
{ borderBottomWidth: 1, borderColor: theme.palette.neutral_25 },
155-
]}
156-
>
157-
<ProfileAvatar identityKey={identity} size="md" />
158-
<View style={Atoms.flex_1}>
159-
<Text
160-
variant="secondary"
161-
fontWeight="semibold"
162-
numberOfLines={1}
163-
selectable={false}
164-
>
165-
{name ?? 'Anonymous'}
166-
</Text>
167-
<Text
168-
variant="small"
169-
color="neutral_500"
170-
numberOfLines={1}
171-
selectable={false}
172-
style={{ fontFamily: 'monospace' }}
173-
>
174-
{shortenIdentityId(identity)}
175-
</Text>
176-
</View>
177-
{!isSelf && <FollowButton identity={identity} />}
178-
</View>
179-
</Pressable>
135+
style={{ borderBottomWidth: 1, borderColor: theme.palette.neutral_25 }}
136+
trailing={!isSelf ? <FollowButton identity={identity} /> : undefined}
137+
/>
180138
);
181139
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { Text } from '@/src/common/components';
2+
import { ProfileAvatar } from '@/src/common/components/Avatar/ProfileAvatar';
3+
import {
4+
shortenIdentityId,
5+
truncateName,
6+
} from '@/src/common/lib/polycentric-hooks';
7+
import { Atoms, useTheme } from '@/src/common/theme';
8+
import { useProfile } from './hooks/useProfile';
9+
import type { FetchMode } from '@polycentric/react-native';
10+
import type { ReactNode } from 'react';
11+
import { Pressable, type StyleProp, View, type ViewStyle } from 'react-native';
12+
13+
/**
14+
* A pressable identity row: avatar, name, and alias/shortened-id subtitle,
15+
* with an optional trailing element. Runs edge to edge (the hover
16+
* background spans the full width) with the standard horizontal inset.
17+
*/
18+
export function ProfileRow({
19+
identity,
20+
onPress,
21+
trailing,
22+
size = 'md',
23+
fetchMode,
24+
fallbackName,
25+
fallbackAlias,
26+
style,
27+
}: {
28+
identity: string;
29+
onPress?: () => void;
30+
trailing?: ReactNode;
31+
size?: 'sm' | 'md';
32+
fetchMode?: FetchMode;
33+
/** Shown when the profile has no cached name/alias yet. */
34+
fallbackName?: string | null;
35+
fallbackAlias?: string | null;
36+
style?: StyleProp<ViewStyle>;
37+
}) {
38+
const { theme } = useTheme();
39+
const profile = useProfile(identity, { fetchMode });
40+
const name = profile.name ?? fallbackName;
41+
const alias = profile.alias ?? fallbackAlias;
42+
43+
return (
44+
<Pressable
45+
onPress={onPress}
46+
style={({ hovered, pressed }) => [
47+
(hovered || pressed) && {
48+
backgroundColor: theme.palette.neutral_25,
49+
},
50+
]}
51+
>
52+
<View
53+
style={[
54+
Atoms.flex_row,
55+
Atoms.align_center,
56+
Atoms.gap_md,
57+
Atoms.px_lg,
58+
size === 'sm' ? Atoms.py_sm : Atoms.py_md,
59+
style,
60+
]}
61+
>
62+
<ProfileAvatar identityKey={identity} size={size} />
63+
<View style={Atoms.flex_1}>
64+
<Text
65+
variant="secondary"
66+
fontWeight="semibold"
67+
numberOfLines={1}
68+
selectable={false}
69+
>
70+
{name ? truncateName(name, 32) : 'Anonymous'}
71+
</Text>
72+
<Text
73+
variant="small"
74+
color="neutral_500"
75+
numberOfLines={1}
76+
selectable={false}
77+
style={alias ? undefined : { fontFamily: 'monospace' }}
78+
>
79+
{alias ?? shortenIdentityId(identity)}
80+
</Text>
81+
</View>
82+
{trailing}
83+
</View>
84+
</Pressable>
85+
);
86+
}

apps/polycentric/src/features/profile/hooks/useProfiles.ts

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { usePolycentric } from '@/src/common/lib/polycentric-hooks';
22
import { useQueryStore } from '@/src/common/query/hooks/useQuery';
33
import { FetchMode, Query } from '@polycentric/react-native';
44
import { useEffect, useMemo } from 'react';
5+
import { useShallow } from 'zustand/react/shallow';
56
import { decodeProfile, type DecodedProfile } from '../lib/decodeProfile';
67
import { profileQueryKey } from './useProfile';
78

@@ -10,13 +11,12 @@ export function useProfiles(
1011
): Map<string, DecodedProfile | null> {
1112
const client = usePolycentric();
1213

13-
// Key the effect on contents, not array identity, so a re-render with an
14-
// equal list doesn't churn subscriptions.
14+
// Key on contents, not array identity, so a re-render with an equal list
15+
// doesn't churn subscriptions.
1516
const joined = identities.join('\n');
17+
const ids = useMemo(() => (joined ? joined.split('\n') : []), [joined]);
1618

1719
useEffect(() => {
18-
if (!joined) return;
19-
const ids = joined.split('\n');
2020
const store = useQueryStore.getState();
2121
for (const identity of ids) {
2222
store.subscribe(profileQueryKey(identity).join('\0'), {
@@ -32,25 +32,33 @@ export function useProfiles(
3232
store.unsubscribe(profileQueryKey(identity).join('\0'));
3333
}
3434
};
35-
}, [joined, client]);
35+
}, [ids, client]);
3636

37-
const queries = useQueryStore((s) => s.queries);
37+
// Select only the subscribed entries' data — the store rebuilds its map on
38+
// every query update anywhere, so selecting the whole map would re-render
39+
// (and re-decode every profile) on unrelated ticks.
40+
const data = useQueryStore(
41+
useShallow((s) =>
42+
ids.map(
43+
(identity) => s.queries.get(profileQueryKey(identity).join('\0'))?.data,
44+
),
45+
),
46+
);
3847

3948
return useMemo(() => {
4049
const map = new Map<string, DecodedProfile | null>();
41-
if (!joined) return map;
42-
for (const identity of joined.split('\n')) {
43-
const data = queries.get(profileQueryKey(identity).join('\0'))?.data;
50+
ids.forEach((identity, i) => {
51+
const bytes = data[i];
4452
let decoded: DecodedProfile | null = null;
45-
if (data && data.byteLength > 0) {
53+
if (bytes && bytes.byteLength > 0) {
4654
try {
47-
decoded = decodeProfile(data);
55+
decoded = decodeProfile(bytes);
4856
} catch {
4957
decoded = null;
5058
}
5159
}
5260
map.set(identity, decoded);
53-
}
61+
});
5462
return map;
55-
}, [queries, joined]);
63+
}, [ids, data]);
5664
}

apps/polycentric/src/features/profile/search/ProfileSearchInput.tsx

Lines changed: 18 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
1-
import { ProfileAvatar } from '@/src/common/components/Avatar/ProfileAvatar';
1+
import { ProfileRow } from '@/src/features/profile/ProfileRow';
22
import { Text, TextInput } from '@/src/common/components/primitives';
3-
import {
4-
shortenIdentityId,
5-
truncateName,
6-
} from '@/src/common/lib/polycentric-hooks';
73
import { Atoms, useTheme } from '@/src/common/theme';
8-
import { useProfile } from '@/src/features/profile/hooks/useProfile';
94
import { FetchMode } from '@polycentric/react-native';
105
import { Fragment, useState } from 'react';
11-
import { ActivityIndicator, Pressable, View } from 'react-native';
6+
import { ActivityIndicator, View } from 'react-native';
127
import {
138
type ProfileSuggestion,
149
useProfileSuggestions,
@@ -137,63 +132,29 @@ function SuggestionRow({
137132
disabled: boolean;
138133
}) {
139134
const { theme } = useTheme();
140-
// Followed profiles read from cache like every other list; alias/id
141-
// matches are usually strangers, so fetch their profile to show a name.
142-
const profile = useProfile(suggestion.identity, {
143-
fetchMode:
144-
suggestion.source === 'following'
145-
? FetchMode.OfflineOnly
146-
: FetchMode.Default,
147-
});
148-
const name = profile.name ?? suggestion.name;
149-
const alias = profile.alias ?? suggestion.alias;
150135

151136
return (
152-
<Pressable
137+
<ProfileRow
138+
identity={suggestion.identity}
153139
onPress={disabled ? undefined : onPress}
154-
style={({ hovered, pressed }) => [
155-
(hovered || pressed) && {
156-
backgroundColor: theme.palette.neutral_25,
157-
},
158-
]}
159-
>
160-
<View
161-
style={[
162-
Atoms.flex_row,
163-
Atoms.align_center,
164-
Atoms.gap_md,
165-
Atoms.px_lg,
166-
Atoms.py_md,
167-
]}
168-
>
169-
<ProfileAvatar identityKey={suggestion.identity} size="md" />
170-
<View style={Atoms.flex_1}>
171-
<Text
172-
variant="secondary"
173-
fontWeight="semibold"
174-
numberOfLines={1}
175-
selectable={false}
176-
>
177-
{name ? truncateName(name, 32) : 'Anonymous'}
178-
</Text>
179-
<Text
180-
variant="small"
181-
color="neutral_500"
182-
numberOfLines={1}
183-
selectable={false}
184-
style={alias ? undefined : { fontFamily: 'monospace' }}
185-
>
186-
{alias ?? shortenIdentityId(suggestion.identity)}
187-
</Text>
188-
</View>
189-
{pending && (
140+
// Followed profiles read from cache like every other list; alias/id
141+
// matches are usually strangers, so fetch their profile to show a name.
142+
fetchMode={
143+
suggestion.source === 'following'
144+
? FetchMode.OfflineOnly
145+
: FetchMode.Default
146+
}
147+
fallbackName={suggestion.name}
148+
fallbackAlias={suggestion.alias}
149+
trailing={
150+
pending ? (
190151
<ActivityIndicator
191152
size="small"
192153
color={theme.palette.primary_500}
193154
accessibilityLabel="Working"
194155
/>
195-
)}
196-
</View>
197-
</Pressable>
156+
) : undefined
157+
}
158+
/>
198159
);
199160
}

apps/polycentric/src/features/verifications/claims/ClaimListItem.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Atoms, useTheme } from '@/src/common/theme';
44
import { router } from 'expo-router';
55
import { Pressable, View } from 'react-native';
66
import { DecodedClaim } from '../hooks/useClaimById';
7+
import { ClaimVerificationStatus } from '../utils/claim-status';
78
import { CLAIM_TYPES } from '../utils/forms';
89
import { resolveClaimTitle } from '../utils/render';
910
import { ClaimTypeChip } from './toolbar/ClaimTypeChip';
@@ -18,7 +19,7 @@ export function ClaimListItem({
1819
onPress,
1920
showOwner = false,
2021
}: {
21-
claim: DecodedClaim;
22+
claim: DecodedClaim & { status?: ClaimVerificationStatus };
2223
onPress?: () => void;
2324
// Show who made the claim — for lists that aren't the viewer's own.
2425
showOwner?: boolean;
@@ -82,7 +83,10 @@ export function ClaimListItem({
8283
color={claimType?.color}
8384
/>
8485
<TimeChip createdAt={claim.createdAt} />
85-
<StatusChip />
86+
<StatusChip
87+
verifiedCount={claim.status?.verifiedCount}
88+
totalCount={claim.status?.totalCount}
89+
/>
8690
</View>
8791
</View>
8892
</Pressable>

0 commit comments

Comments
 (0)