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() { if let Some(reactions) = event.reactions.clone() {
return Some(WsPushEvent::ReactionUpdated { return Some(WsPushEvent::ReactionUpdated {
room_id: event.room_id, room_id: event.room_id,
message_id: event.id, message_id: event.message_id.unwrap_or(event.id),
reactions, reactions,
}); });
} }

View File

@ -176,7 +176,7 @@ impl MessageProducer {
pub async fn publish_reaction_event( pub async fn publish_reaction_event(
&self, &self,
room_id: uuid::Uuid, room_id: uuid::Uuid,
_message_id: uuid::Uuid, message_id: uuid::Uuid,
reactions: Vec<ReactionGroup>, reactions: Vec<ReactionGroup>,
) { ) {
let Some(pubsub) = &self.pubsub else { let Some(pubsub) = &self.pubsub else {
@ -196,6 +196,7 @@ impl MessageProducer {
seq: 0, seq: 0,
display_name: None, display_name: None,
reactions: Some(reactions), reactions: Some(reactions),
message_id: Some(message_id),
}; };
pubsub.publish_room_message(room_id, &event).await; 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. /// Present when this event carries reaction updates for the message.
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub reactions: Option<Vec<ReactionGroup>>, 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)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -61,6 +64,7 @@ impl From<RoomMessageEnvelope> for RoomMessageEvent {
seq: e.seq, seq: e.seq,
display_name: None, display_name: None,
reactions: None, reactions: None,
message_id: None,
} }
} }
} }

View File

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

View File

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

View File

@ -11,7 +11,6 @@ import { Button } from '@/components/ui/button';
import { parseFunctionCalls, type FunctionCall } from '@/lib/functionCallParser'; import { parseFunctionCalls, type FunctionCall } from '@/lib/functionCallParser';
import { formatMessageTime } from '../shared/formatters'; import { formatMessageTime } from '../shared/formatters';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { SmilePlus } from 'lucide-react';
import { useUser, useRoom, useTheme } from '@/contexts'; import { useUser, useRoom, useTheme } from '@/contexts';
import { memo, useMemo, useState, useCallback, useRef } from 'react'; import { memo, useMemo, useState, useCallback, useRef } from 'react';
import { ModelIcon } from '../icon-match'; import { ModelIcon } from '../icon-match';
@ -20,6 +19,7 @@ import { MessageContent } from './MessageContent';
import { ThreadIndicator } from '../RoomThreadPanel'; import { ThreadIndicator } from '../RoomThreadPanel';
import { getSenderDisplayName, getSenderModelId, getAvatarFromUiMessage, getSenderUserUid, isUserSender } from '../sender'; import { getSenderDisplayName, getSenderModelId, getAvatarFromUiMessage, getSenderUserUid, isUserSender } from '../sender';
import { MessageReactions } from './MessageReactions'; import { MessageReactions } from './MessageReactions';
import { ReactionPicker } from './ReactionPicker';
// Sender colors — AI Studio clean palette // Sender colors — AI Studio clean palette
const SENDER_COLORS: Record<string, string> = { 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" 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 }} style={{ background: 'var(--card)', border: '1px solid var(--room-border)', borderRadius: 6 }}
> >
<button <ReactionPicker onReact={handleReaction} />
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>
{onReply && ( {onReply && (
<button <button
className="flex h-7 w-7 items-center justify-center rounded-md transition-colors" 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 type { MentionItem, MessageAST, MentionType } from './types';
import { Paperclip, Smile, Send, X } from 'lucide-react'; import { Paperclip, Smile, Send, X } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { COMMON_EMOJIS } from '../../shared';
import { useTheme } from '@/contexts'; import { useTheme } from '@/contexts';
export interface IMEditorProps { export interface IMEditorProps {
@ -92,22 +93,7 @@ type Palette = typeof LIGHT;
// ─── Emoji Picker ───────────────────────────────────────────────────────────── // ─── Emoji Picker ─────────────────────────────────────────────────────────────
const EMOJIS = [ function EmojiPicker({ onClose, onSelect, p }: { onClose: () => void; onSelect: (emoji: string) => void; p: Palette }) {
{ 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 }) {
return ( return (
<div <div
className="absolute bottom-full left-0 mb-2 z-50" className="absolute bottom-full left-0 mb-2 z-50"
@ -132,14 +118,14 @@ function EmojiPicker({ onClose, onSelect, p }: { onClose: () => void; onSelect:
</button> </button>
</div> </div>
<div className="grid p-2 gap-0.5" style={{ gridTemplateColumns: 'repeat(6, 1fr)' }}> <div className="grid p-2 gap-0.5" style={{ gridTemplateColumns: 'repeat(6, 1fr)' }}>
{EMOJIS.map(e => ( {COMMON_EMOJIS.map(emoji => (
<button <button
key={e.name} key={emoji}
onClick={() => onSelect(e.name, e.url)} onClick={() => onSelect(emoji)}
className="w-9 h-9 flex items-center justify-center rounded-lg transition-all duration-100 cursor-pointer hover:scale-110" 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' }} style={{ background: 'transparent' }}
> >
<img src={e.url} alt={e.name} className="w-5 h-5 pointer-events-none" /> {emoji}
</button> </button>
))} ))}
</div> </div>
@ -448,7 +434,7 @@ export const IMEditor = forwardRef<IMEditorHandle, IMEditorProps>(function IMEdi
> >
<Smile size={18} /> <Smile size={18} />
</button> </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> </div>
<label <label