89 lines
2.5 KiB
Rust
89 lines
2.5 KiB
Rust
//! Chat panel widget — scrollable chat history.
|
|
|
|
use ratatui::layout::Rect;
|
|
use ratatui::style::{Modifier, Style};
|
|
use ratatui::text::{Line, Span};
|
|
use ratatui::widgets::{Block, Borders, List, ListItem};
|
|
use ratatui::Frame;
|
|
|
|
use crate::chat::{ChatEntry, ChatState};
|
|
|
|
use crate::tui::App;
|
|
|
|
pub fn render(frame: &mut Frame, area: Rect, chat: &ChatState, app: &App) {
|
|
let block = Block::default()
|
|
.title(" 💬 Chat ")
|
|
.borders(Borders::ALL)
|
|
.border_style(Style::default().fg(app.theme.border));
|
|
|
|
let inner = block.inner(area);
|
|
frame.render_widget(block, area);
|
|
|
|
if chat.history.is_empty() {
|
|
let empty = ratatui::widgets::Paragraph::new("No messages yet. Start typing!")
|
|
.style(Style::default().fg(app.theme.time));
|
|
frame.render_widget(empty, inner);
|
|
return;
|
|
}
|
|
|
|
let visible_height = inner.height as usize;
|
|
let total = chat.history.len();
|
|
|
|
// Calculate scroll position
|
|
let end = if app.scroll_offset >= total {
|
|
total
|
|
} else {
|
|
total - app.scroll_offset
|
|
};
|
|
let start = end.saturating_sub(visible_height);
|
|
|
|
let items: Vec<ListItem> = chat.history[start..end]
|
|
.iter()
|
|
.map(|entry| format_entry(entry, app))
|
|
.collect();
|
|
|
|
let list = List::new(items);
|
|
frame.render_widget(list, inner);
|
|
}
|
|
|
|
fn format_entry(entry: &ChatEntry, app: &App) -> ListItem<'static> {
|
|
let time = chrono::DateTime::from_timestamp_millis(entry.timestamp as i64)
|
|
.map(|dt| dt.format("%H:%M").to_string())
|
|
.unwrap_or_default();
|
|
|
|
if entry.is_system {
|
|
let line = Line::from(vec![
|
|
Span::styled(
|
|
format!("[{}] ", time),
|
|
Style::default().fg(app.theme.time),
|
|
),
|
|
Span::styled(
|
|
format!("*** {} ***", entry.text),
|
|
Style::default()
|
|
.fg(app.theme.system_msg)
|
|
.add_modifier(Modifier::ITALIC),
|
|
),
|
|
]);
|
|
return ListItem::new(line);
|
|
}
|
|
|
|
let name_color = if entry.is_self {
|
|
app.theme.self_name
|
|
} else {
|
|
app.theme.peer_name
|
|
};
|
|
|
|
let line = Line::from(vec![
|
|
Span::styled(format!("[{}] ", time), Style::default().fg(app.theme.time)),
|
|
Span::styled(
|
|
format!("{}: ", entry.sender_name),
|
|
Style::default()
|
|
.fg(name_color)
|
|
.add_modifier(Modifier::BOLD),
|
|
),
|
|
Span::styled(entry.text.clone(), Style::default().fg(app.theme.text)),
|
|
]);
|
|
|
|
ListItem::new(line)
|
|
}
|