diff --git a/live-embed/src/Post.tsx b/live-embed/src/Post.tsx index 96b7b91..c69ff18 100644 --- a/live-embed/src/Post.tsx +++ b/live-embed/src/Post.tsx @@ -1,64 +1,30 @@ import { useEffect, useState } from 'react'; - -const nicerSource = source => ({ - 'app.bsky.feed.like:subject.uri': 'like', // likes - 'app.bsky.feed.repost:subject.uri': 'repost', // reposts - 'app.bsky.feed.post:embed.record.uri': 'quote', // normal quotes - 'app.bsky.feed.post:embed.record.record.uri': 'quote', // RecordWithMedia quotes - 'app.bsky.feed.post:reply.root.uri': 'reply', // all child replies -}[source]); +import { getPostStats } from './constellation'; +import linkSources from './linkSources'; export function Post({ atUri, updatedLinks }) { const [baseStats, setBaseStats] = useState({}); - - const nicerStats = {}; - - for (const [collection, paths] of Object.entries(baseStats)) { - for (const [oldStylePath, counts] of Object.entries(paths)) { - const newStylePath = `${collection}:${oldStylePath.slice(1)}`; - const name = nicerSource(newStylePath); - if (!name) continue; // perils of constellation's (soon-deprecated) /all - if (name === 'like') { - nicerStats[name] = counts.distinct_dids; - } else { - nicerStats[name] = counts.records; - } - } - } + const liveStats = { ...baseStats }; for (const [key, val] of Object.entries(updatedLinks)) { - const name = nicerSource(key); - if (!nicerStats[name]) nicerStats[name] = 0; - nicerStats[name] += val; + const name = linkSources[key]; + if (!liveStats[name]) liveStats[name] = 0; + liveStats[name] += val; } useEffect(() => { - let cancel = false; - - (async () => { - try { - const url = new URL('/links/all', 'https://constellation.microcosm.blue'); - url.searchParams.set('target', atUri); - const res = await fetch(url); - if (!res.ok) throw new Error(res); - const { links } = await res.json(); - setBaseStats(links); - } catch (e) { - console.warn('fetching base stats failed', e); - } - })(); - - return () => cancel = true; + let alive = true; + getPostStats(atUri).then( + stats => alive && setBaseStats(stats), + e => console.warn('fetching base stats failed', e)); + return () => alive = false; }, [atUri]); - useState() - - return (

{atUri}
- {JSON.stringify(nicerStats)} + {JSON.stringify(liveStats)}

); diff --git a/live-embed/src/constellation.ts b/live-embed/src/constellation.ts new file mode 100644 index 0000000..4961d06 --- /dev/null +++ b/live-embed/src/constellation.ts @@ -0,0 +1,39 @@ +import linkSources from './linkSources'; + +/** + * get nice historical counts from constellation + * + * constellation's api still uses separated collection/path sources, and + * likes should be distinct where everything else is record counts. + * + * constellation still can only specify one link source per request or /all + * + * handles stuff like that + **/ +export async function getPostStats( + atUri: string, + endpoint: string = 'https://constellation.microcosm.blue' +) { + const url = new URL('/links/all', endpoint); + url.searchParams.set('target', atUri); + const res = await fetch(url); + if (!res.ok) throw new Error(res); + const { links } = await res.json(); + + const niceLinks = {}; + + for (const [collection, paths] of Object.entries(links)) { + for (const [oldStylePath, counts] of Object.entries(paths)) { + const newStylePath = `${collection}:${oldStylePath.slice(1)}`; + const name = linkSources[newStylePath]; + if (!name) continue; // perils of constellation's (soon-deprecated) /all + if (name === 'like') { + niceLinks[name] = counts.distinct_dids; + } else { + niceLinks[name] = counts.records; + } + } + } + + return niceLinks; +} diff --git a/live-embed/src/linkSources.ts b/live-embed/src/linkSources.ts new file mode 100644 index 0000000..1d33217 --- /dev/null +++ b/live-embed/src/linkSources.ts @@ -0,0 +1,9 @@ +const linkSources = { + 'app.bsky.feed.like:subject.uri': 'like', + 'app.bsky.feed.repost:subject.uri': 'repost', // actual repost + 'app.bsky.feed.post:embed.record.uri': 'repost', // normal quote (grouped for count) + 'app.bsky.feed.post:embed.record.record.uri': 'repost', // RecordWithMedia quote (grouped for count) + 'app.bsky.feed.post:reply.root.uri': 'reply', // root: count all descendent replies +}; + +export { linkSources as default }; diff --git a/live-embed/src/samplePosts.ts b/live-embed/src/samplePosts.ts index 0f91975..f1024e7 100644 --- a/live-embed/src/samplePosts.ts +++ b/live-embed/src/samplePosts.ts @@ -1,8 +1,9 @@ +import { getPostStats } from './constellation'; const SEKELETON_API = 'https://discover.bsky.app/xrpc/app.bsky.feed.getFeedSkeleton'; const FEED = 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot'; const POLL_DELAY = 9000; -const POST_LIMIT = 10; +const POST_LIMIT = 5; async function getFeed() { const url = new URL(SEKELETON_API); @@ -31,13 +32,29 @@ export function rotatingPair(onRotate: any) { const { feed } = await getFeed(); if (dying) return; + const withStats = await Promise.all(feed.map(async ({ post }) => { + if (seen.has(post)) return { post, total: 0 }; + let stats = {}; + try { + stats = await getPostStats(post); + } catch (e) { + console.warn('failed to get stats from constellation', e); + } + const total = Array.from(Object.values(stats)).reduce((a, b) => a + b, 0); + return ({ post, total }) + })) + if (dying) return; + + // idk if sorting by most interactions yields more-interactive posts but eh + withStats.sort(({ total: a }, { total: b }) => b - a); + // special case: first load if (A === null && B === null) { - if (feed.length < 2) throw new Error('feed returned fewer than two posts to start'); - seen.add(A = feed[0].post); - seen.add(B = feed[1].post); + if (withStats.length < 2) throw new Error('withStats returned fewer than two posts to start'); + seen.add(A = withStats[0].post); + seen.add(B = withStats[1].post); } else { - for (const { post } of feed) { + for (const { post } of withStats) { if (seen.has(post)) { continue; } @@ -53,7 +70,7 @@ export function rotatingPair(onRotate: any) { } onRotate([A, B]); } catch (e) { - console.error('hmm, failed to get feed', e); + console.error('hmm, failed to get withStats', e); } timer = setTimeout(next, POLL_DELAY); } diff --git a/live-embed/src/spacedust.ts b/live-embed/src/spacedust.ts index 2b010f3..4960b1f 100644 --- a/live-embed/src/spacedust.ts +++ b/live-embed/src/spacedust.ts @@ -1,3 +1,4 @@ +import linkSources from './linkSources'; type SpacedustStatus = 'disconnected' | 'connecting' | 'connected'; @@ -22,13 +23,7 @@ export class Spacedust { #subjects: string[]; #subjectsDirty: boolean = false; // in case we try to update while disconnected - #sources: string[] = [ // hard-coding for demo - "app.bsky.feed.like:subject.uri", // likes - "app.bsky.feed.repost:subject.uri", // reposts - "app.bsky.feed.post:embed.record.uri", // normal quotes - "app.bsky.feed.post:embed.record.record.uri", // RecordWithMedia quotes - "app.bsky.feed.post:reply.root.uri", // all child replies - ]; + #sources: string[] = Object.keys(linkSources); // hard-coding for demo #eol: boolean = false; // flag: we should shut down constructor(