diff --git a/src/components/room/DiscordChannelSidebar.tsx b/src/components/room/DiscordChannelSidebar.tsx index f1ef110..8337e37 100644 --- a/src/components/room/DiscordChannelSidebar.tsx +++ b/src/components/room/DiscordChannelSidebar.tsx @@ -13,6 +13,7 @@ import { PointerSensor, useSensor, useSensors, + useDroppable, type DragEndEvent, type UniqueIdentifier, } from '@dnd-kit/core'; @@ -141,6 +142,9 @@ const ChannelGroup = memo(function ChannelGroup({ }) { const ids: UniqueIdentifier[] = rooms.map((r) => `${DRAG_PREFIX}${r.id}`); + // Make the category header a droppable zone so rooms can be dragged onto it + const { setNodeRef: setHeaderRef, isOver: isOverHeader } = useDroppable({ id: categoryName }); + return (
undefined /* handled by DnD */ : undefined} > + ); + })} + + {/* Add more reaction — opens full picker */} (fu { id: 'code-review', label: '/code-review', description: 'Request AI code review', type: 'command' as const }, ]; + // Special mention items — @here (online), @channel (all members) + const SPECIAL_MENTIONS = [ + { + id: '__here__', + label: 'here', + description: 'Notify online members', + type: 'special_here' as const, + }, + { + id: '__channel__', + label: 'channel', + description: 'Notify all members', + type: 'special_channel' as const, + }, + ]; + // Transform room data into MentionItems — memoized to prevent IMEditor re-creation const mentionItems = useMemo(() => ({ users: members.map((m) => ({ @@ -64,6 +80,7 @@ export const MessageInput = forwardRef(fu channels: [] as { id: string; label: string; type: 'channel'; avatar?: string }[], ai: [] as { id: string; label: string; type: 'ai'; avatar?: string }[], commands: SLASH_COMMANDS, + specialMentions: SPECIAL_MENTIONS, }), [members]); // File upload handler — POST to /rooms/{room_id}/upload diff --git a/src/components/room/message/MessageList.tsx b/src/components/room/message/MessageList.tsx index 157e17e..25b6add 100644 --- a/src/components/room/message/MessageList.tsx +++ b/src/components/room/message/MessageList.tsx @@ -108,10 +108,13 @@ export const MessageList = memo(function MessageList({ const result: MessageRow[] = []; let lastDateKey: string | null = null; let lastSenderKey: string | null = null; + let lastMessageTime: number | null = null; + const GROUP_GAP_MS = 5 * 60 * 1000; // 5 minutes for (const message of messages) { const dateKey = getDateKey(message.send_at); const senderKey = getSenderKey(message); + const msgTime = new Date(message.send_at).getTime(); if (dateKey !== lastDateKey) { result.push({ @@ -121,9 +124,14 @@ export const MessageList = memo(function MessageList({ }); lastDateKey = dateKey; lastSenderKey = null; + lastMessageTime = null; } - const grouped = senderKey === lastSenderKey; + // Group if: same sender AND within 5-minute gap (Discord-style) + const sameSender = senderKey === lastSenderKey; + const withinTimeGap = lastMessageTime !== null && (msgTime - lastMessageTime) < GROUP_GAP_MS; + const grouped = sameSender && withinTimeGap; + result.push({ type: 'message', message, @@ -132,6 +140,7 @@ export const MessageList = memo(function MessageList({ key: message.id, }); lastSenderKey = senderKey; + lastMessageTime = msgTime; } return result; }, [messages, replyMap]); diff --git a/src/components/room/message/editor/IMEditor.tsx b/src/components/room/message/editor/IMEditor.tsx index 916dc2d..f742e0b 100644 --- a/src/components/room/message/editor/IMEditor.tsx +++ b/src/components/room/message/editor/IMEditor.tsx @@ -26,6 +26,7 @@ export interface IMEditorProps { channels: MentionItem[]; ai: MentionItem[]; commands: MentionItem[]; + specialMentions?: MentionItem[]; }; onUploadFile?: (file: File) => Promise<{ id: string; url: string }>; placeholder?: string; @@ -185,6 +186,10 @@ function MentionDropdown({ p: Palette; query: string; }) { + const SPECIAL_TYPES = ['special_here', 'special_channel']; + const specialItems = items.filter((item) => SPECIAL_TYPES.includes(item.type)); + const regularItems = items.filter((item) => !SPECIAL_TYPES.includes(item.type)); + return (
) : (
- {items.map((item, i) => { + {/* Special mentions section */} + {specialItems.length > 0 && regularItems.length > 0 && ( +
+ Notify +
+ )} + {specialItems.map((item) => { + const realIndex = items.indexOf(item); + const icon = item.type === 'special_here' ? '📍' : '📢'; + return ( + + ); + })} + {specialItems.length > 0 && regularItems.length > 0 && ( +
+ )} + {/* Regular mentions section */} + {regularItems.map((item) => { + const realIndex = items.indexOf(item); const badge = getBadge(item.type); return (