Skip to content

Commit 8e23722

Browse files
Merge upstream develop
2 parents e2133c9 + 405217d commit 8e23722

11 files changed

Lines changed: 487 additions & 11 deletions

File tree

apps/polycentric/src/features/post/ConversationView.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,14 @@ import {
1212
} from '@/src/common/lib/polycentric-hooks';
1313
import { Post } from './Post';
1414
import { usePostById } from './hooks/usePostById';
15+
import { useThreadReplies } from './hooks/useThreadReplies';
1516
import { Atoms } from '@/src/common/theme';
1617
import { ComposerInput } from '../composer';
1718

1819
interface ConversationViewProps {
1920
post: PostData;
2021
}
2122

22-
// TODO: reply list previously lived in a store-backed feed. Reintroduce reply
23-
// loading via `listEvents` with a parent-pointer filter once that filter is
24-
// supported.
2523
export function ConversationView({ post }: ConversationViewProps) {
2624
const [refreshing, setRefreshing] = useState(false);
2725
const { height: windowHeight } = useWindowDimensions();
@@ -34,15 +32,17 @@ export function ConversationView({ post }: ConversationViewProps) {
3432
post.reply?.parent?.identity,
3533
post.reply?.parent?.sequence,
3634
);
35+
const { replies } = useThreadReplies(post);
3736

3837
const items = useMemo(() => {
3938
const list: PostData[] = [];
4039
if (rootPost) list.push(rootPost);
4140
// Skip parent if it's the same as root (thread of depth 1).
4241
if (parentPost && parentPost.id !== rootPost?.id) list.push(parentPost);
4342
list.push(post);
43+
list.push(...replies);
4444
return list;
45-
}, [rootPost, parentPost, post]);
45+
}, [rootPost, parentPost, post, replies]);
4646

4747
const handleRefresh = useCallback(() => {
4848
setRefreshing(true);
@@ -66,9 +66,11 @@ export function ConversationView({ post }: ConversationViewProps) {
6666
post={item}
6767
hideReplyingTo={false}
6868
disablePress={item.id === post.id}
69-
showThreadLineAbove={!!item.reply?.parent}
70-
showThreadLineBelow={index < items.length - 1}
71-
hideBottomBorder={item.id !== post.id}
69+
showThreadLineAbove={item.id === post.id && !!item.reply?.parent}
70+
showThreadLineBelow={
71+
index < items.length - 1 && !!post.reply?.parent
72+
}
73+
hideBottomBorder={index < items.length - 1 && !!post.reply?.parent}
7274
/>
7375
{item.id === post.id ? <ComposerInput replyTo={replyTo} /> : null}
7476
</View>
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { useEffect, useState } from 'react';
2+
import { COLLECTION, v2 } from '@polycentric/react-native';
3+
import {
4+
decodeV2PostBundle,
5+
usePolycentricContext,
6+
type PostData,
7+
} from '@/src/common/lib/polycentric-hooks';
8+
9+
/**
10+
* Load the direct replies for a given post via the server's `GetPostThread`
11+
* RPC. The server returns the parent post plus the replies — we only surface
12+
* the replies here because the caller already has the parent.
13+
*/
14+
export function useThreadReplies(
15+
post: PostData | undefined,
16+
options?: { limit?: number },
17+
): { replies: PostData[]; isLoading: boolean; error: Error | null } {
18+
const { client } = usePolycentricContext();
19+
const [replies, setReplies] = useState<PostData[]>([]);
20+
const [isLoading, setIsLoading] = useState(false);
21+
const [error, setError] = useState<Error | null>(null);
22+
23+
useEffect(() => {
24+
if (!post) {
25+
setReplies([]);
26+
return;
27+
}
28+
29+
let cancelled = false;
30+
setIsLoading(true);
31+
setError(null);
32+
33+
(async () => {
34+
try {
35+
const response = await client.getPostThread({
36+
eventKey: v2.EventKey.create({
37+
collection: COLLECTION.FEED,
38+
identity: post.identity,
39+
signedBy: post.signedBy,
40+
sequence: BigInt(post.sequence),
41+
}),
42+
limit: options?.limit ?? null,
43+
});
44+
if (cancelled) return;
45+
46+
const decoded: PostData[] = [];
47+
for (const bundle of response?.replies ?? []) {
48+
const d = decodeV2PostBundle(bundle);
49+
if (d) decoded.push(d);
50+
}
51+
setReplies(decoded);
52+
} catch (e) {
53+
if (!cancelled) {
54+
setError(e instanceof Error ? e : new Error(String(e)));
55+
}
56+
} finally {
57+
if (!cancelled) setIsLoading(false);
58+
}
59+
})();
60+
61+
return () => {
62+
cancelled = true;
63+
};
64+
}, [client, post, options?.limit]);
65+
66+
return { replies, isLoading, error };
67+
}

packages/js-core/src/platform-interfaces/runtime-core.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,18 @@ export interface IPolycentricCore {
159159
identity?: string | null,
160160
): Promise<Uint8Array>;
161161

162+
/**
163+
* Fetch a parent post and its direct replies from a server via gRPC-web.
164+
*
165+
* @param server_url - Base URL of the gRPC-web server
166+
* @param request_bytes - Serialized `GetPostThreadRequest` proto bytes
167+
* @returns Serialized `GetPostThreadResponse` proto bytes
168+
*/
169+
get_post_thread(
170+
server_url: string,
171+
request_bytes: Uint8Array,
172+
): Promise<Uint8Array>;
173+
162174
/**
163175
* Decode `image` bytes, resize into `width` x `height` per `mode`,
164176
* and encode as JPEG.

packages/js-core/src/polycentric-client.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,42 @@ export class PolycentricClient {
473473
return bundles;
474474
}
475475

476+
/**
477+
* Fetch a parent post and its direct replies for a given EventKey.
478+
* Queries every configured server and returns the first successful
479+
* response. Does not persist — callers decide what to do with the
480+
* response.
481+
*/
482+
async getPostThread(options: {
483+
eventKey: Proto.EventKey;
484+
limit?: number | null;
485+
}): Promise<Proto.GetPostThreadResponse | null> {
486+
if (!this.core) throw new Error('Core not initialized');
487+
488+
const requestBytes = Proto.GetPostThreadRequest.toBinary(
489+
Proto.GetPostThreadRequest.create({
490+
eventKey: options.eventKey,
491+
limit: options.limit ?? 0,
492+
}),
493+
);
494+
495+
const results = await Promise.allSettled(
496+
this.servers.map((server) =>
497+
this.core!.get_post_thread(server, requestBytes),
498+
),
499+
);
500+
501+
for (const result of results) {
502+
if (result.status === 'rejected') {
503+
console.error('getPostThread failed for a server:', result.reason);
504+
continue;
505+
}
506+
return Proto.GetPostThreadResponse.fromBinary(result.value);
507+
}
508+
509+
return null;
510+
}
511+
476512
/**
477513
* Return non-deleted event bundles for an `(identity, collection)` stream
478514
* from the local core. Tombstone CRDT is applied by the core: any event

packages/js-core/src/proto/polycentric/v2/feeds.client.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import type { RpcTransport } from "@protobuf-ts/runtime-rpc";
55
import type { ServiceInfo } from "@protobuf-ts/runtime-rpc";
66
import { FeedsService } from "./feeds";
7+
import type { GetPostThreadResponse } from "./feeds";
8+
import type { GetPostThreadRequest } from "./feeds";
79
import { stackIntercept } from "@protobuf-ts/runtime-rpc";
810
import type { GetFeedResponse } from "./feeds";
911
import type { GetFeedRequest } from "./feeds";
@@ -22,6 +24,12 @@ export interface IFeedsServiceClient {
2224
* @generated from protobuf rpc: GetFeed
2325
*/
2426
getFeed(input: GetFeedRequest, options?: RpcOptions): UnaryCall<GetFeedRequest, GetFeedResponse>;
27+
/**
28+
* Replies to a post
29+
*
30+
* @generated from protobuf rpc: GetPostThread
31+
*/
32+
getPostThread(input: GetPostThreadRequest, options?: RpcOptions): UnaryCall<GetPostThreadRequest, GetPostThreadResponse>;
2533
}
2634
/**
2735
* *
@@ -44,4 +52,13 @@ export class FeedsServiceClient implements IFeedsServiceClient, ServiceInfo {
4452
const method = this.methods[0], opt = this._transport.mergeOptions(options);
4553
return stackIntercept<GetFeedRequest, GetFeedResponse>("unary", this._transport, method, opt, input);
4654
}
55+
/**
56+
* Replies to a post
57+
*
58+
* @generated from protobuf rpc: GetPostThread
59+
*/
60+
getPostThread(input: GetPostThreadRequest, options?: RpcOptions): UnaryCall<GetPostThreadRequest, GetPostThreadResponse> {
61+
const method = this.methods[1], opt = this._transport.mergeOptions(options);
62+
return stackIntercept<GetPostThreadRequest, GetPostThreadResponse>("unary", this._transport, method, opt, input);
63+
}
4764
}

packages/js-core/src/proto/polycentric/v2/feeds.ts

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { UnknownFieldHandler } from "@protobuf-ts/runtime";
1111
import type { PartialMessage } from "@protobuf-ts/runtime";
1212
import { reflectionMergePartial } from "@protobuf-ts/runtime";
1313
import { MessageType } from "@protobuf-ts/runtime";
14+
import { EventKey } from "./event_key";
1415
import { EventBundle } from "./events";
1516
/**
1617
* @generated from protobuf message polycentric.v2.GetFeedRequest
@@ -57,6 +58,35 @@ export interface GetFeedResponse {
5758
*/
5859
previousToken: string;
5960
}
61+
/**
62+
* @generated from protobuf message polycentric.v2.GetPostThreadRequest
63+
*/
64+
export interface GetPostThreadRequest {
65+
/**
66+
* Parent post to return the thread for
67+
*
68+
* @generated from protobuf field: polycentric.v2.EventKey event_key = 1
69+
*/
70+
eventKey?: EventKey;
71+
/**
72+
* @generated from protobuf field: int32 limit = 2
73+
*/
74+
limit: number;
75+
}
76+
/**
77+
* @generated from protobuf message polycentric.v2.GetPostThreadResponse
78+
*/
79+
export interface GetPostThreadResponse {
80+
/**
81+
* @generated from protobuf field: polycentric.v2.EventBundle parent = 1
82+
*/
83+
parent?: EventBundle;
84+
/**
85+
* @generated from protobuf field: repeated polycentric.v2.EventBundle replies = 2
86+
*/
87+
replies: EventBundle[]; // string next_token = 3;
88+
// string previous_token = 4;
89+
}
6090
/**
6191
* @generated from protobuf enum polycentric.v2.FeedAlgorithm
6292
*/
@@ -212,9 +242,118 @@ class GetFeedResponse$Type extends MessageType<GetFeedResponse> {
212242
* @generated MessageType for protobuf message polycentric.v2.GetFeedResponse
213243
*/
214244
export const GetFeedResponse = new GetFeedResponse$Type();
245+
// @generated message type with reflection information, may provide speed optimized methods
246+
class GetPostThreadRequest$Type extends MessageType<GetPostThreadRequest> {
247+
constructor() {
248+
super("polycentric.v2.GetPostThreadRequest", [
249+
{ no: 1, name: "event_key", kind: "message", T: () => EventKey },
250+
{ no: 2, name: "limit", kind: "scalar", T: 5 /*ScalarType.INT32*/ }
251+
]);
252+
}
253+
create(value?: PartialMessage<GetPostThreadRequest>): GetPostThreadRequest {
254+
const message = globalThis.Object.create((this.messagePrototype!));
255+
message.limit = 0;
256+
if (value !== undefined)
257+
reflectionMergePartial<GetPostThreadRequest>(this, message, value);
258+
return message;
259+
}
260+
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: GetPostThreadRequest): GetPostThreadRequest {
261+
let message = target ?? this.create(), end = reader.pos + length;
262+
while (reader.pos < end) {
263+
let [fieldNo, wireType] = reader.tag();
264+
switch (fieldNo) {
265+
case /* polycentric.v2.EventKey event_key */ 1:
266+
message.eventKey = EventKey.internalBinaryRead(reader, reader.uint32(), options, message.eventKey);
267+
break;
268+
case /* int32 limit */ 2:
269+
message.limit = reader.int32();
270+
break;
271+
default:
272+
let u = options.readUnknownField;
273+
if (u === "throw")
274+
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
275+
let d = reader.skip(wireType);
276+
if (u !== false)
277+
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
278+
}
279+
}
280+
return message;
281+
}
282+
internalBinaryWrite(message: GetPostThreadRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
283+
/* polycentric.v2.EventKey event_key = 1; */
284+
if (message.eventKey)
285+
EventKey.internalBinaryWrite(message.eventKey, writer.tag(1, WireType.LengthDelimited).fork(), options).join();
286+
/* int32 limit = 2; */
287+
if (message.limit !== 0)
288+
writer.tag(2, WireType.Varint).int32(message.limit);
289+
let u = options.writeUnknownFields;
290+
if (u !== false)
291+
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
292+
return writer;
293+
}
294+
}
295+
/**
296+
* @generated MessageType for protobuf message polycentric.v2.GetPostThreadRequest
297+
*/
298+
export const GetPostThreadRequest = new GetPostThreadRequest$Type();
299+
// @generated message type with reflection information, may provide speed optimized methods
300+
class GetPostThreadResponse$Type extends MessageType<GetPostThreadResponse> {
301+
constructor() {
302+
super("polycentric.v2.GetPostThreadResponse", [
303+
{ no: 1, name: "parent", kind: "message", T: () => EventBundle },
304+
{ no: 2, name: "replies", kind: "message", repeat: 2 /*RepeatType.UNPACKED*/, T: () => EventBundle }
305+
]);
306+
}
307+
create(value?: PartialMessage<GetPostThreadResponse>): GetPostThreadResponse {
308+
const message = globalThis.Object.create((this.messagePrototype!));
309+
message.replies = [];
310+
if (value !== undefined)
311+
reflectionMergePartial<GetPostThreadResponse>(this, message, value);
312+
return message;
313+
}
314+
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: GetPostThreadResponse): GetPostThreadResponse {
315+
let message = target ?? this.create(), end = reader.pos + length;
316+
while (reader.pos < end) {
317+
let [fieldNo, wireType] = reader.tag();
318+
switch (fieldNo) {
319+
case /* polycentric.v2.EventBundle parent */ 1:
320+
message.parent = EventBundle.internalBinaryRead(reader, reader.uint32(), options, message.parent);
321+
break;
322+
case /* repeated polycentric.v2.EventBundle replies */ 2:
323+
message.replies.push(EventBundle.internalBinaryRead(reader, reader.uint32(), options));
324+
break;
325+
default:
326+
let u = options.readUnknownField;
327+
if (u === "throw")
328+
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
329+
let d = reader.skip(wireType);
330+
if (u !== false)
331+
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
332+
}
333+
}
334+
return message;
335+
}
336+
internalBinaryWrite(message: GetPostThreadResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
337+
/* polycentric.v2.EventBundle parent = 1; */
338+
if (message.parent)
339+
EventBundle.internalBinaryWrite(message.parent, writer.tag(1, WireType.LengthDelimited).fork(), options).join();
340+
/* repeated polycentric.v2.EventBundle replies = 2; */
341+
for (let i = 0; i < message.replies.length; i++)
342+
EventBundle.internalBinaryWrite(message.replies[i], writer.tag(2, WireType.LengthDelimited).fork(), options).join();
343+
let u = options.writeUnknownFields;
344+
if (u !== false)
345+
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
346+
return writer;
347+
}
348+
}
349+
/**
350+
* @generated MessageType for protobuf message polycentric.v2.GetPostThreadResponse
351+
*/
352+
export const GetPostThreadResponse = new GetPostThreadResponse$Type();
215353
/**
216354
* @generated ServiceType for protobuf service polycentric.v2.FeedsService
217355
*/
218356
export const FeedsService = new ServiceType("polycentric.v2.FeedsService", [
219-
{ name: "GetFeed", options: {}, I: GetFeedRequest, O: GetFeedResponse }
357+
{ name: "GetFeed", options: {}, I: GetFeedRequest, O: GetFeedResponse },
358+
{ name: "GetPostThread", options: {}, I: GetPostThreadRequest, O: GetPostThreadResponse }
220359
]);

0 commit comments

Comments
 (0)