feat: integrate invite API and improve notification handling
- Add invite code display and binding functionality via API - Fix notification unread count sync on auth state change - Improve notification mark read with server state validation - Add auth state listener to trigger notification refresh - Add YaoCoinConverter for coin-to-yao type conversion - Remove YaoLegend from divination screens (UI cleanup) - Abbreviate relation labels in yao detail view - Add re-register notice to account delete screen - Update 'coins' terminology to 'points' in localization - Fix backend points consumption to only run in CHAT mode - Add HttpxAuthNoiseFilter to suppress auth endpoint logging - Fix notification static_schema import path - Add test coverage for notification bloc error handling - Update AGENTS.md page header rules and image handling - Delete deprecated run-dev.sh script
This commit is contained in:
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy.dialects.postgresql import insert
|
||||
from sqlalchemy import func, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -111,3 +112,37 @@ class NotificationRepository:
|
||||
await self._session.execute(stmt)
|
||||
await self._session.flush()
|
||||
return count
|
||||
|
||||
async def commit(self) -> None:
|
||||
await self._session.commit()
|
||||
|
||||
async def link_published_notifications_to_user(self, *, user_id: UUID) -> int:
|
||||
notification_ids = list(
|
||||
(
|
||||
await self._session.execute(
|
||||
select(Notification.id).where(
|
||||
Notification.status == "published",
|
||||
Notification.deleted_at.is_(None),
|
||||
)
|
||||
)
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
if not notification_ids:
|
||||
return 0
|
||||
|
||||
stmt = (
|
||||
insert(UserNotification)
|
||||
.values(
|
||||
[
|
||||
{"user_id": user_id, "notification_id": notification_id}
|
||||
for notification_id in notification_ids
|
||||
]
|
||||
)
|
||||
.on_conflict_do_nothing(index_elements=["user_id", "notification_id"])
|
||||
.returning(UserNotification.id)
|
||||
)
|
||||
result = await self._session.execute(stmt)
|
||||
await self._session.flush()
|
||||
return len(list(result.scalars().all()))
|
||||
|
||||
@@ -4,6 +4,7 @@ from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
|
||||
from core.logging import get_logger
|
||||
from core.auth.models import CurrentUser
|
||||
from v1.notifications.dependencies import get_notification_service
|
||||
from v1.notifications.schemas import (
|
||||
@@ -16,6 +17,7 @@ from v1.notifications.service import NotificationService
|
||||
from v1.users.dependencies import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/notifications", tags=["notifications"])
|
||||
logger = get_logger("v1.notifications.router")
|
||||
|
||||
|
||||
@router.get("", response_model=NotificationListResponse)
|
||||
@@ -39,6 +41,13 @@ async def list_notifications(
|
||||
limit=limit,
|
||||
cursor=parsed_cursor,
|
||||
)
|
||||
logger.info(
|
||||
"Notification list fetched",
|
||||
user_id=str(current_user.id),
|
||||
limit=limit,
|
||||
item_count=len(result.items),
|
||||
has_more=result.has_more,
|
||||
)
|
||||
items = []
|
||||
for item in result.items:
|
||||
items.append(
|
||||
@@ -67,6 +76,11 @@ async def get_unread_count(
|
||||
current_user: Annotated[CurrentUser, Depends(get_current_user)],
|
||||
) -> UnreadCountResponse:
|
||||
count = await service.get_unread_count(user_id=current_user.id)
|
||||
logger.info(
|
||||
"Notification unread count fetched",
|
||||
user_id=str(current_user.id),
|
||||
count=count,
|
||||
)
|
||||
return UnreadCountResponse(count=count)
|
||||
|
||||
|
||||
@@ -95,6 +109,11 @@ async def mark_notification_read(
|
||||
user_notification_id=uid,
|
||||
user_id=current_user.id,
|
||||
)
|
||||
logger.info(
|
||||
"Notification marked as read",
|
||||
user_id=str(current_user.id),
|
||||
user_notification_id=str(uid),
|
||||
)
|
||||
return NotificationItemResponse(
|
||||
id=str(item.id),
|
||||
notificationId=str(item.notification_id),
|
||||
@@ -114,4 +133,9 @@ async def mark_all_read(
|
||||
current_user: Annotated[CurrentUser, Depends(get_current_user)],
|
||||
) -> MarkAllReadResponse:
|
||||
updated_count = await service.mark_all_read(user_id=current_user.id)
|
||||
logger.info(
|
||||
"All notifications marked as read",
|
||||
user_id=str(current_user.id),
|
||||
updated_count=updated_count,
|
||||
)
|
||||
return MarkAllReadResponse(updatedCount=updated_count)
|
||||
|
||||
@@ -1,35 +1,21 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Literal, Union
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from schemas.shared.notification import (
|
||||
NotificationPayload,
|
||||
NotificationPayloadNone,
|
||||
NotificationPayloadRoute,
|
||||
NotificationPayloadUrl,
|
||||
)
|
||||
|
||||
class NotificationPayloadNone(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
action: Literal["none"]
|
||||
|
||||
|
||||
class NotificationPayloadRoute(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
action: Literal["open_route"]
|
||||
route: str = Field(max_length=200)
|
||||
entity_id: str | None = Field(default=None, max_length=64)
|
||||
tab: str | None = Field(default=None, max_length=32)
|
||||
|
||||
|
||||
class NotificationPayloadUrl(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
action: Literal["open_url"]
|
||||
url: str = Field(max_length=500)
|
||||
|
||||
|
||||
NotificationPayload = Union[
|
||||
NotificationPayloadNone, NotificationPayloadRoute, NotificationPayloadUrl
|
||||
__all__ = [
|
||||
"NotificationPayload",
|
||||
"NotificationPayloadNone",
|
||||
"NotificationPayloadRoute",
|
||||
"NotificationPayloadUrl",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -103,6 +103,7 @@ class NotificationService:
|
||||
user_notification_id=user_notification_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
await self._repository.commit()
|
||||
payload = _parse_payload(n.payload)
|
||||
return NotificationListItem(
|
||||
id=un.id,
|
||||
@@ -117,7 +118,15 @@ class NotificationService:
|
||||
)
|
||||
|
||||
async def mark_all_read(self, *, user_id: UUID) -> int:
|
||||
return await self._repository.mark_all_read(user_id=user_id)
|
||||
updated_count = await self._repository.mark_all_read(user_id=user_id)
|
||||
if updated_count > 0:
|
||||
await self._repository.commit()
|
||||
return updated_count
|
||||
|
||||
async def link_published_notifications_to_user(self, *, user_id: UUID) -> int:
|
||||
return await self._repository.link_published_notifications_to_user(
|
||||
user_id=user_id
|
||||
)
|
||||
|
||||
|
||||
def _parse_payload(raw: dict[str, object]) -> NotificationPayload:
|
||||
|
||||
Reference in New Issue
Block a user