feat: paginate unread updates inbox (#147)
CI / lint (pull_request) Successful in 11s
CI / build-frontend (pull_request) Successful in 5s

This commit is contained in:
timmy committed 2026-08-06 23:30:32 +00:00
1 parent 953f59fb19
commit a86de5cbf4
9 files changed
+350 -31

No files matched your search

+24 -2
View File
@@ -125,8 +125,7 @@ def _safe_web_url(value: Any) -> str:
return value if parsed.scheme in {"http", "https"} and parsed.netloc else ""
async def notifications() -> list[dict]:
threads = await fetch("notifications?status-types=unread&limit=50")
def _normalize_notifications(threads: Any) -> list[dict]:
if not isinstance(threads, list):
raise ValueError("Gitea notification response was not a list")
normalized = []
@@ -181,6 +180,29 @@ async def notifications() -> list[dict]:
return normalized
async def notifications() -> dict:
return await notification_page(1)
async def notification_page(page: int, limit: int = 50) -> dict:
response = await _get_client().get(
f"/api/v1/notifications?status-types=unread&limit={limit}&page={page}",
headers=_auth(),
)
response.raise_for_status()
items = _normalize_notifications(response.json())
try:
total = max(len(items), int(response.headers.get("X-Total-Count", len(items))))
except (TypeError, ValueError):
total = len(items)
return {
"items": items,
"page": page,
"total": total,
"has_more": page * limit < total,
}
async def mark_notification_read(thread_id: int) -> None:
response = await _get_client().patch(
f"/api/v1/notifications/threads/{thread_id}?to-status=read",
+37 -3
View File
@@ -4,7 +4,7 @@ import time
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI, HTTPException, Path as PathParam
from fastapi import FastAPI, HTTPException, Path as PathParam, Query
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
@@ -53,6 +53,7 @@ EVENT_STREAM_TIMEOUT_SECONDS = 5.0
READINESS_TIMEOUT_SECONDS = 5.0
REVIEW_DETAIL_TIMEOUT_SECONDS = 5.0
NOTIFICATION_MUTATION_TIMEOUT_SECONDS = 5.0
NOTIFICATION_PAGE_TIMEOUT_SECONDS = 5.0
BULK_NOTIFICATION_CONCURRENCY = 5
BULK_NOTIFICATION_DEADLINE_SECONDS = 6.0
LIVE_SNAPSHOT_FRESHNESS_SECONDS = 8.0
@@ -135,7 +136,7 @@ app.include_router(frontend_router)
@app.middleware("http")
async def prevent_live_api_caching(request, call_next):
response = await call_next(request)
if request.url.path in {"/api/v1/context", "/api/v1/events", "/api/v1/live"} or (
if request.url.path in {"/api/v1/context", "/api/v1/events", "/api/v1/live", "/api/v1/notifications"} or (
request.url.path.startswith("/api/v1/repos/")
and request.url.path.endswith("/review")
) or (
@@ -263,10 +264,21 @@ async def _build_live_snapshot() -> dict:
context_ok = not isinstance(context_result, BaseException)
events_ok = not isinstance(events_result, BaseException)
notifications_ok = not isinstance(notifications_result, BaseException)
notification_items = notifications_result
notification_pagination = None
if notifications_ok and isinstance(notifications_result, dict):
notification_items = notifications_result.get("items")
notification_pagination = {
"page": notifications_result.get("page", 1),
"total": notifications_result.get("total", 0),
"has_more": notifications_result.get("has_more") is True,
}
notifications_ok = isinstance(notification_items, list)
return {
"context": context_result if context_ok else None,
"events": events_result if events_ok else None,
"notifications": notifications_result if notifications_ok else None,
"notifications": notification_items if notifications_ok else None,
"notification_pagination": notification_pagination if notifications_ok else None,
"sections": {
"context": "fresh" if context_ok else "temporarily unavailable",
"events": "fresh" if events_ok else "temporarily unavailable",
@@ -479,6 +491,28 @@ async def read_notifications(batch: NotificationReadBatch) -> JSONResponse:
)
@app.get("/api/v1/notifications")
async def notification_page(page: int = Query(default=1, ge=1)) -> JSONResponse:
try:
result = await asyncio.wait_for(
gitea_proxy.notification_page(page),
timeout=NOTIFICATION_PAGE_TIMEOUT_SECONDS,
)
except TimeoutError:
return JSONResponse(
{"error": "Unread updates timed out. Please retry."},
status_code=503,
headers={"Retry-After": "1"},
)
except Exception:
return JSONResponse(
{"error": "Unread updates are temporarily unavailable. Please retry."},
status_code=503,
headers={"Retry-After": "1"},
)
return JSONResponse(result)
@app.patch("/api/v1/notifications/{thread_id}/read")
async def read_notification(thread_id: int = PathParam(gt=0)) -> JSONResponse:
try: