diff --git a/src/lib/reddit/RedditPostCard.svelte b/src/lib/reddit/RedditPostCard.svelte index b246160..2f0b40b 100644 --- a/src/lib/reddit/RedditPostCard.svelte +++ b/src/lib/reddit/RedditPostCard.svelte @@ -2,7 +2,7 @@ /* eslint-disable svelte/no-navigation-without-resolve */ import { goto } from '$app/navigation'; import { Avatar } from '@foxui/core'; - import { Heart, MessageCircle, Repeat2 } from '@lucide/svelte'; + import { Heart, MessageCircle, Repeat2, Repeat } from '@lucide/svelte'; import { wireEmbedClicks } from '$lib/components/embed'; import { blueskyPostToPostData } from '$lib/components'; import { Post } from '$lib/components'; @@ -11,9 +11,21 @@ import { loginModalState } from '$lib/LoginModal.svelte'; import { isAccentColor } from './accent-colors'; import type { PostRow, PostWithCommunity } from './db'; - type PostView = Record & { + + // `getQuotedPosts` uses the authenticated appview client, so this view + // already includes per-viewer state like `viewer.like` plus the live + // like/reply/repost counts. We treat the QUOTED post as the canonical + // thing users interact with — the community's wrapping quote post is + // just a forwarder. + type PostView = { uri: string; + cid: string; author: { handle: string }; + likeCount?: number; + replyCount?: number; + repostCount?: number; + viewer?: { like?: string; repost?: string }; + [key: string]: unknown; }; type CardRow = PostRow | PostWithCommunity; @@ -25,15 +37,12 @@ /** Tailwind color label, e.g. "pink". Only needed when `row` is a bare * PostRow (no community_accent_color). On a PostWithCommunity the row * already carries the cached accent color. */ - accentColor: accentColorProp, - /** Initial viewer like URI (from getPostsViewerState) — null = not liked. */ - likeUri: initialLikeUri = null + accentColor: accentColorProp }: { row: CardRow; quoted?: PostView | null; showCommunity?: boolean; accentColor?: string | null; - likeUri?: string | null; } = $props(); const communityHandle = $derived( @@ -82,19 +91,22 @@ return `${Math.floor(s / 86400)}d`; } - // Optimistic like state. `likeUri` comes in from the parent (seeded by - // getPostsViewerState); we also track an optimistic delta on like_count - // so the visible number flips immediately on click. + // All counts + viewer state come straight from the quoted PostView. + // We track optimistic state locally so the heart flips instantly on + // click without waiting for a server roundtrip. let likeUri = $state(null); let likeDelta = $state(0); let likeBusy = $state(false); const isLiked = $derived(likeUri !== null); - const displayLikeCount = $derived(row.like_count + likeDelta); + const baseLikeCount = $derived(quoted?.likeCount ?? 0); + const displayLikeCount = $derived(baseLikeCount + likeDelta); + const replyCount = $derived(quoted?.replyCount ?? 0); + const repostCount = $derived(quoted?.repostCount ?? 0); $effect(() => { - // Reset optimistic state when the parent swaps in fresh viewer data. - // Reading initialLikeUri inside the effect makes it reactive. - likeUri = initialLikeUri; + // Re-seed optimistic state whenever the parent swaps in a fresh + // PostView. Reading inside the effect keeps it reactive. + likeUri = quoted?.viewer?.like ?? null; likeDelta = 0; }); @@ -105,7 +117,7 @@ loginModalState.open = true; return; } - if (likeBusy) return; + if (likeBusy || !quoted) return; likeBusy = true; const wasLiked = isLiked; // Optimistic flip. @@ -123,7 +135,7 @@ } else { likeDelta = 1; try { - const result = await likePost({ uri: row.uri, cid: row.cid }); + const result = await likePost({ uri: quoted.uri, cid: quoted.cid }); likeUri = result.uri; } catch (err) { console.error('[RedditPostCard] like failed', err); @@ -153,10 +165,17 @@ {/if} -

{row.title}

+ {#if row.title} +

{row.title}

+ {:else} +
+ + reposted +
+ {/if} {#if quoted && quotedPostData} -
+
`/profile/${handle}`} onclickhandle={(handle) => goto(`/profile/${handle}`)} /> + +
+ + + {replyCount} + + + {repostCount} + +
{:else if quoted === null} -
+
(quoted post unavailable)
{:else} -
+
{/if} - -
- - - {row.reply_count} - - - {row.repost_count} - -
diff --git a/src/lib/reddit/bot.ts b/src/lib/reddit/bot.ts index 613ab10..888574f 100644 --- a/src/lib/reddit/bot.ts +++ b/src/lib/reddit/bot.ts @@ -470,20 +470,29 @@ async function createSubmissionPost( if (!quotedCid) return false; const createdAt = new Date().toISOString(); + const isRepost = submission.title.length === 0; + let result: { uri: string; cid: string }; try { - result = await client.createRecord('app.bsky.feed.post', { - $type: 'app.bsky.feed.post', - text: submission.title, - createdAt, - embed: { - $type: 'app.bsky.embed.record', - record: { - uri: submission.postUri, - cid: quotedCid + if (isRepost) { + // No title → straight repost. The community account boosts the + // original post, no commentary added. + result = await client.createRecord('app.bsky.feed.repost', { + $type: 'app.bsky.feed.repost', + subject: { uri: submission.postUri, cid: quotedCid }, + createdAt + }); + } else { + result = await client.createRecord('app.bsky.feed.post', { + $type: 'app.bsky.feed.post', + text: submission.title, + createdAt, + embed: { + $type: 'app.bsky.embed.record', + record: { uri: submission.postUri, cid: quotedCid } } - } - }); + }); + } } catch (e) { console.error('[createSubmissionPost] rookery createRecord failed', e); return false; @@ -548,7 +557,6 @@ export async function refreshPostMetrics(db: D1Database): Promise { export async function runCronTick(env: App.Platform['env']): Promise<{ communitiesChecked: number; postsCreated: number; - postsRefreshed: number; errors: string[]; }> { const db = env.DB; @@ -592,15 +600,9 @@ export async function runCronTick(env: App.Platform['env']): Promise<{ } } - const postsRefreshed = await refreshPostMetrics(db).catch((e) => { - errors.push(`refresh: ${String(e)}`); - return 0; - }); - return { communitiesChecked: communities.length, postsCreated, - postsRefreshed, errors }; } diff --git a/src/lib/reddit/submission.ts b/src/lib/reddit/submission.ts index 9b19a6f..5f7a6f7 100644 --- a/src/lib/reddit/submission.ts +++ b/src/lib/reddit/submission.ts @@ -31,6 +31,8 @@ export type DmInput = { * (`app.bsky.embed.record`). * * Returns null if the message doesn't reference a recognizable Bluesky post. + * Empty title is allowed — the bot turns title-less submissions into reposts + * instead of quote posts. */ export async function parseSubmission(msg: DmInput): Promise { const text = msg.text ?? ''; @@ -39,9 +41,7 @@ export async function parseSubmission(msg: DmInput): Promise([]); let quoted = $state>({}); - let viewerStates = $state>({}); onMount(async () => { try { const f = await getHomeFeed({ limit: 50 }); feed = f; if (f.length > 0) { - const uris = f.map((p) => p.uri); - const [quotedRes, viewerRes] = await Promise.all([ - getQuotedPosts({ uris: f.map((p) => p.quoted_post_uri) }), - user.did - ? getPostsViewerState({ uris }) - : Promise.resolve({ states: {} }) - ]); - quoted = quotedRes.posts; - viewerStates = viewerRes.states; + const res = await getQuotedPosts({ uris: f.map((p) => p.quoted_post_uri) }); + quoted = res.posts; } } catch (e) { console.error(e); @@ -54,7 +44,6 @@ row={p} quoted={(quoted[p.quoted_post_uri] ?? undefined) as never} showCommunity - likeUri={viewerStates[p.uri]?.likeUri ?? null} /> {/each}
diff --git a/src/routes/c/[handle]/+page.svelte b/src/routes/c/[handle]/+page.svelte index 189d3fe..32a60b2 100644 --- a/src/routes/c/[handle]/+page.svelte +++ b/src/routes/c/[handle]/+page.svelte @@ -5,12 +5,7 @@ import { Loader2, Check, UserPlus } from '@lucide/svelte'; import { getCommunity, getCommunityPosts } from '$lib/reddit/server/communities.remote'; import { getQuotedPosts } from '$lib/reddit/server/quoted-posts.remote'; - import { - followUser, - unfollowUser, - getProfile, - getPostsViewerState - } from '$lib/atproto/server/feed.remote'; + import { followUser, unfollowUser, getProfile } from '$lib/atproto/server/feed.remote'; import { user } from '$lib/atproto/auth.svelte'; import { loginModalState } from '$lib/LoginModal.svelte'; import RedditPostCard from '$lib/reddit/RedditPostCard.svelte'; @@ -23,7 +18,6 @@ let community = $state(null); let posts = $state([]); let quoted = $state>({}); - let viewerStates = $state>({}); // Follow state — populated after the community loads, if the user is // signed in. `followUri` is the AT-URI of the user's follow record @@ -38,7 +32,6 @@ community = null; posts = []; quoted = {}; - viewerStates = {}; followUri = null; try { const info = await getCommunity({ handle }); @@ -52,15 +45,8 @@ posts = rows; if (rows.length > 0) { - const postUris = rows.map((r) => r.uri); - const [quotedRes, viewerRes] = await Promise.all([ - getQuotedPosts({ uris: rows.map((r) => r.quoted_post_uri) }), - user.did - ? getPostsViewerState({ uris: postUris }) - : Promise.resolve({ states: {} }) - ]); - quoted = quotedRes.posts; - viewerStates = viewerRes.states; + const res = await getQuotedPosts({ uris: rows.map((r) => r.quoted_post_uri) }); + quoted = res.posts; } // If the viewer is signed in, fetch viewer.following from the @@ -175,7 +161,6 @@ row={p} quoted={(quoted[p.quoted_post_uri] ?? undefined) as never} accentColor={community.accentColor} - likeUri={viewerStates[p.uri]?.likeUri ?? null} /> {/each}