use models::RoomId; use queue::{ReactionGroup, RoomMessageEvent, RoomMessageStreamChunkEvent, TypingEvent}; use room::types::NotificationEvent; use super::types::WsOutEvent; use crate::event::{member, message, notify, reaction}; pub struct EventDispatcher; impl EventDispatcher { pub fn dispatch_message(event: &RoomMessageEvent) -> WsOutEvent { WsOutEvent::MessageNew { room_id: event.room_id, data: message::MessageNewService { id: event.id, seq: event.seq, room: event.room_id, sender_type: event.sender_type.clone(), sender_id: event.sender_id, display_name: event.display_name.clone(), thread: event.thread_id, in_reply_to: event.in_reply_to, content: event.content.clone(), content_type: event.content_type.clone(), thinking_content: event.thinking_content.clone(), thinking_is_chunked: false, send_at: event.send_at, reactions: None, }, } } /// Dispatch MessageStreamStart — WS only sends message_id + SSE URL. /// Full chunk content is delivered via dedicated SSE endpoint. pub fn dispatch_stream_start(event: &RoomMessageStreamChunkEvent) -> WsOutEvent { WsOutEvent::MessageStreamStart { room_id: event.room_id, data: message::MessageStreamStartService { message_id: event.message_id, room: event.room_id, sse_url: format!("/ws/ai-stream/{}/{}", event.room_id, event.message_id), display_name: event.display_name.clone(), }, } } /// Dispatch MessageStreamDone — WS notifies client that SSE stream ended. pub fn dispatch_stream_done(event: &RoomMessageStreamChunkEvent) -> WsOutEvent { WsOutEvent::MessageStreamDone { room_id: event.room_id, data: message::MessageStreamDoneService { message_id: event.message_id, room: event.room_id, content: event.content.clone(), thinking_content: None, display_name: event.display_name.clone(), error: event.error.clone(), }, } } pub fn dispatch_typing(event: &TypingEvent) -> WsOutEvent { match event.action.as_str() { "start" => WsOutEvent::TypingStart { room_id: event.room_id, data: member::TypingStartService { room: event.room_id, user: event.user_id, username: event.username.clone(), avatar_url: event.avatar_url.clone(), sender_type: event.sender_type.clone(), }, }, _ => WsOutEvent::TypingStop { room_id: event.room_id, data: member::TypingStopService { room: event.room_id, user: event.user_id, username: event.username.clone(), avatar_url: event.avatar_url.clone(), sender_type: event.sender_type.clone(), }, }, } } pub fn dispatch_notification(event: &NotificationEvent) -> WsOutEvent { WsOutEvent::NotifyCreated { data: notify::NotifyCreatedService { id: event.notification.id, room: event.notification.room, project: event.notification.project, user_id: event.notification.user_id, notification_type: event.event_type.clone(), title: event.notification.title.clone(), content: event.notification.content.clone(), related_message_id: event.notification.related_message_id, related_user_id: event.notification.related_user_id, related_room_id: event.notification.related_room_id, metadata: event.notification.metadata.clone(), created_at: event.timestamp, deep_link_url: event.deep_link_url.clone(), }, } } pub fn dispatch_reactions( room_id: RoomId, message_id: uuid::Uuid, reactions: &[ReactionGroup], ) -> WsOutEvent { WsOutEvent::ReactionBatchUpdated { room_id, data: reaction::ReactionBatchUpdatedService { room: room_id, message: message_id, reactions: reactions .iter() .map(|g| reaction::ReactionGroup { emoji: g.emoji.clone(), count: g.count as i64, reacted_by_me: g.reacted_by_me, users: g .users .iter() .filter_map(|u| u.parse::().ok()) .collect(), }) .collect(), }, } } }