@@ -249,14 +249,34 @@ export function ConversationView() {
249249 conversationScrollBox . add ( emptyText )
250250 } else {
251251 let lastDateLabel = ""
252+ let lastSenderId = ""
253+
252254 for ( const message of reversedMessages ) {
255+ // Date Separator
253256 const dateLabel = formatDateSeparator ( message . timestamp )
254257 if ( dateLabel !== lastDateLabel ) {
255258 conversationScrollBox . add ( DaySeparator ( renderer , dateLabel ) )
256259 lastDateLabel = dateLabel
260+ // Reset sender grouping on new day
261+ lastSenderId = ""
257262 }
258- conversationScrollBox . add ( renderMessage ( renderer , message , isGroup ) )
263+
264+ // Determine if this is the start of a new sequence of messages from the same user
265+ const { senderId } = getSenderInfo ( message , isGroup )
266+ const isSequenceStart = senderId !== lastSenderId
267+
268+ conversationScrollBox . add ( renderMessage ( renderer , message , isGroup , isSequenceStart ) )
269+
270+ // Update last sender
271+ lastSenderId = senderId
259272 }
273+
274+ // Add a spacer at the bottom to prevent messages from being "crushed" by the input bar
275+ const spacer = new BoxRenderable ( renderer , {
276+ height : 1 ,
277+ backgroundColor : WhatsAppTheme . deepDark ,
278+ } )
279+ conversationScrollBox . add ( spacer )
260280 }
261281
262282 // Message input field (bottom)
@@ -760,49 +780,71 @@ function renderReplyContext(
760780 return contextBox
761781}
762782
763- function renderMessage (
764- renderer : CliRenderer ,
783+ // Helper to get sender info (ID, Name, Color)
784+ function getSenderInfo (
765785 message : WAMessageExtended ,
766- isGroupChat : boolean = false
767- ) : BoxRenderable {
786+ isGroupChat : boolean
787+ ) : { senderId : string ; senderName : string ; senderColor : string } {
768788 const isFromMe = message . fromMe
769- const timestamp = new Date ( message . timestamp * 1000 ) . toLocaleTimeString ( [ ] , {
770- hour : "2-digit" ,
771- minute : "2-digit" ,
772- } )
773-
774- // Extract sender name for group chats (only for received messages)
775789 let senderName = ""
776790 let senderId = ""
777- if ( isGroupChat && ! isFromMe && message . from ) {
778- const fromParts = message . from . split ( "@" )
779- senderName = fromParts [ 0 ] // Use phone number as fallback
780- senderId = message . from
781791
782- // If message has participant field (for group messages), use that
783- if ( message . participant ) {
792+ if ( isFromMe ) {
793+ senderName = "You"
794+ senderId = appState . getState ( ) . myProfile ?. id || "me"
795+ } else {
796+ senderId = message . from || ""
797+ if ( isGroupChat && message . participant ) {
784798 senderId = message . participant
799+ }
800+
801+ // Determine name
802+ if ( isGroupChat && message . from ) {
803+ const fromParts = message . from . split ( "@" )
804+ senderName = fromParts [ 0 ] // Fallback
785805
786- // Priority 1: Check contacts cache (user-saved names)
806+ // Priority 1: Check contacts cache
787807 const cachedName = appState . getContactName ( senderId )
788808 if ( cachedName ) {
789809 senderName = cachedName
790810 } else {
791- // Priority 2: Try to get display name from _data
811+ // Priority 2: _data info
792812 const msgData = message . _data
793813 if ( msgData ?. notifyName ) {
794814 senderName = msgData . notifyName
795815 } else if ( msgData ?. pushName ) {
796816 senderName = msgData . pushName
797817 } else {
798- // Priority 3: Fallback to participant ID
818+ // Priority 3: Participant ID parts
799819 const participantParts = senderId . split ( "@" )
800820 senderName = participantParts [ 0 ]
801821 }
802822 }
823+ } else {
824+ // 1:1 Chat sender name
825+ senderName = appState . getContactName ( senderId ) || senderId . split ( "@" ) [ 0 ]
803826 }
804827 }
805828
829+ const senderColor = isFromMe ? WhatsAppTheme . green : getSenderColor ( senderId )
830+
831+ return { senderId, senderName, senderColor }
832+ }
833+
834+ function renderMessage (
835+ renderer : CliRenderer ,
836+ message : WAMessageExtended ,
837+ isGroupChat : boolean = false ,
838+ isSequenceStart : boolean = true
839+ ) : BoxRenderable {
840+ const isFromMe = message . fromMe
841+ const timestamp = new Date ( message . timestamp * 1000 ) . toLocaleTimeString ( [ ] , {
842+ hour : "2-digit" ,
843+ minute : "2-digit" ,
844+ } )
845+
846+ const { senderName, senderColor } = getSenderInfo ( message , isGroupChat )
847+
806848 // Build message bubble content with WhatsApp-like layout
807849 const messageText = message . body || "(media)"
808850 const timestampText = t `${ timestamp } ${ isFromMe ? formatAckStatus ( message . ack , { } ) : "" } `
@@ -812,9 +854,50 @@ function renderMessage(
812854 id : `msg-${ message . id || Date . now ( ) } -row` ,
813855 flexDirection : "row" ,
814856 justifyContent : isFromMe ? "flex-end" : "flex-start" ,
815- marginBottom : 1 ,
857+ marginBottom : 0 , // Tight spacing for grouped messages
858+ marginTop : isSequenceStart ? 1 : 0 , // Add spacing only between groups
816859 } )
817860
861+ // Avatar Column (Only for received messages in Group Chats)
862+ if ( isGroupChat && ! isFromMe ) {
863+ const avatarColumn = new BoxRenderable ( renderer , {
864+ width : 6 , // Match avatarBox width
865+ height : 3 , // Approximate height of avatar
866+ marginRight : 1 , // Gap between avatar column and message bubble
867+ flexDirection : "column" ,
868+ justifyContent : "center" ,
869+ alignItems : "center" ,
870+ } )
871+
872+ if ( isSequenceStart ) {
873+ // Show Avatar
874+ const avatarBox = new BoxRenderable ( renderer , {
875+ width : 6 , // Wider avatar box
876+ height : 3 , // Match column height for vertical centering
877+ backgroundColor : senderColor ,
878+ justifyContent : "center" ,
879+ alignItems : "center" ,
880+ } )
881+ const initials = getInitials ( senderName )
882+ // Manually center text for TUI (width 6)
883+ const centeredInitials = centerText ( initials , 6 )
884+
885+ avatarBox . add (
886+ new TextRenderable ( renderer , {
887+ content : centeredInitials ,
888+ fg : WhatsAppTheme . white ,
889+ attributes : TextAttributes . BOLD ,
890+ } )
891+ )
892+ avatarColumn . add ( avatarBox )
893+ } else {
894+ // Empty placeholder for padding
895+ // Just an empty box or nothing, but width ensures alignment
896+ }
897+
898+ row . add ( avatarColumn )
899+ }
900+
818901 // Create bubble container
819902 // Capture message reference for context menu click handler
820903 const msgRef = message
@@ -862,8 +945,8 @@ function renderMessage(
862945 } ,
863946 } )
864947
865- // Row 1: Sender name (only for group chat received messages)
866- if ( senderName ) {
948+ // Row 1: Sender name (only for group chat received messages AND first in sequence )
949+ if ( isGroupChat && ! isFromMe && isSequenceStart ) {
867950 const senderRow = new BoxRenderable ( renderer , {
868951 id : `msg-${ message . id || Date . now ( ) } -sender` ,
869952 height : 1 ,
@@ -872,7 +955,7 @@ function renderMessage(
872955 } )
873956 const senderText = new TextRenderable ( renderer , {
874957 content : senderName ,
875- fg : getSenderColor ( senderId ) ,
958+ fg : senderColor ,
876959 attributes : TextAttributes . BOLD ,
877960 } )
878961 senderRow . add ( senderText )
@@ -985,3 +1068,10 @@ function DaySeparator(renderer: CliRenderer, label: string): BoxRenderable {
9851068 container . add ( badge )
9861069 return container
9871070}
1071+
1072+ // Helper to center text in a fixed width
1073+ function centerText ( text : string , width : number ) : string {
1074+ if ( text . length >= width ) return text . slice ( 0 , width )
1075+ const paddingLeft = Math . floor ( ( width - text . length ) / 2 )
1076+ return text . padStart ( text . length + paddingLeft ) . padEnd ( width )
1077+ }
0 commit comments