feat(chat): add presence metadata and improve web chat core
Some checks failed
CI / test (push) Failing after 22s

- add user last_seen_at with alembic migration and persist on realtime disconnect
- extend chat serialization with private online/last_seen, group members/online, channel subscribers
- add Redis batch presence lookup helper
- update web chat list/header to display status counters and last-seen labels
- improve delivery receipt handling using last_delivered/last_read boundaries
- include chat info panel and related API/type updates
This commit is contained in:
2026-03-08 02:02:09 +03:00
parent 51275692ac
commit e6a271f8be
17 changed files with 564 additions and 6 deletions

View File

@@ -32,3 +32,18 @@ async def is_user_online(user_id: int) -> bool:
return bool(value and str(value).isdigit() and int(value) > 0)
except RedisError:
return False
async def get_users_online_map(user_ids: list[int]) -> dict[int, bool]:
if not user_ids:
return {}
try:
redis = get_redis_client()
keys = [f"presence:user:{user_id}" for user_id in user_ids]
values = await redis.mget(keys)
return {
user_id: bool(value and str(value).isdigit() and int(value) > 0)
for user_id, value in zip(user_ids, values, strict=False)
}
except RedisError:
return {user_id: False for user_id in user_ids}

View File

@@ -9,12 +9,14 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.chats.repository import list_user_chat_ids
from app.chats.service import ensure_chat_membership
from app.database.session import AsyncSessionLocal
from app.messages.schemas import MessageCreateRequest, MessageRead, MessageStatusUpdateRequest
from app.messages.service import create_chat_message, mark_message_status
from app.realtime.models import ConnectionContext
from app.realtime.presence import mark_user_offline, mark_user_online
from app.realtime.repository import RedisRealtimeRepository
from app.realtime.schemas import ChatEventPayload, MessageStatusPayload, OutgoingRealtimeEvent, SendMessagePayload
from app.users.repository import update_user_last_seen_now
class RealtimeGateway:
@@ -76,6 +78,7 @@ class RealtimeGateway:
if not subscribers:
self._chat_subscribers.pop(chat_id, None)
await mark_user_offline(user_id)
await self._persist_last_seen(user_id)
async def handle_send_message(self, db: AsyncSession, user_id: int, payload: SendMessagePayload) -> None:
message = await create_chat_message(
@@ -197,6 +200,7 @@ class RealtimeGateway:
if not subscribers:
self._chat_subscribers.pop(chat_id, None)
await mark_user_offline(user_id)
await self._persist_last_seen(user_id)
@staticmethod
def _extract_chat_id(channel: str) -> int | None:
@@ -207,5 +211,13 @@ class RealtimeGateway:
return None
return int(chat_id)
async def _persist_last_seen(self, user_id: int) -> None:
try:
async with AsyncSessionLocal() as db:
await update_user_last_seen_now(db, user_id=user_id)
await db.commit()
except Exception:
return
realtime_gateway = RealtimeGateway()