feat: improve chat realtime and media composer UX
All checks were successful
CI / test (push) Successful in 27s
All checks were successful
CI / test (push) Successful in 27s
- add media preview and upload confirmation for image/video - add upload progress tracking for presigned uploads - keep voice recording/upload flow with better UI states - include related realtime/chat updates currently in working tree
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
from sqlalchemy import Select, select
|
||||
from sqlalchemy.orm import aliased
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.chats.models import Chat, ChatMember, ChatMemberRole, ChatType
|
||||
@@ -60,3 +61,21 @@ async def list_user_chat_ids(db: AsyncSession, *, user_id: int) -> list[int]:
|
||||
select(ChatMember.chat_id).where(ChatMember.user_id == user_id).order_by(ChatMember.chat_id.asc())
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def find_private_chat_between_users(db: AsyncSession, *, user_a_id: int, user_b_id: int) -> Chat | None:
|
||||
cm_a = aliased(ChatMember)
|
||||
cm_b = aliased(ChatMember)
|
||||
stmt = (
|
||||
select(Chat)
|
||||
.join(cm_a, cm_a.chat_id == Chat.id)
|
||||
.join(cm_b, cm_b.chat_id == Chat.id)
|
||||
.where(
|
||||
Chat.type == ChatType.PRIVATE,
|
||||
cm_a.user_id == user_a_id,
|
||||
cm_b.user_id == user_b_id,
|
||||
)
|
||||
.limit(1)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@@ -16,6 +16,14 @@ async def create_chat_for_user(db: AsyncSession, *, creator_id: int, payload: Ch
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail="Private chat requires exactly one target user.",
|
||||
)
|
||||
if payload.type == ChatType.PRIVATE:
|
||||
existing_chat = await repository.find_private_chat_between_users(
|
||||
db,
|
||||
user_a_id=creator_id,
|
||||
user_b_id=member_ids[0],
|
||||
)
|
||||
if existing_chat:
|
||||
return existing_chat
|
||||
if payload.type in {ChatType.GROUP, ChatType.CHANNEL} and not payload.title:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
|
||||
@@ -5,6 +5,7 @@ from app.auth.service import get_current_user
|
||||
from app.database.session import get_db
|
||||
from app.messages.schemas import MessageCreateRequest, MessageRead, MessageUpdateRequest
|
||||
from app.messages.service import create_chat_message, delete_message, get_messages, update_message
|
||||
from app.realtime.service import realtime_gateway
|
||||
from app.users.models import User
|
||||
|
||||
router = APIRouter(prefix="/messages", tags=["messages"])
|
||||
@@ -16,7 +17,9 @@ async def create_message(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> MessageRead:
|
||||
return await create_chat_message(db, sender_id=current_user.id, payload=payload)
|
||||
message = await create_chat_message(db, sender_id=current_user.id, payload=payload)
|
||||
await realtime_gateway.publish_message_created(message=message, sender_id=current_user.id)
|
||||
return message
|
||||
|
||||
|
||||
@router.get("/{chat_id}", response_model=list[MessageRead])
|
||||
|
||||
@@ -83,17 +83,7 @@ class RealtimeGateway:
|
||||
sender_id=user_id,
|
||||
payload=MessageCreateRequest(chat_id=payload.chat_id, type=payload.type, text=payload.text),
|
||||
)
|
||||
message_data = MessageRead.model_validate(message).model_dump(mode="json")
|
||||
await self._publish_chat_event(
|
||||
payload.chat_id,
|
||||
event="receive_message",
|
||||
payload={
|
||||
"chat_id": payload.chat_id,
|
||||
"message": message_data,
|
||||
"temp_id": payload.temp_id,
|
||||
"sender_id": user_id,
|
||||
},
|
||||
)
|
||||
await self.publish_message_created(message=message, sender_id=user_id, temp_id=payload.temp_id)
|
||||
|
||||
async def handle_typing_event(self, db: AsyncSession, user_id: int, payload: ChatEventPayload, event: str) -> None:
|
||||
await ensure_chat_membership(db, chat_id=payload.chat_id, user_id=user_id)
|
||||
@@ -149,6 +139,19 @@ class RealtimeGateway:
|
||||
return
|
||||
await self._handle_redis_event(f"chat:{chat_id}", event_payload)
|
||||
|
||||
async def publish_message_created(self, *, message, sender_id: int, temp_id: str | None = None) -> None:
|
||||
message_data = MessageRead.model_validate(message).model_dump(mode="json")
|
||||
await self._publish_chat_event(
|
||||
message.chat_id,
|
||||
event="receive_message",
|
||||
payload={
|
||||
"chat_id": message.chat_id,
|
||||
"message": message_data,
|
||||
"temp_id": temp_id,
|
||||
"sender_id": sender_id,
|
||||
},
|
||||
)
|
||||
|
||||
async def _send_user_event(self, user_id: int, event: OutgoingRealtimeEvent) -> None:
|
||||
user_connections = self._connections.get(user_id, {})
|
||||
if not user_connections:
|
||||
|
||||
Reference in New Issue
Block a user