feat: add post mention

This commit is contained in:
Krispire Admin
2026-06-10 16:28:57 +05:30
parent d86d876ac1
commit cb62adf2de
11 changed files with 472 additions and 34 deletions
+165
View File
@@ -0,0 +1,165 @@
package repository
import (
"context"
"fmt"
"github.com/prabhupadasays/spsays-bff/internal/model"
)
// maxMentionsPerPost caps how many users a post can tag.
const maxMentionsPerPost = 30
// MentionRepo handles post_mentions membership + mention suggestions.
type MentionRepo struct{ pg *Postgres }
func NewMentionRepo(pg *Postgres) *MentionRepo { return &MentionRepo{pg: pg} }
// Suggest returns up to `limit` users to mention, prioritised: people the
// viewer follows (rank 0), then people who follow the viewer (rank 1), then
// everyone else (rank 2). Excludes the viewer. Optional name/handle search.
// Offset paginates for the infinite-scroll popover.
func (r *MentionRepo) Suggest(ctx context.Context, viewerID, q string, limit, offset int) ([]model.UserSummary, error) {
like := "%" + q + "%"
rows, err := r.pg.Pool.Query(ctx, `
SELECT u.id, u.handle, u.name, u.avatar_url,
CASE
WHEN EXISTS (SELECT 1 FROM follows f
WHERE f.follower_id = $1 AND f.following_id = u.id
AND f.status = 'accepted') THEN 0
WHEN EXISTS (SELECT 1 FROM follows f
WHERE f.follower_id = u.id AND f.following_id = $1
AND f.status = 'accepted') THEN 1
ELSE 2
END AS rank
FROM users u
WHERE u.id <> $1
AND ($2 = '' OR u.name ILIKE $3 OR u.handle ILIKE $3)
ORDER BY rank, u.name, u.id
LIMIT $4 OFFSET $5`,
viewerID, q, like, limit, offset,
)
if err != nil {
return nil, fmt.Errorf("suggest mentions: %w", err)
}
defer rows.Close()
out := make([]model.UserSummary, 0, limit)
for rows.Next() {
var s model.UserSummary
var rank int
if err := rows.Scan(&s.ID, &s.Handle, &s.Name, &s.AvatarURL, &rank); err != nil {
return nil, fmt.Errorf("scan suggestion: %w", err)
}
out = append(out, s)
}
return out, rows.Err()
}
// AddMany tags the given users on a post (idempotent, capped). Used on create.
func (r *MentionRepo) AddMany(ctx context.Context, postID string, userIDs []string) error {
if len(userIDs) > maxMentionsPerPost {
userIDs = userIDs[:maxMentionsPerPost]
}
for _, uid := range userIDs {
if _, err := r.pg.Pool.Exec(ctx,
`INSERT INTO post_mentions (post_id, user_id) VALUES ($1, $2)
ON CONFLICT (post_id, user_id) DO NOTHING`,
postID, uid,
); err != nil {
return fmt.Errorf("add mention: %w", err)
}
}
return nil
}
// Add tags a single user (idempotent), enforcing the per-post cap.
func (r *MentionRepo) Add(ctx context.Context, postID, userID string) error {
var n int
if err := r.pg.Pool.QueryRow(ctx,
`SELECT count(*) FROM post_mentions WHERE post_id = $1`, postID,
).Scan(&n); err != nil {
return fmt.Errorf("count mentions: %w", err)
}
if n >= maxMentionsPerPost {
return fmt.Errorf("a post can mention at most %d people", maxMentionsPerPost)
}
if _, err := r.pg.Pool.Exec(ctx,
`INSERT INTO post_mentions (post_id, user_id) VALUES ($1, $2)
ON CONFLICT (post_id, user_id) DO NOTHING`,
postID, userID,
); err != nil {
return fmt.Errorf("add mention: %w", err)
}
return nil
}
// Remove untags a user from a post.
func (r *MentionRepo) Remove(ctx context.Context, postID, userID string) error {
if _, err := r.pg.Pool.Exec(ctx,
`DELETE FROM post_mentions WHERE post_id = $1 AND user_id = $2`,
postID, userID,
); err != nil {
return fmt.Errorf("remove mention: %w", err)
}
return nil
}
// ListForPost returns the users tagged on a post (insertion order).
func (r *MentionRepo) ListForPost(ctx context.Context, postID string) ([]model.UserSummary, error) {
rows, err := r.pg.Pool.Query(ctx, `
SELECT u.id, u.handle, u.name, u.avatar_url
FROM post_mentions m JOIN users u ON u.id = m.user_id
WHERE m.post_id = $1
ORDER BY m.created_at ASC`, postID)
if err != nil {
return nil, fmt.Errorf("list mentions: %w", err)
}
defer rows.Close()
out := []model.UserSummary{}
for rows.Next() {
var s model.UserSummary
if err := rows.Scan(&s.ID, &s.Handle, &s.Name, &s.AvatarURL); err != nil {
return nil, fmt.Errorf("scan mention: %w", err)
}
out = append(out, s)
}
return out, rows.Err()
}
// HydrateCounts fills MentionsCount on the given posts (one grouped query).
func (r *MentionRepo) HydrateCounts(ctx context.Context, posts []*model.Post) error {
ids := make([]string, 0, len(posts))
for _, p := range posts {
if p != nil {
ids = append(ids, p.ID)
}
}
if len(ids) == 0 {
return nil
}
rows, err := r.pg.Pool.Query(ctx,
`SELECT post_id, count(*) FROM post_mentions WHERE post_id = ANY($1) GROUP BY post_id`, ids)
if err != nil {
return fmt.Errorf("hydrate mention counts: %w", err)
}
counts := make(map[string]int, len(ids))
for rows.Next() {
var id string
var c int
if err := rows.Scan(&id, &c); err != nil {
rows.Close()
return fmt.Errorf("scan mention count: %w", err)
}
counts[id] = c
}
rows.Close()
if err := rows.Err(); err != nil {
return err
}
for _, p := range posts {
if p != nil {
p.MentionsCount = counts[p.ID]
}
}
return nil
}
+26
View File
@@ -171,6 +171,32 @@ func (r *PostRepo) ListByHandle(ctx context.Context, handle string, limit, offse
return r.ListByUser(ctx, userID, limit, offset)
}
// ListMentioningByHandle returns posts that tag the given user (handle),
// newest first — the "Mentions" profile tab. Same shape as the profile feed.
func (r *PostRepo) ListMentioningByHandle(ctx context.Context, handle string, limit, offset int) ([]model.Post, int, error) {
var userID string
if err := r.pg.Pool.QueryRow(ctx, `SELECT id FROM users WHERE handle = $1`, handle).Scan(&userID); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return []model.Post{}, 0, nil
}
return nil, 0, fmt.Errorf("lookup user by handle: %w", err)
}
var total int
if err := r.pg.Pool.QueryRow(ctx,
`SELECT count(*) FROM posts p JOIN post_mentions pm ON pm.post_id = p.id WHERE pm.user_id = $1`,
userID,
).Scan(&total); err != nil {
return nil, 0, fmt.Errorf("count mentioning posts: %w", err)
}
q := `SELECT ` + postSelectColumns + postSelectFrom + `
JOIN post_mentions pm ON pm.post_id = p.id
WHERE pm.user_id = $1
ORDER BY p.created_at DESC, p.id
LIMIT $2 OFFSET $3`
return r.collectPosts(ctx, total, q, userID, limit, offset)
}
// ListByUser returns the user's profile feed — every post they own,
// authored or reshared. Reshares are interleaved naturally because
// they're stored as real rows; sort by created_at gets the right order
+14
View File
@@ -380,6 +380,20 @@ func (r *NotificationRepo) UpsertLike(ctx context.Context, userID, actorID, post
return count, emailDue, nil
}
// CreateMention persists a "you were mentioned" notification linked to the
// post. Unlike likes there's no aggregation — a mention is a deliberate,
// low-volume event.
func (r *NotificationRepo) CreateMention(ctx context.Context, userID, actorID, postID string) error {
if _, err := r.pg.Pool.Exec(ctx,
`INSERT INTO notifications (user_id, actor_id, kind, post_id)
VALUES ($1, $2, 'post_mention', $3)`,
userID, actorID, postID,
); err != nil {
return fmt.Errorf("create mention notification: %w", err)
}
return nil
}
// LikeEmailsEnabled reports whether the user wants aggregated like emails.
func (r *NotificationRepo) LikeEmailsEnabled(ctx context.Context, userID string) (bool, error) {
var enabled bool