feat: add unread updates to My Work (#133)
CI / lint (pull_request) Successful in 12s
CI / build-frontend (pull_request) Successful in 5s

This commit is contained in:
timmy committed 2026-08-06 19:59:39 +00:00
1 parent d75a55f3f7
commit f34fc8b5f5
7 files changed
+336 -13

No files matched your search

+64
View File
@@ -2,6 +2,7 @@ import asyncio
import os
import shlex
from typing import Any
from urllib.parse import urlsplit
import httpx
@@ -117,6 +118,69 @@ async def issues() -> list[dict]:
)
def _safe_web_url(value: Any) -> str:
if not isinstance(value, str):
return ""
parsed = urlsplit(value)
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")
if not isinstance(threads, list):
raise ValueError("Gitea notification response was not a list")
normalized = []
for thread in threads:
if not isinstance(thread, dict):
continue
repository = thread.get("repository")
subject = thread.get("subject")
repository = repository if isinstance(repository, dict) else {}
subject = subject if isinstance(subject, dict) else {}
subject_url = _safe_web_url(subject.get("html_url"))
latest_url = _safe_web_url(subject.get("latest_comment_html_url"))
number_text = (
urlsplit(subject_url).path.rstrip("/").rsplit("/", 1)[-1]
if subject_url
else ""
)
normalized.append(
{
"id": thread.get("id"),
"unread": thread.get("unread") is True,
"updated_at": (
thread.get("updated_at")
if isinstance(thread.get("updated_at"), str)
else ""
),
"repository": (
repository.get("full_name")
if isinstance(repository.get("full_name"), str)
else ""
),
"number": int(number_text) if number_text.isdigit() else None,
"title": (
subject.get("title")
if isinstance(subject.get("title"), str) and subject.get("title")
else "Untitled update"
),
"subject_type": (
subject.get("type")
if isinstance(subject.get("type"), str) and subject.get("type")
else "Update"
),
"state": (
subject.get("state")
if isinstance(subject.get("state"), str)
else ""
),
"url": latest_url or subject_url,
"subject_url": subject_url,
}
)
return normalized
async def pull_requests() -> list[dict]:
assigned, review_requested = await asyncio.gather(
fetch("repos/issues/search?state=open&assigned=true&type=pulls&limit=50"),
+6 -1
View File
@@ -14,6 +14,7 @@ from src.gitea_proxy import (
current_user,
is_requested_review,
issues,
notifications,
pull_requests,
pull_review_detail,
repos,
@@ -224,19 +225,23 @@ async def _build_live_snapshot() -> dict:
user_data = await current_user()
if not isinstance(user_data, dict) or not user_data.get("login"):
raise ContextPayloadError("Gitea current-user response was invalid")
context_result, events_result = await asyncio.gather(
context_result, events_result, notifications_result = await asyncio.gather(
_load_context_for_user(user_data),
activity_events(user_data),
notifications(),
return_exceptions=True,
)
context_ok = not isinstance(context_result, BaseException)
events_ok = not isinstance(events_result, BaseException)
notifications_ok = not isinstance(notifications_result, BaseException)
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,
"sections": {
"context": "fresh" if context_ok else "temporarily unavailable",
"events": "fresh" if events_ok else "temporarily unavailable",
"notifications": "fresh" if notifications_ok else "temporarily unavailable",
},
}