feat: friend request notifications
This commit is contained in:
@@ -835,6 +835,21 @@ func (h *ProfileHandler) Following(w http.ResponseWriter, r *http.Request) {
|
|||||||
response.JSON(w, http.StatusOK, followListResp{Users: users, Total: total})
|
response.JSON(w, http.StatusOK, followListResp{Users: users, Total: total})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IncomingFollowRequests — GET /follow-requests?limit=&offset=&q=. The caller's
|
||||||
|
// pending incoming follow requests (auth required), for the notifications
|
||||||
|
// "Friend Requests" tab where they're accepted/rejected.
|
||||||
|
func (h *ProfileHandler) IncomingFollowRequests(w http.ResponseWriter, r *http.Request) {
|
||||||
|
uid := middleware.UserID(r.Context())
|
||||||
|
limit, offset := parseLimitOffset(r)
|
||||||
|
q := strings.TrimSpace(r.URL.Query().Get("q"))
|
||||||
|
users, total, err := h.graph.IncomingRequests(r.Context(), uid, q, limit, offset)
|
||||||
|
if err != nil {
|
||||||
|
mapSocialErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.JSON(w, http.StatusOK, followListResp{Users: users, Total: total})
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// shared helpers
|
// shared helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -98,22 +98,29 @@ func (r *FollowRepo) Status(ctx context.Context, followerID, followingID string)
|
|||||||
// them), newest first, optionally filtered by a name/handle search `q`.
|
// them), newest first, optionally filtered by a name/handle search `q`.
|
||||||
// Returns the page plus the total matching count (for pagination).
|
// Returns the page plus the total matching count (for pagination).
|
||||||
func (r *FollowRepo) ListFollowers(ctx context.Context, userID, q string, limit, offset int) ([]model.UserSummary, int, error) {
|
func (r *FollowRepo) ListFollowers(ctx context.Context, userID, q string, limit, offset int) ([]model.UserSummary, int, error) {
|
||||||
return r.listRelations(ctx, "f.following_id = $1", "f.follower_id", userID, q, limit, offset)
|
return r.listRelations(ctx, "f.following_id = $1", "f.follower_id", "accepted", userID, q, limit, offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListIncomingRequests returns the users with a pending request to follow
|
||||||
|
// userID (newest first), optionally filtered by `q`, plus the total count.
|
||||||
|
func (r *FollowRepo) ListIncomingRequests(ctx context.Context, userID, q string, limit, offset int) ([]model.UserSummary, int, error) {
|
||||||
|
return r.listRelations(ctx, "f.following_id = $1", "f.follower_id", "pending", userID, q, limit, offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListFollowing returns the users userID follows (accepted), newest first,
|
// ListFollowing returns the users userID follows (accepted), newest first,
|
||||||
// optionally filtered by `q`. Returns the page plus the total count.
|
// optionally filtered by `q`. Returns the page plus the total count.
|
||||||
func (r *FollowRepo) ListFollowing(ctx context.Context, userID, q string, limit, offset int) ([]model.UserSummary, int, error) {
|
func (r *FollowRepo) ListFollowing(ctx context.Context, userID, q string, limit, offset int) ([]model.UserSummary, int, error) {
|
||||||
return r.listRelations(ctx, "f.follower_id = $1", "f.following_id", userID, q, limit, offset)
|
return r.listRelations(ctx, "f.follower_id = $1", "f.following_id", "accepted", userID, q, limit, offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
// listRelations powers both directions: `whereOwn` pins the row to userID on
|
// listRelations powers follower/following/pending-request listings: `whereOwn`
|
||||||
// one side, `joinCol` is the follows column holding the *other* user (the one
|
// pins the row to userID on one side, `joinCol` is the follows column holding
|
||||||
// we join to users and return). `q` filters by name/handle (ILIKE); empty `q`
|
// the *other* user (the one we join to users and return), `status` is the edge
|
||||||
// disables the filter. The column expressions are fixed internal constants —
|
// status to match ('accepted' or 'pending'). `q` filters by name/handle
|
||||||
// never user input — so this stays injection-safe.
|
// (ILIKE); empty `q` disables the filter. The column/status expressions are
|
||||||
|
// fixed internal constants — never user input — so this stays injection-safe.
|
||||||
func (r *FollowRepo) listRelations(
|
func (r *FollowRepo) listRelations(
|
||||||
ctx context.Context, whereOwn, joinCol, userID, q string, limit, offset int,
|
ctx context.Context, whereOwn, joinCol, status, userID, q string, limit, offset int,
|
||||||
) ([]model.UserSummary, int, error) {
|
) ([]model.UserSummary, int, error) {
|
||||||
like := "%" + q + "%"
|
like := "%" + q + "%"
|
||||||
|
|
||||||
@@ -121,7 +128,7 @@ func (r *FollowRepo) listRelations(
|
|||||||
countSQL := `
|
countSQL := `
|
||||||
SELECT count(*)
|
SELECT count(*)
|
||||||
FROM follows f JOIN users u ON u.id = ` + joinCol + `
|
FROM follows f JOIN users u ON u.id = ` + joinCol + `
|
||||||
WHERE ` + whereOwn + ` AND f.status = 'accepted'
|
WHERE ` + whereOwn + ` AND f.status = '` + status + `'
|
||||||
AND ($2 = '' OR u.name ILIKE $3 OR u.handle ILIKE $3)`
|
AND ($2 = '' OR u.name ILIKE $3 OR u.handle ILIKE $3)`
|
||||||
if err := r.pg.Pool.QueryRow(ctx, countSQL, userID, q, like).Scan(&total); err != nil {
|
if err := r.pg.Pool.QueryRow(ctx, countSQL, userID, q, like).Scan(&total); err != nil {
|
||||||
return nil, 0, fmt.Errorf("count relations: %w", err)
|
return nil, 0, fmt.Errorf("count relations: %w", err)
|
||||||
@@ -133,7 +140,7 @@ func (r *FollowRepo) listRelations(
|
|||||||
listSQL := `
|
listSQL := `
|
||||||
SELECT u.handle, u.name, u.avatar_url
|
SELECT u.handle, u.name, u.avatar_url
|
||||||
FROM follows f JOIN users u ON u.id = ` + joinCol + `
|
FROM follows f JOIN users u ON u.id = ` + joinCol + `
|
||||||
WHERE ` + whereOwn + ` AND f.status = 'accepted'
|
WHERE ` + whereOwn + ` AND f.status = '` + status + `'
|
||||||
AND ($2 = '' OR u.name ILIKE $3 OR u.handle ILIKE $3)
|
AND ($2 = '' OR u.name ILIKE $3 OR u.handle ILIKE $3)
|
||||||
ORDER BY f.created_at DESC, u.handle ASC
|
ORDER BY f.created_at DESC, u.handle ASC
|
||||||
LIMIT $4 OFFSET $5`
|
LIMIT $4 OFFSET $5`
|
||||||
|
|||||||
@@ -859,6 +859,13 @@ func (s *Server) setupRouter() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Caller's pending incoming follow requests (the notifications
|
||||||
|
// "Friend Requests" tab). Caller-scoped, so it lives outside /u/{handle}.
|
||||||
|
if s.profileH != nil {
|
||||||
|
r.With(middleware.RequireAuth(s.authIssuer, s.userRepo)).
|
||||||
|
Get("/follow-requests", s.profileH.IncomingFollowRequests)
|
||||||
|
}
|
||||||
|
|
||||||
r.Route("/u", func(r chi.Router) {
|
r.Route("/u", func(r chi.Router) {
|
||||||
if s.profileH == nil {
|
if s.profileH == nil {
|
||||||
r.HandleFunc("/*", s.authUnavailable)
|
r.HandleFunc("/*", s.authUnavailable)
|
||||||
|
|||||||
@@ -354,6 +354,13 @@ func (s *SocialGraphService) Following(ctx context.Context, handle, q string, li
|
|||||||
return s.follows.ListFollowing(ctx, u.ID, q, limit, offset)
|
return s.follows.ListFollowing(ctx, u.ID, q, limit, offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IncomingRequests returns the pending follow requests addressed to userID
|
||||||
|
// (the caller) — the "Friend Requests" tab. Caller-scoped, so it takes the id
|
||||||
|
// directly rather than a handle.
|
||||||
|
func (s *SocialGraphService) IncomingRequests(ctx context.Context, userID, q string, limit, offset int) ([]model.UserSummary, int, error) {
|
||||||
|
return s.follows.ListIncomingRequests(ctx, userID, q, limit, offset)
|
||||||
|
}
|
||||||
|
|
||||||
// ProfileExtras is the read used by the public-profile endpoint: the
|
// ProfileExtras is the read used by the public-profile endpoint: the
|
||||||
// target's follower/following counts plus, when a non-owner viewer is
|
// target's follower/following counts plus, when a non-owner viewer is
|
||||||
// signed in, their relationship to the target. rel is nil for anonymous
|
// signed in, their relationship to the target. rel is nil for anonymous
|
||||||
|
|||||||
Reference in New Issue
Block a user