grain.social is a photo sharing platform built on atproto. grain.social
atproto photography appview
57
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: prevent duplicate favorites and dedupe fav counts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+14 -4
+2 -2
app/lib/components/molecules/FavoriteButton.svelte
··· 69 69 type="button" 70 70 class="stat faved" 71 71 title="Unfavorite" 72 - onclick={() => requireAuth() && favUri && favUri !== 'pending' && deleteFavMut.mutate(favUri)} 72 + onclick={() => requireAuth() && !createFavMut.isPending && !deleteFavMut.isPending && favUri && favUri !== 'pending' && deleteFavMut.mutate(favUri)} 73 73 > 74 74 <Heart size={22} fill="currentColor" /> 75 75 {#if displayCount > 0}<span class="stat-count">{displayCount}</span>{/if} ··· 79 79 type="button" 80 80 class="stat" 81 81 title="Favorite" 82 - onclick={() => requireAuth() && createFavMut.mutate()} 82 + onclick={() => requireAuth() && !createFavMut.isPending && !deleteFavMut.isPending && !isFaved && createFavMut.mutate()} 83 83 > 84 84 <Heart size={22} /> 85 85 {#if displayCount > 0}<span class="stat-count">{displayCount}</span>{/if}
+1 -1
app/lib/components/molecules/FollowButton.svelte
··· 73 73 if (!requireAuth()) return 74 74 if (isFollowing && followUri && followUri !== 'pending') { 75 75 unfollowMut.mutate(followUri) 76 - } else if (!isFollowing) { 76 + } else if (!isFollowing && !followMut.isPending) { 77 77 followMut.mutate() 78 78 } 79 79 }
+11 -1
server/feeds/_hydrate.ts
··· 79 79 80 80 const [profiles, favCounts, commentCounts, labelsByUri, galleryItemRows] = await Promise.all([ 81 81 ctx.lookup<GrainActorProfile>("social.grain.actor.profile", "did", dids), 82 - ctx.count("social.grain.favorite", "subject", galleryUris), 82 + galleryUris.length > 0 83 + ? (ctx.db.query( 84 + `SELECT subject, COUNT(DISTINCT did) as count FROM "social.grain.favorite" 85 + WHERE subject IN (${galleryUris.map((_, i) => `$${i + 1}`).join(",")}) GROUP BY subject`, 86 + galleryUris, 87 + ) as Promise<{ subject: string; count: number }[]>).then((rows) => { 88 + const m = new Map<string, number>(); 89 + for (const r of rows) m.set(r.subject, Number(r.count)); 90 + return m; 91 + }) 92 + : Promise.resolve(new Map<string, number>()), 83 93 ctx.count("social.grain.comment", "subject", galleryUris), 84 94 ctx.labels(galleryUris) as Promise<Map<string, Label[]>>, 85 95 galleryUris.length > 0