feat: add post mention
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user