feat: reply to unread updates in My Work (#153)
CI / lint (pull_request) Successful in 11s
CI / build-frontend (pull_request) Successful in 4s

This commit is contained in:
timmy committed 2026-08-07 01:01:59 +00:00
1 parent 62ebabb132
commit 83de2a7499
8 files changed
+370 -13

No files matched your search

+35
View File
@@ -1,5 +1,6 @@
import asyncio
import os
import re
import shlex
from typing import Any
from urllib.parse import urlsplit
@@ -279,6 +280,40 @@ async def notification_detail(thread_id: int) -> dict:
}
async def reply_to_notification(thread_id: int, body: str) -> 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 = thread.get("repository")
subject = thread.get("subject")
if not isinstance(repository, dict) or not isinstance(subject, dict):
raise ValueError("Notification does not identify a conversation")
repository_name = repository.get("full_name")
subject_path = _gitea_api_path(subject.get("url"))
match = re.fullmatch(
r"repos/([^/]+/[^/]+)/(issues|pulls)/(\d+)", subject_path
)
if (
not match
or match.group(1) != repository_name
or subject.get("type") not in {"Issue", "Pull"}
):
raise ValueError("Notification subject is not a supported conversation")
response = await _get_client().post(
f"/api/v1/repos/{match.group(1)}/issues/{match.group(3)}/comments",
headers=_auth(),
json={"body": body},
)
response.raise_for_status()
comment = response.json()
if not isinstance(comment, dict):
raise ValueError("Gitea comment response was not an object")
return {
"id": comment.get("id"),
"url": _safe_web_url(comment.get("html_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"),
+35 -10
View File
@@ -5,10 +5,9 @@ from contextlib import asynccontextmanager
from pathlib import Path
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
from pydantic import BaseModel, Field, PositiveInt
from pydantic import BaseModel, Field, PositiveInt, field_validator
from src import gitea_proxy
from src.gitea_proxy import (
@@ -82,6 +81,18 @@ class NotificationReadBatch(BaseModel):
ids: list[PositiveInt] = Field(min_length=1, max_length=50)
class NotificationReply(BaseModel):
body: str = Field(min_length=1, max_length=10_000)
@field_validator("body")
@classmethod
def strip_body(cls, value: str) -> str:
value = value.strip()
if not value:
raise ValueError("reply must not be blank")
return value
def _context_payload(user_data, repo_data, issues_data, prs_data) -> dict:
user_model = User(
id=user_data["id"],
@@ -127,14 +138,6 @@ def _context_payload(user_data, repo_data, issues_data, prs_data) -> dict:
]
return compute(user_model, repo_models, issue_models, pr_models).model_dump()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=False,
allow_methods=["*"],
allow_headers=["*"],
)
app.mount("/static", StaticFiles(directory=FRONTEND_DIR), name="static")
app.include_router(frontend_router)
@@ -626,6 +629,28 @@ async def read_notification(thread_id: int = PathParam(gt=0)) -> JSONResponse:
return JSONResponse({"id": thread_id, "status": "read"})
@app.post("/api/v1/notifications/{thread_id}/reply", status_code=201)
async def reply_to_notification(
reply: NotificationReply, thread_id: int = PathParam(gt=0)
) -> JSONResponse:
try:
result = await asyncio.wait_for(
gitea_proxy.reply_to_notification(thread_id, reply.body),
timeout=NOTIFICATION_MUTATION_TIMEOUT_SECONDS,
)
except Exception:
return JSONResponse(
{
"error": (
"The reply could not be posted. Your draft is safe; please retry."
)
},
status_code=503,
headers={"Retry-After": "1"},
)
return JSONResponse(result, status_code=201)
@app.get("/api/v1/repos/{owner}/{repo}/pulls/{number}/review")
async def review_detail(owner: str, repo: str, number: int):
async def load_requested_review():