feat(api): read-all takes an optional up_to cutoff (M489)
release / govulncheck (push) Successful in 16s
release / web (push) Successful in 1m22s
release / go (push) Successful in 1m40s
release / integration (push) Successful in 4m40s
release / android (push) Successful in 5m1s
release / Build signed APK (releases and dev) (push) Successful in 5m12s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 25s
release / Verify release artifacts (tag releases only) (push) Skipped

POST /api/me/notifications/read-all accepts {"up_to": RFC3339}. Android
queues "mark all read" for replay when offline, and a replay landing later
must not mark notices that arrived in between, which the user never saw.
A coalesced notice updated since then has a newer created_at, so it stays
unread. An empty body still marks everything.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 07:27:29 -04:00
co-authored by Claude Opus 5.5
parent 87f8ed6147
commit f11dba2336
5 changed files with 65 additions and 6 deletions
+14 -3
View File
@@ -214,11 +214,22 @@ func (q *Queries) ListNotifications(ctx context.Context, arg ListNotificationsPa
const markAllNotificationsRead = `-- name: MarkAllNotificationsRead :execrows
UPDATE user_notifications
SET read_at = now()
WHERE user_id = $1 AND read_at IS NULL
WHERE user_id = $1
AND read_at IS NULL
AND ($2::timestamptz IS NULL OR created_at <= $2)
`
func (q *Queries) MarkAllNotificationsRead(ctx context.Context, userID pgtype.UUID) (int64, error) {
result, err := q.db.Exec(ctx, markAllNotificationsRead, userID)
type MarkAllNotificationsReadParams struct {
UserID pgtype.UUID
UpTo pgtype.Timestamptz
}
// up_to, when set, limits it to what existed when the user asked: a "mark
// all read" queued offline and replayed later must not mark notices that
// arrived in between, which the user never saw. A coalesced row updated
// since then carries a newer created_at, so it stays unread too.
func (q *Queries) MarkAllNotificationsRead(ctx context.Context, arg MarkAllNotificationsReadParams) (int64, error) {
result, err := q.db.Exec(ctx, markAllNotificationsRead, arg.UserID, arg.UpTo)
if err != nil {
return 0, err
}
+7 -1
View File
@@ -57,9 +57,15 @@ UPDATE user_notifications
WHERE id = sqlc.arg(id) AND user_id = sqlc.arg(user_id);
-- name: MarkAllNotificationsRead :execrows
-- up_to, when set, limits it to what existed when the user asked: a "mark
-- all read" queued offline and replayed later must not mark notices that
-- arrived in between, which the user never saw. A coalesced row updated
-- since then carries a newer created_at, so it stays unread too.
UPDATE user_notifications
SET read_at = now()
WHERE user_id = $1 AND read_at IS NULL;
WHERE user_id = sqlc.arg(user_id)
AND read_at IS NULL
AND (sqlc.narg(up_to)::timestamptz IS NULL OR created_at <= sqlc.narg(up_to));
-- name: TrimNotifications :execrows
-- Retention: read rows go after read_cutoff, and anything at all after