import * as style from '../style.js'
const SYSTEM_EVENTS = {
1: 'joined', 2: 'call started', 3: 'avatar changed',
4: 'invite created', 5: 'left', 6: 'title changed',
7: 'removed', 8: 'call ended', 9: 'call missed'
}
export function chatView (model, width, height) {
const lines = []
const room = model.rooms[model.selectedRoomIndex]
// Header
const title = room ? (room.title || room.roomId.slice(0, 16)) : 'No room selected'
lines.push(style.bold(style.truncate(' ' + title, width)))
lines.push(style.hline(width, 'rounded'))
if (!room) {
lines.push(style.dim(style.center('Select a room to start chatting', width)))
while (lines.length < height) lines.push('')
return lines
}
// Messages area (height - header(2) - separator(1) - input(2))
const msgHeight = height - 5
const messages = model.messages || []
const wrapped = []
for (const msg of messages) {
const formatted = formatMessage(msg, width - 1)
for (const line of formatted) {
wrapped.push(line)
}
}
// Scroll from bottom: show most recent messages
const scrollPos = model.chatScroll || 0
const start = Math.max(0, wrapped.length - msgHeight - scrollPos)
const end = Math.max(0, wrapped.length - scrollPos)
const visible = wrapped.slice(start, end)
// Pad top if fewer messages than space, then fill with visible messages
if (visible.length < msgHeight) {
const pad = msgHeight - visible.length
for (let i = 0; i < pad; i++) lines.push('')
}
for (const l of visible.slice(0, msgHeight)) lines.push(l)
// Input separator
lines.push(style.hline(width, 'rounded'))
// Input line
const prompt = style.cyan('> ')
const inputText = model.inputBuffer || ''
const cursorPos = model.cursorPos != null ? model.cursorPos : inputText.length
const maxInput = width - 3
let displayInput = inputText
if (displayInput.length > maxInput) {
const start = Math.max(0, cursorPos - maxInput + 1)
displayInput = displayInput.slice(start, start + maxInput)
}
lines.push(prompt + style.padRight(displayInput, maxInput))
while (lines.length < height) lines.push('')
return lines
}
function formatSystemEvent (system) {
if (typeof system === 'number') {
return SYSTEM_EVENTS[system] || 'event ' + system
}
if (typeof system === 'object' && system !== null) {
for (const [key, val] of Object.entries(system)) {
if (val === null || val === false) continue
const label = key.replace(/([A-Z])/g, ' $1').toLowerCase().trim()
if (typeof val === 'object' && val.from && val.to) {
return label + ': ' + val.from + ' \u2192 ' + val.to
}
if (typeof val === 'object' && val.memberId) {
return label
}
return label
}
}
return 'system event'
}
function formatMessage (msg, width) {
if (msg.system) {
const name = formatSystemEvent(msg.system)
return [style.dim(style.center('--- ' + name + ' ---', width))]
}
const time = msg.timestamp
? new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
: '??:??'
const sender = msg.name || (msg.memberId ? msg.memberId.slice(0, 8) : '???')
const deleted = msg.deleted ? style.dim(' [deleted]') : ''
const header = style.brightBlack(time + ' ') + style.bold(style.cyan(sender)) + deleted
const text = msg.text || ''
if (!text) return [header]
const lines = [header]
// Split on newlines first, then wrap each line
const paragraphs = text.split('\n')
for (const para of paragraphs) {
if (!para) {
lines.push('')
continue
}
const words = para.split(' ')
let current = ' '
for (const word of words) {
if (current.length + word.length + 1 > width && current.length > 1) {
lines.push(current)
current = ' ' + word
} else {
current += (current.length > 1 ? ' ' : '') + word
}
}
if (current.length > 1) lines.push(current)
}
return lines
}