diff --git a/src/components/channel/MessageItem.tsx b/src/components/channel/MessageItem.tsx index cfe46a7..fec840c 100644 --- a/src/components/channel/MessageItem.tsx +++ b/src/components/channel/MessageItem.tsx @@ -21,6 +21,7 @@ interface Props { onSetEmojiPicker: (msgId: string | null) => void; onShowEditHistory: (msgId: string) => void; roomId: string; + readOnly?: boolean; } const COMMON_EMOJIS = ['👍', '❤️', '😄', '😭', '😡', '🎉', '🚀', '👀']; @@ -37,14 +38,16 @@ export function MessageItem({ onOpenThread, onSetEmojiPicker, onShowEditHistory, + readOnly = false, }: Props) { const [isHovered, setIsHovered] = useState(false); const isRevoked = !!msg.revoked; const { addPin, removePin, pinnedMessages } = useRoom(); const isPinned = pinnedMessages.some((p) => p.message === msg.id); const handleTogglePin = () => { - if (isPinned) removePin(msg.id); - else addPin(msg.id); + if (readOnly) return; + const action = isPinned ? removePin(msg.id) : addPin(msg.id); + action.catch((err) => console.error('[MessageItem] failed to toggle pin:', err)); }; const isEdited = !!msg.edited_at; const isStreaming = msg.is_streaming === true; @@ -53,6 +56,7 @@ export function MessageItem({ return (
( ))}
{/* Hover actions — Discord-style: button center aligns with message top edge */} - {isHovered && !isRevoked && ( + {isHovered && !isRevoked && !readOnly && (
+ {msg.sender_type !== 'ai' && ( <> - )} +
)} diff --git a/src/components/channel/MessageList.tsx b/src/components/channel/MessageList.tsx index 59c73bf..d4c91a3 100644 --- a/src/components/channel/MessageList.tsx +++ b/src/components/channel/MessageList.tsx @@ -41,6 +41,7 @@ interface Props { onShowEditHistory: (msgId: string) => void; onStartReached?: () => void; roomId: string; + readOnly?: boolean; } export function MessageList({ @@ -57,6 +58,7 @@ export function MessageList({ onShowEditHistory, onStartReached, roomId, + readOnly = false, }: Props) { const virtuosoRef = useRef(null); const initialScrollDoneRef = useRef(false); @@ -208,6 +210,7 @@ export function MessageList({ onSetEmojiPicker={onSetEmojiPicker} onShowEditHistory={onShowEditHistory} roomId={roomId} + readOnly={readOnly} /> ); }} diff --git a/src/components/channel/PinPanel.tsx b/src/components/channel/PinPanel.tsx index 28e4763..27f4edd 100644 --- a/src/components/channel/PinPanel.tsx +++ b/src/components/channel/PinPanel.tsx @@ -1,14 +1,18 @@ -import { Pin, X } from 'lucide-react'; -import type { PinnedMessage } from '@/contexts/room'; +import { Pin, PinOff, X } from 'lucide-react'; +import type { Message, PinnedMessage } from '@/contexts/room'; import { formatRelativeTime } from '@/contexts/room'; interface Props { pins: PinnedMessage[]; + messages: Message[]; onClose: () => void; onGotoMessage: (messageId: string) => void; + onUnpin?: (messageId: string) => void; } -export function PinPanel({ pins, onClose, onGotoMessage }: Props) { +export function PinPanel({ pins, messages, onClose, onGotoMessage, onUnpin }: Props) { + const messageById = new Map(messages.map((msg) => [msg.id, msg])); + return (
@@ -28,23 +32,60 @@ export function PinPanel({ pins, onClose, onGotoMessage }: Props) { No pinned messages yet.
) : ( - pins.map((pin) => ( - - )) + pins.map((pin) => { + const msg = messageById.get(pin.message); + return ( +
+ + {onUnpin && ( + + )} +
+ ); + }) )}
); -} \ No newline at end of file +} diff --git a/src/components/channel/ThreadPanel.tsx b/src/components/channel/ThreadPanel.tsx index 50510c4..241187d 100644 --- a/src/components/channel/ThreadPanel.tsx +++ b/src/components/channel/ThreadPanel.tsx @@ -5,6 +5,9 @@ import type { Message } from '@/contexts/room'; import { formatTime } from '@/contexts/room'; import { Avatar } from './Avatar'; import { getWsClient } from '@/ws'; +import { ProjectJoinBanner } from '@/app/project/layout'; +import { IrRenderer } from '@/lib/ir/renderer'; +import { extractIrNodes } from '@/lib/ir/parser'; function safeGetClient() { try { return getWsClient(); } catch { return null; } @@ -21,6 +24,7 @@ interface Props { sendMessage: (content: string, opts?: { thread?: string }) => void; onTypingStart: () => void; onTypingStop: () => void; + readOnly?: boolean; } const COMMON_EMOJIS = ['👍', '❤️', '😄', '😭', '😡', '🎉']; @@ -32,6 +36,7 @@ export function ThreadPanel({ sendMessage, onTypingStart, onTypingStop, + readOnly = false, }: Props) { const [inputValue, setInputValue] = useState(''); const [showReactionPicker, setShowReactionPicker] = useState(null); @@ -43,7 +48,7 @@ export function ThreadPanel({ }, [thread.messages]); const handleSend = () => { - if (!inputValue.trim()) return; + if (readOnly || !inputValue.trim()) return; sendMessage(inputValue.trim(), { thread: thread.id }); setInputValue(''); onTypingStop(); @@ -51,6 +56,7 @@ export function ThreadPanel({ }; const handleInputChange = (e: React.ChangeEvent) => { + if (readOnly) return; setInputValue(e.target.value); if (typingTimeoutRef.current) clearTimeout(typingTimeoutRef.current); onTypingStart(); @@ -58,11 +64,13 @@ export function ThreadPanel({ }; const handleResolve = () => { + if (readOnly) return; const c = safeGetClient(); if (c) c.resolveThread(thread.id); }; const handleArchive = () => { + if (readOnly) return; const c = safeGetClient(); if (c) c.archiveThread(thread.id); }; @@ -118,7 +126,7 @@ export function ThreadPanel({ {isRevoked ? ( Message deleted ) : ( - msg.content + )} @@ -136,6 +144,11 @@ export function ThreadPanel({ )} + {readOnly ? ( +
+ +
+ ) : (
+ )} ); -} \ No newline at end of file +}