feat: triage unread updates in My Work (#151)
CI / lint (pull_request) Successful in 11s
CI / build-frontend (pull_request) Successful in 5s

This commit is contained in:
timmy committed 2026-08-07 00:35:34 +00:00
1 parent 5b864c028e
commit f7073e1393
7 files changed
+628 -8

No files matched your search

+68
View File
@@ -211,6 +211,74 @@ async def mark_notification_read(thread_id: int) -> None:
response.raise_for_status()
def _gitea_api_path(value: Any) -> str:
if not isinstance(value, str):
return ""
parsed = urlsplit(value)
configured = urlsplit(GITEA_URL)
prefix = "/api/v1/"
if (
parsed.scheme not in {"http", "https"}
or parsed.netloc != configured.netloc
or not parsed.path.startswith(prefix)
):
return ""
return parsed.path[len(prefix):] + (("?" + parsed.query) if parsed.query else "")
async def notification_detail(thread_id: int) -> dict:
thread = await fetch(f"notifications/threads/{thread_id}")
if not isinstance(thread, dict):
raise ValueError("Gitea notification thread response was not an object")
repository_value = thread.get("repository")
repository = repository_value if isinstance(repository_value, dict) else {}
subject_value = thread.get("subject")
subject = subject_value if isinstance(subject_value, dict) else {}
subject_path = _gitea_api_path(subject.get("url"))
comment_path = _gitea_api_path(subject.get("latest_comment_url"))
subject_detail = await fetch(subject_path) if subject_path else {}
comment = await fetch(comment_path) if comment_path else {}
subject_detail = subject_detail if isinstance(subject_detail, dict) else {}
comment = comment if isinstance(comment, dict) else {}
user_value = comment.get("user")
user = user_value if isinstance(user_value, dict) else {}
subject_url = _safe_web_url(subject.get("html_url"))
latest_url = _safe_web_url(comment.get("html_url")) or _safe_web_url(
subject.get("latest_comment_html_url")
)
return {
"id": thread_id,
"repository": repository.get("full_name", "")
if isinstance(repository.get("full_name"), str)
else "",
"title": subject.get("title", "")
if isinstance(subject.get("title"), str)
else "",
"subject_type": subject.get("type", "Update")
if isinstance(subject.get("type"), str)
else "Update",
"state": subject.get("state", "")
if isinstance(subject.get("state"), str)
else "",
"url": latest_url or subject_url,
"subject_body": subject_detail.get("body", "")
if isinstance(subject_detail.get("body"), str)
else "",
"latest_comment": {
"author": user.get("login", "")
if isinstance(user.get("login"), str)
else "",
"body": comment.get("body", "")
if isinstance(comment.get("body"), str)
else "",
"created_at": comment.get("created_at", "")
if isinstance(comment.get("created_at"), str)
else "",
"url": latest_url,
},
}
async def pull_requests() -> list[dict]:
assigned, review_requested = await asyncio.gather(
fetch("repos/issues/search?state=open&assigned=true&type=pulls&limit=50"),
+28 -5
View File
@@ -17,6 +17,7 @@ from src.gitea_proxy import (
is_requested_review,
issues,
mark_notification_read,
notification_detail,
notifications,
pull_requests,
pull_review_detail,
@@ -54,6 +55,7 @@ READINESS_TIMEOUT_SECONDS = 5.0
REVIEW_DETAIL_TIMEOUT_SECONDS = 5.0
NOTIFICATION_MUTATION_TIMEOUT_SECONDS = 5.0
NOTIFICATION_PAGE_TIMEOUT_SECONDS = 5.0
NOTIFICATION_DETAIL_TIMEOUT_SECONDS = 5.0
BULK_NOTIFICATION_CONCURRENCY = 5
BULK_NOTIFICATION_DEADLINE_SECONDS = 6.0
LIVE_SNAPSHOT_FRESHNESS_SECONDS = 8.0
@@ -140,13 +142,10 @@ 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", "/api/v1/notifications"} or (
if request.url.path in {"/api/v1/context", "/api/v1/events", "/api/v1/live"} or (
request.url.path.startswith("/api/v1/repos/")
and request.url.path.endswith("/review")
) or (
request.url.path.startswith("/api/v1/notifications/")
and request.url.path.endswith("/read")
):
) or request.url.path.startswith("/api/v1/notifications"):
response.headers["Cache-Control"] = "no-store"
return response
@@ -581,6 +580,30 @@ async def notification_page(page: int = Query(default=1, ge=1)) -> JSONResponse:
return JSONResponse(result)
@app.get("/api/v1/notifications/{thread_id}")
async def notification_thread_detail(
thread_id: int = PathParam(gt=0),
) -> JSONResponse:
try:
result = await asyncio.wait_for(
notification_detail(thread_id),
timeout=NOTIFICATION_DETAIL_TIMEOUT_SECONDS,
)
except TimeoutError:
return JSONResponse(
{"error": "Loading the update timed out. Please retry."},
status_code=503,
headers={"Retry-After": "1"},
)
except Exception:
return JSONResponse(
{"error": "The update is 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: