refactor(room): WebSocket queue and message editor improvements
- 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:
parent
c4fb943e07
commit
a09f66b779
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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],
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user