refactor(room): WebSocket queue and message editor improvements
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

- Enhance ws_universal.rs with queue message support
- Add queue types and producer improvements
- Simplify MessageBubble.tsx rendering logic
- Refactor IMEditor.tsx with improved message handling
- Update DiscordChatPanel.tsx with message enhancements
This commit is contained in:
ZhenYi 2026-04-18 19:29:36 +08:00
parent c4fb943e07
commit a09f66b779
7 changed files with 20 additions and 33 deletions

View File

@ -397,7 +397,7 @@ async fn poll_push_streams(
if let Some(reactions) = event.reactions.clone() {
return Some(WsPushEvent::ReactionUpdated {
room_id: event.room_id,
message_id: event.id,
message_id: event.message_id.unwrap_or(event.id),
reactions,
});
}

View File

@ -176,7 +176,7 @@ impl MessageProducer {
pub async fn publish_reaction_event(
&self,
room_id: uuid::Uuid,
_message_id: uuid::Uuid,
message_id: uuid::Uuid,
reactions: Vec<ReactionGroup>,
) {
let Some(pubsub) = &self.pubsub else {
@ -196,6 +196,7 @@ impl MessageProducer {
seq: 0,
display_name: None,
reactions: Some(reactions),
message_id: Some(message_id),
};
pubsub.publish_room_message(room_id, &event).await;
}

View File

@ -35,6 +35,9 @@ pub struct RoomMessageEvent {
/// Present when this event carries reaction updates for the message.
#[serde(skip_serializing_if = "Option::is_none")]
pub reactions: Option<Vec<ReactionGroup>>,
/// Target message ID for reaction update events.
#[serde(skip_serializing_if = "Option::is_none")]
pub message_id: Option<Uuid>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -61,6 +64,7 @@ impl From<RoomMessageEnvelope> for RoomMessageEvent {
seq: e.seq,
display_name: None,
reactions: None,
message_id: None,
}
}
}

View File

@ -1065,6 +1065,7 @@ impl RoomService {
display_name: Some(ai_display_name.clone()),
in_reply_to: None,
reactions: None,
message_id: None,
};
room_manager.broadcast(room_id_inner, msg_event).await;
room_manager.metrics.messages_sent.increment(1);
@ -1207,6 +1208,7 @@ impl RoomService {
display_name: model_display_name,
in_reply_to: None,
reactions: None,
message_id: None,
};
room_manager.broadcast(room_id, event).await;

View File

@ -79,6 +79,7 @@ export function DiscordChatPanel({ room, isAdmin, onClose, onDelete }: DiscordCh
(content: string) => {
sendMessage(content, 'text', replyingTo?.id ?? undefined);
setReplyingTo(null);
messageInputRef.current?.clearContent();
},
[sendMessage, replyingTo],
);

View File

@ -11,7 +11,6 @@ import { Button } from '@/components/ui/button';
import { parseFunctionCalls, type FunctionCall } from '@/lib/functionCallParser';
import { formatMessageTime } from '../shared/formatters';
import { cn } from '@/lib/utils';
import { SmilePlus } from 'lucide-react';
import { useUser, useRoom, useTheme } from '@/contexts';
import { memo, useMemo, useState, useCallback, useRef } from 'react';
import { ModelIcon } from '../icon-match';
@ -20,6 +19,7 @@ import { MessageContent } from './MessageContent';
import { ThreadIndicator } from '../RoomThreadPanel';
import { getSenderDisplayName, getSenderModelId, getAvatarFromUiMessage, getSenderUserUid, isUserSender } from '../sender';
import { MessageReactions } from './MessageReactions';
import { ReactionPicker } from './ReactionPicker';
// Sender colors — AI Studio clean palette
const SENDER_COLORS: Record<string, string> = {
@ -375,14 +375,7 @@ export const MessageBubble = memo(function MessageBubble({
className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity absolute -top-3 right-3"
style={{ background: 'var(--card)', border: '1px solid var(--room-border)', borderRadius: 6 }}
>
<button
className="flex h-7 w-7 items-center justify-center rounded-md transition-colors"
style={{ color: 'var(--room-text-muted)' }}
onClick={() => handleReaction('👍')}
title="Add reaction"
>
<SmilePlus className="size-3.5" />
</button>
<ReactionPicker onReact={handleReaction} />
{onReply && (
<button
className="flex h-7 w-7 items-center justify-center rounded-md transition-colors"

View File

@ -13,6 +13,7 @@ import { CustomEmojiNode } from './EmojiNode';
import type { MentionItem, MessageAST, MentionType } from './types';
import { Paperclip, Smile, Send, X } from 'lucide-react';
import { cn } from '@/lib/utils';
import { COMMON_EMOJIS } from '../../shared';
import { useTheme } from '@/contexts';
export interface IMEditorProps {
@ -92,22 +93,7 @@ type Palette = typeof LIGHT;
// ─── Emoji Picker ─────────────────────────────────────────────────────────────
const EMOJIS = [
{ name: 'thumbsup', url: 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72/1f44d.png' },
{ name: 'thumbsdown', url: 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72/1f44e.png' },
{ name: 'heart', url: 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72/2764.png' },
{ name: 'laugh', url: 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72/1f602.png' },
{ name: 'rocket', url: 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72/1f680.png' },
{ name: 'fire', url: 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72/1f525.png' },
{ name: 'eyes', url: 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72/1f440.png' },
{ name: 'check', url: 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72/2705.png' },
{ name: 'star', url: 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72/2b50.png' },
{ name: 'clap', url: 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72/1f44f.png' },
{ name: 'thinking', url: 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72/1f914.png' },
{ name: 'wave', url: 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72/1f44b.png' },
];
function EmojiPicker({ onClose, onSelect, p }: { onClose: () => void; onSelect: (n: string, u: string) => void; p: Palette }) {
function EmojiPicker({ onClose, onSelect, p }: { onClose: () => void; onSelect: (emoji: string) => void; p: Palette }) {
return (
<div
className="absolute bottom-full left-0 mb-2 z-50"
@ -132,14 +118,14 @@ function EmojiPicker({ onClose, onSelect, p }: { onClose: () => void; onSelect:
</button>
</div>
<div className="grid p-2 gap-0.5" style={{ gridTemplateColumns: 'repeat(6, 1fr)' }}>
{EMOJIS.map(e => (
{COMMON_EMOJIS.map(emoji => (
<button
key={e.name}
onClick={() => onSelect(e.name, e.url)}
className="w-9 h-9 flex items-center justify-center rounded-lg transition-all duration-100 cursor-pointer hover:scale-110"
key={emoji}
onClick={() => onSelect(emoji)}
className="w-9 h-9 flex items-center justify-center rounded-lg transition-all duration-100 cursor-pointer hover:scale-110 text-[18px]"
style={{ background: 'transparent' }}
>
<img src={e.url} alt={e.name} className="w-5 h-5 pointer-events-none" />
{emoji}
</button>
))}
</div>
@ -448,7 +434,7 @@ export const IMEditor = forwardRef<IMEditorHandle, IMEditorProps>(function IMEdi
>
<Smile size={18} />
</button>
{showEmoji && <EmojiPicker onClose={() => setShowEmoji(false)} onSelect={(n, u) => { editor?.chain().focus().insertContent({ type: 'emoji', attrs: { name: n, url: u } }).insertContent(' ').run(); setShowEmoji(false); }} p={p} />}
{showEmoji && <EmojiPicker onClose={() => setShowEmoji(false)} onSelect={(emoji) => { editor?.chain().focus().insertContent(emoji).insertContent(' ').run(); setShowEmoji(false); }} p={p} />}
</div>
<label