gitdataai/libs/room/src/draft_and_history.rs
ZhenYi cf5c728286 fix(room): fix scrolling lag, N+1 queries, and multiple WS token requests
Frontend:
- P0: Replace constant estimateSize(40px) with content-based estimation
  using line count and reply presence for accurate virtual list scroll
- P1: Replace Shadow DOM custom elements with styled spans for @mentions,
  eliminating expensive attachShadow calls per mention instance
- P1: Remove per-message ResizeObserver (one per bubble), replace with
  static inline toolbar layout to avoid observer overhead
- P2: Fix WS token re-fetch on every room switch by preserving token
  across navigation and not clearing activeRoomIdRef on cleanup

Backend:
- P1: Fix reaction check+insert race condition by moving into transaction
  instead of separate query + on-conflict insert
- P2: Fix N+1 queries in get_mention_notifications with batch fetch
  for users and rooms using IN clauses
- P2: Update room_last_activity in broadcast_stream_chunk to prevent
  idle room cleanup during active AI streaming
- P3: Use enum comparison instead of to_string() in room_member_leave
2026-04-17 21:08:40 +08:00

228 lines
7.2 KiB
Rust

use crate::error::RoomError;
use crate::service::RoomService;
use crate::ws_context::WsUserContext;
use chrono::Utc;
use models::rooms::NotificationType;
use models::rooms::room_message_edit_history;
use models::users::user as user_model;
use sea_orm::*;
use uuid::Uuid;
#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
pub struct MessageEditHistoryEntry {
pub old_content: String,
pub new_content: String,
pub edited_at: chrono::DateTime<Utc>,
}
#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
pub struct MessageEditHistoryResponse {
pub message_id: Uuid,
pub history: Vec<MessageEditHistoryEntry>,
pub total_edits: i64,
}
#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
pub struct MentionNotificationResponse {
pub message_id: Uuid,
pub mentioned_by: Uuid,
pub mentioned_by_name: String,
pub content_preview: String,
pub room_id: Uuid,
pub room_name: String,
pub created_at: chrono::DateTime<Utc>,
}
#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
pub struct DraftResponse {
pub room_id: Uuid,
pub content: String,
pub saved_at: chrono::DateTime<Utc>,
}
#[derive(Debug, Clone, serde::Deserialize)]
pub struct DraftSaveRequest {
pub content: String,
}
impl RoomService {
pub async fn save_message_edit_history(
&self,
message_id: Uuid,
user_id: Uuid,
old_content: String,
new_content: String,
) -> Result<(), RoomError> {
let history = room_message_edit_history::ActiveModel {
id: Set(Uuid::now_v7()),
message: Set(message_id),
user: Set(user_id),
old_content: Set(old_content),
new_content: Set(new_content),
edited_at: Set(Utc::now()),
};
history.insert(&self.db).await?;
Ok(())
}
pub async fn get_message_edit_history(
&self,
message_id: Uuid,
ctx: &WsUserContext,
) -> Result<MessageEditHistoryResponse, RoomError> {
let user_id = ctx.user_id;
let message = models::rooms::room_message::Entity::find_by_id(message_id)
.one(&self.db)
.await?
.ok_or_else(|| RoomError::NotFound("Message not found".to_string()))?;
self.require_room_member(message.room, user_id).await?;
let history = room_message_edit_history::Entity::find()
.filter(room_message_edit_history::Column::Message.eq(message_id))
.order_by_asc(room_message_edit_history::Column::EditedAt)
.all(&self.db)
.await?;
let total_edits = history.len() as i64;
let history_entries = history
.into_iter()
.map(|h| MessageEditHistoryEntry {
old_content: h.old_content,
new_content: h.new_content,
edited_at: h.edited_at,
})
.collect();
Ok(MessageEditHistoryResponse {
message_id,
history: history_entries,
total_edits,
})
}
pub async fn get_mention_notifications(
&self,
limit: Option<u64>,
ctx: &WsUserContext,
) -> Result<Vec<MentionNotificationResponse>, RoomError> {
let user_id = ctx.user_id;
let limit = limit.unwrap_or(50);
let notifications = models::rooms::room_notifications::Entity::find()
.filter(models::rooms::room_notifications::Column::UserId.eq(user_id))
.filter(
models::rooms::room_notifications::Column::NotificationType
.eq(NotificationType::Mention),
)
.order_by_desc(models::rooms::room_notifications::Column::CreatedAt)
.limit(limit)
.all(&self.db)
.await?;
// Batch fetch related users to avoid N+1 queries
let related_user_ids: Vec<Uuid> = notifications
.iter()
.filter_map(|n| n.related_user_id)
.collect();
let users: std::collections::HashMap<Uuid, String> = if !related_user_ids.is_empty() {
user_model::Entity::find()
.filter(user_model::Column::Uid.is_in(related_user_ids))
.all(&self.db)
.await?
.into_iter()
.map(|u| (u.uid, u.display_name.unwrap_or(u.username)))
.collect()
} else {
std::collections::HashMap::new()
};
// Batch fetch room names to avoid N+1 queries
let room_ids: Vec<Uuid> = notifications
.iter()
.filter_map(|n| n.room)
.collect();
let rooms: std::collections::HashMap<Uuid, String> = if !room_ids.is_empty() {
models::rooms::room::Entity::find()
.filter(models::rooms::room::Column::Id.is_in(room_ids))
.all(&self.db)
.await?
.into_iter()
.map(|r| (r.id, r.room_name))
.collect()
} else {
std::collections::HashMap::new()
};
let result = notifications
.into_iter()
.map(|notification| {
let mentioned_by_name = notification
.related_user_id
.and_then(|uid| users.get(&uid))
.cloned()
.unwrap_or_else(|| "Unknown User".to_string());
let room_name = notification
.room
.and_then(|rid| rooms.get(&rid))
.cloned()
.unwrap_or_else(|| "Unknown Room".to_string());
let content_preview = notification
.content
.unwrap_or_default()
.chars()
.take(100)
.collect();
MentionNotificationResponse {
message_id: notification.related_message_id.unwrap_or_default(),
mentioned_by: notification.related_user_id.unwrap_or_default(),
mentioned_by_name,
content_preview,
room_id: notification.room.unwrap_or_default(),
room_name,
created_at: notification.created_at,
}
})
.collect();
Ok(result)
}
pub async fn mark_mention_notifications_read(
&self,
ctx: &WsUserContext,
) -> Result<(), RoomError> {
let user_id = ctx.user_id;
use sea_orm::sea_query::Expr;
let now = Utc::now();
models::rooms::room_notifications::Entity::update_many()
.col_expr(
models::rooms::room_notifications::Column::IsRead,
Expr::value(true),
)
.col_expr(
models::rooms::room_notifications::Column::ReadAt,
Expr::value(Some(now)),
)
.filter(models::rooms::room_notifications::Column::UserId.eq(user_id))
.filter(
models::rooms::room_notifications::Column::NotificationType
.eq(NotificationType::Mention),
)
.filter(models::rooms::room_notifications::Column::IsRead.eq(false))
.exec(&self.db)
.await?;
Ok(())
}
}