feat: add feed

This commit is contained in:
Krispire Admin
2026-06-13 23:55:02 +05:30
parent 2135ec334c
commit 8a2bdf945a
7 changed files with 232 additions and 0 deletions
+97
View File
@@ -213,6 +213,103 @@ func (r *PostRepo) ListByUser(ctx context.Context, userID string, limit, offset
return r.collectPosts(ctx, total, q, userID, limit, offset)
}
// FeedForViewer returns one page of the global home feed for `viewerID`,
// newest-first within a two-tier priority:
//
// tier 0 — posts by people the viewer follows that they haven't liked yet
// tier 1 — everything else (followed-and-liked, followers', strangers')
//
// The viewer's own posts and anything involving a block (either direction)
// are excluded. The tier is computed in ORDER BY via correlated EXISTS, so no
// extra columns leak into scanPost. No total is computed (infinite scroll
// infers "more" from a full page); offset paging keeps it simple. At larger
// scale this would move to a precomputed fan-out / keyset cursor.
func (r *PostRepo) FeedForViewer(ctx context.Context, viewerID string, limit, offset int) ([]model.Post, error) {
q := `SELECT ` + postSelectColumns + postSelectFrom + `
WHERE p.user_id <> $1
AND NOT EXISTS (
SELECT 1 FROM user_blocks b
WHERE (b.blocker_id = $1 AND b.blocked_id = p.user_id)
OR (b.blocker_id = p.user_id AND b.blocked_id = $1)
)
ORDER BY
(CASE
WHEN EXISTS (
SELECT 1 FROM follows f
WHERE f.follower_id = $1 AND f.following_id = p.user_id
AND f.status = 'accepted'
)
AND NOT EXISTS (
SELECT 1 FROM post_likes pl
WHERE pl.post_id = p.id AND pl.user_id = $1
)
THEN 0 ELSE 1
END) ASC,
p.created_at DESC, p.id DESC
LIMIT $2 OFFSET $3`
posts, _, err := r.collectPosts(ctx, 0, q, viewerID, limit, offset)
return posts, err
}
// HydrateViewerRelationships fills ViewerFollowsAuthor / AuthorFollowsViewer
// across a page of feed posts in two batched queries (one per direction).
// No-op for anonymous viewers.
func (r *PostRepo) HydrateViewerRelationships(ctx context.Context, viewerID string, posts []model.Post) error {
if viewerID == "" || len(posts) == 0 {
return nil
}
seen := map[string]bool{}
authorIDs := make([]string, 0, len(posts))
for i := range posts {
id := posts[i].UserID
if id != "" && id != viewerID && !seen[id] {
seen[id] = true
authorIDs = append(authorIDs, id)
}
}
if len(authorIDs) == 0 {
return nil
}
following, err := r.followEdges(ctx,
`SELECT following_id FROM follows
WHERE follower_id = $1 AND status = 'accepted' AND following_id = ANY($2)`,
viewerID, authorIDs)
if err != nil {
return err
}
followers, err := r.followEdges(ctx,
`SELECT follower_id FROM follows
WHERE following_id = $1 AND status = 'accepted' AND follower_id = ANY($2)`,
viewerID, authorIDs)
if err != nil {
return err
}
for i := range posts {
posts[i].ViewerFollowsAuthor = following[posts[i].UserID]
posts[i].AuthorFollowsViewer = followers[posts[i].UserID]
}
return nil
}
// followEdges runs a one-column follow query and collects the ids into a set.
func (r *PostRepo) followEdges(ctx context.Context, query, viewerID string, ids []string) (map[string]bool, error) {
rows, err := r.pg.Pool.Query(ctx, query, viewerID, ids)
if err != nil {
return nil, fmt.Errorf("hydrate follow edges: %w", err)
}
defer rows.Close()
out := map[string]bool{}
for rows.Next() {
var id string
if err := rows.Scan(&id); err != nil {
return nil, fmt.Errorf("scan follow edge: %w", err)
}
out[id] = true
}
return out, rows.Err()
}
// Delete removes a post only if it belongs to the caller. Cascades:
// - the post's images (post_images.post_id FK)
// - any reshares of this post made by other users (posts.reshared_from_post_id FK)