fix(room): align ReactionGroup types with frontend and guard reaction update handler
Some checks are pending
CI / Rust Lint & Check (push) Waiting to run
CI / Rust Tests (push) Waiting to run
CI / Frontend Lint & Type Check (push) Waiting to run
CI / Frontend Build (push) Blocked by required conditions

- Fix ReactionGroup.count: i64 -> i32 and users: Vec<Uuid> -> Vec<String>
  to match frontend ReactionItem (count: number, users: string[]).
  Mismatched types caused the WS reaction update to silently fail.
  Also update ReactionItem in api/ws_types.rs to match.
- Add activeRoomIdRef guard in onRoomReactionUpdated to prevent stale
  room state from processing outdated events after room switch.
- Switch from prev.map() to targeted findIndex+spread in onRoomReactionUpdated
  to avoid unnecessary array recreation.
This commit is contained in:
ZhenYi 2026-04-17 22:59:13 +08:00
parent 047782e585
commit a171d691c6
4 changed files with 27 additions and 19 deletions

View File

@ -449,9 +449,9 @@ impl From<room::MessageReactionsResponse> for ReactionListData {
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct ReactionItem { pub struct ReactionItem {
pub emoji: String, pub emoji: String,
pub count: i64, pub count: i32,
pub reacted_by_me: bool, pub reacted_by_me: bool,
pub users: Vec<Uuid>, pub users: Vec<String>,
} }
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]

View File

@ -40,9 +40,10 @@ pub struct RoomMessageEvent {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReactionGroup { pub struct ReactionGroup {
pub emoji: String, pub emoji: String,
pub count: i64, pub count: i32,
pub reacted_by_me: bool, pub reacted_by_me: bool,
pub users: Vec<Uuid>, /// Stored as strings (UUIDs) to match the frontend's `users: string[]` type.
pub users: Vec<String>,
} }
impl From<RoomMessageEnvelope> for RoomMessageEvent { impl From<RoomMessageEnvelope> for RoomMessageEvent {

View File

@ -11,9 +11,9 @@ use uuid::Uuid;
#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)] #[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
pub struct ReactionGroupResponse { pub struct ReactionGroupResponse {
pub emoji: String, pub emoji: String,
pub count: i64, pub count: i32,
pub reacted_by_me: bool, pub reacted_by_me: bool,
pub users: Vec<Uuid>, pub users: Vec<String>,
} }
#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)] #[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
@ -83,9 +83,9 @@ impl RoomService {
.into_iter() .into_iter()
.map(|g| ReactionGroup { .map(|g| ReactionGroup {
emoji: g.emoji, emoji: g.emoji,
count: g.count, count: g.count as i32,
reacted_by_me: g.reacted_by_me, reacted_by_me: g.reacted_by_me,
users: g.users, users: g.users.into_iter().map(|u| u.to_string()).collect(),
}) })
.collect(); .collect();
self.queue self.queue
@ -121,9 +121,9 @@ impl RoomService {
.into_iter() .into_iter()
.map(|g| ReactionGroup { .map(|g| ReactionGroup {
emoji: g.emoji, emoji: g.emoji,
count: g.count, count: g.count as i32,
reacted_by_me: g.reacted_by_me, reacted_by_me: g.reacted_by_me,
users: g.users, users: g.users.into_iter().map(|u| u.to_string()).collect(),
}) })
.collect(); .collect();
self.queue self.queue
@ -258,11 +258,15 @@ impl RoomService {
grouped grouped
.into_iter() .into_iter()
.map(|(emoji, user_reactions)| { .map(|(emoji, user_reactions)| {
let count = user_reactions.len() as i64; let count = user_reactions.len() as i32;
let reacted_by_me = current_user_id let reacted_by_me = current_user_id
.map(|uid| user_reactions.iter().any(|r| r.user == uid)) .map(|uid| user_reactions.iter().any(|r| r.user == uid))
.unwrap_or(false); .unwrap_or(false);
let users = user_reactions.iter().take(3).map(|r| r.user).collect(); let users = user_reactions
.iter()
.take(3)
.map(|r| r.user.to_string())
.collect();
ReactionGroupResponse { ReactionGroupResponse {
emoji, emoji,

View File

@ -517,15 +517,18 @@ export function RoomProvider({
} }
}, },
onRoomReactionUpdated: (payload: RoomReactionUpdatedPayload) => { onRoomReactionUpdated: (payload: RoomReactionUpdatedPayload) => {
// Guard: ignore events for rooms that are no longer active.
// Without this, a WS event arriving after room switch could update
// the wrong room's message list (same message ID, different room).
if (!activeRoomIdRef.current) return;
setMessages((prev) => { setMessages((prev) => {
const updated = prev.map((m) => const existingIdx = prev.findIndex((m) => m.id === payload.message_id);
m.id === payload.message_id if (existingIdx === -1) return prev;
? { ...m, reactions: payload.reactions } const updated = [...prev];
: m, updated[existingIdx] = { ...updated[existingIdx], reactions: payload.reactions };
);
// Persist reaction update to IndexedDB // Persist reaction update to IndexedDB
const msg = updated.find((m) => m.id === payload.message_id); saveMessage(updated[existingIdx]).catch(() => {});
if (msg) saveMessage(msg).catch(() => {});
return updated; return updated;
}); });
}, },