Skip to content

Commit ec410fd

Browse files
Merge upstream develop
2 parents bea1f55 + 2192ff6 commit ec410fd

34 files changed

Lines changed: 5198 additions & 1207 deletions

Cargo.lock

Lines changed: 398 additions & 354 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,4 @@ exclude = ["legacy/"]
1111
resolver = "2"
1212

1313
[workspace.dependencies]
14-
uniffi = { version = "=0.31.0", features = ["tokio"] }
15-
16-
# Size-optimized profile for rs-core FFI artifacts (Android, iOS).
17-
[profile.release-rs-core]
18-
inherits = "release"
19-
opt-level = "s"
20-
lto = "fat"
21-
codegen-units = 1
22-
panic = "abort"
23-
strip = "debuginfo"
14+
uniffi = { version = "=0.31.0", features = ["tokio"] }

apps/polycentric/app.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
2929
},
3030
android: {
3131
adaptiveIcon: {
32-
foregroundImage: './src/common/assets/images/PolycentricLogoTransparent1024.png',
32+
foregroundImage:
33+
'./src/common/assets/images/PolycentricLogoTransparent1024.png',
3334
backgroundColor: '#F2F5F9',
3435
},
3536
edgeToEdgeEnabled: true,

apps/polycentric/src/features/composer/ComposeSheetInner.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,6 @@ export function ComposeSheetInner({
178178
const event = await client.buildEvent(content);
179179

180180
const signedEvent = await client.signEvent(event);
181-
console.log(signedEvent);
182181
// `commitEvent` persists the event locally and, when content is
183182
// passed, seeds the core's content store + emits contentCreated
184183
// with both signedEvent and content so feeds can decode directly.

apps/polycentric/src/features/feed/ExploreScreen.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,19 @@ import { openCompose } from '@/src/common/constants';
88
import { isWeb } from '@/src/common/util/platform';
99
import { Atoms, useTheme } from '@/src/common/theme';
1010
import { ActivityIndicator, RefreshControl, View } from 'react-native';
11+
import { useFocusEffect, useIsFocused } from 'expo-router';
12+
import { useState } from 'react';
1113

1214
export default function ExploreScreen() {
1315
const { theme } = useTheme();
1416
const showComposeFab = !isWeb;
1517

16-
const feed = useExploreFeed({ enabled: true });
18+
const [enabled, setEnabled] = useState<boolean>(false);
19+
const feed = useExploreFeed({ enabled });
20+
21+
useFocusEffect(() => {
22+
setEnabled(true);
23+
});
1724

1825
if (feed.error) {
1926
return (
@@ -38,6 +45,7 @@ export default function ExploreScreen() {
3845
<Screen>
3946
<Screen.PrimaryColumn>
4047
<FeedViewer
48+
keyExtractor={(item) => item.id}
4149
data={feed.items}
4250
ListHeaderComponent={!isWeb ? Screen.Topbar : undefined}
4351
ListEmptyComponent={

apps/polycentric/src/features/feed/FeedScreen.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { openCompose } from '@/src/common/constants';
99
import { isWeb } from '@/src/common/util/platform';
1010
import { Atoms, useTheme } from '@/src/common/theme';
1111
import { ActivityIndicator, RefreshControl, View } from 'react-native';
12+
import { useFocusEffect, useIsFocused } from 'expo-router';
13+
import { useState } from 'react';
1214

1315
const ListHeader = () => (
1416
<>
@@ -21,7 +23,12 @@ export default function FeedScreen() {
2123
const { theme } = useTheme();
2224
const showComposeFab = !isWeb;
2325

24-
const feed = useFollowingFeed({ enabled: true });
26+
const [enabled, setEnabled] = useState<boolean>(false);
27+
const feed = useFollowingFeed({ enabled });
28+
29+
useFocusEffect(() => {
30+
setEnabled(true);
31+
});
2532

2633
if (feed.error) {
2734
return (
@@ -46,6 +53,7 @@ export default function FeedScreen() {
4653
<Screen>
4754
<Screen.PrimaryColumn>
4855
<FeedViewer
56+
keyExtractor={(item) => item.id}
4957
data={feed.items}
5058
ListHeaderComponent={ListHeader}
5159
ListEmptyComponent={

apps/polycentric/src/features/feed/hooks/useAuthorFeed.ts

Lines changed: 0 additions & 68 deletions
This file was deleted.

apps/polycentric/src/features/feed/hooks/useExploreFeed.ts

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
import { useCallback, useEffect, useState } from 'react';
1+
import { useCallback, useEffect, useRef, useState } from 'react';
2+
import { QueryStatus, v2 } from '@polycentric/react-native';
23
import {
34
decodeV2PostBundle,
45
usePolycentricContext,
56
type PostData,
67
} from '@/src/common/lib/polycentric-hooks';
78
import { EMPTY_POSTS, NOOP, type FeedHookResult } from './types';
89

10+
type Sub = { unsubscribe: () => void };
11+
912
export function useExploreFeed(options?: {
1013
perServerLimit?: number;
1114
enabled?: boolean;
@@ -16,34 +19,60 @@ export function useExploreFeed(options?: {
1619
const [isLoading, setIsLoading] = useState(false);
1720
const [error, setError] = useState<Error | null>(null);
1821

19-
const fetchFeed = useCallback(async () => {
20-
if (client.servers.length === 0) return;
21-
setIsLoading(true);
22+
// Hold the live subscription so we can cancel on unmount / refresh.
23+
// The rust core fans out to every configured server and emits each
24+
// merged response as it arrives.
25+
const subscriptionRef = useRef<Sub | null>(null);
26+
27+
const cleanup = useCallback(() => {
28+
subscriptionRef.current?.unsubscribe();
29+
subscriptionRef.current = null;
30+
}, []);
31+
32+
const fetchFeed = useCallback(() => {
33+
cleanup();
34+
setItems(EMPTY_POSTS);
2235
setError(null);
23-
try {
24-
const responses = await client.getExploreFeed({
25-
identity: client.activeIdentityKey,
26-
limit: options?.perServerLimit ?? null,
27-
});
28-
const posts: PostData[] = [];
29-
for (const response of responses) {
30-
for (const bundle of response.eventBundles) {
31-
const decoded = decodeV2PostBundle(bundle);
32-
if (decoded) posts.push(decoded);
36+
setIsLoading(true);
37+
38+
// Rust-side merge_fn already dedupes by EventKey — each next()
39+
// carries the full merged feed plus a status. `Loading` stays in
40+
// effect until the last per-server response arrives.
41+
const observable = client.core.getExploreFeed(
42+
client.activeIdentityKey ?? undefined,
43+
undefined,
44+
undefined,
45+
undefined,
46+
);
47+
subscriptionRef.current = observable.subscribe({
48+
next: (result) => {
49+
if (result.data) {
50+
const response = v2.GetFeedResponse.fromBinary(
51+
new Uint8Array(result.data),
52+
);
53+
const posts: PostData[] = [];
54+
for (const bundle of response.eventBundles) {
55+
const decoded = decodeV2PostBundle(bundle);
56+
if (decoded) posts.push(decoded);
57+
}
58+
setItems(posts);
3359
}
34-
}
35-
setItems(posts);
36-
} catch (e) {
37-
console.warn('[useExploreFeed] fetch failed', e);
38-
setError(e instanceof Error ? e : new Error(String(e)));
39-
} finally {
40-
setIsLoading(false);
41-
}
42-
}, [client, options?.perServerLimit]);
60+
setIsLoading(result.status === QueryStatus.Loading);
61+
},
62+
error: (message: string) => {
63+
console.warn('useExploreFeed error:', message);
64+
setError(new Error(message));
65+
},
66+
complete: () => {
67+
setIsLoading(false);
68+
},
69+
});
70+
}, [client, cleanup]);
4371

4472
useEffect(() => {
4573
if (enabled) fetchFeed();
46-
}, [enabled, fetchFeed]);
74+
return cleanup;
75+
}, [enabled, fetchFeed, cleanup]);
4776

4877
return {
4978
items,

apps/polycentric/src/features/feed/hooks/useFollowingFeed.ts

Lines changed: 54 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { useCallback, useEffect, useState } from 'react';
1+
import { useCallback, useEffect, useRef, useState } from 'react';
2+
import { QueryStatus, v2 } from '@polycentric/react-native';
23
import {
34
decodeV2PostBundle,
45
useLocalPostInjection,
@@ -7,6 +8,8 @@ import {
78
} from '@/src/common/lib/polycentric-hooks';
89
import { EMPTY_POSTS, NOOP, type FeedHookResult } from './types';
910

11+
type Sub = { unsubscribe: () => void };
12+
1013
export function useFollowingFeed(options?: {
1114
limit?: number;
1215
enabled?: boolean;
@@ -17,37 +20,65 @@ export function useFollowingFeed(options?: {
1720
const [isLoading, setIsLoading] = useState(false);
1821
const [error, setError] = useState<Error | null>(null);
1922

20-
const fetchFeed = useCallback(async () => {
21-
if (client.servers.length === 0) return;
22-
if (!client.activeIdentityKey) {
23+
// Hold the live subscription so we can cancel on unmount / refresh /
24+
// identity change. The rust core fans out to every configured server
25+
// and emits each merged response as it arrives.
26+
const subscriptionRef = useRef<Sub | null>(null);
27+
28+
const cleanup = useCallback(() => {
29+
subscriptionRef.current?.unsubscribe();
30+
subscriptionRef.current = null;
31+
}, []);
32+
33+
const fetchFeed = useCallback(() => {
34+
const followerIdentity = client.activeIdentityKey;
35+
if (!followerIdentity) {
36+
cleanup();
2337
setItems(EMPTY_POSTS);
2438
return;
2539
}
2640

27-
setIsLoading(true);
41+
cleanup();
42+
setItems(EMPTY_POSTS);
2843
setError(null);
29-
try {
30-
const responses = await client.getFollowingFeed({
31-
limit: options?.limit ?? null,
32-
});
33-
const posts: PostData[] = [];
34-
for (const response of responses) {
35-
for (const bundle of response.eventBundles) {
36-
const decoded = decodeV2PostBundle(bundle);
37-
if (decoded) posts.push(decoded);
44+
setIsLoading(true);
45+
46+
const observable = client.core.getFollowingFeed(
47+
followerIdentity,
48+
undefined,
49+
undefined,
50+
undefined,
51+
);
52+
subscriptionRef.current = observable.subscribe({
53+
next: (result) => {
54+
if (result.data) {
55+
const response = v2.GetFeedResponse.fromBinary(
56+
new Uint8Array(result.data),
57+
);
58+
59+
const decoded = response.eventBundles
60+
.map((bundle) => decodeV2PostBundle(bundle))
61+
.filter((post) => !!post);
62+
63+
setItems(decoded);
3864
}
39-
}
40-
setItems(posts);
41-
} catch (e) {
42-
setError(e instanceof Error ? e : new Error(String(e)));
43-
} finally {
44-
setIsLoading(false);
45-
}
46-
}, [client, options?.limit]);
65+
66+
setIsLoading(result.status === QueryStatus.Loading);
67+
},
68+
error: (message: string) => {
69+
console.warn('useFollowingFeed error:', message);
70+
setError(new Error(message));
71+
},
72+
complete: () => {
73+
setIsLoading(false);
74+
},
75+
});
76+
}, [client, cleanup]);
4777

4878
useEffect(() => {
4979
if (enabled) fetchFeed();
50-
}, [enabled, fetchFeed]);
80+
return cleanup;
81+
}, [enabled, fetchFeed, cleanup]);
5182

5283
useLocalPostInjection({
5384
match: () => true,

0 commit comments

Comments
 (0)