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
228 lines
7.2 KiB
Rust
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(())
|
|
}
|
|
}
|