Split monolithic files into modular architecture: - ui/messages.rs (893→365 lines): extract modals/, compose_bar.rs - tdlib/messages.rs (836→3 files): split into messages/mod, convert, operations - config/mod.rs (642→3 files): extract validation.rs, loader.rs - Code duplication cleanup: shared components, ~220 lines removed - Documentation: PROJECT_STRUCTURE.md rewrite, 16 files got //! docs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
117 lines
3.3 KiB
Rust
117 lines
3.3 KiB
Rust
//! Shared message list rendering for search and pinned modals
|
|
|
|
use crate::tdlib::MessageInfo;
|
|
use ratatui::{
|
|
layout::Alignment,
|
|
style::{Color, Modifier, Style},
|
|
text::{Line, Span},
|
|
widgets::{Block, Borders, Paragraph},
|
|
};
|
|
|
|
/// Renders a single message item with marker, sender, date, and wrapped text
|
|
pub fn render_message_item(
|
|
msg: &MessageInfo,
|
|
is_selected: bool,
|
|
content_width: usize,
|
|
max_preview_lines: usize,
|
|
) -> Vec<Line<'static>> {
|
|
let mut lines = Vec::new();
|
|
|
|
// Marker, sender name, and date
|
|
let marker = if is_selected { "▶ " } else { " " };
|
|
let sender_color = if msg.is_outgoing() {
|
|
Color::Green
|
|
} else {
|
|
Color::Cyan
|
|
};
|
|
let sender_name = if msg.is_outgoing() {
|
|
"Вы".to_string()
|
|
} else {
|
|
msg.sender_name().to_string()
|
|
};
|
|
|
|
lines.push(Line::from(vec![
|
|
Span::styled(
|
|
marker.to_string(),
|
|
Style::default()
|
|
.fg(Color::Yellow)
|
|
.add_modifier(Modifier::BOLD),
|
|
),
|
|
Span::styled(
|
|
format!("{} ", sender_name),
|
|
Style::default()
|
|
.fg(sender_color)
|
|
.add_modifier(Modifier::BOLD),
|
|
),
|
|
Span::styled(
|
|
format!("({})", crate::utils::format_datetime(msg.date())),
|
|
Style::default().fg(Color::Gray),
|
|
),
|
|
]));
|
|
|
|
// Wrapped message text
|
|
let msg_color = if is_selected {
|
|
Color::Yellow
|
|
} else {
|
|
Color::White
|
|
};
|
|
let max_width = content_width.saturating_sub(4);
|
|
let wrapped = crate::ui::messages::wrap_text_with_offsets(msg.text(), max_width);
|
|
let wrapped_count = wrapped.len();
|
|
|
|
for wrapped_line in wrapped.into_iter().take(max_preview_lines) {
|
|
lines.push(Line::from(vec![
|
|
Span::raw(" ".to_string()),
|
|
Span::styled(wrapped_line.text, Style::default().fg(msg_color)),
|
|
]));
|
|
}
|
|
if wrapped_count > max_preview_lines {
|
|
lines.push(Line::from(vec![
|
|
Span::raw(" ".to_string()),
|
|
Span::styled("...".to_string(), Style::default().fg(Color::Gray)),
|
|
]));
|
|
}
|
|
|
|
lines
|
|
}
|
|
|
|
/// Calculates scroll offset to keep selected item visible
|
|
pub fn calculate_scroll_offset(
|
|
selected_index: usize,
|
|
lines_per_item: usize,
|
|
visible_height: u16,
|
|
) -> u16 {
|
|
let visible = visible_height.saturating_sub(2) as usize;
|
|
let selected_line = selected_index * lines_per_item;
|
|
if selected_line > visible / 2 {
|
|
(selected_line - visible / 2) as u16
|
|
} else {
|
|
0
|
|
}
|
|
}
|
|
|
|
/// Renders a help bar with keyboard shortcuts
|
|
pub fn render_help_bar(shortcuts: &[(&str, &str, Color)], border_color: Color) -> Paragraph<'static> {
|
|
let mut spans: Vec<Span<'static>> = Vec::new();
|
|
for (i, (key, label, color)) in shortcuts.iter().enumerate() {
|
|
if i > 0 {
|
|
spans.push(Span::raw(" ".to_string()));
|
|
}
|
|
spans.push(Span::styled(
|
|
format!(" {} ", key),
|
|
Style::default()
|
|
.fg(*color)
|
|
.add_modifier(Modifier::BOLD),
|
|
));
|
|
spans.push(Span::raw(label.to_string()));
|
|
}
|
|
|
|
Paragraph::new(Line::from(spans))
|
|
.block(
|
|
Block::default()
|
|
.borders(Borders::ALL)
|
|
.border_style(Style::default().fg(border_color)),
|
|
)
|
|
.alignment(Alignment::Center)
|
|
}
|