11import { Hash , MessageSquare , Send , Bookmark , Share2 , MoreVertical , X , Paperclip , Smile , UserPlus , FileUp , Image as ImageIcon , Link2 } from "lucide-react" ;
22import { useEffect , useRef , useState , type ChangeEvent , type KeyboardEvent } from "react" ;
33import { createFileMessageAttachment , createLinkMessageAttachment , createLinkMessageAttachmentFromText , messageAttachmentGroups , messageAttachmentTypeLabels , type MessageAttachment , type MessageAttachmentType } from "./messageAttachments" ;
4- import { TypingIndicator } from "./TypingIndicator" ;
54import { EmojiPicker } from "./EmojiPicker" ;
65import { MessageReactions , toggleMessageReaction , type MessageReaction } from "./MessageReactions" ;
76import { MessageAttachmentCard } from "./MessageAttachmentCard" ;
7+ import { TypingIndicatorBar } from "./TypingIndicatorBar" ;
88
99interface Thread {
1010 id : number ;
@@ -91,6 +91,19 @@ function saveThreads(storageKey: string, threads: Thread[]) {
9191 }
9292}
9393
94+ const currentUserDisplayName = "김재준" ;
95+ const currentUserAvatar = currentUserDisplayName . charAt ( 0 ) ;
96+ const selfUserNames = new Set ( [ "나" , "me" , "you" , "jean" , "jeaju" , currentUserDisplayName ] ) ;
97+
98+ function isSelfUser ( user ?: string ) {
99+ return selfUserNames . has ( ( user ?? "" ) . trim ( ) . toLowerCase ( ) ) ;
100+ }
101+
102+ function getDisplayUserName ( user ?: string ) {
103+ const trimmed = ( user ?? "" ) . trim ( ) ;
104+ return isSelfUser ( trimmed ) ? currentUserDisplayName : trimmed ;
105+ }
106+
94107export function ChannelPanel ( { channelId, repoId, repoName, reactions, replyCounts = { } , onOpenThread, onOpenInvite, onToggleReaction } : ChannelPanelProps ) {
95108 const channelStorageId = channelId ?? repoId ?? "general" ;
96109 const channelStorageKey = `${ CHANNEL_THREADS_KEY_PREFIX } :${ channelStorageId } ` ;
@@ -145,11 +158,15 @@ export function ChannelPanel({ channelId, repoId, repoName, reactions, replyCoun
145158 const scrollContainer = scrollContainerRef . current ;
146159 if ( ! scrollContainer ) return ;
147160
148- scrollContainer . scrollTo ( {
149- top : scrollContainer . scrollHeight ,
150- behavior : "smooth"
161+ const frameId = window . requestAnimationFrame ( ( ) => {
162+ scrollContainer . scrollTo ( {
163+ top : scrollContainer . scrollHeight ,
164+ behavior : "smooth"
165+ } ) ;
151166 } ) ;
152- } , [ threads . length , responderTyping ] ) ;
167+
168+ return ( ) => window . cancelAnimationFrame ( frameId ) ;
169+ } , [ threads . length , responderTyping , messageText ] ) ;
153170
154171 const triggerResponderTyping = ( ) => {
155172 if ( responderTypingTimerRef . current ) {
@@ -170,15 +187,12 @@ export function ChannelPanel({ channelId, repoId, repoName, reactions, replyCoun
170187 const canSendMessage = messageText . trim ( ) . length > 0 || selectedAttachments . length > 0 ;
171188 const composerTyping = messageText . trim ( ) . length > 0 ;
172189 const typingLabel = responderTyping
173- ? "CodeDock AI가 답변을 정리 중입니다"
190+ ? composerTyping
191+ ? `CodeDock AI, ${ currentUserDisplayName } 입력 중입니다`
192+ : "CodeDock AI가 답변을 정리 중입니다"
174193 : composerTyping
175194 ? "내가 입력 중입니다"
176195 : "" ;
177- const typingNote = responderTyping
178- ? "채널 맥락을 확인하고 다음 메시지를 준비합니다."
179- : composerTyping
180- ? "팀원에게 입력 중 상태로 표시됩니다."
181- : "" ;
182196
183197 const handleAttachmentToggle = ( attachment : MessageAttachment ) => {
184198 setSelectedAttachments ( ( prev ) =>
@@ -253,8 +267,8 @@ export function ChannelPanel({ channelId, repoId, repoName, reactions, replyCoun
253267
254268 const nextThread : Thread = {
255269 id : Date . now ( ) ,
256- user : '나' ,
257- avatar : '나' ,
270+ user : currentUserDisplayName ,
271+ avatar : currentUserAvatar ,
258272 message : trimmedMessage || `${ outgoingAttachments . length } 개 항목을 공유합니다.` ,
259273 time : '방금' ,
260274 replies : 0 ,
@@ -319,30 +333,51 @@ export function ChannelPanel({ channelId, repoId, repoName, reactions, replyCoun
319333 < div className = "grid gap-4" >
320334 { threads . map ( ( thread ) => {
321335 const displayedReplyCount = replyCounts [ thread . id ] ?? thread . replies ;
336+ const isOwnThread = isSelfUser ( thread . user ) ;
322337
323338 return (
324339 < div
325340 key = { thread . id }
326341 className = "rounded-xl overflow-hidden relative group"
327342 style = { {
328- background : 'rgba(5, 11, 20, 0.6)' ,
329- border : '1px solid rgba(32, 227, 255, 0.14)'
343+ width : '100%' ,
344+ background : isOwnThread ? 'rgba(32, 227, 255, 0.075)' : 'rgba(5, 11, 20, 0.54)' ,
345+ border : isOwnThread ? '1px solid rgba(32, 227, 255, 0.18)' : '1px solid rgba(32, 227, 255, 0.14)' ,
346+ borderRadius : '12px' ,
347+ boxShadow : 'none'
330348 } }
331349 onMouseEnter = { ( ) => setHoveredMessageId ( thread . id ) }
332350 onMouseLeave = { ( ) => setHoveredMessageId ( null ) }
333351 >
334352 < div className = "w-full px-5 py-4" >
335353 < div className = "flex items-start gap-3" >
336- < span style = { { fontSize : '28px' , lineHeight : 1 } } > { thread . avatar } </ span >
354+ < span className = "grid h-10 w-10 flex-shrink-0 place-items-center rounded-full" style = { {
355+ background : isOwnThread ? 'rgba(32, 227, 255, 0.16)' : 'rgba(32, 227, 255, 0.12)' ,
356+ border : isOwnThread ? '1px solid rgba(32, 227, 255, 0.30)' : '1px solid rgba(32, 227, 255, 0.22)' ,
357+ color : 'var(--neon-cyan)' ,
358+ fontSize : thread . avatar . length > 2 ? '18px' : '13px' ,
359+ fontWeight : 950 ,
360+ lineHeight : 1
361+ } } > { isOwnThread ? currentUserAvatar : thread . avatar } </ span >
337362 < div className = "flex-1 min-w-0" >
338363 < div className = "flex items-center gap-2 mb-1" >
339364 < span className = "tracking-tight" style = { {
340365 fontSize : '13px' ,
341366 fontWeight : 900 ,
342- color : 'var(--matrix-green)'
367+ color : isOwnThread ? 'var(--neon-cyan)' : 'var(--matrix-green)'
343368 } } >
344- { thread . user }
369+ { isOwnThread ? getDisplayUserName ( thread . user ) : thread . user }
345370 </ span >
371+ { isOwnThread && (
372+ < span className = "rounded px-1.5 py-0.5 tracking-tight" style = { {
373+ background : 'rgba(32, 227, 255, 0.12)' ,
374+ color : 'var(--neon-cyan)' ,
375+ fontSize : '10px' ,
376+ fontWeight : 950
377+ } } >
378+ 내 메시지
379+ </ span >
380+ ) }
346381 < span className = "tracking-tight" style = { {
347382 fontSize : '11px' ,
348383 fontWeight : 700 ,
@@ -484,13 +519,6 @@ export function ChannelPanel({ channelId, repoId, repoName, reactions, replyCoun
484519 </ div >
485520 ) ;
486521 } ) }
487- { typingLabel && (
488- < TypingIndicator
489- label = { typingLabel }
490- note = { typingNote }
491- avatar = { responderTyping ? "AI" : "나" }
492- />
493- ) }
494522 </ div >
495523 </ div >
496524
@@ -693,20 +721,22 @@ export function ChannelPanel({ channelId, repoId, repoName, reactions, replyCoun
693721 onChange = { ( event ) => handleLocalFilesSelected ( event , "image" ) }
694722 />
695723
696- < div className = "flex items-center gap-2 px-4 py-3 rounded-xl" style = { {
724+ < TypingIndicatorBar label = { typingLabel } />
725+
726+ < div className = "relative flex items-center gap-2 px-4 py-3 rounded-xl" style = { {
697727 background : 'rgba(5, 11, 20, 0.6)' ,
698728 border : '1px solid rgba(32, 227, 255, 0.14)'
699729 } } >
700- < input
701- type = "text"
702- value = { messageText }
703- onChange = { ( event ) => setMessageText ( event . target . value ) }
704- onKeyDown = { handleMessageKeyDown }
705- placeholder = { `#${ channelLabel } 에 메시지 보내기` }
706- className = "min-w-0 flex-1 bg-transparent border-0 outline-none tracking-tight"
707- style = { { color : 'var(--white)' , fontSize : '14px' , fontWeight : 700 } }
708- />
709- < div className = "flex shrink-0 items-center gap-1" >
730+ < input
731+ type = "text"
732+ value = { messageText }
733+ onChange = { ( event ) => setMessageText ( event . target . value ) }
734+ onKeyDown = { handleMessageKeyDown }
735+ placeholder = { `#${ channelLabel } 에 메시지 보내기` }
736+ className = "min-w-0 flex-1 bg-transparent border-0 outline-none tracking-tight"
737+ style = { { color : 'var(--white)' , fontSize : '14px' , fontWeight : 700 } }
738+ />
739+ < div className = "flex shrink-0 items-center gap-1" >
710740 < button
711741 onClick = { ( ) => togglePanel ( 'attachment' ) }
712742 className = "w-9 h-9 rounded-lg border-0 flex items-center justify-center transition-all cursor-pointer"
0 commit comments