Skip to content

Commit ab73155

Browse files
brandon-futomarkharding
authored andcommitted
[#116] Reply counts on posts
Changelog: feature
1 parent 6137c93 commit ab73155

53 files changed

Lines changed: 1831 additions & 521 deletions

File tree

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/common/lib/polycentric-hooks/helpers.ts

Lines changed: 17 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ export type PostData = {
4949
* key so a repost is distinct from the original post. */
5050
repostId?: string;
5151

52+
/** Our estimation for how many replies this post has. */
53+
replyCount?: number;
54+
5255
signedEvent: v2.SignedEvent;
5356
};
5457

@@ -121,6 +124,10 @@ export function decodeBundle<K extends ContentKind>(
121124
return { event, content, signedEvent: bundle.signedEvent };
122125
}
123126

127+
export function eventKeyId(eventKey: v2.EventKey): string {
128+
return bytesToHex(v2.EventKey.toBinary(eventKey));
129+
}
130+
124131
/** Decode a v2 EventBundle into PostData, or null if not a post. */
125132
export function decodePostBundle(bundle: v2.EventBundle): PostData | null {
126133
const decoded = decodeBundle(bundle, 'post');
@@ -130,20 +137,16 @@ export function decodePostBundle(bundle: v2.EventBundle): PostData | null {
130137
const key = event.key;
131138
if (!key?.signedBy?.key) return null;
132139

133-
const id = bytesToHex(v2.EventKey.toBinary(key));
140+
const id = eventKeyId(key);
134141
const reply = post.reply
135142
? {
136-
rootId: post.reply.root
137-
? bytesToHex(v2.EventKey.toBinary(post.reply.root))
138-
: undefined,
143+
rootId: post.reply.root ? eventKeyId(post.reply.root) : undefined,
139144
parentId: post.reply.parent
140-
? bytesToHex(v2.EventKey.toBinary(post.reply.parent))
145+
? eventKeyId(post.reply.parent)
141146
: undefined,
142147
}
143148
: undefined;
144-
const quoteId = post.quote
145-
? bytesToHex(v2.EventKey.toBinary(post.quote))
146-
: undefined;
149+
const quoteId = post.quote ? eventKeyId(post.quote) : undefined;
147150

148151
return {
149152
id,
@@ -156,6 +159,7 @@ export function decodePostBundle(bundle: v2.EventBundle): PostData | null {
156159
links: post.links,
157160
reply,
158161
quoteId,
162+
replyCount: bundle.meta?.replyCount,
159163
signedEvent: v2.SignedEvent.create({
160164
eventBytes: signedEvent.eventBytes,
161165
signature: signedEvent.signature,
@@ -183,8 +187,8 @@ function decodeRepostBundle(bundle: v2.EventBundle): {
183187
if (!target) return null;
184188
return {
185189
repostedBy: key.identity,
186-
targetId: bytesToHex(v2.EventKey.toBinary(target)),
187-
repostId: bytesToHex(v2.EventKey.toBinary(key)),
190+
targetId: eventKeyId(target),
191+
repostId: eventKeyId(key),
188192
};
189193
} catch {
190194
return null;
@@ -196,13 +200,6 @@ function decodeRepostBundle(bundle: v2.EventBundle): {
196200
* directly; reposts resolve their target post from `event_hints` (the
197201
* server ships the reposted post alongside) and surface it tagged with
198202
* `repostedBy`. A repost whose target isn't in the hints is dropped.
199-
*
200-
* Items are de-duplicated by their list key (`repostId ?? id`). The explore
201-
* feed fans out across multiple servers and paginates with a single forward
202-
* token, so the same post routinely arrives more than once. Without this,
203-
* duplicate keys reach FlashList and recycled cells flash / the scroll
204-
* position jumps while paginating. The first occurrence wins so already-
205-
* rendered rows keep their position.
206203
*/
207204
export function decodeFeedItems(response: v2.GetFeedResponse): PostData[] {
208205
const hintPosts = new Map<string, PostData>();
@@ -213,25 +210,17 @@ export function decodeFeedItems(response: v2.GetFeedResponse): PostData[] {
213210
}
214211

215212
const items: PostData[] = [];
216-
const seen = new Set<string>();
217-
const pushUnique = (item: PostData) => {
218-
const key = item.repostId ?? item.id;
219-
if (seen.has(key)) return;
220-
seen.add(key);
221-
items.push(item);
222-
};
223-
224213
for (const bundle of response.eventBundles) {
225214
const post = decodePostBundle(bundle);
226215
if (post) {
227-
pushUnique(post);
216+
items.push(post);
228217
continue;
229218
}
230219
const repost = decodeRepostBundle(bundle);
231220
if (repost) {
232221
const target = hintPosts.get(repost.targetId);
233222
if (target) {
234-
pushUnique({
223+
items.push({
235224
...target,
236225
repostedBy: repost.repostedBy,
237226
repostId: repost.repostId,
@@ -242,19 +231,6 @@ export function decodeFeedItems(response: v2.GetFeedResponse): PostData[] {
242231
return items;
243232
}
244233

245-
/** Extract the items and next page indicator from the feed response. */
246-
export function decodeFeedQueryResult(
247-
data: ArrayBuffer | undefined,
248-
): [PostData[], boolean] {
249-
if (!data) {
250-
return [[], false];
251-
}
252-
253-
const response = v2.GetFeedResponse.fromBinary(new Uint8Array(data));
254-
const hasNext = !!response.pageInfo?.hasNextPage;
255-
return [decodeFeedItems(response), hasNext];
256-
}
257-
258234
/** Get the forward cursor token from a previous feed query result */
259235
export function extractFeedToken(
260236
_status: QueryStatus | undefined,
@@ -333,7 +309,7 @@ export function bundleEventId(bundle: v2.EventBundle): string | null {
333309
try {
334310
const event = v2.Event.fromBinary(bundle.signedEvent.eventBytes);
335311
if (!event.key) return null;
336-
return bytesToHex(v2.EventKey.toBinary(event.key));
312+
return eventKeyId(event.key);
337313
} catch {
338314
return null;
339315
}

apps/polycentric/src/common/lib/polycentric-hooks/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ export {
2929
decodeBundle,
3030
decodePostBundle as decodeV2PostBundle,
3131
decodeFeedItems,
32-
decodeFeedQueryResult,
3332
extractFeedToken,
3433
shouldExtend,
3534
pubkeyStr,
@@ -39,6 +38,7 @@ export {
3938
bytesToHex,
4039
bundleEventId,
4140
hexToBytes,
41+
eventKeyId,
4242
truncateName,
4343
publicKeyToString,
4444
stringToPublicKey,

apps/polycentric/src/common/query/hooks/useQuery.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,15 @@ export enum RefreshStrategy {
6868
Eager,
6969
}
7070

71+
export type QueryKey = string[];
72+
7173
type QueryArgs = {
7274
client: PolycentricClient;
7375
queryKey: QueryKey;
7476
query: Query;
7577
opts: QueryOpts | undefined;
7678
};
7779

78-
type QueryKey = string[];
79-
8080
type SubscriptionRef = {
8181
refCount: number;
8282
dispose: () => void;

apps/polycentric/src/features/composer/hooks/useComposer.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ import {
1212
import { invalidateQuery } from '@/src/common/query/hooks/useQuery';
1313
import { parseTextLinks } from '@/src/common/util/parseTextLinks';
1414
import {
15+
alterPostReplyCount,
1516
feedQueryKeys,
1617
injectPostIntoFeedCache,
18+
injectReplyIntoThreadCache,
19+
threadQueryKey,
1720
} from '@/src/features/feed/hooks/feedCache';
18-
import { injectReplyIntoThreadCache } from '@/src/features/post/hooks/useThread';
1921
import { COLLECTION, type types, v2 } from '@polycentric/react-native';
2022
import * as ImagePicker from 'expo-image-picker';
2123
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
@@ -339,8 +341,13 @@ export function useComposer({
339341

340342
// Optimistically add the new event to the below query
341343
if (isReply && replyTo) {
342-
injectReplyIntoThreadCache(replyTo.id, newBundle);
344+
injectReplyIntoThreadCache(newBundle);
345+
alterPostReplyCount(threadQueryKey(replyTo.id), replyTo.id, 1);
346+
alterPostReplyCount(feedQueryKeys.following(), replyTo.id, 1);
347+
alterPostReplyCount(feedQueryKeys.identity(identity), replyTo.id, 1);
348+
alterPostReplyCount(feedQueryKeys.explore(identity), replyTo.id, 1);
343349
}
350+
344351
injectPostIntoFeedCache(feedQueryKeys.following(), newBundle);
345352
injectPostIntoFeedCache(feedQueryKeys.identity(identity), newBundle);
346353
injectPostIntoFeedCache(feedQueryKeys.explore(identity), newBundle);

0 commit comments

Comments
 (0)